diff --git a/.artifactignore b/.artifactignore new file mode 100644 index 0000000000..a287e295ae --- /dev/null +++ b/.artifactignore @@ -0,0 +1,3 @@ +**/* +!**/bin/** +!**/obj/** diff --git a/.editorconfig b/.editorconfig index eba04ad326..faf5c7766a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -282,7 +282,7 @@ dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR___ # All public/protected/protected_internal constant fields must be PascalCase # https://docs.microsoft.com/dotnet/standard/design-guidelines/field -dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal, internal, private dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group @@ -356,24 +356,13 @@ dotnet_naming_rule.parameters_rule.symbols = parameters_group dotnet_naming_rule.parameters_rule.style = camel_case_style dotnet_naming_rule.parameters_rule.severity = warning -# Private static fields use camelCase and start with s_ -dotnet_naming_symbols.private_static_field_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_static_field_symbols.required_modifiers = static, shared -dotnet_naming_symbols.private_static_field_symbols.applicable_kinds = field -dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.symbols = private_static_field_symbols -dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.style = camel_case_and_prefix_with_s_underscore_style -dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.severity = warning -dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.required_prefix = s_ -dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.capitalization = camel_case - # Instance fields use camelCase and are prefixed with '_' -dotnet_naming_symbols.private_field_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_field_symbols.applicable_kinds = field -dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.symbols = private_field_symbols -dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.style = camel_case_and_prefix_with_underscore_style -dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.severity = warning -dotnet_naming_style.camel_case_and_prefix_with_underscore_style.required_prefix = _ -dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization = camel_case +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = warning +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style +dotnet_naming_symbols.instance_fields.applicable_kinds = field +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ ########################################## # License @@ -408,4 +397,4 @@ dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -########################################## \ No newline at end of file +########################################## diff --git a/.github/BUILD.md b/.github/BUILD.md index 5f962a8911..12b64b287f 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -1,33 +1,99 @@ -# Umbraco CMS Build +# Umbraco CMS Build ## Are you sure? In order to use Umbraco as a CMS and build your website with it, you should not build it yourself. If you're reading this then you're trying to contribute to Umbraco or you're debugging a complex issue. -- Are you about to create a pull request for Umbraco? +- Are you about to [create a pull request for Umbraco][contribution guidelines]? - Are you trying to get to the bottom of a problem in your existing Umbraco installation? If the answer is yes, please read on. Otherwise, make sure to head on over [to the download page](https://our.umbraco.com/download) and start using Umbraco CMS as intended. -**Table of contents** +## Table of contents -[Building from source](#building-from-source) - * [The quick build](#quick) - * [Build infrastructure](#build-infrastructure) - * [Properties](#properties) - * [GetUmbracoVersion](#getumbracoversion) - * [SetUmbracoVersion](#setumbracoversion) - * [Build](#build) - * [Build-UmbracoDocs](#build-umbracodocs) - * [Verify-NuGet](#verify-nuget) - * [Cleaning up](#cleaning-up) +↖️ You can jump to any section by using the "table of contents" button ( ![Table of contents icon](img/tableofcontentsicon.svg) ) above. -[Azure DevOps](#azure-devops) -[Quirks](#quirks) - * [Powershell quirks](#powershell-quirks) - * [Git quirks](#git-quirks) +## Debugging source locally +Did you read ["Are you sure"](#are-you-sure)? + +[More details about contributing to Umbraco and how to use the GitHub tooling can be found in our guide to contributing.][contribution guidelines] + +If you want to run a build without debugging, see [Building from source](#building-from-source) below. This runs the build in the same way it is run on our build servers. + +#### Debugging with VS Code + +In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed. + + * [Visual Studio Code](https://code.visualstudio.com/) + * [dotnet SDK v6.0.2+](https://dotnet.microsoft.com/en-us/download) + * [Node.js v14+](https://nodejs.org/en/download/) + * npm v7+ (installed with Node.js) + * [Git command line](https://git-scm.com/download/) + +Open the root folder of the repository in Visual Studio Code. + +To build the front end you'll need to open the command pallet (Ctrl + Shift + P) and run `>Tasks: Run Task` followed by `Client Watch` and then run the `Client Build` task in the same way. + +You can also run the tasks manually on the command line: + +``` +cd src\Umbraco.Web.UI.Client +npm install +npm run dev +``` + +or + +``` +cd src\Umbraco.Web.UI.Client +npm install +gulp dev +``` + +**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.** + +You might run into [Gulp quirks](#gulp-quirks). + +The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making. + +To run the C# portion of the project, either hit F5 to begin debugging, or manually using the command line: + +``` +dotnet watch --project .\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj +``` + +**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.** + +When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging. + +#### Debugging with Visual Studio + +In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed. + + * [Visual Studio 2019 v16.8+ with .NET 6.0.2+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects) + * [Node.js v14+](https://nodejs.org/en/download/) + * npm v7+ (installed with Node.js) + * [Git command line](https://git-scm.com/download/) + +The easiest way to get started is to open `umbraco.sln` in Visual Studio. + +To build the front end, you'll first need to run `cd src\Umbraco.Web.UI.Client && npm install` in the command line (or `cd src\Umbraco.Web.UI.Client; npm install` in PowerShell). Then find the Task Runner Explorer (View → Other Windows → Task Runner Explorer) and run the `build` task under `Gulpfile.js`. You may need to refresh the Task Runner Explorer before the tasks load. + +If you're working on the backoffice, you may wish to run the `dev` command instead while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. + +**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.** + +You might run into [Gulp quirks](#gulp-quirks). + +The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making. + +"The rest" is a C# based codebase, which is mostly ASP.NET Core MVC based. You can make changes, build them in Visual Studio, and hit F5 to see the result. + +**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.** + +When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging. ## Building from source @@ -38,13 +104,14 @@ Did you read ["Are you sure"](#are-you-sure)? To build Umbraco, fire up PowerShell and move to Umbraco's repository root (the directory that contains `src`, `build`, `LICENSE.md`...). There, trigger the build with the following command: build/build.ps1 - + If you only see a build.bat-file, you're probably on the wrong branch. If you switch to the correct branch (v8/contrib) the file will appear and you can build it. You might run into [Powershell quirks](#powershell-quirks). If it runs without errors; Hooray! Now you can continue with [the next step](CONTRIBUTING.md#how-do-i-begin) and open the solution and build it. + ### Build Infrastructure The Umbraco Build infrastructure relies on a PowerShell object. The object can be retrieved with: @@ -145,7 +212,7 @@ To perform a more complete clear, you will want to also delete the content of th The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory. git clean -xdf . - + For git documentation see: * git [clean]() * git [reset]() @@ -214,3 +281,19 @@ The best solution is to unblock the Zip file before un-zipping: right-click the ### Git Quirks Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details). + +### Gulp Quirks + +You may need to run the following commands to set up gulp properly: + + ``` +npm cache clean --force +npm ci +npm run build + ``` + + + +[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco" +[ starter kits ]: https://our.umbraco.com/packages/?category=Starter%20Kits&version=9 "Browse starter kits available for v9 on Our " +[ disable browser caching ]: https://techwiser.com/disable-cache-google-chrome-firefox "Instructions on how to disable browser caching in Chrome and Firefox" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e8b378fb15..28618fb548 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,192 +2,131 @@ 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 -The following is a set of guidelines, for contributing to Umbraco CMS. +These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgement, and feel free to propose changes to this document in a pull request. -These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. +## Coding not your thing? Or want more ways to contribute? -Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you 💖. +This document covers contributing to the codebase of the CMS but [the community site has plenty of inspiration for other ways to get involved.][get involved] -**Code of conduct** +If you don't feel you'd like to make code changes here, you can visit our [documentation repository][docs repo] and use your experience to contribute to making the docs we have, even better. -This project and everyone participating in it, is governed by the [our Code of Conduct](https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md). +We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Collaborators and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. -**Table of contents** +## Table of contents -[Contributing code changes](#contributing-code-changes) - * [Guidelines for contributions we welcome](#guidelines-for-contributions-we-welcome) - * [Ownership and copyright](#ownership-and-copyright) - * [What can I start with?](#what-can-i-start-with) - * [How do I begin?](#how-do-i-begin) - * [Pull requests](#pull-requests) +- [Before you start](#before-you-start) + * [Code of Conduct](#code-of-conduct) + * [What can I contribute?](#what-can-i-contribute) + + [Making larger changes](#making-larger-changes) + + [Pull request or package?](#pull-request-or-package) + + [Ownership and copyright](#ownership-and-copyright) +- [Finding your first issue: Up for grabs](#finding-your-first-issue-up-for-grabs) +- [Making your changes](#making-your-changes) + + [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) + + [Style guide](#style-guide) + + [Questions?](#questions) +- [Creating a pull request](#creating-a-pull-request) +- [The review process](#the-review-process) + * [Dealing with requested changes](#dealing-with-requested-changes) + + [No longer available?](#no-longer-available) + * [The Core Collaborators team](#the-core-collaborators-team) -[Reviews](#reviews) - * [Styleguides](#styleguides) - * [The Core Contributors](#the-core-contributors-team) - * [Questions?](#questions) +## Before you start -[Working with the code](#working-with-the-code) - * [Building Umbraco from source code](#building-umbraco-from-source-code) - * [Working with the source code](#working-with-the-source-code) - * [Making changes after the PR is open](#making-changes-after-the-pr-is-open) - * [Which branch should I target for my contributions?](#which-branch-should-i-target-for-my-contributions) - * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) -## Contributing code changes +### Code of Conduct -This document gives you a quick overview on how to get started. +This project and everyone participating in it, is governed by the [our Code of Conduct][code of conduct]. -### Guidelines for contributions we welcome +### What can I contribute? -Not all changes are wanted, so on occasion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. +We categorise pull requests (PRs) into two categories: -We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes, so we can ensure that you don't put all your hard work into something we would not be able to merge. +| PR type | Definition | +| --------- | ------------------------------------------------------------ | +| Small PRs | Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. | +| Large PRs | New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). | -Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. +We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process][review process]. + +We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations. + +Not all changes are wanted, so on occasion we might close a PR without merging it but if we do, we will give you feedback why we can't accept your changes. **So make sure to [talk to us before making large changes][making larger changes]**, so we can ensure that you don't put all your hard work into something we would not be able to merge. + +#### Making larger changes + +[making larger changes]: #making-larger-changes + +Please make sure to describe your larger ideas in an [issue (bugs)][issues] or [discussion (new features)][discussions], it helps to put in mock up screenshots or videos. If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented. + +If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the small PRs process above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress. + +#### Pull request or package? + +[pr or package]: #pull-request-or-package + +If you're unsure about whether your changes belong in the core Umbraco CMS or if you should turn your idea into a package instead, make sure to [talk to us][making larger changes]. + +If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. Eventually, a package could "graduate" to be included in the CMS. #### Ownership and copyright -It is your responsibility to make sure that you're allowed to share the code you're providing us. -For example, you should have permission from your employer or customer to share code. +It is your responsibility to make sure that you're allowed to share the code you're providing us. For example, you should have permission from your employer or customer to share code. Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS. If you're not sure, leave a note on your contribution and we will be happy to guide you. -When your contribution has been accepted, it will be [MIT licensed](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/LICENSE.md) from that time onwards. +When your contribution has been accepted, it will be [MIT licensed][MIT license] from that time onwards. -### What can I start with? +## Finding your first issue: Up for grabs -Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` issues](https://github.com/umbraco/Umbraco-CMS/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs+) +Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. -### How do I begin? +If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. + +## Making your changes Great question! The short version goes like this: - * **Fork** - create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS) +1. **Fork** - ![Fork the repository](img/forkrepository.png) + Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo] + + ![Fork the repository](img/forkrepository.png) + +1. **Clone** - * **Clone** - when GitHub has created your fork, you can clone it in your favorite Git tool + When GitHub has created your fork, you can clone it in your favorite Git tool + + ![Clone the fork](img/clonefork.png) + +1. **Switch to the correct branch** - ![Clone the fork](img/clonefork.png) + Switch to the `v10/contrib` branch - * **Switch to the correct branch** - switch to the `v9/contrib` branch - * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md) - * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) - * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v9/contrib`, create a new branch first. - * **Push** - great, now you can push the changes up to your fork on GitHub - * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here](https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. +1. **Build** - ![Create a pull request](img/createpullrequest.png) + Build your fork of Umbraco locally as described in the build documentation: you can [debug with Visual Studio Code][build - debugging with code] or [with Visual Studio][build - debugging with vs]. -### Pull requests -The most successful pull requests usually look a like this: +1. **Branch** - * Fill in the required template (shown when starting a PR on GitHub), and link your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. - * Include screenshots and animated GIFs in your pull request whenever possible. - * Unit tests, while optional, are awesome. Thank you! - * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated. + Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `v10/contrib`, create a new branch first. -Again, these are guidelines, not strict requirements. However, the more information that you give to us, the more we have to work with when considering your contributions. Good documentation of a pull request can really speed up the time it takes to review and merge your work! +1. **Change** -## Reviews + Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback][questions]. -You've sent us your first contribution - congratulations! Now what? +1. **Commit and push** -The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes. + Done? Yay! 🎉 -We have [a process in place which you can read all about](REVIEW_PROCESS.md). The very abbreviated version is: + Remember to commit to your new `temp` branch, and don't commit to `v10/contrib`. Then you can push the changes up to your fork on GitHub. -- Your PR will get a reply within 48 hours -- An in-depth reply will be added within at most 2 weeks -- The PR will be either merged or rejected within at most 4 weeks -- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case. +#### Keeping your Umbraco fork in sync with the main repository +[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository -### Styleguides - -To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up. - -That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. - -### The Core Contributors team - -The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: - -- [Nathan Woulfe](https://github.com/nathanwoulfe) -- [Joe Glombek](https://github.com/glombek) -- [Laura Weatherhead](https://github.com/lssweatherhead) -- [Michael Latouche](https://github.com/mikecp) -- [Owain Williams](https://github.com/OwainWilliams) - - -These wonderful people aim to provide you with a first reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. Hq will have final sign-off and will check the work again before it is merged. - -### Questions? - -You can get in touch with [the core contributors team](#the-core-contributors-team) in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: - -- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward. -- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. - -## Working with the code - -### Building Umbraco from source code - -In order to build the Umbraco source code locally, first make sure you have the following installed. - - * [Visual Studio 2019 v16.8+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/) - * [Node.js v10+](https://nodejs.org/en/download/) - * npm v6.4.1+ (installed with Node.js) - * [Git command line](https://git-scm.com/download/) - -The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2019 (version 16.3 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. - -Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. - -![Gulp build in Visual Studio](img/gulpbuild.png) - -After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser. Follow the directions there to get a working Umbraco install up and running. - -### Working with the source code - -Some parts of our source code are over 10 years old now. And when we say "old", we mean "mature" of course! - -There are two big areas that you should know about: - - 1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. - You may need to run the following commands to set up gulp properly: - ``` - npm cache clean --force - npm ci - npm run build - ``` - The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to disable caching in the browser to help you to see the changes you're making. - - 2. "The rest" is a C# based codebase, which is mostly ASP.NET MVC based. You can make changes, build them in Visual Studio, and hit `F5` to see the result. - -To find the general areas for something you're looking to fix or improve, have a look at the following two parts of the API documentation. - - * [The AngularJS based backoffice files](https://apidocs.umbraco.com/v9/ui#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) - * [The C# application](https://apidocs.umbraco.com/v9/csharp/) - -### Which branch should I target for my contributions? - -We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v9/contrib`. If you are working on v9, this is the branch you should be targetting. For v8 contributions, please target 'v8/contrib' - -Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise. - -![Which branch should I target?](img/defaultbranch.png) - -### Making changes after the PR is open - -If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco! - -### Keeping your Umbraco fork in sync with the main repository - -We recommend you to sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. - -Also, if you have submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course. +Once you've already got a fork and cloned your fork locally, you can skip steps 1 and 2 going forward. Just remember to keep your fork up to date before making further changes. To sync your fork with this original one, you'll have to add the upstream url. You only have to do this once: @@ -199,13 +138,110 @@ Then when you want to get the changes from the main repository: ``` git fetch upstream -git rebase upstream/v9/contrib +git rebase upstream/v10/contrib ``` -In this command we're syncing with the `v9/contrib` branch, but you can of course choose another one if needed. +In this command we're syncing with the `v10/contrib` branch, but you can of course choose another one if needed. -(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated)) +[More information on how this works can be found on the thoughtbot blog.][sync fork ext] -### And finally +#### Style guide -We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) and use your experience to contribute to making the docs we have, even better. We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Contributors and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. +To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up. + +That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. + +#### Questions? +[questions]: #questions + +You can get in touch with [the core contributors team][core collabs] in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: + +- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward. +- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs]. +- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. + +## Creating a pull request + +Exciting! You're ready to show us your changes. + +We recommend you to [sync with our repository][sync fork] before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. + +GitHub will have picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. +![Create a pull request](img/createpullrequest.png) + +We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v10/contrib`. If you are working on v9, this is the branch you should be targeting. + +Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise. + +## The review process +[review process]: #the-review-process + +You've sent us your first contribution - congratulations! Now what? + +The [Core Collaborators team][Core collabs] can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes. + +You will get an initial automated reply from our [Friendly Umbraco Robot, Umbrabot][Umbrabot], to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. You can take this opportunity to double check everything is in order based off the handy checklist Umbrabot provides. + +You will get feedback as soon as the [Core Collaborators team][Core collabs] can after opening the PR. You’ll most likely get feedback within a couple of weeks. Then there are a few possible outcomes: + +- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco +- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible +- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see +- Your proposed change is awesome but... not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!). See [making larger changes][making larger changes] and [pull request or package?][pr or package] + +### Dealing with requested changes + +If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco! + +#### No longer available? + +We understand you have other things to do and can't just drop everything to help us out. + +So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. + +If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. + +There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. + +### The Core Collaborators team +[Core collabs]: #the-core-collaborators-team + +The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan][Sebastiaan], who gets assistance from the following community members who have committed to volunteering their free time: + +- [Nathan Woulfe][Nathan Woulfe] +- [Joe Glombek][Joe Glombek] +- [Laura Weatherhead][Laura Weatherhead] +- [Michael Latouche][Michael Latouche] +- [Owain Williams][Owain Williams] + + +These wonderful people aim to provide you with a reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. HQ will have final sign-off and will check the work again before it is merged. + + + + + +[MIT license]: ../LICENSE.md "Umbraco's license declaration" +[build - debugging with vs]: BUILD.md#debugging-with-visual-studio "Details on building and debugging Umbraco with Visual Studio" +[build - debugging with code]: BUILD.md#debugging-with-vs-code "Details on building and debugging Umbraco with Visual Studio Code" + + + +[Nathan Woulfe]: https://github.com/nathanwoulfe "Nathan's GitHub profile" +[Joe Glombek]: https://github.com/glombek "Joe's GitHub profile" +[Laura Weatherhead]: https://github.com/lssweatherhead "Laura's GitHub profile" +[Michael Latouche]: https://github.com/mikecp "Michael's GitHub profile" +[Owain Williams]: https://github.com/OwainWilliams "Owain's GitHub profile" +[Sebastiaan]: https://github.com/nul800sebastiaan "Senastiaan's GitHub profile" +[ Umbrabot ]: https://github.com/umbrabot +[git flow]: https://jeffkreeftmeijer.com/git-flow/ "An explanation of git flow" +[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated" +[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests" +[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/ +[get involved]: https://community.umbraco.com/get-involved/ +[docs repo]: https://github.com/umbraco/UmbracoDocs +[code of conduct]: https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md +[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs +[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS +[issues]: https://github.com/umbraco/Umbraco-CMS/issues +[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions diff --git a/.github/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md deleted file mode 100644 index 0ac35e6897..0000000000 --- a/.github/CONTRIBUTION_GUIDELINES.md +++ /dev/null @@ -1,35 +0,0 @@ -# Contributing to Umbraco CMS - -When you’re considering creating a pull request for Umbraco CMS, we will categorize them in two different sizes, small and large. - -The process for both sizes is very similar, as [explained in the contribution document](CONTRIBUTING.md#how-do-i-begin). - -## Small PRs -Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. - -We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process](REVIEW_PROCESS.md). - -### Up for grabs - -Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. - -If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. - -## Large PRs -New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). - -We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations. - -Please make sure to describe your idea in an issue, it helps to put in mockup screenshots or videos. - -If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented. - -If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the [small PRs process](#small-prs) above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress. - -It is highly recommended that you speak to the HQ before making large, complex changes. - -### Pull request or package? - -If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. - -Eventually, a package could "graduate" to be included in the CMS. diff --git a/.github/REVIEW_PROCESS.md b/.github/REVIEW_PROCESS.md deleted file mode 100644 index 917d25b090..0000000000 --- a/.github/REVIEW_PROCESS.md +++ /dev/null @@ -1,25 +0,0 @@ -# Review process - -You're an awesome person and have sent us your contribution in the form of a pull request! It's now time to relax for a bit and wait for our response. - -In order to set some expectations, here's what happens next. - -## Review process - -You will get an initial reply within 48 hours (workdays) to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. - -You will get feedback within at most 14 days after opening the PR. You’ll most likely get feedback sooner though. Then there are a few possible outcomes: - -- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco -- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible -- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see -- Your proposed change is awesome but.. not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!) - -## Are you still available? - -We understand you have other things to do and can't just drop everything to help us out. -So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. - -If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. - -There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. \ No newline at end of file diff --git a/.github/img/tableofcontentsicon.svg b/.github/img/tableofcontentsicon.svg new file mode 100644 index 0000000000..a08c8742d3 --- /dev/null +++ b/.github/img/tableofcontentsicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/.github/workflows/add-issues-to-review-project.yml b/.github/workflows/add-issues-to-review-project.yml new file mode 100644 index 0000000000..4590c173fc --- /dev/null +++ b/.github/workflows/add-issues-to-review-project.yml @@ -0,0 +1,53 @@ +name: Add issues to review project + +on: + issues: + types: + - opened + +jobs: + get-user-type: + runs-on: ubuntu-latest + outputs: + ignored: ${{ steps.set-output.outputs.ignored }} + steps: + - name: Install dependencies + run: | + npm install node-fetch@2 + - uses: actions/github-script@v5 + name: "Determing HQ user or not" + id: set-output + with: + script: | + const fetch = require('node-fetch'); + const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/users/IsIgnoredUser', { + method: 'post', + body: JSON.stringify('${{ github.event.issue.user.login }}'), + headers: { + 'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}', + 'Content-Type': 'application/json' + } + }); + + var isIgnoredUser = true; + try { + if(response.status === 200) { + const data = await response.text(); + isIgnoredUser = data === "true"; + } else { + console.log("Returned data not indicate success:", response.status); + } + } catch(error) { + console.log(error); + }; + core.setOutput("ignored", isIgnoredUser); + console.log("Ignored is", isIgnoredUser); + add-to-project: + if: needs.get-user-type.outputs.ignored == 'false' + runs-on: ubuntu-latest + needs: [get-user-type] + steps: + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/${{ github.repository_owner }}/projects/21 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index add4e13c77..c686f373e1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,7 +14,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/issues-first-response.yml b/.github/workflows/issues-first-response.yml new file mode 100644 index 0000000000..134a368785 --- /dev/null +++ b/.github/workflows/issues-first-response.yml @@ -0,0 +1,53 @@ +name: issue-first-response + +on: + issues: + types: [opened] + +jobs: + send-response: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Install dependencies + run: | + npm install node-fetch@2 + - name: Fetch random comment 🗣️ and add it to the issue + uses: actions/github-script@v6 + with: + script: | + const fetch = require('node-fetch') + + const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', { + method: 'post', + body: JSON.stringify({ + repo: '${{ github.repository }}', + number: '${{ github.event.number }}', + actor: '${{ github.actor }}', + commentType: 'opened-issue-first-comment' + }), + headers: { + 'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}', + 'Content-Type': 'application/json' + } + }); + + try { + const data = await response.text(); + + if(response.status === 200 && data !== '') { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: data + }); + } else { + console.log("Status code did not indicate success:", response.status); + console.log("Returned data:", data); + } + } catch(error) { + console.log(error); + } diff --git a/.github/workflows/pr-first-response.yml b/.github/workflows/pr-first-response.yml index 0969a883c9..991a5d0808 100644 --- a/.github/workflows/pr-first-response.yml +++ b/.github/workflows/pr-first-response.yml @@ -4,12 +4,12 @@ on: pull_request_target: types: [opened] -jobs: +jobs: send-response: runs-on: ubuntu-latest permissions: issues: write - pull-requests: write + pull-requests: write steps: - name: Install dependencies run: | @@ -45,8 +45,17 @@ jobs: body: data }); } else { - console.log("Status code did not indicate success:", response.status); + console.log("Returned data not indicate success."); + + if(response.status !== 200) { + console.log("Status code:", response.status) + } + console.log("Returned data:", data); + + if(data === '') { + console.log("An empty response usually indicates that either no comment was found or the actor user was not eligible for getting an automated response (HQ users are not getting auto-responses).") + } } } catch(error) { console.log(error); diff --git a/.github/workflows/up-for-grabs-response.yml b/.github/workflows/up-for-grabs-response.yml new file mode 100644 index 0000000000..eb16eb7779 --- /dev/null +++ b/.github/workflows/up-for-grabs-response.yml @@ -0,0 +1,63 @@ +name: labeled-up-for-grabs-first-comment + +on: + issues: + types: + - labeled + +jobs: + send-response: + if: github.event.label.name == 'community/up-for-grabs' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Install dependencies + run: | + npm install node-fetch@2 + - name: Fetch comment 🗣️ and add it to the issue + uses: actions/github-script@v6 + with: + script: | + const fetch = require('node-fetch'); + const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', { + method: 'post', + body: JSON.stringify({ + repo: '${{ github.repository }}', + number: '${{ github.event.issue.number }}', + actor: '${{ github.event.issue.user.login }}', + commentType: 'labeled-up-for-grabs-first-comment' + }), + headers: { + 'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}', + 'Content-Type': 'application/json' + } + }); + + try { + const data = await response.text(); + + if(response.status === 200 && data !== '') { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: data + }); + } else { + console.log("Returned data not indicate success."); + + if(response.status !== 200) { + console.log("Status code:", response.status) + } + + console.log("Returned data:", data); + + if(data === '') { + console.log("An empty response usually indicates that either no comment was found or the actor user was not eligible for getting an automated response (HQ users are not getting auto-responses).") + } + } + } catch(error) { + console.log(error); + }; diff --git a/.gitignore b/.gitignore index 348474c8cb..26f96f89b7 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,10 @@ preserve.belle /src/Umbraco.Web.UI.Docs/api/ /src/Umbraco.Web.UI.Docs/package-lock.json +# csharp-docs +/build/csharp-docs/api/ +/build/csharp-docs/_site/ + # Build /build.out/ /build.tmp/ @@ -64,8 +68,6 @@ preserve.belle /build/docs.zip /build/ui-docs.zip /build/csharp-docs.zip -/build/ApiDocs/ -/src/ApiDocs/api/ /src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/ # Environment specific data @@ -102,4 +104,5 @@ cypress.env.json /tests/Umbraco.Tests.UnitTests/[Uu]mbraco/[Dd]ata/TEMP/ # Ignore auto-generated schema -/src/Umbraco.Web.UI/[Uu]mbraco/config/appsettings-schema.json +/src/Umbraco.Web.UI/appsettings-schema.json +/src/Umbraco.Cms/appsettings-schema.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1d4324a34d..99876bc77e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -33,6 +33,18 @@ "$gulp-tsc" ] }, + { + "label": "Client Watch", + "detail": "runs npm run dev for Umbraco.Web.UI.Client", + "promptOnClose": true, + "group": "build", + "type": "npm", + "script": "dev", + "path": "src/Umbraco.Web.UI.Client/", + "problemMatcher": [ + "$gulp-tsc" + ] + }, { "label": "Dotnet build", "detail": "Dotnet build of SLN", diff --git a/Directory.Build.props b/Directory.Build.props index fcf605f555..8ae2e0baec 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,18 @@ - - - - + + + + + + + + all + 3.5.107 + + + + all + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec deleted file mode 100644 index f105f33cda..0000000000 --- a/build/NuSpecs/UmbracoCms.nuspec +++ /dev/null @@ -1,31 +0,0 @@ - - - - Umbraco.Cms - 10.0.0 - Umbraco Cms - Umbraco HQ - Umbraco HQ - MIT - https://umbraco.com/ - https://umbraco.com/dist/nuget/logo-small.png - false - Installs Umbraco Cms in your Visual Studio ASP.NET Core project - Installs Umbraco Cms in your Visual Studio ASP.NET Core project - en-US - umbraco - - - - - - - - - - - - - - - diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 9657898c0d..ae8ca25f52 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -1,721 +1,571 @@ name: $(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +parameters: + - name: sqlServerIntegrationTests + displayName: Run SQL Server Integration Tests + type: boolean + default: false + - name: myGetDeploy + displayName: Deploy to MyGet + type: boolean + default: false + - name: nuGetDeploy + displayName: Deploy to NuGet + type: boolean + default: false + - name: buildApiDocs + displayName: Build API docs + type: boolean + default: false + - name: uploadApiDocs + displayName: Upload API docs + type: boolean + default: false + variables: - buildConfiguration: Release - SA_PASSWORD: UmbracoIntegration123! - UMBRACO__CMS_GLOBAL__ID: 00000000-0000-0000-0000-000000000042 - UmbracoBuild: AzurePipeline - nodeVersion: 14.18.1 -resources: - containers: - - container: mssql - image: 'mcr.microsoft.com/mssql/server:2017-latest' - env: - ACCEPT_EULA: 'Y' - SA_PASSWORD: $(SA_PASSWORD) - MSSQL_PID: Developer - ports: - - '1433:1433' - options: '--name mssql' + buildConfiguration: Release + SA_PASSWORD: UmbracoIntegration123! + UMBRACO__CMS_GLOBAL__ID: 00000000-0000-0000-0000-000000000042 + nodeVersion: 14.18.1 + DOTNET_NOLOGO: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + stages: - - stage: Determine_build_type - displayName: Determine build type - dependsOn: [ ] - jobs: - - job: Set_build_variables - displayName: Set build variables - pool: - vmImage: windows-latest - steps: - - task: PowerShell@1 - name: setReleaseVariable - displayName: Set isRelease variable - inputs: - scriptType: inlineScript - inlineScript: > - $isRelease = [regex]::matches($env:BUILD_SOURCEBRANCH,"v\d+\/\d+.\d+.*") + ############################################### + ## Build + ############################################### + - stage: Build + variables: + npm_config_cache: $(Pipeline.Workspace)/.npm_client + jobs: + - job: A + displayName: Build Umbraco CMS + pool: + vmImage: 'ubuntu-latest' + steps: + - task: NodeTool@0 + displayName: Use node $(nodeVersion) + inputs: + versionSpec: $(nodeVersion) + - task: Cache@2 + displayName: Cache node_modules + inputs: + key: '"npm_client" | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json' + restoreKeys: | + "npm_client" | "$(Agent.OS)" + "npm_client" + path: $(npm_config_cache) + - script: npm ci --no-fund --no-audit --prefer-offline + workingDirectory: src/Umbraco.Web.UI.Client + displayName: Run npm ci + - task: gulp@0 + displayName: Run gulp build + inputs: + gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js + targets: coreBuild + workingDirectory: src/Umbraco.Web.UI.Client + - task: DotNetCoreCLI@2 + displayName: Run dotnet build + inputs: + command: build + projects: umbraco.sln + arguments: '--configuration $(buildConfiguration)' + - script: | + version="$(Build.BuildNumber)" + echo "varsion: $version" - if ($isRelease.Count -gt 0){ - Write-Host "##vso[build.addbuildtag]Release build" - Write-Host "##vso[task.setvariable variable=isRelease;isOutput=true]true" - }else{ - Write-Host "##vso[task.setvariable variable=isRelease;isOutput=true]false" - } - - stage: Unit_Tests - displayName: Unit Tests - dependsOn: [] - jobs: - - job: Linux_Unit_Tests - displayName: Linux - pool: - vmImage: ubuntu-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: '**/umbraco.sln' - - task: DotNetCoreCLI@2 - displayName: dotnet test - inputs: - command: test - projects: '**/*.Tests.UnitTests.csproj' - arguments: '--no-build' - - job: MacOS_Unit_Tests - displayName: Mac OS - pool: - vmImage: macOS-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: '**/umbraco.sln' - - task: DotNetCoreCLI@2 - displayName: dotnet test - inputs: - command: test - projects: '**/*.Tests.UnitTests.csproj' - arguments: '--no-build' - - job: Windows_Unit_Tests - displayName: Windows - pool: - vmImage: windows-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: '**/umbraco.sln' - - task: DotNetCoreCLI@2 - displayName: dotnet test - inputs: - command: test - projects: '**/*.Tests.UnitTests.csproj' - arguments: '--no-build' + major="$(echo $version | cut -d '.' -f 1)" + echo "major version: $major" - - stage: Integration_Tests - displayName: Integration Tests - dependsOn: [] - jobs: + echo "##vso[task.setvariable variable=majorVersion;isOutput=true]$major" + displayName: Set major version + name: determineMajorVersion + - task: PowerShell@2 + displayName: Prepare nupkg + inputs: + targetType: inline + script: | + $umbracoVersion = "$(Build.BuildNumber)" -replace "\+",".g" + $templatePaths = Get-ChildItem 'templates/**/.template.config/template.json' - - job: Linux_Integration_Tests_SQLite - timeoutInMinutes: 120 - displayName: Linux (SQLite) - pool: - vmImage: ubuntu-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: '**/umbraco.sln' - - task: DotNetCoreCLI@2 - displayName: dotnet test - inputs: - command: test - projects: '**/Umbraco.Tests.Integration.csproj' - arguments: '--no-build' - env: - Tests__Database__DatabaseType: 'Sqlite' - Umbraco__Cms__global__MainDomLock: 'FileSystemMainDomLock' + foreach ($templatePath in $templatePaths) { + $a = Get-Content $templatePath -Raw | ConvertFrom-Json + if ($a.symbols -and $a.symbols.UmbracoVersion) { + $a.symbols.UmbracoVersion.defaultValue = $umbracoVersion + $a | ConvertTo-Json -Depth 32 | Set-Content $templatePath + } + } - - job: Linux_Integration_Tests_SQLServer - services: - mssql: mssql - timeoutInMinutes: 120 - displayName: Linux (SQL Server) - pool: - vmImage: ubuntu-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: '**/umbraco.sln' - - task: DotNetCoreCLI@2 - displayName: dotnet test - inputs: - command: test - projects: '**/Umbraco.Tests.Integration.csproj' - arguments: '--no-build' - env: - Tests__Database__DatabaseType: 'SqlServer' - Tests__Database__SQLServerMasterConnectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);' - Umbraco__Cms__global__MainDomLock: 'SqlMainDomLock' + dotnet pack --configuration $(buildConfiguration) umbraco.sln -o $(Build.ArtifactStagingDirectory)/nupkg + - script: | + sha="$(Build.SourceVersion)" + sha=${sha:0:7} + buildnumber="$(Build.BuildNumber)_$(Build.BuildId)_$sha" + echo "##vso[build.updatebuildnumber]$buildnumber" + displayName: Update build number + - task: PublishPipelineArtifact@1 + displayName: Publish nupkg + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/nupkg + artifactName: nupkg + - task: PublishPipelineArtifact@1 + displayName: Publish build artifacts + inputs: + targetPath: $(Build.SourcesDirectory) + artifactName: build_output - - job: macOS_Integration_Tests_SQLite - timeoutInMinutes: 120 - displayName: macOS (SQLite) - pool: - vmImage: macOS-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: '**/umbraco.sln' - - task: DotNetCoreCLI@2 - displayName: dotnet test - inputs: - command: test - projects: '**/Umbraco.Tests.Integration.csproj' - arguments: '--no-build' - env: - Tests__Database__DatabaseType: 'Sqlite' - Umbraco__Cms__global__MainDomLock: 'FileSystemMainDomLock' + - stage: Build_Docs + condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.buildApiDocs}})) + displayName: Prepare API Documentation + dependsOn: Build + variables: + umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ] + jobs: + # C# API Reference + - job: + displayName: Build C# API Reference + pool: + vmImage: 'windows-latest' + steps: + - task: PowerShell@2 + displayName: Install DocFX + inputs: + targetType: inline + script: | + choco install docfx --version=2.59.2 -y + if ($lastexitcode -ne 0){ + throw ("Error installing DocFX") + } + - task: PowerShell@2 + displayName: Generate metadata + inputs: + targetType: inline + script: | + docfx metadata "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json" + if ($lastexitcode -ne 0){ + throw ("Error generating metadata.") + } + - task: PowerShell@2 + displayName: Generate documentation + inputs: + targetType: inline + script: | + docfx build "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json" + if ($lastexitcode -ne 0){ + throw ("Error generating documentation.") + } + - task: ArchiveFiles@2 + displayName: Archive C# Docs + inputs: + rootFolderOrFile: $(Build.SourcesDirectory)/build/csharp-docs/_site + includeRootFolder: false + archiveFile: $(Build.ArtifactStagingDirectory)/csharp-docs.zip + - task: PublishPipelineArtifact@1 + displayName: Publish C# Docs + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/csharp-docs.zip + artifact: csharp-docs - - job: Windows_Integration_Tests_LocalDb - timeoutInMinutes: 120 - displayName: Windows (LocalDb) - pool: - vmImage: windows-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - powershell: sqllocaldb start mssqllocaldb - displayName: Start MSSQL LocalDb - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: '**/umbraco.sln' - - task: DotNetCoreCLI@2 - displayName: dotnet test - inputs: - command: test - projects: '**/Umbraco.Tests.Integration.csproj' - arguments: '--no-build' - env: - Tests__Database__DatabaseType: 'LocalDb' - Umbraco__Cms__global__MainDomLock: 'MainDomSemaphoreLock' + # js API Reference + - job: + displayName: Build js API Reference + pool: + vmImage: 'ubuntu-latest' + steps: + - task: NodeTool@0 + displayName: Use Node 10.15.0 + inputs: + versionSpec: 10.15.0 # Won't work with 14.18.1 + - script: | + npm ci --no-fund --no-audit --prefer-offline + npx gulp docs - - stage: Acceptance_Tests - displayName: Acceptance Tests - dependsOn: [] - variables: - - name: Umbraco__CMS__Unattended__InstallUnattended + major="$(umbracoMajorVersion)" + echo "major version: $major" + + baseUrl="https://apidocs.umbraco.com/v$major/ui/" + echo "baseUrl: $baseUrl" + + sed -i "s|baseUrl = .*|baseUrl = '$baseUrl',|" api/index.html + displayName: Generate js Docs + workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Docs + - task: ArchiveFiles@2 + displayName: Archive js Docs + inputs: + rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Docs/api + includeRootFolder: false + archiveFile: $(Build.ArtifactStagingDirectory)/ui-docs.zip + - task: PublishPipelineArtifact@1 + displayName: Publish js Docs + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/ui-docs.zip + artifact: ui-docs + + ############################################### + ## Test + ############################################### + - stage: Unit + displayName: Unit Tests + dependsOn: Build + jobs: + # Unit Tests + - job: + displayName: Unit Tests + strategy: + matrix: + Windows: + vmImage: 'windows-latest' + Linux: + vmImage: 'ubuntu-latest' + macOS: + vmImage: 'macOS-latest' + pool: + vmImage: $(vmImage) + steps: + - task: DownloadPipelineArtifact@2 + displayName: Download build artifacts + inputs: + artifact: build_output + path: $(Build.SourcesDirectory) + - task: UseDotNet@2 + condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) # net6 already on the other images + displayName: Use net6 + inputs: + version: 6.x + - task: DotNetCoreCLI@2 + displayName: Run dotnet test + inputs: + command: test + projects: '**/*.Tests.UnitTests.csproj' + arguments: '--no-build --configuration $(buildConfiguration)' + testRunTitle: Unit Tests - $(Agent.OS) + + - stage: Integration + displayName: Integration Tests + dependsOn: Build + jobs: + # Integration Tests (SQLite) + - job: + displayName: Integration Tests (SQLite) + strategy: + matrix: + Windows: + vmImage: 'windows-latest' + Linux: + vmImage: 'ubuntu-latest' + macOS: + vmImage: 'macOS-latest' + pool: + vmImage: $(vmImage) + steps: + - task: DownloadPipelineArtifact@2 + displayName: Download build artifacts + inputs: + artifact: build_output + path: $(Build.SourcesDirectory) + - task: UseDotNet@2 + displayName: Use net6 + condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) # net6 already on the other images + inputs: + version: 6.x + - task: DotNetCoreCLI@2 + displayName: Run dotnet test + inputs: + command: test + projects: '**/*.Tests.Integration.csproj' + arguments: '--no-build --configuration $(buildConfiguration)' + testRunTitle: Integration Tests SQLite - $(Agent.OS) + env: + Tests__Database__DatabaseType: 'Sqlite' + Umbraco__Cms__global__MainDomLock: 'FileSystemMainDomLock' + + # Integration Tests (SQL Server) + - job: + timeoutInMinutes: 120 + condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.sqlServerIntegrationTests}}) + displayName: Integration Tests (SQL Server) + strategy: + matrix: + Windows: + vmImage: 'windows-latest' + testDb: LocalDb + connectionString: N/A + Linux: + vmImage: 'ubuntu-latest' + testDb: SqlServer + connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);' + pool: + vmImage: $(vmImage) + steps: + - task: DownloadPipelineArtifact@2 + displayName: Download build artifacts + inputs: + artifact: build_output + path: $(Build.SourcesDirectory) + - powershell: sqllocaldb start mssqllocaldb + displayName: Start localdb (Windows only) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + - powershell: docker run --name mssql -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD=$(SA_PASSWORD) -e MSSQL_PID=Developer mcr.microsoft.com/mssql/server:2019-latest + displayName: Start SQL Server (Linux only) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - task: DotNetCoreCLI@2 + displayName: Run dotnet test + inputs: + command: test + projects: '**/*.Tests.Integration.csproj' + arguments: '--no-build --configuration $(buildConfiguration)' + testRunTitle: Integration Tests SQL Server - $(Agent.OS) + env: + Tests__Database__DatabaseType: $(testDb) + Tests__Database__SQLServerMasterConnectionString: $(connectionString) + Umbraco__Cms__global__MainDomLock: 'SqlMainDomLock' + + - stage: E2E + variables: + npm_config_cache: $(Pipeline.Workspace)/.npm_e2e + CYPRESS_CACHE_FOLDER: $(Pipeline.Workspace)/cypress_binaries + displayName: E2E Tests + dependsOn: Build + jobs: + # E2E Tests + - job: + displayName: E2E Tests + variables: + - name: Umbraco__CMS__Unattended__InstallUnattended # Windows only value: true - - name: Umbraco__CMS__Unattended__UnattendedUserName + - name: Umbraco__CMS__Unattended__UnattendedUserName # Windows only value: Cypress Test - - name: Umbraco__CMS__Unattended__UnattendedUserEmail + - name: Umbraco__CMS__Unattended__UnattendedUserEmail # Windows only value: cypress@umbraco.com - - name: Umbraco__CMS__Unattended__UnattendedUserPassword + - name: Umbraco__CMS__Unattended__UnattendedUserPassword # Windows only value: UmbracoAcceptance123! - - name: Umbraco__CMS__Global__InstallMissingDatabase + - name: Umbraco__CMS__Global__InstallMissingDatabase # Windows only value: true - jobs: - - job: Windows_Acceptance_tests - variables: - - name: UmbracoDatabaseServer - value: (LocalDB)\MSSQLLocalDB - - name: UmbracoDatabaseName - value: Cypress - - name: ConnectionStrings__umbracoDbDSN - value: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true; - displayName: Windows - pool: - vmImage: windows-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x + - name: UmbracoDatabaseServer # Windows only + value: (LocalDB)\MSSQLLocalDB + - name: UmbracoDatabaseName # Windows only + value: Cypress + - name: ConnectionStrings__umbracoDbDSN # Windows only + value: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true; + - name: CYPRESS_BASE_URL + value: http://localhost:8080 + strategy: + matrix: + Linux: + vmImage: 'ubuntu-latest' + dockerfile: umbraco-linux.docker + dockerImageName: umbraco-linux + Windows: + vmImage: 'windows-latest' + pool: + vmImage: $(vmImage) + steps: + - task: DownloadPipelineArtifact@2 + displayName: Download nupkg + inputs: + artifact: nupkg + path: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/misc/nupkg + - task: NodeTool@0 + displayName: Use Node $(nodeVersion) + inputs: + versionSpec: $(nodeVersion) + - task: Cache@2 + displayName: Cache node_modules + inputs: + key: '"npm_e2e" | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json' + restoreKeys: | + "npm_e2e" | "$(Agent.OS)" + "npm_e2e" + path: $(npm_config_cache) + - task: Cache@2 + displayName: Cache cypress binaries + inputs: + key: '"cypress_binaries" | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json' + path: $(CYPRESS_CACHE_FOLDER) + - task: PowerShell@2 + displayName: Generate Cypress.env.json + inputs: + targetType: inline + script: > + @{ username = "$(Umbraco__CMS__Unattended__UnattendedUserEmail)"; password = "$(Umbraco__CMS__Unattended__UnattendedUserPassword)" } | ConvertTo-Json | Set-Content -Path "tests/Umbraco.Tests.AcceptanceTest/cypress.env.json"# + - script: npm ci --no-fund --no-audit --prefer-offline + workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/ + displayName: Run npm ci + - powershell: sqllocaldb start mssqllocaldb + displayName: Start localdb (Windows only) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer + displayName: Create database (Windows only) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + # Linux containers smooth + - task: PowerShell@2 + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + displayName: Build & run container (Linux only) + inputs: + workingDirectory: tests/Umbraco.Tests.AcceptanceTest/misc + targetType: inline + script: | + $sha = 'g$(Build.SourceVersion)'.substring(0, 8) + docker build -t $(dockerImageName):$sha -f $(dockerfile) . + mkdir -p $(Build.ArtifactStagingDirectory)/docker-images + docker save -o $(Build.ArtifactStagingDirectory)/docker-images/$(dockerImageName).$sha.tar $(dockerImageName):$sha + docker run --name $(dockerImageName) -dp 8080:5000 -e UMBRACO__CMS_GLOBAL__ID=$(UMBRACO__CMS_GLOBAL__ID) $(dockerImageName):$sha + docker ps + # Windows containers take forever. + # --no-launch-profile stops ASPNETCORE_ENVIRONMENT=Development which breaks the users.ts tests (smtp config = invite user button) + # Urls matching docker setup. + - task: PowerShell@2 + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + displayName: Build & run app (Windows only) + inputs: + workingDirectory: tests/Umbraco.Tests.AcceptanceTest/misc + targetType: inline + script: | + dotnet new --install ./nupkg/Umbraco.Templates.*.nupkg + dotnet new umbraco --name Cypress -o . --no-restore + dotnet restore --configfile ./nuget.config + dotnet build --no-restore -c Release + Start-Process -FilePath "dotnet" -ArgumentList "run --no-build -c Release --no-launch-profile --urls $(CYPRESS_BASE_URL)" + - task: PowerShell@2 + displayName: Wait for app + inputs: + targetType: inline + workingDirectory: tests/Umbraco.Tests.AcceptanceTest + script: | + npm i -g wait-on + wait-on -v --interval 1000 --timeout 120000 $(CYPRESS_BASE_URL) + - task: PowerShell@2 + displayName: Run Cypress (Desktop) + continueOnError: true + inputs: + targetType: inline + workingDirectory: tests/Umbraco.Tests.AcceptanceTest + script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' + - task: PublishTestResults@2 + displayName: Publish test results + condition: always() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'tests/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' + mergeTestResults: true + testRunTitle: "e2e - $(Agent.OS)" + - task: CopyFiles@2 + displayName: Prepare artifacts + condition: always() + inputs: + sourceFolder: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts + targetFolder: $(Build.ArtifactStagingDirectory)/cypresss + - task: PublishPipelineArtifact@1 + displayName: "Publish test artifacts" + condition: always() + inputs: + targetPath: $(Build.ArtifactStagingDirectory) + artifact: 'E2E artifacts - $(Agent.OS) - Attempt #$(System.JobAttempt)' - - powershell: sqllocaldb start mssqllocaldb - displayName: Start MSSQL LocalDb - - powershell: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer - displayName: Create database -# - task: DotNetCoreCLI@2 -# displayName: dotnet build -# inputs: -# command: build -# projects: '**/Umbraco.Web.UI.csproj' - - task: NodeTool@0 - displayName: Use Node $(nodeVersion) - inputs: - versionSpec: $(nodeVersion) - - task: Npm@1 - displayName: npm ci (Client) - inputs: - command: ci - workingDir: src\Umbraco.Web.UI.Client - verbose: false - - task: gulp@0 - displayName: gulp build - inputs: - gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js - targets: build - workingDirectory: src\Umbraco.Web.UI.Client - - powershell: Start-Process -FilePath "dotnet" -ArgumentList "run", "--project", "src\Umbraco.Web.UI\Umbraco.Web.UI.csproj" - displayName: dotnet run - - task: PowerShell@1 - displayName: Generate Cypress.env.json - inputs: - scriptType: inlineScript - inlineScript: > - @{ username = $env:Umbraco__CMS__Unattended__UnattendedUserEmail; password = $env:Umbraco__CMS__Unattended__UnattendedUserPassword } | ConvertTo-Json | Set-Content -Path "tests\Umbraco.Tests.AcceptanceTest\cypress.env.json" - - task: Npm@1 - name: PrepareTask - displayName: npm ci (AcceptanceTest) - inputs: - command: ci - workingDir: 'tests\Umbraco.Tests.AcceptanceTest' - - task: PowerShell@2 - displayName: Run Cypress (Desktop) - condition: always() - continueOnError: true - inputs: - targetType: inline - workingDirectory: tests\Umbraco.Tests.AcceptanceTest - script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' + ############################################### + ## Release + ############################################### + - stage: Deploy_MyGet + displayName: MyGet pre-release + dependsOn: + - Unit + - Integration + # - E2E # TODO: Enable when stable. + condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.myGetDeploy}})) + jobs: + - job: + displayName: Push to pre-release feed + steps: + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download nupkg + inputs: + artifact: nupkg + path: $(Build.ArtifactStagingDirectory)/nupkg + - task: NuGetCommand@2 + displayName: Nuget push + inputs: + command: 'push' + packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg + nuGetFeedType: 'external' + publishFeedCredentials: 'MyGet - Pre-releases' - - task: PublishTestResults@2 - condition: always() - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'tests/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' - mergeTestResults: true - testRunTitle: "Test results Desktop" -# - task: Npm@1 -# displayName: Run Cypress (Tablet portrait) -# condition: always() -# inputs: -# workingDir: tests\Umbraco.Tests.AcceptanceTest -# command: 'custom' -# customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' -# -# - task: Npm@1 -# displayName: Run Cypress (Mobile protrait) -# condition: always() -# inputs: -# workingDir: tests\Umbraco.Tests.AcceptanceTest -# command: 'custom' -# customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - - task: PublishPipelineArtifact@1 - displayName: "Publish test artifacts" - inputs: - targetPath: '$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts' - artifact: 'Test artifacts - Windows - Attempt #$(System.JobAttempt)' + - stage: Deploy_NuGet + displayName: NuGet release + dependsOn: + - Deploy_MyGet + - Build_Docs + condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.nuGetDeploy}})) + jobs: + - job: + displayName: Push to NuGet + steps: + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download nupkg + inputs: + artifact: nupkg + path: $(Build.ArtifactStagingDirectory)/nupkg + - task: NuGetCommand@2 + displayName: Nuget push + inputs: + command: 'push' + packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg + nuGetFeedType: 'external' + publishFeedCredentials: 'NuGet - Umbraco.*' - - job: Linux_Acceptance_tests_SqlServer - displayName: Linux (SQL Server) - variables: - - name: UmbracoDatabaseServer - value: localhost - - name: UmbracoDatabaseName - value: Cypress - - name: ConnectionStrings__umbracoDbDSN - value: Server=localhost,1433;Database=$(UmbracoDatabaseName);User Id=sa;Password=$(SA_PASSWORD); - services: - mssql: mssql - pool: - vmImage: ubuntu-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: Bash@3 - displayName: Create database - inputs: - targetType: 'inline' - script: 'sqlcmd -S . -U sa -P $SA_PASSWORD -Q "CREATE DATABASE $DBNAME"' - env: - DBNAME: $(UmbracoDatabaseName) - SA_PASSWORD: $(SA_PASSWORD) - - task: NodeTool@0 - displayName: Use Node $(nodeVersion) - inputs: - versionSpec: $(nodeVersion) - - task: Npm@1 - displayName: npm ci (Client) - inputs: - command: ci - workingDir: src/Umbraco.Web.UI.Client - verbose: false - - task: gulp@0 - displayName: gulp build - inputs: - gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js - targets: build - workingDirectory: src/Umbraco.Web.UI.Client - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: src/Umbraco.Web.UI/Umbraco.Web.UI.csproj - - task: Bash@3 - displayName: dotnet run - inputs: - targetType: 'inline' - script: 'nohup dotnet run --no-build --project ./src/Umbraco.Web.UI/ > $(Build.ArtifactStagingDirectory)/dotnet_run_log_linux.txt &' - - task: Bash@3 - displayName: Generate Cypress.env.json - inputs: - targetType: 'inline' - script: 'echo "{ \"username\": \"$USERNAME\", \"password\": \"$PASSWORD\" }" > "tests/Umbraco.Tests.AcceptanceTest/cypress.env.json"' - env: - USERNAME: $(Umbraco__CMS__Unattended__UnattendedUserEmail) - PASSWORD: $(Umbraco__CMS__Unattended__UnattendedUserPassword) - - task: Npm@1 - name: PrepareTask - displayName: npm ci (AcceptanceTest) - inputs: - command: ci - workingDir: 'tests/Umbraco.Tests.AcceptanceTest' - - task: Bash@3 - displayName: Run Cypress (Desktop) - condition: always() - continueOnError: true - inputs: - targetType: inline - workingDirectory: tests/Umbraco.Tests.AcceptanceTest - script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - - task: PublishTestResults@2 - condition: always() - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'tests/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' - mergeTestResults: true - testRunTitle: "Test results Desktop" - # - task: Npm@1 - # displayName: Run Cypress (Tablet portrait) - # condition: always() - # inputs: - # workingDir: tests/Umbraco.Tests.AcceptanceTest - # command: 'custom' - # customCommand: 'run test -- --config="viewportHeight=1366,viewportWidth=1024,screenshotsFolder=cypress/artifacts/tablet/screenshots,videosFolder=cypress/artifacts/tablet/videos,videoUploadOnPasses=false"' - # - # - task: Npm@1 - # displayName: Run Cypress (Mobile protrait) - # condition: always() - # inputs: - # workingDir: tests/Umbraco.Tests.AcceptanceTest - # command: 'custom' - # customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"' - - task: PublishPipelineArtifact@1 - displayName: "Publish test artifacts" - inputs: - targetPath: '$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts' - artifact: 'Test artifacts - Linux (SQL Server) - Attempt #$(System.JobAttempt)' - - task: PublishPipelineArtifact@1 - displayName: "Publish run log" - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/dotnet_run_log_linux.txt' - artifact: 'Test Run logs - Linux (SQL Server) - Attempt #$(System.JobAttempt)' - - - job: Linux_Acceptance_tests_SQLite - displayName: Linux (SQLite) - variables: - - name: ConnectionStrings__umbracoDbDSN - value: Data Source=|DataDirectory|/umbraco-cms-cypress.sqlite.db;Cache=Private;Foreign Keys=True - - name: ConnectionStrings__umbracoDbDSN_ProviderName - value: Microsoft.Data.SQLite - pool: - vmImage: ubuntu-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: NodeTool@0 - displayName: Use Node $(nodeVersion) - inputs: - versionSpec: $(nodeVersion) - - task: Npm@1 - displayName: npm ci (Client) - inputs: - command: ci - workingDir: src/Umbraco.Web.UI.Client - verbose: false - - task: gulp@0 - displayName: gulp build - inputs: - gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js - targets: build - workingDirectory: src/Umbraco.Web.UI.Client - - task: DotNetCoreCLI@2 - displayName: dotnet build - inputs: - command: build - projects: src/Umbraco.Web.UI/Umbraco.Web.UI.csproj - - task: Bash@3 - displayName: dotnet run - inputs: - targetType: 'inline' - script: 'nohup dotnet run --no-build -p ./src/Umbraco.Web.UI/ > $(Build.ArtifactStagingDirectory)/dotnet_run_log_linux.txt &' - - task: Bash@3 - displayName: Generate Cypress.env.json - inputs: - targetType: 'inline' - script: 'echo "{ \"username\": \"$USERNAME\", \"password\": \"$PASSWORD\" }" > "tests/Umbraco.Tests.AcceptanceTest/cypress.env.json"' - env: - USERNAME: $(Umbraco__CMS__Unattended__UnattendedUserEmail) - PASSWORD: $(Umbraco__CMS__Unattended__UnattendedUserPassword) - - task: Npm@1 - name: PrepareTask - displayName: npm ci (AcceptanceTest) - inputs: - command: ci - workingDir: 'tests/Umbraco.Tests.AcceptanceTest' - - task: Bash@3 - displayName: Run Cypress (Desktop) - condition: always() - continueOnError: true - inputs: - targetType: inline - workingDirectory: tests/Umbraco.Tests.AcceptanceTest - script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - - task: PublishTestResults@2 - condition: always() - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'tests/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' - mergeTestResults: true - testRunTitle: "Test results Desktop" - - - task: PublishPipelineArtifact@1 - displayName: "Publish test artifacts" - inputs: - targetPath: '$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts' - artifact: 'Test artifacts - Linux (SQLite) - Attempt #$(System.JobAttempt)' - - task: PublishPipelineArtifact@1 - displayName: "Publish run log" - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/dotnet_run_log_linux.txt' - artifact: 'Test Run logs - Linux (SQLite) - Attempt #$(System.JobAttempt)' - - - stage: Artifacts - dependsOn: [] - jobs: - - job: Build_Artifacts - displayName: Build Artifacts - pool: - vmImage: windows-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: NuGetToolInstaller@1 - displayName: Use NuGet Latest - - task: NuGetCommand@2 - displayName: Restore NuGet Packages - inputs: - restoreSolution: 'umbraco.sln' - feedsToUse: config - - task: PowerShell@1 - displayName: Update Version and Artifact Name - inputs: - scriptType: inlineScript - inlineScript: > - Write-Host "Working folder: $pwd" - - $ubuild = build/build.ps1 -get -continue - - $version = $ubuild.GetUmbracoVersion() - - $isRelease = [regex]::matches($env:BUILD_SOURCEBRANCH,"v\d+\/\d+.\d+.*") - - if ($isRelease.Count -gt 0) { - $continuous = $version.Semver - } else { - $date = (Get-Date).ToString("yyyyMMdd") - $continuous = "$($version.release)-preview$date.$(Build.BuildId)" - $ubuild.SetUmbracoVersion($continuous) - - # Update the default Umbraco version in templates - $templatePaths = Get-ChildItem 'templates/**/.template.config/template.json' - foreach ($templatePath in $templatePaths) { - $a = Get-Content $templatePath -Raw | ConvertFrom-Json - if ($a.symbols -and $a.symbols.UmbracoVersion) { - $a.symbols.UmbracoVersion.defaultValue = $continuous - $a | ConvertTo-Json -Depth 32 | Set-Content $templatePath - } - } - } - - Write-Host "##vso[build.updatebuildnumber]$continuous.$(Build.BuildId)" - - Write-Host "Building: $continuous" - - task: PowerShell@1 - displayName: Prepare Build - inputs: - scriptType: inlineScript - inlineScript: | - Write-Host "Working folder: $pwd" - $ubuild = build\build.ps1 -get - - $ubuild.PrepareBuild("vso") - - task: PowerShell@1 - displayName: Prepare JSON Schema - inputs: - scriptType: inlineScript - inlineScript: | - Write-Host "Working folder: $pwd" - $ubuild = build\build.ps1 -get -continue - - $ubuild.CompileJsonSchema() - - task: NodeTool@0 - displayName: Use Node $(nodeVersion) - inputs: - versionSpec: $(nodeVersion) - - task: Npm@1 - displayName: npm ci (Client) - inputs: - command: ci - workingDir: src\Umbraco.Web.UI.Client - verbose: false - - task: gulp@0 - displayName: gulp build - inputs: - gulpFile: src\Umbraco.Web.UI.Client\gulpfile.js - targets: build - workingDirectory: src\Umbraco.Web.UI.Client - publishJUnitResults: true - testResultsFiles: '**\TESTS-*.xml' - - task: PowerShell@1 - displayName: Prepare Packages - inputs: - scriptType: inlineScript - inlineScript: | - Write-Host "Working folder: $pwd" - $ubuild = build\build.ps1 -get -continue - - $ubuild.CompileUmbraco() - $ubuild.PreparePackages() - - task: PowerShell@1 - displayName: Verify & Package NuGet - inputs: - scriptType: inlineScript - inlineScript: | - Write-Host "Working folder: $pwd" - $ubuild = build\build.ps1 -get -continue - - $ubuild.VerifyNuGet() - $ubuild.PackageNuGet() - - task: CopyFiles@2 - displayName: Copy NuPkg Files to Staging - inputs: - SourceFolder: build.out - Contents: '*.*nupkg' - TargetFolder: $(build.artifactstagingdirectory) - CleanTargetFolder: true - - task: PublishBuildArtifacts@1 - displayName: Publish NuPkg Files - inputs: - PathtoPublish: $(build.artifactstagingdirectory) - ArtifactName: nupkg - - task: CopyFiles@2 - displayName: Copy Log Files to Staging - inputs: - SourceFolder: build.tmp - Contents: '*.log' - TargetFolder: $(build.artifactstagingdirectory) - CleanTargetFolder: true - condition: succeededOrFailed() - - task: PublishBuildArtifacts@1 - displayName: Publish Log Files - inputs: - PathtoPublish: $(build.artifactstagingdirectory) - ArtifactName: logs - condition: succeededOrFailed() - - stage: Artifacts_Docs - displayName: 'Static Code Documentation' - dependsOn: [Determine_build_type] - jobs: - - job: Generate_Docs_CSharp - timeoutInMinutes: 60 - displayName: Generate C# Docs - condition: eq(stageDependencies.Determine_build_type.Set_build_variables.outputs['setReleaseVariable.isRelease'], 'true') - pool: - vmImage: windows-latest - steps: - - task: UseDotNet@2 - displayName: Use .Net 6.x - inputs: - version: 6.x - - task: PowerShell@2 - displayName: 'Prep build tool - C# Docs' - inputs: - targetType: inline - script: | - choco install docfx --version=2.59.0 -y - if ($lastexitcode -ne 0){ - throw ("Error installing DocFX") - } - docfx metadata --loglevel Verbose "$(Build.SourcesDirectory)\src\ApiDocs\docfx.json" - if ($lastexitcode -ne 0){ - throw ("Error generating docs.") - } - docfx build --loglevel Verbose "$(Build.SourcesDirectory)\src\ApiDocs\docfx.json" - if ($lastexitcode -ne 0){ - throw ("Error generating docs.") - } - errorActionPreference: continue - workingDirectory: build - - task: ArchiveFiles@2 - displayName: 'Zip C# Docs' - inputs: - rootFolderOrFile: $(Build.SourcesDirectory)\src\ApiDocs\_site - includeRootFolder: false - archiveType: zip - archiveFile: $(Build.ArtifactStagingDirectory)\docs\csharp-docs.zip - replaceExistingArchive: true - - task: PublishPipelineArtifact@1 - displayName: Publish to artifacts - C# Docs - inputs: - targetPath: $(Build.ArtifactStagingDirectory)\docs\csharp-docs.zip - artifact: docs-cs - publishLocation: pipeline - - job: Generate_Docs_JS - timeoutInMinutes: 60 - displayName: Generate JS Docs - condition: eq(stageDependencies.Determine_build_type.Set_build_variables.outputs['setReleaseVariable.isRelease'], 'true') - pool: - vmImage: windows-latest - steps: - - task: PowerShell@2 - displayName: Prep build tool - JS Docs - inputs: - targetType: inline - script: | - $uenv=./build.ps1 -get -doc - $uenv.SandboxNode() - $uenv.CompileBelle() - $uenv.PrepareAngularDocs() - $uenv.RestoreNode() - errorActionPreference: continue - workingDirectory: build - - task: PublishPipelineArtifact@1 - displayName: Publish to artifacts - JS Docs - inputs: - targetPath: $(Build.Repository.LocalPath)\build.out\ - artifact: docs - publishLocation: pipeline + - stage: Upload_API_Docs + pool: + vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :( + variables: + umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ] + displayName: Upload API Documention + dependsOn: + - Build + - Deploy_NuGet + condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.uploadApiDocs}})) + jobs: + - job: + displayName: Upload C# Docs + steps: + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download artifact + inputs: + artifact: csharp-docs + path: $(Build.SourcesDirectory) + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: $(Build.SourcesDirectory)/csharp-docs.zip + destinationFolder: $(Build.ArtifactStagingDirectory)/csharp-docs + - task: AzureFileCopy@4 + displayName: 'Copy C# Docs to blob storage' + inputs: + SourcePath: '$(Build.ArtifactStagingDirectory)/csharp-docs/*' + azureSubscription: umbraco-storage + Destination: AzureBlob + storage: umbracoapidocs + ContainerName: '$web' + BlobPrefix: v$(umbracoMajorVersion)/csharp + - job: + displayName: Upload js Docs + steps: + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download artifact + inputs: + artifact: ui-docs + path: $(Build.SourcesDirectory) + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: $(Build.SourcesDirectory)/ui-docs.zip + destinationFolder: $(Build.ArtifactStagingDirectory)/ui-docs + - task: AzureFileCopy@4 + displayName: 'Copy UI Docs to blob storage' + inputs: + SourcePath: '$(Build.ArtifactStagingDirectory)/ui-docs/*' + azureSubscription: umbraco-storage + Destination: AzureBlob + storage: umbracoapidocs + ContainerName: '$web' + BlobPrefix: v$(umbracoMajorVersion)/ui diff --git a/build/build-bootstrap.ps1 b/build/build-bootstrap.ps1 deleted file mode 100644 index 4c946ba289..0000000000 --- a/build/build-bootstrap.ps1 +++ /dev/null @@ -1,95 +0,0 @@ - - # this script should be dot-sourced into the build.ps1 scripts - # right after the parameters declaration - # ie - # . "$PSScriptRoot\build-bootstrap.ps1" - - # THIS FILE IS DISTRIBUTED AS PART OF UMBRACO.BUILD - # DO NOT MODIFY IT - ALWAYS USED THE COMMON VERSION - - # ################################################################ - # BOOTSTRAP - # ################################################################ - - # reset errors - $error.Clear() - - # ensure we have temp folder for downloads - $scriptRoot = "$PSScriptRoot" - $scriptTemp = "$scriptRoot\temp" - if (-not (test-path $scriptTemp)) { mkdir $scriptTemp > $null } - - # get NuGet - $cache = 4 - $nuget = "$scriptTemp\nuget.exe" - # ensure the correct NuGet-source is used. This one is used by Umbraco - $nugetsourceUmbraco = "https://www.myget.org/F/umbracoprereleases/api/v3/index.json" - if (-not $local) - { - $source = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" - if ((test-path $nuget) -and ((ls $nuget).CreationTime -lt [DateTime]::Now.AddDays(-$cache))) - { - Remove-Item $nuget -force -errorAction SilentlyContinue > $null - } - if (-not (test-path $nuget)) - { - Write-Host "Download NuGet..." - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest $source -OutFile $nuget - if (-not $?) { throw "Failed to download NuGet." } - } - } - elseif (-not (test-path $nuget)) - { - throw "Failed to locate NuGet.exe." - } - - # NuGet notes - # As soon as we use -ConfigFile, NuGet uses that file, and only that file, and does not - # merge configuration from system level. See comments in NuGet.Client solution, class - # NuGet.Configuration.Settings, method LoadDefaultSettings. - # For NuGet to merge configurations, it needs to "find" the file in the current directory, - # or above. Which means we cannot really use -ConfigFile but instead have to have Umbraco's - # NuGet.config file at root, and always run NuGet.exe while at root or in a directory below - # root. - - $solutionRoot = "$scriptRoot\.." - $testPwd = [System.IO.Path]::GetFullPath($pwd.Path) + "\" - $testRoot = [System.IO.Path]::GetFullPath($solutionRoot) + "\" - if (-not $testPwd.ToLower().StartsWith($testRoot.ToLower())) - { - throw "Cannot run outside of the solution's root." - } - - # get the build system - if (-not $local) - { - $params = "-OutputDirectory", $scriptTemp, "-Verbosity", "quiet", "-PreRelease", "-Source", $nugetsourceUmbraco - &$nuget install Umbraco.Build @params - if (-not $?) { throw "Failed to download Umbraco.Build." } - } - - # ensure we have the build system - $ubuildPath = ls "$scriptTemp\Umbraco.Build.*" | sort -property CreationTime -descending | select -first 1 - if (-not $ubuildPath) - { - throw "Failed to locate the build system." - } - - # boot the build system - # this creates $global:ubuild - return &"$ubuildPath\ps\Boot.ps1" - - # at that point the build.ps1 script must boot the build system - # eg - # $ubuild.Boot($ubuildPath.FullName, [System.IO.Path]::GetFullPath("$scriptRoot\.."), - # @{ Local = $local; With7Zip = $false; WithNode = $false }, - # @{ continue = $continue }) - # if (-not $?) { throw "Failed to boot the build system." } - # - # and it's good practice to report - # eg - # Write-Host "Umbraco.Whatever Build" - # Write-Host "Umbraco.Build v$($ubuild.BuildVersion)" - - # eof diff --git a/build/build.ps1 b/build/build.ps1 deleted file mode 100644 index 0c2780fb03..0000000000 --- a/build/build.ps1 +++ /dev/null @@ -1,491 +0,0 @@ - - param ( - # get, don't execute - [Parameter(Mandatory=$false)] - [Alias("g")] - [switch] $get = $false, - - # run local, don't download, assume everything is ready - [Parameter(Mandatory=$false)] - [Alias("l")] - [Alias("loc")] - [switch] $local = $false, - - # enable docfx - [Parameter(Mandatory=$false)] - [Alias("doc")] - [switch] $docfx = $false, - - # keep the build directories, don't clear them - [Parameter(Mandatory=$false)] - [Alias("c")] - [Alias("cont")] - [switch] $continue = $false, - - # execute a command - [Parameter(Mandatory=$false, ValueFromRemainingArguments=$true)] - [String[]] - $command - ) - - # ################################################################ - # BOOTSTRAP - # ################################################################ - - # create and boot the buildsystem - $ubuild = &"$PSScriptRoot\build-bootstrap.ps1" - if (-not $?) { return } - $ubuild.Boot($PSScriptRoot, - @{ Local = $local; WithDocFx = $docfx }, - @{ Continue = $continue }) - if ($ubuild.OnError()) { return } - - Write-Host "Umbraco CMS Build" - Write-Host "Umbraco.Build v$($ubuild.BuildVersion)" - - # ################################################################ - # TASKS - # ################################################################ - - $ubuild.DefineMethod("SetMoreUmbracoVersion", - { - param ( $semver ) - - $port = "" + $semver.Major + $semver.Minor + ("" + $semver.Patch).PadLeft(2, '0') - Write-Host "Update port in launchSettings.json to $port" - $filePath = "$($this.SolutionRoot)\src\Umbraco.Web.UI\Properties\launchSettings.json" - $this.ReplaceFileText($filePath, ` - "http://localhost:(\d+)?", ` - "http://localhost:$port") - }) - - $ubuild.DefineMethod("SandboxNode", - { - $global:node_path = $env:path - $nodePath = $this.BuildEnv.NodePath - $gitExe = (Get-Command git).Source - if (-not $gitExe) { $gitExe = (Get-Command git).Path } - $gitPath = [System.IO.Path]::GetDirectoryName($gitExe) - $env:path = "$nodePath;$gitPath" - - $global:node_nodepath = $this.ClearEnvVar("NODEPATH") - $global:node_npmcache = $this.ClearEnvVar("NPM_CONFIG_CACHE") - $global:node_npmprefix = $this.ClearEnvVar("NPM_CONFIG_PREFIX") - - # https://github.com/gruntjs/grunt-contrib-connect/issues/235 - $this.SetEnvVar("NODE_NO_HTTP2", "1") - }) - - $ubuild.DefineMethod("RestoreNode", - { - $env:path = $node_path - - $this.SetEnvVar("NODEPATH", $node_nodepath) - $this.SetEnvVar("NPM_CONFIG_CACHE", $node_npmcache) - $this.SetEnvVar("NPM_CONFIG_PREFIX", $node_npmprefix) - - $this.ClearEnvVar("NODE_NO_HTTP2") - }) - - $ubuild.DefineMethod("CompileBelle", - { - $src = "$($this.SolutionRoot)\src" - $log = "$($this.BuildTemp)\belle.log" - - - Write-Host "Compile Belle" - Write-Host "Logging to $log" - - # get a temp clean node env (will restore) - $this.SandboxNode() - - # stupid PS is going to gather all "warnings" in $error - # so we have to take care of it else they'll bubble and kill the build - if ($error.Count -gt 0) { return } - - try { - Push-Location "$($this.SolutionRoot)\src\Umbraco.Web.UI.Client" - Write-Output "" > $log - - Write-Output "### node version is:" > $log - node -v >> $log 2>&1 - if (-not $?) { throw "Failed to report node version." } - - Write-Output "### npm version is:" >> $log 2>&1 - npm -v >> $log 2>&1 - if (-not $?) { throw "Failed to report npm version." } - - Write-Output "### clean npm cache" >> $log 2>&1 - npm cache clean --force >> $log 2>&1 - $error.Clear() # that one can fail 'cos security bug - ignore - - Write-Output "### npm ci" >> $log 2>&1 - npm ci >> $log 2>&1 - Write-Output ">> $? $($error.Count)" >> $log 2>&1 - # Don't really care about the messages from npm ci making us think there are errors - $error.Clear() - - Write-Output "### gulp build for version $($this.Version.Release)" >> $log 2>&1 - npm run build --buildversion=$this.Version.Release >> $log 2>&1 - - # We can ignore this warning, we need to update to node 12 at some point - https://github.com/jsdom/jsdom/issues/2939 - $indexes = [System.Collections.ArrayList]::new() - $index = 0; - $error | ForEach-Object { - # Find which of the errors is the ExperimentalWarning - if($_.ToString().Contains("ExperimentalWarning: The fs.promises API is experimental")) { - [void]$indexes.Add($index) - } - $index++ - } - $indexes | ForEach-Object { - # Loop through the list of indexes and remove the errors that we expect and feel confident we can ignore - $error.Remove($error[$_]) - } - - if (-not $?) { throw "Failed to build" } # that one is expected to work - } finally { - Pop-Location - - # FIXME: should we filter the log to find errors? - #get-content .\build.tmp\belle.log | %{ if ($_ -match "build") { write $_}} - - # restore - $this.RestoreNode() - } - - # setting node_modules folder to hidden - # used to prevent VS13 from crashing on it while loading the websites project - # also makes sure aspnet compiler does not try to handle rogue files and chokes - # in VSO with Microsoft.VisualC.CppCodeProvider -related errors - # use get-item -force 'cos it might be hidden already - Write-Host "Set hidden attribute on node_modules" - $dir = Get-Item -force "$src\Umbraco.Web.UI.Client\node_modules" - $dir.Attributes = $dir.Attributes -bor ([System.IO.FileAttributes]::Hidden) - }) - - $ubuild.DefineMethod("CompileUmbraco", - { - $buildConfiguration = "Release" - - $src = "$($this.SolutionRoot)\src" - $log = "$($this.BuildTemp)\build.umbraco.log" - - Write-Host "Compile Umbraco" - Write-Host "Logging to $log" - - & dotnet build "$src\Umbraco.Web.UI\Umbraco.Web.UI.csproj" ` - --configuration $buildConfiguration ` - --output "$($this.BuildTemp)\bin\\" ` - > $log - - # get files into WebApp\bin - & dotnet publish "$src\Umbraco.Web.UI\Umbraco.Web.UI.csproj" ` - --configuration Release --output "$($this.BuildTemp)\WebApp\bin\\" ` - > $log - - # remove extra files - $webAppBin = "$($this.BuildTemp)\WebApp\bin" - $excludeDirs = @("$($webAppBin)\refs","$($webAppBin)\runtimes","$($webAppBin)\umbraco","$($webAppBin)\wwwroot") - $excludeFiles = @("$($webAppBin)\appsettings.*","$($webAppBin)\*.deps.json","$($webAppBin)\*.exe","$($webAppBin)\*.config","$($webAppBin)\*.runtimeconfig.json") - $this.RemoveDirectory($excludeDirs) - $this.RemoveFile($excludeFiles) - - # copy rest of the files into WebApp - $excludeUmbracoDirs = @("$($this.BuildTemp)\WebApp\umbraco\lib","$($this.BuildTemp)\WebApp\umbraco\Data","$($this.BuildTemp)\WebApp\umbraco\Logs") - $this.RemoveDirectory($excludeUmbracoDirs) - $this.CopyFiles("$($this.SolutionRoot)\src\Umbraco.Web.UI\Views", "*", "$($this.BuildTemp)\WebApp\Views") - Copy-Item "$($this.SolutionRoot)\src\Umbraco.Web.UI\appsettings.json" "$($this.BuildTemp)\WebApp" - - if (-not $?) { throw "Failed to compile Umbraco.Web.UI." } - - # /p:UmbracoBuild tells the csproj that we are building from PS, not VS - }) - - $ubuild.DefineMethod("CompileJsonSchema", - { - Write-Host "Generating JSON Schema for AppSettings" - Write-Host "Logging to $($this.BuildTemp)\json.schema.log" - - ## NOTE: Need to specify the outputfile to point to the build temp folder - &dotnet run --project "$($this.SolutionRoot)\src\JsonSchema\JsonSchema.csproj" ` - -c Release > "$($this.BuildTemp)\json.schema.log" ` - -- ` - --outputFile "$($this.BuildTemp)\WebApp\umbraco\config\appsettings-schema.json" - }) - - $ubuild.DefineMethod("PrepareTests", - { - Write-Host "Prepare Tests" - - # FIXME: - idea is to avoid rebuilding everything for tests - # but because of our weird assembly versioning (with .* stuff) - # everything gets rebuilt all the time... - #Copy-Files "$tmp\bin" "." "$tmp\tests" - - # data - Write-Host "Copy data files" - if (-not (Test-Path -Path "$($this.BuildTemp)\tests\Packaging" )) - { - Write-Host "Create packaging directory" - mkdir "$($this.BuildTemp)\tests\Packaging" > $null - } - #$this.CopyFiles("$($this.SolutionRoot)\src\Umbraco.Tests\Packaging\Packages", "*", "$($this.BuildTemp)\tests\Packaging\Packages") - - # required for package install tests - if (-not (Test-Path -Path "$($this.BuildTemp)\tests\bin" )) - { - Write-Host "Create bin directory" - mkdir "$($this.BuildTemp)\tests\bin" > $null - } - }) - - $ubuild.DefineMethod("CompileTests", - { - $buildConfiguration = "Release" - $log = "$($this.BuildTemp)\msbuild.tests.log" - - Write-Host "Compile Tests" - Write-Host "Logging to $log" - - # beware of the weird double \\ at the end of paths - # see http://edgylogic.com/blog/powershell-and-external-commands-done-right/ - &dotnet msbuild "$($this.SolutionRoot)\tests\Umbraco.Tests\Umbraco.Tests.csproj" ` - -target:Build ` - -property:WarningLevel=0 ` - -property:Configuration=$buildConfiguration ` - -property:Platform=AnyCPU ` - -property:UseWPP_CopyWebApplication=True ` - -property:PipelineDependsOnBuild=False ` - -property:OutDir="$($this.BuildTemp)\tests\\" ` - -property:Verbosity=minimal ` - -property:UmbracoBuild=True ` - > $log - - if (-not $?) { throw "Failed to compile tests." } - - # /p:UmbracoBuild tells the csproj that we are building from PS - }) - - $ubuild.DefineMethod("PreparePackages", - { - Write-Host "Prepare Packages" - - $src = "$($this.SolutionRoot)\src" - $tmp = "$($this.BuildTemp)" - - # cleanup build - Write-Host "Clean build" - $this.RemoveFile("$tmp\bin\*.dll.config") - $this.RemoveFile("$tmp\WebApp\bin\*.dll.config") - - # cleanup presentation - Write-Host "Cleanup presentation" - $this.RemoveDirectory("$tmp\WebApp\umbraco.presentation") - - # create directories - Write-Host "Create directories" - mkdir "$tmp\WebApp\App_Data" > $null - #mkdir "$tmp\WebApp\Media" > $null - #mkdir "$tmp\WebApp\Views" > $null - - # copy various files - Write-Host "Copy xml documentation" - Copy-Item -force "$tmp\bin\*.xml" "$tmp\WebApp\bin" - - # offset the modified timestamps on all umbraco dlls, as WebResources - # break if date is in the future, which, due to timezone offsets can happen. - Write-Host "Offset dlls timestamps" - Get-ChildItem -r "$tmp\*.dll" | ForEach-Object { - $_.CreationTime = $_.CreationTime.AddHours(-11) - $_.LastWriteTime = $_.LastWriteTime.AddHours(-11) - } - }) - - - $ubuild.DefineMethod("PrepareBuild", - { - Write-host "Set environment" - $env:UMBRACO_VERSION=$this.Version.Semver.ToString() - $env:UMBRACO_RELEASE=$this.Version.Release - $env:UMBRACO_COMMENT=$this.Version.Comment - $env:UMBRACO_BUILD=$this.Version.Build - $env:UMBRACO_TMP="$($this.SolutionRoot)\build.tmp" - - if ($args -and $args[0] -eq "vso") - { - Write-host "Set VSO environment" - # set environment variable for VSO - # https://github.com/Microsoft/vsts-tasks/issues/375 - # https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md - Write-Host ("##vso[task.setvariable variable=UMBRACO_VERSION;]$($this.Version.Semver.ToString())") - Write-Host ("##vso[task.setvariable variable=UMBRACO_RELEASE;]$($this.Version.Release)") - Write-Host ("##vso[task.setvariable variable=UMBRACO_COMMENT;]$($this.Version.Comment)") - Write-Host ("##vso[task.setvariable variable=UMBRACO_BUILD;]$($this.Version.Build)") - - Write-Host ("##vso[task.setvariable variable=UMBRACO_TMP;]$($this.SolutionRoot)\build.tmp") - } - }) - - $nugetsourceUmbraco = "https://api.nuget.org/v3/index.json" - - $ubuild.DefineMethod("RestoreNuGet", - { - Write-Host "Restore NuGet" - Write-Host "Logging to $($this.BuildTemp)\nuget.restore.log" - $params = "-Source", $nugetsourceUmbraco - &$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" @params - if (-not $?) { throw "Failed to restore NuGet packages." } - }) - - $ubuild.DefineMethod("PackageNuGet", - { - $nuspecs = "$($this.SolutionRoot)\build\NuSpecs" - $templates = "$($this.SolutionRoot)\templates" - - Write-Host "Create NuGet packages" - - &dotnet pack "$($this.SolutionRoot)\umbraco.sln" ` - --output "$($this.BuildOutput)" ` - --verbosity detailed ` - -c Release ` - -p:PackageVersion="$($this.Version.Semver.ToString())" > "$($this.BuildTemp)\pack.umbraco.log" - - &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.nuspec" ` - -Properties BuildTmp="$($this.BuildTemp)" ` - -Version "$($this.Version.Semver.ToString())" ` - -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cms.log" - if (-not $?) { throw "Failed to pack NuGet UmbracoCms." } - - &$this.BuildEnv.NuGet Pack "$templates\Umbraco.Templates.nuspec" ` - -Properties BuildTmp="$($this.BuildTemp)" ` - -Version "$($this.Version.Semver.ToString())" ` - -NoDefaultExcludes ` - -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.templates.log" - if (-not $?) { throw "Failed to pack NuGet Umbraco.Templates." } - - # run hook - if ($this.HasMethod("PostPackageNuGet")) - { - Write-Host "Run PostPackageNuGet hook" - $this.PostPackageNuGet(); - if (-not $?) { throw "Failed to run hook." } - } - }) - - $ubuild.DefineMethod("VerifyNuGet", - { - $this.VerifyNuGetConsistency( - ("UmbracoCms"), - ("Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.Web.UI", "Umbraco.Examine.Lucene", "Umbraco.PublishedCache.NuCache", "Umbraco.Web.Common", "Umbraco.Web.Website", "Umbraco.Web.BackOffice", "Umbraco.Cms.Persistence.Sqlite", "Umbraco.Cms.Persistence.SqlServer")) - if ($this.OnError()) { return } - }) - - $ubuild.DefineMethod("PrepareCSharpDocs", - { - Write-Host "Prepare C# Documentation" - - $src = "$($this.SolutionRoot)\src" - $tmp = $this.BuildTemp - $out = $this.BuildOutput - $DocFxJson = Join-Path -Path $src "\ApiDocs\docfx.json" - $DocFxSiteOutput = Join-Path -Path $tmp "\_site\*.*" - - # run DocFx - $DocFx = $this.BuildEnv.DocFx - - & $DocFx metadata $DocFxJson - & $DocFx build $DocFxJson - - # zip it - & $this.BuildEnv.Zip a -tzip -r "$out\csharp-docs.zip" $DocFxSiteOutput - }) - - $ubuild.DefineMethod("PrepareAngularDocs", - { - Write-Host "Prepare Angular Documentation" - - $src = "$($this.SolutionRoot)\src" - $out = $this.BuildOutput - - # Check if the solution has been built - if (!(Test-Path "$src\Umbraco.Web.UI.Client\node_modules")) {throw "Umbraco needs to be built before generating the Angular Docs"} - - "Moving to Umbraco.Web.UI.Docs folder" - cd $src\Umbraco.Web.UI.Docs - - "Generating the docs and waiting before executing the next commands" - & npm ci - & npx gulp docs - - Pop-Location - - # change baseUrl - $BaseUrl = "https://apidocs.umbraco.com/v9/ui/" - $IndexPath = "./api/index.html" - (Get-Content $IndexPath).replace('origin + location.href.substr(origin.length).replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath - - # zip it - & $this.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Docs\api\*.*" - }) - - $ubuild.DefineMethod("Build", - { - $error.Clear() - - $this.PrepareBuild() - if ($this.OnError()) { return } - $this.RestoreNuGet() - if ($this.OnError()) { return } - $this.CompileBelle() - if ($this.OnError()) { return } - $this.CompileUmbraco() - if ($this.OnError()) { return } - $this.CompileJsonSchema() - if ($this.OnError()) { return } - $this.PrepareTests() - if ($this.OnError()) { return } - $this.CompileTests() - if ($this.OnError()) { return } - # not running tests - $this.PreparePackages() - if ($this.OnError()) { return } - $this.VerifyNuGet() - if ($this.OnError()) { return } - $this.PackageNuGet() - if ($this.OnError()) { return } - $this.PostPackageHook() - if ($this.OnError()) { return } - - Write-Host "Done" - }) - - $ubuild.DefineMethod("PostPackageHook", - { - # run hook - if ($this.HasMethod("PostPackage")) - { - Write-Host "Run PostPackage hook" - $this.PostPackage(); - if (-not $?) { throw "Failed to run hook." } - } - }) - - # ################################################################ - # RUN - # ################################################################ - - # configure - $ubuild.ReleaseBranches = @( "master" ) - - # run - if (-not $get) - { - if ($command.Length -eq 0) - { - $command = @( "Build" ) - } - $ubuild.RunMethod($command); - if ($ubuild.OnError()) { return } - } - if ($get) { return $ubuild } diff --git a/src/ApiDocs/docfx.filter.yml b/build/csharp-docs/docfx.filter.yml similarity index 100% rename from src/ApiDocs/docfx.filter.yml rename to build/csharp-docs/docfx.filter.yml diff --git a/src/ApiDocs/docfx.json b/build/csharp-docs/docfx.json similarity index 84% rename from src/ApiDocs/docfx.json rename to build/csharp-docs/docfx.json index e5f6dd7410..195f2a7fc3 100644 --- a/src/ApiDocs/docfx.json +++ b/build/csharp-docs/docfx.json @@ -3,18 +3,17 @@ { "src": [ { - "src": "../", + "src": "../../src", "files": [ - "**/*.csproj", - "**/Umbraco.Infrastructure/**/*.cs" + "**/*.csproj" ], "exclude": [ "**/obj/**", "**/bin/**", "**/Umbraco.Web.csproj", - "**/Umbraco.Infrastructure.csproj", "**/Umbraco.Web.UI.csproj", - "**/**.Test**/*.csproj" + "**/Umbraco.Cms.StaticAssets.csproj", + "**/JsonSchema.csproj" ] } ], diff --git a/src/ApiDocs/index.md b/build/csharp-docs/index.md similarity index 100% rename from src/ApiDocs/index.md rename to build/csharp-docs/index.md diff --git a/src/ApiDocs/toc.yml b/build/csharp-docs/toc.yml similarity index 100% rename from src/ApiDocs/toc.yml rename to build/csharp-docs/toc.yml diff --git a/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial b/build/csharp-docs/umbracotemplate/partials/class.tmpl.partial similarity index 100% rename from src/ApiDocs/umbracotemplate/partials/class.tmpl.partial rename to build/csharp-docs/umbracotemplate/partials/class.tmpl.partial diff --git a/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial b/build/csharp-docs/umbracotemplate/partials/footer.tmpl.partial similarity index 100% rename from src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial rename to build/csharp-docs/umbracotemplate/partials/footer.tmpl.partial diff --git a/src/ApiDocs/umbracotemplate/partials/head.tmpl.partial b/build/csharp-docs/umbracotemplate/partials/head.tmpl.partial similarity index 100% rename from src/ApiDocs/umbracotemplate/partials/head.tmpl.partial rename to build/csharp-docs/umbracotemplate/partials/head.tmpl.partial diff --git a/src/ApiDocs/umbracotemplate/partials/namespace.tmpl.partial b/build/csharp-docs/umbracotemplate/partials/namespace.tmpl.partial similarity index 100% rename from src/ApiDocs/umbracotemplate/partials/namespace.tmpl.partial rename to build/csharp-docs/umbracotemplate/partials/namespace.tmpl.partial diff --git a/src/ApiDocs/umbracotemplate/partials/navbar.tmpl.partial b/build/csharp-docs/umbracotemplate/partials/navbar.tmpl.partial similarity index 100% rename from src/ApiDocs/umbracotemplate/partials/navbar.tmpl.partial rename to build/csharp-docs/umbracotemplate/partials/navbar.tmpl.partial diff --git a/src/ApiDocs/umbracotemplate/partials/rest.tmpl.partial b/build/csharp-docs/umbracotemplate/partials/rest.tmpl.partial similarity index 100% rename from src/ApiDocs/umbracotemplate/partials/rest.tmpl.partial rename to build/csharp-docs/umbracotemplate/partials/rest.tmpl.partial diff --git a/src/ApiDocs/umbracotemplate/styles/main.css b/build/csharp-docs/umbracotemplate/styles/main.css similarity index 100% rename from src/ApiDocs/umbracotemplate/styles/main.css rename to build/csharp-docs/umbracotemplate/styles/main.css diff --git a/opened-issue-first-comment.yml b/opened-issue-first-comment.yml new file mode 100644 index 0000000000..134a368785 --- /dev/null +++ b/opened-issue-first-comment.yml @@ -0,0 +1,53 @@ +name: issue-first-response + +on: + issues: + types: [opened] + +jobs: + send-response: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Install dependencies + run: | + npm install node-fetch@2 + - name: Fetch random comment 🗣️ and add it to the issue + uses: actions/github-script@v6 + with: + script: | + const fetch = require('node-fetch') + + const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', { + method: 'post', + body: JSON.stringify({ + repo: '${{ github.repository }}', + number: '${{ github.event.number }}', + actor: '${{ github.actor }}', + commentType: 'opened-issue-first-comment' + }), + headers: { + 'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}', + 'Content-Type': 'application/json' + } + }); + + try { + const data = await response.text(); + + if(response.status === 200 && data !== '') { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: data + }); + } else { + console.log("Status code did not indicate success:", response.status); + console.log("Returned data:", data); + } + } catch(error) { + console.log(error); + } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6eaa51e431..ce54e08edd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - + 10.0.0 @@ -42,4 +42,12 @@ true + + + + true + 10.0.0 + true + true + diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index e80666038c..fb9db8387a 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -35,9 +35,8 @@ namespace JsonSchema /// public class CmsDefinition { - public ActiveDirectorySettings? ActiveDirectory { get; set; } - public ContentSettings? Content { get; set; } + public CoreDebugSettings? Debug { get; set; } public ExceptionFilterSettings? ExceptionFilter { get; set; } @@ -94,6 +93,8 @@ namespace JsonSchema public HelpPageSettings? HelpPage { get; set; } public InstallDefaultDataSettings? DefaultDataCreation { get; set; } + + public DataTypesSettings? DataTypes { get; set; } } /// diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 97608c756b..ea0ce9b7c3 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -4,16 +4,20 @@ net6.0 true false + false - - + + - + + + 3.5.107 + @@ -31,5 +35,4 @@ - diff --git a/src/JsonSchema/Options.cs b/src/JsonSchema/Options.cs index 83a2a8ef94..4471ee49ce 100644 --- a/src/JsonSchema/Options.cs +++ b/src/JsonSchema/Options.cs @@ -7,7 +7,7 @@ namespace JsonSchema { internal class Options { - [Option('o', "outputFile", Required = false, HelpText = "Set path of the output file.", Default = "../../../../Umbraco.Web.UI/umbraco/config/appsettings-schema.json")] + [Option('o', "outputFile", Required = false, HelpText = "Set path of the output file.", Default = "../../../../Umbraco.Web.UI/appsettings-schema.json")] public string OutputFile { get; set; } = null!; } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs index 19ec0738c8..ae16a9735f 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Persistence.SqlServer; /// -/// Constants related to SQLite. +/// Constants related to SQLite. /// public static class Constants { /// - /// SQLite provider name. + /// SQLite provider name. /// public const string ProviderName = "Microsoft.Data.SqlClient"; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs index 65bd0b5d65..0c09f87d51 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs @@ -1,25 +1,24 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class ColumnInSchemaDto { - internal class ColumnInSchemaDto - { - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("ORDINAL_POSITION")] - public int OrdinalPosition { get; set; } + [Column("ORDINAL_POSITION")] + public int OrdinalPosition { get; set; } - [Column("COLUMN_DEFAULT")] - public string ColumnDefault { get; set; } = null!; + [Column("COLUMN_DEFAULT")] + public string ColumnDefault { get; set; } = null!; - [Column("IS_NULLABLE")] - public string IsNullable { get; set; } = null!; + [Column("IS_NULLABLE")] + public string IsNullable { get; set; } = null!; - [Column("DATA_TYPE")] - public string DataType { get; set; } = null!; - } + [Column("DATA_TYPE")] + public string DataType { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs index 351979570c..b0299a489d 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs @@ -1,16 +1,15 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class ConstraintPerColumnDto { - internal class ConstraintPerColumnDto - { - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("CONSTRAINT_NAME")] - public string ConstraintName { get; set; } = null!; - } + [Column("CONSTRAINT_NAME")] + public string ConstraintName { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs index 3a633d4e0e..fe87ef2909 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs @@ -1,13 +1,12 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos -{ - internal class ConstraintPerTableDto - { - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; - [Column("CONSTRAINT_NAME")] - public string ConstraintName { get; set; } = null!; - } +internal class ConstraintPerTableDto +{ + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; + + [Column("CONSTRAINT_NAME")] + public string ConstraintName { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs index e0e1dfbe2f..a1bde415a3 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs @@ -1,18 +1,18 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class DefaultConstraintPerColumnDto { - internal class DefaultConstraintPerColumnDto - { - [Column("TABLE_NAME")] public string TableName { get; set; } = null!; + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("NAME")] - public string Name { get; set; } = null!; + [Column("NAME")] + public string Name { get; set; } = null!; - [Column("DEFINITION")] - public string Definition { get; set; } = null!; - } + [Column("DEFINITION")] + public string Definition { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs index e78f354e46..e85d91f1dd 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs @@ -1,20 +1,18 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class DefinedIndexDto { - internal class DefinedIndexDto - { + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; + [Column("INDEX_NAME")] + public string IndexName { get; set; } = null!; - [Column("INDEX_NAME")] - public string IndexName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; - - [Column("UNIQUE")] - public short Unique { get; set; } - } + [Column("UNIQUE")] + public short Unique { get; set; } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs index 7c5df6c497..43541ec2a3 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs @@ -1,11 +1,12 @@ using System.Data.Common; using NPoco; using StackExchange.Profiling; +using StackExchange.Profiling.Data; namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; public class SqlServerAddMiniProfilerInterceptor : SqlServerConnectionInterceptor { public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) - => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); + => new ProfiledDbConnection(conn, MiniProfiler.Current); } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs index bdf5745d42..139efea85f 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs @@ -21,8 +21,12 @@ public class SqlServerAddRetryPolicyInterceptor : SqlServerConnectionInterceptor return conn; } - RetryPolicy? connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); - RetryPolicy? commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); + RetryPolicy? connectionRetryPolicy = + RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionStrings.CurrentValue + .ConnectionString); + RetryPolicy? commandRetryPolicy = + RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionStrings.CurrentValue + .ConnectionString); if (connectionRetryPolicy == null && commandRetryPolicy == null) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs index e9784ce270..d74511bf11 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs @@ -3,1503 +3,1490 @@ using System.Data; using System.Data.Common; using System.Diagnostics; using System.Globalization; +using System.Text; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// A base implementation of that is suitable for +/// . +/// +/// +/// Borrowed from Microsoft: +/// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ +/// This implementation is designed to be very memory efficient requiring few memory resources and to support +/// rapid transfer of data to SQL Server. +/// Subclasses should implement , , +/// , , +/// . +/// If they contain disposable resources they should override . +/// SD: Alternatively, we could have used a LinqEntityDataReader which is nicer to use but it uses quite a lot of +/// reflection and +/// I thought this would just be quicker. +/// Simple example of that: +/// https://github.com/gridsum/DataflowEx/blob/master/Gridsum.DataflowEx/Databases/BulkDataReader.cs +/// Full example of that: +/// https://github.com/matthewschrager/Repository/blob/master/Repository.EntityFramework/EntityDataReader.cs +/// So we know where to find that if we ever need it, these would convert any Linq data source to an IDataReader +/// +internal abstract class BulkDataReader : IDataReader { + #region Fields + /// - /// A base implementation of that is suitable for . + /// The containing the input row set's schema information + /// + /// requires to function correctly. + /// + private DataTable? _schemaTable = new(); + + /// + /// The mapping from the row set input to the target table's columns. + /// + private List? _columnMappings = new(); + + #endregion + + #region Subclass utility routines + + /// + /// The mapping from the row set input to the target table's columns. /// /// - /// - /// Borrowed from Microsoft: - /// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ - /// - /// This implementation is designed to be very memory efficient requiring few memory resources and to support - /// rapid transfer of data to SQL Server. - /// - /// Subclasses should implement , , - /// , , . - /// If they contain disposable resources they should override . - /// - /// SD: Alternatively, we could have used a LinqEntityDataReader which is nicer to use but it uses quite a lot of reflection and - /// I thought this would just be quicker. - /// Simple example of that: https://github.com/gridsum/DataflowEx/blob/master/Gridsum.DataflowEx/Databases/BulkDataReader.cs - /// Full example of that: https://github.com/matthewschrager/Repository/blob/master/Repository.EntityFramework/EntityDataReader.cs - /// So we know where to find that if we ever need it, these would convert any Linq data source to an IDataReader - /// + /// If necessary, will be called to initialize the mapping. /// - internal abstract class BulkDataReader : IDataReader + public ReadOnlyCollection ColumnMappings { - - #region Fields - - /// - /// The containing the input row set's schema information - /// requires to function correctly. - /// - private DataTable? _schemaTable = new DataTable(); - - /// - /// The mapping from the row set input to the target table's columns. - /// - private List? _columnMappings = new List(); - - #endregion - - #region Subclass utility routines - - /// - /// The mapping from the row set input to the target table's columns. - /// - /// - /// If necessary, will be called to initialize the mapping. - /// - public ReadOnlyCollection ColumnMappings + get { - get + if (_columnMappings?.Count == 0) { - if (this._columnMappings?.Count == 0) + // Need to add the column definitions and mappings. + AddSchemaTableRows(); + + if (_columnMappings.Count == 0) { - // Need to add the column definitions and mappings. - AddSchemaTableRows(); - - if (this._columnMappings.Count == 0) - { - throw new InvalidOperationException("AddSchemaTableRows did not add rows."); - } - - Debug.Assert(this._schemaTable?.Rows.Count == FieldCount); + throw new InvalidOperationException("AddSchemaTableRows did not add rows."); } - return new ReadOnlyCollection(_columnMappings!); + Debug.Assert(_schemaTable?.Rows.Count == FieldCount); + } + + return new ReadOnlyCollection(_columnMappings!); + } + } + + /// + /// The name of the input row set's schema. + /// + /// + /// This may be different from the target schema but usually they are identical. + /// + protected abstract string SchemaName + { + get; + } + + /// + /// The name of the input row set's table. + /// + /// + /// This may be different from the target table but usually they are identical. + /// + protected abstract string TableName + { + get; + } + + /// + /// Adds the input row set's schema to the object. + /// + /// + /// Call + /// + /// to do this for each row. + /// + /// + protected abstract void AddSchemaTableRows(); + + /// + /// For each , the optional columns that may have values. + /// + /// + /// This is used for checking the parameters of + /// + /// . + /// + /// + private static readonly Dictionary> AllowedOptionalColumnCombinations = new() + { + {SqlDbType.BigInt, new List()}, + {SqlDbType.Binary, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Bit, new List()}, + {SqlDbType.Char, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Date, new List()}, + {SqlDbType.DateTime, new List()}, + {SqlDbType.DateTime2, new List {SchemaTableColumn.NumericPrecision}}, + {SqlDbType.DateTimeOffset, new List {SchemaTableColumn.NumericPrecision}}, + {SqlDbType.Decimal, new List {SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale}}, + {SqlDbType.Float, new List {SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale}}, + {SqlDbType.Image, new List()}, + {SqlDbType.Int, new List()}, + {SqlDbType.Money, new List()}, + {SqlDbType.NChar, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.NText, new List()}, + {SqlDbType.NVarChar, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Real, new List()}, + {SqlDbType.SmallDateTime, new List()}, + {SqlDbType.SmallInt, new List()}, + {SqlDbType.SmallMoney, new List()}, + {SqlDbType.Structured, new List()}, + {SqlDbType.Text, new List()}, + {SqlDbType.Time, new List {SchemaTableColumn.NumericPrecision}}, + {SqlDbType.Timestamp, new List()}, + {SqlDbType.TinyInt, new List()}, + {SqlDbType.Udt, new List {DataTypeNameSchemaColumn}}, + {SqlDbType.UniqueIdentifier, new List()}, + {SqlDbType.VarBinary, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.VarChar, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Variant, new List()}, + { + SqlDbType.Xml, + new List + { + XmlSchemaCollectionDatabaseSchemaColumn, + XmlSchemaCollectionOwningSchemaSchemaColumn, + XmlSchemaCollectionNameSchemaColumn } } + }; - /// - /// The name of the input row set's schema. - /// - /// - /// This may be different from the target schema but usually they are identical. - /// - protected abstract string SchemaName + /// + /// A helper method to support . + /// + /// + /// This methods does extensive argument checks. These errors will cause hard to diagnose exceptions in latter + /// processing so it is important to detect them when they can be easily associated with the code defect. + /// + /// + /// The combination of values for the parameters is not supported. + /// + /// + /// A null value for the parameter is not supported. + /// + /// + /// The name of the column. + /// + /// + /// The size of the column which may be null if not applicable. + /// + /// + /// The precision of the column which may be null if not applicable. + /// + /// + /// The scale of the column which may be null if not applicable. + /// + /// + /// Are the column values unique (i.e. never duplicated)? + /// + /// + /// Is the column part of the primary key? + /// + /// + /// Is the column nullable (i.e. optional)? + /// + /// + /// The corresponding . + /// + /// + /// The schema name of the UDT. + /// + /// + /// The type name of the UDT. + /// + /// + /// For XML columns the schema collection's database name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's schema name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's name. Otherwise, null. + /// + /// + protected void AddSchemaTableRow( + string columnName, + int? columnSize, + short? numericPrecision, + short? numericScale, + bool isUnique, + bool isKey, + bool allowDbNull, + SqlDbType providerType, + string? udtSchema, + string? udtType, + string? xmlSchemaCollectionDatabase, + string? xmlSchemaCollectionOwningSchema, + string? xmlSchemaCollectionName) + { + if (string.IsNullOrEmpty(columnName)) { - get; + throw new ArgumentException("columnName must be a nonempty string."); } - /// - /// The name of the input row set's table. - /// - /// - /// This may be different from the target table but usually they are identical. - /// - protected abstract string TableName + if (columnSize.HasValue && columnSize.Value <= 0) { - get; + throw new ArgumentOutOfRangeException("columnSize"); } - /// - /// Adds the input row set's schema to the object. - /// - /// - /// Call - /// to do this for each row. - /// - /// - protected abstract void AddSchemaTableRows(); - - /// - /// For each , the optional columns that may have values. - /// - /// - /// This is used for checking the parameters of . - /// - /// - private static readonly Dictionary> AllowedOptionalColumnCombinations = new Dictionary> + if (numericPrecision.HasValue && numericPrecision.Value <= 0) { - { SqlDbType.BigInt, new List { } }, - { SqlDbType.Binary, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Bit, new List { } }, - { SqlDbType.Char, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Date, new List { } }, - { SqlDbType.DateTime, new List { } }, - { SqlDbType.DateTime2, new List { SchemaTableColumn.NumericPrecision } }, - { SqlDbType.DateTimeOffset, new List { SchemaTableColumn.NumericPrecision } }, - { SqlDbType.Decimal, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, - { SqlDbType.Float, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, - { SqlDbType.Image, new List { } }, - { SqlDbType.Int, new List { } }, - { SqlDbType.Money, new List { } }, - { SqlDbType.NChar, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.NText, new List { } }, - { SqlDbType.NVarChar, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Real, new List { } }, - { SqlDbType.SmallDateTime, new List { } }, - { SqlDbType.SmallInt, new List { } }, - { SqlDbType.SmallMoney, new List { } }, - { SqlDbType.Structured, new List { } }, - { SqlDbType.Text, new List { } }, - { SqlDbType.Time, new List { SchemaTableColumn.NumericPrecision } }, - { SqlDbType.Timestamp, new List { } }, - { SqlDbType.TinyInt, new List { } }, - { SqlDbType.Udt, new List { BulkDataReader.DataTypeNameSchemaColumn } }, - { SqlDbType.UniqueIdentifier, new List { } }, - { SqlDbType.VarBinary, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.VarChar, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Variant, new List { } }, - { SqlDbType.Xml, new List { BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, BulkDataReader.XmlSchemaCollectionNameSchemaColumn } } - }; + throw new ArgumentOutOfRangeException("numericPrecision"); + } - /// - /// A helper method to support . - /// - /// - /// This methods does extensive argument checks. These errors will cause hard to diagnose exceptions in latter - /// processing so it is important to detect them when they can be easily associated with the code defect. - /// - /// - /// The combination of values for the parameters is not supported. - /// - /// - /// A null value for the parameter is not supported. - /// - /// - /// The name of the column. - /// - /// - /// The size of the column which may be null if not applicable. - /// - /// - /// The precision of the column which may be null if not applicable. - /// - /// - /// The scale of the column which may be null if not applicable. - /// - /// - /// Are the column values unique (i.e. never duplicated)? - /// - /// - /// Is the column part of the primary key? - /// - /// - /// Is the column nullable (i.e. optional)? - /// - /// - /// The corresponding . - /// - /// - /// The schema name of the UDT. - /// - /// - /// The type name of the UDT. - /// - /// - /// For XML columns the schema collection's database name. Otherwise, null. - /// - /// - /// For XML columns the schema collection's schema name. Otherwise, null. - /// - /// - /// For XML columns the schema collection's name. Otherwise, null. - /// - /// - protected void AddSchemaTableRow(string columnName, - int? columnSize, - short? numericPrecision, - short? numericScale, - bool isUnique, - bool isKey, - bool allowDbNull, - SqlDbType providerType, - string? udtSchema, - string? udtType, - string? xmlSchemaCollectionDatabase, - string? xmlSchemaCollectionOwningSchema, - string? xmlSchemaCollectionName) + if (numericScale.HasValue && numericScale.Value < 0) { - if (string.IsNullOrEmpty(columnName)) - { - throw new ArgumentException("columnName must be a nonempty string."); - } - else if (columnSize.HasValue && columnSize.Value <= 0) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - else if (numericPrecision.HasValue && numericPrecision.Value <= 0) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - else if (numericScale.HasValue && numericScale.Value < 0) - { - throw new ArgumentOutOfRangeException("columnSize"); - } + throw new ArgumentOutOfRangeException("columnSize"); + } - List? allowedOptionalColumnList; - if (BulkDataReader.AllowedOptionalColumnCombinations.TryGetValue(providerType, out allowedOptionalColumnList)) + if (AllowedOptionalColumnCombinations.TryGetValue(providerType, out List? allowedOptionalColumnList)) + { + if ((columnSize.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.ColumnSize)) || + (numericPrecision.HasValue && + !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericPrecision)) || + (numericScale.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericScale)) || + (udtSchema != null && !allowedOptionalColumnList.Contains(DataTypeNameSchemaColumn)) || + (udtType != null && !allowedOptionalColumnList.Contains(DataTypeNameSchemaColumn)) || + (xmlSchemaCollectionDatabase != null && + !allowedOptionalColumnList.Contains(XmlSchemaCollectionDatabaseSchemaColumn)) || + (xmlSchemaCollectionOwningSchema != null && + !allowedOptionalColumnList.Contains(XmlSchemaCollectionOwningSchemaSchemaColumn)) || + (xmlSchemaCollectionName != null && + !allowedOptionalColumnList.Contains(XmlSchemaCollectionNameSchemaColumn))) { - if ((columnSize.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.ColumnSize)) || - (numericPrecision.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericPrecision)) || - (numericScale.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericScale)) || - (udtSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || - (udtType != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || - (xmlSchemaCollectionDatabase != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn)) || - (xmlSchemaCollectionOwningSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn)) || - (xmlSchemaCollectionName != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionNameSchemaColumn))) + throw new ArgumentException("Columns are set that are incompatible with the value of providerType."); + } + } + else + { + throw new ArgumentException("providerType is unsupported."); + } + + Type dataType; // Corresponding CLR type. + string dataTypeName; // Corresponding SQL Server type. + var isLong = false; // Is the column a large value column (e.g. nvarchar(max))? + + switch (providerType) + { + case SqlDbType.BigInt: + dataType = typeof(long); + dataTypeName = "bigint"; + break; + + case SqlDbType.Binary: + dataType = typeof(byte[]); + + if (!columnSize.HasValue) { - throw new ArgumentException("Columns are set that are incompatible with the value of providerType."); + throw new ArgumentException("columnSize must be specified for \"binary\" type columns."); } - } - else - { - throw new ArgumentException("providerType is unsupported."); - } - Type dataType; // Corresponding CLR type. - string dataTypeName; // Corresponding SQL Server type. - bool isLong = false; // Is the column a large value column (e.g. nvarchar(max))? + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } - switch (providerType) - { - case SqlDbType.BigInt: - dataType = typeof(long); - dataTypeName = "bigint"; - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "binary({0})", + columnSize.Value); + break; - case SqlDbType.Binary: - dataType = typeof(byte[]); + case SqlDbType.Bit: + dataType = typeof(bool); + dataTypeName = "bit"; + break; - if (!columnSize.HasValue) - { - throw new ArgumentException("columnSize must be specified for \"binary\" type columns."); - } - else if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } + case SqlDbType.Char: + dataType = typeof(string); - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "binary({0})", - columnSize.Value); - break; + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"char\" type columns."); + } - case SqlDbType.Bit: - dataType = typeof(bool); - dataTypeName = "bit"; - break; + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } - case SqlDbType.Char: - dataType = typeof(string); + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "char({0})", + columnSize.Value); + break; - if (!columnSize.HasValue) - { - throw new ArgumentException("columnSize must be specified for \"char\" type columns."); - } - else if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } + case SqlDbType.Date: + dataType = typeof(DateTime); + dataTypeName = "date"; + break; - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "char({0})", - columnSize.Value); - break; + case SqlDbType.DateTime: + dataType = typeof(DateTime); + dataTypeName = "datetime"; + break; - case SqlDbType.Date: - dataType = typeof(DateTime); - dataTypeName = "date"; - break; + case SqlDbType.DateTime2: + dataType = typeof(DateTime); - case SqlDbType.DateTime: - dataType = typeof(DateTime); - dataTypeName = "datetime"; - break; - - case SqlDbType.DateTime2: - dataType = typeof(DateTime); - - if (numericPrecision.HasValue) - { - if (numericPrecision.Value > 7) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "datetime2({0})", - numericPrecision.Value); - } - else - { - dataTypeName = "datetime2"; - } - break; - - case SqlDbType.DateTimeOffset: - dataType = typeof(DateTimeOffset); - - if (numericPrecision.HasValue) - { - if (numericPrecision.Value > 7) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "datetimeoffset({0})", - numericPrecision.Value); - } - else - { - dataTypeName = "datetimeoffset"; - } - break; - - case SqlDbType.Decimal: - dataType = typeof(decimal); - - if (!numericPrecision.HasValue || !numericScale.HasValue) - { - throw new ArgumentException("numericPrecision and numericScale must be specified for \"decimal\" type columns."); - } - else if (numericPrecision > 38) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - else if (numericScale.Value > numericPrecision.Value) - { - throw new ArgumentException("numericScale must not be larger than numericPrecision for \"decimal\" type columns."); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "decimal({0}, {1})", - numericPrecision.Value, - numericScale.Value); - break; - - case SqlDbType.Float: - dataType = typeof(double); - - if (!numericPrecision.HasValue) - { - throw new ArgumentException("numericPrecision must be specified for \"float\" type columns"); - } - else if (numericPrecision > 53) + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) { throw new ArgumentOutOfRangeException("numericPrecision"); } - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "float({0})", - numericPrecision.Value); - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "datetime2({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetime2"; + } - case SqlDbType.Image: - dataType = typeof(byte[]); - dataTypeName = "image"; - break; + break; - case SqlDbType.Int: - dataType = typeof(int); - dataTypeName = "int"; - break; + case SqlDbType.DateTimeOffset: + dataType = typeof(DateTimeOffset); - case SqlDbType.Money: - dataType = typeof(decimal); - dataTypeName = "money"; - break; - - case SqlDbType.NChar: - dataType = typeof(string); - - if (!columnSize.HasValue) + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) { - throw new ArgumentException("columnSize must be specified for \"nchar\" type columns"); + throw new ArgumentOutOfRangeException("numericPrecision"); } - else if (columnSize > 4000) + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "datetimeoffset({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetimeoffset"; + } + + break; + + case SqlDbType.Decimal: + dataType = typeof(decimal); + + if (!numericPrecision.HasValue || !numericScale.HasValue) + { + throw new ArgumentException( + "numericPrecision and numericScale must be specified for \"decimal\" type columns."); + } + + if (numericPrecision > 38) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + if (numericScale.Value > numericPrecision.Value) + { + throw new ArgumentException( + "numericScale must not be larger than numericPrecision for \"decimal\" type columns."); + } + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "decimal({0}, {1})", + numericPrecision.Value, + numericScale.Value); + break; + + case SqlDbType.Float: + dataType = typeof(double); + + if (!numericPrecision.HasValue) + { + throw new ArgumentException("numericPrecision must be specified for \"float\" type columns"); + } + + if (numericPrecision > 53) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "float({0})", + numericPrecision.Value); + break; + + case SqlDbType.Image: + dataType = typeof(byte[]); + dataTypeName = "image"; + break; + + case SqlDbType.Int: + dataType = typeof(int); + dataTypeName = "int"; + break; + + case SqlDbType.Money: + dataType = typeof(decimal); + dataTypeName = "money"; + break; + + case SqlDbType.NChar: + dataType = typeof(string); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"nchar\" type columns"); + } + + if (columnSize > 4000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "nchar({0})", + columnSize.Value); + break; + + case SqlDbType.NText: + dataType = typeof(string); + dataTypeName = "ntext"; + break; + + case SqlDbType.NVarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 4000) { throw new ArgumentOutOfRangeException("columnSize"); } - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "nchar({0})", - columnSize.Value); - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "nvarchar({0})", + columnSize.Value); + } + else + { + isLong = true; - case SqlDbType.NText: - dataType = typeof(string); - dataTypeName = "ntext"; - break; + dataTypeName = "nvarchar(max)"; + } - case SqlDbType.NVarChar: - dataType = typeof(string); + break; - if (columnSize.HasValue) + case SqlDbType.Real: + dataType = typeof(float); + dataTypeName = "real"; + break; + + case SqlDbType.SmallDateTime: + dataType = typeof(DateTime); + dataTypeName = "smalldatetime"; + break; + + case SqlDbType.SmallInt: + dataType = typeof(short); + dataTypeName = "smallint"; + break; + + case SqlDbType.SmallMoney: + dataType = typeof(decimal); + dataTypeName = "smallmoney"; + break; + + // SqlDbType.Structured not supported because it related to nested rowsets. + + case SqlDbType.Text: + dataType = typeof(string); + dataTypeName = "text"; + break; + + case SqlDbType.Time: + dataType = typeof(TimeSpan); + + if (numericPrecision.HasValue) + { + if (numericPrecision > 7) { - if (columnSize > 4000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "nvarchar({0})", - columnSize.Value); - } - else - { - isLong = true; - - dataTypeName = "nvarchar(max)"; - } - break; - - case SqlDbType.Real: - dataType = typeof(float); - dataTypeName = "real"; - break; - - case SqlDbType.SmallDateTime: - dataType = typeof(DateTime); - dataTypeName = "smalldatetime"; - break; - - case SqlDbType.SmallInt: - dataType = typeof(short); - dataTypeName = "smallint"; - break; - - case SqlDbType.SmallMoney: - dataType = typeof(decimal); - dataTypeName = "smallmoney"; - break; - - // SqlDbType.Structured not supported because it related to nested rowsets. - - case SqlDbType.Text: - dataType = typeof(string); - dataTypeName = "text"; - break; - - case SqlDbType.Time: - dataType = typeof(TimeSpan); - - if (numericPrecision.HasValue) - { - if (numericPrecision > 7) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "time({0})", - numericPrecision.Value); - } - else - { - dataTypeName = "time"; - } - break; - - - // SqlDbType.Timestamp not supported because rowversions are not settable. - - case SqlDbType.TinyInt: - dataType = typeof(byte); - dataTypeName = "tinyint"; - break; - - case SqlDbType.Udt: - if (string.IsNullOrEmpty(udtSchema)) - { - throw new ArgumentException("udtSchema must be nonnull and nonempty for \"UDT\" columns."); - } - else if (string.IsNullOrEmpty(udtType)) - { - throw new ArgumentException("udtType must be nonnull and nonempty for \"UDT\" columns."); + throw new ArgumentOutOfRangeException("numericPrecision"); } - dataType = typeof(object); - using (SqlCommandBuilder commandBuilder = new SqlCommandBuilder()) + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "time({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "time"; + } + + break; + + + // SqlDbType.Timestamp not supported because rowversions are not settable. + + case SqlDbType.TinyInt: + dataType = typeof(byte); + dataTypeName = "tinyint"; + break; + + case SqlDbType.Udt: + if (string.IsNullOrEmpty(udtSchema)) + { + throw new ArgumentException("udtSchema must be nonnull and nonempty for \"UDT\" columns."); + } + + if (string.IsNullOrEmpty(udtType)) + { + throw new ArgumentException("udtType must be nonnull and nonempty for \"UDT\" columns."); + } + + dataType = typeof(object); + using (var commandBuilder = new SqlCommandBuilder()) + { + dataTypeName = commandBuilder.QuoteIdentifier(udtSchema) + "." + + commandBuilder.QuoteIdentifier(udtType); + } + + break; + + case SqlDbType.UniqueIdentifier: + dataType = typeof(Guid); + dataTypeName = "uniqueidentifier"; + break; + + case SqlDbType.VarBinary: + dataType = typeof(byte[]); + + if (columnSize.HasValue) + { + if (columnSize > 8000) { - dataTypeName = commandBuilder.QuoteIdentifier(udtSchema) + "." + commandBuilder.QuoteIdentifier(udtType); + throw new ArgumentOutOfRangeException("columnSize"); } - break; - case SqlDbType.UniqueIdentifier: - dataType = typeof(Guid); - dataTypeName = "uniqueidentifier"; - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "varbinary({0})", + columnSize.Value); + } + else + { + isLong = true; - case SqlDbType.VarBinary: - dataType = typeof(byte[]); + dataTypeName = "varbinary(max)"; + } - if (columnSize.HasValue) + break; + + case SqlDbType.VarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 8000) { - if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "varbinary({0})", - columnSize.Value); + throw new ArgumentOutOfRangeException("columnSize"); } - else + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "varchar({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "varchar(max)"; + } + + break; + + case SqlDbType.Variant: + dataType = typeof(object); + dataTypeName = "sql_variant"; + break; + + case SqlDbType.Xml: + dataType = typeof(string); + + if (xmlSchemaCollectionName == null) + { + if (xmlSchemaCollectionDatabase != null || xmlSchemaCollectionOwningSchema != null) { - isLong = true; - - dataTypeName = "varbinary(max)"; + throw new ArgumentException( + "xmlSchemaCollectionDatabase and xmlSchemaCollectionOwningSchema must be null if xmlSchemaCollectionName is null for \"xml\" columns."); } - break; - case SqlDbType.VarChar: - dataType = typeof(string); - - if (columnSize.HasValue) + dataTypeName = "xml"; + } + else + { + if (xmlSchemaCollectionName.Length == 0) { - if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "varchar({0})", - columnSize.Value); + throw new ArgumentException( + "xmlSchemaCollectionName must be nonempty or null for \"xml\" columns."); } - else + + if (xmlSchemaCollectionDatabase != null && + xmlSchemaCollectionDatabase.Length == 0) { - isLong = true; - - dataTypeName = "varchar(max)"; + throw new ArgumentException( + "xmlSchemaCollectionDatabase must be null or nonempty for \"xml\" columns."); } - break; - case SqlDbType.Variant: - dataType = typeof(object); - dataTypeName = "sql_variant"; - break; - - case SqlDbType.Xml: - dataType = typeof(string); - - if (xmlSchemaCollectionName == null) + if (xmlSchemaCollectionOwningSchema != null && + xmlSchemaCollectionOwningSchema.Length == 0) { - if (xmlSchemaCollectionDatabase != null || xmlSchemaCollectionOwningSchema != null) - { - throw new ArgumentException("xmlSchemaCollectionDatabase and xmlSchemaCollectionOwningSchema must be null if xmlSchemaCollectionName is null for \"xml\" columns."); - } - - dataTypeName = "xml"; + throw new ArgumentException( + "xmlSchemaCollectionOwningSchema must be null or nonempty for \"xml\" columns."); } - else + + var schemaCollection = new StringBuilder("xml("); + + if (xmlSchemaCollectionDatabase != null) { - if (xmlSchemaCollectionName.Length == 0) - { - throw new ArgumentException("xmlSchemaCollectionName must be nonempty or null for \"xml\" columns."); - } - else if (xmlSchemaCollectionDatabase != null && - xmlSchemaCollectionDatabase.Length == 0) - { - throw new ArgumentException("xmlSchemaCollectionDatabase must be null or nonempty for \"xml\" columns."); - } - else if (xmlSchemaCollectionOwningSchema != null && - xmlSchemaCollectionOwningSchema.Length == 0) - { - throw new ArgumentException("xmlSchemaCollectionOwningSchema must be null or nonempty for \"xml\" columns."); - } - - System.Text.StringBuilder schemaCollection = new System.Text.StringBuilder("xml("); - - if (xmlSchemaCollectionDatabase != null) - { - schemaCollection.Append("[" + xmlSchemaCollectionDatabase + "]"); - } - - schemaCollection.Append("[" + (xmlSchemaCollectionOwningSchema == null ? SchemaName : xmlSchemaCollectionOwningSchema) + "]"); - schemaCollection.Append("[" + xmlSchemaCollectionName + "]"); - - dataTypeName = schemaCollection.ToString(); + schemaCollection.Append("[" + xmlSchemaCollectionDatabase + "]"); } - break; - default: - throw new ArgumentOutOfRangeException("providerType"); + schemaCollection.Append("[" + (xmlSchemaCollectionOwningSchema ?? SchemaName) + "]"); + schemaCollection.Append("[" + xmlSchemaCollectionName + "]"); - } + dataTypeName = schemaCollection.ToString(); + } - this._schemaTable?.Rows.Add(columnName, - _schemaTable.Rows.Count, - columnSize, - numericPrecision, - numericScale, - isUnique, - isKey, - "TraceServer", - "TraceWarehouse", - columnName, - SchemaName, - TableName, - dataType, - allowDbNull, - providerType, - false, // isAliased - false, // isExpression - false, // isIdentity, - false, // isAutoIncrement, - false, // isRowVersion, - false, // isHidden, - isLong, - true, // isReadOnly, - dataType, - dataTypeName, - xmlSchemaCollectionDatabase, - xmlSchemaCollectionOwningSchema, - xmlSchemaCollectionName); + break; - this._columnMappings?.Add(new SqlBulkCopyColumnMapping(columnName, columnName)); + default: + throw new ArgumentOutOfRangeException("providerType"); } - #endregion + _schemaTable?.Rows.Add( + columnName, + _schemaTable.Rows.Count, + columnSize, + numericPrecision, + numericScale, + isUnique, + isKey, + "TraceServer", + "TraceWarehouse", + columnName, + SchemaName, + TableName, + dataType, + allowDbNull, + providerType, + false, // isAliased + false, // isExpression + false, // isIdentity, + false, // isAutoIncrement, + false, // isRowVersion, + false, // isHidden, + isLong, + true, // isReadOnly, + dataType, + dataTypeName, + xmlSchemaCollectionDatabase, + xmlSchemaCollectionOwningSchema, + xmlSchemaCollectionName); - #region Constructors + _columnMappings?.Add(new SqlBulkCopyColumnMapping(columnName, columnName)); + } - private const string IsIdentitySchemaColumn = "IsIdentity"; + #endregion - private const string DataTypeNameSchemaColumn = "DataTypeName"; + #region Constructors - private const string XmlSchemaCollectionDatabaseSchemaColumn = "XmlSchemaCollectionDatabase"; + private const string IsIdentitySchemaColumn = "IsIdentity"; - private const string XmlSchemaCollectionOwningSchemaSchemaColumn = "XmlSchemaCollectionOwningSchema"; + private const string DataTypeNameSchemaColumn = "DataTypeName"; - private const string XmlSchemaCollectionNameSchemaColumn = "XmlSchemaCollectionName"; + private const string XmlSchemaCollectionDatabaseSchemaColumn = "XmlSchemaCollectionDatabase"; - /// - /// Constructor. - /// - protected BulkDataReader() + private const string XmlSchemaCollectionOwningSchemaSchemaColumn = "XmlSchemaCollectionOwningSchema"; + + private const string XmlSchemaCollectionNameSchemaColumn = "XmlSchemaCollectionName"; + + /// + /// Constructor. + /// + protected BulkDataReader() + { + _schemaTable.Locale = CultureInfo.InvariantCulture; + + DataColumnCollection columns = _schemaTable.Columns; + + columns.Add(SchemaTableColumn.ColumnName, typeof(string)); + columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)); + columns.Add(SchemaTableColumn.ColumnSize, typeof(int)); + columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)); + columns.Add(SchemaTableColumn.NumericScale, typeof(short)); + columns.Add(SchemaTableColumn.IsUnique, typeof(bool)); + columns.Add(SchemaTableColumn.IsKey, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)); + columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)); + columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)); + columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)); + columns.Add(SchemaTableColumn.BaseTableName, typeof(string)); + columns.Add(SchemaTableColumn.DataType, typeof(Type)); + columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)); + columns.Add(SchemaTableColumn.ProviderType, typeof(int)); + columns.Add(SchemaTableColumn.IsAliased, typeof(bool)); + columns.Add(SchemaTableColumn.IsExpression, typeof(bool)); + columns.Add(IsIdentitySchemaColumn, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)); + columns.Add(SchemaTableColumn.IsLong, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type)); + columns.Add(DataTypeNameSchemaColumn, typeof(string)); + columns.Add(XmlSchemaCollectionDatabaseSchemaColumn, typeof(string)); + columns.Add(XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(string)); + columns.Add(XmlSchemaCollectionNameSchemaColumn, typeof(string)); + } + + #endregion + + #region IDataReader + + /// + /// Gets a value indicating the depth of nesting for the current row. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns 0. + /// + /// + public int Depth => 0; + + /// + /// Gets the number of columns in the current row. (Inherited from .) + /// + /// + public int FieldCount => GetSchemaTable().Rows.Count; + + /// + /// Is the bulk copy process open? + /// + private bool _isOpen = true; + + /// + /// Gets a value indicating whether the data reader is closed. (Inherited from .) + /// + /// + public bool IsClosed => !_isOpen; + + /// + /// Gets the column located at the specified index. (Inherited from .) + /// + /// + /// No column with the specified index was found. + /// + /// + /// The zero-based index of the column to get. + /// + /// + /// The column located at the specified index as an . + /// + /// + public object this[int i] => GetValue(i); + + /// + /// Gets the column with the specified name. (Inherited from .) + /// + /// + /// No column with the specified name was found. + /// + /// + /// The name of the column to find. + /// + /// + /// The column located at the specified name as an . + /// + /// + public object this[string name] => GetValue(GetOrdinal(name)); + + /// + /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. (Inherited from + /// .) + /// + /// + /// Always returns -1 which is the expected behaviour for statements. + /// + /// + public virtual int RecordsAffected => -1; + + /// + /// Closes the . (Inherited from .) + /// + /// + public void Close() => _isOpen = false; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public bool GetBoolean(int i) => (bool)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public byte GetByte(int i) => (byte)GetValue(i); + + /// + /// Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer + /// offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of bytes. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of bytes to read. + /// + /// + /// The actual number of bytes read. + /// + /// + public long GetBytes( + int i, + long fieldOffset, + byte[]? buffer, + int bufferoffset, + int length) + { + var data = (byte[])GetValue(i); + + if (buffer != null) { - this._schemaTable.Locale = System.Globalization.CultureInfo.InvariantCulture; - - DataColumnCollection columns = _schemaTable.Columns; - - columns.Add(SchemaTableColumn.ColumnName, typeof(string)); - columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)); - columns.Add(SchemaTableColumn.ColumnSize, typeof(int)); - columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)); - columns.Add(SchemaTableColumn.NumericScale, typeof(short)); - columns.Add(SchemaTableColumn.IsUnique, typeof(bool)); - columns.Add(SchemaTableColumn.IsKey, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)); - columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)); - columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)); - columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)); - columns.Add(SchemaTableColumn.BaseTableName, typeof(string)); - columns.Add(SchemaTableColumn.DataType, typeof(Type)); - columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)); - columns.Add(SchemaTableColumn.ProviderType, typeof(int)); - columns.Add(SchemaTableColumn.IsAliased, typeof(bool)); - columns.Add(SchemaTableColumn.IsExpression, typeof(bool)); - columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)); - columns.Add(SchemaTableColumn.IsLong, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type)); - columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(string)); - columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(string)); - columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(string)); - columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(string)); + Array.Copy(data, fieldOffset, buffer, bufferoffset, length); } - #endregion + return data.LongLength; + } - #region IDataReader + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public char GetChar(int i) + { + char result; - /// - /// Gets a value indicating the depth of nesting for the current row. (Inherited from .) - /// - /// - /// does not support nested result sets so this method always returns 0. - /// - /// - public int Depth + var data = GetValue(i); + var dataAsChar = data as char?; + + if (dataAsChar.HasValue) { - get { return 0; } + result = dataAsChar.Value; + } + else if (data is char[] dataAsCharArray && + dataAsCharArray.Length == 1) + { + result = dataAsCharArray[0]; + } + else if (data is string dataAsString && + dataAsString.Length == 1) + { + result = dataAsString[0]; + } + else + { + throw new InvalidOperationException("GetValue did not return a Char compatible type."); } - /// - /// Gets the number of columns in the current row. (Inherited from .) - /// - /// - public int FieldCount + return result; + } + + /// + /// Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given + /// buffer offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of characters. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of characters to read. + /// + /// + /// The actual number of characters read. + /// + /// + public long GetChars( + int i, + long fieldoffset, + char[]? buffer, + int bufferoffset, + int length) + { + var data = GetValue(i); + + var dataAsCharArray = data as char[]; + + if (data is string dataAsString) { - get { return GetSchemaTable().Rows.Count; } + dataAsCharArray = dataAsString.ToCharArray((int)fieldoffset, length); + } + else if (dataAsCharArray == null) + { + throw new InvalidOperationException("GetValue did not return either a Char array or a String."); } - /// - /// Is the bulk copy process open? - /// - bool _isOpen = true; - - /// - /// Gets a value indicating whether the data reader is closed. (Inherited from .) - /// - /// - public bool IsClosed + if (buffer != null) { - get { return !_isOpen; } + Array.Copy(dataAsCharArray, fieldoffset, buffer, bufferoffset, length); } - /// - /// Gets the column located at the specified index. (Inherited from .) - /// - /// - /// No column with the specified index was found. - /// - /// - /// The zero-based index of the column to get. - /// - /// - /// The column located at the specified index as an . - /// - /// - public object this[int i] + return dataAsCharArray.LongLength; + } + + /// + /// Returns an IDataReader for the specified column ordinal. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns null. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The for the specified column ordinal (null). + /// + /// + public IDataReader GetData(int i) + { + if (i < 0 || i >= FieldCount) { - get { return GetValue(i); } + throw new ArgumentOutOfRangeException("i"); } - /// - /// Gets the column with the specified name. (Inherited from .) - /// - /// - /// No column with the specified name was found. - /// - /// - /// The name of the column to find. - /// - /// - /// The column located at the specified name as an . - /// - /// - public object this[string name] + return null!; + } + + /// + /// The data type information for the specified field. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The data type information for the specified field. + /// + /// + public string GetDataTypeName(int i) => GetFieldType(i).Name; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public DateTime GetDateTime(int i) => (DateTime)GetValue(i); + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public DateTimeOffset GetDateTimeOffset(int i) => (DateTimeOffset)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public decimal GetDecimal(int i) => (decimal)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public double GetDouble(int i) => (double)GetValue(i); + + /// + /// Gets the information corresponding to the type of that would be returned + /// from . + /// (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The information corresponding to the type of that would be returned from + /// . + /// + /// + public Type GetFieldType(int i) => (Type)GetSchemaTable().Rows[i][SchemaTableColumn.DataType]; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public float GetFloat(int i) => (float)this[i]; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public Guid GetGuid(int i) => (Guid)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public short GetInt16(int i) => (short)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public int GetInt32(int i) => (int)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public long GetInt64(int i) => (long)GetValue(i); + + /// + /// Gets the name for the field to find. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The name of the field or the empty string (""), if there is no value to return. + /// + /// + public string GetName(int i) => (string)GetSchemaTable().Rows[i][SchemaTableColumn.ColumnName]; + + /// + /// Return the index of the named field. (Inherited from .) + /// + /// + /// The index of the named field was not found. + /// + /// + /// The name of the field to find. + /// + /// + /// The index of the named field. + /// + /// + public int GetOrdinal(string name) + { + if (name == null) // Empty strings are handled as a IndexOutOfRangeException. { - get { return GetValue(GetOrdinal(name)); } + throw new ArgumentNullException("name"); } - /// - /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. (Inherited from .) - /// - /// - /// Always returns -1 which is the expected behaviour for statements. - /// - /// - public virtual int RecordsAffected - { - get { return -1; } - } + var result = -1; - /// - /// Closes the . (Inherited from .) - /// - /// - public void Close() - { - this._isOpen = false; - } + var rowCount = FieldCount; - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public bool GetBoolean(int i) - { - return (bool)GetValue(i); - } + DataRowCollection schemaRows = GetSchemaTable().Rows; - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public byte GetByte(int i) + // Case sensitive search + for (var ordinal = 0; ordinal < rowCount; ordinal++) { - return (byte)GetValue(i); - } - - /// - /// Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer offset. - /// (Inherited from .) - /// - /// - /// If you pass a buffer that is null, returns the length of the row in bytes. - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The index within the field from which to start the read operation. - /// - /// - /// The buffer into which to read the stream of bytes. - /// - /// - /// The index for buffer to start the read operation. - /// - /// - /// The number of bytes to read. - /// - /// - /// The actual number of bytes read. - /// - /// - public long GetBytes(int i, - long fieldOffset, - byte[]? buffer, - int bufferoffset, - int length) - { - byte[] data = (byte[])GetValue(i); - - if (buffer != null) + if (string.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.Ordinal)) { - Array.Copy(data, fieldOffset, buffer, bufferoffset, length); + result = ordinal; } - - return data.LongLength; } - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public char GetChar(int i) + if (result == -1) { - char result; - - object data = GetValue(i); - char? dataAsChar = data as char?; - char[]? dataAsCharArray = data as char[]; - string? dataAsString = data as string; - - if (dataAsChar.HasValue) + // Case insensitive search. + for (var ordinal = 0; ordinal < rowCount; ordinal++) { - result = dataAsChar.Value; - } - else if (dataAsCharArray != null && - dataAsCharArray.Length == 1) - { - result = dataAsCharArray[0]; - } - else if (dataAsString != null && - dataAsString.Length == 1) - { - result = dataAsString[0]; - } - else - { - throw new InvalidOperationException("GetValue did not return a Char compatible type."); - } - - return result; - } - - /// - /// Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given buffer offset. - /// (Inherited from .) - /// - /// - /// If you pass a buffer that is null, returns the length of the row in bytes. - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The index within the field from which to start the read operation. - /// - /// - /// The buffer into which to read the stream of characters. - /// - /// - /// The index for buffer to start the read operation. - /// - /// - /// The number of characters to read. - /// - /// - /// The actual number of characters read. - /// - /// - public long GetChars(int i, - long fieldoffset, - char[]? buffer, - int bufferoffset, - int length) - { - object data = GetValue(i); - - string? dataAsString = data as string; - char[]? dataAsCharArray = data as char[]; - - if (dataAsString != null) - { - dataAsCharArray = dataAsString.ToCharArray((int)fieldoffset, length); - } - else if (dataAsCharArray == null) - { - throw new InvalidOperationException("GetValue did not return either a Char array or a String."); - } - - if (buffer != null) - { - Array.Copy(dataAsCharArray, fieldoffset, buffer, bufferoffset, length); - } - - return dataAsCharArray.LongLength; - } - - /// - /// Returns an IDataReader for the specified column ordinal. (Inherited from .) - /// - /// - /// does not support nested result sets so this method always returns null. - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The for the specified column ordinal (null). - /// - /// - public IDataReader GetData(int i) - { - if (i < 0 || i >= this.FieldCount) - { - throw new ArgumentOutOfRangeException("i"); - } - - return null!; - } - - /// - /// The data type information for the specified field. (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The data type information for the specified field. - /// - /// - public string GetDataTypeName(int i) - { - return GetFieldType(i).Name; - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public DateTime GetDateTime(int i) - { - return (DateTime)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - public DateTimeOffset GetDateTimeOffset(int i) - { - return (DateTimeOffset)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public decimal GetDecimal(int i) - { - return (decimal)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public double GetDouble(int i) - { - return (double)GetValue(i); - } - - /// - /// Gets the information corresponding to the type of that would be returned from . - /// (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The information corresponding to the type of that would be returned from . - /// - /// - public Type GetFieldType(int i) - { - return (Type)GetSchemaTable().Rows[i][SchemaTableColumn.DataType]; - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public float GetFloat(int i) - { - return (float)this[i]; - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public Guid GetGuid(int i) - { - return (Guid)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public short GetInt16(int i) - { - return (short)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public int GetInt32(int i) - { - return (int)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public long GetInt64(int i) - { - return (long)GetValue(i); - } - - /// - /// Gets the name for the field to find. (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The name of the field or the empty string (""), if there is no value to return. - /// - /// - public string GetName(int i) - { - return (string)GetSchemaTable().Rows[i][SchemaTableColumn.ColumnName]; - } - - /// - /// Return the index of the named field. (Inherited from .) - /// - /// - /// The index of the named field was not found. - /// - /// - /// The name of the field to find. - /// - /// - /// The index of the named field. - /// - /// - public int GetOrdinal(string name) - { - if (name == null) // Empty strings are handled as a IndexOutOfRangeException. - { - throw new ArgumentNullException("name"); - } - - int result = -1; - - int rowCount = FieldCount; - - DataRowCollection schemaRows = GetSchemaTable().Rows; - - // Case sensitive search - for (int ordinal = 0; ordinal < rowCount; ordinal++) - { - if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.Ordinal)) + if (string.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.OrdinalIgnoreCase)) { result = ordinal; } } - - if (result == -1) - { - // Case insensitive search. - for (int ordinal = 0; ordinal < rowCount; ordinal++) - { - if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.OrdinalIgnoreCase)) - { - result = ordinal; - } - } - } - - if (result == -1) - { - throw new IndexOutOfRangeException(name); - } - - return result; } - /// - /// Returns a that describes the column metadata of the . (Inherited from .) - /// - /// - /// The is closed. - /// - /// - /// A that describes the column metadata. - /// - /// - public DataTable GetSchemaTable() + if (result == -1) { - if (IsClosed) - { - throw new InvalidOperationException("The IDataReader is closed."); - } - - if (_schemaTable?.Rows.Count == 0) - { - // Need to add the column definitions and mappings - _schemaTable.TableName = TableName; - - AddSchemaTableRows(); - - Debug.Assert(_schemaTable.Rows.Count == FieldCount); - } - - return _schemaTable!; + throw new IndexOutOfRangeException(name); } - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public string GetString(int i) - { - return (string)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - public TimeSpan GetTimeSpan(int i) - { - return (TimeSpan)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public abstract object GetValue(int i); - - /// - /// Populates an array of objects with the column values of the current record. (Inherited from .) - /// - /// - /// was null. - /// - /// - /// An array of to copy the attribute fields into. - /// - /// - /// The number of instances of in the array. - /// - /// - public int GetValues(object[] values) - { - if (values == null) - { - throw new ArgumentNullException("values"); - } - - int fieldCount = Math.Min(FieldCount, values.Length); - - for (int i = 0; i < fieldCount; i++) - { - values[i] = GetValue(i); - } - - return fieldCount; - } - - /// - /// Return whether the specified field is set to null. (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// True if the specified field is set to null; otherwise, false. - /// - /// - public bool IsDBNull(int i) - { - object data = GetValue(i); - - return data == null || Convert.IsDBNull(data); - } - - /// - /// Advances the data reader to the next result, when reading the results of batch SQL statements. (Inherited from .) - /// - /// - /// for returns a single result set so false is always returned. - /// - /// - /// True if there are more rows; otherwise, false. for returns a single result set so false is always returned. - /// - /// - public bool NextResult() - { - return false; - } - - /// - /// Advances the to the next record. (Inherited from .) - /// - /// - /// True if there are more rows; otherwise, false. - /// - /// - public abstract bool Read(); - - #endregion - - #region IDisposable - - /// - /// Has the object been disposed? - /// - bool _disposed = false; - - /// - /// Dispose of any disposable and expensive resources. - /// - /// - /// Is this call the result of a call? - /// - protected virtual void Dispose(bool disposing) - { - if (!this._disposed) - { - this._disposed = true; - - if (disposing) - { - if (_schemaTable != null) - { - _schemaTable.Dispose(); - this._schemaTable = null; - } - - this._columnMappings = null; - - this._isOpen = false; - - GC.SuppressFinalize(this); - } - } - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. (Inherited from .) - /// - /// - public void Dispose() - { - Dispose(true); - } - - /// - /// Finalizer - /// - /// - /// has no unmanaged resources but a subclass may thus a finalizer is required. - /// - ~BulkDataReader() - { - Dispose(false); - } - - #endregion - + return result; } + + /// + /// Returns a that describes the column metadata of the . (Inherited + /// from .) + /// + /// + /// The is closed. + /// + /// + /// A that describes the column metadata. + /// + /// + public DataTable GetSchemaTable() + { + if (IsClosed) + { + throw new InvalidOperationException("The IDataReader is closed."); + } + + if (_schemaTable?.Rows.Count == 0) + { + // Need to add the column definitions and mappings + _schemaTable.TableName = TableName; + + AddSchemaTableRows(); + + Debug.Assert(_schemaTable.Rows.Count == FieldCount); + } + + return _schemaTable!; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public string GetString(int i) => (string)GetValue(i); + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public TimeSpan GetTimeSpan(int i) => (TimeSpan)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public abstract object GetValue(int i); + + /// + /// Populates an array of objects with the column values of the current record. (Inherited from + /// .) + /// + /// + /// was null. + /// + /// + /// An array of to copy the attribute fields into. + /// + /// + /// The number of instances of in the array. + /// + /// + public int GetValues(object[] values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + var fieldCount = Math.Min(FieldCount, values.Length); + + for (var i = 0; i < fieldCount; i++) + { + values[i] = GetValue(i); + } + + return fieldCount; + } + + /// + /// Return whether the specified field is set to null. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// True if the specified field is set to null; otherwise, false. + /// + /// + public bool IsDBNull(int i) + { + var data = GetValue(i); + + return data == null || Convert.IsDBNull(data); + } + + /// + /// Advances the data reader to the next result, when reading the results of batch SQL statements. (Inherited from + /// .) + /// + /// + /// for returns a single result set so false is always returned. + /// + /// + /// True if there are more rows; otherwise, false. for returns a + /// single result set so false is always returned. + /// + /// + public bool NextResult() => false; + + /// + /// Advances the to the next record. (Inherited from .) + /// + /// + /// True if there are more rows; otherwise, false. + /// + /// + public abstract bool Read(); + + #endregion + + #region IDisposable + + /// + /// Has the object been disposed? + /// + private bool _disposed; + + /// + /// Dispose of any disposable and expensive resources. + /// + /// + /// Is this call the result of a call? + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + + if (disposing) + { + if (_schemaTable != null) + { + _schemaTable.Dispose(); + _schemaTable = null; + } + + _columnMappings = null; + + _isOpen = false; + + GC.SuppressFinalize(this); + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. (Inherited + /// from .) + /// + /// + public void Dispose() => Dispose(true); + + /// + /// Finalizer + /// + /// + /// has no unmanaged resources but a subclass may thus a finalizer is required. + /// + ~BulkDataReader() => Dispose(false); + + #endregion } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs index 4856e1e117..7256317c15 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs @@ -6,216 +6,218 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Abstract class for defining MS sql implementations +/// +/// +public abstract class MicrosoftSqlSyntaxProviderBase : SqlSyntaxProviderBase + where TSyntax : ISqlSyntaxProvider { - /// - /// Abstract class for defining MS sql implementations - /// - /// - public abstract class MicrosoftSqlSyntaxProviderBase : SqlSyntaxProviderBase - where TSyntax : ISqlSyntaxProvider + private readonly ILogger _logger; + + protected MicrosoftSqlSyntaxProviderBase() { - private readonly ILogger _logger; + _logger = StaticApplicationLogging.CreateLogger(); - protected MicrosoftSqlSyntaxProviderBase() + AutoIncrementDefinition = "IDENTITY(1,1)"; + GuidColumnDefinition = "UniqueIdentifier"; + RealColumnDefinition = "FLOAT"; + BoolColumnDefinition = "BIT"; + DecimalColumnDefinition = "DECIMAL(38,6)"; + TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ + BlobColumnDefinition = "VARBINARY(MAX)"; + } + + public override string RenameTable => "sp_rename '{0}', '{1}'"; + + public override string AddColumn => "ALTER TABLE {0} ADD {1}"; + + public override string GetQuotedTableName(string? tableName) + { + if (tableName?.Contains(".") == false) { - _logger = StaticApplicationLogging.CreateLogger(); - - AutoIncrementDefinition = "IDENTITY(1,1)"; - GuidColumnDefinition = "UniqueIdentifier"; - RealColumnDefinition = "FLOAT"; - BoolColumnDefinition = "BIT"; - DecimalColumnDefinition = "DECIMAL(38,6)"; - TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ - BlobColumnDefinition = "VARBINARY(MAX)"; + return $"[{tableName}]"; } - public override string RenameTable => "sp_rename '{0}', '{1}'"; + var tableNameParts = tableName?.Split(Core.Constants.CharArrays.Period, 2); + return $"[{tableNameParts?[0]}].[{tableNameParts?[1]}]"; + } - public override string AddColumn => "ALTER TABLE {0} ADD {1}"; + public override string GetQuotedColumnName(string? columnName) => $"[{columnName}]"; - public override string GetQuotedTableName(string? tableName) + public override string GetQuotedName(string? name) => $"[{name}]"; + + public override string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + { + switch (columnType) { - if (tableName?.Contains(".") == false) - return $"[{tableName}]"; + case TextColumnType.NVarchar: + return base.GetStringColumnEqualComparison(column, paramIndex, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for = comparison with NText columns but allows this syntax + return $"{column} LIKE @{paramIndex}"; + default: + throw new ArgumentOutOfRangeException(nameof(columnType)); + } + } - var tableNameParts = tableName?.Split(Cms.Core.Constants.CharArrays.Period, 2); - return $"[{tableNameParts?[0]}].[{tableNameParts?[1]}]"; + public override string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnWildcardComparison(column, paramIndex, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return $"{column} LIKE @{paramIndex}"; + default: + throw new ArgumentOutOfRangeException(nameof(columnType)); + } + } + + /// + /// This uses a the DbTypeMap created and custom mapping to resolve the SqlDbType + /// + /// + /// + public virtual SqlDbType GetSqlDbType(Type clrType) + { + DbType dbType = DbTypeMap.ColumnDbTypeMap[clrType]; + return GetSqlDbType(dbType); + } + + /// + /// Returns the mapped SqlDbType for the DbType specified + /// + /// + /// + public virtual SqlDbType GetSqlDbType(DbType dbType) + { + SqlDbType sqlDbType; + + //SEE: https://msdn.microsoft.com/en-us/library/cc716729(v=vs.110).aspx + // and https://msdn.microsoft.com/en-us/library/yy6y35y8%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 + switch (dbType) + { + case DbType.AnsiString: + sqlDbType = SqlDbType.VarChar; + break; + case DbType.Binary: + sqlDbType = SqlDbType.VarBinary; + break; + case DbType.Byte: + sqlDbType = SqlDbType.TinyInt; + break; + case DbType.Boolean: + sqlDbType = SqlDbType.Bit; + break; + case DbType.Currency: + sqlDbType = SqlDbType.Money; + break; + case DbType.Date: + sqlDbType = SqlDbType.Date; + break; + case DbType.DateTime: + sqlDbType = SqlDbType.DateTime; + break; + case DbType.Decimal: + sqlDbType = SqlDbType.Decimal; + break; + case DbType.Double: + sqlDbType = SqlDbType.Float; + break; + case DbType.Guid: + sqlDbType = SqlDbType.UniqueIdentifier; + break; + case DbType.Int16: + sqlDbType = SqlDbType.SmallInt; + break; + case DbType.Int32: + sqlDbType = SqlDbType.Int; + break; + case DbType.Int64: + sqlDbType = SqlDbType.BigInt; + break; + case DbType.Object: + sqlDbType = SqlDbType.Variant; + break; + case DbType.SByte: + throw new NotSupportedException("Inferring a SqlDbType from SByte is not supported."); + case DbType.Single: + sqlDbType = SqlDbType.Real; + break; + case DbType.String: + sqlDbType = SqlDbType.NVarChar; + break; + case DbType.Time: + sqlDbType = SqlDbType.Time; + break; + case DbType.UInt16: + throw new NotSupportedException("Inferring a SqlDbType from UInt16 is not supported."); + case DbType.UInt32: + throw new NotSupportedException("Inferring a SqlDbType from UInt32 is not supported."); + case DbType.UInt64: + throw new NotSupportedException("Inferring a SqlDbType from UInt64 is not supported."); + case DbType.VarNumeric: + throw new NotSupportedException("Inferring a VarNumeric from UInt64 is not supported."); + case DbType.AnsiStringFixedLength: + sqlDbType = SqlDbType.Char; + break; + case DbType.StringFixedLength: + sqlDbType = SqlDbType.NChar; + break; + case DbType.Xml: + sqlDbType = SqlDbType.Xml; + break; + case DbType.DateTime2: + sqlDbType = SqlDbType.DateTime2; + break; + case DbType.DateTimeOffset: + sqlDbType = SqlDbType.DateTimeOffset; + break; + default: + throw new ArgumentOutOfRangeException(); } - public override string GetQuotedColumnName(string? columnName) => $"[{columnName}]"; + return sqlDbType; + } - public override string GetQuotedName(string? name) => $"[{name}]"; + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) + { + var createSql = Format(tableDefinition); + var createPrimaryKeySql = FormatPrimaryKey(tableDefinition); + List foreignSql = Format(tableDefinition.ForeignKeys); - public override string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + _logger.LogInformation("Create table:\n {Sql}", createSql); + database.Execute(new Sql(createSql)); + + if (skipKeysAndIndexes) { - switch (columnType) - { - case TextColumnType.NVarchar: - return base.GetStringColumnEqualComparison(column, paramIndex, columnType); - case TextColumnType.NText: - //MSSQL doesn't allow for = comparison with NText columns but allows this syntax - return $"{column} LIKE @{paramIndex}"; - default: - throw new ArgumentOutOfRangeException(nameof(columnType)); - } + return; } - public override string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + //If any statements exists for the primary key execute them here + if (string.IsNullOrEmpty(createPrimaryKeySql) == false) { - switch (columnType) - { - case TextColumnType.NVarchar: - return base.GetStringColumnWildcardComparison(column, paramIndex, columnType); - case TextColumnType.NText: - //MSSQL doesn't allow for upper methods with NText columns - return $"{column} LIKE @{paramIndex}"; - default: - throw new ArgumentOutOfRangeException(nameof(columnType)); - } + _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); + database.Execute(new Sql(createPrimaryKeySql)); } - /// - /// This uses a the DbTypeMap created and custom mapping to resolve the SqlDbType - /// - /// - /// - public virtual SqlDbType GetSqlDbType(Type clrType) + List indexSql = Format(tableDefinition.Indexes); + //Loop through index statements and execute sql + foreach (var sql in indexSql) { - var dbType = DbTypeMap.ColumnDbTypeMap[clrType]; - return GetSqlDbType(dbType); + _logger.LogInformation("Create Index:\n {Sql}", sql); + database.Execute(new Sql(sql)); } - /// - /// Returns the mapped SqlDbType for the DbType specified - /// - /// - /// - public virtual SqlDbType GetSqlDbType(DbType dbType) + //Loop through foreignkey statements and execute sql + foreach (var sql in foreignSql) { - SqlDbType sqlDbType; - - //SEE: https://msdn.microsoft.com/en-us/library/cc716729(v=vs.110).aspx - // and https://msdn.microsoft.com/en-us/library/yy6y35y8%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 - switch (dbType) - { - case DbType.AnsiString: - sqlDbType = SqlDbType.VarChar; - break; - case DbType.Binary: - sqlDbType = SqlDbType.VarBinary; - break; - case DbType.Byte: - sqlDbType = SqlDbType.TinyInt; - break; - case DbType.Boolean: - sqlDbType = SqlDbType.Bit; - break; - case DbType.Currency: - sqlDbType = SqlDbType.Money; - break; - case DbType.Date: - sqlDbType = SqlDbType.Date; - break; - case DbType.DateTime: - sqlDbType = SqlDbType.DateTime; - break; - case DbType.Decimal: - sqlDbType = SqlDbType.Decimal; - break; - case DbType.Double: - sqlDbType = SqlDbType.Float; - break; - case DbType.Guid: - sqlDbType = SqlDbType.UniqueIdentifier; - break; - case DbType.Int16: - sqlDbType = SqlDbType.SmallInt; - break; - case DbType.Int32: - sqlDbType = SqlDbType.Int; - break; - case DbType.Int64: - sqlDbType = SqlDbType.BigInt; - break; - case DbType.Object: - sqlDbType = SqlDbType.Variant; - break; - case DbType.SByte: - throw new NotSupportedException("Inferring a SqlDbType from SByte is not supported."); - case DbType.Single: - sqlDbType = SqlDbType.Real; - break; - case DbType.String: - sqlDbType = SqlDbType.NVarChar; - break; - case DbType.Time: - sqlDbType = SqlDbType.Time; - break; - case DbType.UInt16: - throw new NotSupportedException("Inferring a SqlDbType from UInt16 is not supported."); - case DbType.UInt32: - throw new NotSupportedException("Inferring a SqlDbType from UInt32 is not supported."); - case DbType.UInt64: - throw new NotSupportedException("Inferring a SqlDbType from UInt64 is not supported."); - case DbType.VarNumeric: - throw new NotSupportedException("Inferring a VarNumeric from UInt64 is not supported."); - case DbType.AnsiStringFixedLength: - sqlDbType = SqlDbType.Char; - break; - case DbType.StringFixedLength: - sqlDbType = SqlDbType.NChar; - break; - case DbType.Xml: - sqlDbType = SqlDbType.Xml; - break; - case DbType.DateTime2: - sqlDbType = SqlDbType.DateTime2; - break; - case DbType.DateTimeOffset: - sqlDbType = SqlDbType.DateTimeOffset; - break; - default: - throw new ArgumentOutOfRangeException(); - } - return sqlDbType; - } - - public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) - { - var createSql = Format(tableDefinition); - var createPrimaryKeySql = FormatPrimaryKey(tableDefinition); - List foreignSql = Format(tableDefinition.ForeignKeys); - - _logger.LogInformation("Create table:\n {Sql}", createSql); - database.Execute(new Sql(createSql)); - - if (skipKeysAndIndexes) - { - return; - } - - //If any statements exists for the primary key execute them here - if (string.IsNullOrEmpty(createPrimaryKeySql) == false) - { - _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); - database.Execute(new Sql(createPrimaryKeySql)); - } - - List indexSql = Format(tableDefinition.Indexes); - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - _logger.LogInformation("Create Index:\n {Sql}", sql); - database.Execute(new Sql(sql)); - } - - //Loop through foreignkey statements and execute sql - foreach (var sql in foreignSql) - { - _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); - database.Execute(new Sql(sql)); - } + _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); + database.Execute(new Sql(sql)); } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs index 8a05e78258..2b9d35b959 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs @@ -4,142 +4,166 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// A data reader used for reading collections of PocoData entity types +/// +/// +/// We are using a custom data reader so that tons of memory is not consumed when rebuilding this table, previously +/// we'd generate SQL insert statements, but we'd have to put all of the XML structures into memory first. +/// Alternatively +/// we can use .net's DataTable, but this also requires putting everything into memory. By using a DataReader we don't +/// have to +/// store every content item and it's XML structure in memory to get it into the DB, we can stream it into the db with +/// this +/// reader. +/// +internal class PocoDataDataReader : BulkDataReader + where TSyntax : ISqlSyntaxProvider { - /// - /// A data reader used for reading collections of PocoData entity types - /// - /// - /// We are using a custom data reader so that tons of memory is not consumed when rebuilding this table, previously - /// we'd generate SQL insert statements, but we'd have to put all of the XML structures into memory first. Alternatively - /// we can use .net's DataTable, but this also requires putting everything into memory. By using a DataReader we don't have to - /// store every content item and it's XML structure in memory to get it into the DB, we can stream it into the db with this - /// reader. - /// - internal class PocoDataDataReader : BulkDataReader - where TSyntax : ISqlSyntaxProvider + private readonly ColumnDefinition[] _columnDefinitions; + private readonly IEnumerator _enumerator; + private readonly PocoColumn[] _readerColumns; + private readonly MicrosoftSqlSyntaxProviderBase _sqlSyntaxProvider; + private readonly TableDefinition _tableDefinition; + private int _recordsAffected = -1; + + public PocoDataDataReader( + IEnumerable dataSource, + PocoData pd, + MicrosoftSqlSyntaxProviderBase sqlSyntaxProvider) { - private readonly MicrosoftSqlSyntaxProviderBase _sqlSyntaxProvider; - private readonly TableDefinition _tableDefinition; - private readonly PocoColumn[] _readerColumns; - private readonly IEnumerator _enumerator; - private readonly ColumnDefinition[] _columnDefinitions; - private int _recordsAffected = -1; - - public PocoDataDataReader( - IEnumerable dataSource, - PocoData pd, - MicrosoftSqlSyntaxProviderBase sqlSyntaxProvider) + if (dataSource == null) { - if (dataSource == null) throw new ArgumentNullException(nameof(dataSource)); - if (sqlSyntaxProvider == null) throw new ArgumentNullException(nameof(sqlSyntaxProvider)); - - _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); - if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.Type); - - // only real columns, exclude result/computed columns - // Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59 - _readerColumns = pd.Columns - .Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false) - .Select(x => x.Value) - .ToArray(); - - _sqlSyntaxProvider = sqlSyntaxProvider; - _enumerator = dataSource.GetEnumerator(); - _columnDefinitions = _tableDefinition.Columns.ToArray(); - + throw new ArgumentNullException(nameof(dataSource)); } - protected override string SchemaName => _tableDefinition.SchemaName; - - protected override string TableName => _tableDefinition.Name; - - public override int RecordsAffected => _recordsAffected <= 0 ? -1 : _recordsAffected; - - /// - /// This will automatically add the schema rows based on the Poco table definition and the columns passed in - /// - protected override void AddSchemaTableRows() + if (sqlSyntaxProvider == null) { - //var colNames = _readerColumns.Select(x => x.ColumnName).ToArray(); - //foreach (var col in _columnDefinitions.Where(x => colNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) - foreach (var col in _columnDefinitions) + throw new ArgumentNullException(nameof(sqlSyntaxProvider)); + } + + _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); + if (_tableDefinition == null) + { + throw new InvalidOperationException("No table definition found for type " + pd.Type); + } + + // only real columns, exclude result/computed columns + // Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59 + _readerColumns = pd.Columns + .Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false) + .Select(x => x.Value) + .ToArray(); + + _sqlSyntaxProvider = sqlSyntaxProvider; + _enumerator = dataSource.GetEnumerator(); + _columnDefinitions = _tableDefinition.Columns.ToArray(); + } + + protected override string SchemaName => _tableDefinition.SchemaName; + + protected override string TableName => _tableDefinition.Name; + + public override int RecordsAffected => _recordsAffected <= 0 ? -1 : _recordsAffected; + + /// + /// This will automatically add the schema rows based on the Poco table definition and the columns passed in + /// + protected override void AddSchemaTableRows() + { + //var colNames = _readerColumns.Select(x => x.ColumnName).ToArray(); + //foreach (var col in _columnDefinitions.Where(x => colNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) + foreach (ColumnDefinition col in _columnDefinitions) + { + SqlDbType sqlDbType; + if (col.CustomDbType.HasValue) { - SqlDbType sqlDbType; - if (col.CustomDbType.HasValue) + //get the SqlDbType from the 'special type' + switch (col.CustomDbType) { - //get the SqlDbType from the 'special type' - switch (col.CustomDbType) - { - case var x when x == SpecialDbType.NTEXT: - sqlDbType = SqlDbType.NText; - break; - case var x when x == SpecialDbType.NCHAR: - sqlDbType = SqlDbType.NChar; - break; - case var x when x == SpecialDbType.NVARCHARMAX: - sqlDbType = SqlDbType.NVarChar; - break; - default: - throw new ArgumentOutOfRangeException("The custom DB type " + col.CustomDbType + " is not supported for bulk import statements."); - } + case var x when x == SpecialDbType.NTEXT: + sqlDbType = SqlDbType.NText; + break; + case var x when x == SpecialDbType.NCHAR: + sqlDbType = SqlDbType.NChar; + break; + case var x when x == SpecialDbType.NVARCHARMAX: + sqlDbType = SqlDbType.NVarChar; + break; + default: + throw new ArgumentOutOfRangeException("The custom DB type " + col.CustomDbType + + " is not supported for bulk import statements."); } - else if (col.Type.HasValue) - { - //get the SqlDbType from the DbType - sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.Type.Value); - } - else - { - //get the SqlDbType from the CLR type - sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.PropertyType); - } - - AddSchemaTableRow( - col.Name, - col.Size > 0 ? (int?)col.Size : null, - col.Precision > 0 ? (short?)col.Precision : null, - null, col.IsUnique, col.IsIdentity, col.IsNullable, sqlDbType, - null, null, null, null, null); } - } - - /// - /// Get the value from the column index for the current object - /// - /// - /// - public override object GetValue(int i) - { - return _enumerator.Current == null ? null! : _readerColumns[i].GetValue(_enumerator.Current); - } - - /// - /// Advance the cursor - /// - /// - public override bool Read() - { - var result = _enumerator.MoveNext(); - if (result) + else if (col.Type.HasValue) { - if (_recordsAffected == -1) - _recordsAffected = 0; - _recordsAffected++; + //get the SqlDbType from the DbType + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.Type.Value); } - return result; + else + { + //get the SqlDbType from the CLR type + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.PropertyType); + } + + AddSchemaTableRow( + col.Name, + col.Size > 0 ? col.Size : null, + col.Precision > 0 ? (short?)col.Precision : null, + null, + col.IsUnique, + col.IsIdentity, + col.IsNullable, + sqlDbType, + null, + null, + null, + null, + null); + } + } + + /// + /// Get the value from the column index for the current object + /// + /// + /// + public override object GetValue(int i) => + _enumerator.Current == null ? null! : _readerColumns[i].GetValue(_enumerator.Current); + + /// + /// Advance the cursor + /// + /// + public override bool Read() + { + var result = _enumerator.MoveNext(); + if (result) + { + if (_recordsAffected == -1) + { + _recordsAffected = 0; + } + + _recordsAffected++; } - /// - /// Ensure the enumerator is disposed - /// - /// - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); + return result; + } - if (disposing) - _enumerator.Dispose(); + /// + /// Ensure the enumerator is disposed + /// + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _enumerator.Dispose(); } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs index cf74a8549f..0dbc62fb49 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -6,13 +6,13 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// Provider metadata for SQL Azure +/// Provider metadata for SQL Azure /// [DataContract] public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("7858e827-8951-4fe0-a7fe-6883011b1f1b"); + public Guid Id => new("7858e827-8951-4fe0-a7fe-6883011b1f1b"); /// public int SortOrder => 3; @@ -59,37 +59,49 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata var password = databaseModel.Password; if (server.Contains(".") && ServerStartsWithTcp(server) == false) + { server = $"tcp:{server}"; + } if (server.Contains(".") == false && ServerStartsWithTcp(server)) { - string serverName = server.Contains(",") + var serverName = server.Contains(",") ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) : server; var portAddition = string.Empty; if (server.Contains(",")) + { portAddition = server.Substring(server.IndexOf(",", StringComparison.Ordinal)); + } server = $"{serverName}.database.windows.net{portAddition}"; } if (ServerStartsWithTcp(server) == false) + { server = $"tcp:{server}.database.windows.net"; + } if (server.Contains(",") == false) + { server = $"{server},1433"; + } if (user.Contains("@") == false) { var userDomain = server; if (ServerStartsWithTcp(server)) + { userDomain = userDomain.Substring(userDomain.IndexOf(":", StringComparison.Ordinal) + 1); + } if (userDomain.Contains(".")) + { userDomain = userDomain.Substring(0, userDomain.IndexOf(".", StringComparison.Ordinal)); + } user = $"{user}@{userDomain}"; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs index 1741b8ffe1..30a503e5f9 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs @@ -7,13 +7,13 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// Provider metadata for SQL Server LocalDb +/// Provider metadata for SQL Server LocalDb /// [DataContract] public class SqlLocalDbDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("05a7e9ed-aa6a-43af-a309-63422c87c675"); + public Guid Id => new("05a7e9ed-aa6a-43af-a309-63422c87c675"); /// public int SortOrder => 1; diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs index cfd30bbd90..dbaab82ad4 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs @@ -1,74 +1,85 @@ using System.Data; +using System.Data.Common; using Microsoft.Data.SqlClient; using NPoco; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// A bulk sql insert provider for Sql Server +/// +public class SqlServerBulkSqlInsertProvider : IBulkSqlInsertProvider { - /// - /// A bulk sql insert provider for Sql Server - /// - public class SqlServerBulkSqlInsertProvider : IBulkSqlInsertProvider + public string ProviderName => Constants.ProviderName; + + public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - public string ProviderName => Constants.ProviderName; - - public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) + T[] recordsA = records.ToArray(); + if (recordsA.Length == 0) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; - - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - - return BulkInsertRecordsSqlServer(database, pocoData, recordsA); + return 0; } - /// - /// Bulk-insert records using SqlServer BulkCopy method. - /// - /// The type of the records. - /// The database. - /// The PocoData object corresponding to the record's type. - /// The records. - /// The number of records that were inserted. - private int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + PocoData? pocoData = database.PocoDataFactory.ForType(typeof(T)); + if (pocoData == null) { - // TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items. - // It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader - // which in theory should be more efficient than NPocos way of building up an in-memory DataTable. + throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + } - // create command against the original database.Connection - using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) + return BulkInsertRecordsSqlServer(database, pocoData, recordsA); + } + + /// + /// Bulk-insert records using SqlServer BulkCopy method. + /// + /// The type of the records. + /// The database. + /// The PocoData object corresponding to the record's type. + /// The records. + /// The number of records that were inserted. + private int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + { + // TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items. + // It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader + // which in theory should be more efficient than NPocos way of building up an in-memory DataTable. + + // create command against the original database.Connection + using (DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) + { + // use typed connection and transaction or SqlBulkCopy + SqlConnection tConnection = NPocoDatabaseExtensions.GetTypedConnection(database.Connection); + SqlTransaction tTransaction = + NPocoDatabaseExtensions.GetTypedTransaction(command.Transaction); + var tableName = pocoData.TableInfo.TableName; + + if (database.SqlContext.SqlSyntax is not SqlServerSyntaxProvider syntax) { - // use typed connection and transaction or SqlBulkCopy - var tConnection = NPocoDatabaseExtensions.GetTypedConnection(database.Connection); - var tTransaction = NPocoDatabaseExtensions.GetTypedTransaction(command.Transaction); - var tableName = pocoData.TableInfo.TableName; + throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); + } - var syntax = database.SqlContext.SqlSyntax as SqlServerSyntaxProvider; - if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); + using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) + { + // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout. + BulkCopyTimeout = 0, + DestinationTableName = tableName, - using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) + // be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50 + BatchSize = 4096, + }) + using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) + { + // we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared + // to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses + // the names instead of their ordering. + foreach (SqlBulkCopyColumnMapping col in bulkReader.ColumnMappings) { - BulkCopyTimeout = 0, // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout. - DestinationTableName = tableName, - // be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50 - BatchSize = 4096 - }) - using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) - { - //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared - //to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses - //the names instead of their ordering. - foreach (var col in bulkReader.ColumnMappings) - { - copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn); - } - - copy.WriteToServer(bulkReader); - return bulkReader.RecordsAffected; + copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn); } + + copy.WriteToServer(bulkReader); + return bulkReader.RecordsAffected; } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs index 205519d0b1..dd092e820d 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs @@ -1,61 +1,60 @@ using Microsoft.Data.SqlClient; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +public class SqlServerDatabaseCreator : IDatabaseCreator { - public class SqlServerDatabaseCreator : IDatabaseCreator + public string ProviderName => Constants.ProviderName; + + public void Create(string connectionString) { - public string ProviderName => Constants.ProviderName; + var builder = new SqlConnectionStringBuilder(connectionString); - public void Create(string connectionString) + // Get connection string without database specific information + var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString) { - var builder = new SqlConnectionStringBuilder(connectionString); + AttachDBFilename = string.Empty, + InitialCatalog = string.Empty + }; + var masterConnectionString = masterBuilder.ConnectionString; - // Get connection string without database specific information - var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString) + string fileName = builder.AttachDBFilename, + database = builder.InitialCatalog; + + // Create database + if (!string.IsNullOrEmpty(fileName) && !File.Exists(fileName)) + { + if (string.IsNullOrWhiteSpace(database)) { - AttachDBFilename = string.Empty, - InitialCatalog = string.Empty - }; - var masterConnectionString = masterBuilder.ConnectionString; - - string fileName = builder.AttachDBFilename, - database = builder.InitialCatalog; - - // Create database - if (!string.IsNullOrEmpty(fileName) && !File.Exists(fileName)) - { - if (string.IsNullOrWhiteSpace(database)) - { - // Use a temporary database name - database = "Umbraco-" + Guid.NewGuid(); - } - - using var connection = new SqlConnection(masterConnectionString); - connection.Open(); - - using var command = new SqlCommand( - $"CREATE DATABASE [{database}] ON (NAME='{database}', FILENAME='{fileName}');" + - $"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" + - $"EXEC sp_detach_db @dbname='{database}';", - connection); - command.ExecuteNonQuery(); - - connection.Close(); + // Use a temporary database name + database = "Umbraco-" + Guid.NewGuid(); } - else if (!string.IsNullOrEmpty(database)) - { - using var connection = new SqlConnection(masterConnectionString); - connection.Open(); - using var command = new SqlCommand( - $"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '{database}') " + - $"CREATE DATABASE [{database}];", - connection); - command.ExecuteNonQuery(); + using var connection = new SqlConnection(masterConnectionString); + connection.Open(); - connection.Close(); - } + using var command = new SqlCommand( + $"CREATE DATABASE [{database}] ON (NAME='{database}', FILENAME='{fileName}');" + + $"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" + + $"EXEC sp_detach_db @dbname='{database}';", + connection); + command.ExecuteNonQuery(); + + connection.Close(); + } + else if (!string.IsNullOrEmpty(database)) + { + using var connection = new SqlConnection(masterConnectionString); + connection.Open(); + + using var command = new SqlCommand( + $"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '{database}') " + + $"CREATE DATABASE [{database}];", + connection); + command.ExecuteNonQuery(); + + connection.Close(); } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs index 8c840f1778..8b36736804 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -5,13 +5,13 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// Provider metadata for SQL Server +/// Provider metadata for SQL Server /// [DataContract] public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("5e1ad149-1951-4b74-90bf-2ac2aada9e73"); + public Guid Id => new("5e1ad149-1951-4b74-90bf-2ac2aada9e73"); /// public int SortOrder => 2; diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs index 7c8effb2b3..358eae2e38 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -13,17 +13,17 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// SQL Server implementation of . +/// SQL Server implementation of . /// public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism { + private readonly IOptionsMonitor _connectionStrings; + private readonly IOptionsMonitor _globalSettings; private readonly ILogger _logger; private readonly Lazy _scopeAccessor; // Hooray it's a circular dependency. - private readonly IOptionsMonitor _globalSettings; - private readonly IOptionsMonitor _connectionStrings; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public SqlServerDistributedLockingMechanism( ILogger logger, @@ -39,7 +39,7 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism /// public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + string.Equals(_connectionStrings.CurrentValue.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); /// public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) @@ -104,11 +104,9 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism public DistributedLockType LockType { get; } - public void Dispose() - { + public void Dispose() => // Mostly no op, cleaned up by completing transaction in scope. _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); - } public override string ToString() => $"SqlServerDistributedLock({LockId}, {LockType}"; @@ -124,19 +122,21 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqlServerDistributedLockingMechanism requires a transaction to function."); } if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + throw new InvalidOperationException( + "A transaction with minimum ReadCommitted isolation level is required."); } const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id"; db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); - var i = db.ExecuteScalar(query, new {id = LockId}); + var i = db.ExecuteScalar(query, new { id = LockId }); if (i == null) { @@ -156,19 +156,22 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqlServerDistributedLockingMechanism requires a transaction to function."); } if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + throw new InvalidOperationException( + "A transaction with minimum ReadCommitted isolation level is required."); } - const string query = @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id"; + const string query = + @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id"; db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); - var i = db.Execute(query, new {id = LockId}); + var i = db.Execute(query, new { id = LockId }); if (i == 0) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs index a4fc33d98c..64ed9e3566 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; @@ -12,146 +13,133 @@ using Umbraco.Cms.Persistence.SqlServer.Dtos; using Umbraco.Extensions; using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Represents an SqlSyntaxProvider for Sql Server. +/// +public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { - /// - /// Represents an SqlSyntaxProvider for Sql Server. - /// - public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase + public enum EngineEdition { - private readonly IOptions _globalSettings; - private readonly ILogger _logger; + Unknown = 0, + Desktop = 1, + Standard = 2, + Enterprise = 3, // Also developer edition + Express = 4, + Azure = 5, + } - public SqlServerSyntaxProvider(IOptions globalSettings) - : this(globalSettings, StaticApplicationLogging.CreateLogger()) + public enum VersionName + { + Invalid = -1, + Unknown = 0, + V7 = 1, + V2000 = 2, + V2005 = 3, + V2008 = 4, + V2012 = 5, + V2014 = 6, + V2016 = 7, + V2017 = 8, + V2019 = 9, + Other = 99, + } + + private readonly IOptions _globalSettings; + private readonly ILogger _logger; + + public SqlServerSyntaxProvider(IOptions globalSettings) + : this(globalSettings, StaticApplicationLogging.CreateLogger()) + { + } + + public SqlServerSyntaxProvider(IOptions globalSettings, ILogger logger) + { + _globalSettings = globalSettings; + _logger = logger; + } + + public override string ProviderName => Constants.ProviderName; + + public ServerVersionInfo? ServerVersion { get; private set; } + + public override string DbProvider => ServerVersion?.IsAzure ?? false ? "SqlAzure" : "SqlServer"; + + public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted; + + public override string DeleteDefaultConstraint => "ALTER TABLE {0} DROP CONSTRAINT {2}"; + + public override string DropIndex => "DROP INDEX {0} ON {1}"; + + public override string RenameColumn => "sp_rename '{0}.{1}', '{2}', 'COLUMN'"; + + public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4}){5}"; + + public override DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) + { + var setting = _globalSettings.Value.DatabaseFactoryServerVersion; + var fromSettings = false; + + if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") + || !Enum.TryParse(setting.Substring("SqlServer.".Length), out VersionName versionName, true)) { + versionName = GetSetVersion(connectionString, ProviderName, _logger).ProductVersionName; } - public SqlServerSyntaxProvider(IOptions globalSettings, ILogger logger) + _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, DatabaseType.SqlServer2012, fromSettings ? "settings" : "detected"); + + return DatabaseType.SqlServer2012; + } + + private static VersionName MapProductVersion(string productVersion) + { + var firstPart = string.IsNullOrWhiteSpace(productVersion) + ? "??" + : productVersion.Split(Core.Constants.CharArrays.Period)[0]; + switch (firstPart) { - _globalSettings = globalSettings; - _logger = logger; + case "??": + return VersionName.Invalid; + case "15": + return VersionName.V2019; + case "14": + return VersionName.V2017; + case "13": + return VersionName.V2016; + case "12": + return VersionName.V2014; + case "11": + return VersionName.V2012; + case "10": + return VersionName.V2008; + case "9": + return VersionName.V2005; + case "8": + return VersionName.V2000; + case "7": + return VersionName.V7; + default: + return VersionName.Other; + } + } + + internal ServerVersionInfo GetSetVersion(string? connectionString, string? providerName, ILogger logger) + { + // var factory = DbProviderFactories.GetFactory(providerName); + SqlClientFactory? factory = SqlClientFactory.Instance; + DbConnection? connection = factory.CreateConnection(); + + if (connection == null) + { + throw new InvalidOperationException($"Could not create a connection for provider \"{providerName}\"."); } - public override string ProviderName => Constants.ProviderName; + // Edition: "Express Edition", "Windows Azure SQL Database..." + // EngineEdition: 1/Desktop 2/Standard 3/Enterprise 4/Express 5/Azure + // ProductLevel: RTM, SPx, CTP... - public ServerVersionInfo? ServerVersion { get; private set; } - - public enum VersionName - { - Invalid = -1, - Unknown = 0, - V7 = 1, - V2000 = 2, - V2005 = 3, - V2008 = 4, - V2012 = 5, - V2014 = 6, - V2016 = 7, - V2017 = 8, - V2019 = 9, - Other = 99 - } - - public enum EngineEdition - { - Unknown = 0, - Desktop = 1, - Standard = 2, - Enterprise = 3,// Also developer edition - Express = 4, - Azure = 5 - } - - public override DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) - { - var setting = _globalSettings.Value.DatabaseFactoryServerVersion; - var fromSettings = false; - - if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") - || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) - { - versionName = GetSetVersion(connectionString, ProviderName, _logger).ProductVersionName; - } - - _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, DatabaseType.SqlServer2012, fromSettings ? "settings" : "detected"); - - return DatabaseType.SqlServer2012; - } - - public class ServerVersionInfo - { - public ServerVersionInfo() - { - ProductVersionName = VersionName.Unknown; - EngineEdition = EngineEdition.Unknown; - } - - public ServerVersionInfo(string edition, string instanceName, string productVersion, EngineEdition engineEdition, string machineName, string productLevel) - { - Edition = edition; - InstanceName = instanceName; - ProductVersion = productVersion; - ProductVersionName = MapProductVersion(ProductVersion); - EngineEdition = engineEdition; - MachineName = machineName; - ProductLevel = productLevel; - } - - public string? Edition { get; } - public string? InstanceName { get; } - public string? ProductVersion { get; } - public VersionName ProductVersionName { get; } - public EngineEdition EngineEdition { get; } - public bool IsAzure => EngineEdition == EngineEdition.Azure; - public string? MachineName { get; } - public string? ProductLevel { get; } - } - - private static VersionName MapProductVersion(string productVersion) - { - var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split(Cms.Core.Constants.CharArrays.Period)[0]; - switch (firstPart) - { - case "??": - return VersionName.Invalid; - case "15": - return VersionName.V2019; - case "14": - return VersionName.V2017; - case "13": - return VersionName.V2016; - case "12": - return VersionName.V2014; - case "11": - return VersionName.V2012; - case "10": - return VersionName.V2008; - case "9": - return VersionName.V2005; - case "8": - return VersionName.V2000; - case "7": - return VersionName.V7; - default: - return VersionName.Other; - } - } - - internal ServerVersionInfo GetSetVersion(string? connectionString, string? providerName, ILogger logger) - { - //var factory = DbProviderFactories.GetFactory(providerName); - var factory = SqlClientFactory.Instance; - var connection = factory.CreateConnection(); - - if (connection == null) - throw new InvalidOperationException($"Could not create a connection for provider \"{providerName}\"."); - - // Edition: "Express Edition", "Windows Azure SQL Database..." - // EngineEdition: 1/Desktop 2/Standard 3/Enterprise 4/Express 5/Azure - // ProductLevel: RTM, SPx, CTP... - - const string sql = @"select + const string sql = @"select SERVERPROPERTY('Edition') Edition, SERVERPROPERTY('EditionID') EditionId, SERVERPROPERTY('InstanceName') InstanceName, @@ -163,99 +151,102 @@ namespace Umbraco.Cms.Persistence.SqlServer.Services SERVERPROPERTY('ResourceLastUpdateDateTime') ResourceLastUpdateDateTime, SERVERPROPERTY('ProductLevel') ProductLevel;"; - string GetString(IDataReader reader, int ordinal, string defaultValue) - => reader.IsDBNull(ordinal) ? defaultValue : reader.GetString(ordinal); + string GetString(IDataReader reader, int ordinal, string defaultValue) + { + return reader.IsDBNull(ordinal) ? defaultValue : reader.GetString(ordinal); + } - int GetInt32(IDataReader reader, int ordinal, int defaultValue) - => reader.IsDBNull(ordinal) ? defaultValue : reader.GetInt32(ordinal); + int GetInt32(IDataReader reader, int ordinal, int defaultValue) + { + return reader.IsDBNull(ordinal) ? defaultValue : reader.GetInt32(ordinal); + } - connection.ConnectionString = connectionString; - ServerVersionInfo version; - using (connection) + connection.ConnectionString = connectionString; + ServerVersionInfo version; + using (connection) + { + try { - try + connection.Open(); + DbCommand command = connection.CreateCommand(); + command.CommandText = sql; + using (DbDataReader reader = command.ExecuteReader()) { - connection.Open(); - var command = connection.CreateCommand(); - command.CommandText = sql; - using (var reader = command.ExecuteReader()) - { - reader.Read(); - // InstanceName can be NULL for the default instance - version = new ServerVersionInfo( - GetString(reader, 0, "Unknown"), - GetString(reader, 2, "(default)"), - GetString(reader, 3, string.Empty), - (EngineEdition) GetInt32(reader, 5, 0), - GetString(reader, 7, "DEFAULT"), - GetString(reader, 9, "Unknown")); - } - connection.Close(); - } - catch (Exception e) - { - logger.LogError(e, "Failed to detected SqlServer version."); - version = new ServerVersionInfo(); // all unknown + reader.Read(); + // InstanceName can be NULL for the default instance + version = new ServerVersionInfo( + GetString(reader, 0, "Unknown"), + GetString(reader, 2, "(default)"), + GetString(reader, 3, string.Empty), + (EngineEdition)GetInt32(reader, 5, 0), + GetString(reader, 7, "DEFAULT"), + GetString(reader, 9, "Unknown")); } + + connection.Close(); + } + catch (Exception e) + { + logger.LogError(e, "Failed to detected SqlServer version."); + version = new ServerVersionInfo(); // all unknown } - - return ServerVersion = version; } - /// - /// SQL Server stores default values assigned to columns as constraints, it also stores them with named values, this is the only - /// server type that does this, therefore this method doesn't exist on any other syntax provider - /// - /// - public IEnumerable> GetDefaultConstraintsPerColumn(IDatabase db) - { - var items = db.Fetch("SELECT TableName = t.Name, ColumnName = c.Name, dc.Name, dc.[Definition] FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND c.column_id = dc.parent_column_id INNER JOIN sys.schemas as s on t.[schema_id] = s.[schema_id] WHERE s.name = (SELECT SCHEMA_NAME())"); - return items.Select(x => new Tuple(x.TableName, x.ColumnName, x.Name, x.Definition)); - } + return ServerVersion = version; + } - public override string DbProvider => ServerVersion?.IsAzure ?? false ? "SqlAzure" : "SqlServer"; + /// + /// SQL Server stores default values assigned to columns as constraints, it also stores them with named values, this is + /// the only + /// server type that does this, therefore this method doesn't exist on any other syntax provider + /// + /// + public IEnumerable> GetDefaultConstraintsPerColumn(IDatabase db) + { + List? items = db.Fetch( + "SELECT TableName = t.Name, ColumnName = c.Name, dc.Name, dc.[Definition] FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND c.column_id = dc.parent_column_id INNER JOIN sys.schemas as s on t.[schema_id] = s.[schema_id] WHERE s.name = (SELECT SCHEMA_NAME())"); + return items.Select(x => + new Tuple(x.TableName, x.ColumnName, x.Name, x.Definition)); + } - public override IEnumerable GetTablesInSchema(IDatabase db) - { - return db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - } + public override IEnumerable GetTablesInSchema(IDatabase db) => db.Fetch( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted; + public override IEnumerable GetColumnsInSchema(IDatabase db) + { + List? items = db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); + return + items.Select( + item => + new ColumnInfo(item.TableName, item.ColumnName, item.OrdinalPosition, item.ColumnDefault, item.IsNullable, item.DataType)).ToList(); + } - public override IEnumerable GetColumnsInSchema(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - return - items.Select( - item => - new ColumnInfo(item.TableName, item.ColumnName, item.OrdinalPosition, item.ColumnDefault, - item.IsNullable, item.DataType)).ToList(); - } + /// + public override IEnumerable> GetConstraintsPerTable(IDatabase db) + { + List items = + db.Fetch( + "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); + return items.Select(item => new Tuple(item.TableName, item.ConstraintName)).ToList(); + } - /// - public override IEnumerable> GetConstraintsPerTable(IDatabase db) - { - var items = - db.Fetch( - "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - return items.Select(item => new Tuple(item.TableName, item.ConstraintName)).ToList(); - } + /// + public override IEnumerable> GetConstraintsPerColumn(IDatabase db) + { + List? items = + db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); + return items.Select(item => + new Tuple(item.TableName, item.ColumnName, item.ConstraintName)).ToList(); + } - /// - public override IEnumerable> GetConstraintsPerColumn(IDatabase db) - { - var items = - db.Fetch( - "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - return items.Select(item => new Tuple(item.TableName, item.ColumnName, item.ConstraintName)).ToList(); - } - - /// - public override IEnumerable> GetDefinedIndexes(IDatabase db) - { - var items = - db.Fetch( - @"select T.name as TABLE_NAME, I.name as INDEX_NAME, AC.Name as COLUMN_NAME, + /// + public override IEnumerable> GetDefinedIndexes(IDatabase db) + { + List? items = + db.Fetch( + @"select T.name as TABLE_NAME, I.name as INDEX_NAME, AC.Name as COLUMN_NAME, CASE WHEN I.is_unique_constraint = 1 OR I.is_unique = 1 THEN 1 ELSE 0 END AS [UNIQUE] from sys.tables as T inner join sys.indexes as I on T.[object_id] = I.[object_id] inner join sys.index_columns as IC on IC.[object_id] = I.[object_id] and IC.[index_id] = I.[index_id] @@ -263,182 +254,203 @@ from sys.tables as T inner join sys.indexes as I on T.[object_id] = I.[object_id inner join sys.schemas as S on T.[schema_id] = S.[schema_id] WHERE S.name = (SELECT SCHEMA_NAME()) AND I.is_primary_key = 0 order by T.name, I.name"); - return items.Select(item => new Tuple(item.TableName, item.IndexName, item.ColumnName, - item.Unique == 1)).ToList(); + return items.Select(item => new Tuple(item.TableName, item.IndexName, item.ColumnName, item.Unique == 1)).ToList(); + } - } - - /// - public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName) - { - constraintName = db.Fetch(@"select con.[name] as [constraintName] + /// + public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName) + { + constraintName = db.Fetch( + @"select con.[name] as [constraintName] from sys.default_constraints con join sys.columns col on con.object_id=col.default_object_id join sys.tables tbl on col.object_id=tbl.object_id -where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) - .FirstOrDefault(); - return !constraintName.IsNullOrWhiteSpace(); - } - - public override bool DoesTableExist(IDatabase db, string tableName) - { - var result = - db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = (SELECT SCHEMA_NAME())", - new { TableName = tableName }); - - return result > 0; - } - - public override string FormatColumnRename(string? tableName, string? oldName, string? newName) - { - return string.Format(RenameColumn, tableName, oldName, newName); - } - - public override string FormatTableRename(string? oldName, string? newName) - { - return string.Format(RenameTable, oldName, newName); - } - - protected override string FormatIdentity(ColumnDefinition column) - { - return column.IsIdentity ? GetIdentityString(column) : string.Empty; - } - - public override Sql SelectTop(Sql sql, int top) - { - return new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); - } - - private static string GetIdentityString(ColumnDefinition column) - { - return "IDENTITY(1,1)"; - } - - protected override string? FormatSystemMethods(SystemMethods systemMethod) - { - switch (systemMethod) - { - case SystemMethods.NewGuid: - return "NEWID()"; - case SystemMethods.CurrentDateTime: - return "GETDATE()"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; - } - - return null; - } - - public override string DeleteDefaultConstraint => "ALTER TABLE {0} DROP CONSTRAINT {2}"; - - public override string DropIndex => "DROP INDEX {0} ON {1}"; - - public override string RenameColumn => "sp_rename '{0}.{1}', '{2}', 'COLUMN'"; - - public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4}){5}"; - public override string Format(IndexDefinition index) - { - var name = string.IsNullOrEmpty(index.Name) - ? $"IX_{index.TableName}_{index.ColumnName}" - : index.Name; - - var columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); - - var includeColumns = index.IncludeColumns?.Any() ?? false - ? $" INCLUDE ({string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name)))})" - : string.Empty; - - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns, includeColumns); - } - - - public override Sql InsertForUpdateHint(Sql sql) - { - // go find the first FROM clause, and append the lock hint - Sql s = sql; - var updated = false; - - while (s != null) - { - var sqlText = SqlInspector.GetSqlText(s); - if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) - { - SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); - updated = true; - break; - } - - s = SqlInspector.GetSqlRhs(sql); - } - - if (updated) - SqlInspector.Reset(sql); - - return sql; - } - - public override Sql AppendForUpdateHint(Sql sql) - => sql.Append(" WITH (UPDLOCK) "); - - public override Sql.SqlJoinClause LeftJoinWithNestedJoin( - Sql sql, - Func, - Sql> nestedJoin, - string? alias = null) - { - Type type = typeof(TDto); - - var tableName = GetQuotedTableName(type.GetTableName()); - var join = tableName; - - if (alias != null) - { - var quotedAlias = GetQuotedTableName(alias); - join += " " + quotedAlias; - } - - var nestedSql = new Sql(sql.SqlContext); - nestedSql = nestedJoin(nestedSql); - - Sql.SqlJoinClause sqlJoin = sql.LeftJoin(join); - sql.Append(nestedSql); - return sqlJoin; - } - - #region Sql Inspection - - private static SqlInspectionUtilities? _sqlInspector; - - private static SqlInspectionUtilities SqlInspector => _sqlInspector ?? (_sqlInspector = new SqlInspectionUtilities()); - - private class SqlInspectionUtilities - { - private readonly Func _getSqlText; - private readonly Action _setSqlText; - private readonly Func _getSqlRhs; - private readonly Action _setSqlFinal; - - public SqlInspectionUtilities() - { - (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); - _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); - _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); - } - - public string GetSqlText(Sql sql) => _getSqlText(sql); - - public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); - - public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); - - public void Reset(Sql sql) => _setSqlFinal(sql, null); - } - - #endregion +where tbl.[name]=@0 and col.[name]=@1;", + tableName, + columnName) + .FirstOrDefault(); + return !constraintName.IsNullOrWhiteSpace(); } + + public override bool DoesTableExist(IDatabase db, string tableName) + { + var result = + db.ExecuteScalar( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = (SELECT SCHEMA_NAME())", + new { TableName = tableName }); + + return result > 0; + } + + public override string FormatColumnRename(string? tableName, string? oldName, string? newName) => + string.Format(RenameColumn, tableName, oldName, newName); + + public override string FormatTableRename(string? oldName, string? newName) => + string.Format(RenameTable, oldName, newName); + + protected override string FormatIdentity(ColumnDefinition column) => + column.IsIdentity ? GetIdentityString(column) : string.Empty; + + public override Sql SelectTop(Sql sql, int top) => new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); + + private static string GetIdentityString(ColumnDefinition column) => "IDENTITY(1,1)"; + + protected override string? FormatSystemMethods(SystemMethods systemMethod) + { + switch (systemMethod) + { + case SystemMethods.NewGuid: + return "NEWID()"; + case SystemMethods.CurrentDateTime: + return "GETDATE()"; + + // case SystemMethods.NewSequentialId: + // return "NEWSEQUENTIALID()"; + // case SystemMethods.CurrentUTCDateTime: + // return "GETUTCDATE()"; + } + + return null; + } + + public override string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; + + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + var includeColumns = index.IncludeColumns?.Any() ?? false + ? $" INCLUDE ({string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name)))})" + : string.Empty; + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), GetQuotedTableName(index.TableName), columns, includeColumns); + } + + + public override Sql InsertForUpdateHint(Sql sql) + { + // go find the first FROM clause, and append the lock hint + Sql s = sql; + var updated = false; + + while (s != null) + { + var sqlText = SqlInspector.GetSqlText(s); + if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) + { + SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); + updated = true; + break; + } + + s = SqlInspector.GetSqlRhs(sql); + } + + if (updated) + { + SqlInspector.Reset(sql); + } + + return sql; + } + + public override Sql AppendForUpdateHint(Sql sql) + => sql.Append(" WITH (UPDLOCK) "); + + public override Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, + Sql> nestedJoin, + string? alias = null) + { + Type type = typeof(TDto); + + var tableName = GetQuotedTableName(type.GetTableName()); + var join = tableName; + + if (alias != null) + { + var quotedAlias = GetQuotedTableName(alias); + join += " " + quotedAlias; + } + + var nestedSql = new Sql(sql.SqlContext); + nestedSql = nestedJoin(nestedSql); + + Sql.SqlJoinClause sqlJoin = sql.LeftJoin(join); + sql.Append(nestedSql); + return sqlJoin; + } + + public class ServerVersionInfo + { + public ServerVersionInfo() + { + ProductVersionName = VersionName.Unknown; + EngineEdition = EngineEdition.Unknown; + } + + public ServerVersionInfo(string edition, string instanceName, string productVersion, EngineEdition engineEdition, string machineName, string productLevel) + { + Edition = edition; + InstanceName = instanceName; + ProductVersion = productVersion; + ProductVersionName = MapProductVersion(ProductVersion); + EngineEdition = engineEdition; + MachineName = machineName; + ProductLevel = productLevel; + } + + public string? Edition { get; } + + public string? InstanceName { get; } + + public string? ProductVersion { get; } + + public VersionName ProductVersionName { get; } + + public EngineEdition EngineEdition { get; } + + public bool IsAzure => EngineEdition == EngineEdition.Azure; + + public string? MachineName { get; } + + public string? ProductLevel { get; } + } + + #region Sql Inspection + + private static SqlInspectionUtilities? _sqlInspector; + + private static SqlInspectionUtilities SqlInspector => +_sqlInspector ??= new SqlInspectionUtilities(); + + private class SqlInspectionUtilities + { + private readonly Func _getSqlRhs; + private readonly Func _getSqlText; + private readonly Action _setSqlFinal; + private readonly Action _setSqlText; + + public SqlInspectionUtilities() + { + (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); + _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); + _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); + } + + public string GetSqlText(Sql sql) => _getSqlText(sql); + + public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); + + public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); + + public void Reset(Sql sql) => _setSqlFinal(sql, null); + } + + #endregion } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs index 60d64c09df..e81a65e74f 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Persistence.SqlServer; /// -/// Automatically adds SQL Server support to Umbraco when this project is referenced. +/// Automatically adds SQL Server support to Umbraco when this project is referenced. /// public class SqlServerComposer : IComposer { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs index 4b9fb8d11a..a47e92bf2e 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs @@ -13,27 +13,34 @@ using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Persistence.SqlServer; /// -/// SQLite support extensions for IUmbracoBuilder. +/// SQLite support extensions for IUmbracoBuilder. /// public static class UmbracoBuilderExtensions { /// - /// Add required services for SQL Server support. + /// Add required services for SQL Server support. /// public static IUmbracoBuilder AddUmbracoSqlServerSupport(this IUmbracoBuilder builder) { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); DbProviderFactories.UnregisterFactory(Constants.ProviderName); DbProviderFactories.RegisterFactory(Constants.ProviderName, SqlClientFactory.Instance); diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs index 76e408423c..5c10f46828 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs @@ -1,12 +1,15 @@ namespace Umbraco.Cms.Persistence.Sqlite; /// -/// Constants related to SQLite. +/// Constants related to SQLite. /// public static class Constants { /// - /// SQLite provider name. + /// SQLite provider name. /// - public const string ProviderName = "Microsoft.Data.SQLite"; + public const string ProviderName = "Microsoft.Data.Sqlite"; + + [Obsolete("This will be removed in Umbraco 12. Use Constants.ProviderName instead")] + public const string ProviderNameLegacy = "Microsoft.Data.SQLite"; } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs index eb76319040..9d7f3b29ad 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs @@ -1,11 +1,12 @@ using System.Data.Common; using NPoco; using StackExchange.Profiling; +using StackExchange.Profiling.Data; namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; public class SqliteAddMiniProfilerInterceptor : SqliteConnectionInterceptor { public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) - => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); + => new ProfiledDbConnection(conn, MiniProfiler.Current); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs index ef22e9c0b6..5c195291b0 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs @@ -8,5 +8,6 @@ namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; public class SqliteAddPreferDeferredInterceptor : SqliteConnectionInterceptor { public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) - => new SqlitePreferDeferredTransactionsConnection(conn as SqliteConnection ?? throw new InvalidOperationException()); + => new SqlitePreferDeferredTransactionsConnection(conn as SqliteConnection ?? + throw new InvalidOperationException()); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs index bd9bb1924d..eac4152df4 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs @@ -14,7 +14,7 @@ public class SqliteNullableGuidScalarMapper : ScalarMapper { if (value is null || value == DBNull.Value) { - return default(Guid?); + return default; } return Guid.TryParse($"{value}", out Guid result) diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs index f7b2836f1a..930478e453 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs @@ -8,7 +8,7 @@ public class SqlitePocoGuidMapper : DefaultMapper { if (destType == typeof(Guid)) { - return (value) => + return value => { var result = Guid.Parse($"{value}"); return result; @@ -17,7 +17,7 @@ public class SqlitePocoGuidMapper : DefaultMapper if (destType == typeof(Guid?)) { - return (value) => + return value => { if (Guid.TryParse($"{value}", out Guid result)) { diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs index 895ee21ef6..ff0a64a74b 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.Sqlite.Services; /// -/// Implements for SQLite. +/// Implements for SQLite. /// public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider { @@ -12,17 +12,23 @@ public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; + T[] recordsA = records.ToArray(); + if (recordsA.Length == 0) + { + return 0; + } - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + PocoData? pocoData = database.PocoDataFactory.ForType(typeof(T)); + if (pocoData == null) + { + throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + } return BulkInsertRecordsSqlite(database, pocoData, recordsA); } /// - /// Bulk-insert records using SqlServer BulkCopy method. + /// Bulk-insert records using SqlServer BulkCopy method. /// /// The type of the records. /// The database. @@ -39,7 +45,7 @@ public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider database.BeginTransaction(); } - foreach (var record in records) + foreach (T record in records) { database.Insert(record); count++; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs index 84c8eea1db..54d6063ad7 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Persistence; @@ -6,38 +5,35 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.Sqlite.Services; /// -/// Implements for SQLite. +/// Implements for SQLite. /// public class SqliteDatabaseCreator : IDatabaseCreator { private readonly ILogger _logger; + public SqliteDatabaseCreator(ILogger logger) => _logger = logger; + /// public string ProviderName => Constants.ProviderName; - public SqliteDatabaseCreator(ILogger logger) - { - _logger = logger; - } - /// - /// Creates a SQLite database file. + /// Creates a SQLite database file. /// /// - /// - /// With journal_mode = wal we have snapshot isolation. - /// - /// - /// Concurrent read/write can take occur, committing a write transaction will have no impact - /// on open read transactions as they see only committed data from the point in time that they began reading. - /// - /// - /// A write transaction still requires exclusive access to database files so concurrent writes are not possible. - /// - /// - /// Read more Isolation in SQLite
- /// Read more Write-Ahead Logging - ///
+ /// + /// With journal_mode = wal we have snapshot isolation. + /// + /// + /// Concurrent read/write can take occur, committing a write transaction will have no impact + /// on open read transactions as they see only committed data from the point in time that they began reading. + /// + /// + /// A write transaction still requires exclusive access to database files so concurrent writes are not possible. + /// + /// + /// Read more Isolation in SQLite
+ /// Read more Write-Ahead Logging + ///
///
public void Create(string connectionString) { @@ -88,7 +84,7 @@ public class SqliteDatabaseCreator : IDatabaseCreator // Copy our blank(ish) wal mode sqlite database to its final location. try { - File.Copy(tempFile, original.DataSource, overwrite: true); + File.Copy(tempFile, original.DataSource, true); } catch (Exception ex) { diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs index 0b684551c7..54a7a5cb1d 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services; public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("530386a2-b219-4d5f-b68c-b965e14c9ac9"); + public Guid Id => new("530386a2-b219-4d5f-b68c-b965e14c9ac9"); /// public int SortOrder => -1; @@ -47,12 +47,12 @@ public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata /// /// - /// - /// Required to ensure database creator is used regardless of configured InstallMissingDatabase value. - /// - /// - /// Ensures database setup with journal_mode = wal; - /// + /// + /// Required to ensure database creator is used regardless of configured InstallMissingDatabase value. + /// + /// + /// Ensures database setup with journal_mode = wal; + /// /// public bool ForceCreateDatabase => true; @@ -64,7 +64,7 @@ public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata DataSource = $"{ConnectionStrings.DataDirectoryPlaceholder}/{databaseModel.DatabaseName}.sqlite.db", ForeignKeys = true, Pooling = true, - Cache = SqliteCacheMode.Shared, + Cache = SqliteCacheMode.Shared }; return builder.ConnectionString; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs index 4a47d41846..a4a31416fa 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -4,7 +4,6 @@ using Microsoft.Data.SqlClient; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.DistributedLocking.Exceptions; @@ -17,10 +16,10 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services; public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism { - private readonly ILogger _logger; - private readonly Lazy _scopeAccessor; private readonly IOptionsMonitor _connectionStrings; private readonly IOptionsMonitor _globalSettings; + private readonly ILogger _logger; + private readonly Lazy _scopeAccessor; public SqliteDistributedLockingMechanism( ILogger logger, @@ -36,7 +35,7 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism /// public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + string.Equals(_connectionStrings.CurrentValue.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); // With journal_mode=wal we can always read a snapshot. public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) @@ -101,11 +100,9 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism public DistributedLockType LockType { get; } - public void Dispose() - { + public void Dispose() => // Mostly no op, cleaned up by completing transaction in scope. _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); - } public override string ToString() => $"SqliteDistributedLock({LockId})"; @@ -123,7 +120,8 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqliteDistributedLockingMechanism requires a transaction to function."); } } @@ -140,7 +138,8 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqliteDistributedLockingMechanism requires a transaction to function."); } var query = @$"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id = {LockId}"; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs index 4076718266..acc8afb508 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Data.Sqlite; +using SQLitePCL; namespace Umbraco.Cms.Persistence.Sqlite.Services; @@ -6,7 +7,7 @@ public static class SqliteExceptionExtensions { public static bool IsBusyOrLocked(this SqliteException ex) => ex.SqliteErrorCode - is SQLitePCL.raw.SQLITE_BUSY - or SQLitePCL.raw.SQLITE_LOCKED - or SQLitePCL.raw.SQLITE_LOCKED_SHAREDCACHE; + is raw.SQLITE_BUSY + or raw.SQLITE_LOCKED + or raw.SQLITE_LOCKED_SHAREDCACHE; } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs index 4e126056d6..7c5a9f56f8 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs @@ -9,22 +9,7 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection { private readonly SqliteConnection _inner; - public SqlitePreferDeferredTransactionsConnection(SqliteConnection inner) - { - _inner = inner; - } - - protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) - => _inner.BeginTransaction(isolationLevel, deferred: true); // <-- The important bit - - public override void ChangeDatabase(string databaseName) - => _inner.ChangeDatabase(databaseName); - - public override void Close() - => _inner.Close(); - - public override void Open() - => _inner.Open(); + public SqlitePreferDeferredTransactionsConnection(SqliteConnection inner) => _inner = inner; public override string Database => _inner.Database; @@ -38,9 +23,6 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection public override string ServerVersion => _inner.ServerVersion; - protected override DbCommand CreateDbCommand() - => new CommandWrapper(_inner.CreateCommand()); - [AllowNull] public override string ConnectionString { @@ -48,26 +30,26 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection set => _inner.ConnectionString = value; } + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + => _inner.BeginTransaction(isolationLevel, true); // <-- The important bit + + public override void ChangeDatabase(string databaseName) + => _inner.ChangeDatabase(databaseName); + + public override void Close() + => _inner.Close(); + + public override void Open() + => _inner.Open(); + + protected override DbCommand CreateDbCommand() + => new CommandWrapper(_inner.CreateCommand()); + private class CommandWrapper : DbCommand { private readonly DbCommand _inner; - public CommandWrapper(DbCommand inner) - { - _inner = inner; - } - - public override void Cancel() - => _inner.Cancel(); - - public override int ExecuteNonQuery() - => _inner.ExecuteNonQuery(); - - public override object? ExecuteScalar() - => _inner.ExecuteScalar(); - - public override void Prepare() - => _inner.Prepare(); + public CommandWrapper(DbCommand inner) => _inner = inner; [AllowNull] public override string CommandText @@ -97,10 +79,7 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection protected override DbConnection? DbConnection { get => _inner.Connection; - set - { - _inner.Connection = (value as SqlitePreferDeferredTransactionsConnection)?._inner; - } + set => _inner.Connection = (value as SqlitePreferDeferredTransactionsConnection)?._inner; } protected override DbParameterCollection DbParameterCollection @@ -118,6 +97,18 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection set => _inner.DesignTimeVisible = value; } + public override void Cancel() + => _inner.Cancel(); + + public override int ExecuteNonQuery() + => _inner.ExecuteNonQuery(); + + public override object? ExecuteScalar() + => _inner.ExecuteScalar(); + + public override void Prepare() + => _inner.Prepare(); + protected override DbParameter CreateDbParameter() => _inner.CreateParameter(); diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs index cf1d707d69..66f542712a 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Persistence.Sqlite.Mappers; namespace Umbraco.Cms.Persistence.Sqlite.Services; /// -/// Implements for SQLite. +/// Implements for SQLite. /// public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory { @@ -12,5 +12,5 @@ public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory public string ProviderName => Constants.ProviderName; /// - public NPocoMapperCollection Mappers => new NPocoMapperCollection(() => new[] { new SqlitePocoGuidMapper() }); + public NPocoMapperCollection Mappers => new(() => new[] { new SqlitePocoGuidMapper() }); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index be6f013f26..fe8bf7b6a1 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -38,6 +38,20 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase [typeof(Guid)] = new SqliteGuidScalarMapper(), [typeof(Guid?)] = new SqliteNullableGuidScalarMapper(), }; + + IntColumnDefinition = "INTEGER"; + LongColumnDefinition = "INTEGER"; + BoolColumnDefinition = "INTEGER"; + + GuidColumnDefinition = "TEXT"; + DateTimeColumnDefinition = "TEXT"; + DateTimeOffsetColumnDefinition = "TEXT"; + TimeColumnDefinition = "TEXT"; + DecimalColumnDefinition = "TEXT"; // REAL would be lossy. - https://docs.microsoft.com/en-us/dotnet/standard/data/sqlite/types + + RealColumnDefinition = "REAL"; + + BlobColumnDefinition = "BLOB"; } /// @@ -54,14 +68,12 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase /// public override string DbProvider => Constants.ProviderName; - /// public override bool SupportsIdentityInsert() => false; /// public override bool SupportsClustered() => false; - public override string GetIndexType(IndexTypes indexTypes) { switch (indexTypes) @@ -73,6 +85,32 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } } + public override string Format(TableDefinition table) + { + var columns = Format(table.Columns); + var primaryKey = FormatPrimaryKey(table); + List foreignKeys = Format(table.ForeignKeys); + + var sb = new StringBuilder(); + sb.AppendLine($"CREATE TABLE {table.Name}"); + sb.AppendLine("("); + sb.Append(columns); + + if (!string.IsNullOrEmpty(primaryKey)) + { + sb.AppendLine($", {primaryKey}"); + } + + foreach (var foreignKey in foreignKeys) + { + sb.AppendLine($", {foreignKey}"); + } + + sb.AppendLine(")"); + + return sb.ToString(); + } + public override List Format(IEnumerable foreignKeys) { return foreignKeys.Select(Format).ToList(); @@ -117,7 +155,9 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase public override string ConvertIntegerToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDecimalToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDateToOrderableString => "{0}"; /// @@ -127,16 +167,14 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase public override string GetSpecialDbType(SpecialDbType dbType, int customSize) => GetSpecialDbType(dbType); /// - public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, - out string constraintName) + public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, out string constraintName) { // TODO: SQLite constraintName = string.Empty; return false; } - public override string GetFieldNameForUpdate(Expression> fieldSelector, - string? tableAlias = null) + public override string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null) { var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; var fieldName = GetColumnName(field!); @@ -180,15 +218,14 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase return string.Join(" || ", args.AsEnumerable()); } - public override string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, - string? referenceName = null, bool forInsert = false) + public override string GetColumn(DatabaseType dbType, string tableName, string columnName, string? columnAlias, string? referenceName = null, bool forInsert = false) { if (forInsert) { return dbType.EscapeSqlIdentifier(columnName); } - return base.GetColumn(dbType, tableName, columnName, columnAlias, referenceName, forInsert); + return base.GetColumn(dbType, tableName, columnName, columnAlias!, referenceName, forInsert); } public override string FormatPrimaryKey(TableDefinition table) @@ -217,14 +254,12 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase return $"CONSTRAINT {constraintName} {constraintType} ({columns})"; } - /// public override Sql SelectTop(Sql sql, int top) { // SQLite uses LIMIT as opposed to TOP // SELECT TOP 5 * FROM My_Table // SELECT * FROM My_Table LIMIT 5; - return sql.Append($"LIMIT {top}"); } @@ -239,8 +274,7 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase return sb.ToString().TrimStart(','); } - public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, - bool skipKeysAndIndexes = false) + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) { var columns = Format(tableDefinition.Columns); var primaryKey = FormatPrimaryKey(tableDefinition); @@ -309,17 +343,16 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } } - /// public override IEnumerable> GetConstraintsPerColumn(IDatabase db) { - var items = db.Fetch("select * from sqlite_master where type = 'table'") + IEnumerable items = db.Fetch("select * from sqlite_master where type = 'table'") .Where(x => !x.Name.StartsWith("sqlite_")); List foundConstraints = new(); foreach (SqliteMaster row in items) { - var altPk = Regex.Match(row.Sql, @"CONSTRAINT (?PK_\w+)\s.*UNIQUE \(""(?.+?)""\)"); + Match altPk = Regex.Match(row.Sql, @"CONSTRAINT (?PK_\w+)\s.*UNIQUE \(""(?.+?)""\)"); if (altPk.Success) { var field = altPk.Groups["field"].Value; @@ -328,14 +361,14 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } else { - var identity = Regex.Match(row.Sql, @"""(?.+)"".*AUTOINCREMENT"); + Match identity = Regex.Match(row.Sql, @"""(?.+)"".*AUTOINCREMENT"); if (identity.Success) { foundConstraints.Add(new Constraint(row.Name, identity.Groups["field"].Value, $"PK_{row.Name}")); } } - var pk = Regex.Match(row.Sql, @"CONSTRAINT (?\w+)\s.*PRIMARY KEY \(""(?.+?)""\)"); + Match pk = Regex.Match(row.Sql, @"CONSTRAINT (?\w+)\s.*PRIMARY KEY \(""(?.+?)""\)"); if (pk.Success) { var field = pk.Groups["field"].Value; @@ -344,9 +377,9 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } var fkRegex = new Regex(@"CONSTRAINT (?\w+) FOREIGN KEY \(""(?.+?)""\) REFERENCES"); - var foreignKeys = fkRegex.Matches(row.Sql).Cast(); + IEnumerable foreignKeys = fkRegex.Matches(row.Sql).Cast(); { - foreach (var fk in foreignKeys) + foreach (Match fk in foreignKeys) { var field = fk.Groups["field"].Value; var constraint = fk.Groups["constraint"].Value; @@ -356,8 +389,7 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } // item.TableName, item.ColumnName, item.ConstraintName - return foundConstraints - .Select(x => Tuple.Create(x.TableName, x.ColumnName, x.ConstraintName)); + return foundConstraints.Select(x => Tuple.Create(x.TableName, x.ColumnName, x.ConstraintName)); } public override Sql.SqlJoinClause LeftJoinWithNestedJoin( @@ -410,15 +442,20 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase private class SqliteMaster { public string Type { get; set; } = null!; + public string Name { get; set; } = null!; + public string Sql { get; set; } = null!; } private class IndexMeta { public string TableName { get; set; } = null!; + public string IndexName { get; set; } = null!; + public string ColumnName { get; set; } = null!; + public bool IsUnique { get; set; } } } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs index d638f713a2..f9aa15bd77 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Persistence.Sqlite; /// -/// Automatically adds SQLite support to Umbraco when this project is referenced. +/// Automatically adds SQLite support to Umbraco when this project is referenced. /// public class SqliteComposer : IComposer { diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj index 57055ec96b..5aa062df17 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj +++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs index 8843844818..b3002fa8fe 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs @@ -14,36 +14,47 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.Sqlite; /// -/// SQLite support extensions for IUmbracoBuilder. +/// SQLite support extensions for IUmbracoBuilder. /// public static class UmbracoBuilderExtensions { /// - /// Add required services for SQLite support. + /// Add required services for SQLite support. /// public static IUmbracoBuilder AddUmbracoSqliteSupport(this IUmbracoBuilder builder) { // TryAddEnumerable takes both TService and TImplementation into consideration (unlike TryAddSingleton) builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); DbProviderFactories.UnregisterFactory(Constants.ProviderName); - DbProviderFactories.RegisterFactory(Constants.ProviderName, Microsoft.Data.Sqlite.SqliteFactory.Instance); + DbProviderFactories.RegisterFactory(Constants.ProviderName, SqliteFactory.Instance); + + // Remove this registration in Umbraco 12 + DbProviderFactories.UnregisterFactory(Constants.ProviderNameLegacy); + DbProviderFactories.RegisterFactory(Constants.ProviderNameLegacy, SqliteFactory.Instance); // Prevent accidental creation of SQLite database files builder.Services.PostConfigureAll(options => { // Skip empty connection string and other providers - if (!options.IsConnectionStringConfigured() || options.ProviderName != Constants.ProviderName) + if (!options.IsConnectionStringConfigured() || (options.ProviderName != Constants.ProviderName && options.ProviderName != Constants.ProviderNameLegacy)) { return; } diff --git a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj index 8e27992ff9..1fbbd8c42f 100644 --- a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj +++ b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj @@ -14,7 +14,7 @@ buildTransitive - + @@ -37,8 +37,6 @@ - - @@ -57,8 +55,6 @@ - - @@ -67,17 +63,5 @@ A fix was put in place in Web SDK to update for wwwwroot in case someone runs npm build etc in a target, we're borrowing their trick. https://github.com/dotnet/sdk/blob/e2b2b1a4ac56c955b84d62fe71cda3b6f258b42b/src/WebSdk/Publish/Targets/ComputeTargets/Microsoft.NET.Sdk.Publish.ComputeFiles.targets --> - - - <_UmbracoFolderFiles Include="umbraco\config\**" /> - <_UmbracoFolderFiles Include="umbraco\PartialViewMacros\**" /> - <_UmbracoFolderFiles Include="umbraco\UmbracoBackOffice\**" /> - <_UmbracoFolderFiles Include="umbraco\UmbracoInstall\**" /> - <_UmbracoFolderFiles Include="umbraco\UmbracoWebsite\**" /> - <_UmbracoFolderFiles Include="umbraco\UmbracoWebsite\**" /> - <_UmbracoFolderFiles Include="umbraco\Licenses\**" /> - - - diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml index 599dff7a2a..51a9d3d9fa 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml @@ -1,4 +1,4 @@ -@using Microsoft.Extensions.Options; +@using Microsoft.Extensions.Options; @using System.Globalization @using Umbraco.Cms.Core @using Umbraco.Cms.Core.Configuration @@ -88,7 +88,7 @@ - + diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml index 797c647049..406c311312 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml @@ -11,6 +11,7 @@ Install Umbraco + @@ -25,12 +26,12 @@
-
-
+

A server error occurred

This is most likely due to an error during application startup

diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/NoNodes.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/NoNodes.cshtml index 69d8318a1f..e773e81516 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/NoNodes.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/NoNodes.cshtml @@ -36,10 +36,10 @@
-

Easy start with Umbraco.tv

+

Easy start with Umbraco Learning Base

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

- Umbraco.tv → + Umbraco Learning Base →
diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj new file mode 100644 index 0000000000..23e8febd18 --- /dev/null +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -0,0 +1,46 @@ + + + net6.0 + false + Umbraco.Cms + Umbraco.Cms + Installs Umbraco CMS in your ASP.NET Core project + false + + + + + + + + + + + + $(ProjectDir)appsettings-schema.json + $(ProjectDir)../JsonSchema/ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/NuSpecs/buildTransitive/Umbraco.Cms.props b/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props similarity index 77% rename from build/NuSpecs/buildTransitive/Umbraco.Cms.props rename to src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props index ea0b013665..7266a05f6e 100644 --- a/build/NuSpecs/buildTransitive/Umbraco.Cms.props +++ b/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props @@ -5,4 +5,8 @@ $(DefaultItemExcludes);umbraco/Logs/** $(DefaultItemExcludes);wwwroot/media/** + + + + diff --git a/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.targets b/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.targets new file mode 100644 index 0000000000..bd0fdbf304 --- /dev/null +++ b/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.targets @@ -0,0 +1,41 @@ + + + $(MSBuildThisFileDirectory)..\appsettings-schema.json + + + + + + + + + + + + + + <_AppPluginsFiles Include="App_Plugins\**" /> + + + + + + + + <_UmbracoFolderFiles Include="umbraco\config\**" /> + <_UmbracoFolderFiles Include="umbraco\PartialViewMacros\**" /> + <_UmbracoFolderFiles Include="umbraco\UmbracoBackOffice\**" /> + <_UmbracoFolderFiles Include="umbraco\UmbracoInstall\**" /> + <_UmbracoFolderFiles Include="umbraco\UmbracoWebsite\**" /> + <_UmbracoFolderFiles Include="umbraco\Licenses\**" /> + + + + diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index 0638f605af..3bd946837f 100644 --- a/src/Umbraco.Core/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Core/Actions/ActionAssignDomain.cs @@ -9,25 +9,26 @@ namespace Umbraco.Cms.Core.Actions; public class ActionAssignDomain : IAction { /// - /// The unique action letter + /// The unique action letter /// public const char ActionLetter = 'I'; - /// + /// public char Letter => ActionLetter; - /// - public string Alias => "assignDomain"; + /// + // This is all lower-case because of case sensitive filesystems, see issue: https://github.com/umbraco/Umbraco-CMS/issues/11670 + public string Alias => "assigndomain"; - /// + /// public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; - /// - public string Icon => "home"; + /// + public string Icon => "icon-home"; - /// + /// public bool ShowInNotifier => false; - /// + /// public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionBrowse.cs b/src/Umbraco.Core/Actions/ActionBrowse.cs index 5be16a01c9..2620888a30 100644 --- a/src/Umbraco.Core/Actions/ActionBrowse.cs +++ b/src/Umbraco.Core/Actions/ActionBrowse.cs @@ -1,40 +1,41 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is used as a security constraint that grants a user the ability to view nodes in a tree +/// that has permissions applied to it. +/// +/// +/// This action should not be invoked. It is used as the minimum required permission to view nodes in the content tree. +/// By +/// granting a user this permission, the user is able to see the node in the tree but not edit the document. This may +/// be used by other trees +/// that support permissions in the future. +/// +public class ActionBrowse : IAction { /// - /// This action is used as a security constraint that grants a user the ability to view nodes in a tree - /// that has permissions applied to it. + /// The unique action letter /// - /// - /// This action should not be invoked. It is used as the minimum required permission to view nodes in the content tree. By - /// granting a user this permission, the user is able to see the node in the tree but not edit the document. This may be used by other trees - /// that support permissions in the future. - /// - public class ActionBrowse : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'F'; + public const char ActionLetter = 'F'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public bool ShowInNotifier => false; + /// + public bool ShowInNotifier => false; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Icon => string.Empty; + /// + public string Icon => string.Empty; - /// - public string Alias => "browse"; + /// + public string Alias => "browse"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionCollection.cs b/src/Umbraco.Core/Actions/ActionCollection.cs index 1e396952a2..b204075b88 100644 --- a/src/Umbraco.Core/Actions/ActionCollection.cs +++ b/src/Umbraco.Core/Actions/ActionCollection.cs @@ -1,60 +1,56 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// The collection of actions +/// +public class ActionCollection : BuilderCollectionBase { /// - /// The collection of actions + /// Initializes a new instance of the class. /// - public class ActionCollection : BuilderCollectionBase + public ActionCollection(Func> items) + : base(items) { - /// - /// Initializes a new instance of the class. - /// - public ActionCollection(Func> items) - : base(items) - { - } + } - /// - /// Gets the action of the specified type. - /// - /// The specified type to get - /// The action - public T? GetAction() - where T : IAction => this.OfType().FirstOrDefault(); + /// + /// Gets the action of the specified type. + /// + /// The specified type to get + /// The action + public T? GetAction() + where T : IAction => this.OfType().FirstOrDefault(); - /// - /// Gets the actions by the specified letters - /// - public IEnumerable GetByLetters(IEnumerable letters) - { - IAction[] actions = this.ToArray(); // no worry: internally, it's already an array - return letters - .Where(x => x.Length == 1) - .Select(x => actions.FirstOrDefault(y => y.Letter == x[0])) - .WhereNotNull() - .ToList(); - } + /// + /// Gets the actions by the specified letters + /// + public IEnumerable GetByLetters(IEnumerable letters) + { + IAction[] actions = this.ToArray(); // no worry: internally, it's already an array + return letters + .Where(x => x.Length == 1) + .Select(x => actions.FirstOrDefault(y => y.Letter == x[0])) + .WhereNotNull() + .ToList(); + } - /// - /// Gets the actions from an EntityPermission - /// - public IReadOnlyList FromEntityPermission(EntityPermission entityPermission) - { - IAction[] actions = this.ToArray(); // no worry: internally, it's already an array - return entityPermission.AssignedPermissions - .Where(x => x.Length == 1) - .SelectMany(x => actions.Where(y => y.Letter == x[0])) - .WhereNotNull() - .ToList(); - } + /// + /// Gets the actions from an EntityPermission + /// + public IReadOnlyList FromEntityPermission(EntityPermission entityPermission) + { + IAction[] actions = this.ToArray(); // no worry: internally, it's already an array + return entityPermission.AssignedPermissions + .Where(x => x.Length == 1) + .SelectMany(x => actions.Where(y => y.Letter == x[0])) + .WhereNotNull() + .ToList(); } } diff --git a/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs b/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs index 58e70e4a2a..aac1556234 100644 --- a/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs +++ b/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs @@ -1,35 +1,32 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// The action collection builder +/// +public class ActionCollectionBuilder : LazyCollectionBuilderBase { - /// - /// The action collection builder - /// - public class ActionCollectionBuilder : LazyCollectionBuilderBase + /// + protected override ActionCollectionBuilder This => this; + + /// + protected override IEnumerable CreateItems(IServiceProvider factory) { - /// - protected override ActionCollectionBuilder This => this; + var items = base.CreateItems(factory).ToList(); - /// - protected override IEnumerable CreateItems(IServiceProvider factory) + // Validate the items, no actions should exist that do not either expose notifications or permissions + var invalidItems = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList(); + if (invalidItems.Count == 0) { - var items = base.CreateItems(factory).ToList(); - - // Validate the items, no actions should exist that do not either expose notifications or permissions - var invalidItems = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList(); - if (invalidItems.Count == 0) - { - return items; - } - - var invalidActions = string.Join(", ", invalidItems.Select(x => "'" + x.Alias + "'")); - throw new InvalidOperationException($"Invalid actions {invalidActions}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}."); + return items; } + + var invalidActions = string.Join(", ", invalidItems.Select(x => "'" + x.Alias + "'")); + throw new InvalidOperationException( + $"Invalid actions {invalidActions}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}."); } } diff --git a/src/Umbraco.Core/Actions/ActionCopy.cs b/src/Umbraco.Core/Actions/ActionCopy.cs index 83a855d1ff..f7d9d699a5 100644 --- a/src/Umbraco.Core/Actions/ActionCopy.cs +++ b/src/Umbraco.Core/Actions/ActionCopy.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when copying a document, media, member +/// +public class ActionCopy : IAction { /// - /// This action is invoked when copying a document, media, member + /// The unique action letter /// - public class ActionCopy : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'O'; + public const char ActionLetter = 'O'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "copy"; + /// + public string Alias => "copy"; - /// - public string Category => Constants.Conventions.PermissionCategories.StructureCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.StructureCategory; - /// - public string Icon => "documents"; + /// + public string Icon => "icon-documents"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs index 806868af40..ee249d9ef8 100644 --- a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs +++ b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs @@ -1,29 +1,28 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when creating a blueprint from a content +/// +public class ActionCreateBlueprintFromContent : IAction { - /// - /// This action is invoked when creating a blueprint from a content - /// - public class ActionCreateBlueprintFromContent : IAction - { - /// - public char Letter => 'ï'; + /// + public char Letter => 'ï'; - /// - public bool ShowInNotifier => false; + /// + public bool ShowInNotifier => false; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Icon => "blueprint"; + /// + public string Icon => Constants.Icons.Blueprint; - /// - public string Alias => "createblueprint"; + /// + public string Alias => "createblueprint"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionDelete.cs b/src/Umbraco.Core/Actions/ActionDelete.cs index b31a8b9c45..055b2a8f98 100644 --- a/src/Umbraco.Core/Actions/ActionDelete.cs +++ b/src/Umbraco.Core/Actions/ActionDelete.cs @@ -1,39 +1,38 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document, media, member is deleted +/// +public class ActionDelete : IAction { /// - /// This action is invoked when a document, media, member is deleted + /// The unique action alias /// - public class ActionDelete : IAction - { - /// - /// The unique action alias - /// - private const string ActionAlias = "delete"; + public const string ActionAlias = "delete"; - /// - /// The unique action letter - /// - public const char ActionLetter = 'D'; + /// + /// The unique action letter + /// + public const char ActionLetter = 'D'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => ActionAlias; + /// + public string Alias => ActionAlias; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "delete"; + /// + public string Icon => "icon-delete"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionMove.cs b/src/Umbraco.Core/Actions/ActionMove.cs index 0f8b4b8305..c1f6089793 100644 --- a/src/Umbraco.Core/Actions/ActionMove.cs +++ b/src/Umbraco.Core/Actions/ActionMove.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked upon creation of a document, media, member +/// +public class ActionMove : IAction { /// - /// This action is invoked upon creation of a document, media, member + /// The unique action letter /// - public class ActionMove : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'M'; + public const char ActionLetter = 'M'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "move"; + /// + public string Alias => "move"; - /// - public string Category => Constants.Conventions.PermissionCategories.StructureCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.StructureCategory; - /// - public string Icon => "enter"; + /// + public string Icon => "icon-enter"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionNew.cs b/src/Umbraco.Core/Actions/ActionNew.cs index 25e85cd377..bd08a95ac5 100644 --- a/src/Umbraco.Core/Actions/ActionNew.cs +++ b/src/Umbraco.Core/Actions/ActionNew.cs @@ -1,39 +1,38 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked upon creation of a document +/// +public class ActionNew : IAction { /// - /// This action is invoked upon creation of a document + /// The unique action alias /// - public class ActionNew : IAction - { - /// - /// The unique action alias - /// - public const string ActionAlias = "create"; + public const string ActionAlias = "create"; - /// - /// The unique action letter - /// - public const char ActionLetter = 'C'; + /// + /// The unique action letter + /// + public const char ActionLetter = 'C'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => ActionAlias; + /// + public string Alias => ActionAlias; - /// - public string Icon => "add"; + /// + public string Icon => "icon-add"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionNotify.cs b/src/Umbraco.Core/Actions/ActionNotify.cs index 3f1e855cff..10845b01b9 100644 --- a/src/Umbraco.Core/Actions/ActionNotify.cs +++ b/src/Umbraco.Core/Actions/ActionNotify.cs @@ -1,29 +1,28 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked upon modifying the notification of a content +/// +public class ActionNotify : IAction { - /// - /// This action is invoked upon modifying the notification of a content - /// - public class ActionNotify : IAction - { - /// - public char Letter => 'N'; + /// + public char Letter => 'N'; - /// - public bool ShowInNotifier => false; + /// + public bool ShowInNotifier => false; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Icon => "megaphone"; + /// + public string Icon => "icon-megaphone"; - /// - public string Alias => "notify"; + /// + public string Alias => "notify"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionProtect.cs b/src/Umbraco.Core/Actions/ActionProtect.cs index 10684a69e2..7b54a0ec98 100644 --- a/src/Umbraco.Core/Actions/ActionProtect.cs +++ b/src/Umbraco.Core/Actions/ActionProtect.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document is protected or unprotected +/// +public class ActionProtect : IAction { /// - /// This action is invoked when a document is protected or unprotected + /// The unique action letter /// - public class ActionProtect : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'P'; + public const char ActionLetter = 'P'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "protect"; + /// + public string Alias => "protect"; - /// - public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; - /// - public string Icon => "lock"; + /// + public string Icon => "icon-lock"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionPublish.cs b/src/Umbraco.Core/Actions/ActionPublish.cs index 02f77d6862..e07b0935bc 100644 --- a/src/Umbraco.Core/Actions/ActionPublish.cs +++ b/src/Umbraco.Core/Actions/ActionPublish.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document is being published +/// +public class ActionPublish : IAction { /// - /// This action is invoked when a document is being published + /// The unique action letter /// - public class ActionPublish : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'U'; + public const char ActionLetter = 'U'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "publish"; + /// + public string Alias => "publish"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => string.Empty; + /// + public string Icon => string.Empty; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionRestore.cs b/src/Umbraco.Core/Actions/ActionRestore.cs index 164c93e2d5..395c678fe4 100644 --- a/src/Umbraco.Core/Actions/ActionRestore.cs +++ b/src/Umbraco.Core/Actions/ActionRestore.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when the content/media item is to be restored from the recycle bin +/// +public class ActionRestore : IAction { /// - /// This action is invoked when the content/media item is to be restored from the recycle bin + /// The unique action alias /// - public class ActionRestore : IAction - { - /// - /// The unique action alias - /// - public const string ActionAlias = "restore"; + public const string ActionAlias = "restore"; - /// - public char Letter => 'V'; + /// + public char Letter => 'V'; - /// - public string Alias => ActionAlias; + /// + public string Alias => ActionAlias; - /// - public string? Category => null; + /// + public string? Category => null; - /// - public string Icon => "undo"; + /// + public string Icon => "icon-undo"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => false; - } + /// + public bool CanBePermissionAssigned => false; } diff --git a/src/Umbraco.Core/Actions/ActionRights.cs b/src/Umbraco.Core/Actions/ActionRights.cs index fff7cc8652..4cd8674122 100644 --- a/src/Umbraco.Core/Actions/ActionRights.cs +++ b/src/Umbraco.Core/Actions/ActionRights.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when rights are changed on a document +/// +public class ActionRights : IAction { /// - /// This action is invoked when rights are changed on a document + /// The unique action letter /// - public class ActionRights : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'R'; + public const char ActionLetter = 'R'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "rights"; + /// + public string Alias => "rights"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "vcard"; + /// + public string Icon => "icon-vcard"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionRollback.cs b/src/Umbraco.Core/Actions/ActionRollback.cs index 565a8469c5..63021a2ae3 100644 --- a/src/Umbraco.Core/Actions/ActionRollback.cs +++ b/src/Umbraco.Core/Actions/ActionRollback.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when copying a document is being rolled back +/// +public class ActionRollback : IAction { /// - /// This action is invoked when copying a document is being rolled back + /// The unique action letter /// - public class ActionRollback : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'K'; + public const char ActionLetter = 'K'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "rollback"; + /// + public string Alias => "rollback"; - /// - public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; - /// - public string Icon => "undo"; + /// + public string Icon => "icon-undo"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionSort.cs b/src/Umbraco.Core/Actions/ActionSort.cs index 1f87bfcc3c..23e65d7533 100644 --- a/src/Umbraco.Core/Actions/ActionSort.cs +++ b/src/Umbraco.Core/Actions/ActionSort.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when children to a document, media, member is being sorted +/// +public class ActionSort : IAction { /// - /// This action is invoked when children to a document, media, member is being sorted + /// The unique action letter /// - public class ActionSort : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'S'; + public const char ActionLetter = 'S'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "sort"; + /// + public string Alias => "sort"; - /// - public string Category => Constants.Conventions.PermissionCategories.StructureCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.StructureCategory; - /// - public string Icon => "navigation-vertical"; + /// + public string Icon => "icon-navigation-vertical"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionToPublish.cs b/src/Umbraco.Core/Actions/ActionToPublish.cs index 654b71661d..8df53b3e4a 100644 --- a/src/Umbraco.Core/Actions/ActionToPublish.cs +++ b/src/Umbraco.Core/Actions/ActionToPublish.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when children to a document is being sent to published (by an editor without publishrights) +/// +public class ActionToPublish : IAction { /// - /// This action is invoked when children to a document is being sent to published (by an editor without publishrights) + /// The unique action letter /// - public class ActionToPublish : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'H'; + public const char ActionLetter = 'H'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "sendtopublish"; + /// + public string Alias => "sendtopublish"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "outbox"; + /// + public string Icon => "icon-outbox"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionUnpublish.cs b/src/Umbraco.Core/Actions/ActionUnpublish.cs index 6e9ec8506b..f8ebb918f9 100644 --- a/src/Umbraco.Core/Actions/ActionUnpublish.cs +++ b/src/Umbraco.Core/Actions/ActionUnpublish.cs @@ -1,35 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document is being unpublished +/// +public class ActionUnpublish : IAction { /// - /// This action is invoked when a document is being unpublished + /// The unique action letter /// - public class ActionUnpublish : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'Z'; + public const char ActionLetter = 'Z'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "unpublish"; + /// + public string Alias => "unpublish"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "circle-dotted"; + /// + public string Icon => "icon-circle-dotted"; - /// - public bool ShowInNotifier => false; - - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool ShowInNotifier => false; + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionUpdate.cs b/src/Umbraco.Core/Actions/ActionUpdate.cs index 3f8092c1fc..2d01ef176c 100644 --- a/src/Umbraco.Core/Actions/ActionUpdate.cs +++ b/src/Umbraco.Core/Actions/ActionUpdate.cs @@ -1,34 +1,33 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when copying a document or media +/// +public class ActionUpdate : IAction { /// - /// This action is invoked when copying a document or media + /// The unique action letter /// - public class ActionUpdate : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'A'; + public const char ActionLetter = 'A'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "update"; + /// + public string Alias => "update"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "save"; + /// + public string Icon => "icon-save"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/IAction.cs b/src/Umbraco.Core/Actions/IAction.cs index 2d9876afc6..f57e697a2e 100644 --- a/src/Umbraco.Core/Actions/IAction.cs +++ b/src/Umbraco.Core/Actions/IAction.cs @@ -1,49 +1,48 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// Defines a back office action that can be permission assigned or subscribed to for notifications +/// +/// +/// If an IAction returns false for both ShowInNotifier and CanBePermissionAssigned then the IAction should not exist +/// +public interface IAction : IDiscoverable { /// - /// Defines a back office action that can be permission assigned or subscribed to for notifications + /// Gets the letter used to assign a permission (must be unique) + /// + char Letter { get; } + + /// + /// Gets a value indicating whether whether to allow subscribing to notifications for this action + /// + bool ShowInNotifier { get; } + + /// + /// Gets a value indicating whether whether to allow assigning permissions based on this action + /// + bool CanBePermissionAssigned { get; } + + /// + /// Gets the icon to display for this action + /// + string Icon { get; } + + /// + /// Gets the alias for this action (must be unique) + /// + string Alias { get; } + + /// + /// Gets the category used for this action /// /// - /// If an IAction returns false for both ShowInNotifier and CanBePermissionAssigned then the IAction should not exist + /// Used in the UI when assigning permissions /// - public interface IAction : IDiscoverable - { - /// - /// Gets the letter used to assign a permission (must be unique) - /// - char Letter { get; } - - /// - /// Gets a value indicating whether whether to allow subscribing to notifications for this action - /// - bool ShowInNotifier { get; } - - /// - /// Gets a value indicating whether whether to allow assigning permissions based on this action - /// - bool CanBePermissionAssigned { get; } - - /// - /// Gets the icon to display for this action - /// - string Icon { get; } - - /// - /// Gets the alias for this action (must be unique) - /// - string Alias { get; } - - /// - /// Gets the category used for this action - /// - /// - /// Used in the UI when assigning permissions - /// - string? Category { get; } - } + string? Category { get; } } diff --git a/src/Umbraco.Core/Attempt.cs b/src/Umbraco.Core/Attempt.cs index 71eabd2f0d..7a438dece6 100644 --- a/src/Umbraco.Core/Attempt.cs +++ b/src/Umbraco.Core/Attempt.cs @@ -1,126 +1,108 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Provides ways to create attempts. +/// +public static class Attempt { + // note: + // cannot rely on overloads only to differentiate between with/without status + // in some cases it will always be ambiguous, so be explicit w/ 'WithStatus' methods + /// - /// Provides ways to create attempts. + /// Creates a successful attempt with a result. /// - public static class Attempt - { - // note: - // cannot rely on overloads only to differentiate between with/without status - // in some cases it will always be ambiguous, so be explicit w/ 'WithStatus' methods + /// The type of the attempted operation result. + /// The result of the attempt. + /// The successful attempt. + public static Attempt Succeed(TResult? result) => Attempt.Succeed(result); - /// - /// Creates a successful attempt with a result. - /// - /// The type of the attempted operation result. - /// The result of the attempt. - /// The successful attempt. - public static Attempt Succeed(TResult? result) - { - return Attempt.Succeed(result); - } + /// + /// Creates a successful attempt with a result and a status. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// The status of the attempt. + /// The result of the attempt. + /// The successful attempt. + public static Attempt SucceedWithStatus(TStatus status, TResult result) => + Attempt.Succeed(status, result); - /// - /// Creates a successful attempt with a result and a status. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// The status of the attempt. - /// The result of the attempt. - /// The successful attempt. - public static Attempt SucceedWithStatus(TStatus status, TResult result) - { - return Attempt.Succeed(status, result); - } + /// + /// Creates a failed attempt. + /// + /// The type of the attempted operation result. + /// The failed attempt. + public static Attempt Fail() => Attempt.Fail(); - /// - /// Creates a failed attempt. - /// - /// The type of the attempted operation result. - /// The failed attempt. - public static Attempt Fail() - { - return Attempt.Fail(); - } + /// + /// Creates a failed attempt with a result. + /// + /// The type of the attempted operation result. + /// The result of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result) => Attempt.Fail(result); - /// - /// Creates a failed attempt with a result. - /// - /// The type of the attempted operation result. - /// The result of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result) - { - return Attempt.Fail(result); - } + /// + /// Creates a failed attempt with a result and a status. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// The status of the attempt. + /// The result of the attempt. + /// The failed attempt. + public static Attempt FailWithStatus(TStatus status, TResult result) => + Attempt.Fail(status, result); - /// - /// Creates a failed attempt with a result and a status. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// The status of the attempt. - /// The result of the attempt. - /// The failed attempt. - public static Attempt FailWithStatus(TStatus status, TResult result) - { - return Attempt.Fail(status, result); - } + /// + /// Creates a failed attempt with a result and an exception. + /// + /// The type of the attempted operation result. + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result, Exception exception) => + Attempt.Fail(result, exception); - /// - /// Creates a failed attempt with a result and an exception. - /// - /// The type of the attempted operation result. - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result, Exception exception) - { - return Attempt.Fail(result, exception); - } + /// + /// Creates a failed attempt with a result, an exception and a status. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// The status of the attempt. + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt FailWithStatus(TStatus status, TResult result, Exception exception) => Attempt.Fail(status, result, exception); + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// The type of the attempted operation result. + /// A value indicating whether the attempt is successful. + /// The result of the attempt. + /// The attempt. + public static Attempt If(bool condition, TResult result) => + Attempt.If(condition, result); - /// - /// Creates a failed attempt with a result, an exception and a status. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// The status of the attempt. - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt FailWithStatus(TStatus status, TResult result, Exception exception) - { - return Attempt.Fail(status, result, exception); - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// The type of the attempted operation result. - /// A value indicating whether the attempt is successful. - /// The result of the attempt. - /// The attempt. - public static Attempt If(bool condition, TResult result) - { - return Attempt.If(condition, result); - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// A value indicating whether the attempt is successful. - /// The status of the successful attempt. - /// The status of the failed attempt. - /// The result of the attempt. - /// The attempt. - public static Attempt IfWithStatus(bool condition, TStatus succStatus, TStatus failStatus, TResult result) - { - return Attempt.If(condition, succStatus, failStatus, result); - } - } + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// A value indicating whether the attempt is successful. + /// The status of the successful attempt. + /// The status of the failed attempt. + /// The result of the attempt. + /// The attempt. + public static Attempt IfWithStatus( + bool condition, + TStatus succStatus, + TStatus failStatus, + TResult result) => + Attempt.If( + condition, + succStatus, + failStatus, + result); } diff --git a/src/Umbraco.Core/AttemptOfTResult.cs b/src/Umbraco.Core/AttemptOfTResult.cs index 5cf85964cc..2969755d94 100644 --- a/src/Umbraco.Core/AttemptOfTResult.cs +++ b/src/Umbraco.Core/AttemptOfTResult.cs @@ -1,141 +1,111 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents the result of an operation attempt. +/// +/// The type of the attempted operation result. +[Serializable] +public struct Attempt { - /// - /// Represents the result of an operation attempt. - /// - /// The type of the attempted operation result. - [Serializable] - public struct Attempt + // optimize, use a singleton failed attempt + private static readonly Attempt Failed = new(false, default, null); + + // private - use Succeed() or Fail() methods to create attempts + private Attempt(bool success, TResult? result, Exception? exception) { - // private - use Succeed() or Fail() methods to create attempts - private Attempt(bool success, TResult? result, Exception? exception) - { - Success = success; - Result = result; - Exception = exception; - } - - /// - /// Gets a value indicating whether this was successful. - /// - public bool Success { get; } - - /// - /// Gets the exception associated with an unsuccessful attempt. - /// - public Exception? Exception { get; } - - /// - /// Gets the attempt result. - /// - public TResult? Result { get; } - - /// - /// Gets the attempt result, if successful, else a default value. - /// - public TResult ResultOr(TResult value) - { - if (Success && Result is not null) - { - return Result; - } - - return value; - } - - // optimize, use a singleton failed attempt - private static readonly Attempt Failed = new Attempt(false, default(TResult), null); - - /// - /// Creates a successful attempt. - /// - /// The successful attempt. - public static Attempt Succeed() - { - return new Attempt(true, default(TResult), null); - } - - /// - /// Creates a successful attempt with a result. - /// - /// The result of the attempt. - /// The successful attempt. - public static Attempt Succeed(TResult? result) - { - return new Attempt(true, result, null); - } - - /// - /// Creates a failed attempt. - /// - /// The failed attempt. - public static Attempt Fail() - { - return Failed; - } - - /// - /// Creates a failed attempt with an exception. - /// - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(Exception? exception) - { - return new Attempt(false, default(TResult), exception); - } - - /// - /// Creates a failed attempt with a result. - /// - /// The result of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result) - { - return new Attempt(false, result, null); - } - - /// - /// Creates a failed attempt with a result and an exception. - /// - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result, Exception exception) - { - return new Attempt(false, result, exception); - } - - /// - /// Creates a successful or a failed attempt. - /// - /// A value indicating whether the attempt is successful. - /// The attempt. - public static Attempt If(bool condition) - { - return condition ? new Attempt(true, default(TResult), null) : Failed; - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// A value indicating whether the attempt is successful. - /// The result of the attempt. - /// The attempt. - public static Attempt If(bool condition, TResult? result) - { - return new Attempt(condition, result, null); - } - - /// - /// Implicitly operator to check if the attempt was successful without having to access the 'success' property - /// - /// - /// - public static implicit operator bool(Attempt a) - { - return a.Success; - } + Success = success; + Result = result; + Exception = exception; } + + /// + /// Gets a value indicating whether this was successful. + /// + public bool Success { get; } + + /// + /// Gets the exception associated with an unsuccessful attempt. + /// + public Exception? Exception { get; } + + /// + /// Gets the attempt result. + /// + public TResult? Result { get; } + + /// + /// Implicitly operator to check if the attempt was successful without having to access the 'success' property + /// + /// + /// + public static implicit operator bool(Attempt a) => a.Success; + + /// + /// Gets the attempt result, if successful, else a default value. + /// + public TResult ResultOr(TResult value) + { + if (Success && Result is not null) + { + return Result; + } + + return value; + } + + /// + /// Creates a successful attempt. + /// + /// The successful attempt. + public static Attempt Succeed() => new(true, default, null); + + /// + /// Creates a successful attempt with a result. + /// + /// The result of the attempt. + /// The successful attempt. + public static Attempt Succeed(TResult? result) => new(true, result, null); + + /// + /// Creates a failed attempt. + /// + /// The failed attempt. + public static Attempt Fail() => Failed; + + /// + /// Creates a failed attempt with an exception. + /// + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(Exception? exception) => new(false, default, exception); + + /// + /// Creates a failed attempt with a result. + /// + /// The result of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result) => new(false, result, null); + + /// + /// Creates a failed attempt with a result and an exception. + /// + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result, Exception exception) => new(false, result, exception); + + /// + /// Creates a successful or a failed attempt. + /// + /// A value indicating whether the attempt is successful. + /// The attempt. + public static Attempt If(bool condition) => condition ? new Attempt(true, default, null) : Failed; + + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// A value indicating whether the attempt is successful. + /// The result of the attempt. + /// The attempt. + public static Attempt If(bool condition, TResult? result) => new(condition, result, null); } diff --git a/src/Umbraco.Core/AttemptOfTResultTStatus.cs b/src/Umbraco.Core/AttemptOfTResultTStatus.cs index 65a3e48334..e88465b3ad 100644 --- a/src/Umbraco.Core/AttemptOfTResultTStatus.cs +++ b/src/Umbraco.Core/AttemptOfTResultTStatus.cs @@ -1,142 +1,121 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents the result of an operation attempt. +/// +/// The type of the attempted operation result. +/// The type of the attempted operation status. +[Serializable] +public struct Attempt { - /// - /// Represents the result of an operation attempt. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - [Serializable] - public struct Attempt + // private - use Succeed() or Fail() methods to create attempts + private Attempt(bool success, TResult result, TStatus status, Exception? exception) { - /// - /// Gets a value indicating whether this was successful. - /// - public bool Success { get; } - - /// - /// Gets the exception associated with an unsuccessful attempt. - /// - public Exception? Exception { get; } - - /// - /// Gets the attempt result. - /// - public TResult Result { get; } - - /// - /// Gets the attempt status. - /// - public TStatus Status { get; } - - // private - use Succeed() or Fail() methods to create attempts - private Attempt(bool success, TResult result, TStatus status, Exception? exception) - { - Success = success; - Result = result; - Status = status; - Exception = exception; - } - - /// - /// Creates a successful attempt. - /// - /// The status of the attempt. - /// The successful attempt. - public static Attempt Succeed(TStatus status) - { - return new Attempt(true, default(TResult), status, null); - } - - /// - /// Creates a successful attempt with a result. - /// - /// The status of the attempt. - /// The result of the attempt. - /// The successful attempt. - public static Attempt Succeed(TStatus status, TResult result) - { - return new Attempt(true, result, status, null); - } - - /// - /// Creates a failed attempt. - /// - /// The status of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status) - { - return new Attempt(false, default(TResult), status, null); - } - - /// - /// Creates a failed attempt with an exception. - /// - /// The status of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status, Exception exception) - { - return new Attempt(false, default(TResult), status, exception); - } - - /// - /// Creates a failed attempt with a result. - /// - /// The status of the attempt. - /// The result of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status, TResult result) - { - return new Attempt(false, result, status, null); - } - - /// - /// Creates a failed attempt with a result and an exception. - /// - /// The status of the attempt. - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status, TResult result, Exception exception) - { - return new Attempt(false, result, status, exception); - } - - /// - /// Creates a successful or a failed attempt. - /// - /// A value indicating whether the attempt is successful. - /// The status of the successful attempt. - /// The status of the failed attempt. - /// The attempt. - public static Attempt If(bool condition, TStatus succStatus, TStatus failStatus) - { - return new Attempt(condition, default(TResult), condition ? succStatus : failStatus, null); - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// A value indicating whether the attempt is successful. - /// The status of the successful attempt. - /// The status of the failed attempt. - /// The result of the attempt. - /// The attempt. - public static Attempt If(bool condition, TStatus succStatus, TStatus failStatus, TResult result) - { - return new Attempt(condition, result, condition ? succStatus : failStatus, null); - } - - /// - /// Implicitly operator to check if the attempt was successful without having to access the 'success' property - /// - /// - /// - public static implicit operator bool(Attempt a) - { - return a.Success; - } + Success = success; + Result = result; + Status = status; + Exception = exception; } + + /// + /// Gets a value indicating whether this was successful. + /// + public bool Success { get; } + + /// + /// Gets the exception associated with an unsuccessful attempt. + /// + public Exception? Exception { get; } + + /// + /// Gets the attempt result. + /// + public TResult Result { get; } + + /// + /// Gets the attempt status. + /// + public TStatus Status { get; } + + /// + /// Implicitly operator to check if the attempt was successful without having to access the 'success' property + /// + /// + /// + public static implicit operator bool(Attempt a) => a.Success; + + /// + /// Creates a successful attempt. + /// + /// The status of the attempt. + /// The successful attempt. + public static Attempt Succeed(TStatus status) => + new Attempt(true, default, status, null); + + /// + /// Creates a successful attempt with a result. + /// + /// The status of the attempt. + /// The result of the attempt. + /// The successful attempt. + public static Attempt Succeed(TStatus status, TResult result) => + new Attempt(true, result, status, null); + + /// + /// Creates a failed attempt. + /// + /// The status of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status) => + new Attempt(false, default, status, null); + + /// + /// Creates a failed attempt with an exception. + /// + /// The status of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status, Exception exception) => + new Attempt(false, default, status, exception); + + /// + /// Creates a failed attempt with a result. + /// + /// The status of the attempt. + /// The result of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status, TResult result) => + new Attempt(false, result, status, null); + + /// + /// Creates a failed attempt with a result and an exception. + /// + /// The status of the attempt. + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status, TResult result, Exception exception) => + new Attempt(false, result, status, exception); + + /// + /// Creates a successful or a failed attempt. + /// + /// A value indicating whether the attempt is successful. + /// The status of the successful attempt. + /// The status of the failed attempt. + /// The attempt. + public static Attempt If(bool condition, TStatus succStatus, TStatus failStatus) => + new Attempt(condition, default, condition ? succStatus : failStatus, null); + + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// A value indicating whether the attempt is successful. + /// The status of the successful attempt. + /// The status of the failed attempt. + /// The result of the attempt. + /// The attempt. + public static Attempt + If(bool condition, TStatus succStatus, TStatus failStatus, TResult result) => + new Attempt(condition, result, condition ? succStatus : failStatus, null); } diff --git a/src/Umbraco.Core/Cache/AppCacheExtensions.cs b/src/Umbraco.Core/Cache/AppCacheExtensions.cs index f5e92cc116..0f1f242ed0 100644 --- a/src/Umbraco.Core/Cache/AppCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/AppCacheExtensions.cs @@ -1,66 +1,64 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extensions for strongly typed access +/// +public static class AppCacheExtensions { - /// - /// Extensions for strongly typed access - /// - public static class AppCacheExtensions + public static T? GetCacheItem( + this IAppPolicyCache provider, + string cacheKey, + Func getCacheItem, + TimeSpan? timeout, + bool isSliding = false, + string[]? dependentFiles = null) { - public static T? GetCacheItem(this IAppPolicyCache provider, - string cacheKey, - Func getCacheItem, - TimeSpan? timeout, - bool isSliding = false, - string[]? dependentFiles = null) + var result = provider.Get(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); + return result == null ? default : result.TryConvertTo().Result; + } + + public static void InsertCacheItem( + this IAppPolicyCache provider, + string cacheKey, + Func getCacheItem, + TimeSpan? timeout = null, + bool isSliding = false, + string[]? dependentFiles = null) => + provider.Insert(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); + + public static IEnumerable GetCacheItemsByKeySearch(this IAppCache provider, string keyStartsWith) + { + IEnumerable result = provider.SearchByKey(keyStartsWith); + return result.Select(x => x.TryConvertTo().Result); + } + + public static IEnumerable GetCacheItemsByKeyExpression(this IAppCache provider, string regexString) + { + IEnumerable result = provider.SearchByRegex(regexString); + return result.Select(x => x.TryConvertTo().Result); + } + + public static T? GetCacheItem(this IAppCache provider, string cacheKey) + { + var result = provider.Get(cacheKey); + if (result == null) { - var result = provider.Get(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); - return result == null ? default(T) : result.TryConvertTo().Result; + return default; } - public static void InsertCacheItem(this IAppPolicyCache provider, - string cacheKey, - Func getCacheItem, - TimeSpan? timeout = null, - bool isSliding = false, - string[]? dependentFiles = null) + return result.TryConvertTo().Result; + } + + public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) + { + var result = provider.Get(cacheKey, () => getCacheItem()); + if (result == null) { - provider.Insert(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); + return default; } - public static IEnumerable GetCacheItemsByKeySearch(this IAppCache provider, string keyStartsWith) - { - var result = provider.SearchByKey(keyStartsWith); - return result.Select(x => x.TryConvertTo().Result); - } - - public static IEnumerable GetCacheItemsByKeyExpression(this IAppCache provider, string regexString) - { - var result = provider.SearchByRegex(regexString); - return result.Select(x => x.TryConvertTo().Result); - } - - public static T? GetCacheItem(this IAppCache provider, string cacheKey) - { - var result = provider.Get(cacheKey); - if (result == null) - { - return default(T); - } - return result.TryConvertTo().Result; - } - - public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) - { - var result = provider.Get(cacheKey, () => getCacheItem()); - if (result == null) - { - return default(T); - } - return result.TryConvertTo().Result; - } + return result.TryConvertTo().Result; } } diff --git a/src/Umbraco.Core/Cache/AppCaches.cs b/src/Umbraco.Core/Cache/AppCaches.cs index a04ece0d04..faca2e14f4 100644 --- a/src/Umbraco.Core/Cache/AppCaches.cs +++ b/src/Umbraco.Core/Cache/AppCaches.cs @@ -1,100 +1,97 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents the application caches. +/// +public class AppCaches : IDisposable { + private bool _disposedValue; + /// - /// Represents the application caches. + /// Initializes a new instance of the with cache providers. /// - public class AppCaches : IDisposable + public AppCaches( + IAppPolicyCache runtimeCache, + IRequestCache requestCache, + IsolatedCaches isolatedCaches) { - private bool _disposedValue; + RuntimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); + RequestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + IsolatedCaches = isolatedCaches ?? throw new ArgumentNullException(nameof(isolatedCaches)); + } - /// - /// Initializes a new instance of the with cache providers. - /// - public AppCaches( - IAppPolicyCache runtimeCache, - IRequestCache requestCache, - IsolatedCaches isolatedCaches) + /// + /// Gets the special disabled instance. + /// + /// + /// When used by repositories, all cache policies apply, but the underlying caches do not cache anything. + /// Used by tests. + /// + public static AppCaches Disabled { get; } = new(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); + + /// + /// Gets the special no-cache instance. + /// + /// + /// When used by repositories, all cache policies are bypassed. + /// Used by repositories that do no cache. + /// + public static AppCaches NoCache { get; } = new(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); + + /// + /// Gets the per-request cache. + /// + /// + /// The per-request caches works on top of the current HttpContext items. + /// Outside a web environment, the behavior of that cache is unspecified. + /// + public IRequestCache RequestCache { get; } + + /// + /// Gets the runtime cache. + /// + /// + /// The runtime cache is the main application cache. + /// + public IAppPolicyCache RuntimeCache { get; } + + /// + /// Gets the isolated caches. + /// + /// + /// + /// Isolated caches are used by e.g. repositories, to ensure that each cached entity + /// type has its own cache, so that lookups are fast and the repository does not need to + /// search through all keys on a global scale. + /// + /// + public IsolatedCaches IsolatedCaches { get; } + + public static AppCaches Create(IRequestCache requestCache) => + new( + new DeepCloneAppCache(new ObjectCacheAppCache()), + requestCache, + new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - RuntimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); - RequestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - IsolatedCaches = isolatedCaches ?? throw new ArgumentNullException(nameof(isolatedCaches)); - } - - /// - /// Gets the special disabled instance. - /// - /// - /// When used by repositories, all cache policies apply, but the underlying caches do not cache anything. - /// Used by tests. - /// - public static AppCaches Disabled { get; } = new AppCaches(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); - - /// - /// Gets the special no-cache instance. - /// - /// - /// When used by repositories, all cache policies are bypassed. - /// Used by repositories that do no cache. - /// - public static AppCaches NoCache { get; } = new AppCaches(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); - - /// - /// Gets the per-request cache. - /// - /// - /// The per-request caches works on top of the current HttpContext items. - /// Outside a web environment, the behavior of that cache is unspecified. - /// - public IRequestCache RequestCache { get; } - - /// - /// Gets the runtime cache. - /// - /// - /// The runtime cache is the main application cache. - /// - public IAppPolicyCache RuntimeCache { get; } - - /// - /// Gets the isolated caches. - /// - /// - /// Isolated caches are used by e.g. repositories, to ensure that each cached entity - /// type has its own cache, so that lookups are fast and the repository does not need to - /// search through all keys on a global scale. - /// - public IsolatedCaches IsolatedCaches { get; } - - public static AppCaches Create(IRequestCache requestCache) - { - return new AppCaches( - new DeepCloneAppCache(new ObjectCacheAppCache()), - requestCache, - new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - RuntimeCache.DisposeIfDisposable(); - RequestCache.DisposeIfDisposable(); - IsolatedCaches.Dispose(); - } - - _disposedValue = true; + RuntimeCache.DisposeIfDisposable(); + RequestCache.DisposeIfDisposable(); + IsolatedCaches.Dispose(); } - } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _disposedValue = true; } } } diff --git a/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs b/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs index 53e45bbb2e..1cf3b1461e 100644 --- a/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs +++ b/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs @@ -1,99 +1,93 @@ -using System; using System.Collections.Concurrent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Provides a base class for implementing a dictionary of . +/// +/// The type of the dictionary key. +public abstract class AppPolicedCacheDictionary : IDisposable + where TKey : notnull { /// - /// Provides a base class for implementing a dictionary of . + /// Gets the internal cache factory, for tests only! /// - /// The type of the dictionary key. - public abstract class AppPolicedCacheDictionary : IDisposable - where TKey : notnull + private readonly Func _cacheFactory; + + private readonly ConcurrentDictionary _caches = new(); + private bool _disposedValue; + + /// + /// Initializes a new instance of the class. + /// + /// + protected AppPolicedCacheDictionary(Func cacheFactory) => _cacheFactory = cacheFactory; + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + /// + /// Gets or creates a cache. + /// + public IAppPolicyCache GetOrCreate(TKey key) + => _caches.GetOrAdd(key, k => _cacheFactory(k)); + + /// + /// Removes a cache. + /// + public void Remove(TKey key) => _caches.TryRemove(key, out _); + + /// + /// Removes all caches. + /// + public void RemoveAll() => _caches.Clear(); + + /// + /// Clears all caches. + /// + public void ClearAllCaches() { - private readonly ConcurrentDictionary _caches = new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - /// - protected AppPolicedCacheDictionary(Func cacheFactory) + foreach (IAppPolicyCache cache in _caches.Values) { - _cacheFactory = cacheFactory; + cache.Clear(); } + } - /// - /// Gets the internal cache factory, for tests only! - /// - private readonly Func _cacheFactory; - private bool _disposedValue; + /// + /// Tries to get a cache. + /// + protected Attempt Get(TKey key) + => _caches.TryGetValue(key, out IAppPolicyCache? cache) + ? Attempt.Succeed(cache) + : Attempt.Fail(); - /// - /// Gets or creates a cache. - /// - public IAppPolicyCache GetOrCreate(TKey key) - => _caches.GetOrAdd(key, k => _cacheFactory(k)); - - /// - /// Tries to get a cache. - /// - protected Attempt Get(TKey key) - => _caches.TryGetValue(key, out var cache) ? Attempt.Succeed(cache) : Attempt.Fail(); - - /// - /// Removes a cache. - /// - public void Remove(TKey key) + /// + /// Clears a cache. + /// + protected void ClearCache(TKey key) + { + if (_caches.TryGetValue(key, out IAppPolicyCache? cache)) { - _caches.TryRemove(key, out _); + cache.Clear(); } + } - /// - /// Removes all caches. - /// - public void RemoveAll() + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - _caches.Clear(); - } - - /// - /// Clears a cache. - /// - protected void ClearCache(TKey key) - { - if (_caches.TryGetValue(key, out var cache)) - cache.Clear(); - } - - /// - /// Clears all caches. - /// - public void ClearAllCaches() - { - foreach (var cache in _caches.Values) - cache.Clear(); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) + if (disposing) { - if (disposing) + foreach (IAppPolicyCache value in _caches.Values) { - foreach(var value in _caches.Values) - { - value.DisposeIfDisposable(); - } + value.DisposeIfDisposable(); } - - _disposedValue = true; } - } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _disposedValue = true; } } } diff --git a/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs b/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs index 582915fb2e..11ddd8a183 100644 --- a/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs @@ -1,46 +1,36 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ApplicationCacheRefresher : CacheRefresherBase { - public sealed class ApplicationCacheRefresher : CacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("B15F34A1-BC1D-4F8B-8369-3222728AB4C8"); + + public ApplicationCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public ApplicationCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - } + } - #region Define + public override Guid RefresherUniqueId => UniqueId; - public static readonly Guid UniqueId = Guid.Parse("B15F34A1-BC1D-4F8B-8369-3222728AB4C8"); + public override string Name => "Application Cache Refresher"; - public override Guid RefresherUniqueId => UniqueId; + public override void RefreshAll() + { + AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); + base.RefreshAll(); + } - public override string Name => "Application Cache Refresher"; + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } - #endregion - - #region Refresher - - public override void RefreshAll() - { - AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); - base.Remove(id); - } - - #endregion + public override void Remove(int id) + { + AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); + base.Remove(id); } } diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index acabe0fcc4..04ae44a647 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Constants storing cache keys used in caching +/// +public static class CacheKeys { - /// - /// Constants storing cache keys used in caching - /// - public static class CacheKeys - { - public const string ApplicationsCacheKey = "ApplicationCache"; // used by SectionService + public const string ApplicationsCacheKey = "ApplicationCache"; // used by SectionService - // TODO: this one can probably be removed - public const string TemplateFrontEndCacheKey = "template"; + // TODO: this one can probably be removed + public const string TemplateFrontEndCacheKey = "template"; - public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers - public const string MacroFromAliasCacheKey = "macroFromAlias_"; + public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers + public const string MacroFromAliasCacheKey = "macroFromAlias_"; - public const string UserGroupGetByAliasCacheKeyPrefix = "UserGroupRepository_GetByAlias_"; + public const string UserGroupGetByAliasCacheKeyPrefix = "UserGroupRepository_GetByAlias_"; - public const string UserAllContentStartNodesPrefix = "AllContentStartNodes"; - public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; - public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; - public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; - - public const string ContentRecycleBinCacheKey = "recycleBin_content"; - public const string MediaRecycleBinCacheKey = "recycleBin_media"; - } + public const string UserAllContentStartNodesPrefix = "AllContentStartNodes"; + public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; + public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; + public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; + + public const string ContentRecycleBinCacheKey = "recycleBin_content"; + public const string MediaRecycleBinCacheKey = "recycleBin_media"; } diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/CacheRefresherBase.cs index 7b962065c5..849d42309a 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherBase.cs @@ -1,122 +1,104 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for cache refreshers that handles events. +/// +/// The actual cache refresher type is used for strongly typed events. +public abstract class CacheRefresherBase : ICacheRefresher + where TNotification : CacheRefresherNotification { /// - /// A base class for cache refreshers that handles events. + /// Initializes a new instance of the . /// - /// The actual cache refresher type. - /// The actual cache refresher type is used for strongly typed events. - public abstract class CacheRefresherBase : ICacheRefresher - where TNotification : CacheRefresherNotification + protected CacheRefresherBase(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) { - /// - /// Initializes a new instance of the . - /// - /// A cache helper. - protected CacheRefresherBase(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - { - AppCaches = appCaches; - EventAggregator = eventAggregator; - NotificationFactory = factory; - } - - #region Define - - /// - /// Gets the unique identifier of the refresher. - /// - public abstract Guid RefresherUniqueId { get; } - - /// - /// Gets the name of the refresher. - /// - public abstract string Name { get; } - - /// - /// Gets the for - /// - protected ICacheRefresherNotificationFactory NotificationFactory { get; } - - #endregion - - #region Refresher - - /// - /// Refreshes all entities. - /// - public virtual void RefreshAll() - { - // NOTE: We pass in string.Empty here because if we pass in NULL this causes problems with - // the underlying ActivatorUtilities.CreateInstance which doesn't seem to support passing in - // null to an 'object' parameter and we end up with "A suitable constructor for type 'ZYZ' could not be located." - // In this case, all cache refreshers should be checking for the type first before checking for a msg value - // so this shouldn't cause any issues. - OnCacheUpdated(NotificationFactory.Create(string.Empty, MessageType.RefreshAll)); - } - - /// - /// Refreshes an entity. - /// - /// The entity's identifier. - public virtual void Refresh(int id) - { - OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); - } - - /// - /// Refreshes an entity. - /// - /// The entity's identifier. - public virtual void Refresh(Guid id) - { - OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); - } - - /// - /// Removes an entity. - /// - /// The entity's identifier. - public virtual void Remove(int id) - { - OnCacheUpdated(NotificationFactory.Create(id, MessageType.RemoveById)); - } - - #endregion - - #region Protected - - /// - /// Gets the cache helper. - /// - protected AppCaches AppCaches { get; } - - protected IEventAggregator EventAggregator { get; } - - /// - /// Clears the cache for all repository entities of a specified type. - /// - /// The type of the entities. - protected void ClearAllIsolatedCacheByEntityType() - where TEntity : class, IEntity - { - AppCaches.IsolatedCaches.ClearCache(); - } - - /// - /// Raises the CacheUpdated event. - /// - /// The event sender. - /// The event arguments. - protected void OnCacheUpdated(CacheRefresherNotification notification) - { - EventAggregator.Publish(notification); - } - - #endregion + AppCaches = appCaches; + EventAggregator = eventAggregator; + NotificationFactory = factory; } + + #region Define + + /// + /// Gets the unique identifier of the refresher. + /// + public abstract Guid RefresherUniqueId { get; } + + /// + /// Gets the name of the refresher. + /// + public abstract string Name { get; } + + /// + /// Gets the for + /// + protected ICacheRefresherNotificationFactory NotificationFactory { get; } + + #endregion + + #region Refresher + + /// + /// Refreshes all entities. + /// + public virtual void RefreshAll() => + + // NOTE: We pass in string.Empty here because if we pass in NULL this causes problems with + // the underlying ActivatorUtilities.CreateInstance which doesn't seem to support passing in + // null to an 'object' parameter and we end up with "A suitable constructor for type 'ZYZ' could not be located." + // In this case, all cache refreshers should be checking for the type first before checking for a msg value + // so this shouldn't cause any issues. + OnCacheUpdated(NotificationFactory.Create(string.Empty, MessageType.RefreshAll)); + + /// + /// Refreshes an entity. + /// + /// The entity's identifier. + public virtual void Refresh(int id) => + OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); + + /// + /// Refreshes an entity. + /// + /// The entity's identifier. + public virtual void Refresh(Guid id) => + OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); + + /// + /// Removes an entity. + /// + /// The entity's identifier. + public virtual void Remove(int id) => + OnCacheUpdated(NotificationFactory.Create(id, MessageType.RemoveById)); + + #endregion + + #region Protected + + /// + /// Gets the cache helper. + /// + protected AppCaches AppCaches { get; } + + protected IEventAggregator EventAggregator { get; } + + /// + /// Clears the cache for all repository entities of a specified type. + /// + /// The type of the entities. + protected void ClearAllIsolatedCacheByEntityType() + where TEntity : class, IEntity => + AppCaches.IsolatedCaches.ClearCache(); + + /// + /// Raises the CacheUpdated event. + /// + protected void OnCacheUpdated(CacheRefresherNotification notification) => EventAggregator.Publish(notification); + + #endregion } diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollection.cs b/src/Umbraco.Core/Cache/CacheRefresherCollection.cs index b9dc7f5984..301f6bbdaf 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherCollection.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherCollection.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Cache -{ - public class CacheRefresherCollection : BuilderCollectionBase - { - public CacheRefresherCollection(Func> items) : base(items) - { - } +namespace Umbraco.Cms.Core.Cache; - public ICacheRefresher? this[Guid id] - => this.FirstOrDefault(x => x.RefresherUniqueId == id); +public class CacheRefresherCollection : BuilderCollectionBase +{ + public CacheRefresherCollection(Func> items) + : base(items) + { } + + public ICacheRefresher? this[Guid id] + => this.FirstOrDefault(x => x.RefresherUniqueId == id); } diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs index 34a274a177..79b44ab53d 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public class CacheRefresherCollectionBuilder : LazyCollectionBuilderBase { - public class CacheRefresherCollectionBuilder : LazyCollectionBuilderBase - { - protected override CacheRefresherCollectionBuilder This => this; - } + protected override CacheRefresherCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs b/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs index bd41ee9d9b..40bab16b12 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs @@ -1,24 +1,24 @@ -using System; using Umbraco.Cms.Core.Notifications; -using Umbraco.Extensions; using Umbraco.Cms.Core.Sync; +using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A that uses ActivatorUtilities to create the +/// instances +/// +public sealed class CacheRefresherNotificationFactory : ICacheRefresherNotificationFactory { + private readonly IServiceProvider _serviceProvider; + + public CacheRefresherNotificationFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + /// - /// A that uses ActivatorUtilities to create the instances + /// Create a using ActivatorUtilities /// - public sealed class CacheRefresherNotificationFactory : ICacheRefresherNotificationFactory - { - private readonly IServiceProvider _serviceProvider; - - public CacheRefresherNotificationFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - - /// - /// Create a using ActivatorUtilities - /// - /// The to create - public TNotification Create(object msgObject, MessageType type) where TNotification : CacheRefresherNotification - => _serviceProvider.CreateInstance(new object[] { msgObject, type }); - } + /// The to create + public TNotification Create(object msgObject, MessageType type) + where TNotification : CacheRefresherNotification + => _serviceProvider.CreateInstance(msgObject, type); } diff --git a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs index ff55a201f5..a515d5c5d1 100644 --- a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -11,168 +8,170 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ContentCacheRefresher : PayloadCacheRefresherBase { - public sealed class ContentCacheRefresher : PayloadCacheRefresherBase + private readonly IDomainService _domainService; + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public ContentCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IIdKeyMap idKeyMap, + IDomainService domainService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IIdKeyMap _idKeyMap; - private readonly IDomainService _domainService; - - public ContentCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IIdKeyMap idKeyMap, - IDomainService domainService, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - _publishedSnapshotService = publishedSnapshotService; - _idKeyMap = idKeyMap; - _domainService = domainService; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "ContentCacheRefresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - AppCaches.RuntimeCache.ClearOfType(); - AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); - - var idsRemoved = new HashSet(); - var isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); - - foreach (var payload in payloads.Where(x => x.Id != default)) - { - //By INT Id - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - //By GUID Key - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); - - _idKeyMap.ClearCache(payload.Id); - - // remove those that are in the branch - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) - { - var pathid = "," + payload.Id + ","; - isolatedCache.ClearOfType((k, v) => v.Path?.Contains(pathid) ?? false); - } - - //if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) - { - idsRemoved.Add(payload.Id); - } - } - - if (idsRemoved.Count > 0) - { - var assignedDomains = _domainService.GetAll(true)?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList(); - - if (assignedDomains?.Count > 0) - { - // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, - // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the - // DomainCacheRefresher? - - ClearAllIsolatedCacheByEntityType(); - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - // notify - _publishedSnapshotService.Notify(assignedDomains.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); - } - } - - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - - // TODO: what about this? - // should rename it, and then, this is only for Deploy, and then, ??? - //if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) - // ... - - NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); - - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() => throw new NotSupportedException(); - - public override void Refresh(int id) => throw new NotSupportedException(); - - public override void Refresh(Guid id) => throw new NotSupportedException(); - - public override void Remove(int id) => throw new NotSupportedException(); - - #endregion - - #region Json - - /// - /// Refreshes the publish snapshot service and if there are published changes ensures that partial view caches are refreshed too - /// - /// - /// - /// - internal static void NotifyPublishedSnapshotService(IPublishedSnapshotService service, AppCaches appCaches, JsonPayload[] payloads) - { - service.Notify(payloads, out _, out var publishedChanged); - - if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) - { - // when a public version changes - appCaches.ClearPartialViewCache(); - } - } - - public class JsonPayload - { - public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) - { - Id = id; - Key = key; - ChangeTypes = changeTypes; - } - - public int Id { get; } - public Guid? Key { get; } - public TreeChangeTypes ChangeTypes { get; } - } - - #endregion - - #region Indirect - - public static void RefreshContentTypes(AppCaches appCaches) - { - // we could try to have a mechanism to notify the PublishedCachesService - // and figure out whether published items were modified or not... keep it - // simple for now, just clear the whole thing - - appCaches.ClearPartialViewCache(); - - appCaches.IsolatedCaches.ClearCache(); - appCaches.IsolatedCaches.ClearCache(); - } - - #endregion - + _publishedSnapshotService = publishedSnapshotService; + _idKeyMap = idKeyMap; + _domainService = domainService; } + + #region Indirect + + public static void RefreshContentTypes(AppCaches appCaches) + { + // we could try to have a mechanism to notify the PublishedCachesService + // and figure out whether published items were modified or not... keep it + // simple for now, just clear the whole thing + appCaches.ClearPartialViewCache(); + + appCaches.IsolatedCaches.ClearCache(); + appCaches.IsolatedCaches.ClearCache(); + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "ContentCacheRefresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + AppCaches.RuntimeCache.ClearOfType(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); + + var idsRemoved = new HashSet(); + IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); + + foreach (JsonPayload payload in payloads.Where(x => x.Id != default)) + { + // By INT Id + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + + // By GUID Key + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + + _idKeyMap.ClearCache(payload.Id); + + // remove those that are in the branch + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) + { + var pathid = "," + payload.Id + ","; + isolatedCache.ClearOfType((k, v) => v.Path?.Contains(pathid) ?? false); + } + + // if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) + { + idsRemoved.Add(payload.Id); + } + } + + if (idsRemoved.Count > 0) + { + var assignedDomains = _domainService.GetAll(true) + ?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList(); + + if (assignedDomains?.Count > 0) + { + // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, + // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the + // DomainCacheRefresher? + ClearAllIsolatedCacheByEntityType(); + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + // notify + _publishedSnapshotService.Notify(assignedDomains + .Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); + } + } + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + + // TODO: what about this? + // should rename it, and then, this is only for Deploy, and then, ??? + // if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + // ... + NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); + + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion + + #region Json + + /// + /// Refreshes the publish snapshot service and if there are published changes ensures that partial view caches are + /// refreshed too + /// + /// + /// + /// + internal static void NotifyPublishedSnapshotService(IPublishedSnapshotService service, AppCaches appCaches, JsonPayload[] payloads) + { + service.Notify(payloads, out _, out var publishedChanged); + + if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) + { + // when a public version changes + appCaches.ClearPartialViewCache(); + } + } + + public class JsonPayload + { + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) + { + Id = id; + Key = key; + ChangeTypes = changeTypes; + } + + public int Id { get; } + + public Guid? Key { get; } + + public TreeChangeTypes ChangeTypes { get; } + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs index 9a709e9a9f..e1a82d6108 100644 --- a/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -11,136 +9,127 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase { - public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase + private readonly IContentTypeCommonRepository _contentTypeCommonRepository; + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public ContentTypeCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IPublishedModelFactory publishedModelFactory, + IIdKeyMap idKeyMap, + IContentTypeCommonRepository contentTypeCommonRepository, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IContentTypeCommonRepository _contentTypeCommonRepository; - private readonly IIdKeyMap _idKeyMap; - - public ContentTypeCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IPublishedModelFactory publishedModelFactory, - IIdKeyMap idKeyMap, - IContentTypeCommonRepository contentTypeCommonRepository, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - _publishedSnapshotService = publishedSnapshotService; - _publishedModelFactory = publishedModelFactory; - _idKeyMap = idKeyMap; - _contentTypeCommonRepository = contentTypeCommonRepository; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("6902E22C-9C10-483C-91F3-66B7CAE9E2F5"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Content Type Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - // TODO: refactor - // we should NOT directly clear caches here, but instead ask whatever class - // is managing the cache to please clear that cache properly - - _contentTypeCommonRepository.ClearCache(); // always - - if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) - { - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - } - - if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) - { - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - } - - if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) - { - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - } - - foreach (var id in payloads.Select(x => x.Id)) - { - _idKeyMap.ClearCache(id); - } - - if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) - // don't try to be clever - refresh all - ContentCacheRefresher.RefreshContentTypes(AppCaches); - - if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) - // don't try to be clever - refresh all - MediaCacheRefresher.RefreshMediaTypes(AppCaches); - - if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) - // don't try to be clever - refresh all - MemberCacheRefresher.RefreshMemberTypes(AppCaches); - - // refresh the models and cache - _publishedModelFactory.WithSafeLiveFactoryReset(() => - _publishedSnapshotService.Notify(payloads)); - - // now we can trigger the event - base.Refresh(payloads); - } - - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(string itemType, int id, ContentTypeChangeTypes changeTypes) - { - ItemType = itemType; - Id = id; - ChangeTypes = changeTypes; - } - - public string ItemType { get; } - - public int Id { get; } - - public ContentTypeChangeTypes ChangeTypes { get; } - } - - #endregion + _publishedSnapshotService = publishedSnapshotService; + _publishedModelFactory = publishedModelFactory; + _idKeyMap = idKeyMap; + _contentTypeCommonRepository = contentTypeCommonRepository; } + + #region Json + + public class JsonPayload + { + public JsonPayload(string itemType, int id, ContentTypeChangeTypes changeTypes) + { + ItemType = itemType; + Id = id; + ChangeTypes = changeTypes; + } + + public string ItemType { get; } + + public int Id { get; } + + public ContentTypeChangeTypes ChangeTypes { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("6902E22C-9C10-483C-91F3-66B7CAE9E2F5"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Content Type Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + // TODO: refactor + // we should NOT directly clear caches here, but instead ask whatever class + // is managing the cache to please clear that cache properly + _contentTypeCommonRepository.ClearCache(); // always + + if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } + + if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } + + if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } + + foreach (var id in payloads.Select(x => x.Id)) + { + _idKeyMap.ClearCache(id); + } + + if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) + { + // don't try to be clever - refresh all + ContentCacheRefresher.RefreshContentTypes(AppCaches); + } + + if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) + { + // don't try to be clever - refresh all + MediaCacheRefresher.RefreshMediaTypes(AppCaches); + } + + if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) + { + // don't try to be clever - refresh all + MemberCacheRefresher.RefreshMemberTypes(AppCaches); + } + + // refresh the models and cache + _publishedModelFactory.WithSafeLiveFactoryReset(() => + _publishedSnapshotService.Notify(payloads)); + + // now we can trigger the event + base.Refresh(payloads); + } + + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs index 44d730be83..ea661c5498 100644 --- a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -10,121 +9,105 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase { - public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public DataTypeCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IPublishedModelFactory publishedModelFactory, + IIdKeyMap idKeyMap, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IIdKeyMap _idKeyMap; - - public DataTypeCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IPublishedModelFactory publishedModelFactory, - IIdKeyMap idKeyMap, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - _publishedSnapshotService = publishedSnapshotService; - _publishedModelFactory = publishedModelFactory; - _idKeyMap = idKeyMap; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Data Type Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - //we need to clear the ContentType runtime cache since that is what caches the - // db data type to store the value against and anytime a datatype changes, this also might change - // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type - - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - - var dataTypeCache = AppCaches.IsolatedCaches.Get(); - - foreach (var payload in payloads) - { - _idKeyMap.ClearCache(payload.Id); - - if (dataTypeCache.Success) - { - dataTypeCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - } - } - - // TODO: not sure I like these? - TagsValueConverter.ClearCaches(); - SliderValueConverter.ClearCaches(); - - // refresh the models and cache - - _publishedModelFactory.WithSafeLiveFactoryReset(() => - _publishedSnapshotService.Notify(payloads)); - - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, Guid key, bool removed) - { - Id = id; - Key = key; - Removed = removed; - } - - public int Id { get; } - - public Guid Key { get; } - - public bool Removed { get; } - } - - #endregion + _publishedSnapshotService = publishedSnapshotService; + _publishedModelFactory = publishedModelFactory; + _idKeyMap = idKeyMap; } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, Guid key, bool removed) + { + Id = id; + Key = key; + Removed = removed; + } + + public int Id { get; } + + public Guid Key { get; } + + public bool Removed { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Data Type Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + // we need to clear the ContentType runtime cache since that is what caches the + // db data type to store the value against and anytime a datatype changes, this also might change + // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + + Attempt dataTypeCache = AppCaches.IsolatedCaches.Get(); + + foreach (JsonPayload payload in payloads) + { + _idKeyMap.ClearCache(payload.Id); + + if (dataTypeCache.Success) + { + dataTypeCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + } + } + + // TODO: not sure I like these? + TagsValueConverter.ClearCaches(); + SliderValueConverter.ClearCaches(); + + // refresh the models and cache + _publishedModelFactory.WithSafeLiveFactoryReset(() => + _publishedSnapshotService.Notify(payloads)); + + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/DeepCloneAppCache.cs b/src/Umbraco.Core/Cache/DeepCloneAppCache.cs index 60a0d8d7b3..da86be4b70 100644 --- a/src/Umbraco.Core/Cache/DeepCloneAppCache.cs +++ b/src/Umbraco.Core/Cache/DeepCloneAppCache.cs @@ -1,178 +1,163 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements by wrapping an inner other +/// instance, and ensuring that all inserts and returns are deep cloned copies of the cache item, +/// when the item is deep-cloneable. +/// +public class DeepCloneAppCache : IAppPolicyCache, IDisposable { + private bool _disposedValue; + /// - /// Implements by wrapping an inner other - /// instance, and ensuring that all inserts and returns are deep cloned copies of the cache item, - /// when the item is deep-cloneable. + /// Initializes a new instance of the class. /// - public class DeepCloneAppCache : IAppPolicyCache, IDisposable + public DeepCloneAppCache(IAppPolicyCache innerCache) { - private bool _disposedValue; + Type type = typeof(DeepCloneAppCache); - /// - /// Initializes a new instance of the class. - /// - public DeepCloneAppCache(IAppPolicyCache innerCache) + if (innerCache.GetType() == type) { - var type = typeof (DeepCloneAppCache); - - if (innerCache.GetType() == type) - throw new InvalidOperationException($"A {type} cannot wrap another instance of itself."); - - InnerCache = innerCache; + throw new InvalidOperationException($"A {type} cannot wrap another instance of itself."); } - /// - /// Gets the inner cache. - /// - private IAppPolicyCache InnerCache { get; } + InnerCache = innerCache; + } - /// - public object? Get(string key) + /// + /// Gets the inner cache. + /// + private IAppPolicyCache InnerCache { get; } + + /// + public object? Get(string key) + { + var item = InnerCache.Get(key); + return CheckCloneableAndTracksChanges(item); + } + + /// + public object? Get(string key, Func factory) + { + var cached = InnerCache.Get(key, () => { - var item = InnerCache.Get(key); - return CheckCloneableAndTracksChanges(item); - } + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - /// - public object? Get(string key, Func factory) + // do not store null values (backward compat), clone / reset to go into the cache + return value == null ? null : CheckCloneableAndTracksChanges(value); + }); + return CheckCloneableAndTracksChanges(cached); + } + + /// + public IEnumerable SearchByKey(string keyStartsWith) => + InnerCache.SearchByKey(keyStartsWith) + .Select(CheckCloneableAndTracksChanges); + + /// + public IEnumerable SearchByRegex(string regex) => + InnerCache.SearchByRegex(regex) + .Select(CheckCloneableAndTracksChanges); + + /// + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) + { + var cached = InnerCache.Get( + key, + () => { - var cached = InnerCache.Get(key, () => - { - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - // do not store null values (backward compat), clone / reset to go into the cache - return value == null ? null : CheckCloneableAndTracksChanges(value); - }); - return CheckCloneableAndTracksChanges(cached); - } + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result + .Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - /// - public IEnumerable SearchByKey(string keyStartsWith) - { - return InnerCache.SearchByKey(keyStartsWith) - .Select(CheckCloneableAndTracksChanges); - } - - /// - public IEnumerable SearchByRegex(string regex) - { - return InnerCache.SearchByRegex(regex) - .Select(CheckCloneableAndTracksChanges); - } - - /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) - { - var cached = InnerCache.Get(key, () => - { - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - // do not store null values (backward compat), clone / reset to go into the cache - return value == null ? null : CheckCloneableAndTracksChanges(value); - - // clone / reset to go into the cache - }, timeout, isSliding, dependentFiles); + // do not store null values (backward compat), clone / reset to go into the cache + return value == null ? null : CheckCloneableAndTracksChanges(value); // clone / reset to go into the cache - return CheckCloneableAndTracksChanges(cached); - } + }, + timeout, + isSliding, + dependentFiles); - /// - public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + // clone / reset to go into the cache + return CheckCloneableAndTracksChanges(cached); + } + + /// + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) => + InnerCache.Insert( + key, + () => { - InnerCache.Insert(key, () => + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result + .Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache + + // do not store null values (backward compat), clone / reset to go into the cache + return value == null ? null : CheckCloneableAndTracksChanges(value); + }, + timeout, + isSliding, + dependentFiles); + + /// + public void Clear() => InnerCache.Clear(); + + /// + public void Clear(string key) => InnerCache.Clear(key); + + /// + public void ClearOfType(Type type) => InnerCache.ClearOfType(type); + + /// + public void ClearOfType() => InnerCache.ClearOfType(); + + /// + public void ClearOfType(Func predicate) => InnerCache.ClearOfType(predicate); + + /// + public void ClearByKey(string keyStartsWith) => InnerCache.ClearByKey(keyStartsWith); + + /// + public void ClearByRegex(string regex) => InnerCache.ClearByRegex(regex); + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) { - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - // do not store null values (backward compat), clone / reset to go into the cache - return value == null ? null : CheckCloneableAndTracksChanges(value); - }, timeout, isSliding, dependentFiles); - } - - /// - public void Clear() - { - InnerCache.Clear(); - } - - /// - public void Clear(string key) - { - InnerCache.Clear(key); - } - - /// - public void ClearOfType(Type type) - { - InnerCache.ClearOfType(type); - } - - /// - public void ClearOfType() - { - InnerCache.ClearOfType(); - } - - /// - public void ClearOfType(Func predicate) - { - InnerCache.ClearOfType(predicate); - } - - /// - public void ClearByKey(string keyStartsWith) - { - InnerCache.ClearByKey(keyStartsWith); - } - - /// - public void ClearByRegex(string regex) - { - InnerCache.ClearByRegex(regex); - } - - private static object? CheckCloneableAndTracksChanges(object? input) - { - if (input is IDeepCloneable cloneable) - { - input = cloneable.DeepClone(); + InnerCache.DisposeIfDisposable(); } - // reset dirty initial properties - if (input is IRememberBeingDirty tracksChanges) - { - tracksChanges.ResetDirtyProperties(false); - input = tracksChanges; - } - - return input; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - InnerCache.DisposeIfDisposable(); - } - - _disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _disposedValue = true; } } + + private static object? CheckCloneableAndTracksChanges(object? input) + { + if (input is IDeepCloneable cloneable) + { + input = cloneable.DeepClone(); + } + + // reset dirty initial properties + if (input is IRememberBeingDirty tracksChanges) + { + tracksChanges.ResetDirtyProperties(false); + input = tracksChanges; + } + + return input; + } } diff --git a/src/Umbraco.Core/Cache/DictionaryAppCache.cs b/src/Umbraco.Core/Cache/DictionaryAppCache.cs index 296050a361..5bf5848309 100644 --- a/src/Umbraco.Core/Cache/DictionaryAppCache.cs +++ b/src/Umbraco.Core/Cache/DictionaryAppCache.cs @@ -1,111 +1,103 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements on top of a concurrent dictionary. +/// +public class DictionaryAppCache : IRequestCache { /// - /// Implements on top of a concurrent dictionary. + /// Gets the internal items dictionary, for tests only! /// - public class DictionaryAppCache : IRequestCache + private readonly ConcurrentDictionary _items = new(); + + public int Count => _items.Count; + + /// + public bool IsAvailable => true; + + /// + public virtual object? Get(string key) => _items.TryGetValue(key, out var value) ? value : null; + + /// + public virtual object? Get(string key, Func factory) => _items.GetOrAdd(key, _ => factory()); + + public bool Set(string key, object? value) => _items.TryAdd(key, value); + + public bool Remove(string key) => _items.TryRemove(key, out _); + + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) { - /// - /// Gets the internal items dictionary, for tests only! - /// - private readonly ConcurrentDictionary _items = new ConcurrentDictionary(); - - public int Count => _items.Count; - - /// - public bool IsAvailable => true; - - /// - public virtual object? Get(string key) + var items = new List(); + foreach ((string key, object? value) in _items) { - return _items.TryGetValue(key, out var value) ? value : null; + if (key.InvariantStartsWith(keyStartsWith)) + { + items.Add(value); + } } - /// - public virtual object? Get(string key, Func factory) - { - return _items.GetOrAdd(key, _ => factory()); - } - - public bool Set(string key, object? value) => _items.TryAdd(key, value); - - public bool Remove(string key) => _items.TryRemove(key, out _); - - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) - { - var items = new List(); - foreach (var (key, value) in _items) - if (key.InvariantStartsWith(keyStartsWith)) - items.Add(value); - return items; - } - - /// - public IEnumerable SearchByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - var items = new List(); - foreach (var (key, value) in _items) - if (compiled.IsMatch(key)) - items.Add(value); - return items; - } - - /// - public virtual void Clear() - { - _items.Clear(); - } - - /// - public virtual void Clear(string key) - { - _items.TryRemove(key, out _); - } - - /// - public virtual void ClearOfType(Type type) - { - _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == type); - } - - /// - public virtual void ClearOfType() - { - var typeOfT = typeof(T); - ClearOfType(typeOfT); - } - - /// - public virtual void ClearOfType(Func predicate) - { - var typeOfT = typeof(T); - _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == typeOfT && predicate(kvp.Key, (T)kvp.Value)); - } - - /// - public virtual void ClearByKey(string keyStartsWith) - { - _items.RemoveAll(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)); - } - - /// - public virtual void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - _items.RemoveAll(kvp => compiled.IsMatch(kvp.Key)); - } - - public IEnumerator> GetEnumerator() => _items.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return items; } + + /// + public IEnumerable SearchByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + var items = new List(); + foreach ((string key, object? value) in _items) + { + if (compiled.IsMatch(key)) + { + items.Add(value); + } + } + + return items; + } + + /// + public virtual void Clear() => _items.Clear(); + + /// + public virtual void Clear(string key) => _items.TryRemove(key, out _); + + /// + public virtual void ClearOfType(Type type) => + _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == type); + + /// + public virtual void ClearOfType() + { + Type typeOfT = typeof(T); + ClearOfType(typeOfT); + } + + /// + public virtual void ClearOfType(Func predicate) + { + Type typeOfT = typeof(T); + _items.RemoveAll(kvp => + kvp.Value != null && kvp.Value.GetType() == typeOfT && predicate(kvp.Key, (T)kvp.Value)); + } + + /// + public virtual void ClearByKey(string keyStartsWith) => + _items.RemoveAll(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)); + + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + _items.RemoveAll(kvp => compiled.IsMatch(kvp.Key)); + } + + public IEnumerator> GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs b/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs index dbe84b114e..c10640986c 100644 --- a/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs @@ -1,40 +1,31 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class DictionaryCacheRefresher : CacheRefresherBase { - public sealed class DictionaryCacheRefresher : CacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("D1D7E227-F817-4816-BFE9-6C39B6152884"); + + public DictionaryCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public DictionaryCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator , factory) - { } + } - #region Define + public override Guid RefresherUniqueId => UniqueId; - public static readonly Guid UniqueId = Guid.Parse("D1D7E227-F817-4816-BFE9-6C39B6152884"); + public override string Name => "Dictionary Cache Refresher"; - public override Guid RefresherUniqueId => UniqueId; + public override void Refresh(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } - public override string Name => "Dictionary Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } - - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Remove(id); - } - - #endregion + public override void Remove(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Remove(id); } } diff --git a/src/Umbraco.Core/Cache/DistributedCache.cs b/src/Umbraco.Core/Cache/DistributedCache.cs index 95c17b946d..0adb0ea370 100644 --- a/src/Umbraco.Core/Cache/DistributedCache.cs +++ b/src/Umbraco.Core/Cache/DistributedCache.cs @@ -1,176 +1,192 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents the entry point into Umbraco's distributed cache infrastructure. +/// +/// +/// +/// The distributed cache infrastructure ensures that distributed caches are +/// invalidated properly in load balancing environments. +/// +/// +/// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache, Examine/Lucene +/// indexes +/// +/// +public sealed class DistributedCache { - /// - /// Represents the entry point into Umbraco's distributed cache infrastructure. - /// - /// - /// - /// The distributed cache infrastructure ensures that distributed caches are - /// invalidated properly in load balancing environments. - /// - /// - /// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache, Examine/Lucene indexes - /// - /// - public sealed class DistributedCache + private readonly CacheRefresherCollection _cacheRefreshers; + private readonly IServerMessenger _serverMessenger; + + public DistributedCache(IServerMessenger serverMessenger, CacheRefresherCollection cacheRefreshers) { - private readonly IServerMessenger _serverMessenger; - private readonly CacheRefresherCollection _cacheRefreshers; - - public DistributedCache(IServerMessenger serverMessenger, CacheRefresherCollection cacheRefreshers) - { - _serverMessenger = serverMessenger; - _cacheRefreshers = cacheRefreshers; - } - - #region Core notification methods - - /// - /// Notifies the distributed cache of specified item invalidation, for a specified . - /// - /// The type of the invalidated items. - /// The unique identifier of the ICacheRefresher. - /// A function returning the unique identifier of items. - /// The invalidated items. - /// - /// This method is much better for performance because it does not need to re-lookup object instances. - /// - public void Refresh(Guid refresherGuid, Func getNumericId, params T[] instances) - { - if (refresherGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - getNumericId, - instances); - } - - /// - /// Notifies the distributed cache of a specified item invalidation, for a specified . - /// - /// The unique identifier of the ICacheRefresher. - /// The unique identifier of the invalidated item. - public void Refresh(Guid refresherGuid, int id) - { - if (refresherGuid == Guid.Empty || id == default(int)) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - id); - } - - /// - /// Notifies the distributed cache of a specified item invalidation, for a specified . - /// - /// The unique identifier of the ICacheRefresher. - /// The unique identifier of the invalidated item. - public void Refresh(Guid refresherGuid, Guid id) - { - if (refresherGuid == Guid.Empty || id == Guid.Empty) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - id); - } - - // payload should be an object, or array of objects, NOT a - // Linq enumerable of some sort (IEnumerable, query...) - public void RefreshByPayload(Guid refresherGuid, TPayload[] payload) - { - if (refresherGuid == Guid.Empty || payload == null) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payload); - } - - // so deal with IEnumerable - public void RefreshByPayload(Guid refresherGuid, IEnumerable payloads) - where TPayload : class - { - if (refresherGuid == Guid.Empty || payloads == null) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payloads.ToArray()); - } - - ///// - ///// Notifies the distributed cache, for a specified . - ///// - ///// The unique identifier of the ICacheRefresher. - ///// The notification content. - //internal void Notify(Guid refresherId, object payload) - //{ - // if (refresherId == Guid.Empty || payload == null) return; - - // _serverMessenger.Notify( - // Current.ServerRegistrar.Registrations, - // GetRefresherById(refresherId), - // json); - //} - - /// - /// Notifies the distributed cache of a global invalidation for a specified . - /// - /// The unique identifier of the ICacheRefresher. - public void RefreshAll(Guid refresherGuid) - { - if (refresherGuid == Guid.Empty) return; - - _serverMessenger.QueueRefreshAll( - GetRefresherById(refresherGuid)); - } - - /// - /// Notifies the distributed cache of a specified item removal, for a specified . - /// - /// The unique identifier of the ICacheRefresher. - /// The unique identifier of the removed item. - public void Remove(Guid refresherGuid, int id) - { - if (refresherGuid == Guid.Empty || id == default(int)) return; - - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - id); - } - - /// - /// Notifies the distributed cache of specified item removal, for a specified . - /// - /// The type of the removed items. - /// The unique identifier of the ICacheRefresher. - /// A function returning the unique identifier of items. - /// The removed items. - /// - /// This method is much better for performance because it does not need to re-lookup object instances. - /// - public void Remove(Guid refresherGuid, Func getNumericId, params T[] instances) - { - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - getNumericId, - instances); - } - - #endregion - - // helper method to get an ICacheRefresher by its unique identifier - private ICacheRefresher GetRefresherById(Guid refresherGuid) - { - ICacheRefresher? refresher = _cacheRefreshers[refresherGuid]; - if (refresher == null) - { - throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); - } - - return refresher; - } + _serverMessenger = serverMessenger; + _cacheRefreshers = cacheRefreshers; } + + #region Core notification methods + + /// + /// Notifies the distributed cache of specified item invalidation, for a specified . + /// + /// The type of the invalidated items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. + /// + /// This method is much better for performance because it does not need to re-lookup object instances. + /// + public void Refresh(Guid refresherGuid, Func getNumericId, params T[] instances) + { + if (refresherGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + getNumericId, + instances); + } + + // helper method to get an ICacheRefresher by its unique identifier + private ICacheRefresher GetRefresherById(Guid refresherGuid) + { + ICacheRefresher? refresher = _cacheRefreshers[refresherGuid]; + if (refresher == null) + { + throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); + } + + return refresher; + } + + /// + /// Notifies the distributed cache of a specified item invalidation, for a specified . + /// + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. + public void Refresh(Guid refresherGuid, int id) + { + if (refresherGuid == Guid.Empty || id == default) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + id); + } + + /// + /// Notifies the distributed cache of a specified item invalidation, for a specified . + /// + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. + public void Refresh(Guid refresherGuid, Guid id) + { + if (refresherGuid == Guid.Empty || id == Guid.Empty) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + id); + } + + // payload should be an object, or array of objects, NOT a + // Linq enumerable of some sort (IEnumerable, query...) + public void RefreshByPayload(Guid refresherGuid, TPayload[] payload) + { + if (refresherGuid == Guid.Empty || payload == null) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + payload); + } + + // so deal with IEnumerable + public void RefreshByPayload(Guid refresherGuid, IEnumerable payloads) + where TPayload : class + { + if (refresherGuid == Guid.Empty || payloads == null) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + payloads.ToArray()); + } + + ///// + ///// Notifies the distributed cache, for a specified . + ///// + ///// The unique identifier of the ICacheRefresher. + ///// The notification content. + // internal void Notify(Guid refresherId, object payload) + // { + // if (refresherId == Guid.Empty || payload == null) return; + + // _serverMessenger.Notify( + // Current.ServerRegistrar.Registrations, + // GetRefresherById(refresherId), + // json); + // } + + /// + /// Notifies the distributed cache of a global invalidation for a specified . + /// + /// The unique identifier of the ICacheRefresher. + public void RefreshAll(Guid refresherGuid) + { + if (refresherGuid == Guid.Empty) + { + return; + } + + _serverMessenger.QueueRefreshAll( + GetRefresherById(refresherGuid)); + } + + /// + /// Notifies the distributed cache of a specified item removal, for a specified . + /// + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the removed item. + public void Remove(Guid refresherGuid, int id) + { + if (refresherGuid == Guid.Empty || id == default) + { + return; + } + + _serverMessenger.QueueRemove( + GetRefresherById(refresherGuid), + id); + } + + /// + /// Notifies the distributed cache of specified item removal, for a specified . + /// + /// The type of the removed items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. + /// + /// This method is much better for performance because it does not need to re-lookup object instances. + /// + public void Remove(Guid refresherGuid, Func getNumericId, params T[] instances) => + _serverMessenger.QueueRemove( + GetRefresherById(refresherGuid), + getNumericId, + instances); + + #endregion } diff --git a/src/Umbraco.Core/Cache/DomainCacheRefresher.cs b/src/Umbraco.Core/Cache/DomainCacheRefresher.cs index 28e62c854d..a6e46ee2e4 100644 --- a/src/Umbraco.Core/Cache/DomainCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/DomainCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -6,78 +5,74 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class DomainCacheRefresher : PayloadCacheRefresherBase { - public sealed class DomainCacheRefresher : PayloadCacheRefresherBase + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public DomainCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) => + _publishedSnapshotService = publishedSnapshotService; + + #region Json + + public class JsonPayload { - private readonly IPublishedSnapshotService _publishedSnapshotService; - - public DomainCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) + public JsonPayload(int id, DomainChangeTypes changeType) { - _publishedSnapshotService = publishedSnapshotService; + Id = id; + ChangeType = changeType; } - #region Define - - public static readonly Guid UniqueId = Guid.Parse("11290A79-4B57-4C99-AD72-7748A3CF38AF"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Domain Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - ClearAllIsolatedCacheByEntityType(); - - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - - // notify - _publishedSnapshotService.Notify(payloads); - // then trigger event - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() => throw new NotSupportedException(); - - public override void Refresh(int id) => throw new NotSupportedException(); - - public override void Refresh(Guid id) => throw new NotSupportedException(); - - public override void Remove(int id) => throw new NotSupportedException(); - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, DomainChangeTypes changeType) - { - Id = id; - ChangeType = changeType; - } - - public int Id { get; } - - public DomainChangeTypes ChangeType { get; } - } - - #endregion + public int Id { get; } + public DomainChangeTypes ChangeType { get; } } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("11290A79-4B57-4C99-AD72-7748A3CF38AF"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Domain Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + ClearAllIsolatedCacheByEntityType(); + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + + // notify + _publishedSnapshotService.Notify(payloads); + + // then trigger event + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs index 6c3b8855d2..6476c76f96 100644 --- a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs +++ b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs @@ -1,166 +1,172 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements a fast on top of a concurrent dictionary. +/// +public class FastDictionaryAppCache : IAppCache { /// - /// Implements a fast on top of a concurrent dictionary. + /// Gets the internal items dictionary, for tests only! /// - public class FastDictionaryAppCache : IAppCache + private readonly ConcurrentDictionary> _items = new(); + + public IEnumerable Keys => _items.Keys; + + public int Count => _items.Count; + + /// + public object? Get(string cacheKey) { + _items.TryGetValue(cacheKey, out Lazy? result); // else null + return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null + } - /// - /// Gets the internal items dictionary, for tests only! - /// - private readonly ConcurrentDictionary> _items = new ConcurrentDictionary>(); + /// + public object? Get(string cacheKey, Func getCacheItem) + { + Lazy? result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(getCacheItem)); - public IEnumerable Keys => _items.Keys; - - public int Count => _items.Count; - - /// - public object? Get(string cacheKey) + var value = result.Value; // will not throw (safe lazy) + if (!(value is SafeLazy.ExceptionHolder eh)) { - _items.TryGetValue(cacheKey, out var result); // else null - return result == null ? null : SafeLazy.GetSafeLazyValue(result!); // return exceptions as null + return value; } - /// - public object? Get(string cacheKey, Func getCacheItem) + // and... it's in the cache anyway - so contrary to other cache providers, + // which would trick with GetSafeLazyValue, we need to remove by ourselves, + // in order NOT to cache exceptions + _items.TryRemove(cacheKey, out result); + eh.Exception.Throw(); // throw once! + return null; // never reached + } + + /// + public IEnumerable SearchByKey(string keyStartsWith) => + _items + .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)) + .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) + .Where(x => x != null); + + /// + public IEnumerable SearchByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + return _items + .Where(kvp => compiled.IsMatch(kvp.Key)) + .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) + .Where(x => x != null); + } + + /// + public void Clear() => _items.Clear(); + + /// + public void Clear(string key) => _items.TryRemove(key, out _); + + /// + public void ClearOfType(Type? type) + { + if (type == null) { - var result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(getCacheItem)); - - var value = result.Value; // will not throw (safe lazy) - if (!(value is SafeLazy.ExceptionHolder eh)) - return value; - - // and... it's in the cache anyway - so contrary to other cache providers, - // which would trick with GetSafeLazyValue, we need to remove by ourselves, - // in order NOT to cache exceptions - - _items.TryRemove(cacheKey, out result); - eh.Exception.Throw(); // throw once! - return null; // never reached + return; } - /// - public IEnumerable SearchByKey(string keyStartsWith) + var isInterface = type.IsInterface; + + foreach (KeyValuePair> kvp in _items + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue(x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? type.IsInstanceOfType(value) : value.GetType() == type); + })) { - return _items - .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)) - .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) - .Where(x => x != null); + _items.TryRemove(kvp.Key, out _); } + } - /// - public IEnumerable SearchByRegex(string regex) + /// + public void ClearOfType() + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + foreach (KeyValuePair> kvp in _items + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue(x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? value is T : value.GetType() == typeOfT); + })) { - var compiled = new Regex(regex, RegexOptions.Compiled); - return _items - .Where(kvp => compiled.IsMatch(kvp.Key)) - .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) - .Where(x => x != null); + _items.TryRemove(kvp.Key, out _); } + } - /// - public void Clear() + /// + public void ClearOfType(Func predicate) + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + foreach (KeyValuePair> kvp in _items + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue(x.Value, true); + if (value == null) + { + return true; + } + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? value is T : value.GetType() == typeOfT) + + // run predicate on the 'public key' part only, ie without prefix + && predicate(x.Key, (T)value); + })) { - _items.Clear(); + _items.TryRemove(kvp.Key, out _); } + } - /// - public void Clear(string key) + /// + public void ClearByKey(string keyStartsWith) + { + foreach (KeyValuePair> ikvp in _items + .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith))) { - _items.TryRemove(key, out _); + _items.TryRemove(ikvp.Key, out _); } + } - /// - public void ClearOfType(Type type) + /// + public void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + foreach (KeyValuePair> ikvp in _items + .Where(kvp => compiled.IsMatch(kvp.Key))) { - if (type == null) return; - var isInterface = type.IsInterface; - - foreach (var kvp in _items - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue(x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); - })) - _items.TryRemove(kvp.Key, out _); - } - - /// - public void ClearOfType() - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - - foreach (var kvp in _items - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue(x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); - })) - _items.TryRemove(kvp.Key, out _); - } - - /// - public void ClearOfType(Func predicate) - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - - foreach (var kvp in _items - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue(x.Value, true); - if (value == null) return true; - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return (isInterface ? (value is T) : (value.GetType() == typeOfT)) - // run predicate on the 'public key' part only, ie without prefix - && predicate(x.Key, (T)value); - })) - _items.TryRemove(kvp.Key, out _); - } - - /// - public void ClearByKey(string keyStartsWith) - { - foreach (var ikvp in _items - .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith))) - _items.TryRemove(ikvp.Key, out _); - } - - /// - public void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - foreach (var ikvp in _items - .Where(kvp => compiled.IsMatch(kvp.Key))) - _items.TryRemove(ikvp.Key, out _); + _items.TryRemove(ikvp.Key, out _); } } } diff --git a/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs b/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs index e0bbd57397..967d5aa5a7 100644 --- a/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs +++ b/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs @@ -1,281 +1,290 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Provides a base class to fast, dictionary-based implementations. +/// +public abstract class FastDictionaryAppCacheBase : IAppCache { - /// - /// Provides a base class to fast, dictionary-based implementations. - /// - public abstract class FastDictionaryAppCacheBase : IAppCache + // prefix cache keys so we know which one are ours + protected const string CacheItemPrefix = "umbrtmche"; + + #region IAppCache + + /// + public virtual object? Get(string key) { - // prefix cache keys so we know which one are ours - protected const string CacheItemPrefix = "umbrtmche"; - - #region IAppCache - - /// - public virtual object? Get(string key) + key = GetCacheKey(key); + Lazy? result; + try { - key = GetCacheKey(key); - Lazy? result; - try - { - EnterReadLock(); - result = GetEntry(key) as Lazy; // null if key not found - } - finally - { - ExitReadLock(); - } - return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null + EnterReadLock(); + result = GetEntry(key) as Lazy; // null if key not found + } + finally + { + ExitReadLock(); } - /// - public abstract object? Get(string key, Func factory); - - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) - { - var plen = CacheItemPrefix.Length + 1; - IEnumerable> entries; - try - { - EnterReadLock(); - entries = GetDictionaryEntries() - .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) - .ToArray(); // evaluate while locked - } - finally - { - ExitReadLock(); - } - - return entries - .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null - .Where(x => x != null)!; // backward compat, don't store null values in the cache - } - - /// - public virtual IEnumerable SearchByRegex(string regex) - { - const string prefix = CacheItemPrefix + "-"; - var compiled = new Regex(regex, RegexOptions.Compiled); - var plen = prefix.Length; - IEnumerable> entries; - try - { - EnterReadLock(); - entries = GetDictionaryEntries() - .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) - .ToArray(); // evaluate while locked - } - finally - { - ExitReadLock(); - } - return entries - .Select(x => SafeLazy.GetSafeLazyValue( (Lazy)x.Value)) // return exceptions as null - .Where(x => x != null); // backward compatible, don't store null values in the cache - } - - /// - public virtual void Clear() - { - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries().ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void Clear(string key) - { - var cacheKey = GetCacheKey(key); - try - { - EnterWriteLock(); - RemoveEntry(cacheKey); - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Type type) - { - if (type == null) return; - var isInterface = type.IsInterface; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy) x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); - }) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType() - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy) x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); - }) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Func predicate) - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - var plen = CacheItemPrefix.Length + 1; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy) x.Value, true); - if (value == null) return true; - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return (isInterface ? (value is T) : (value.GetType() == typeOfT)) - // run predicate on the 'public key' part only, ie without prefix - && predicate(((string) x.Key).Substring(plen), (T) value); - })) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearByKey(string keyStartsWith) - { - var plen = CacheItemPrefix.Length + 1; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - var plen = CacheItemPrefix.Length + 1; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - #endregion - - #region Dictionary - - // manipulate the underlying cache entries - // these *must* be called from within the appropriate locks - // and use the full prefixed cache keys - protected abstract IEnumerable> GetDictionaryEntries(); - protected abstract void RemoveEntry(string key); - protected abstract object? GetEntry(string key); - - // read-write lock the underlying cache - //protected abstract IDisposable ReadLock { get; } - //protected abstract IDisposable WriteLock { get; } - - protected abstract void EnterReadLock(); - protected abstract void ExitReadLock(); - protected abstract void EnterWriteLock(); - protected abstract void ExitWriteLock(); - - protected string GetCacheKey(string key) => $"{CacheItemPrefix}-{key}"; - - - - #endregion + return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null } + + /// + public abstract object? Get(string key, Func factory); + + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + IEnumerable> entries; + try + { + EnterReadLock(); + entries = GetDictionaryEntries() + .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) + .ToArray(); // evaluate while locked + } + finally + { + ExitReadLock(); + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null)!; // backward compat, don't store null values in the cache + } + + /// + public virtual IEnumerable SearchByRegex(string regex) + { + const string prefix = CacheItemPrefix + "-"; + var compiled = new Regex(regex, RegexOptions.Compiled); + var plen = prefix.Length; + IEnumerable> entries; + try + { + EnterReadLock(); + entries = GetDictionaryEntries() + .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) + .ToArray(); // evaluate while locked + } + finally + { + ExitReadLock(); + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null); // backward compatible, don't store null values in the cache + } + + /// + public virtual void Clear() + { + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries().ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void Clear(string key) + { + var cacheKey = GetCacheKey(key); + try + { + EnterWriteLock(); + RemoveEntry(cacheKey); + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearOfType(Type? type) + { + if (type == null) + { + return; + } + + var isInterface = type.IsInterface; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || + (isInterface ? type.IsInstanceOfType(value) : value.GetType() == type); + }) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearOfType() + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? value is T : value.GetType() == typeOfT); + }) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearOfType(Func predicate) + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + var plen = CacheItemPrefix.Length + 1; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + if (value == null) + { + return true; + } + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? value is T : value.GetType() == typeOfT) + + // run predicate on the 'public key' part only, ie without prefix + && predicate(((string) x.Key).Substring(plen), (T) value); + })) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearByKey(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + var plen = CacheItemPrefix.Length + 1; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + #endregion + + #region Dictionary + + // manipulate the underlying cache entries + // these *must* be called from within the appropriate locks + // and use the full prefixed cache keys + protected abstract IEnumerable> GetDictionaryEntries(); + + protected abstract void RemoveEntry(string key); + + protected abstract object? GetEntry(string key); + + // read-write lock the underlying cache + // protected abstract IDisposable ReadLock { get; } + // protected abstract IDisposable WriteLock { get; } + protected abstract void EnterReadLock(); + + protected abstract void ExitReadLock(); + + protected abstract void EnterWriteLock(); + + protected abstract void ExitWriteLock(); + + protected string GetCacheKey(string key) => $"{CacheItemPrefix}-{key}"; + + #endregion } diff --git a/src/Umbraco.Core/Cache/IAppCache.cs b/src/Umbraco.Core/Cache/IAppCache.cs index 81cfc2e114..187ff6fc11 100644 --- a/src/Umbraco.Core/Cache/IAppCache.cs +++ b/src/Umbraco.Core/Cache/IAppCache.cs @@ -1,94 +1,96 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Defines an application cache. +/// +public interface IAppCache { /// - /// Defines an application cache. + /// Gets an item identified by its key. /// - public interface IAppCache - { - /// - /// Gets an item identified by its key. - /// - /// The key of the item. - /// The item, or null if the item was not found. - object? Get(string key); + /// The key of the item. + /// The item, or null if the item was not found. + object? Get(string key); - /// - /// Gets or creates an item identified by its key. - /// - /// The key of the item. - /// A factory function that can create the item. - /// The item. - object? Get(string key, Func factory); + /// + /// Gets or creates an item identified by its key. + /// + /// The key of the item. + /// A factory function that can create the item. + /// The item. + object? Get(string key, Func factory); - /// - /// Gets items with a key starting with the specified value. - /// - /// The StartsWith value to use in the search. - /// Items matching the search. - IEnumerable SearchByKey(string keyStartsWith); + /// + /// Gets items with a key starting with the specified value. + /// + /// The StartsWith value to use in the search. + /// Items matching the search. + IEnumerable SearchByKey(string keyStartsWith); - /// - /// Gets items with a key matching a regular expression. - /// - /// The regular expression. - /// Items matching the search. - IEnumerable SearchByRegex(string regex); + /// + /// Gets items with a key matching a regular expression. + /// + /// The regular expression. + /// Items matching the search. + IEnumerable SearchByRegex(string regex); - /// - /// Removes all items from the cache. - /// - void Clear(); + /// + /// Removes all items from the cache. + /// + void Clear(); - /// - /// Removes an item identified by its key from the cache. - /// - /// The key of the item. - void Clear(string key); + /// + /// Removes an item identified by its key from the cache. + /// + /// The key of the item. + void Clear(string key); - /// - /// Removes items of a specified type from the cache. - /// - /// The type to remove. - /// - /// If the type is an interface, then all items of a type implementing that interface are - /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from - /// the specified type are not removed). - /// Performs a case-sensitive search. - /// - void ClearOfType(Type type); + /// + /// Removes items of a specified type from the cache. + /// + /// The type to remove. + /// + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// + /// Performs a case-sensitive search. + /// + void ClearOfType(Type type); - /// - /// Removes items of a specified type from the cache. - /// - /// The type of the items to remove. - /// If the type is an interface, then all items of a type implementing that interface are - /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from - /// the specified type are not removed). - void ClearOfType(); + /// + /// Removes items of a specified type from the cache. + /// + /// The type of the items to remove. + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// + void ClearOfType(); - /// - /// Removes items of a specified type from the cache. - /// - /// The type of the items to remove. - /// The predicate to satisfy. - /// If the type is an interface, then all items of a type implementing that interface are - /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from - /// the specified type are not removed). - void ClearOfType(Func predicate); + /// + /// Removes items of a specified type from the cache. + /// + /// The type of the items to remove. + /// The predicate to satisfy. + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// + void ClearOfType(Func predicate); - /// - /// Clears items with a key starting with the specified value. - /// - /// The StartsWith value to use in the search. - void ClearByKey(string keyStartsWith); + /// + /// Clears items with a key starting with the specified value. + /// + /// The StartsWith value to use in the search. + void ClearByKey(string keyStartsWith); - /// - /// Clears items with a key matching a regular expression. - /// - /// The regular expression. - void ClearByRegex(string regex); - } + /// + /// Clears items with a key matching a regular expression. + /// + /// The regular expression. + void ClearByRegex(string regex); } diff --git a/src/Umbraco.Core/Cache/IAppPolicyCache.cs b/src/Umbraco.Core/Cache/IAppPolicyCache.cs index ec59bf390b..1d0044c057 100644 --- a/src/Umbraco.Core/Cache/IAppPolicyCache.cs +++ b/src/Umbraco.Core/Cache/IAppPolicyCache.cs @@ -1,43 +1,42 @@ -using System; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Defines an application cache that support cache policies. +/// +/// +/// A cache policy can be used to cache with timeouts, +/// or depending on files, and with a remove callback, etc. +/// +public interface IAppPolicyCache : IAppCache { /// - /// Defines an application cache that support cache policies. + /// Gets an item identified by its key. /// - /// A cache policy can be used to cache with timeouts, - /// or depending on files, and with a remove callback, etc. - public interface IAppPolicyCache : IAppCache - { - /// - /// Gets an item identified by its key. - /// - /// The key of the item. - /// A factory function that can create the item. - /// An optional cache timeout. - /// An optional value indicating whether the cache timeout is sliding (default is false). - /// Files the cache entry depends on. - /// The item. - object? Get( - string key, - Func factory, - TimeSpan? timeout, - bool isSliding = false, - string[]? dependentFiles = null); + /// The key of the item. + /// A factory function that can create the item. + /// An optional cache timeout. + /// An optional value indicating whether the cache timeout is sliding (default is false). + /// Files the cache entry depends on. + /// The item. + object? Get( + string key, + Func factory, + TimeSpan? timeout, + bool isSliding = false, + string[]? dependentFiles = null); - /// - /// Inserts an item. - /// - /// The key of the item. - /// A factory function that can create the item. - /// An optional cache timeout. - /// An optional value indicating whether the cache timeout is sliding (default is false). - /// Files the cache entry depends on. - void Insert( - string key, - Func factory, - TimeSpan? timeout = null, - bool isSliding = false, - string[]? dependentFiles = null); - } + /// + /// Inserts an item. + /// + /// The key of the item. + /// A factory function that can create the item. + /// An optional cache timeout. + /// An optional value indicating whether the cache timeout is sliding (default is false). + /// Files the cache entry depends on. + void Insert( + string key, + Func factory, + TimeSpan? timeout = null, + bool isSliding = false, + string[]? dependentFiles = null); } diff --git a/src/Umbraco.Core/Cache/ICacheRefresher.cs b/src/Umbraco.Core/Cache/ICacheRefresher.cs index 97a3bf08eb..dba0cd3b3f 100644 --- a/src/Umbraco.Core/Cache/ICacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ICacheRefresher.cs @@ -1,33 +1,37 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Cache -{ - /// - /// The IcacheRefresher Interface is used for load balancing. - /// - /// - public interface ICacheRefresher : IDiscoverable - { - Guid RefresherUniqueId { get; } - string Name { get; } - void RefreshAll(); - void Refresh(int id); - void Remove(int id); - void Refresh(Guid id); - } +namespace Umbraco.Cms.Core.Cache; - /// - /// Strongly type cache refresher that is able to refresh cache of real instances of objects as well as IDs - /// - /// - /// - /// This is much better for performance when we're not running in a load balanced environment so we can refresh the cache - /// against a already resolved object instead of looking the object back up by id. - /// - public interface ICacheRefresher : ICacheRefresher - { - void Refresh(T instance); - void Remove(T instance); - } +/// +/// The IcacheRefresher Interface is used for load balancing. +/// +public interface ICacheRefresher : IDiscoverable +{ + Guid RefresherUniqueId { get; } + + string Name { get; } + + void RefreshAll(); + + void Refresh(int id); + + void Remove(int id); + + void Refresh(Guid id); +} + +/// +/// Strongly type cache refresher that is able to refresh cache of real instances of objects as well as IDs +/// +/// +/// +/// This is much better for performance when we're not running in a load balanced environment so we can refresh the +/// cache +/// against a already resolved object instead of looking the object back up by id. +/// +public interface ICacheRefresher : ICacheRefresher +{ + void Refresh(T instance); + + void Remove(T instance); } diff --git a/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs b/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs index 04b91e43d8..35eb7a279c 100644 --- a/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs +++ b/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs @@ -1,17 +1,17 @@ -using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Factory for creating cache refresher notification instances +/// +public interface ICacheRefresherNotificationFactory { /// - /// Factory for creating cache refresher notification instances + /// Creates a /// - public interface ICacheRefresherNotificationFactory - { - /// - /// Creates a - /// - /// The to create - TNotification Create(object msgObject, MessageType type) where TNotification : CacheRefresherNotification; - } + /// The to create + TNotification Create(object msgObject, MessageType type) + where TNotification : CacheRefresherNotification; } diff --git a/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs b/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs index 619fc1eb56..d01bf617fd 100644 --- a/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A cache refresher that supports refreshing or removing cache based on a custom Json payload +/// +public interface IJsonCacheRefresher : ICacheRefresher { /// - /// A cache refresher that supports refreshing or removing cache based on a custom Json payload + /// Refreshes, clears, etc... any cache based on the information provided in the json /// - public interface IJsonCacheRefresher : ICacheRefresher - { - /// - /// Refreshes, clears, etc... any cache based on the information provided in the json - /// - /// - void Refresh(string json); - } + /// + void Refresh(string json); } diff --git a/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs b/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs index 21dfdd840d..426481ea0a 100644 --- a/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A cache refresher that supports refreshing cache based on a custom payload +/// +public interface IPayloadCacheRefresher : IJsonCacheRefresher { /// - /// A cache refresher that supports refreshing cache based on a custom payload + /// Refreshes, clears, etc... any cache based on the information provided in the payload /// - public interface IPayloadCacheRefresher : IJsonCacheRefresher - { - /// - /// Refreshes, clears, etc... any cache based on the information provided in the payload - /// - /// - void Refresh(TPayload[] payloads); - } + /// + void Refresh(TPayload[] payloads); } diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs index af44f2c085..4352f9be31 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -1,76 +1,73 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public interface IRepositoryCachePolicy + where TEntity : class, IEntity { - public interface IRepositoryCachePolicy - where TEntity : class, IEntity - { - /// - /// Gets an entity from the cache, else from the repository. - /// - /// The identifier. - /// The repository PerformGet method. - /// The repository PerformGetAll method. - /// The entity with the specified identifier, if it exits, else null. - /// First considers the cache then the repository. - TEntity? Get(TId? id, Func performGet, Func?> performGetAll); + /// + /// Gets an entity from the cache, else from the repository. + /// + /// The identifier. + /// The repository PerformGet method. + /// The repository PerformGetAll method. + /// The entity with the specified identifier, if it exits, else null. + /// First considers the cache then the repository. + TEntity? Get(TId? id, Func performGet, Func?> performGetAll); - /// - /// Gets an entity from the cache. - /// - /// The identifier. - /// The entity with the specified identifier, if it is in the cache already, else null. - /// Does not consider the repository at all. - TEntity? GetCached(TId id); + /// + /// Gets an entity from the cache. + /// + /// The identifier. + /// The entity with the specified identifier, if it is in the cache already, else null. + /// Does not consider the repository at all. + TEntity? GetCached(TId id); - /// - /// Gets a value indicating whether an entity with a specified identifier exists. - /// - /// The identifier. - /// The repository PerformExists method. - /// The repository PerformGetAll method. - /// A value indicating whether an entity with the specified identifier exists. - /// First considers the cache then the repository. - bool Exists(TId id, Func performExists, Func?> performGetAll); + /// + /// Gets a value indicating whether an entity with a specified identifier exists. + /// + /// The identifier. + /// The repository PerformExists method. + /// The repository PerformGetAll method. + /// A value indicating whether an entity with the specified identifier exists. + /// First considers the cache then the repository. + bool Exists(TId id, Func performExists, Func?> performGetAll); - /// - /// Creates an entity. - /// - /// The entity. - /// The repository PersistNewItem method. - /// Creates the entity in the repository, and updates the cache accordingly. - void Create(TEntity entity, Action persistNew); + /// + /// Creates an entity. + /// + /// The entity. + /// The repository PersistNewItem method. + /// Creates the entity in the repository, and updates the cache accordingly. + void Create(TEntity entity, Action persistNew); - /// - /// Updates an entity. - /// - /// The entity. - /// The repository PersistUpdatedItem method. - /// Updates the entity in the repository, and updates the cache accordingly. - void Update(TEntity entity, Action persistUpdated); + /// + /// Updates an entity. + /// + /// The entity. + /// The repository PersistUpdatedItem method. + /// Updates the entity in the repository, and updates the cache accordingly. + void Update(TEntity entity, Action persistUpdated); - /// - /// Removes an entity. - /// - /// The entity. - /// The repository PersistDeletedItem method. - /// Removes the entity from the repository and clears the cache. - void Delete(TEntity entity, Action persistDeleted); + /// + /// Removes an entity. + /// + /// The entity. + /// The repository PersistDeletedItem method. + /// Removes the entity from the repository and clears the cache. + void Delete(TEntity entity, Action persistDeleted); - /// - /// Gets entities. - /// - /// The identifiers. - /// The repository PerformGetAll method. - /// If is empty, all entities, else the entities with the specified identifiers. - /// Get all the entities. Either from the cache or the repository depending on the implementation. - TEntity[] GetAll(TId[]? ids, Func> performGetAll); + /// + /// Gets entities. + /// + /// The identifiers. + /// The repository PerformGetAll method. + /// If is empty, all entities, else the entities with the specified identifiers. + /// Get all the entities. Either from the cache or the repository depending on the implementation. + TEntity[] GetAll(TId[]? ids, Func> performGetAll); - /// - /// Clears the entire cache. - /// - void ClearAll(); - } + /// + /// Clears the entire cache. + /// + void ClearAll(); } diff --git a/src/Umbraco.Core/Cache/IRequestCache.cs b/src/Umbraco.Core/Cache/IRequestCache.cs index 02f37e6ea9..f88bc3bb24 100644 --- a/src/Umbraco.Core/Cache/IRequestCache.cs +++ b/src/Umbraco.Core/Cache/IRequestCache.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +public interface IRequestCache : IAppCache, IEnumerable> { - public interface IRequestCache : IAppCache, IEnumerable> - { - bool Set(string key, object? value); - bool Remove(string key); + /// + /// Returns true if the request cache is available otherwise false + /// + bool IsAvailable { get; } - /// - /// Returns true if the request cache is available otherwise false - /// - bool IsAvailable { get; } - } + bool Set(string key, object? value); + + bool Remove(string key); } diff --git a/src/Umbraco.Core/Cache/IValueEditorCache.cs b/src/Umbraco.Core/Cache/IValueEditorCache.cs index f283d730b5..790907c750 100644 --- a/src/Umbraco.Core/Cache/IValueEditorCache.cs +++ b/src/Umbraco.Core/Cache/IValueEditorCache.cs @@ -1,12 +1,11 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public interface IValueEditorCache { - public interface IValueEditorCache - { - public IDataValueEditor GetValueEditor(IDataEditor dataEditor, IDataType dataType); - public void ClearCache(IEnumerable dataTypeIds); - } + public IDataValueEditor GetValueEditor(IDataEditor dataEditor, IDataType dataType); + + public void ClearCache(IEnumerable dataTypeIds); } diff --git a/src/Umbraco.Core/Cache/IsolatedCaches.cs b/src/Umbraco.Core/Cache/IsolatedCaches.cs index 7c273c9136..31dc6fe095 100644 --- a/src/Umbraco.Core/Cache/IsolatedCaches.cs +++ b/src/Umbraco.Core/Cache/IsolatedCaches.cs @@ -1,41 +1,41 @@ -using System; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Represents a dictionary of for types. +/// +/// +/// +/// Isolated caches are used by e.g. repositories, to ensure that each cached entity +/// type has its own cache, so that lookups are fast and the repository does not need to +/// search through all keys on a global scale. +/// +/// +public class IsolatedCaches : AppPolicedCacheDictionary { /// - /// Represents a dictionary of for types. + /// Initializes a new instance of the class. /// - /// - /// Isolated caches are used by e.g. repositories, to ensure that each cached entity - /// type has its own cache, so that lookups are fast and the repository does not need to - /// search through all keys on a global scale. - /// - public class IsolatedCaches : AppPolicedCacheDictionary + /// + public IsolatedCaches(Func cacheFactory) + : base(cacheFactory) { - /// - /// Initializes a new instance of the class. - /// - /// - public IsolatedCaches(Func cacheFactory) - : base(cacheFactory) - { } - - /// - /// Gets a cache. - /// - public IAppPolicyCache GetOrCreate() - => GetOrCreate(typeof(T)); - - /// - /// Tries to get a cache. - /// - public Attempt Get() - => Get(typeof(T)); - - /// - /// Clears a cache. - /// - public void ClearCache() - => ClearCache(typeof(T)); } + + /// + /// Gets a cache. + /// + public IAppPolicyCache GetOrCreate() + => GetOrCreate(typeof(T)); + + /// + /// Tries to get a cache. + /// + public Attempt Get() + => Get(typeof(T)); + + /// + /// Clears a cache. + /// + public void ClearCache() + => ClearCache(typeof(T)); } diff --git a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs b/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs index a6b705ae5d..b22cff56d2 100644 --- a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs @@ -3,58 +3,46 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for "json" cache refreshers. +/// +/// The actual cache refresher type is used for strongly typed events. +public abstract class JsonCacheRefresherBase : CacheRefresherBase, + IJsonCacheRefresher + where TNotification : CacheRefresherNotification { /// - /// A base class for "json" cache refreshers. + /// Initializes a new instance of the . /// - /// The actual cache refresher type. - /// The actual cache refresher type is used for strongly typed events. - public abstract class JsonCacheRefresherBase : CacheRefresherBase, IJsonCacheRefresher - where TNotification : CacheRefresherNotification - { - protected IJsonSerializer JsonSerializer { get; } + protected JsonCacheRefresherBase( + AppCaches appCaches, + IJsonSerializer jsonSerializer, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) => + JsonSerializer = jsonSerializer; - /// - /// Initializes a new instance of the . - /// - /// A cache helper. - protected JsonCacheRefresherBase( - AppCaches appCaches, - IJsonSerializer jsonSerializer, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - JsonSerializer = jsonSerializer; - } + protected IJsonSerializer JsonSerializer { get; } - /// - /// Refreshes as specified by a json payload. - /// - /// The json payload. - public virtual void Refresh(string json) - { - OnCacheUpdated(NotificationFactory.Create(json, MessageType.RefreshByJson)); - } + /// + /// Refreshes as specified by a json payload. + /// + /// The json payload. + public virtual void Refresh(string json) => + OnCacheUpdated(NotificationFactory.Create(json, MessageType.RefreshByJson)); - #region Json - /// - /// Deserializes a json payload into an object payload. - /// - /// The json payload. - /// The deserialized object payload. - public TJsonPayload[]? Deserialize(string json) - { - return JsonSerializer.Deserialize(json); - } + #region Json + /// + /// Deserializes a json payload into an object payload. + /// + /// The json payload. + /// The deserialized object payload. + public TJsonPayload[]? Deserialize(string json) => JsonSerializer.Deserialize(json); - public string Serialize(params TJsonPayload[] jsonPayloads) - { - return JsonSerializer.Serialize(jsonPayloads); - } - #endregion + public string Serialize(params TJsonPayload[] jsonPayloads) => JsonSerializer.Serialize(jsonPayloads); - } + #endregion } diff --git a/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs index 414c51c186..2ff447246b 100644 --- a/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -7,146 +6,152 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services.Changes; using static Umbraco.Cms.Core.Cache.LanguageCacheRefresher.JsonPayload; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase { - public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase + public LanguageCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) => + _publishedSnapshotService = publishedSnapshotService; + + /// + /// Clears all domain caches + /// + private void RefreshDomains() { - public LanguageCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) + ClearAllIsolatedCacheByEntityType(); + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + DomainCacheRefresher.JsonPayload[] payloads = new[] { - _publishedSnapshotService = publishedSnapshotService; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); - private readonly IPublishedSnapshotService _publishedSnapshotService; - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Language Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - if (payloads.Length == 0) return; - - var clearDictionary = false; - var clearContent = false; - - //clear all no matter what type of payload - ClearAllIsolatedCacheByEntityType(); - - foreach (var payload in payloads) - { - switch (payload.ChangeType) - { - case LanguageChangeType.Update: - clearDictionary = true; - break; - case LanguageChangeType.Remove: - case LanguageChangeType.ChangeCulture: - clearDictionary = true; - clearContent = true; - break; - } - } - - if (clearDictionary) - { - ClearAllIsolatedCacheByEntityType(); - } - - //if this flag is set, we will tell the published snapshot service to refresh ALL content and evict ALL IContent items - if (clearContent) - { - //clear all domain caches - RefreshDomains(); - ContentCacheRefresher.RefreshContentTypes(AppCaches); // we need to evict all IContent items - //now refresh all nucache - var clearContentPayload = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - ContentCacheRefresher.NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, clearContentPayload); - } - - // then trigger event - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() => throw new NotSupportedException(); - - public override void Refresh(int id) => throw new NotSupportedException(); - - public override void Refresh(Guid id) => throw new NotSupportedException(); - - public override void Remove(int id) => throw new NotSupportedException(); - - #endregion - - /// - /// Clears all domain caches - /// - private void RefreshDomains() - { - ClearAllIsolatedCacheByEntityType(); - - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - - var payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; - _publishedSnapshotService.Notify(payloads); - } - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, string isoCode, LanguageChangeType changeType) - { - Id = id; - IsoCode = isoCode; - ChangeType = changeType; - } - - public int Id { get; } - public string IsoCode { get; } - public LanguageChangeType ChangeType { get; } - - public enum LanguageChangeType - { - /// - /// A new languages has been added - /// - Add = 0, - - /// - /// A language has been deleted - /// - Remove = 1, - - /// - /// A language has been updated - but it's culture remains the same - /// - Update = 2, - - /// - /// A language has been updated - it's culture has changed - /// - ChangeCulture = 3 - } - } - - #endregion + new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll), + }; + _publishedSnapshotService.Notify(payloads); } + + #region Json + + public class JsonPayload + { + public enum LanguageChangeType + { + /// + /// A new languages has been added + /// + Add = 0, + + /// + /// A language has been deleted + /// + Remove = 1, + + /// + /// A language has been updated - but it's culture remains the same + /// + Update = 2, + + /// + /// A language has been updated - it's culture has changed + /// + ChangeCulture = 3, + } + + public JsonPayload(int id, string isoCode, LanguageChangeType changeType) + { + Id = id; + IsoCode = isoCode; + ChangeType = changeType; + } + + public int Id { get; } + + public string IsoCode { get; } + + public LanguageChangeType ChangeType { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Language Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + if (payloads.Length == 0) + { + return; + } + + var clearDictionary = false; + var clearContent = false; + + // clear all no matter what type of payload + ClearAllIsolatedCacheByEntityType(); + + foreach (JsonPayload payload in payloads) + { + switch (payload.ChangeType) + { + case LanguageChangeType.Update: + clearDictionary = true; + break; + case LanguageChangeType.Remove: + case LanguageChangeType.ChangeCulture: + clearDictionary = true; + clearContent = true; + break; + } + } + + if (clearDictionary) + { + ClearAllIsolatedCacheByEntityType(); + } + + // if this flag is set, we will tell the published snapshot service to refresh ALL content and evict ALL IContent items + if (clearContent) + { + // clear all domain caches + RefreshDomains(); + ContentCacheRefresher.RefreshContentTypes(AppCaches); // we need to evict all IContent items + + // now refresh all nucache + ContentCacheRefresher.JsonPayload[] clearContentPayload = + new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + ContentCacheRefresher.NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, clearContentPayload); + } + + // then trigger event + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs index 8f49ce134c..9975abae8c 100644 --- a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs @@ -1,112 +1,108 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class MacroCacheRefresher : PayloadCacheRefresherBase { - public sealed class MacroCacheRefresher : PayloadCacheRefresherBase + public MacroCacheRefresher( + AppCaches appCaches, + IJsonSerializer jsonSerializer, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, jsonSerializer, eventAggregator, factory) { - public MacroCacheRefresher( - AppCaches appCaches, - IJsonSerializer jsonSerializer, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, jsonSerializer, eventAggregator, factory) - { - - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("7B1E683C-5F34-43dd-803D-9699EA1E98CA"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Macro Cache Refresher"; - - #endregion - - #region Refresher - - public override void RefreshAll() - { - foreach (var prefix in GetAllMacroCacheKeys()) - AppCaches.RuntimeCache.ClearByKey(prefix); - - ClearAllIsolatedCacheByEntityType(); - - base.RefreshAll(); - } - - public override void Refresh(string json) - { - var payloads = Deserialize(json); - - if (payloads is not null) - { - Refresh(payloads); - } - } - - public override void Refresh(JsonPayload[] payloads) - { - foreach (var payload in payloads) - { - foreach (var alias in GetCacheKeysForAlias(payload.Alias)) - { - AppCaches.RuntimeCache.ClearByKey(alias); - } - - Attempt macroRepoCache = AppCaches.IsolatedCaches.Get(); - if (macroRepoCache) - { - macroRepoCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - macroRepoCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Alias)); // Repository caching of macro definition by alias - } - } - - base.Refresh(payloads); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, string alias) - { - Id = id; - Alias = alias; - } - - public int Id { get; } - - public string Alias { get; } - } - - #endregion - - #region Helpers - - internal static string[] GetAllMacroCacheKeys() - { - return new[] - { - CacheKeys.MacroContentCacheKey, // macro render cache - CacheKeys.MacroFromAliasCacheKey, // lookup macro by alias - }; - } - - internal static string[] GetCacheKeysForAlias(string alias) - { - return GetAllMacroCacheKeys().Select(x => x + alias).ToArray(); - } - - #endregion } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, string alias) + { + Id = id; + Alias = alias; + } + + public int Id { get; } + + public string Alias { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("7B1E683C-5F34-43dd-803D-9699EA1E98CA"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Macro Cache Refresher"; + + #endregion + + #region Refresher + + public override void RefreshAll() + { + foreach (var prefix in GetAllMacroCacheKeys()) + { + AppCaches.RuntimeCache.ClearByKey(prefix); + } + + ClearAllIsolatedCacheByEntityType(); + + base.RefreshAll(); + } + + public override void Refresh(string json) + { + JsonPayload[]? payloads = Deserialize(json); + + if (payloads is not null) + { + Refresh(payloads); + } + } + + public override void Refresh(JsonPayload[] payloads) + { + foreach (JsonPayload payload in payloads) + { + foreach (var alias in GetCacheKeysForAlias(payload.Alias)) + { + AppCaches.RuntimeCache.ClearByKey(alias); + } + + Attempt macroRepoCache = AppCaches.IsolatedCaches.Get(); + if (macroRepoCache) + { + macroRepoCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + macroRepoCache.Result?.Clear( + RepositoryCacheKeys + .GetKey(payload.Alias)); // Repository caching of macro definition by alias + } + } + + base.Refresh(payloads); + } + + #endregion + + #region Helpers + + internal static string[] GetAllMacroCacheKeys() => + new[] + { + CacheKeys.MacroContentCacheKey, // macro render cache + CacheKeys.MacroFromAliasCacheKey, // lookup macro by alias + }; + + internal static string[] GetCacheKeysForAlias(string alias) => + GetAllMacroCacheKeys().Select(x => x + alias).ToArray(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs index 2efd23d71f..43e6a7ce47 100644 --- a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -9,120 +8,119 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache -{ - public sealed class MediaCacheRefresher : PayloadCacheRefresherBase - { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IIdKeyMap _idKeyMap; +namespace Umbraco.Cms.Core.Cache; - public MediaCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) +public sealed class MediaCacheRefresher : PayloadCacheRefresherBase +{ + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public MediaCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IIdKeyMap idKeyMap, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) + { + _publishedSnapshotService = publishedSnapshotService; + _idKeyMap = idKeyMap; + } + + #region Indirect + + public static void RefreshMediaTypes(AppCaches appCaches) => appCaches.IsolatedCaches.ClearCache(); + + #endregion + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) { - _publishedSnapshotService = publishedSnapshotService; - _idKeyMap = idKeyMap; + Id = id; + Key = key; + ChangeTypes = changeTypes; } - #region Define + public int Id { get; } - public static readonly Guid UniqueId = Guid.Parse("B29286DD-2D40-4DDB-B325-681226589FEC"); + public Guid? Key { get; } - public override Guid RefresherUniqueId => UniqueId; + public TreeChangeTypes ChangeTypes { get; } + } - public override string Name => "Media Cache Refresher"; + #endregion - #endregion + #region Define - #region Refresher + public static readonly Guid UniqueId = Guid.Parse("B29286DD-2D40-4DDB-B325-681226589FEC"); - public override void Refresh(JsonPayload[] payloads) + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Media Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[]? payloads) + { + if (payloads == null) { - if (payloads == null) return; + return; + } - _publishedSnapshotService.Notify(payloads, out var anythingChanged); + _publishedSnapshotService.Notify(payloads, out var anythingChanged); - if (anythingChanged) + if (anythingChanged) + { + AppCaches.ClearPartialViewCache(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); + + Attempt mediaCache = AppCaches.IsolatedCaches.Get(); + + foreach (JsonPayload payload in payloads) { - AppCaches.ClearPartialViewCache(); - AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); - - var mediaCache = AppCaches.IsolatedCaches.Get(); - - foreach (var payload in payloads) + if (payload.ChangeTypes == TreeChangeTypes.Remove) { - if (payload.ChangeTypes == TreeChangeTypes.Remove) - _idKeyMap.ClearCache(payload.Id); + _idKeyMap.ClearCache(payload.Id); + } - if (!mediaCache.Success) continue; + if (!mediaCache.Success) + { + continue; + } - // repository cache - // it *was* done for each pathId but really that does not make sense - // only need to do it for the current media - mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + // repository cache + // it *was* done for each pathId but really that does not make sense + // only need to do it for the current media + mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Key)); - // remove those that are in the branch - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) - { - var pathid = "," + payload.Id + ","; - mediaCache.Result?.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false); - } + // remove those that are in the branch + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) + { + var pathid = "," + payload.Id + ","; + mediaCache.Result?.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false); } } - - base.Refresh(payloads); } - // these events should never trigger - // everything should be JSON - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) - { - Id = id; - Key = key; - ChangeTypes = changeTypes; - } - - public int Id { get; } - public Guid? Key { get; } - public TreeChangeTypes ChangeTypes { get; } - } - - #endregion - - #region Indirect - - public static void RefreshMediaTypes(AppCaches appCaches) - { - appCaches.IsolatedCaches.ClearCache(); - } - - #endregion + base.Refresh(payloads); } + + // these events should never trigger + // everything should be JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs index 9869f226b9..ac9dac5a09 100644 --- a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs @@ -1,6 +1,5 @@ -//using Newtonsoft.Json; +// using Newtonsoft.Json; -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -9,89 +8,76 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class MemberCacheRefresher : PayloadCacheRefresherBase { - public sealed class MemberCacheRefresher : PayloadCacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA"); + + private readonly IIdKeyMap _idKeyMap; + + public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) => + _idKeyMap = idKeyMap; + + #region Indirect + + public static void RefreshMemberTypes(AppCaches appCaches) => appCaches.IsolatedCaches.ClearCache(); + + #endregion + + public class JsonPayload { - private readonly IIdKeyMap _idKeyMap; - - public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) + // [JsonConstructor] + public JsonPayload(int id, string? username, bool removed) { - _idKeyMap = idKeyMap; + Id = id; + Username = username; + Removed = removed; } - public class JsonPayload + public int Id { get; } + + public string? Username { get; } + + public bool Removed { get; } + } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Member Cache Refresher"; + + public override void Refresh(JsonPayload[] payloads) + { + ClearCache(payloads); + base.Refresh(payloads); + } + + public override void Refresh(int id) + { + ClearCache(new JsonPayload(id, null, false)); + base.Refresh(id); + } + + public override void Remove(int id) + { + ClearCache(new JsonPayload(id, null, false)); + base.Remove(id); + } + + private void ClearCache(params JsonPayload[] payloads) + { + AppCaches.ClearPartialViewCache(); + Attempt memberCache = AppCaches.IsolatedCaches.Get(); + + foreach (JsonPayload p in payloads) { - //[JsonConstructor] - public JsonPayload(int id, string? username, bool removed) + _idKeyMap.ClearCache(p.Id); + if (memberCache.Success) { - Id = id; - Username = username; - Removed = removed; + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Username)); } - - public int Id { get; } - public string? Username { get; } - public bool Removed { get; } } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Member Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - ClearCache(payloads); - base.Refresh(payloads); - } - - public override void Refresh(int id) - { - ClearCache(new JsonPayload(id, null, false)); - base.Refresh(id); - } - - public override void Remove(int id) - { - ClearCache(new JsonPayload(id, null, false)); - base.Remove(id); - } - - private void ClearCache(params JsonPayload[] payloads) - { - AppCaches.ClearPartialViewCache(); - var memberCache = AppCaches.IsolatedCaches.Get(); - - foreach (var p in payloads) - { - _idKeyMap.ClearCache(p.Id); - if (memberCache.Success) - { - memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Id)); - memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Username)); - } - } - - } - - #endregion - - #region Indirect - - public static void RefreshMemberTypes(AppCaches appCaches) - { - appCaches.IsolatedCaches.ClearCache(); - } - - #endregion } } diff --git a/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs index 0866f7b39a..05bd6049c8 100644 --- a/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs @@ -1,74 +1,70 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class MemberGroupCacheRefresher : PayloadCacheRefresherBase { - public sealed class MemberGroupCacheRefresher : PayloadCacheRefresherBase + public MemberGroupCacheRefresher(AppCaches appCaches, IJsonSerializer jsonSerializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, jsonSerializer, eventAggregator, factory) { - public MemberGroupCacheRefresher(AppCaches appCaches, IJsonSerializer jsonSerializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, jsonSerializer, eventAggregator, factory) - { - - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("187F236B-BD21-4C85-8A7C-29FBA3D6C00C"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Member Group Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(string json) - { - ClearCache(); - base.Refresh(json); - } - - public override void Refresh(int id) - { - ClearCache(); - base.Refresh(id); - } - - public override void Remove(int id) - { - ClearCache(); - base.Remove(id); - } - - private void ClearCache() - { - // Since we cache by group name, it could be problematic when renaming to - // previously existing names - see http://issues.umbraco.org/issue/U4-10846. - // To work around this, just clear all the cache items - AppCaches.IsolatedCaches.ClearCache(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, string name) - { - Id = id; - Name = name; - } - - public string Name { get; } - public int Id { get; } - } - - - #endregion } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, string name) + { + Id = id; + Name = name; + } + + public string Name { get; } + + public int Id { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("187F236B-BD21-4C85-8A7C-29FBA3D6C00C"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Member Group Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(string json) + { + ClearCache(); + base.Refresh(json); + } + + public override void Refresh(int id) + { + ClearCache(); + base.Refresh(id); + } + + public override void Remove(int id) + { + ClearCache(); + base.Remove(id); + } + + private void ClearCache() => + + // Since we cache by group name, it could be problematic when renaming to + // previously existing names - see http://issues.umbraco.org/issue/U4-10846. + // To work around this, just clear all the cache items + AppCaches.IsolatedCaches.ClearCache(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/NoAppCache.cs b/src/Umbraco.Core/Cache/NoAppCache.cs index ef22a51ab0..70edbcf61d 100644 --- a/src/Umbraco.Core/Cache/NoAppCache.cs +++ b/src/Umbraco.Core/Cache/NoAppCache.cs @@ -1,93 +1,85 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements and do not cache. +/// +public class NoAppCache : IAppPolicyCache, IRequestCache { - /// - /// Implements and do not cache. - /// - public class NoAppCache : IAppPolicyCache, IRequestCache + protected NoAppCache() { - protected NoAppCache() { } - - /// - /// Gets the singleton instance. - /// - public static NoAppCache Instance { get; } = new NoAppCache(); - - /// - public bool IsAvailable => false; - - /// - public virtual object? Get(string cacheKey) - { - return null; - } - - /// - public virtual object? Get(string cacheKey, Func factory) - { - return factory(); - } - - public bool Set(string key, object? value) => false; - - public bool Remove(string key) => false; - - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) - { - return Enumerable.Empty(); - } - - /// - public IEnumerable SearchByRegex(string regex) - { - return Enumerable.Empty(); - } - - /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) - { - return factory(); - } - - /// - public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) - { } - - /// - public virtual void Clear() - { } - - /// - public virtual void Clear(string key) - { } - - /// - public virtual void ClearOfType(Type type) - { } - - /// - public virtual void ClearOfType() - { } - - /// - public virtual void ClearOfType(Func predicate) - { } - - /// - public virtual void ClearByKey(string keyStartsWith) - { } - - /// - public virtual void ClearByRegex(string regex) - { } - - public IEnumerator> GetEnumerator() => new Dictionary().GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + /// + /// Gets the singleton instance. + /// + public static NoAppCache Instance { get; } = new(); + + /// + public bool IsAvailable => false; + + /// + public virtual object? Get(string cacheKey) => null; + + /// + public virtual object? Get(string cacheKey, Func factory) => factory(); + + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) => Enumerable.Empty(); + + /// + public IEnumerable SearchByRegex(string regex) => Enumerable.Empty(); + + /// + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) => factory(); + + /// + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + { + } + + /// + public virtual void Clear() + { + } + + /// + public virtual void Clear(string key) + { + } + + /// + public virtual void ClearOfType(Type type) + { + } + + /// + public virtual void ClearOfType() + { + } + + /// + public virtual void ClearOfType(Func predicate) + { + } + + /// + public virtual void ClearByKey(string keyStartsWith) + { + } + + /// + public virtual void ClearByRegex(string regex) + { + } + + public bool Set(string key, object? value) => false; + + public bool Remove(string key) => false; + + public IEnumerator> GetEnumerator() => + new Dictionary().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs index b99975e0e4..2b662d4c2c 100644 --- a/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs @@ -1,53 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public class NoCacheRepositoryCachePolicy : IRepositoryCachePolicy + where TEntity : class, IEntity { - public class NoCacheRepositoryCachePolicy : IRepositoryCachePolicy - where TEntity : class, IEntity + private NoCacheRepositoryCachePolicy() { - private NoCacheRepositoryCachePolicy() { } + } - public static NoCacheRepositoryCachePolicy Instance { get; } = new NoCacheRepositoryCachePolicy(); + public static NoCacheRepositoryCachePolicy Instance { get; } = new(); - public TEntity? Get(TId? id, Func performGet, Func?> performGetAll) - { - return performGet(id); - } + public TEntity? Get(TId? id, Func performGet, Func?> performGetAll) => + performGet(id); - public TEntity? GetCached(TId id) - { - return null; - } + public TEntity? GetCached(TId id) => null; - public bool Exists(TId id, Func performExists, Func?> performGetAll) - { - return performExists(id); - } + public bool Exists(TId id, Func performExists, Func?> performGetAll) => + performExists(id); - public void Create(TEntity entity, Action persistNew) - { - persistNew(entity); - } + public void Create(TEntity entity, Action persistNew) => persistNew(entity); - public void Update(TEntity entity, Action persistUpdated) - { - persistUpdated(entity); - } + public void Update(TEntity entity, Action persistUpdated) => persistUpdated(entity); - public void Delete(TEntity entity, Action persistDeleted) - { - persistDeleted(entity); - } + public void Delete(TEntity entity, Action persistDeleted) => persistDeleted(entity); - public TEntity[] GetAll(TId[]? ids, Func?> performGetAll) - { - return performGetAll(ids)?.ToArray() ?? Array.Empty(); - } + public TEntity[] GetAll(TId[]? ids, Func?> performGetAll) => + performGetAll(ids)?.ToArray() ?? Array.Empty(); - public void ClearAll() - { } + public void ClearAll() + { } } diff --git a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs index 4ec91c4933..dcd83ece94 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs @@ -1,367 +1,423 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Caching; using System.Text.RegularExpressions; -using System.Threading; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache -{ - /// - /// Implements on top of a . - /// - public class ObjectCacheAppCache : IAppPolicyCache, IDisposable - { - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - private bool _disposedValue; +namespace Umbraco.Cms.Core.Cache; - /// - /// Initializes a new instance of the . - /// - public ObjectCacheAppCache() +/// +/// Implements on top of a . +/// +public class ObjectCacheAppCache : IAppPolicyCache, IDisposable +{ + private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion); + private bool _disposedValue; + + /// + /// Initializes a new instance of the . + /// + public ObjectCacheAppCache() => + + // the MemoryCache is created with name "in-memory". That name is + // used to retrieve configuration options. It does not identify the memory cache, i.e. + // each instance of this class has its own, independent, memory cache. + MemoryCache = new MemoryCache("in-memory"); + + /// + /// Gets the internal memory cache, for tests only! + /// + public ObjectCache MemoryCache { get; private set; } + + /// + public object? Get(string key) + { + Lazy? result; + try { - // the MemoryCache is created with name "in-memory". That name is - // used to retrieve configuration options. It does not identify the memory cache, i.e. - // each instance of this class has its own, independent, memory cache. + _locker.EnterReadLock(); + result = MemoryCache.Get(key) as Lazy; // null if key not found + } + finally + { + if (_locker.IsReadLockHeld) + { + _locker.ExitReadLock(); + } + } + + return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null + } + + /// + public object? Get(string key, Func factory) => Get(key, factory, null); + + /// + public IEnumerable SearchByKey(string keyStartsWith) + { + KeyValuePair[] entries; + try + { + _locker.EnterReadLock(); + entries = MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .ToArray(); // evaluate while locked + } + finally + { + if (_locker.IsReadLockHeld) + { + _locker.ExitReadLock(); + } + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null) // backward compat, don't store null values in the cache + .ToList()!; + } + + /// + public IEnumerable SearchByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + + KeyValuePair[] entries; + try + { + _locker.EnterReadLock(); + entries = MemoryCache + .Where(x => compiled.IsMatch(x.Key)) + .ToArray(); // evaluate while locked + } + finally + { + if (_locker.IsReadLockHeld) + { + _locker.ExitReadLock(); + } + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null) // backward compat, don't store null values in the cache + .ToList()!; + } + + /// + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) + { + // see notes in HttpRuntimeAppCache + Lazy? result; + + try + { + _locker.EnterUpgradeableReadLock(); + + result = MemoryCache.Get(key) as Lazy; + + // get non-created as NonCreatedValue & exceptions as null + if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) + { + result = SafeLazy.GetSafeLazy(factory); + CacheItemPolicy policy = GetPolicy(timeout, isSliding, dependentFiles); + + try + { + _locker.EnterWriteLock(); + + // NOTE: This does an add or update + MemoryCache.Set(key, result, policy); + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + } + finally + { + if (_locker.IsUpgradeableReadLockHeld) + { + _locker.ExitUpgradeableReadLock(); + } + } + + // return result.Value; + var value = result.Value; // will not throw (safe lazy) + if (value is SafeLazy.ExceptionHolder eh) + { + eh.Exception.Throw(); // throw once! + } + + return value; + } + + /// + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + { + // NOTE - here also we must insert a Lazy but we can evaluate it right now + // and make sure we don't store a null value. + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result.Value; // force evaluation now + if (value == null) + { + return; // do not store null values (backward compat) + } + + CacheItemPolicy policy = GetPolicy(timeout, isSliding, dependentFiles); + + // NOTE: This does an add or update + MemoryCache.Set(key, result, policy); + } + + /// + public virtual void Clear() + { + try + { + _locker.EnterWriteLock(); + MemoryCache.DisposeIfDisposable(); MemoryCache = new MemoryCache("in-memory"); } - - /// - /// Gets the internal memory cache, for tests only! - /// - public ObjectCache MemoryCache { get; private set; } - - /// - public object? Get(string key) + finally { - Lazy? result; - try + if (_locker.IsWriteLockHeld) { - _locker.EnterReadLock(); - result = MemoryCache.Get(key) as Lazy; // null if key not found + _locker.ExitWriteLock(); } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null - } - - /// - public object? Get(string key, Func factory) - { - return Get(key, factory, null); - } - - /// - public IEnumerable SearchByKey(string keyStartsWith) - { - KeyValuePair[] entries; - try - { - _locker.EnterReadLock(); - entries = MemoryCache - .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) - .ToArray(); // evaluate while locked - } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - return entries - .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null - .Where(x => x != null) // backward compat, don't store null values in the cache - .ToList()!; - } - - /// - public IEnumerable SearchByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - - KeyValuePair[] entries; - try - { - _locker.EnterReadLock(); - entries = MemoryCache - .Where(x => compiled.IsMatch(x.Key)) - .ToArray(); // evaluate while locked - } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - return entries - .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null - .Where(x => x != null) // backward compat, don't store null values in the cache - .ToList()!; - } - - /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) - { - // see notes in HttpRuntimeAppCache - - Lazy? result; - - try - { - _locker.EnterUpgradeableReadLock(); - - result = MemoryCache.Get(key) as Lazy; - if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null - { - result = SafeLazy.GetSafeLazy(factory); - var policy = GetPolicy(timeout, isSliding, dependentFiles); - - try - { - _locker.EnterWriteLock(); - //NOTE: This does an add or update - MemoryCache.Set(key, result, policy); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - } - finally - { - if (_locker.IsUpgradeableReadLockHeld) - _locker.ExitUpgradeableReadLock(); - } - - //return result.Value; - - var value = result.Value; // will not throw (safe lazy) - if (value is SafeLazy.ExceptionHolder eh) eh.Exception.Throw(); // throw once! - return value; - } - - /// - public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) - { - // NOTE - here also we must insert a Lazy but we can evaluate it right now - // and make sure we don't store a null value. - - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - if (value == null) return; // do not store null values (backward compat) - - var policy = GetPolicy(timeout, isSliding, dependentFiles); - //NOTE: This does an add or update - MemoryCache.Set(key, result, policy); - } - - /// - public virtual void Clear() - { - try - { - _locker.EnterWriteLock(); - MemoryCache.DisposeIfDisposable(); - MemoryCache = new MemoryCache("in-memory"); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void Clear(string key) - { - try - { - _locker.EnterWriteLock(); - if (MemoryCache[key] == null) return; - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Type type) - { - if (type == null) return; - var isInterface = type.IsInterface; - try - { - _locker.EnterWriteLock(); - foreach (var key in MemoryCache - .Where(x => - { - // x.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); - }) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType() - { - try - { - _locker.EnterWriteLock(); - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - foreach (var key in MemoryCache - .Where(x => - { - // x.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); - - }) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Func predicate) - { - try - { - _locker.EnterWriteLock(); - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - foreach (var key in MemoryCache - .Where(x => - { - // x.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); - if (value == null) return true; - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return (isInterface ? (value is T) : (value.GetType() == typeOfT)) - && predicate(x.Key, (T)value); - }) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearByKey(string keyStartsWith) - { - try - { - _locker.EnterWriteLock(); - foreach (var key in MemoryCache - .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - - try - { - _locker.EnterWriteLock(); - foreach (var key in MemoryCache - .Where(x => compiled.IsMatch(x.Key)) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) - { - var absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : (timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value)); - var sliding = isSliding == false ? ObjectCache.NoSlidingExpiration : (timeout ?? ObjectCache.NoSlidingExpiration); - - var policy = new CacheItemPolicy - { - AbsoluteExpiration = absolute, - SlidingExpiration = sliding - }; - - if (dependentFiles != null && dependentFiles.Any()) - { - policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); - } - - return policy; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _locker.Dispose(); - } - _disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); } } + + /// + public virtual void Clear(string key) + { + try + { + _locker.EnterWriteLock(); + if (MemoryCache[key] == null) + { + return; + } + + MemoryCache.Remove(key); + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearOfType(Type type) + { + if (type == null) + { + return; + } + + var isInterface = type.IsInterface; + try + { + _locker.EnterWriteLock(); + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || + (isInterface ? type.IsInstanceOfType(value) : value.GetType() == type); + }) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearOfType() + { + try + { + _locker.EnterWriteLock(); + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? value is T : value.GetType() == typeOfT); + }) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearOfType(Func predicate) + { + try + { + _locker.EnterWriteLock(); + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + if (value == null) + { + return true; + } + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? value is T : value.GetType() == typeOfT) + && predicate(x.Key, (T)value); + }) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearByKey(string keyStartsWith) + { + try + { + _locker.EnterWriteLock(); + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + + try + { + _locker.EnterWriteLock(); + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => compiled.IsMatch(x.Key)) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _locker.Dispose(); + } + + _disposedValue = true; + } + } + + private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + { + DateTimeOffset absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : + timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value); + TimeSpan sliding = isSliding == false + ? ObjectCache.NoSlidingExpiration + : timeout ?? ObjectCache.NoSlidingExpiration; + + var policy = new CacheItemPolicy { AbsoluteExpiration = absolute, SlidingExpiration = sliding }; + + if (dependentFiles != null && dependentFiles.Any()) + { + policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); + } + + return policy; + } } diff --git a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs b/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs index 2dc3ddcf1b..f371e80979 100644 --- a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs @@ -3,49 +3,48 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for "payload" class refreshers. +/// +/// The payload type. +/// The notification type +/// The actual cache refresher type is used for strongly typed events. +public abstract class + PayloadCacheRefresherBase : JsonCacheRefresherBase, + IPayloadCacheRefresher + where TNotification : CacheRefresherNotification { /// - /// A base class for "payload" class refreshers. + /// Initializes a new instance of the . /// - /// The actual cache refresher type. - /// The payload type. - /// The actual cache refresher type is used for strongly typed events. - public abstract class PayloadCacheRefresherBase : JsonCacheRefresherBase, IPayloadCacheRefresher - where TNotification : CacheRefresherNotification + /// A cache helper. + /// + /// + /// + protected PayloadCacheRefresherBase(AppCaches appCaches, IJsonSerializer serializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - - /// - /// Initializes a new instance of the . - /// - /// A cache helper. - /// - protected PayloadCacheRefresherBase(AppCaches appCaches, IJsonSerializer serializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - } - - - #region Refresher - - public override void Refresh(string json) - { - var payload = Deserialize(json); - if (payload is not null) - { - Refresh(payload); - } - } - - /// - /// Refreshes as specified by a payload. - /// - /// The payload. - public virtual void Refresh(TPayload[] payloads) - { - OnCacheUpdated(NotificationFactory.Create(payloads, MessageType.RefreshByPayload)); - } - - #endregion } + + #region Refresher + + public override void Refresh(string json) + { + TPayload[]? payload = Deserialize(json); + if (payload is not null) + { + Refresh(payload); + } + } + + /// + /// Refreshes as specified by a payload. + /// + /// The payload. + public virtual void Refresh(TPayload[] payloads) => + OnCacheUpdated(NotificationFactory.Create(payloads, MessageType.RefreshByPayload)); + + #endregion } diff --git a/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs b/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs index 5c9eb20b4c..9124d7350e 100644 --- a/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs @@ -1,52 +1,51 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class PublicAccessCacheRefresher : CacheRefresherBase { - public sealed class PublicAccessCacheRefresher : CacheRefresherBase + #region Define + + public static readonly Guid UniqueId = Guid.Parse("1DB08769-B104-4F8B-850E-169CAC1DF2EC"); + + public PublicAccessCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public PublicAccessCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("1DB08769-B104-4F8B-850E-169CAC1DF2EC"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Public Access Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(Guid id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } - - public override void Refresh(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } - - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); - } - - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Remove(id); - } - - #endregion } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Public Access Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(Guid id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } + + public override void Refresh(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } + + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } + + public override void Remove(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Remove(id); + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs index 9f1c45374e..8da3cd5be0 100644 --- a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs @@ -1,55 +1,51 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class RelationTypeCacheRefresher : CacheRefresherBase { - public sealed class RelationTypeCacheRefresher : CacheRefresherBase + public RelationTypeCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public RelationTypeCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } + } - #region Define + public static readonly Guid UniqueId = Guid.Parse("D8375ABA-4FB3-4F86-B505-92FBA1B6F7C9"); - public static readonly Guid UniqueId = Guid.Parse("D8375ABA-4FB3-4F86-B505-92FBA1B6F7C9"); + public override Guid RefresherUniqueId => UniqueId; - public override Guid RefresherUniqueId => UniqueId; + public override string Name => "Relation Type Cache Refresher"; - public override string Name => "Relation Type Cache Refresher"; + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } - #endregion - - #region Refresher - - public override void RefreshAll() + public override void Refresh(int id) + { + Attempt cache = AppCaches.IsolatedCaches.Get(); + if (cache.Success) { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); + cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); } - public override void Refresh(int id) + base.Refresh(id); + } + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + // base.Refresh(id); + public override void Remove(int id) + { + Attempt cache = AppCaches.IsolatedCaches.Get(); + if (cache.Success) { - var cache = AppCaches.IsolatedCaches.Get(); - if (cache.Success) cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - base.Refresh(id); + cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); } - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - //base.Refresh(id); - } - - public override void Remove(int id) - { - var cache = AppCaches.IsolatedCaches.Get(); - if (cache.Success) cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - base.Remove(id); - } - - #endregion + base.Remove(id); } } diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index c719ce72e5..ba7b251aa0 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -1,51 +1,50 @@ -using System; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Specifies how a repository cache policy should cache entities. +/// +public class RepositoryCachePolicyOptions { /// - /// Specifies how a repository cache policy should cache entities. + /// Ctor - sets GetAllCacheValidateCount = true /// - public class RepositoryCachePolicyOptions + public RepositoryCachePolicyOptions(Func performCount) { - /// - /// Ctor - sets GetAllCacheValidateCount = true - /// - public RepositoryCachePolicyOptions(Func performCount) - { - PerformCount = performCount; - GetAllCacheValidateCount = true; - GetAllCacheAllowZeroCount = false; - } - - /// - /// Ctor - sets GetAllCacheValidateCount = false - /// - public RepositoryCachePolicyOptions() - { - PerformCount = null; - GetAllCacheValidateCount = false; - GetAllCacheAllowZeroCount = false; - } - - /// - /// Callback required to get count for GetAllCacheValidateCount - /// - public Func? PerformCount { get; set; } - - /// - /// True/false as to validate the total item count when all items are returned from cache, the default is true but this - /// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the normal - /// GetAll method. - /// - /// - /// setting this to return false will improve performance of GetAll cache with no params but should only be used - /// for specific circumstances - /// - public bool GetAllCacheValidateCount { get; set; } - - /// - /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no results found - /// - public bool GetAllCacheAllowZeroCount { get; set; } + PerformCount = performCount; + GetAllCacheValidateCount = true; + GetAllCacheAllowZeroCount = false; } + + /// + /// Ctor - sets GetAllCacheValidateCount = false + /// + public RepositoryCachePolicyOptions() + { + PerformCount = null; + GetAllCacheValidateCount = false; + GetAllCacheAllowZeroCount = false; + } + + /// + /// Callback required to get count for GetAllCacheValidateCount + /// + public Func? PerformCount { get; set; } + + /// + /// True/false as to validate the total item count when all items are returned from cache, the default is true but this + /// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the + /// normal + /// GetAll method. + /// + /// + /// setting this to return false will improve performance of GetAll cache with no params but should only be used + /// for specific circumstances + /// + public bool GetAllCacheValidateCount { get; set; } + + /// + /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no + /// results found + /// + public bool GetAllCacheAllowZeroCount { get; set; } } diff --git a/src/Umbraco.Core/Cache/SafeLazy.cs b/src/Umbraco.Core/Cache/SafeLazy.cs index 387e5c0271..40512ece67 100644 --- a/src/Umbraco.Core/Cache/SafeLazy.cs +++ b/src/Umbraco.Core/Cache/SafeLazy.cs @@ -1,63 +1,64 @@ -using System; using System.Runtime.ExceptionServices; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public static class SafeLazy { - public static class SafeLazy - { - // an object that represent a value that has not been created yet - internal static readonly object ValueNotCreated = new object(); + // an object that represent a value that has not been created yet + internal static readonly object ValueNotCreated = new(); - public static Lazy GetSafeLazy(Func getCacheItem) + public static Lazy GetSafeLazy(Func getCacheItem) => + + // try to generate the value and if it fails, + // wrap in an ExceptionHolder - would be much simpler + // to just use lazy.IsValueFaulted alas that field is + // internal + new Lazy(() => { - // try to generate the value and if it fails, - // wrap in an ExceptionHolder - would be much simpler - // to just use lazy.IsValueFaulted alas that field is - // internal - return new Lazy(() => - { - try - { - return getCacheItem(); - } - catch (Exception e) - { - return new ExceptionHolder(ExceptionDispatchInfo.Capture(e)); - } - }); - } - - public static object? GetSafeLazyValue(Lazy? lazy, bool onlyIfValueIsCreated = false) - { - // if onlyIfValueIsCreated, do not trigger value creation - // must return something, though, to differentiate from null values - if (onlyIfValueIsCreated && lazy?.IsValueCreated == false) return ValueNotCreated; - - // if execution has thrown then lazy.IsValueCreated is false - // and lazy.IsValueFaulted is true (but internal) so we use our - // own exception holder (see Lazy source code) to return null - if (lazy?.Value is ExceptionHolder) return null; - - // we have a value and execution has not thrown so returning - // here does not throw - unless we're re-entering, take care of it try { - return lazy?.Value; + return getCacheItem(); } - catch (InvalidOperationException e) + catch (Exception e) { - throw new InvalidOperationException("The method that computes a value for the cache has tried to read that value from the cache.", e); + return new ExceptionHolder(ExceptionDispatchInfo.Capture(e)); } + }); + + public static object? GetSafeLazyValue(Lazy? lazy, bool onlyIfValueIsCreated = false) + { + // if onlyIfValueIsCreated, do not trigger value creation + // must return something, though, to differentiate from null values + if (onlyIfValueIsCreated && lazy?.IsValueCreated == false) + { + return ValueNotCreated; } - public class ExceptionHolder + // if execution has thrown then lazy.IsValueCreated is false + // and lazy.IsValueFaulted is true (but internal) so we use our + // own exception holder (see Lazy source code) to return null + if (lazy?.Value is ExceptionHolder) { - public ExceptionHolder(ExceptionDispatchInfo e) - { - Exception = e; - } + return null; + } - public ExceptionDispatchInfo Exception { get; } + // we have a value and execution has not thrown so returning + // here does not throw - unless we're re-entering, take care of it + try + { + return lazy?.Value; + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException( + "The method that computes a value for the cache has tried to read that value from the cache.", e); } } + + public class ExceptionHolder + { + public ExceptionHolder(ExceptionDispatchInfo e) => Exception = e; + + public ExceptionDispatchInfo Exception { get; } + } } diff --git a/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs index 0bc2c6c5ef..221ad7c836 100644 --- a/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs @@ -1,66 +1,61 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class TemplateCacheRefresher : CacheRefresherBase { - public sealed class TemplateCacheRefresher : CacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("DD12B6A0-14B9-46e8-8800-C154F74047C8"); + + private readonly IContentTypeCommonRepository _contentTypeCommonRepository; + private readonly IIdKeyMap _idKeyMap; + + public TemplateCacheRefresher( + AppCaches appCaches, + IIdKeyMap idKeyMap, + IContentTypeCommonRepository contentTypeCommonRepository, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - private readonly IIdKeyMap _idKeyMap; - private readonly IContentTypeCommonRepository _contentTypeCommonRepository; + _idKeyMap = idKeyMap; + _contentTypeCommonRepository = contentTypeCommonRepository; + } - public TemplateCacheRefresher(AppCaches appCaches, IIdKeyMap idKeyMap, IContentTypeCommonRepository contentTypeCommonRepository, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - _idKeyMap = idKeyMap; - _contentTypeCommonRepository = contentTypeCommonRepository; - } + public override Guid RefresherUniqueId => UniqueId; - #region Define + public override string Name => "Template Cache Refresher"; - public static readonly Guid UniqueId = Guid.Parse("DD12B6A0-14B9-46e8-8800-C154F74047C8"); + public override void Refresh(int id) + { + RemoveFromCache(id); + base.Refresh(id); + } - public override Guid RefresherUniqueId => UniqueId; + public override void Remove(int id) + { + RemoveFromCache(id); - public override string Name => "Template Cache Refresher"; + // During removal we need to clear the runtime cache for templates, content and content type instances!!! + // all three of these types are referenced by templates, and the cache needs to be cleared on every server, + // otherwise things like looking up content type's after a template is removed is still going to show that + // it has an associated template. + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + _contentTypeCommonRepository.ClearCache(); - #endregion + base.Remove(id); + } - #region Refresher + private void RemoveFromCache(int id) + { + _idKeyMap.ClearCache(id); + AppCaches.RuntimeCache.Clear($"{CacheKeys.TemplateFrontEndCacheKey}{id}"); - public override void Refresh(int id) - { - RemoveFromCache(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - RemoveFromCache(id); - - //During removal we need to clear the runtime cache for templates, content and content type instances!!! - // all three of these types are referenced by templates, and the cache needs to be cleared on every server, - // otherwise things like looking up content type's after a template is removed is still going to show that - // it has an associated template. - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - _contentTypeCommonRepository.ClearCache(); - - base.Remove(id); - } - - private void RemoveFromCache(int id) - { - _idKeyMap.ClearCache(id); - AppCaches.RuntimeCache.Clear($"{CacheKeys.TemplateFrontEndCacheKey}{id}"); - - //need to clear the runtime cache for templates - ClearAllIsolatedCacheByEntityType(); - } - - #endregion + // need to clear the runtime cache for templates + ClearAllIsolatedCacheByEntityType(); } } diff --git a/src/Umbraco.Core/Cache/UserCacheRefresher.cs b/src/Umbraco.Core/Cache/UserCacheRefresher.cs index 10c4865ba8..d1dc194f9b 100644 --- a/src/Umbraco.Core/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserCacheRefresher.cs @@ -1,56 +1,55 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class UserCacheRefresher : CacheRefresherBase { - public sealed class UserCacheRefresher : CacheRefresherBase + #region Define + + public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6"); + + public UserCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public UserCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "User Cache Refresher"; - - #endregion - - #region Refresher - - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - var userCache = AppCaches.IsolatedCaches.Get(); - if (userCache.Success) - { - userCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); - } - - - base.Remove(id); - } - #endregion } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "User Cache Refresher"; + + #endregion + + #region Refresher + + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } + + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } + + public override void Remove(int id) + { + Attempt userCache = AppCaches.IsolatedCaches.Get(); + if (userCache.Success) + { + userCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); + userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); + userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); + userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); + userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); + } + + base.Remove(id); + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs index a889146794..ccf004a8d7 100644 --- a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs @@ -1,71 +1,70 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Handles User group cache invalidation/refreshing +/// +/// +/// This also needs to clear the user cache since IReadOnlyUserGroup's are attached to IUser objects +/// +public sealed class UserGroupCacheRefresher : CacheRefresherBase { - /// - /// Handles User group cache invalidation/refreshing - /// - /// - /// This also needs to clear the user cache since IReadOnlyUserGroup's are attached to IUser objects - /// - public sealed class UserGroupCacheRefresher : CacheRefresherBase + #region Define + + public static readonly Guid UniqueId = Guid.Parse("45178038-B232-4FE8-AA1A-F2B949C44762"); + + public UserGroupCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public UserGroupCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("45178038-B232-4FE8-AA1A-F2B949C44762"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "User Group Cache Refresher"; - - #endregion - - #region Refresher - - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - var userGroupCache = AppCaches.IsolatedCaches.Get(); - if (userGroupCache.Success) - { - userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); - } - - //We'll need to clear all user cache too - ClearAllIsolatedCacheByEntityType(); - - base.RefreshAll(); - } - - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - var userGroupCache = AppCaches.IsolatedCaches.Get(); - if (userGroupCache.Success) - { - userGroupCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); - } - - //we don't know what user's belong to this group without doing a look up so we'll need to just clear them all - ClearAllIsolatedCacheByEntityType(); - - base.Remove(id); - } - - #endregion } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "User Group Cache Refresher"; + + #endregion + + #region Refresher + + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + Attempt userGroupCache = AppCaches.IsolatedCaches.Get(); + if (userGroupCache.Success) + { + userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); + } + + // We'll need to clear all user cache too + ClearAllIsolatedCacheByEntityType(); + + base.RefreshAll(); + } + + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } + + public override void Remove(int id) + { + Attempt userGroupCache = AppCaches.IsolatedCaches.Get(); + if (userGroupCache.Success) + { + userGroupCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); + userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); + } + + // we don't know what user's belong to this group without doing a look up so we'll need to just clear them all + ClearAllIsolatedCacheByEntityType(); + + base.Remove(id); + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/ValueEditorCache.cs b/src/Umbraco.Core/Cache/ValueEditorCache.cs index 7d5f20efb4..358134ab14 100644 --- a/src/Umbraco.Core/Cache/ValueEditorCache.cs +++ b/src/Umbraco.Core/Cache/ValueEditorCache.cs @@ -1,60 +1,57 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public class ValueEditorCache : IValueEditorCache { - public class ValueEditorCache : IValueEditorCache + private readonly object _dictionaryLocker; + private readonly Dictionary> _valueEditorCache; + + public ValueEditorCache() { - private readonly Dictionary> _valueEditorCache; - private readonly object _dictionaryLocker; + _valueEditorCache = new Dictionary>(); + _dictionaryLocker = new object(); + } - public ValueEditorCache() + public IDataValueEditor GetValueEditor(IDataEditor editor, IDataType dataType) + { + // Lock just in case multiple threads uses the cache at the same time. + lock (_dictionaryLocker) { - _valueEditorCache = new Dictionary>(); - _dictionaryLocker = new object(); - } - - public IDataValueEditor GetValueEditor(IDataEditor editor, IDataType dataType) - { - // Lock just in case multiple threads uses the cache at the same time. - lock (_dictionaryLocker) + // We try and get the dictionary based on the IDataEditor alias, + // this is here just in case a data type can have more than one value data editor. + // If this is not the case this could be simplified quite a bit, by just using the inner dictionary only. + IDataValueEditor? valueEditor; + if (_valueEditorCache.TryGetValue(editor.Alias, out Dictionary? dataEditorCache)) { - // We try and get the dictionary based on the IDataEditor alias, - // this is here just in case a data type can have more than one value data editor. - // If this is not the case this could be simplified quite a bit, by just using the inner dictionary only. - IDataValueEditor? valueEditor; - if (_valueEditorCache.TryGetValue(editor.Alias, out Dictionary? dataEditorCache)) + if (dataEditorCache.TryGetValue(dataType.Id, out valueEditor)) { - if (dataEditorCache.TryGetValue(dataType.Id, out valueEditor)) - { - return valueEditor; - } - - valueEditor = editor.GetValueEditor(dataType.Configuration); - dataEditorCache[dataType.Id] = valueEditor; return valueEditor; } valueEditor = editor.GetValueEditor(dataType.Configuration); - _valueEditorCache[editor.Alias] = new Dictionary { [dataType.Id] = valueEditor }; + dataEditorCache[dataType.Id] = valueEditor; return valueEditor; } - } - public void ClearCache(IEnumerable dataTypeIds) + valueEditor = editor.GetValueEditor(dataType.Configuration); + _valueEditorCache[editor.Alias] = new Dictionary { [dataType.Id] = valueEditor }; + return valueEditor; + } + } + + public void ClearCache(IEnumerable dataTypeIds) + { + lock (_dictionaryLocker) { - lock (_dictionaryLocker) + // If a datatype is saved or deleted we have to clear any value editors based on their ID from the cache, + // since it could mean that their configuration has changed. + foreach (var id in dataTypeIds) { - // If a datatype is saved or deleted we have to clear any value editors based on their ID from the cache, - // since it could mean that their configuration has changed. - foreach (var id in dataTypeIds) + foreach (Dictionary editors in _valueEditorCache.Values) { - foreach (Dictionary editors in _valueEditorCache.Values) - { - editors.Remove(id); - } + editors.Remove(id); } } } diff --git a/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs b/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs index c815ca7a71..68ccdea20d 100644 --- a/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs @@ -1,57 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ValueEditorCacheRefresher : PayloadCacheRefresherBase { - public sealed class ValueEditorCacheRefresher : PayloadCacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("D28A1DBB-2308-4918-9A92-2F8689B6CBFE"); + private readonly IValueEditorCache _valueEditorCache; + + public ValueEditorCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory, + IValueEditorCache valueEditorCache) + : base(appCaches, serializer, eventAggregator, factory) => + _valueEditorCache = valueEditorCache; + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "ValueEditorCacheRefresher"; + + public override void Refresh(DataTypeCacheRefresher.JsonPayload[] payloads) { - private readonly IValueEditorCache _valueEditorCache; - - public ValueEditorCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory, - IValueEditorCache valueEditorCache) : base(appCaches, serializer, eventAggregator, factory) - { - _valueEditorCache = valueEditorCache; - } - - public static readonly Guid UniqueId = Guid.Parse("D28A1DBB-2308-4918-9A92-2F8689B6CBFE"); - public override Guid RefresherUniqueId => UniqueId; - public override string Name => "ValueEditorCacheRefresher"; - - public override void Refresh(DataTypeCacheRefresher.JsonPayload[] payloads) - { - IEnumerable ids = payloads.Select(x => x.Id); - _valueEditorCache.ClearCache(ids); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } + IEnumerable ids = payloads.Select(x => x.Id); + _valueEditorCache.ClearCache(ids); } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); } diff --git a/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs b/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs index f6ee121742..12e95f1e04 100644 --- a/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs +++ b/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs @@ -1,35 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.CodeAnnotations +/// +/// Attribute to add a Friendly Name string with an UmbracoObjectType enum value +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +public class FriendlyNameAttribute : Attribute { /// - /// Attribute to add a Friendly Name string with an UmbracoObjectType enum value + /// friendly name value /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)] - public class FriendlyNameAttribute : Attribute - { - /// - /// friendly name value - /// - private readonly string _friendlyName; + private readonly string _friendlyName; - /// - /// Initializes a new instance of the FriendlyNameAttribute class - /// Sets the friendly name value - /// - /// attribute value - public FriendlyNameAttribute(string friendlyName) - { - this._friendlyName = friendlyName; - } + /// + /// Initializes a new instance of the FriendlyNameAttribute class + /// Sets the friendly name value + /// + /// attribute value + public FriendlyNameAttribute(string friendlyName) => _friendlyName = friendlyName; - /// - /// Gets the friendly name - /// - /// string of friendly name - public override string ToString() - { - return this._friendlyName; - } - } + /// + /// Gets the friendly name + /// + /// string of friendly name + public override string ToString() => _friendlyName; } diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs index 6c4e2b9d04..13ec38f892 100644 --- a/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs +++ b/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs @@ -1,26 +1,20 @@ -using System; +namespace Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.CodeAnnotations +/// +/// Attribute to associate a GUID string and Type with an UmbracoObjectType Enum value +/// +[AttributeUsage(AttributeTargets.Field)] +public class UmbracoObjectTypeAttribute : Attribute { - /// - /// Attribute to associate a GUID string and Type with an UmbracoObjectType Enum value - /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] - public class UmbracoObjectTypeAttribute : Attribute + public UmbracoObjectTypeAttribute(string objectId) => ObjectId = new Guid(objectId); + + public UmbracoObjectTypeAttribute(string objectId, Type modelType) { - public UmbracoObjectTypeAttribute(string objectId) - { - ObjectId = new Guid(objectId); - } - - public UmbracoObjectTypeAttribute(string objectId, Type modelType) - { - ObjectId = new Guid(objectId); - ModelType = modelType; - } - - public Guid ObjectId { get; private set; } - - public Type? ModelType { get; private set; } + ObjectId = new Guid(objectId); + ModelType = modelType; } + + public Guid ObjectId { get; } + + public Type? ModelType { get; } } diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs index 5f889daa5c..90df3185c6 100644 --- a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs +++ b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs @@ -1,15 +1,9 @@ -using System; +namespace Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.CodeAnnotations +[AttributeUsage(AttributeTargets.Field)] +public class UmbracoUdiTypeAttribute : Attribute { - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] - public class UmbracoUdiTypeAttribute : Attribute - { - public string UdiType { get; private set; } + public UmbracoUdiTypeAttribute(string udiType) => UdiType = udiType; - public UmbracoUdiTypeAttribute(string udiType) - { - UdiType = udiType; - } - } + public string UdiType { get; } } diff --git a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs index a9bd71c6cc..abbde4f3f0 100644 --- a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs @@ -1,43 +1,44 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (int, string) for fast dictionaries. +/// +/// +/// The integer part of the key must be greater than, or equal to, zero. +/// The string part of the key is case-insensitive. +/// Null is a valid value for both parts. +/// +public struct CompositeIntStringKey : IEquatable { - /// - /// Represents a composite key of (int, string) for fast dictionaries. - /// - /// - /// The integer part of the key must be greater than, or equal to, zero. - /// The string part of the key is case-insensitive. - /// Null is a valid value for both parts. - /// - public struct CompositeIntStringKey : IEquatable - { - private readonly int _key1; - private readonly string _key2; + private readonly int _key1; + private readonly string _key2; - /// - /// Initializes a new instance of the struct. - /// - public CompositeIntStringKey(int? key1, string key2) + /// + /// Initializes a new instance of the struct. + /// + public CompositeIntStringKey(int? key1, string? key2) + { + if (key1 < 0) { - if (key1 < 0) throw new ArgumentOutOfRangeException(nameof(key1)); - _key1 = key1 ?? -1; - _key2 = key2?.ToLowerInvariant() ?? "NULL"; + throw new ArgumentOutOfRangeException(nameof(key1)); } - public bool Equals(CompositeIntStringKey other) - => _key2 == other._key2 && _key1 == other._key1; - - public override bool Equals(object? obj) - => obj is CompositeIntStringKey other && _key2 == other._key2 && _key1 == other._key1; - - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1; - - public static bool operator ==(CompositeIntStringKey key1, CompositeIntStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; - - public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._key1; + _key1 = key1 ?? -1; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; } + + public static bool operator ==(CompositeIntStringKey key1, CompositeIntStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + + public bool Equals(CompositeIntStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object? obj) + => obj is CompositeIntStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => (_key2.GetHashCode() * 31) + _key1; } diff --git a/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs b/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs index 2886de92f1..0b3ec1aa92 100644 --- a/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs @@ -1,41 +1,38 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (string, string) for fast dictionaries. +/// +/// +/// The string parts of the key are case-insensitive. +/// Null is a valid value for both parts. +/// +public struct CompositeNStringNStringKey : IEquatable { + private readonly string _key1; + private readonly string _key2; + /// - /// Represents a composite key of (string, string) for fast dictionaries. + /// Initializes a new instance of the struct. /// - /// - /// The string parts of the key are case-insensitive. - /// Null is a valid value for both parts. - /// - public struct CompositeNStringNStringKey : IEquatable + public CompositeNStringNStringKey(string? key1, string? key2) { - private readonly string _key1; - private readonly string _key2; - - /// - /// Initializes a new instance of the struct. - /// - public CompositeNStringNStringKey(string? key1, string? key2) - { - _key1 = key1?.ToLowerInvariant() ?? "NULL"; - _key2 = key2?.ToLowerInvariant() ?? "NULL"; - } - - public bool Equals(CompositeNStringNStringKey other) - => _key2 == other._key2 && _key1 == other._key1; - - public override bool Equals(object? obj) - => obj is CompositeNStringNStringKey other && _key2 == other._key2 && _key1 == other._key1; - - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1.GetHashCode(); - - public static bool operator ==(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; - - public static bool operator !=(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._key1; + _key1 = key1?.ToLowerInvariant() ?? "NULL"; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; } + + public static bool operator ==(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + + public bool Equals(CompositeNStringNStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object? obj) + => obj is CompositeNStringNStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => (_key2.GetHashCode() * 31) + _key1.GetHashCode(); } diff --git a/src/Umbraco.Core/Collections/CompositeStringStringKey.cs b/src/Umbraco.Core/Collections/CompositeStringStringKey.cs index 01f94bf149..6fd25f6c12 100644 --- a/src/Umbraco.Core/Collections/CompositeStringStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeStringStringKey.cs @@ -1,41 +1,38 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (string, string) for fast dictionaries. +/// +/// +/// The string parts of the key are case-insensitive. +/// Null is NOT a valid value for neither parts. +/// +public struct CompositeStringStringKey : IEquatable { + private readonly string _key1; + private readonly string _key2; + /// - /// Represents a composite key of (string, string) for fast dictionaries. + /// Initializes a new instance of the struct. /// - /// - /// The string parts of the key are case-insensitive. - /// Null is NOT a valid value for neither parts. - /// - public struct CompositeStringStringKey : IEquatable + public CompositeStringStringKey(string? key1, string? key2) { - private readonly string _key1; - private readonly string _key2; - - /// - /// Initializes a new instance of the struct. - /// - public CompositeStringStringKey(string? key1, string? key2) - { - _key1 = key1?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key1)); - _key2 = key2?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key2)); - } - - public bool Equals(CompositeStringStringKey other) - => _key2 == other._key2 && _key1 == other._key1; - - public override bool Equals(object? obj) - => obj is CompositeStringStringKey other && _key2 == other._key2 && _key1 == other._key1; - - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1.GetHashCode(); - - public static bool operator ==(CompositeStringStringKey key1, CompositeStringStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; - - public static bool operator !=(CompositeStringStringKey key1, CompositeStringStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._key1; + _key1 = key1?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key1)); + _key2 = key2?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key2)); } + + public static bool operator ==(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + + public bool Equals(CompositeStringStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object? obj) + => obj is CompositeStringStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => (_key2.GetHashCode() * 31) + _key1.GetHashCode(); } diff --git a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs index ea737e0522..ea9a5a496f 100644 --- a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs +++ b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs @@ -1,62 +1,52 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (Type, Type) for fast dictionaries. +/// +public struct CompositeTypeTypeKey : IEquatable { /// - /// Represents a composite key of (Type, Type) for fast dictionaries. + /// Initializes a new instance of the struct. /// - public struct CompositeTypeTypeKey : IEquatable + public CompositeTypeTypeKey(Type type1, Type type2) + : this() { - /// - /// Initializes a new instance of the struct. - /// - public CompositeTypeTypeKey(Type type1, Type type2) - : this() + Type1 = type1; + Type2 = type2; + } + + /// + /// Gets the first type. + /// + public Type Type1 { get; } + + /// + /// Gets the second type. + /// + public Type Type2 { get; } + + public static bool operator ==(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) => + key1.Type1 == key2.Type1 && key1.Type2 == key2.Type2; + + public static bool operator !=(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) => + key1.Type1 != key2.Type1 || key1.Type2 != key2.Type2; + + /// + public bool Equals(CompositeTypeTypeKey other) => Type1 == other.Type1 && Type2 == other.Type2; + + /// + public override bool Equals(object? obj) + { + CompositeTypeTypeKey other = obj is CompositeTypeTypeKey key ? key : default; + return Type1 == other.Type1 && Type2 == other.Type2; + } + + /// + public override int GetHashCode() + { + unchecked { - Type1 = type1; - Type2 = type2; - } - - /// - /// Gets the first type. - /// - public Type Type1 { get; } - - /// - /// Gets the second type. - /// - public Type Type2 { get; } - - /// - public bool Equals(CompositeTypeTypeKey other) - { - return Type1 == other.Type1 && Type2 == other.Type2; - } - - /// - public override bool Equals(object? obj) - { - var other = obj is CompositeTypeTypeKey key ? key : default; - return Type1 == other.Type1 && Type2 == other.Type2; - } - - public static bool operator ==(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) - { - return key1.Type1 == key2.Type1 && key1.Type2 == key2.Type2; - } - - public static bool operator !=(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) - { - return key1.Type1 != key2.Type1 || key1.Type2 != key2.Type2; - } - - /// - public override int GetHashCode() - { - unchecked - { - return (Type1.GetHashCode() * 397) ^ Type2.GetHashCode(); - } + return (Type1.GetHashCode() * 397) ^ Type2.GetHashCode(); } } } diff --git a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs index f9c10e5607..a79c61a173 100644 --- a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs +++ b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs @@ -1,216 +1,277 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// A thread-safe representation of a . +/// Enumerating this collection is thread-safe and will only operate on a clone that is generated before returning the +/// enumerator. +/// +/// +[Serializable] +public class ConcurrentHashSet : ICollection { + private readonly HashSet _innerSet = new(); + private readonly ReaderWriterLockSlim _instanceLocker = new(LockRecursionPolicy.NoRecursion); + /// - /// A thread-safe representation of a . - /// Enumerating this collection is thread-safe and will only operate on a clone that is generated before returning the enumerator. + /// Gets the number of elements contained in the . /// - /// - [Serializable] - public class ConcurrentHashSet : ICollection + /// + /// The number of elements contained in the . + /// + /// 2 + public int Count { - private readonly HashSet _innerSet = new HashSet(); - private readonly ReaderWriterLockSlim _instanceLocker = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// A that can be used to iterate through the collection. - /// - /// 1 - public IEnumerator GetEnumerator() - { - return GetThreadSafeClone().GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - /// 2 - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Removes the first occurrence of a specific object from the . - /// - /// - /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . - /// - /// The object to remove from the .The is read-only. - public bool Remove(T item) - { - try - { - _instanceLocker.EnterWriteLock(); - return _innerSet.Remove(item); - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - - /// - /// Gets the number of elements contained in the . - /// - /// - /// The number of elements contained in the . - /// - /// 2 - public int Count - { - get - { - try - { - _instanceLocker.EnterReadLock(); - return _innerSet.Count; - } - finally - { - if (_instanceLocker.IsReadLockHeld) - _instanceLocker.ExitReadLock(); - } - - } - } - - /// - /// Gets a value indicating whether the is read-only. - /// - /// - /// true if the is read-only; otherwise, false. - /// - public bool IsReadOnly => false; - - /// - /// Adds an item to the . - /// - /// The object to add to the .The is read-only. - public void Add(T item) - { - try - { - _instanceLocker.EnterWriteLock(); - _innerSet.Add(item); - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - /// - /// Attempts to add an item to the collection - /// - /// - /// - public bool TryAdd(T item) - { - if (Contains(item)) return false; - try - { - _instanceLocker.EnterWriteLock(); - - //double check - if (_innerSet.Contains(item)) return false; - _innerSet.Add(item); - return true; - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - /// - /// Removes all items from the . - /// - /// The is read-only. - public void Clear() - { - try - { - _instanceLocker.EnterWriteLock(); - _innerSet.Clear(); - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - /// - /// Determines whether the contains a specific value. - /// - /// - /// true if is found in the ; otherwise, false. - /// - /// The object to locate in the . - public bool Contains(T item) + get { try { _instanceLocker.EnterReadLock(); - return _innerSet.Contains(item); + return _innerSet.Count; } finally { if (_instanceLocker.IsReadLockHeld) + { _instanceLocker.ExitReadLock(); + } } } - - /// - /// Copies the elements of the to an , starting at a specified index. - /// - /// The one-dimensional that is the destination of the elements copied from the . The array must have zero-based indexing.The zero-based index in at which copying begins. is a null reference (Nothing in Visual Basic). is less than zero. is equal to or greater than the length of the -or- The number of elements in the source is greater than the available space from to the end of the destination . - public void CopyTo(T[] array, int index) - { - var clone = GetThreadSafeClone(); - clone.CopyTo(array, index); - } - - private HashSet GetThreadSafeClone() - { - HashSet? clone = null; - try - { - _instanceLocker.EnterReadLock(); - clone = new HashSet(_innerSet, _innerSet.Comparer); - } - finally - { - if (_instanceLocker.IsReadLockHeld) - _instanceLocker.ExitReadLock(); - } - return clone; - } - - /// - /// Copies the elements of the to an , starting at a particular index. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. The zero-based index in at which copying begins. is null. is less than zero. is multidimensional.-or- The number of elements in the source is greater than the available space from to the end of the destination . The type of the source cannot be cast automatically to the type of the destination . 2 - public void CopyTo(Array array, int index) - { - var clone = GetThreadSafeClone(); - Array.Copy(clone.ToArray(), 0, array, index, clone.Count); - } + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + public IEnumerator GetEnumerator() => GetThreadSafeClone().GetEnumerator(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + /// 2 + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Removes the first occurrence of a specific object from the + /// . + /// + /// + /// true if was successfully removed from the + /// ; otherwise, false. This method also returns false if + /// is not found in the original . + /// + /// The object to remove from the . + /// + /// The is + /// read-only. + /// + public bool Remove(T item) + { + try + { + _instanceLocker.EnterWriteLock(); + return _innerSet.Remove(item); + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Gets a value indicating whether the is read-only. + /// + /// + /// true if the is read-only; otherwise, false. + /// + public bool IsReadOnly => false; + + /// + /// Adds an item to the . + /// + /// The object to add to the . + /// + /// The is + /// read-only. + /// + public void Add(T item) + { + try + { + _instanceLocker.EnterWriteLock(); + _innerSet.Add(item); + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Removes all items from the . + /// + /// + /// The is + /// read-only. + /// + public void Clear() + { + try + { + _instanceLocker.EnterWriteLock(); + _innerSet.Clear(); + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Determines whether the contains a specific value. + /// + /// + /// true if is found in the ; + /// otherwise, false. + /// + /// The object to locate in the . + public bool Contains(T item) + { + try + { + _instanceLocker.EnterReadLock(); + return _innerSet.Contains(item); + } + finally + { + if (_instanceLocker.IsReadLockHeld) + { + _instanceLocker.ExitReadLock(); + } + } + } + + /// + /// Copies the elements of the to an + /// , starting at a specified index. + /// + /// + /// The one-dimensional that is the destination of the elements copied + /// from the . The array must have + /// zero-based indexing. + /// + /// The zero-based index in at which copying begins. + /// + /// is a null reference (Nothing in Visual + /// Basic). + /// + /// is less than zero. + /// + /// is equal to or greater than the length of the + /// -or- The number of elements in the source + /// is greater than the available space from + /// to the end of the destination . + /// + public void CopyTo(T[] array, int index) + { + HashSet clone = GetThreadSafeClone(); + clone.CopyTo(array, index); + } + + /// + /// Attempts to add an item to the collection + /// + /// + /// + public bool TryAdd(T item) + { + if (Contains(item)) + { + return false; + } + + try + { + _instanceLocker.EnterWriteLock(); + + // double check + if (_innerSet.Contains(item)) + { + return false; + } + + _innerSet.Add(item); + return true; + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Copies the elements of the to an , + /// starting at a particular index. + /// + /// + /// The one-dimensional that is the destination of the elements copied + /// from . The must have zero-based + /// indexing. + /// + /// The zero-based index in at which copying begins. + /// is null. + /// is less than zero. + /// + /// is multidimensional.-or- The number of elements + /// in the source is greater than the available space from + /// to the end of the destination . + /// + /// + /// The type of the source + /// cannot be cast automatically to the type of the destination . + /// + /// 2 + public void CopyTo(Array array, int index) + { + HashSet clone = GetThreadSafeClone(); + Array.Copy(clone.ToArray(), 0, array, index, clone.Count); + } + + private HashSet GetThreadSafeClone() + { + HashSet? clone = null; + try + { + _instanceLocker.EnterReadLock(); + clone = new HashSet(_innerSet, _innerSet.Comparer); + } + finally + { + if (_instanceLocker.IsReadLockHeld) + { + _instanceLocker.ExitReadLock(); + } + } + + return clone; } } diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs index db7677153c..301795281c 100644 --- a/src/Umbraco.Core/Collections/DeepCloneableList.cs +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -1,159 +1,137 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// A List that can be deep cloned with deep cloned elements and can reset the collection's items dirty flags +/// +/// +public class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty { + private readonly ListCloneBehavior _listCloneBehavior; + + public DeepCloneableList(ListCloneBehavior listCloneBehavior) => _listCloneBehavior = listCloneBehavior; + + public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) + : base(collection) => + _listCloneBehavior = listCloneBehavior; + /// - /// A List that can be deep cloned with deep cloned elements and can reset the collection's items dirty flags + /// Default behavior is CloneOnce /// - /// - public class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty + /// + public DeepCloneableList(IEnumerable collection) + : this(collection, ListCloneBehavior.CloneOnce) { - private readonly ListCloneBehavior _listCloneBehavior; - - public DeepCloneableList(ListCloneBehavior listCloneBehavior) - { - _listCloneBehavior = listCloneBehavior; - } - - public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) : base(collection) - { - _listCloneBehavior = listCloneBehavior; - } - - /// - /// Default behavior is CloneOnce - /// - /// - public DeepCloneableList(IEnumerable collection) - : this(collection, ListCloneBehavior.CloneOnce) - { - } - - /// - /// Creates a new list and adds each element as a deep cloned element if it is of type IDeepCloneable - /// - /// - public object DeepClone() - { - switch (_listCloneBehavior) - { - case ListCloneBehavior.CloneOnce: - //we are cloning once, so create a new list in none mode - // and deep clone all items into it - var newList = new DeepCloneableList(ListCloneBehavior.None); - foreach (var item in this) - { - if (item is IDeepCloneable dc) - { - newList.Add((T)dc.DeepClone()); - } - else - { - newList.Add(item); - } - } - return newList; - case ListCloneBehavior.None: - //we are in none mode, so just return a new list with the same items - return new DeepCloneableList(this, ListCloneBehavior.None); - case ListCloneBehavior.Always: - //always clone to new list - var newList2 = new DeepCloneableList(ListCloneBehavior.Always); - foreach (var item in this) - { - if (item is IDeepCloneable dc) - { - newList2.Add((T)dc.DeepClone()); - } - else - { - newList2.Add(item); - } - } - return newList2; - default: - throw new ArgumentOutOfRangeException(); - } - } - - #region IRememberBeingDirty - public bool IsDirty() - { - return this.OfType().Any(x => x.IsDirty()); - } - - public bool WasDirty() - { - return this.OfType().Any(x => x.WasDirty()); - } - - /// - /// Always return false, the list has no properties that can be dirty. - public bool IsPropertyDirty(string propName) - { - return false; - } - - /// - /// Always return false, the list has no properties that can be dirty. - public bool WasPropertyDirty(string propertyName) - { - return false; - } - - /// - /// Always return an empty enumerable, the list has no properties that can be dirty. - public IEnumerable GetDirtyProperties() - { - return Enumerable.Empty(); - } - - public void ResetDirtyProperties() - { - foreach (var dc in this.OfType()) - { - dc.ResetDirtyProperties(); - } - } - - public void DisableChangeTracking() - { - // noop - } - - public void EnableChangeTracking() - { - // noop - } - - public void ResetWereDirtyProperties() - { - foreach (var dc in this.OfType()) - { - dc.ResetWereDirtyProperties(); - } - } - - public void ResetDirtyProperties(bool rememberDirty) - { - foreach (var dc in this.OfType()) - { - dc.ResetDirtyProperties(rememberDirty); - } - } - - /// Always return an empty enumerable, the list has no properties that can be dirty. - public IEnumerable GetWereDirtyProperties() - { - return Enumerable.Empty(); - } - - public event PropertyChangedEventHandler? PropertyChanged; // noop - #endregion } + + public event PropertyChangedEventHandler? PropertyChanged; // noop + + /// + /// Creates a new list and adds each element as a deep cloned element if it is of type IDeepCloneable + /// + /// + public object DeepClone() + { + switch (_listCloneBehavior) + { + case ListCloneBehavior.CloneOnce: + // we are cloning once, so create a new list in none mode + // and deep clone all items into it + var newList = new DeepCloneableList(ListCloneBehavior.None); + foreach (T item in this) + { + if (item is IDeepCloneable dc) + { + newList.Add((T)dc.DeepClone()); + } + else + { + newList.Add(item); + } + } + + return newList; + case ListCloneBehavior.None: + // we are in none mode, so just return a new list with the same items + return new DeepCloneableList(this, ListCloneBehavior.None); + case ListCloneBehavior.Always: + // always clone to new list + var newList2 = new DeepCloneableList(ListCloneBehavior.Always); + foreach (T item in this) + { + if (item is IDeepCloneable dc) + { + newList2.Add((T)dc.DeepClone()); + } + else + { + newList2.Add(item); + } + } + + return newList2; + default: + throw new ArgumentOutOfRangeException(); + } + } + + #region IRememberBeingDirty + + public bool IsDirty() => this.OfType().Any(x => x.IsDirty()); + + public bool WasDirty() => this.OfType().Any(x => x.WasDirty()); + + /// + /// Always return false, the list has no properties that can be dirty. + public bool IsPropertyDirty(string propName) => false; + + /// + /// Always return false, the list has no properties that can be dirty. + public bool WasPropertyDirty(string propertyName) => false; + + /// + /// Always return an empty enumerable, the list has no properties that can be dirty. + public IEnumerable GetDirtyProperties() => Enumerable.Empty(); + + public void ResetDirtyProperties() + { + foreach (IRememberBeingDirty dc in this.OfType()) + { + dc.ResetDirtyProperties(); + } + } + + public void DisableChangeTracking() + { + // noop + } + + public void EnableChangeTracking() + { + // noop + } + + public void ResetWereDirtyProperties() + { + foreach (IRememberBeingDirty dc in this.OfType()) + { + dc.ResetWereDirtyProperties(); + } + } + + public void ResetDirtyProperties(bool rememberDirty) + { + foreach (IRememberBeingDirty dc in this.OfType()) + { + dc.ResetDirtyProperties(rememberDirty); + } + } + + /// Always return an empty enumerable, the list has no properties that can be dirty. + public IEnumerable GetWereDirtyProperties() => Enumerable.Empty(); + + #endregion } diff --git a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs index f4702f0124..579716456b 100644 --- a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs +++ b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs @@ -1,41 +1,42 @@ -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// Allows clearing all event handlers +/// +/// +public class EventClearingObservableCollection : ObservableCollection, INotifyCollectionChanged { - /// - /// Allows clearing all event handlers - /// - /// - public class EventClearingObservableCollection : ObservableCollection, INotifyCollectionChanged + // need to explicitly implement with event accessor syntax in order to override in order to to clear + // c# events are weird, they do not behave the same way as other c# things that are 'virtual', + // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 + // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event + private NotifyCollectionChangedEventHandler? _changed; + + public EventClearingObservableCollection() { - public EventClearingObservableCollection() - { - } - - public EventClearingObservableCollection(List list) : base(list) - { - } - - public EventClearingObservableCollection(IEnumerable collection) : base(collection) - { - } - - // need to explicitly implement with event accessor syntax in order to override in order to to clear - // c# events are weird, they do not behave the same way as other c# things that are 'virtual', - // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 - // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event - private NotifyCollectionChangedEventHandler? _changed; - event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged - { - add { _changed += value; } - remove { _changed -= value; } - } - - /// - /// Clears all event handlers for the event - /// - public void ClearCollectionChangedEvents() => _changed = null; } + + public EventClearingObservableCollection(List list) + : base(list) + { + } + + public EventClearingObservableCollection(IEnumerable collection) + : base(collection) + { + } + + event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged + { + add => _changed += value; + remove => _changed -= value; + } + + /// + /// Clears all event handlers for the event + /// + public void ClearCollectionChangedEvents() => _changed = null; } diff --git a/src/Umbraco.Core/Collections/ListCloneBehavior.cs b/src/Umbraco.Core/Collections/ListCloneBehavior.cs index 148141f783..4fc9edf3ae 100644 --- a/src/Umbraco.Core/Collections/ListCloneBehavior.cs +++ b/src/Umbraco.Core/Collections/ListCloneBehavior.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +public enum ListCloneBehavior { - public enum ListCloneBehavior - { - /// - /// When set, DeepClone will clone the items one time and the result list behavior will be None - /// - CloneOnce, + /// + /// When set, DeepClone will clone the items one time and the result list behavior will be None + /// + CloneOnce, - /// - /// When set, DeepClone will not clone any items - /// - None, + /// + /// When set, DeepClone will not clone any items + /// + None, - /// - /// When set, DeepClone will always clone all items - /// - Always - } + /// + /// When set, DeepClone will always clone all items + /// + Always, } diff --git a/src/Umbraco.Core/Collections/ObservableDictionary.cs b/src/Umbraco.Core/Collections/ObservableDictionary.cs index 1ea6a827c4..9e52b4dae7 100644 --- a/src/Umbraco.Core/Collections/ObservableDictionary.cs +++ b/src/Umbraco.Core/Collections/ObservableDictionary.cs @@ -1,257 +1,250 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Linq; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// An ObservableDictionary +/// +/// +/// Assumes that the key will not change and is unique for each element in the collection. +/// Collection is not thread-safe, so calls should be made single-threaded. +/// +/// The type of elements contained in the BindableCollection +/// The type of the indexing key +public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, + IDictionary, INotifyCollectionChanged + where TKey : notnull { + // need to explicitly implement with event accessor syntax in order to override in order to to clear + // c# events are weird, they do not behave the same way as other c# things that are 'virtual', + // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 + // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event + private NotifyCollectionChangedEventHandler? _changed; /// - /// An ObservableDictionary + /// Create new ObservableDictionary /// - /// - /// Assumes that the key will not change and is unique for each element in the collection. - /// Collection is not thread-safe, so calls should be made single-threaded. - /// - /// The type of elements contained in the BindableCollection - /// The type of the indexing key - public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, IDictionary, INotifyCollectionChanged - where TKey : notnull + /// Selector function to create key from value + /// The equality comparer to use when comparing keys, or null to use the default comparer. + public ObservableDictionary(Func keySelector, IEqualityComparer? equalityComparer = null) { - protected Dictionary Indecies { get; } - protected Func KeySelector { get; } + KeySelector = keySelector ?? throw new ArgumentException(nameof(keySelector)); + Indecies = new Dictionary(equalityComparer); + } - /// - /// Create new ObservableDictionary - /// - /// Selector function to create key from value - /// The equality comparer to use when comparing keys, or null to use the default comparer. - public ObservableDictionary(Func keySelector, IEqualityComparer? equalityComparer = null) + protected Dictionary Indecies { get; } + + protected Func KeySelector { get; } + + public bool Remove(TKey key) + { + if (!Indecies.ContainsKey(key)) { - KeySelector = keySelector ?? throw new ArgumentException(nameof(keySelector)); - Indecies = new Dictionary(equalityComparer); - } - - #region Protected Methods - - protected override void InsertItem(int index, TValue item) - { - var key = KeySelector(item); - if (Indecies.ContainsKey(key)) - throw new ArgumentException($"An element with the same key '{key}' already exists in the dictionary.", nameof(item)); - - if (index != Count) - { - foreach (var k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList()) - { - Indecies[k]++; - } - } - - base.InsertItem(index, item); - Indecies[key] = index; - } - - protected override void ClearItems() - { - base.ClearItems(); - Indecies.Clear(); - } - - protected override void RemoveItem(int index) - { - var item = this[index]; - var key = KeySelector(item); - - base.RemoveItem(index); - - Indecies.Remove(key); - - foreach (var k in Indecies.Keys.Where(k => Indecies[k] > index).ToList()) - { - Indecies[k]--; - } - } - - #endregion - - // need to explicitly implement with event accessor syntax in order to override in order to to clear - // c# events are weird, they do not behave the same way as other c# things that are 'virtual', - // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 - // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event - private NotifyCollectionChangedEventHandler? _changed; - event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged - { - add { _changed += value; } - remove { _changed -= value; } - } - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => _changed = null; - - public bool ContainsKey(TKey key) - { - return Indecies.ContainsKey(key); - } - - /// - /// Gets or sets the element with the specified key. If setting a new value, new value must have same key. - /// - /// Key of element to replace - /// - public TValue this[TKey key] - { - - get => this[Indecies[key]]; - set - { - //confirm key matches - if (!KeySelector(value)!.Equals(key)) - throw new InvalidOperationException("Key of new value does not match."); - - if (!Indecies.ContainsKey(key)) - { - Add(value); - } - else - { - this[Indecies[key]] = value; - } - } - } - - /// - /// Replaces element at given key with new value. New value must have same key. - /// - /// Key of element to replace - /// New value - /// - /// - /// False if key not found - public bool Replace(TKey key, TValue value) - { - if (!Indecies.ContainsKey(key)) return false; - - //confirm key matches - if (!KeySelector(value)!.Equals(key)) - throw new InvalidOperationException("Key of new value does not match."); - - this[Indecies[key]] = value; - return true; - - } - - public void ReplaceAll(IEnumerable values) - { - if (values == null) throw new ArgumentNullException(nameof(values)); - - Clear(); - - foreach (var value in values) - { - Add(value); - } - } - - public bool Remove(TKey key) - { - if (!Indecies.ContainsKey(key)) return false; - - RemoveAt(Indecies[key]); - return true; - - } - - /// - /// Allows us to change the key of an item - /// - /// - /// - public void ChangeKey(TKey currentKey, TKey newKey) - { - if (!Indecies.ContainsKey(currentKey)) - { - throw new InvalidOperationException($"No item with the key '{currentKey}' was found in the dictionary."); - } - - if (ContainsKey(newKey)) - { - throw new ArgumentException($"An element with the same key '{newKey}' already exists in the dictionary.", nameof(newKey)); - } - - var currentIndex = Indecies[currentKey]; - - Indecies.Remove(currentKey); - Indecies.Add(newKey, currentIndex); - } - - #region IDictionary and IReadOnlyDictionary implementation - - public bool TryGetValue(TKey key, out TValue val) - { - if (Indecies.TryGetValue(key, out var index)) - { - val = this[index]; - return true; - } - val = default!; return false; } - /// - /// Returns all keys - /// - public IEnumerable Keys => Indecies.Keys; + RemoveAt(Indecies[key]); + return true; + } - /// - /// Returns all values - /// - public IEnumerable Values => base.Items; + event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged + { + add => _changed += value; + remove => _changed -= value; + } - ICollection IDictionary.Keys => Indecies.Keys; + public bool ContainsKey(TKey key) => Indecies.ContainsKey(key); - //this will never be used - ICollection IDictionary.Values => Values.ToList(); - - bool ICollection>.IsReadOnly => false; - - IEnumerator> IEnumerable>.GetEnumerator() + /// + /// Gets or sets the element with the specified key. If setting a new value, new value must have same key. + /// + /// Key of element to replace + /// + public TValue this[TKey key] + { + get => this[Indecies[key]]; + set { - foreach (var i in Values) + // confirm key matches + if (!KeySelector(value)!.Equals(key)) { - var key = KeySelector(i); - yield return new KeyValuePair(key, i); + throw new InvalidOperationException("Key of new value does not match."); + } + + if (!Indecies.ContainsKey(key)) + { + Add(value); + } + else + { + this[Indecies[key]] = value; } } + } - void IDictionary.Add(TKey key, TValue value) + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => _changed = null; + + /// + /// Replaces element at given key with new value. New value must have same key. + /// + /// Key of element to replace + /// New value + /// + /// False if key not found + public bool Replace(TKey key, TValue value) + { + if (!Indecies.ContainsKey(key)) + { + return false; + } + + // confirm key matches + if (!KeySelector(value)!.Equals(key)) + { + throw new InvalidOperationException("Key of new value does not match."); + } + + this[Indecies[key]] = value; + return true; + } + + public void ReplaceAll(IEnumerable values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + Clear(); + + foreach (TValue value in values) { Add(value); } - - void ICollection>.Add(KeyValuePair item) - { - Add(item.Value); - } - - bool ICollection>.Contains(KeyValuePair item) - { - return ContainsKey(item.Key); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection>.Remove(KeyValuePair item) - { - return Remove(item.Key); - } - - #endregion } + + /// + /// Allows us to change the key of an item + /// + /// + /// + public void ChangeKey(TKey currentKey, TKey newKey) + { + if (!Indecies.ContainsKey(currentKey)) + { + throw new InvalidOperationException($"No item with the key '{currentKey}' was found in the dictionary."); + } + + if (ContainsKey(newKey)) + { + throw new ArgumentException($"An element with the same key '{newKey}' already exists in the dictionary.", nameof(newKey)); + } + + var currentIndex = Indecies[currentKey]; + + Indecies.Remove(currentKey); + Indecies.Add(newKey, currentIndex); + } + + #region Protected Methods + + protected override void InsertItem(int index, TValue item) + { + TKey key = KeySelector(item); + if (Indecies.ContainsKey(key)) + { + throw new ArgumentException($"An element with the same key '{key}' already exists in the dictionary.", nameof(item)); + } + + if (index != Count) + { + foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList()) + { + Indecies[k]++; + } + } + + base.InsertItem(index, item); + Indecies[key] = index; + } + + protected override void ClearItems() + { + base.ClearItems(); + Indecies.Clear(); + } + + protected override void RemoveItem(int index) + { + TValue item = this[index]; + TKey key = KeySelector(item); + + base.RemoveItem(index); + + Indecies.Remove(key); + + foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] > index).ToList()) + { + Indecies[k]--; + } + } + + #endregion + + #region IDictionary and IReadOnlyDictionary implementation + + public bool TryGetValue(TKey key, out TValue val) + { + if (Indecies.TryGetValue(key, out var index)) + { + val = this[index]; + return true; + } + + val = default!; + return false; + } + + /// + /// Returns all keys + /// + public IEnumerable Keys => Indecies.Keys; + + /// + /// Returns all values + /// + public IEnumerable Values => Items; + + ICollection IDictionary.Keys => Indecies.Keys; + + // this will never be used + ICollection IDictionary.Values => Values.ToList(); + + bool ICollection>.IsReadOnly => false; + + IEnumerator> IEnumerable>.GetEnumerator() + { + foreach (TValue i in Values) + { + TKey key = KeySelector(i); + yield return new KeyValuePair(key, i); + } + } + + void IDictionary.Add(TKey key, TValue value) => Add(value); + + void ICollection>.Add(KeyValuePair item) => Add(item.Value); + + bool ICollection>.Contains(KeyValuePair item) => ContainsKey(item.Key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => + throw new NotImplementedException(); + + bool ICollection>.Remove(KeyValuePair item) => Remove(item.Key); + + #endregion } diff --git a/src/Umbraco.Core/Collections/OrderedHashSet.cs b/src/Umbraco.Core/Collections/OrderedHashSet.cs index e5a34083be..d23c81a7b2 100644 --- a/src/Umbraco.Core/Collections/OrderedHashSet.cs +++ b/src/Umbraco.Core/Collections/OrderedHashSet.cs @@ -1,50 +1,46 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// A custom collection similar to HashSet{T} which only contains unique items, however this collection keeps items in +/// order +/// and is customizable to keep the newest or oldest equatable item +/// +/// +public class OrderedHashSet : KeyedCollection + where T : notnull { - /// - /// A custom collection similar to HashSet{T} which only contains unique items, however this collection keeps items in order - /// and is customizable to keep the newest or oldest equatable item - /// - /// - public class OrderedHashSet : KeyedCollection where T : notnull + private readonly bool _keepOldest; + + public OrderedHashSet(bool keepOldest = true) => _keepOldest = keepOldest; + + protected override void InsertItem(int index, T item) { - private readonly bool _keepOldest; - - public OrderedHashSet(bool keepOldest = true) + if (Dictionary == null) { - _keepOldest = keepOldest; + base.InsertItem(index, item); } - - protected override void InsertItem(int index, T item) + else { - if (Dictionary == null) + var exists = Dictionary.ContainsKey(item); + + // if we want to keep the newest, then we need to remove the old item and add the new one + if (exists == false) { base.InsertItem(index, item); } - else + else if (_keepOldest == false) { - var exists = Dictionary.ContainsKey(item); + if (Remove(item)) + { + index--; + } - //if we want to keep the newest, then we need to remove the old item and add the new one - if (exists == false) - { - base.InsertItem(index, item); - } - else if(_keepOldest == false) - { - if (Remove(item)) - { - index--; - } - base.InsertItem(index, item); - } + base.InsertItem(index, item); } } - - protected override T GetKeyForItem(T item) - { - return item; - } } + + protected override T GetKeyForItem(T item) => item; } diff --git a/src/Umbraco.Core/Collections/StackQueue.cs b/src/Umbraco.Core/Collections/StackQueue.cs index 242766771d..2324eec892 100644 --- a/src/Umbraco.Core/Collections/StackQueue.cs +++ b/src/Umbraco.Core/Collections/StackQueue.cs @@ -1,45 +1,46 @@ -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// Collection that can be both a queue and a stack. +/// +/// +public class StackQueue { - /// - /// Collection that can be both a queue and a stack. - /// - /// - public class StackQueue + private readonly LinkedList _linkedList = new(); + + public int Count => _linkedList.Count; + + public void Clear() => _linkedList.Clear(); + + public void Push(T? obj) => _linkedList.AddFirst(obj); + + public void Enqueue(T? obj) => _linkedList.AddFirst(obj); + + public T Pop() { - private readonly LinkedList _linkedList = new (); - - public int Count => _linkedList.Count; - - public void Clear() => _linkedList.Clear(); - - public void Push(T? obj) => _linkedList.AddFirst(obj); - - public void Enqueue(T? obj) => _linkedList.AddFirst(obj); - - public T Pop() + var obj = default(T); + if (_linkedList.First is not null) { - T? obj = default(T); - if (_linkedList.First is not null) - { - obj = _linkedList.First.Value; - } - _linkedList.RemoveFirst(); - return obj!; + obj = _linkedList.First.Value; } - public T Dequeue() - { - T? obj = default(T); - if (_linkedList.Last is not null) - { - obj = _linkedList.Last.Value; - } - _linkedList.RemoveLast(); - return obj!; - } - - public T? PeekStack() => _linkedList.First is not null ? _linkedList.First.Value : default; - - public T? PeekQueue() => _linkedList.Last is not null ? _linkedList.Last.Value : default; + _linkedList.RemoveFirst(); + return obj!; } + + public T Dequeue() + { + var obj = default(T); + if (_linkedList.Last is not null) + { + obj = _linkedList.Last.Value; + } + + _linkedList.RemoveLast(); + return obj!; + } + + public T? PeekStack() => _linkedList.First is not null ? _linkedList.First.Value : default; + + public T? PeekQueue() => _linkedList.Last is not null ? _linkedList.Last.Value : default; } diff --git a/src/Umbraco.Core/Collections/TopoGraph.cs b/src/Umbraco.Core/Collections/TopoGraph.cs index 11fd155684..fd2161c6d3 100644 --- a/src/Umbraco.Core/Collections/TopoGraph.cs +++ b/src/Umbraco.Core/Collections/TopoGraph.cs @@ -1,133 +1,143 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +public class TopoGraph { - public class TopoGraph + internal const string CycleDependencyError = "Cyclic dependency."; + internal const string MissingDependencyError = "Missing dependency."; + + public static Node CreateNode(TKey key, TItem item, IEnumerable dependencies) => + new(key, item, dependencies); + + public class Node { - internal const string CycleDependencyError = "Cyclic dependency."; - internal const string MissingDependencyError = "Missing dependency."; - - public class Node + public Node(TKey key, TItem item, IEnumerable dependencies) { - public Node(TKey key, TItem item, IEnumerable dependencies) - { - Key = key; - Item = item; - Dependencies = dependencies; - } - - public TKey Key { get; } - public TItem Item { get; } - public IEnumerable Dependencies { get; } + Key = key; + Item = item; + Dependencies = dependencies; } - public static Node CreateNode(TKey key, TItem item, IEnumerable dependencies) - => new Node(key, item, dependencies); + public TKey Key { get; } + + public TItem Item { get; } + + public IEnumerable Dependencies { get; } + } +} + +/// +/// Represents a generic DAG that can be topologically sorted. +/// +/// The type of the keys. +/// The type of the items. +public class TopoGraph : TopoGraph + where TKey : notnull +{ + private readonly Func?> _getDependencies; + private readonly Func _getKey; + private readonly Dictionary _items = new(); + + /// + /// Initializes a new instance of the class. + /// + /// A method that returns the key of an item. + /// A method that returns the dependency keys of an item. + public TopoGraph(Func getKey, Func?> getDependencies) + { + _getKey = getKey; + _getDependencies = getDependencies; } /// - /// Represents a generic DAG that can be topologically sorted. + /// Adds an item to the graph. /// - /// The type of the keys. - /// The type of the items. - public class TopoGraph : TopoGraph - where TKey : notnull + /// The item. + public void AddItem(TItem item) { - private readonly Func _getKey; - private readonly Func?> _getDependencies; - private readonly Dictionary _items = new Dictionary(); + TKey key = _getKey(item); + _items[key] = item; + } - /// - /// Initializes a new instance of the class. - /// - /// A method that returns the key of an item. - /// A method that returns the dependency keys of an item. - public TopoGraph(Func getKey, Func?> getDependencies) + /// + /// Adds items to the graph. + /// + /// The items. + public void AddItems(IEnumerable items) + { + foreach (TItem item in items) { - _getKey = getKey; - _getDependencies = getDependencies; + AddItem(item); + } + } + + /// + /// Gets the sorted items. + /// + /// A value indicating whether to throw on cycles, or just ignore the branch. + /// A value indicating whether to throw on missing dependency, or just ignore the dependency. + /// A value indicating whether to reverse the order. + /// The (topologically) sorted items. + public IEnumerable GetSortedItems(bool throwOnCycle = true, bool throwOnMissing = true, bool reverse = false) + { + var sorted = new TItem[_items.Count]; + var visited = new HashSet(); + var index = reverse ? _items.Count - 1 : 0; + var incr = reverse ? -1 : +1; + + foreach (TItem item in _items.Values) + { + Visit(item, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); } - /// - /// Adds an item to the graph. - /// - /// The item. - public void AddItem(TItem item) + return sorted; + } + + private static bool Contains(TItem[] items, TItem item, int start, int count) => + Array.IndexOf(items, item, start, count) >= 0; + + private void Visit(TItem item, ISet visited, TItem[] sorted, ref int index, int incr, bool throwOnCycle, bool throwOnMissing) + { + if (visited.Contains(item)) { - var key = _getKey(item); - _items[key] = item; - } - - /// - /// Adds items to the graph. - /// - /// The items. - public void AddItems(IEnumerable items) - { - foreach (var item in items) - AddItem(item); - } - - /// - /// Gets the sorted items. - /// - /// A value indicating whether to throw on cycles, or just ignore the branch. - /// A value indicating whether to throw on missing dependency, or just ignore the dependency. - /// A value indicating whether to reverse the order. - /// The (topologically) sorted items. - public IEnumerable GetSortedItems(bool throwOnCycle = true, bool throwOnMissing = true, bool reverse = false) - { - var sorted = new TItem[_items.Count]; - var visited = new HashSet(); - var index = reverse ? _items.Count - 1 : 0; - var incr = reverse ? -1 : +1; - - foreach (var item in _items.Values) - Visit(item, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); - - return sorted; - } - - private static bool Contains(TItem[] items, TItem item, int start, int count) - { - return Array.IndexOf(items, item, start, count) >= 0; - } - - private void Visit(TItem item, ISet visited, TItem[] sorted, ref int index, int incr, bool throwOnCycle, bool throwOnMissing) - { - if (visited.Contains(item)) + // visited but not sorted yet = cycle + var start = incr > 0 ? 0 : index; + var count = incr > 0 ? index : sorted.Length - index; + if (throwOnCycle && Contains(sorted, item, start, count) == false) { - // visited but not sorted yet = cycle - var start = incr > 0 ? 0 : index; - var count = incr > 0 ? index : sorted.Length - index; - if (throwOnCycle && Contains(sorted, item, start, count) == false) - throw new Exception(CycleDependencyError +": " + item); - return; + throw new Exception(CycleDependencyError + ": " + item); } - visited.Add(item); - - var keys = _getDependencies(item); - var dependencies = keys == null ? null : FindDependencies(keys, throwOnMissing); - - if (dependencies != null) - foreach (var dep in dependencies) - Visit(dep, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); - - sorted[index] = item; - index += incr; + return; } - private IEnumerable FindDependencies(IEnumerable keys, bool throwOnMissing) + visited.Add(item); + + IEnumerable? keys = _getDependencies(item); + IEnumerable? dependencies = keys == null ? null : FindDependencies(keys, throwOnMissing); + + if (dependencies != null) { - foreach (var key in keys) + foreach (TItem dep in dependencies) { - TItem? value; - if (_items.TryGetValue(key, out value)) - yield return value; - else if (throwOnMissing) - throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}"); + Visit(dep, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); + } + } + + sorted[index] = item; + index += incr; + } + + private IEnumerable FindDependencies(IEnumerable keys, bool throwOnMissing) + { + foreach (TKey key in keys) + { + if (_items.TryGetValue(key, out TItem? value)) + { + yield return value; + } + else if (throwOnMissing) + { + throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}"); } } } diff --git a/src/Umbraco.Core/Collections/TypeList.cs b/src/Umbraco.Core/Collections/TypeList.cs index 96565a843c..ab51dd56b2 100644 --- a/src/Umbraco.Core/Collections/TypeList.cs +++ b/src/Umbraco.Core/Collections/TypeList.cs @@ -1,33 +1,24 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a list of types. +/// +/// Types in the list are, or derive from, or implement, the base type. +/// The base type. +public class TypeList { + private readonly List _list = new(); + /// - /// Represents a list of types. + /// Adds a type to the list. /// - /// Types in the list are, or derive from, or implement, the base type. - /// The base type. - public class TypeList - { - private readonly List _list = new List(); + /// The type to add. + public void Add() + where T : TBase => + _list.Add(typeof(T)); - /// - /// Adds a type to the list. - /// - /// The type to add. - public void Add() - where T : TBase - { - _list.Add(typeof(T)); - } - - /// - /// Determines whether a type is in the list. - /// - public bool Contains(Type type) - { - return _list.Contains(type); - } - } + /// + /// Determines whether a type is in the list. + /// + public bool Contains(Type type) => _list.Contains(type); } diff --git a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs index 1af9511fb7..ffacd89cff 100644 --- a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs +++ b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs @@ -1,34 +1,32 @@ -using System; using System.Collections; -using System.Collections.Generic; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for builder collections. +/// +/// The type of the items. +public abstract class BuilderCollectionBase : IBuilderCollection { + private readonly LazyReadOnlyCollection _items; + + /// Initializes a new instance of the + /// + /// with items. + /// + /// The items. + public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items); + + /// + public int Count => _items.Count; /// - /// Provides a base class for builder collections. + /// Gets an enumerator. /// - /// The type of the items. - public abstract class BuilderCollectionBase : IBuilderCollection - { - private readonly LazyReadOnlyCollection _items; + public IEnumerator GetEnumerator() => _items.GetEnumerator(); - /// Initializes a new instance of the with items. - /// - /// The items. - public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items); - - /// - public int Count => _items.Count; - - /// - /// Gets an enumerator. - /// - public IEnumerator GetEnumerator() => ((IEnumerable)_items).GetEnumerator(); - - /// - /// Gets an enumerator. - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } + /// + /// Gets an enumerator. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs index 8b5913ab1d..8b1c33a610 100644 --- a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs @@ -1,160 +1,174 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for collection builders. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +public abstract class CollectionBuilderBase : ICollectionBuilder + where TBuilder : CollectionBuilderBase + where TCollection : class, IBuilderCollection { + private readonly object _locker = new(); + private readonly List _types = new(); + private Type[]? _registeredTypes; + /// - /// Provides a base class for collection builders. + /// Gets the collection lifetime. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - public abstract class CollectionBuilderBase : ICollectionBuilder - where TBuilder : CollectionBuilderBase - where TCollection : class, IBuilderCollection + protected virtual ServiceLifetime CollectionLifetime => ServiceLifetime.Singleton; + + /// + public virtual void RegisterWith(IServiceCollection services) { - private readonly List _types = new List(); - private readonly object _locker = new object(); - private Type[]? _registeredTypes; + if (_registeredTypes != null) + { + throw new InvalidOperationException("This builder has already been registered."); + } - /// - /// Gets the internal list of types as an IEnumerable (immutable). - /// - public IEnumerable GetTypes() => _types; + // register the collection + services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, CollectionLifetime)); - /// - public virtual void RegisterWith(IServiceCollection services) + // register the types + RegisterTypes(services); + } + + /// + /// Creates a collection. + /// + /// A collection. + /// Creates a new collection each time it is invoked. + public virtual TCollection CreateCollection(IServiceProvider factory) + => factory.CreateInstance(CreateItemsFactory(factory)); + + /// + /// Gets the internal list of types as an IEnumerable (immutable). + /// + public IEnumerable GetTypes() => _types; + + /// + /// Gets a value indicating whether the collection contains a type. + /// + /// The type to look for. + /// A value indicating whether the collection contains the type. + /// + /// Some builder implementations may use this to expose a public Has{T}() method, + /// when it makes sense. Probably does not make sense for lazy builders, for example. + /// + public virtual bool Has() + where T : TItem => + _types.Contains(typeof(T)); + + /// + /// Gets a value indicating whether the collection contains a type. + /// + /// The type to look for. + /// A value indicating whether the collection contains the type. + /// + /// Some builder implementations may use this to expose a public Has{T}() method, + /// when it makes sense. Probably does not make sense for lazy builders, for example. + /// + public virtual bool Has(Type type) + { + EnsureType(type, "find"); + return _types.Contains(type); + } + + /// + /// Configures the internal list of types. + /// + /// The action to execute. + /// Throws if the types have already been registered. + protected void Configure(Action> action) + { + lock (_locker) { if (_registeredTypes != null) - throw new InvalidOperationException("This builder has already been registered."); - - // register the collection - services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, CollectionLifetime)); - - // register the types - RegisterTypes(services); - } - - /// - /// Gets the collection lifetime. - /// - protected virtual ServiceLifetime CollectionLifetime => ServiceLifetime.Singleton; - - /// - /// Configures the internal list of types. - /// - /// The action to execute. - /// Throws if the types have already been registered. - protected void Configure(Action> action) - { - lock (_locker) { - if (_registeredTypes != null) - throw new InvalidOperationException("Cannot configure a collection builder after it has been registered."); - action(_types); + throw new InvalidOperationException( + "Cannot configure a collection builder after it has been registered."); } - } - /// - /// Gets the types. - /// - /// The internal list of types. - /// The list of types to register. - /// Used by implementations to add types to the internal list, sort the list, etc. - protected virtual IEnumerable GetRegisteringTypes(IEnumerable types) - { - return types; - } - - private void RegisterTypes(IServiceCollection services) - { - lock (_locker) - { - if (_registeredTypes != null) - return; - - var types = GetRegisteringTypes(_types).ToArray(); - - // ensure they are safe - foreach (var type in types) - EnsureType(type, "register"); - - // register them - ensuring that each item is registered with the same lifetime as the collection. - // NOTE: Previously each one was not registered with the same lifetime which would mean that if there - // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what - // we would expect to happen. The same item should be resolved from the container as the collection. - foreach (var type in types) - services.Add(new ServiceDescriptor(type, type, CollectionLifetime)); - - _registeredTypes = types; - } - } - - /// - /// Creates the collection items. - /// - /// The collection items. - protected virtual IEnumerable CreateItems(IServiceProvider factory) - { - if (_registeredTypes == null) - throw new InvalidOperationException("Cannot create items before the collection builder has been registered."); - - return _registeredTypes // respect order - .Select(x => CreateItem(factory, x)) - .ToArray(); // safe - } - - /// - /// Creates a collection item. - /// - protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) - => (TItem)factory.GetRequiredService(itemType); - - /// - /// Creates a collection. - /// - /// A collection. - /// Creates a new collection each time it is invoked. - public virtual TCollection CreateCollection(IServiceProvider factory) - => factory.CreateInstance(CreateItemsFactory(factory)); - - // used to resolve a Func> parameter - private Func> CreateItemsFactory(IServiceProvider factory) => () => CreateItems(factory); - - protected Type EnsureType(Type type, string action) - { - if (typeof(TItem).IsAssignableFrom(type) == false) - throw new InvalidOperationException($"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TItem).FullName}."); - return type; - } - - /// - /// Gets a value indicating whether the collection contains a type. - /// - /// The type to look for. - /// A value indicating whether the collection contains the type. - /// Some builder implementations may use this to expose a public Has{T}() method, - /// when it makes sense. Probably does not make sense for lazy builders, for example. - public virtual bool Has() - where T : TItem - { - return _types.Contains(typeof(T)); - } - - /// - /// Gets a value indicating whether the collection contains a type. - /// - /// The type to look for. - /// A value indicating whether the collection contains the type. - /// Some builder implementations may use this to expose a public Has{T}() method, - /// when it makes sense. Probably does not make sense for lazy builders, for example. - public virtual bool Has(Type type) - { - EnsureType(type, "find"); - return _types.Contains(type); + action(_types); } } + + /// + /// Gets the types. + /// + /// The internal list of types. + /// The list of types to register. + /// Used by implementations to add types to the internal list, sort the list, etc. + protected virtual IEnumerable GetRegisteringTypes(IEnumerable types) => types; + + /// + /// Creates the collection items. + /// + /// The collection items. + protected virtual IEnumerable CreateItems(IServiceProvider factory) + { + if (_registeredTypes == null) + { + throw new InvalidOperationException( + "Cannot create items before the collection builder has been registered."); + } + + return _registeredTypes // respect order + .Select(x => CreateItem(factory, x)) + .ToArray(); // safe + } + + /// + /// Creates a collection item. + /// + protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) + => (TItem)factory.GetRequiredService(itemType); + + protected Type EnsureType(Type type, string action) + { + if (typeof(TItem).IsAssignableFrom(type) == false) + { + throw new InvalidOperationException( + $"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TItem).FullName}."); + } + + return type; + } + + private void RegisterTypes(IServiceCollection services) + { + lock (_locker) + { + if (_registeredTypes != null) + { + return; + } + + Type[] types = GetRegisteringTypes(_types).ToArray(); + + // ensure they are safe + foreach (Type type in types) + { + EnsureType(type, "register"); + } + + // register them - ensuring that each item is registered with the same lifetime as the collection. + // NOTE: Previously each one was not registered with the same lifetime which would mean that if there + // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what + // we would expect to happen. The same item should be resolved from the container as the collection. + foreach (Type type in types) + { + services.Add(new ServiceDescriptor(type, type, CollectionLifetime)); + } + + _registeredTypes = types; + } + } + + // used to resolve a Func> parameter + private Func> CreateItemsFactory(IServiceProvider factory) => () => CreateItems(factory); } diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index c39dd503e0..d64de626d0 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -1,62 +1,67 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents the collection of implementations. +/// +public class ComponentCollection : BuilderCollectionBase { - /// - /// Represents the collection of implementations. - /// - public class ComponentCollection : BuilderCollectionBase + private const int LogThresholdMilliseconds = 100; + private readonly ILogger _logger; + + private readonly IProfilingLogger _profilingLogger; + + public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) + : base(items) { - private const int LogThresholdMilliseconds = 100; + _profilingLogger = profilingLogger; + _logger = logger; + } - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - - public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) - : base(items) + public void Initialize() + { + using (_profilingLogger.DebugDuration( + $"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) { - _profilingLogger = profilingLogger; - _logger = logger; - } - - public void Initialize() - { - using (_profilingLogger.DebugDuration($"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) + foreach (IComponent component in this) { - foreach (var component in this) + Type componentType = component.GetType(); + using (_profilingLogger.DebugDuration( + $"Initializing {componentType.FullName}.", + $"Initialized {componentType.FullName}.", + thresholdMilliseconds: LogThresholdMilliseconds)) { - var componentType = component.GetType(); - using (_profilingLogger.DebugDuration($"Initializing {componentType.FullName}.", $"Initialized {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - component.Initialize(); - } + component.Initialize(); } } } + } - public void Terminate() + public void Terminate() + { + using (_profilingLogger.DebugDuration( + $"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) { - using (_profilingLogger.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) + // terminate components in reverse order + foreach (IComponent component in this.Reverse()) { - foreach (var component in this.Reverse()) // terminate components in reverse order + Type componentType = component.GetType(); + using (_profilingLogger.DebugDuration( + $"Terminating {componentType.FullName}.", + $"Terminated {componentType.FullName}.", + thresholdMilliseconds: LogThresholdMilliseconds)) { - var componentType = component.GetType(); - using (_profilingLogger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) + try { - try - { - component.Terminate(); - component.DisposeIfDisposable(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while terminating component {ComponentType}.", componentType.FullName); - } + component.Terminate(); + component.DisposeIfDisposable(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while terminating component {ComponentType}.", componentType.FullName); } } } diff --git a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs index 1e36c4e8e9..b77dfde819 100644 --- a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs +++ b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs @@ -1,40 +1,40 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Builds a . +/// +public class + ComponentCollectionBuilder : OrderedCollectionBuilderBase { - /// - /// Builds a . - /// - public class ComponentCollectionBuilder : OrderedCollectionBuilderBase + private const int LogThresholdMilliseconds = 100; + + protected override ComponentCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) { - private const int LogThresholdMilliseconds = 100; + IProfilingLogger logger = factory.GetRequiredService(); - public ComponentCollectionBuilder() - { } - - protected override ComponentCollectionBuilder This => this; - - protected override IEnumerable CreateItems(IServiceProvider factory) + using (logger.DebugDuration( + $"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) { - var logger = factory.GetRequiredService(); - - using (logger.DebugDuration($"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) - { - return base.CreateItems(factory); - } + return base.CreateItems(factory); } + } - protected override IComponent CreateItem(IServiceProvider factory, Type itemType) + protected override IComponent CreateItem(IServiceProvider factory, Type itemType) + { + IProfilingLogger logger = factory.GetRequiredService(); + + using (logger.DebugDuration( + $"Creating {itemType.FullName}.", + $"Created {itemType.FullName}.", + thresholdMilliseconds: LogThresholdMilliseconds)) { - var logger = factory.GetRequiredService(); - - using (logger.DebugDuration($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - return base.CreateItem(factory, itemType); - } + return base.CreateItem(factory, itemType); } } } diff --git a/src/Umbraco.Core/Composing/ComponentComposer.cs b/src/Umbraco.Core/Composing/ComponentComposer.cs index c1d921df03..2a9641e64b 100644 --- a/src/Umbraco.Core/Composing/ComponentComposer.cs +++ b/src/Umbraco.Core/Composing/ComponentComposer.cs @@ -1,22 +1,18 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for composers which compose a component. +/// +/// The type of the component +public abstract class ComponentComposer : IComposer + where TComponent : IComponent { - /// - /// Provides a base class for composers which compose a component. - /// - /// The type of the component - public abstract class ComponentComposer : IComposer - where TComponent : IComponent - { - /// - public virtual void Compose(IUmbracoBuilder builder) - { - builder.Components().Append(); - } + /// + public virtual void Compose(IUmbracoBuilder builder) => builder.Components().Append(); - // note: thanks to this class, a component that does not compose anything can be - // registered with one line: - // public class MyComponentComposer : ComponentComposer { } - } + // note: thanks to this class, a component that does not compose anything can be + // registered with one line: + // public class MyComponentComposer : ComponentComposer { } } diff --git a/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs b/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs index c12ddbcd3e..bd3567595d 100644 --- a/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs +++ b/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs @@ -1,59 +1,66 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer requires another composer. +/// +/// +/// +/// This attribute is *not* inherited. This means that a composer class inheriting from +/// another composer class does *not* inherit its requirements. However, the runtime checks +/// the *interfaces* of every composer for their requirements, so requirements declared on +/// interfaces are inherited by every composer class implementing the interface. +/// +/// +/// When targeting a class, indicates a dependency on the composer which must be enabled, +/// unless the requirement has explicitly been declared as weak (and then, only if the composer +/// is enabled). +/// +/// +/// When targeting an interface, indicates a dependency on enabled composers implementing +/// the interface. It could be no composer at all, unless the requirement has explicitly been +/// declared as strong (and at least one composer must be enabled). +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] +public sealed class ComposeAfterAttribute : Attribute { /// - /// Indicates that a composer requires another composer. + /// Initializes a new instance of the class. + /// + /// The type of the required composer. + public ComposeAfterAttribute(Type requiredType) + { + if (typeof(IComposer).IsAssignableFrom(requiredType) == false) + { + throw new ArgumentException( + $"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); + } + + RequiredType = requiredType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The type of the required composer. + /// A value indicating whether the requirement is weak. + public ComposeAfterAttribute(Type requiredType, bool weak) + : this(requiredType) => + Weak = weak; + + /// + /// Gets the required type. + /// + public Type RequiredType { get; } + + /// + /// Gets a value indicating whether the requirement is weak. /// /// - /// This attribute is *not* inherited. This means that a composer class inheriting from - /// another composer class does *not* inherit its requirements. However, the runtime checks - /// the *interfaces* of every composer for their requirements, so requirements declared on - /// interfaces are inherited by every composer class implementing the interface. - /// When targeting a class, indicates a dependency on the composer which must be enabled, - /// unless the requirement has explicitly been declared as weak (and then, only if the composer - /// is enabled). - /// When targeting an interface, indicates a dependency on enabled composers implementing - /// the interface. It could be no composer at all, unless the requirement has explicitly been - /// declared as strong (and at least one composer must be enabled). + /// Returns true if the requirement is weak (requires the other composer if it + /// is enabled), false if the requirement is strong (requires the other composer to be + /// enabled), and null if unspecified, in which case it is strong for classes and weak for + /// interfaces. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] - public sealed class ComposeAfterAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The type of the required composer. - public ComposeAfterAttribute(Type requiredType) - { - if (typeof(IComposer).IsAssignableFrom(requiredType) == false) - throw new ArgumentException($"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); - RequiredType = requiredType; - } - - /// - /// Initializes a new instance of the class. - /// - /// The type of the required composer. - /// A value indicating whether the requirement is weak. - public ComposeAfterAttribute(Type requiredType, bool weak) - : this(requiredType) - { - Weak = weak; - } - - /// - /// Gets the required type. - /// - public Type RequiredType { get; } - - /// - /// Gets a value indicating whether the requirement is weak. - /// - /// Returns true if the requirement is weak (requires the other composer if it - /// is enabled), false if the requirement is strong (requires the other composer to be - /// enabled), and null if unspecified, in which case it is strong for classes and weak for - /// interfaces. - public bool? Weak { get; } - } + public bool? Weak { get; } } diff --git a/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs b/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs index 382772de8d..c41f1e5074 100644 --- a/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs +++ b/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs @@ -1,40 +1,46 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a component is required by another composer. +/// +/// +/// +/// This attribute is *not* inherited. This means that a composer class inheriting from +/// another composer class does *not* inherit its requirements. However, the runtime checks +/// the *interfaces* of every composer for their requirements, so requirements declared on +/// interfaces are inherited by every composer class implementing the interface. +/// +/// +/// When targeting a class, indicates a dependency on the composer which must be enabled, +/// unless the requirement has explicitly been declared as weak (and then, only if the composer +/// is enabled). +/// +/// +/// When targeting an interface, indicates a dependency on enabled composers implementing +/// the interface. It could be no composer at all, unless the requirement has explicitly been +/// declared as strong (and at least one composer must be enabled). +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] +public sealed class ComposeBeforeAttribute : Attribute { /// - /// Indicates that a component is required by another composer. + /// Initializes a new instance of the class. /// - /// - /// This attribute is *not* inherited. This means that a composer class inheriting from - /// another composer class does *not* inherit its requirements. However, the runtime checks - /// the *interfaces* of every composer for their requirements, so requirements declared on - /// interfaces are inherited by every composer class implementing the interface. - /// When targeting a class, indicates a dependency on the composer which must be enabled, - /// unless the requirement has explicitly been declared as weak (and then, only if the composer - /// is enabled). - /// When targeting an interface, indicates a dependency on enabled composers implementing - /// the interface. It could be no composer at all, unless the requirement has explicitly been - /// declared as strong (and at least one composer must be enabled). - /// - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] - public sealed class ComposeBeforeAttribute : Attribute + /// The type of the required composer. + public ComposeBeforeAttribute(Type requiringType) { - /// - /// Initializes a new instance of the class. - /// - /// The type of the required composer. - public ComposeBeforeAttribute(Type requiringType) + if (typeof(IComposer).IsAssignableFrom(requiringType) == false) { - if (typeof(IComposer).IsAssignableFrom(requiringType) == false) - throw new ArgumentException($"Type {requiringType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); - RequiringType = requiringType; + throw new ArgumentException( + $"Type {requiringType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); } - /// - /// Gets the required type. - /// - public Type RequiringType { get; } + RequiringType = requiringType; } + + /// + /// Gets the required type. + /// + public Type RequiringType { get; } } diff --git a/src/Umbraco.Core/Composing/ComposerGraph.cs b/src/Umbraco.Core/Composing/ComposerGraph.cs index 510d59b374..3c602b0ad9 100644 --- a/src/Umbraco.Core/Composing/ComposerGraph.cs +++ b/src/Umbraco.Core/Composing/ComposerGraph.cs @@ -1,351 +1,421 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +// note: this class is NOT thread-safe in any way + +/// +/// Handles the composers. +/// +internal class ComposerGraph { - // note: this class is NOT thread-safe in any way + private readonly IUmbracoBuilder _builder; + private readonly IEnumerable _composerTypes; + private readonly IEnumerable _enableDisableAttributes; + private readonly ILogger _logger; /// - /// Handles the composers. + /// Initializes a new instance of the class. /// - internal class ComposerGraph + /// The composition. + /// The types. + /// + /// The and/or + /// attributes. + /// + /// The logger. + /// + /// composition + /// or + /// composerTypes + /// or + /// enableDisableAttributes + /// or + /// logger + /// + public ComposerGraph(IUmbracoBuilder builder, IEnumerable composerTypes, IEnumerable enableDisableAttributes, ILogger logger) { - private readonly IUmbracoBuilder _builder; - private readonly ILogger _logger; - private readonly IEnumerable _composerTypes; - private readonly IEnumerable _enableDisableAttributes; + _builder = builder ?? throw new ArgumentNullException(nameof(builder)); + _composerTypes = composerTypes ?? throw new ArgumentNullException(nameof(composerTypes)); + _enableDisableAttributes = + enableDisableAttributes ?? throw new ArgumentNullException(nameof(enableDisableAttributes)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } - /// - /// Initializes a new instance of the class. - /// - /// The composition. - /// The types. - /// The and/or attributes. - /// The logger. - /// composition - /// or - /// composerTypes - /// or - /// enableDisableAttributes - /// or - /// logger - public ComposerGraph(IUmbracoBuilder builder, IEnumerable composerTypes, IEnumerable enableDisableAttributes, ILogger logger) + /// + /// Instantiates and composes the composers. + /// + public void Compose() + { + // make sure it is there + _builder.WithCollectionBuilder(); + + IEnumerable orderedComposerTypes = PrepareComposerTypes(); + + foreach (IComposer composer in InstantiateComposers(orderedComposerTypes)) { - _builder = builder ?? throw new ArgumentNullException(nameof(builder)); - _composerTypes = composerTypes ?? throw new ArgumentNullException(nameof(composerTypes)); - _enableDisableAttributes = enableDisableAttributes ?? throw new ArgumentNullException(nameof(enableDisableAttributes)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + composer.Compose(_builder); + } + } + + internal static string GetComposersReport(Dictionary?> requirements) + { + var text = new StringBuilder(); + text.AppendLine("Composers & Dependencies:"); + text.AppendLine(" < compose before"); + text.AppendLine(" > compose after"); + text.AppendLine(" : implements"); + text.AppendLine(" = depends"); + text.AppendLine(); + + bool HasReq(IEnumerable types, Type type) + { + return types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); } - private class EnableInfo + foreach (KeyValuePair?> kvp in requirements) { - public bool Enabled { get; set; } - public int Weight { get; set; } = -1; - } + Type type = kvp.Key; - /// - /// Instantiates and composes the composers. - /// - public void Compose() - { - // make sure it is there - _builder.WithCollectionBuilder(); - - IEnumerable orderedComposerTypes = PrepareComposerTypes(); - - foreach (IComposer composer in InstantiateComposers(orderedComposerTypes)) + text.AppendLine(type.FullName); + foreach (ComposeAfterAttribute attribute in type.GetCustomAttributes()) { - composer.Compose(_builder); + var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); + text.AppendLine(" > " + attribute.RequiredType + + (weak ? " (weak" : " (strong") + + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); } - } - internal IEnumerable PrepareComposerTypes() - { - var requirements = GetRequirements(); - - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); - - var sortedComposerTypes = SortComposers(requirements); - - // bit verbose but should help for troubleshooting - //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; - _logger.LogDebug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); - - return sortedComposerTypes; - } - - internal Dictionary?> GetRequirements(bool throwOnMissing = true) - { - // create a list, remove those that cannot be enabled due to runtime level - var composerTypeList = _composerTypes.ToList(); - - // enable or disable composers - EnableDisableComposers(_enableDisableAttributes, composerTypeList); - - void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) - where TAttribute : Attribute + foreach (ComposeBeforeAttribute attribute in type.GetCustomAttributes()) { - foreach (var attribute in type.GetCustomAttributes()) - { - var typeInAttribute = getTypeInAttribute(attribute); - if (typeInAttribute != null && // if the attribute references a type ... - typeInAttribute.IsInterface && // ... which is an interface ... - typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... - !iset.Contains(typeInAttribute)) // ... which is not already in the list - { - // add it to the new list - iset.Add(typeInAttribute); - set2.Add(typeInAttribute); + text.AppendLine(" < " + attribute.RequiringType); + } - // add all its interfaces implementing IComposer - foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) - { - iset.Add(i); - set2.Add(i); - } - } + foreach (Type i in type.GetInterfaces()) + { + text.AppendLine(" : " + i.FullName); + } + + if (kvp.Value != null) + { + foreach (Type t in kvp.Value) + { + text.AppendLine(" = " + t); } } - // gather interfaces too - var interfaces = new HashSet(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); - composerTypeList.AddRange(interfaces); - var list1 = composerTypeList; - while (list1.Count > 0) - { - var list2 = new List(); - foreach (var t in list1) - { - GatherInterfaces(t, a => a.RequiredType, interfaces, list2); - GatherInterfaces(t, a => a.RequiringType, interfaces, list2); - } - composerTypeList.AddRange(list2); - list1 = list2; - } - - // sort the composers according to their dependencies - var requirements = new Dictionary?>(); - foreach (var type in composerTypeList) - requirements[type] = null; - foreach (var type in composerTypeList) - { - GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); - GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); - } - - return requirements; - } - - internal IEnumerable SortComposers(Dictionary?> requirements) - { - // sort composers - var graph = new TopoGraph?>>(kvp => kvp.Key, kvp => kvp.Value); - graph.AddItems(requirements); - List sortedComposerTypes; - try - { - sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); - } - catch (Exception e) - { - // in case of an error, force-dump everything to log - _logger.LogInformation("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements)); - _logger.LogError(e, "Failed to sort composers."); - throw; - } - - return sortedComposerTypes; - } - - internal static string GetComposersReport(Dictionary?> requirements) - { - var text = new StringBuilder(); - text.AppendLine("Composers & Dependencies:"); - text.AppendLine(" < compose before"); - text.AppendLine(" > compose after"); - text.AppendLine(" : implements"); - text.AppendLine(" = depends"); text.AppendLine(); - - bool HasReq(IEnumerable types, Type type) - => types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); - - foreach (var kvp in requirements) - { - var type = kvp.Key; - - text.AppendLine(type.FullName); - foreach (var attribute in type.GetCustomAttributes()) - { - var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); - text.AppendLine(" > " + attribute.RequiredType + - (weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); - } - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" < " + attribute.RequiringType); - foreach (var i in type.GetInterfaces()) - text.AppendLine(" : " + i.FullName); - if (kvp.Value != null) - foreach (var t in kvp.Value) - text.AppendLine(" = " + t); - text.AppendLine(); - } - text.AppendLine("/"); - text.AppendLine(); - return text.ToString(); } - private static void EnableDisableComposers(IEnumerable enableDisableAttributes, ICollection types) + text.AppendLine("/"); + text.AppendLine(); + return text.ToString(); + } + + internal IEnumerable PrepareComposerTypes() + { + Dictionary?> requirements = GetRequirements(); + + // only for debugging, this is verbose + // _logger.Debug(GetComposersReport(requirements)); + IEnumerable sortedComposerTypes = SortComposers(requirements); + + // bit verbose but should help for troubleshooting + // var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; + _logger.LogDebug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + + return sortedComposerTypes; + } + + internal Dictionary?> GetRequirements(bool throwOnMissing = true) + { + // create a list, remove those that cannot be enabled due to runtime level + var composerTypeList = _composerTypes.ToList(); + + // enable or disable composers + EnableDisableComposers(_enableDisableAttributes, composerTypeList); + + static void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute { - var enabled = new Dictionary(); - - // process the enable/disable attributes - // these two attributes are *not* inherited and apply to *classes* only (not interfaces). - // remote declarations (when a composer enables/disables *another* composer) - // have priority over local declarations (when a composer disables itself) so that - // ppl can enable composers that, by default, are disabled. - // what happens in case of conflicting remote declarations is unspecified. more - // precisely, the last declaration to be processed wins, but the order of the - // declarations depends on the type finder and is unspecified. - - void UpdateEnableInfo(Type composerType, int weight2, Dictionary enabled2, bool value) + foreach (TAttribute attribute in type.GetCustomAttributes()) { - if (enabled.TryGetValue(composerType, out var enableInfo) == false) enableInfo = enabled2[composerType] = new EnableInfo(); - if (enableInfo.Weight > weight2) return; - - enableInfo.Enabled = value; - enableInfo.Weight = weight2; - } - - foreach (var attr in enableDisableAttributes.OfType()) - { - var type = attr.EnabledType; - UpdateEnableInfo(type, 2, enabled, true); - } - - foreach (var attr in enableDisableAttributes.OfType()) - { - var type = attr.DisabledType; - UpdateEnableInfo(type, 2, enabled, false); - } - - foreach (var composerType in types) - { - foreach (var attr in composerType.GetCustomAttributes()) + Type typeInAttribute = getTypeInAttribute(attribute); + if (typeInAttribute != null && // if the attribute references a type ... + typeInAttribute.IsInterface && // ... which is an interface ... + typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... + !iset.Contains(typeInAttribute)) // ... which is not already in the list { - var type = attr.EnabledType ?? composerType; - var weight = type == composerType ? 1 : 3; - UpdateEnableInfo(type, weight, enabled, true); - } + // add it to the new list + iset.Add(typeInAttribute); + set2.Add(typeInAttribute); - foreach (var attr in composerType.GetCustomAttributes()) - { - var type = attr.DisabledType ?? composerType; - var weight = type == composerType ? 1 : 3; - UpdateEnableInfo(type, weight, enabled, false); - } - } - - // remove composers that end up being disabled - foreach (var kvp in enabled.Where(x => x.Value.Enabled == false)) - types.Remove(kvp.Key); - } - - private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary?> requirements, bool throwOnMissing = true) - { - // get 'require' attributes - // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var afterAttributes = type - .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces - .Concat(type.GetCustomAttributes()); // those marking the composer - - // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. - foreach (var attr in afterAttributes) - { - if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) - - // requiring an interface = require any enabled composer implementing that interface - // unless strong, and then require at least one enabled composer implementing that interface - if (attr.RequiredType.IsInterface) - { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList(); - if (implems.Count > 0) + // add all its interfaces implementing IComposer + foreach (Type i in typeInAttribute.GetInterfaces() + .Where(x => typeof(IComposer).IsAssignableFrom(x))) { - if (requirements[type] == null) requirements[type] = new List(); - requirements[type]!.AddRange(implems); - } - else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak - throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); - } - // requiring a class = require that the composer is enabled - // unless weak, and then requires it if it is enabled - else - { - if (types.Contains(attr.RequiredType)) - { - if (requirements[type] == null) requirements[type] = new List(); - requirements[type]!.Add(attr.RequiredType); - } - else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong - throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); - } - } - } - - private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary?> requirements) - { - // get 'required' attributes - // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var beforeAttributes = type - .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces - .Concat(type.GetCustomAttributes()); // those marking the composer - - foreach (var attr in beforeAttributes) - { - if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) - - // required by an interface = by any enabled composer implementing this that interface - if (attr.RequiringType.IsInterface) - { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); - foreach (var implem in implems) - { - if (requirements[implem] == null) requirements[implem] = new List(); - requirements[implem]!.Add(type); - } - } - // required by a class - else - { - if (types.Contains(attr.RequiringType)) - { - if (requirements[attr.RequiringType] == null) requirements[attr.RequiringType] = new List(); - requirements[attr.RequiringType]!.Add(type); + iset.Add(i); + set2.Add(i); } } } } - private static IEnumerable InstantiateComposers(IEnumerable types) + // gather interfaces too + var interfaces = new HashSet(composerTypeList.SelectMany(x => + x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); + composerTypeList.AddRange(interfaces); + List list1 = composerTypeList; + while (list1.Count > 0) { - foreach (Type type in types) + var list2 = new List(); + foreach (Type t in list1) { - ConstructorInfo? ctor = type.GetConstructor(Array.Empty()); + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, list2); + } - if (ctor == null) + composerTypeList.AddRange(list2); + list1 = list2; + } + + // sort the composers according to their dependencies + var requirements = new Dictionary?>(); + foreach (Type type in composerTypeList) + { + requirements[type] = null; + } + + foreach (Type type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); + } + + return requirements; + } + + internal IEnumerable SortComposers(Dictionary?> requirements) + { + // sort composers + var graph = new TopoGraph?>>(kvp => kvp.Key, kvp => kvp.Value); + graph.AddItems(requirements); + List sortedComposerTypes; + try + { + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); + } + catch (Exception e) + { + // in case of an error, force-dump everything to log + _logger.LogInformation("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements)); + _logger.LogError(e, "Failed to sort composers."); + throw; + } + + return sortedComposerTypes; + } + + private static void EnableDisableComposers(IEnumerable enableDisableAttributes, ICollection types) + { + var enabled = new Dictionary(); + + // process the enable/disable attributes + // these two attributes are *not* inherited and apply to *classes* only (not interfaces). + // remote declarations (when a composer enables/disables *another* composer) + // have priority over local declarations (when a composer disables itself) so that + // ppl can enable composers that, by default, are disabled. + // what happens in case of conflicting remote declarations is unspecified. more + // precisely, the last declaration to be processed wins, but the order of the + // declarations depends on the type finder and is unspecified. + void UpdateEnableInfo(Type composerType, int weight2, Dictionary enabled2, bool value) + { + if (enabled.TryGetValue(composerType, out EnableInfo? enableInfo) == false) + { + enableInfo = enabled2[composerType] = new EnableInfo(); + } + + if (enableInfo.Weight > weight2) + { + return; + } + + enableInfo.Enabled = value; + enableInfo.Weight = weight2; + } + + foreach (EnableComposerAttribute attr in enableDisableAttributes.OfType()) + { + Type type = attr.EnabledType; + UpdateEnableInfo(type, 2, enabled, true); + } + + foreach (DisableComposerAttribute attr in enableDisableAttributes.OfType()) + { + Type type = attr.DisabledType; + UpdateEnableInfo(type, 2, enabled, false); + } + + foreach (Type composerType in types) + { + foreach (EnableAttribute attr in composerType.GetCustomAttributes()) + { + Type type = attr.EnabledType ?? composerType; + var weight = type == composerType ? 1 : 3; + UpdateEnableInfo(type, weight, enabled, true); + } + + foreach (DisableAttribute attr in composerType.GetCustomAttributes()) + { + Type type = attr.DisabledType ?? composerType; + var weight = type == composerType ? 1 : 3; + UpdateEnableInfo(type, weight, enabled, false); + } + } + + // remove composers that end up being disabled + foreach (KeyValuePair kvp in enabled.Where(x => x.Value.Enabled == false)) + { + types.Remove(kvp.Key); + } + } + + private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary?> requirements, bool throwOnMissing = true) + { + // get 'require' attributes + // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only + IEnumerable afterAttributes = type + .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces + .Concat(type.GetCustomAttributes()); // those marking the composer + + // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. + foreach (ComposeAfterAttribute attr in afterAttributes) + { + if (attr.RequiredType == type) + { + continue; // ignore self-requirements (+ exclude in implems, below) + } + + // requiring an interface = require any enabled composer implementing that interface + // unless strong, and then require at least one enabled composer implementing that interface + if (attr.RequiredType.IsInterface) + { + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface) + .ToList(); + if (implems.Count > 0) { - throw new InvalidOperationException($"Composer {type.FullName} does not have a parameter-less constructor."); + if (requirements[type] == null) + { + requirements[type] = new List(); + } + + requirements[type]!.AddRange(implems); } - yield return (IComposer) ctor.Invoke(Array.Empty()); + // if explicitly set to !weak, is strong, else is weak + else if (attr.Weak == false && throwOnMissing) + { + throw new Exception( + $"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } + } + + // requiring a class = require that the composer is enabled + // unless weak, and then requires it if it is enabled + else + { + if (types.Contains(attr.RequiredType)) + { + if (requirements[type] == null) + { + requirements[type] = new List(); + } + + requirements[type]!.Add(attr.RequiredType); + } + + // if not explicitly set to weak, is strong + else if (attr.Weak != true && throwOnMissing) + { + throw new Exception( + $"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } } } } + + private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary?> requirements) + { + // get 'required' attributes + // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only + IEnumerable beforeAttributes = type + .GetInterfaces() + .SelectMany(x => x.GetCustomAttributes()) // those marking interfaces + .Concat(type.GetCustomAttributes()); // those marking the composer + + foreach (ComposeBeforeAttribute attr in beforeAttributes) + { + if (attr.RequiringType == type) + { + continue; // ignore self-requirements (+ exclude in implems, below) + } + + // required by an interface = by any enabled composer implementing this that interface + if (attr.RequiringType.IsInterface) + { + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface) + .ToList(); + foreach (Type implem in implems) + { + if (requirements[implem] == null) + { + requirements[implem] = new List(); + } + + requirements[implem]!.Add(type); + } + } + + // required by a class + else + { + if (types.Contains(attr.RequiringType)) + { + if (requirements[attr.RequiringType] == null) + { + requirements[attr.RequiringType] = new List(); + } + + requirements[attr.RequiringType]!.Add(type); + } + } + } + } + + private static IEnumerable InstantiateComposers(IEnumerable types) + { + foreach (Type type in types) + { + ConstructorInfo? ctor = type.GetConstructor(Array.Empty()); + + if (ctor == null) + { + throw new InvalidOperationException( + $"Composer {type.FullName} does not have a parameter-less constructor."); + } + + yield return (IComposer)ctor.Invoke(Array.Empty()); + } + } + + private class EnableInfo + { + public bool Enabled { get; set; } + + public int Weight { get; set; } = -1; + } } diff --git a/src/Umbraco.Core/Composing/CompositionExtensions.cs b/src/Umbraco.Core/Composing/CompositionExtensions.cs index d087af77d8..2906070e4f 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions.cs @@ -1,43 +1,45 @@ -using System; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.PublishedCache; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class CompositionExtensions { - public static class CompositionExtensions + /// + /// Sets the published snapshot service. + /// + /// The builder. + /// A function creating a published snapshot service. + public static IUmbracoBuilder SetPublishedSnapshotService( + this IUmbracoBuilder builder, + Func factory) { - /// - /// Sets the published snapshot service. - /// - /// The builder. - /// A function creating a published snapshot service. - public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published snapshot service. - /// - /// The type of the published snapshot service. - /// The builder. - public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder) - where T : class, IPublishedSnapshotService - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the published snapshot service. + /// + /// The type of the published snapshot service. + /// The builder. + public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder) + where T : class, IPublishedSnapshotService + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the published snapshot service. - /// - /// The builder. - /// A published snapshot service. - public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder, IPublishedSnapshotService service) - { - builder.Services.AddUnique(service); - return builder; - } + /// + /// Sets the published snapshot service. + /// + /// The builder. + /// A published snapshot service. + public static IUmbracoBuilder SetPublishedSnapshotService( + this IUmbracoBuilder builder, + IPublishedSnapshotService service) + { + builder.Services.AddUnique(service); + return builder; } } diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index 5cc38f31a7..0f1d0cc571 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -1,61 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Returns a list of scannable assemblies based on an entry point assembly and it's references +/// +/// +/// This will recursively search through the entry point's assemblies and Umbraco's core assemblies and their +/// references +/// to create a list of scannable assemblies based on whether they themselves or their transitive dependencies +/// reference Umbraco core assemblies. +/// +public class DefaultUmbracoAssemblyProvider : IAssemblyProvider { - /// - /// Returns a list of scannable assemblies based on an entry point assembly and it's references - /// - /// - /// This will recursively search through the entry point's assemblies and Umbraco's core assemblies and their references - /// to create a list of scannable assemblies based on whether they themselves or their transitive dependencies reference Umbraco core assemblies. - /// - public class DefaultUmbracoAssemblyProvider : IAssemblyProvider + private readonly IEnumerable? _additionalTargetAssemblies; + private readonly Assembly _entryPointAssembly; + private readonly ILoggerFactory _loggerFactory; + private List? _discovered; + + public DefaultUmbracoAssemblyProvider( + Assembly? entryPointAssembly, + ILoggerFactory loggerFactory, + IEnumerable? additionalTargetAssemblies = null) { - private readonly Assembly _entryPointAssembly; - private readonly ILoggerFactory _loggerFactory; - private readonly IEnumerable? _additionalTargetAssemblies; - private List? _discovered; + _entryPointAssembly = entryPointAssembly ?? throw new ArgumentNullException(nameof(entryPointAssembly)); + _loggerFactory = loggerFactory; + _additionalTargetAssemblies = additionalTargetAssemblies; + } - public DefaultUmbracoAssemblyProvider( - Assembly? entryPointAssembly, - ILoggerFactory loggerFactory, - IEnumerable? additionalTargetAssemblies = null) + // TODO: It would be worth investigating a netcore3 version of this which would use + // var allAssemblies = System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies); + // that will still only resolve Assemblies that are already loaded but it would also make it possible to + // query dynamically generated assemblies once they are added. It would also provide the ability to probe + // assembly locations that are not in the same place as the entry point assemblies. + public IEnumerable Assemblies + { + get { - _entryPointAssembly = entryPointAssembly ?? throw new ArgumentNullException(nameof(entryPointAssembly)); - _loggerFactory = loggerFactory; - _additionalTargetAssemblies = additionalTargetAssemblies; - } - - // TODO: It would be worth investigating a netcore3 version of this which would use - // var allAssemblies = System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies); - // that will still only resolve Assemblies that are already loaded but it would also make it possible to - // query dynamically generated assemblies once they are added. It would also provide the ability to probe - // assembly locations that are not in the same place as the entry point assemblies. - - public IEnumerable Assemblies - { - get + if (_discovered != null) { - if (_discovered != null) - { - return _discovered; - } - - IEnumerable additionalTargetAssemblies = Constants.Composing.UmbracoCoreAssemblyNames; - if (_additionalTargetAssemblies != null) - { - additionalTargetAssemblies = additionalTargetAssemblies.Concat(_additionalTargetAssemblies); - } - - var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, additionalTargetAssemblies.ToArray(), true, _loggerFactory); - _discovered = finder.Find().ToList(); - return _discovered; } + + IEnumerable additionalTargetAssemblies = Constants.Composing.UmbracoCoreAssemblyNames; + if (_additionalTargetAssemblies != null) + { + additionalTargetAssemblies = additionalTargetAssemblies.Concat(_additionalTargetAssemblies); + } + + var finder = new FindAssembliesWithReferencesTo( + new[] { _entryPointAssembly }, + additionalTargetAssemblies.ToArray(), + true, + _loggerFactory); + _discovered = finder.Find().ToList(); + + return _discovered; } } } diff --git a/src/Umbraco.Core/Composing/DisableAttribute.cs b/src/Umbraco.Core/Composing/DisableAttribute.cs index 23d825ee1c..09d638188d 100644 --- a/src/Umbraco.Core/Composing/DisableAttribute.cs +++ b/src/Umbraco.Core/Composing/DisableAttribute.cs @@ -1,43 +1,44 @@ -using System; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Indicates that a composer should be disabled. +/// +/// +/// +/// If a type is specified, disables the composer of that type, else disables the composer marked with the +/// attribute. +/// +/// This attribute is *not* inherited. +/// This attribute applies to classes only, it is not possible to enable/disable interfaces. +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public class DisableAttribute : Attribute { /// - /// Indicates that a composer should be disabled. + /// Initializes a new instance of the class. /// - /// - /// If a type is specified, disables the composer of that type, else disables the composer marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public class DisableAttribute : Attribute + public DisableAttribute() { - /// - /// Initializes a new instance of the class. - /// - public DisableAttribute() - { } - - public DisableAttribute(string fullTypeName, string assemblyName) - { - DisabledType = Assembly.Load(assemblyName)?.GetType(fullTypeName); - } - - /// - /// Initializes a new instance of the class. - /// - public DisableAttribute(Type disabledType) - { - DisabledType = disabledType; - } - - /// - /// Gets the disabled type, or null if it is the composer marked with the attribute. - /// - public Type? DisabledType { get; } } + + public DisableAttribute(string fullTypeName, string assemblyName) => + DisabledType = Assembly.Load(assemblyName)?.GetType(fullTypeName); + + /// + /// Initializes a new instance of the class. + /// + public DisableAttribute(Type disabledType) => DisabledType = disabledType; + + /// + /// Gets the disabled type, or null if it is the composer marked with the attribute. + /// + public Type? DisabledType { get; } } diff --git a/src/Umbraco.Core/Composing/DisableComposerAttribute.cs b/src/Umbraco.Core/Composing/DisableComposerAttribute.cs index 59b36178cf..2c85d45b46 100644 --- a/src/Umbraco.Core/Composing/DisableComposerAttribute.cs +++ b/src/Umbraco.Core/Composing/DisableComposerAttribute.cs @@ -1,28 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer should be disabled. +/// +/// +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public class DisableComposerAttribute : Attribute { /// - /// Indicates that a composer should be disabled. + /// Initializes a new instance of the class. /// - /// - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public class DisableComposerAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public DisableComposerAttribute(Type disabledType) - { - DisabledType = disabledType; - } + public DisableComposerAttribute(Type disabledType) => DisabledType = disabledType; - /// - /// Gets the disabled type, or null if it is the composer marked with the attribute. - /// - public Type DisabledType { get; } - } + /// + /// Gets the disabled type, or null if it is the composer marked with the attribute. + /// + public Type DisabledType { get; } } diff --git a/src/Umbraco.Core/Composing/EnableAttribute.cs b/src/Umbraco.Core/Composing/EnableAttribute.cs index 90fb1a9cc6..7ca33d50b7 100644 --- a/src/Umbraco.Core/Composing/EnableAttribute.cs +++ b/src/Umbraco.Core/Composing/EnableAttribute.cs @@ -1,37 +1,39 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer should be enabled. +/// +/// +/// +/// If a type is specified, enables the composer of that type, else enables the composer marked with the +/// attribute. +/// +/// This attribute is *not* inherited. +/// This attribute applies to classes only, it is not possible to enable/disable interfaces. +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public class EnableAttribute : Attribute { /// - /// Indicates that a composer should be enabled. + /// Initializes a new instance of the class. /// - /// - /// If a type is specified, enables the composer of that type, else enables the composer marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public class EnableAttribute : Attribute + public EnableAttribute() { - /// - /// Initializes a new instance of the class. - /// - public EnableAttribute() - { } - - /// - /// Initializes a new instance of the class. - /// - public EnableAttribute(Type enabledType) - { - EnabledType = enabledType; - } - - /// - /// Gets the enabled type, or null if it is the composer marked with the attribute. - /// - public Type? EnabledType { get; } } + + /// + /// Initializes a new instance of the class. + /// + public EnableAttribute(Type enabledType) => EnabledType = enabledType; + + /// + /// Gets the enabled type, or null if it is the composer marked with the attribute. + /// + public Type? EnabledType { get; } } diff --git a/src/Umbraco.Core/Composing/EnableComposerAttribute.cs b/src/Umbraco.Core/Composing/EnableComposerAttribute.cs index 048a19a80f..b1a0f53bcd 100644 --- a/src/Umbraco.Core/Composing/EnableComposerAttribute.cs +++ b/src/Umbraco.Core/Composing/EnableComposerAttribute.cs @@ -1,31 +1,32 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer should be enabled. +/// +/// +/// +/// If a type is specified, enables the composer of that type, else enables the composer marked with the +/// attribute. +/// +/// This attribute is *not* inherited. +/// This attribute applies to classes only, it is not possible to enable/disable interfaces. +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public class EnableComposerAttribute : Attribute { /// - /// Indicates that a composer should be enabled. + /// Initializes a new instance of the class. /// - /// - /// If a type is specified, enables the composer of that type, else enables the composer marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public class EnableComposerAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public EnableComposerAttribute(Type enabledType) - { - EnabledType = enabledType; - } + public EnableComposerAttribute(Type enabledType) => EnabledType = enabledType; - /// - /// Gets the enabled type, or null if it is the composer marked with the attribute. - /// - public Type EnabledType { get; } - } + /// + /// Gets the enabled type, or null if it is the composer marked with the attribute. + /// + public Type EnabledType { get; } } diff --git a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs index 78cdb80f58..f9e4ed6dbe 100644 --- a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs +++ b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs @@ -1,69 +1,69 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Composing -{ - /// - /// Finds Assemblies from the entry point assemblies, it's dependencies and it's transitive dependencies that reference that targetAssemblyNames - /// - /// - /// borrowed and modified from here https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/FindAssembliesWithReferencesTo.cs - /// - internal class FindAssembliesWithReferencesTo - { - private readonly Assembly[] _referenceAssemblies; - private readonly string[] _targetAssemblies; - private readonly bool _includeTargets; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; +namespace Umbraco.Cms.Core.Composing; - /// - /// Constructor - /// - /// Entry point assemblies - /// Used to check if the entry point or it's transitive assemblies reference these assembly names - /// If true will also use the target assembly names as entry point assemblies - /// Logger factory for when scanning goes wrong - public FindAssembliesWithReferencesTo(Assembly[] referenceAssemblies, string[] targetAssemblyNames, bool includeTargets, ILoggerFactory loggerFactory) +/// +/// Finds Assemblies from the entry point assemblies, it's dependencies and it's transitive dependencies that reference +/// that targetAssemblyNames +/// +/// +/// borrowed and modified from here +/// https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/FindAssembliesWithReferencesTo.cs +/// +internal class FindAssembliesWithReferencesTo +{ + private readonly bool _includeTargets; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly Assembly[] _referenceAssemblies; + private readonly string[] _targetAssemblies; + + /// + /// Constructor + /// + /// Entry point assemblies + /// + /// Used to check if the entry point or it's transitive assemblies reference these + /// assembly names + /// + /// If true will also use the target assembly names as entry point assemblies + /// Logger factory for when scanning goes wrong + public FindAssembliesWithReferencesTo(Assembly[] referenceAssemblies, string[] targetAssemblyNames, bool includeTargets, ILoggerFactory loggerFactory) + { + _referenceAssemblies = referenceAssemblies; + _targetAssemblies = targetAssemblyNames; + _includeTargets = includeTargets; + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); + } + + public IEnumerable Find() + { + var referenceItems = new List(); + foreach (Assembly assembly in _referenceAssemblies) { - _referenceAssemblies = referenceAssemblies; - _targetAssemblies = targetAssemblyNames; - _includeTargets = includeTargets; - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); + referenceItems.Add(assembly); } - public IEnumerable Find() + if (_includeTargets) { - var referenceItems = new List(); - foreach (var assembly in _referenceAssemblies) + foreach (var target in _targetAssemblies) { - referenceItems.Add(assembly); - } - - if (_includeTargets) - { - foreach(var target in _targetAssemblies) + try { - try - { - referenceItems.Add(Assembly.Load(target)); - } - catch (FileNotFoundException ex) - { - // occurs if we cannot load this ... for example in a test project where we aren't currently referencing Umbraco.Web, etc... - _logger.LogDebug(ex, "Could not load assembly " + target); - } + referenceItems.Add(Assembly.Load(target)); + } + catch (FileNotFoundException ex) + { + // occurs if we cannot load this ... for example in a test project where we aren't currently referencing Umbraco.Web, etc... + _logger.LogDebug(ex, "Could not load assembly " + target); } } - - var provider = new ReferenceResolver(_targetAssemblies, referenceItems, _loggerFactory.CreateLogger()); - var assemblyNames = provider.ResolveAssemblies(); - return assemblyNames.ToList(); } + var provider = new ReferenceResolver(_targetAssemblies, referenceItems, _loggerFactory.CreateLogger()); + IEnumerable assemblyNames = provider.ResolveAssemblies(); + return assemblyNames.ToList(); } } diff --git a/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs b/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs index b985a79494..4478deb5ac 100644 --- a/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs +++ b/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs @@ -1,11 +1,9 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Notifies the TypeFinder that it should ignore the class marked with this attribute. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class HideFromTypeFinderAttribute : Attribute { - /// - /// Notifies the TypeFinder that it should ignore the class marked with this attribute. - /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class HideFromTypeFinderAttribute : Attribute - { } } diff --git a/src/Umbraco.Core/Composing/IAssemblyProvider.cs b/src/Umbraco.Core/Composing/IAssemblyProvider.cs index fdc942ae24..4148c9ee47 100644 --- a/src/Umbraco.Core/Composing/IAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/IAssemblyProvider.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a list of assemblies that can be scanned +/// +public interface IAssemblyProvider { - /// - /// Provides a list of assemblies that can be scanned - /// - public interface IAssemblyProvider - { - IEnumerable Assemblies { get; } - } + IEnumerable Assemblies { get; } } diff --git a/src/Umbraco.Core/Composing/IBuilderCollection.cs b/src/Umbraco.Core/Composing/IBuilderCollection.cs index 5e78cf0c2f..56036997bc 100644 --- a/src/Umbraco.Core/Composing/IBuilderCollection.cs +++ b/src/Umbraco.Core/Composing/IBuilderCollection.cs @@ -1,16 +1,13 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Represents a builder collection, ie an immutable enumeration of items. +/// +/// The type of the items. +public interface IBuilderCollection : IEnumerable { /// - /// Represents a builder collection, ie an immutable enumeration of items. + /// Gets the number of items in the collection. /// - /// The type of the items. - public interface IBuilderCollection : IEnumerable - { - /// - /// Gets the number of items in the collection. - /// - int Count { get; } - } + int Count { get; } } diff --git a/src/Umbraco.Core/Composing/ICollectionBuilder.cs b/src/Umbraco.Core/Composing/ICollectionBuilder.cs index ea09558cad..da25a548e7 100644 --- a/src/Umbraco.Core/Composing/ICollectionBuilder.cs +++ b/src/Umbraco.Core/Composing/ICollectionBuilder.cs @@ -1,33 +1,31 @@ -using System; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a collection builder. +/// +public interface ICollectionBuilder { /// - /// Represents a collection builder. + /// Registers the builder so it can build the collection, by + /// registering the collection and the types. /// - public interface ICollectionBuilder - { - /// - /// Registers the builder so it can build the collection, by - /// registering the collection and the types. - /// - void RegisterWith(IServiceCollection services); - } - - /// - /// Represents a collection builder. - /// - /// The type of the collection. - /// The type of the items. - public interface ICollectionBuilder : ICollectionBuilder - where TCollection : IBuilderCollection - { - /// - /// Creates a collection. - /// - /// A collection. - /// Creates a new collection each time it is invoked. - TCollection CreateCollection(IServiceProvider factory); - } + void RegisterWith(IServiceCollection services); +} + +/// +/// Represents a collection builder. +/// +/// The type of the collection. +/// The type of the items. +public interface ICollectionBuilder : ICollectionBuilder + where TCollection : IBuilderCollection +{ + /// + /// Creates a collection. + /// + /// A collection. + /// Creates a new collection each time it is invoked. + TCollection CreateCollection(IServiceProvider factory); } diff --git a/src/Umbraco.Core/Composing/IComponent.cs b/src/Umbraco.Core/Composing/IComponent.cs index 8e9cf815e8..d5655f8a1f 100644 --- a/src/Umbraco.Core/Composing/IComponent.cs +++ b/src/Umbraco.Core/Composing/IComponent.cs @@ -1,25 +1,28 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a component. +/// +/// +/// Components are created by DI and therefore must have a public constructor. +/// +/// All components are terminated in reverse order when Umbraco terminates, and +/// disposable components are disposed. +/// +/// +/// The Dispose method may be invoked more than once, and components +/// should ensure they support this. +/// +/// +public interface IComponent { /// - /// Represents a component. + /// Initializes the component. /// - /// - /// Components are created by DI and therefore must have a public constructor. - /// All components are terminated in reverse order when Umbraco terminates, and - /// disposable components are disposed. - /// The Dispose method may be invoked more than once, and components - /// should ensure they support this. - /// - public interface IComponent - { - /// - /// Initializes the component. - /// - void Initialize(); + void Initialize(); - /// - /// Terminates the component. - /// - void Terminate(); - } + /// + /// Terminates the component. + /// + void Terminate(); } diff --git a/src/Umbraco.Core/Composing/IComposer.cs b/src/Umbraco.Core/Composing/IComposer.cs index 6f1978ee3e..7d0a859314 100644 --- a/src/Umbraco.Core/Composing/IComposer.cs +++ b/src/Umbraco.Core/Composing/IComposer.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a composer. +/// +public interface IComposer : IDiscoverable { /// - /// Represents a composer. + /// Compose. /// - public interface IComposer : IDiscoverable - { - /// - /// Compose. - /// - void Compose(IUmbracoBuilder builder); - } + void Compose(IUmbracoBuilder builder); } diff --git a/src/Umbraco.Core/Composing/IDiscoverable.cs b/src/Umbraco.Core/Composing/IDiscoverable.cs index 153fde36b6..848c70ddab 100644 --- a/src/Umbraco.Core/Composing/IDiscoverable.cs +++ b/src/Umbraco.Core/Composing/IDiscoverable.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +public interface IDiscoverable { - public interface IDiscoverable - { } } diff --git a/src/Umbraco.Core/Composing/IRuntimeHash.cs b/src/Umbraco.Core/Composing/IRuntimeHash.cs index b19b22a7e9..d641c90538 100644 --- a/src/Umbraco.Core/Composing/IRuntimeHash.cs +++ b/src/Umbraco.Core/Composing/IRuntimeHash.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Used to create a hash value of the current runtime +/// +/// +/// This is used to detect if the runtime itself has changed, like a DLL has changed or another dynamically compiled +/// part of the application has changed. This is used to detect if we need to re-type scan. +/// +public interface IRuntimeHash { - /// - /// Used to create a hash value of the current runtime - /// - /// - /// This is used to detect if the runtime itself has changed, like a DLL has changed or another dynamically compiled - /// part of the application has changed. This is used to detect if we need to re-type scan. - /// - public interface IRuntimeHash - { - string GetHashValue(); - } + string GetHashValue(); } diff --git a/src/Umbraco.Core/Composing/ITypeFinder.cs b/src/Umbraco.Core/Composing/ITypeFinder.cs index 7d59b68869..4bebfae334 100644 --- a/src/Umbraco.Core/Composing/ITypeFinder.cs +++ b/src/Umbraco.Core/Composing/ITypeFinder.cs @@ -1,55 +1,52 @@ -using System; -using System.Collections.Generic; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Used to find objects by implemented types, names and/or attributes +/// +public interface ITypeFinder { /// - /// Used to find objects by implemented types, names and/or attributes + /// Return a list of found local Assemblies that Umbraco should scan for type finding /// - public interface ITypeFinder - { - Type? GetTypeByName(string name); + /// + IEnumerable AssembliesToScan { get; } - /// - /// Return a list of found local Assemblies that Umbraco should scan for type finding - /// - /// - IEnumerable AssembliesToScan { get; } + Type? GetTypeByName(string name); - /// - /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute - /// - /// - /// - /// - /// - /// - IEnumerable FindClassesOfTypeWithAttribute( - Type assignTypeFrom, - Type attributeType, - IEnumerable? assemblies = null, - bool onlyConcreteClasses = true); + /// + /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute + /// + /// + /// + /// + /// + /// + IEnumerable FindClassesOfTypeWithAttribute( + Type assignTypeFrom, + Type attributeType, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true); - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - /// - IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true); + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true); - /// - /// Finds any classes with the attribute. - /// - /// The attribute type - /// The assemblies. - /// if set to true only concrete classes. - /// - IEnumerable FindClassesWithAttribute( - Type attributeType, - IEnumerable? assemblies, - bool onlyConcreteClasses); - } + /// + /// Finds any classes with the attribute. + /// + /// The attribute type + /// The assemblies. + /// if set to true only concrete classes. + /// + IEnumerable FindClassesWithAttribute( + Type attributeType, + IEnumerable? assemblies, + bool onlyConcreteClasses); } diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index baae385af4..49ada40dfa 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -1,129 +1,131 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements a lazy collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +/// +/// This type of collection builder is typically used when type scanning is required (i.e. plugins). +/// +public abstract class + LazyCollectionBuilderBase : CollectionBuilderBase + where TBuilder : LazyCollectionBuilderBase + where TCollection : class, IBuilderCollection { + private readonly List _excluded = new(); + private readonly List>> _producers = new(); + + protected abstract TBuilder This { get; } + /// - /// Implements a lazy collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - /// - /// This type of collection builder is typically used when type scanning is required (i.e. plugins). - /// - public abstract class LazyCollectionBuilderBase : CollectionBuilderBase - where TBuilder : LazyCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - private readonly List>> _producers = new List>>(); - private readonly List _excluded = new List(); - - protected abstract TBuilder This { get; } - - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + Configure(types => { - Configure(types => - { - types.Clear(); - _producers.Clear(); - _excluded.Clear(); - }); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add() - where T : TItem - { - Configure(types => - { - var type = typeof(T); - if (types.Contains(type) == false) - types.Add(type); - }); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add(Type type) - { - Configure(types => - { - EnsureType(type, "register"); - if (types.Contains(type) == false) - types.Add(type); - }); - return This; - } - - /// - /// Adds a types producer to the collection. - /// - /// The types producer. - /// The builder. - public TBuilder Add(Func> producer) - { - Configure(types => - { - _producers.Add(producer); - }); - return This; - } - - /// - /// Excludes a type from the collection. - /// - /// The type to exclude. - /// The builder. - public TBuilder Exclude() - where T : TItem - { - Configure(types => - { - var type = typeof(T); - if (_excluded.Contains(type) == false) - _excluded.Add(type); - }); - return This; - } - - /// - /// Excludes a type from the collection. - /// - /// The type to exclude. - /// The builder. - public TBuilder Exclude(Type type) - { - Configure(types => - { - EnsureType(type, "exclude"); - if (_excluded.Contains(type) == false) - _excluded.Add(type); - }); - return This; - } - - protected override IEnumerable GetRegisteringTypes(IEnumerable types) - { - return types - .Union(_producers.SelectMany(x => x())) - .Distinct() - .Select(x => EnsureType(x, "register")) - .Except(_excluded); - } + types.Clear(); + _producers.Clear(); + _excluded.Clear(); + }); + return This; } + + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => + { + Type type = typeof(T); + if (types.Contains(type) == false) + { + types.Add(type); + } + }); + return This; + } + + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add(Type type) + { + Configure(types => + { + EnsureType(type, "register"); + if (types.Contains(type) == false) + { + types.Add(type); + } + }); + return This; + } + + /// + /// Adds a types producer to the collection. + /// + /// The types producer. + /// The builder. + public TBuilder Add(Func> producer) + { + Configure(types => + { + _producers.Add(producer); + }); + return This; + } + + /// + /// Excludes a type from the collection. + /// + /// The type to exclude. + /// The builder. + public TBuilder Exclude() + where T : TItem + { + Configure(types => + { + Type type = typeof(T); + if (_excluded.Contains(type) == false) + { + _excluded.Add(type); + } + }); + return This; + } + + /// + /// Excludes a type from the collection. + /// + /// The type to exclude. + /// The builder. + public TBuilder Exclude(Type type) + { + Configure(types => + { + EnsureType(type, "exclude"); + if (_excluded.Contains(type) == false) + { + _excluded.Add(type); + } + }); + return This; + } + + protected override IEnumerable GetRegisteringTypes(IEnumerable types) => + types + .Union(_producers.SelectMany(x => x())) + .Distinct() + .Select(x => EnsureType(x, "register")) + .Except(_excluded); } diff --git a/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs b/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs index 67116524ac..eb6f2ed055 100644 --- a/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs +++ b/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs @@ -1,48 +1,46 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +public sealed class LazyReadOnlyCollection : IReadOnlyCollection { - public sealed class LazyReadOnlyCollection : IReadOnlyCollection + private readonly Lazy> _lazyCollection; + private int? _count; + + public LazyReadOnlyCollection(Lazy> lazyCollection) => _lazyCollection = lazyCollection; + + public LazyReadOnlyCollection(Func> lazyCollection) => + _lazyCollection = new Lazy>(lazyCollection); + + public IEnumerable Value => EnsureCollection(); + + public int Count { - private readonly Lazy> _lazyCollection; - private int? _count; - - public LazyReadOnlyCollection(Lazy> lazyCollection) => _lazyCollection = lazyCollection; - - public LazyReadOnlyCollection(Func> lazyCollection) => _lazyCollection = new Lazy>(lazyCollection); - - public IEnumerable Value => EnsureCollection(); - - private IEnumerable EnsureCollection() + get { - if (_lazyCollection == null) - { - _count = 0; - return Enumerable.Empty(); - } + EnsureCollection(); + return _count.GetValueOrDefault(); + } + } - IEnumerable val = _lazyCollection.Value; - if (_count == null) - { - _count = val.Count(); - } - return val; + public IEnumerator GetEnumerator() => Value.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerable EnsureCollection() + { + if (_lazyCollection == null) + { + _count = 0; + return Enumerable.Empty(); } - public int Count + IEnumerable val = _lazyCollection.Value; + if (_count == null) { - get - { - EnsureCollection(); - return _count.GetValueOrDefault(); - } + _count = val.Count(); } - public IEnumerator GetEnumerator() => Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return val; } } diff --git a/src/Umbraco.Core/Composing/LazyResolve.cs b/src/Umbraco.Core/Composing/LazyResolve.cs index afa22f74b6..723d9afe2e 100644 --- a/src/Umbraco.Core/Composing/LazyResolve.cs +++ b/src/Umbraco.Core/Composing/LazyResolve.cs @@ -1,13 +1,12 @@ -using System; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +public class LazyResolve : Lazy + where T : class { - public class LazyResolve : Lazy - where T : class + public LazyResolve(IServiceProvider serviceProvider) + : base(serviceProvider.GetRequiredService) { - public LazyResolve(IServiceProvider serviceProvider) - : base(serviceProvider.GetRequiredService) - { } } } diff --git a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs index 939561f557..d9c733da7d 100644 --- a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs @@ -1,331 +1,418 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements an ordered collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +public abstract class OrderedCollectionBuilderBase : CollectionBuilderBase + where TBuilder : OrderedCollectionBuilderBase + where TCollection : class, IBuilderCollection { + protected abstract TBuilder This { get; } + /// - /// Implements an ordered collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - public abstract class OrderedCollectionBuilderBase : CollectionBuilderBase - where TBuilder : OrderedCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - protected abstract TBuilder This { get; } + Configure(types => types.Clear()); + return This; + } - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + /// + /// Appends a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Append() + where T : TItem + { + Configure(types => { - Configure(types => types.Clear()); - return This; - } - - /// - /// Appends a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Append() - where T : TItem - { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof (T); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } + types.Remove(type); + } - /// - /// Appends a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Append(Type type) + types.Add(type); + }); + return This; + } + + /// + /// Appends a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Append(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "register"); + if (types.Contains(type)) { + types.Remove(type); + } + + types.Add(type); + }); + return This; + } + + /// + /// Appends types to the collections. + /// + /// The types to append. + /// The builder. + public TBuilder Append(IEnumerable types) + { + Configure(list => + { + foreach (Type type in types) + { + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast EnsureType(type, "register"); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } - - /// - /// Appends types to the collections. - /// - /// The types to append. - /// The builder. - public TBuilder Append(IEnumerable types) - { - Configure(list => - { - foreach (var type in types) + if (list.Contains(type)) { - // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast - EnsureType(type, "register"); - if (list.Contains(type)) list.Remove(type); - list.Add(type); + list.Remove(type); } - }); - return This; - } - /// - /// Inserts a type into the collection. - /// - /// The type to insert. - /// The optional index. - /// The builder. - /// Throws if the index is out of range. - public TBuilder Insert(int index = 0) - where T : TItem + list.Add(type); + } + }); + return This; + } + + /// + /// Inserts a type into the collection. + /// + /// The type to insert. + /// The optional index. + /// The builder. + /// Throws if the index is out of range. + public TBuilder Insert(int index = 0) + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) + { + types.Remove(type); + } + + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type into the collection. + /// + /// The type to insert. + /// The builder. + /// Throws if the index is out of range. + public TBuilder Insert(Type type) => Insert(0, type); + + /// + /// Inserts a type into the collection. + /// + /// The index. + /// The type to insert. + /// The builder. + /// Throws if the index is out of range. + public TBuilder Insert(int index, Type type) + { + Configure(types => + { + EnsureType(type, "register"); + if (types.Contains(type)) + { + types.Remove(type); + } + + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type before another type. + /// + /// The other type. + /// The type to insert. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertBefore() + where TBefore : TItem + where T : TItem + { + Configure(types => + { + Type typeBefore = typeof(TBefore); + Type type = typeof(T); + if (typeBefore == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeBefore); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeBefore); // in case removing type changed index + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type before another type. + /// + /// The other type. + /// The type to insert. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertBefore(Type typeBefore, Type type) + { + Configure(types => + { + EnsureType(typeBefore, "find"); + EnsureType(type, "register"); + + if (typeBefore == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeBefore); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeBefore); // in case removing type changed index + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type after another type. + /// + /// The other type. + /// The type to append. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertAfter() + where TAfter : TItem + where T : TItem + { + Configure(types => + { + Type typeAfter = typeof(TAfter); + Type type = typeof(T); + if (typeAfter == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeAfter); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeAfter); // in case removing type changed index + index += 1; // insert here + + if (index == types.Count) + { + types.Add(type); + } + else { - var type = typeof (T); - if (types.Contains(type)) types.Remove(type); types.Insert(index, type); - }); - return This; - } + } + }); + return This; + } - /// - /// Inserts a type into the collection. - /// - /// The type to insert. - /// The builder. - /// Throws if the index is out of range. - public TBuilder Insert(Type type) + /// + /// Inserts a type after another type. + /// + /// The other type. + /// The type to insert. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertAfter(Type typeAfter, Type type) + { + Configure(types => { - return Insert(0, type); - } + EnsureType(typeAfter, "find"); + EnsureType(type, "register"); - /// - /// Inserts a type into the collection. - /// - /// The index. - /// The type to insert. - /// The builder. - /// Throws if the index is out of range. - public TBuilder Insert(int index, Type type) - { - Configure(types => + if (typeAfter == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeAfter); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeAfter); // in case removing type changed index + index += 1; // insert here + + if (index == types.Count) + { + types.Add(type); + } + else { - EnsureType(type, "register"); - if (types.Contains(type)) types.Remove(type); types.Insert(index, type); - }); - return This; - } + } + }); + return This; + } - /// - /// Inserts a type before another type. - /// - /// The other type. - /// The type to insert. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertBefore() - where TBefore : TItem - where T : TItem + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove() + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var typeBefore = typeof(TBefore); - var type = typeof(T); - if (typeBefore == type) throw new InvalidOperationException(); + types.Remove(type); + } + }); + return This; + } - var index = types.IndexOf(typeBefore); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeBefore); // in case removing type changed index - types.Insert(index, type); - }); - return This; - } - - /// - /// Inserts a type before another type. - /// - /// The other type. - /// The type to insert. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertBefore(Type typeBefore, Type type) + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "remove"); + if (types.Contains(type)) { - EnsureType(typeBefore, "find"); - EnsureType(type, "register"); + types.Remove(type); + } + }); + return This; + } - if (typeBefore == type) throw new InvalidOperationException(); - - var index = types.IndexOf(typeBefore); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeBefore); // in case removing type changed index - types.Insert(index, type); - }); - return This; - } - - /// - /// Inserts a type after another type. - /// - /// The other type. - /// The type to append. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertAfter() - where TAfter : TItem - where T : TItem + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace() + where TReplaced : TItem + where T : TItem + { + Configure(types => { - Configure(types => + Type typeReplaced = typeof(TReplaced); + Type type = typeof(T); + if (typeReplaced == type) { - var typeAfter = typeof(TAfter); - var type = typeof(T); - if (typeAfter == type) throw new InvalidOperationException(); + return; + } - var index = types.IndexOf(typeAfter); - if (index < 0) throw new InvalidOperationException(); + var index = types.IndexOf(typeReplaced); + if (index < 0) + { + throw new InvalidOperationException(); + } - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeAfter); // in case removing type changed index - index += 1; // insert here + if (types.Contains(type)) + { + types.Remove(type); + } - if (index == types.Count) - types.Add(type); - else - types.Insert(index, type); - }); - return This; - } + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; + } - /// - /// Inserts a type after another type. - /// - /// The other type. - /// The type to insert. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertAfter(Type typeAfter, Type type) + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace(Type typeReplaced, Type type) + { + Configure(types => { - Configure(types => + EnsureType(typeReplaced, "find"); + EnsureType(type, "register"); + + if (typeReplaced == type) { - EnsureType(typeAfter, "find"); - EnsureType(type, "register"); + return; + } - if (typeAfter == type) throw new InvalidOperationException(); - - var index = types.IndexOf(typeAfter); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeAfter); // in case removing type changed index - index += 1; // insert here - - if (index == types.Count) - types.Add(type); - else - types.Insert(index, type); - }); - return This; - } - - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem - { - Configure(types => + var index = types.IndexOf(typeReplaced); + if (index < 0) { - var type = typeof (T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + throw new InvalidOperationException(); + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) - { - Configure(types => + if (types.Contains(type)) { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace() - where TReplaced : TItem - where T : TItem - { - Configure(types => - { - var typeReplaced = typeof(TReplaced); - var type = typeof(T); - if (typeReplaced == type) return; - - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } - - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace(Type typeReplaced, Type type) - { - Configure(types => - { - EnsureType(typeReplaced, "find"); - EnsureType(type, "register"); - - if (typeReplaced == type) return; - - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; } } diff --git a/src/Umbraco.Core/Composing/ReferenceResolver.cs b/src/Umbraco.Core/Composing/ReferenceResolver.cs index 5b7c5ffde9..1924fb4b75 100644 --- a/src/Umbraco.Core/Composing/ReferenceResolver.cs +++ b/src/Umbraco.Core/Composing/ReferenceResolver.cs @@ -1,195 +1,199 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Reflection; using System.Security; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively. +/// +/// +/// Borrowed and modified from +/// https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs +/// +internal class ReferenceResolver { - /// - /// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively. - /// - /// - /// Borrowed and modified from https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs - /// - internal class ReferenceResolver + private readonly IReadOnlyList _assemblies; + private readonly Dictionary _classifications; + private readonly ILogger _logger; + private readonly List _lookup = new(); + private readonly HashSet _umbracoAssemblies; + + public ReferenceResolver(IReadOnlyList targetAssemblies, IReadOnlyList entryPointAssemblies, ILogger logger) { - private readonly HashSet _umbracoAssemblies; - private readonly IReadOnlyList _assemblies; - private readonly Dictionary _classifications; - private readonly List _lookup = new List(); - private readonly ILogger _logger; - public ReferenceResolver(IReadOnlyList targetAssemblies, IReadOnlyList entryPointAssemblies, ILogger logger) - { - _umbracoAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal); - _assemblies = entryPointAssemblies; - _logger = logger; - _classifications = new Dictionary(); + _umbracoAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal); + _assemblies = entryPointAssemblies; + _logger = logger; + _classifications = new Dictionary(); - foreach (var item in entryPointAssemblies) - { - _lookup.Add(item); - } + foreach (Assembly item in entryPointAssemblies) + { + _lookup.Add(item); } + } - /// - /// Returns a list of assemblies that directly reference or transitively reference the targetAssemblies - /// - /// - /// - /// This includes all assemblies in the same location as the entry point assemblies - /// - public IEnumerable ResolveAssemblies() + protected enum Classification + { + Unknown, + DoesNotReferenceUmbraco, + ReferencesUmbraco, + IsUmbraco, + } + + /// + /// Returns a list of assemblies that directly reference or transitively reference the targetAssemblies + /// + /// + /// + /// This includes all assemblies in the same location as the entry point assemblies + /// + public IEnumerable ResolveAssemblies() + { + var applicationParts = new List(); + + var assemblies = new HashSet(_assemblies); + + // Get the unique directories of the assemblies + var assemblyLocations = GetAssemblyFolders(assemblies).ToList(); + + // Load in each assembly in the directory of the entry assembly to be included in the search + // for Umbraco dependencies/transitive dependencies + foreach (var dir in assemblyLocations) { - var applicationParts = new List(); - - var assemblies = new HashSet(_assemblies); - - // Get the unique directories of the assemblies - var assemblyLocations = GetAssemblyFolders(assemblies).ToList(); - - // Load in each assembly in the directory of the entry assembly to be included in the search - // for Umbraco dependencies/transitive dependencies - foreach(var dir in assemblyLocations) + foreach (var dll in Directory.EnumerateFiles(dir ?? string.Empty, "*.dll")) { - foreach(var dll in Directory.EnumerateFiles(dir ?? string.Empty, "*.dll")) + AssemblyName? assemblyName = null; + try { - AssemblyName? assemblyName = null; - try - { - assemblyName = AssemblyName.GetAssemblyName(dll); - } - catch (BadImageFormatException e) - { - _logger.LogDebug(e, "Could not load {dll} for type scanning, skipping", dll); - } - catch (SecurityException e) - { - _logger.LogError(e, "Could not access {dll} for type scanning due to a security problem", dll); - } - catch (Exception e) - { - _logger.LogInformation(e, "Error: could not load {dll} for type scanning", dll); - } + assemblyName = AssemblyName.GetAssemblyName(dll); + } + catch (BadImageFormatException e) + { + _logger.LogDebug(e, "Could not load {dll} for type scanning, skipping", dll); + } + catch (SecurityException e) + { + _logger.LogError(e, "Could not access {dll} for type scanning due to a security problem", dll); + } + catch (Exception e) + { + _logger.LogInformation(e, "Error: could not load {dll} for type scanning", dll); + } - if (assemblyName != null) - { - // don't include if this is excluded - if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => + if (assemblyName != null) + { + // don't include if this is excluded + if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => assemblyName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) - continue; - - // don't include this item if it's Umbraco Core - if (Constants.Composing.UmbracoCoreAssemblyNames.Any(x=>assemblyName.FullName.StartsWith(x) || (assemblyName.Name?.EndsWith(".Views") ?? false))) - continue; - - var assembly = Assembly.Load(assemblyName); - assemblies.Add(assembly); - } - } - } - - foreach (var item in assemblies) - { - var classification = Resolve(item); - if (classification == Classification.ReferencesUmbraco || classification == Classification.IsUmbraco) - { - applicationParts.Add(item); - } - } - - return applicationParts; - } - - - private IEnumerable GetAssemblyFolders(IEnumerable assemblies) - { - return assemblies.Select(x => Path.GetDirectoryName(GetAssemblyLocation(x))).Distinct(); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs - private string GetAssemblyLocation(Assembly assembly) - { - if (Uri.TryCreate(assembly.CodeBase, UriKind.Absolute, out var result) && - result.IsFile && string.IsNullOrWhiteSpace(result.Fragment)) - { - return result.LocalPath; - } - - return assembly.Location; - } - - private Classification Resolve(Assembly assembly) - { - if (_classifications.TryGetValue(assembly, out var classification)) - { - return classification; - } - - // Initialize the dictionary with a value to short-circuit recursive references. - classification = Classification.Unknown; - _classifications[assembly] = classification; - - if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => assembly.FullName?.StartsWith(f, StringComparison.InvariantCultureIgnoreCase) ?? false)) - { - // if its part of the filter it doesn't reference umbraco - classification = Classification.DoesNotReferenceUmbraco; - } - else if (_umbracoAssemblies.Contains(assembly.GetName().Name!)) - { - classification = Classification.IsUmbraco; - } - else - { - classification = Classification.DoesNotReferenceUmbraco; - foreach (var reference in GetReferences(assembly)) - { - // recurse - var referenceClassification = Resolve(reference); - - if (referenceClassification == Classification.IsUmbraco || referenceClassification == Classification.ReferencesUmbraco) { - classification = Classification.ReferencesUmbraco; - break; + continue; } + + // don't include this item if it's Umbraco Core + if (Constants.Composing.UmbracoCoreAssemblyNames.Any(x => + assemblyName.FullName.StartsWith(x) || (assemblyName.Name?.EndsWith(".Views") ?? false))) + { + continue; + } + + var assembly = Assembly.Load(assemblyName); + assemblies.Add(assembly); } } + } - Debug.Assert(classification != Classification.Unknown); - _classifications[assembly] = classification; + foreach (Assembly item in assemblies) + { + Classification classification = Resolve(item); + if (classification == Classification.ReferencesUmbraco || classification == Classification.IsUmbraco) + { + applicationParts.Add(item); + } + } + + return applicationParts; + } + + protected virtual IEnumerable GetReferences(Assembly assembly) + { + foreach (AssemblyName referenceName in assembly.GetReferencedAssemblies()) + { + // don't include if this is excluded + if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => + referenceName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) + { + continue; + } + + var reference = Assembly.Load(referenceName); + + if (!_lookup.Contains(reference)) + { + // A dependency references an item that isn't referenced by this project. + // We'll add this reference so that we can calculate the classification. + _lookup.Add(reference); + } + + yield return reference; + } + } + + private IEnumerable GetAssemblyFolders(IEnumerable assemblies) => + assemblies.Select(x => Path.GetDirectoryName(GetAssemblyLocation(x))).Distinct(); + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs + private string GetAssemblyLocation(Assembly assembly) + { + if (Uri.TryCreate(assembly.Location, UriKind.Absolute, out Uri? result) && + result.IsFile && string.IsNullOrWhiteSpace(result.Fragment)) + { + return result.LocalPath; + } + + return assembly.Location; + } + + private Classification Resolve(Assembly assembly) + { + if (_classifications.TryGetValue(assembly, out Classification classification)) + { return classification; } - protected virtual IEnumerable GetReferences(Assembly assembly) + // Initialize the dictionary with a value to short-circuit recursive references. + classification = Classification.Unknown; + _classifications[assembly] = classification; + + if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => + assembly.FullName?.StartsWith(f, StringComparison.InvariantCultureIgnoreCase) ?? false)) { - foreach (var referenceName in assembly.GetReferencedAssemblies()) + // if its part of the filter it doesn't reference umbraco + classification = Classification.DoesNotReferenceUmbraco; + } + else if (_umbracoAssemblies.Contains(assembly.GetName().Name!)) + { + classification = Classification.IsUmbraco; + } + else + { + classification = Classification.DoesNotReferenceUmbraco; + foreach (Assembly reference in GetReferences(assembly)) { - // don't include if this is excluded - if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => referenceName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) - continue; + // recurse + Classification referenceClassification = Resolve(reference); - var reference = Assembly.Load(referenceName); - - if (!_lookup.Contains(reference)) + if (referenceClassification == Classification.IsUmbraco || + referenceClassification == Classification.ReferencesUmbraco) { - // A dependency references an item that isn't referenced by this project. - // We'll add this reference so that we can calculate the classification. - - _lookup.Add(reference); + classification = Classification.ReferencesUmbraco; + break; } - yield return reference; } } - protected enum Classification - { - Unknown, - DoesNotReferenceUmbraco, - ReferencesUmbraco, - IsUmbraco, - } + Debug.Assert(classification != Classification.Unknown); + _classifications[assembly] = classification; + return classification; } } diff --git a/src/Umbraco.Core/Composing/RuntimeHash.cs b/src/Umbraco.Core/Composing/RuntimeHash.cs index 5e0523f09d..e66bedf79f 100644 --- a/src/Umbraco.Core/Composing/RuntimeHash.cs +++ b/src/Umbraco.Core/Composing/RuntimeHash.cs @@ -1,93 +1,89 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; using Umbraco.Cms.Core.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Determines the runtime hash based on file system paths to scan +/// +public class RuntimeHash : IRuntimeHash { - /// - /// Determines the runtime hash based on file system paths to scan - /// - public class RuntimeHash : IRuntimeHash + private readonly IProfilingLogger _logger; + private readonly RuntimeHashPaths _paths; + private string? _calculated; + + public RuntimeHash(IProfilingLogger logger, RuntimeHashPaths paths) { - private readonly IProfilingLogger _logger; - private readonly RuntimeHashPaths _paths; - private string? _calculated; + _logger = logger; + _paths = paths; + } - public RuntimeHash(IProfilingLogger logger, RuntimeHashPaths paths) + public string GetHashValue() + { + if (_calculated != null) { - _logger = logger; - _paths = paths; - } - - - public string GetHashValue() - { - if (_calculated != null) - { - return _calculated; - } - - IEnumerable<(FileSystemInfo, bool)> allPaths = _paths.GetFolders() - .Select(x => ((FileSystemInfo)x, false)) - .Concat(_paths.GetFiles().Select(x => ((FileSystemInfo)x.Key, x.Value))); - - _calculated = GetFileHash(allPaths); - return _calculated; } - /// - /// Returns a unique hash for a combination of FileInfo objects. - /// - /// A collection of files. - /// The hash. - /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the - /// file properties (false) or the file contents (true). - private string GetFileHash(IEnumerable<(FileSystemInfo fileOrFolder, bool scanFileContent)> filesAndFolders) + IEnumerable<(FileSystemInfo, bool)> allPaths = _paths.GetFolders() + .Select(x => ((FileSystemInfo)x, false)) + .Concat(_paths.GetFiles().Select(x => ((FileSystemInfo)x.Key, x.Value))); + + _calculated = GetFileHash(allPaths); + + return _calculated; + } + + /// + /// Returns a unique hash for a combination of FileInfo objects. + /// + /// A collection of files. + /// The hash. + /// + /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the + /// file properties (false) or the file contents (true). + /// + private string GetFileHash(IEnumerable<(FileSystemInfo fileOrFolder, bool scanFileContent)> filesAndFolders) + { + using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) { - using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + var uniqContent = new HashSet(); + + using var generator = new HashGenerator(); + + foreach ((FileSystemInfo fileOrFolder, var scanFileContent) in filesAndFolders) { - // get the distinct file infos to hash - var uniqInfos = new HashSet(); - var uniqContent = new HashSet(); - - using var generator = new HashGenerator(); - - foreach ((FileSystemInfo fileOrFolder, bool scanFileContent) in filesAndFolders) + if (scanFileContent) { - if (scanFileContent) + // add each unique file's contents to the hash + // normalize the content for cr/lf and case-sensitivity + if (uniqContent.Add(fileOrFolder.FullName)) { - // add each unique file's contents to the hash - // normalize the content for cr/lf and case-sensitivity - if (uniqContent.Add(fileOrFolder.FullName)) + if (File.Exists(fileOrFolder.FullName) == false) { - if (File.Exists(fileOrFolder.FullName) == false) - { - continue; - } - - using (FileStream fileStream = File.OpenRead(fileOrFolder.FullName)) - { - var hash = fileStream.GetStreamHash(); - generator.AddCaseInsensitiveString(hash); - } + continue; } - } - else - { - // add each unique folder/file to the hash - if (uniqInfos.Add(fileOrFolder.FullName)) + + using (FileStream fileStream = File.OpenRead(fileOrFolder.FullName)) { - generator.AddFileSystemItem(fileOrFolder); + var hash = fileStream.GetStreamHash(); + generator.AddCaseInsensitiveString(hash); } } } - return generator.GenerateHash(); + else + { + // add each unique folder/file to the hash + if (uniqInfos.Add(fileOrFolder.FullName)) + { + generator.AddFileSystemItem(fileOrFolder); + } + } } - } + return generator.GenerateHash(); + } } } diff --git a/src/Umbraco.Core/Composing/RuntimeHashPaths.cs b/src/Umbraco.Core/Composing/RuntimeHashPaths.cs index eac2f83bcd..5720fdebe2 100644 --- a/src/Umbraco.Core/Composing/RuntimeHashPaths.cs +++ b/src/Umbraco.Core/Composing/RuntimeHashPaths.cs @@ -1,44 +1,43 @@ -using System.Collections.Generic; -using System.IO; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Paths used to determine the +/// +public sealed class RuntimeHashPaths { - /// - /// Paths used to determine the - /// - public sealed class RuntimeHashPaths + private readonly Dictionary _files = new(); + private readonly List _paths = new(); + + public RuntimeHashPaths AddFolder(DirectoryInfo pathInfo) { - private readonly List _paths = new List(); - private readonly Dictionary _files = new Dictionary(); - - public RuntimeHashPaths AddFolder(DirectoryInfo pathInfo) - { - _paths.Add(pathInfo); - return this; - } - - /// - /// Creates a runtime hash based on the assembly provider - /// - /// - /// - public RuntimeHashPaths AddAssemblies(IAssemblyProvider assemblyProvider) - { - foreach (Assembly assembly in assemblyProvider.Assemblies) - { - // TODO: We need to test this on a published website - if (!assembly.IsDynamic && assembly.Location != null) - { - AddFile(new FileInfo(assembly.Location)); - } - } - return this; - } - - public void AddFile(FileInfo fileInfo, bool scanFileContent = false) => _files.Add(fileInfo, scanFileContent); - - public IEnumerable GetFolders() => _paths; - public IReadOnlyDictionary GetFiles() => _files; + _paths.Add(pathInfo); + return this; } + + /// + /// Creates a runtime hash based on the assembly provider + /// + /// + /// + public RuntimeHashPaths AddAssemblies(IAssemblyProvider assemblyProvider) + { + foreach (Assembly assembly in assemblyProvider.Assemblies) + { + // TODO: We need to test this on a published website + if (!assembly.IsDynamic && assembly.Location != null) + { + AddFile(new FileInfo(assembly.Location)); + } + } + + return this; + } + + public void AddFile(FileInfo fileInfo, bool scanFileContent = false) => _files.Add(fileInfo, scanFileContent); + + public IEnumerable GetFolders() => _paths; + + public IReadOnlyDictionary GetFiles() => _files; } diff --git a/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs index 358aab75dd..b686067d30 100644 --- a/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs @@ -1,171 +1,207 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements an un-ordered collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +/// +/// +/// A set collection builder is the most basic collection builder, +/// where items are not ordered. +/// +/// +public abstract class SetCollectionBuilderBase : CollectionBuilderBase + where TBuilder : SetCollectionBuilderBase + where TCollection : class, IBuilderCollection { + protected abstract TBuilder This { get; } + /// - /// Implements an un-ordered collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - /// - /// A set collection builder is the most basic collection builder, - /// where items are not ordered. - /// - public abstract class SetCollectionBuilderBase : CollectionBuilderBase - where TBuilder : SetCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - protected abstract TBuilder This { get; } + Configure(types => types.Clear()); + return This; + } - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + /// + /// Adds a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => { - Configure(types => types.Clear()); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Add() - where T : TItem - { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } + types.Remove(type); + } - /// - /// Adds a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Add(Type type) + types.Add(type); + }); + return This; + } + + /// + /// Adds a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Add(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "register"); + if (types.Contains(type)) { + types.Remove(type); + } + + types.Add(type); + }); + return This; + } + + /// + /// Adds types to the collections. + /// + /// The types to append. + /// The builder. + public TBuilder Add(IEnumerable types) + { + Configure(list => + { + foreach (Type type in types) + { + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast EnsureType(type, "register"); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } - - /// - /// Adds types to the collections. - /// - /// The types to append. - /// The builder. - public TBuilder Add(IEnumerable types) - { - Configure(list => - { - foreach (var type in types) + if (list.Contains(type)) { - // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast - EnsureType(type, "register"); - if (list.Contains(type)) list.Remove(type); - list.Add(type); + list.Remove(type); } - }); - return This; - } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem + list.Add(type); + } + }); + return This; + } + + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove() + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "remove"); + if (types.Contains(type)) { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace() - where TReplaced : TItem - where T : TItem + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace() + where TReplaced : TItem + where T : TItem + { + Configure(types => { - Configure(types => + Type typeReplaced = typeof(TReplaced); + Type type = typeof(T); + if (typeReplaced == type) { - var typeReplaced = typeof(TReplaced); - var type = typeof(T); - if (typeReplaced == type) return; + return; + } - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); + var index = types.IndexOf(typeReplaced); + if (index < 0) + { + throw new InvalidOperationException(); + } - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } + if (types.Contains(type)) + { + types.Remove(type); + } - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace(Type typeReplaced, Type type) + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; + } + + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace(Type typeReplaced, Type type) + { + Configure(types => { - Configure(types => + EnsureType(typeReplaced, "find"); + EnsureType(type, "register"); + + if (typeReplaced == type) { - EnsureType(typeReplaced, "find"); - EnsureType(type, "register"); + return; + } - if (typeReplaced == type) return; + var index = types.IndexOf(typeReplaced); + if (index < 0) + { + throw new InvalidOperationException(); + } - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); + if (types.Contains(type)) + { + types.Remove(type); + } - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; } } diff --git a/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs index 40ce3d8a46..072a9d99e3 100644 --- a/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs @@ -1,69 +1,71 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for collections of types. +/// +public abstract class + TypeCollectionBuilderBase : ICollectionBuilder + where TBuilder : TypeCollectionBuilderBase + where TCollection : class, IBuilderCollection { - /// - /// Provides a base class for collections of types. - /// - public abstract class TypeCollectionBuilderBase : ICollectionBuilder - where TBuilder : TypeCollectionBuilderBase - where TCollection : class, IBuilderCollection + private readonly HashSet _types = new(); + + protected abstract TBuilder This { get; } + + public TCollection CreateCollection(IServiceProvider factory) + => factory.CreateInstance(CreateItemsFactory()); + + public void RegisterWith(IServiceCollection services) + => services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); + + public TBuilder Add(Type type) { - private readonly HashSet _types = new HashSet(); - - protected abstract TBuilder This { get; } - - private static Type Validate(Type type, string action) - { - if (!typeof(TConstraint).IsAssignableFrom(type)) - throw new InvalidOperationException($"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TConstraint).FullName}."); - return type; - } - - public TBuilder Add(Type type) - { - _types.Add(Validate(type, "add")); - return This; - } - - public TBuilder Add() - { - Add(typeof(T)); - return This; - } - - public TBuilder Add(IEnumerable types) - { - foreach (var type in types) - { - Add(type); - } - - return This; - } - - public TBuilder Remove(Type type) - { - _types.Remove(Validate(type, "remove")); - return This; - } - - public TBuilder Remove() - { - Remove(typeof(T)); - return This; - } - - public TCollection CreateCollection(IServiceProvider factory) - => factory.CreateInstance(CreateItemsFactory()); - - public void RegisterWith(IServiceCollection services) - => services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); - - // used to resolve a Func> parameter - private Func> CreateItemsFactory() => () => _types; + _types.Add(Validate(type, "add")); + return This; } + + private static Type Validate(Type type, string action) + { + if (!typeof(TConstraint).IsAssignableFrom(type)) + { + throw new InvalidOperationException( + $"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TConstraint).FullName}."); + } + + return type; + } + + public TBuilder Add() + { + Add(typeof(T)); + return This; + } + + public TBuilder Add(IEnumerable types) + { + foreach (Type type in types) + { + Add(type); + } + + return This; + } + + public TBuilder Remove(Type type) + { + _types.Remove(Validate(type, "remove")); + return This; + } + + public TBuilder Remove() + { + Remove(typeof(T)); + return This; + } + + // used to resolve a Func> parameter + private Func> CreateItemsFactory() => () => _types; } diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index dfeac6a731..3ac826880c 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Security; using System.Text; @@ -9,495 +6,502 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +public class TypeFinder : ITypeFinder { + // TODO: Kill this - /// - public class TypeFinder : ITypeFinder + /// + /// this is our assembly filter to filter out known types that def don't contain types we'd like to find or plugins + /// + /// + /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then + /// its an exact name match + /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" + /// + internal static readonly string[] KnownAssemblyExclusionFilter = { - private readonly ILogger _logger; - private readonly IAssemblyProvider _assemblyProvider; - private volatile HashSet? _localFilteredAssemblyCache; - private readonly object _localFilteredAssemblyCacheLocker = new object(); - private readonly List _notifiedLoadExceptionAssemblies = new List(); - private static readonly ConcurrentDictionary s_typeNamesCache = new ConcurrentDictionary(); + "mscorlib,", "netstandard,", "System,", "Antlr3.", "AutoMapper,", "AutoMapper.", "Autofac,", // DI + "Autofac.", "AzureDirectory,", "Castle.", // DI, tests + "ClientDependency.", "CookComputing.", "CSharpTest.", // BTree for NuCache + "DataAnnotationsExtensions,", "DataAnnotationsExtensions.", "Dynamic,", "Examine,", "Examine.", + "HtmlAgilityPack,", "HtmlAgilityPack.", "HtmlDiff,", "ICSharpCode.", "Iesi.Collections,", // used by NHibernate + "JetBrains.Annotations,", "LightInject.", // DI + "LightInject,", "Lucene.", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,", + "NHibernate.", "Newtonsoft.", "NPoco,", "NuGet.", "RouteDebugger,", "Semver.", "Serilog.", "Serilog,", + "ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog + "System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.", + "Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite", + "ReSharperTestRunner32", // used by resharper testrunner + }; - private readonly ITypeFinderConfig? _typeFinderConfig; - // used for benchmark tests - internal bool QueryWithReferencingAssemblies { get; set; } = true; + private static readonly ConcurrentDictionary TypeNamesCache = new(); - public TypeFinder(ILogger logger, IAssemblyProvider assemblyProvider, ITypeFinderConfig? typeFinderConfig = null) + private readonly IAssemblyProvider _assemblyProvider; + private readonly object _localFilteredAssemblyCacheLocker = new(); + private readonly ILogger _logger; + private readonly List _notifiedLoadExceptionAssemblies = new(); + + private readonly ITypeFinderConfig? _typeFinderConfig; + + private string[]? _assembliesAcceptingLoadExceptions; + private volatile HashSet? _localFilteredAssemblyCache; + + public TypeFinder(ILogger logger, IAssemblyProvider assemblyProvider, ITypeFinderConfig? typeFinderConfig = null) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _assemblyProvider = assemblyProvider; + _typeFinderConfig = typeFinderConfig; + } + + /// + public IEnumerable AssembliesToScan + { + get { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _assemblyProvider = assemblyProvider; - _typeFinderConfig = typeFinderConfig; - } - - private string[]? _assembliesAcceptingLoadExceptions = null; - - private string[] AssembliesAcceptingLoadExceptions - { - get + lock (_localFilteredAssemblyCacheLocker) { - if (_assembliesAcceptingLoadExceptions is not null) + if (_localFilteredAssemblyCache != null) { - return _assembliesAcceptingLoadExceptions; - } - - _assembliesAcceptingLoadExceptions = - _typeFinderConfig?.AssembliesAcceptingLoadExceptions.Where(x => !x.IsNullOrWhiteSpace()).ToArray() ?? - Array.Empty(); - - return _assembliesAcceptingLoadExceptions; - } - } - - - private bool AcceptsLoadExceptions(Assembly a) - { - if (AssembliesAcceptingLoadExceptions.Length == 0) - return false; - if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") - return true; - var name = a.GetName().Name; // simple name of the assembly - return AssembliesAcceptingLoadExceptions.Any(pattern => - { - if (pattern.Length > name?.Length) - return false; // pattern longer than name - if (pattern.Length == name?.Length) - return pattern.InvariantEquals(name); // same length, must be identical - if (pattern[pattern.Length] != '.') - return false; // pattern is shorter than name, must end with dot - return name?.StartsWith(pattern) ?? false; // and name must start with pattern - }); - } - - - private IEnumerable GetAllAssemblies() => _assemblyProvider.Assemblies; - - /// - public IEnumerable AssembliesToScan - { - get - { - lock (_localFilteredAssemblyCacheLocker) - { - if (_localFilteredAssemblyCache != null) - return _localFilteredAssemblyCache; - - var assemblies = GetFilteredAssemblies(null, KnownAssemblyExclusionFilter); - _localFilteredAssemblyCache = new HashSet(assemblies); return _localFilteredAssemblyCache; } + + IEnumerable assemblies = GetFilteredAssemblies(null, KnownAssemblyExclusionFilter); + _localFilteredAssemblyCache = new HashSet(assemblies); + return _localFilteredAssemblyCache; } } - - /// - /// Return a distinct list of found local Assemblies and excluding the ones passed in and excluding the exclusion list filter - /// - /// - /// - /// - private IEnumerable GetFilteredAssemblies( - IEnumerable? excludeFromResults = null, - string[]? exclusionFilter = null) - { - if (excludeFromResults == null) - excludeFromResults = new HashSet(); - if (exclusionFilter == null) - exclusionFilter = new string[] { }; - - return GetAllAssemblies() - .Where(x => excludeFromResults.Contains(x) == false - && x.GlobalAssemblyCache == false - && exclusionFilter.Any(f => x.FullName?.StartsWith(f) ?? false) == false); - } - - // TODO: Kill this - - /// - /// this is our assembly filter to filter out known types that def don't contain types we'd like to find or plugins - /// - /// - /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match - /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" - /// - internal static readonly string[] KnownAssemblyExclusionFilter = { - "mscorlib,", - "netstandard,", - "System,", - "Antlr3.", - "AutoMapper,", - "AutoMapper.", - "Autofac,", // DI - "Autofac.", - "AzureDirectory,", - "Castle.", // DI, tests - "ClientDependency.", - "CookComputing.", - "CSharpTest.", // BTree for NuCache - "DataAnnotationsExtensions,", - "DataAnnotationsExtensions.", - "Dynamic,", - "Examine,", - "Examine.", - "HtmlAgilityPack,", - "HtmlAgilityPack.", - "HtmlDiff,", - "ICSharpCode.", - "Iesi.Collections,", // used by NHibernate - "JetBrains.Annotations,", - "LightInject.", // DI - "LightInject,", - "Lucene.", - "Markdown,", - "Microsoft.", - "MiniProfiler,", - "Moq,", - "MySql.", - "NHibernate,", - "NHibernate.", - "Newtonsoft.", - "NPoco,", - "NuGet.", - "RouteDebugger,", - "Semver.", - "Serilog.", - "Serilog,", - "ServiceStack.", - "SqlCE4Umbraco,", - "Superpower,", // used by Serilog - "System.", - "TidyNet,", - "TidyNet.", - "WebDriver,", - "itextsharp,", - "mscorlib,", - "NUnit,", - "NUnit.", - "NUnit3.", - "Selenium.", - "ImageProcessor", - "MiniProfiler.", - "Owin,", - "SQLite", - "ReSharperTestRunner32" // used by resharper testrunner - }; - - /// - /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute - /// - /// - /// - /// - /// - /// - public IEnumerable FindClassesOfTypeWithAttribute( - Type assignTypeFrom, - Type attributeType, - IEnumerable? assemblies = null, - bool onlyConcreteClasses = true) - { - var assemblyList = assemblies ?? AssembliesToScan; - - return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses, - //the additional filter will ensure that any found types also have the attribute applied. - t => t.GetCustomAttributes(attributeType, false).Any()); - } - - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - /// - public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - { - var assemblyList = assemblies ?? AssembliesToScan; - - return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses); - } - - /// - /// Finds any classes with the attribute. - /// - /// The attribute type - /// The assemblies. - /// if set to true only concrete classes. - /// - public IEnumerable FindClassesWithAttribute( - Type attributeType, - IEnumerable? assemblies = null, - bool onlyConcreteClasses = true) - { - var assemblyList = assemblies ?? AssembliesToScan; - - return GetClassesWithAttribute(attributeType, assemblyList, onlyConcreteClasses); - } - - /// - /// Returns a Type for the string type name - /// - /// - /// - public virtual Type? GetTypeByName(string name) - { - - //NOTE: This will not find types in dynamic assemblies unless those assemblies are already loaded - //into the appdomain. - - - // This is exactly what the BuildManager does, if the type is an assembly qualified type - // name it will find it. - if (TypeNameContainsAssembly(name)) - { - return Type.GetType(name); - } - - // It didn't parse, so try loading from each already loaded assembly and cache it - return s_typeNamesCache.GetOrAdd(name, s => - AppDomain.CurrentDomain.GetAssemblies() - .Select(x => x.GetType(s)) - .FirstOrDefault(x => x != null)); - } - - #region Private methods - - // borrowed from aspnet System.Web.UI.Util - private static bool TypeNameContainsAssembly(string typeName) - { - return CommaIndexInTypeName(typeName) > 0; - } - - // borrowed from aspnet System.Web.UI.Util - private static int CommaIndexInTypeName(string typeName) - { - var num1 = typeName.LastIndexOf(','); - if (num1 < 0) - return -1; - var num2 = typeName.LastIndexOf(']'); - if (num2 > num1) - return -1; - return typeName.IndexOf(',', num2 + 1); - } - - private IEnumerable GetClassesWithAttribute( - Type attributeType, - IEnumerable assemblies, - bool onlyConcreteClasses) - { - if (typeof(Attribute).IsAssignableFrom(attributeType) == false) - throw new ArgumentException("Type " + attributeType + " is not an Attribute type."); - - var candidateAssemblies = new HashSet(assemblies); - var attributeAssemblyIsCandidate = candidateAssemblies.Contains(attributeType.Assembly); - candidateAssemblies.Remove(attributeType.Assembly); - var types = new List(); - - var stack = new Stack(); - stack.Push(attributeType.Assembly); - - if (!QueryWithReferencingAssemblies) - { - foreach (var a in candidateAssemblies) - stack.Push(a); - } - - while (stack.Count > 0) - { - var assembly = stack.Pop(); - - IReadOnlyList? assemblyTypes = null; - if (assembly != attributeType.Assembly || attributeAssemblyIsCandidate) - { - // get all assembly types that can be assigned to baseType - try - { - assemblyTypes = GetTypesWithFormattedException(assembly) - .ToList(); // in try block - } - catch (TypeLoadException ex) - { - _logger.LogError(ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); - continue; - } - - types.AddRange(assemblyTypes.Where(x => - x.IsClass // only classes - && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed - && x.IsNestedPrivate == false // exclude nested private - && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract - && x.GetCustomAttribute() == null // exclude hidden - && x.GetCustomAttributes(attributeType, false).Any())); // marked with the attribute - } - - if (assembly != attributeType.Assembly && assemblyTypes?.Where(attributeType.IsAssignableFrom).Any() == false) - continue; - - if (QueryWithReferencingAssemblies) - { - foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) - { - candidateAssemblies.Remove(referencing); - stack.Push(referencing); - } - } - } - - return types; - } - - /// - /// Finds types that are assignable from the assignTypeFrom parameter and will scan for these types in the assembly - /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type - /// deriving from the base type. - /// - /// - /// - /// - /// An additional filter to apply for what types will actually be included in the return value - /// - private IEnumerable GetClassesWithBaseType( - Type baseType, - IEnumerable assemblies, - bool onlyConcreteClasses, - Func? additionalFilter = null) - { - var candidateAssemblies = new HashSet(assemblies); - var baseTypeAssemblyIsCandidate = candidateAssemblies.Contains(baseType.Assembly); - candidateAssemblies.Remove(baseType.Assembly); - var types = new List(); - - var stack = new Stack(); - stack.Push(baseType.Assembly); - - if (!QueryWithReferencingAssemblies) - { - foreach (var a in candidateAssemblies) - stack.Push(a); - } - - while (stack.Count > 0) - { - var assembly = stack.Pop(); - - // get all assembly types that can be assigned to baseType - IReadOnlyList? assemblyTypes = null; - if (assembly != baseType.Assembly || baseTypeAssemblyIsCandidate) - { - try - { - assemblyTypes = GetTypesWithFormattedException(assembly) - .Where(baseType.IsAssignableFrom) - .ToList(); // in try block - } - catch (TypeLoadException ex) - { - _logger.LogError(ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); - continue; - } - - types.AddRange(assemblyTypes.Where(x => - x.IsClass // only classes - && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed - && x.IsNestedPrivate == false // exclude nested private - && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract - && x.GetCustomAttribute(false) == null // exclude hidden - && (additionalFilter == null || additionalFilter(x)))); // filter - } - - if (assembly != baseType.Assembly && (assemblyTypes?.All(x => x.IsSealed) ?? false)) - continue; - - if (QueryWithReferencingAssemblies) - { - foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) - { - candidateAssemblies.Remove(referencing); - stack.Push(referencing); - } - } - } - - return types; - } - - private IEnumerable GetTypesWithFormattedException(Assembly a) - { - //if the assembly is dynamic, do not try to scan it - if (a.IsDynamic) - return Enumerable.Empty(); - - var getAll = a.GetCustomAttribute() == null; - - try - { - //we need to detect if an assembly is partially trusted, if so we cannot go interrogating all of it's types - //only its exported types, otherwise we'll get exceptions. - return getAll ? a.GetTypes() : a.GetExportedTypes(); - } - catch (TypeLoadException ex) // GetExportedTypes *can* throw TypeLoadException! - { - var sb = new StringBuilder(); - AppendCouldNotLoad(sb, a, getAll); - AppendLoaderException(sb, ex); - - // rethrow as ReflectionTypeLoadException (for consistency) with new message - throw new ReflectionTypeLoadException(new Type[0], new Exception[] { ex }, sb.ToString()); - } - catch (ReflectionTypeLoadException rex) // GetTypes throws ReflectionTypeLoadException - { - var sb = new StringBuilder(); - AppendCouldNotLoad(sb, a, getAll); - foreach (var loaderException in rex.LoaderExceptions.WhereNotNull()) - AppendLoaderException(sb, loaderException); - - var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); - - // rethrow with new message, unless accepted - if (AcceptsLoadExceptions(a) == false) - throw ex; - - // log a warning, and return what we can - lock (_notifiedLoadExceptionAssemblies) - { - if (a.FullName is not null && _notifiedLoadExceptionAssemblies.Contains(a.FullName) == false) - { - _notifiedLoadExceptionAssemblies.Add(a.FullName); - _logger.LogWarning(ex, "Could not load all types from {TypeName}.", a.GetName().Name); - } - } - return rex.Types.WhereNotNull().ToArray(); - } - } - - private static void AppendCouldNotLoad(StringBuilder sb, Assembly a, bool getAll) - { - sb.Append("Could not load "); - sb.Append(getAll ? "all" : "exported"); - sb.Append(" types from \""); - sb.Append(a.FullName); - sb.AppendLine("\" due to LoaderExceptions, skipping:"); - } - - private static void AppendLoaderException(StringBuilder sb, Exception loaderException) - { - sb.Append(". "); - sb.Append(loaderException.GetType().FullName); - - if (loaderException is TypeLoadException tloadex) - { - sb.Append(" on "); - sb.Append(tloadex.TypeName); - } - - sb.Append(": "); - sb.Append(loaderException.Message); - sb.AppendLine(); - } - - #endregion - } + + // used for benchmark tests + internal bool QueryWithReferencingAssemblies { get; set; } = true; + + private string[] AssembliesAcceptingLoadExceptions + { + get + { + if (_assembliesAcceptingLoadExceptions is not null) + { + return _assembliesAcceptingLoadExceptions; + } + + _assembliesAcceptingLoadExceptions = + _typeFinderConfig?.AssembliesAcceptingLoadExceptions.Where(x => !x.IsNullOrWhiteSpace()).ToArray() ?? + Array.Empty(); + + return _assembliesAcceptingLoadExceptions; + } + } + + /// + /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute + /// + /// + /// + /// + /// + /// + public IEnumerable FindClassesOfTypeWithAttribute( + Type assignTypeFrom, + Type attributeType, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + { + IEnumerable assemblyList = assemblies ?? AssembliesToScan; + + return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses, + + // the additional filter will ensure that any found types also have the attribute applied. + t => t.GetCustomAttributes(attributeType, false).Any()); + } + + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) + { + IEnumerable assemblyList = assemblies ?? AssembliesToScan; + + return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses); + } + + /// + /// Finds any classes with the attribute. + /// + /// The attribute type + /// The assemblies. + /// if set to true only concrete classes. + /// + public IEnumerable FindClassesWithAttribute( + Type attributeType, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + { + IEnumerable assemblyList = assemblies ?? AssembliesToScan; + + return GetClassesWithAttribute(attributeType, assemblyList, onlyConcreteClasses); + } + + /// + /// Returns a Type for the string type name + /// + /// + /// + public virtual Type? GetTypeByName(string name) + { + // NOTE: This will not find types in dynamic assemblies unless those assemblies are already loaded + // into the appdomain. + + // This is exactly what the BuildManager does, if the type is an assembly qualified type + // name it will find it. + if (TypeNameContainsAssembly(name)) + { + return Type.GetType(name); + } + + // It didn't parse, so try loading from each already loaded assembly and cache it + return TypeNamesCache.GetOrAdd(name, s => + AppDomain.CurrentDomain.GetAssemblies() + .Select(x => x.GetType(s)) + .FirstOrDefault(x => x != null)); + } + + #region Private methods + + // borrowed from aspnet System.Web.UI.Util + private static bool TypeNameContainsAssembly(string typeName) => CommaIndexInTypeName(typeName) > 0; + + private bool AcceptsLoadExceptions(Assembly a) + { + if (AssembliesAcceptingLoadExceptions.Length == 0) + { + return false; + } + + if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") + { + return true; + } + + var name = a.GetName().Name; // simple name of the assembly + return AssembliesAcceptingLoadExceptions.Any(pattern => + { + if (pattern.Length > name?.Length) + { + return false; // pattern longer than name + } + + if (pattern.Length == name?.Length) + { + return pattern.InvariantEquals(name); // same length, must be identical + } + + if (pattern[pattern.Length] != '.') + { + return false; // pattern is shorter than name, must end with dot + } + + return name?.StartsWith(pattern) ?? false; // and name must start with pattern + }); + } + + private IEnumerable GetAllAssemblies() => _assemblyProvider.Assemblies; + + /// + /// Return a distinct list of found local Assemblies and excluding the ones passed in and excluding the exclusion list + /// filter + /// + /// + /// + /// + private IEnumerable GetFilteredAssemblies( + IEnumerable? excludeFromResults = null, + string[]? exclusionFilter = null) + { + if (excludeFromResults == null) + { + excludeFromResults = new HashSet(); + } + + if (exclusionFilter == null) + { + exclusionFilter = new string[] { }; + } + + return GetAllAssemblies() + .Where(x => excludeFromResults.Contains(x) == false + && exclusionFilter.Any(f => x.FullName?.StartsWith(f) ?? false) == false); + } + + // borrowed from aspnet System.Web.UI.Util + private static int CommaIndexInTypeName(string typeName) + { + var num1 = typeName.LastIndexOf(','); + if (num1 < 0) + { + return -1; + } + + var num2 = typeName.LastIndexOf(']'); + if (num2 > num1) + { + return -1; + } + + return typeName.IndexOf(',', num2 + 1); + } + + private static void AppendCouldNotLoad(StringBuilder sb, Assembly a, bool getAll) + { + sb.Append("Could not load "); + sb.Append(getAll ? "all" : "exported"); + sb.Append(" types from \""); + sb.Append(a.FullName); + sb.AppendLine("\" due to LoaderExceptions, skipping:"); + } + + private IEnumerable GetClassesWithAttribute( + Type attributeType, + IEnumerable assemblies, + bool onlyConcreteClasses) + { + if (typeof(Attribute).IsAssignableFrom(attributeType) == false) + { + throw new ArgumentException("Type " + attributeType + " is not an Attribute type."); + } + + var candidateAssemblies = new HashSet(assemblies); + var attributeAssemblyIsCandidate = candidateAssemblies.Contains(attributeType.Assembly); + candidateAssemblies.Remove(attributeType.Assembly); + var types = new List(); + + var stack = new Stack(); + stack.Push(attributeType.Assembly); + + if (!QueryWithReferencingAssemblies) + { + foreach (Assembly a in candidateAssemblies) + { + stack.Push(a); + } + } + + while (stack.Count > 0) + { + Assembly assembly = stack.Pop(); + + IReadOnlyList? assemblyTypes = null; + if (assembly != attributeType.Assembly || attributeAssemblyIsCandidate) + { + // get all assembly types that can be assigned to baseType + try + { + assemblyTypes = GetTypesWithFormattedException(assembly) + .ToList(); // in try block + } + catch (TypeLoadException ex) + { + _logger.LogError( + ex, + "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", + assembly); + continue; + } + + types.AddRange(assemblyTypes.Where(x => + x.IsClass // only classes + && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed + && x.IsNestedPrivate == false // exclude nested private + && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract + && x.GetCustomAttribute() == null // exclude hidden + && x.GetCustomAttributes(attributeType, false).Any())); // marked with the attribute + } + + if (assembly != attributeType.Assembly && + assemblyTypes?.Where(attributeType.IsAssignableFrom).Any() == false) + { + continue; + } + + if (QueryWithReferencingAssemblies) + { + foreach (Assembly referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) + { + candidateAssemblies.Remove(referencing); + stack.Push(referencing); + } + } + } + + return types; + } + + /// + /// Finds types that are assignable from the assignTypeFrom parameter and will scan for these types in the assembly + /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type + /// deriving from the base type. + /// + /// + /// + /// + /// + /// An additional filter to apply for what types will actually be included in the return + /// value + /// + /// + private IEnumerable GetClassesWithBaseType( + Type baseType, + IEnumerable assemblies, + bool onlyConcreteClasses, + Func? additionalFilter = null) + { + var candidateAssemblies = new HashSet(assemblies); + var baseTypeAssemblyIsCandidate = candidateAssemblies.Contains(baseType.Assembly); + candidateAssemblies.Remove(baseType.Assembly); + var types = new List(); + + var stack = new Stack(); + stack.Push(baseType.Assembly); + + if (!QueryWithReferencingAssemblies) + { + foreach (Assembly a in candidateAssemblies) + { + stack.Push(a); + } + } + + while (stack.Count > 0) + { + Assembly assembly = stack.Pop(); + + // get all assembly types that can be assigned to baseType + IReadOnlyList? assemblyTypes = null; + if (assembly != baseType.Assembly || baseTypeAssemblyIsCandidate) + { + try + { + assemblyTypes = GetTypesWithFormattedException(assembly) + .Where(baseType.IsAssignableFrom) + .ToList(); // in try block + } + catch (TypeLoadException ex) + { + _logger.LogError( + ex, + "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", + assembly); + continue; + } + + types.AddRange(assemblyTypes.Where(x => + x.IsClass // only classes + && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed + && x.IsNestedPrivate == false // exclude nested private + && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract + && x.GetCustomAttribute(false) == null // exclude hidden + && (additionalFilter == null || additionalFilter(x)))); // filter + } + + if (assembly != baseType.Assembly && (assemblyTypes?.All(x => x.IsSealed) ?? false)) + { + continue; + } + + if (QueryWithReferencingAssemblies) + { + foreach (Assembly referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) + { + candidateAssemblies.Remove(referencing); + stack.Push(referencing); + } + } + } + + return types; + } + + private IEnumerable GetTypesWithFormattedException(Assembly a) + { + // if the assembly is dynamic, do not try to scan it + if (a.IsDynamic) + { + return Enumerable.Empty(); + } + + var getAll = a.GetCustomAttribute() == null; + + try + { + // we need to detect if an assembly is partially trusted, if so we cannot go interrogating all of it's types + // only its exported types, otherwise we'll get exceptions. + return getAll ? a.GetTypes() : a.GetExportedTypes(); + } + + // GetExportedTypes *can* throw TypeLoadException! + catch (TypeLoadException ex) + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + AppendLoaderException(sb, ex); + + // rethrow as ReflectionTypeLoadException (for consistency) with new message + throw new ReflectionTypeLoadException(new Type[0], new Exception[] { ex }, sb.ToString()); + } + + // GetTypes throws ReflectionTypeLoadException + catch (ReflectionTypeLoadException rex) + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + foreach (Exception loaderException in rex.LoaderExceptions.WhereNotNull()) + { + AppendLoaderException(sb, loaderException); + } + + var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); + + // rethrow with new message, unless accepted + if (AcceptsLoadExceptions(a) == false) + { + throw ex; + } + + // log a warning, and return what we can + lock (_notifiedLoadExceptionAssemblies) + { + if (a.FullName is not null && _notifiedLoadExceptionAssemblies.Contains(a.FullName) == false) + { + _notifiedLoadExceptionAssemblies.Add(a.FullName); + _logger.LogWarning(ex, "Could not load all types from {TypeName}.", a.GetName().Name); + } + } + + return rex.Types.WhereNotNull().ToArray(); + } + } + + private static void AppendLoaderException(StringBuilder sb, Exception loaderException) + { + sb.Append(". "); + sb.Append(loaderException.GetType().FullName); + + if (loaderException is TypeLoadException tloadex) + { + sb.Append(" on "); + sb.Append(tloadex.TypeName); + } + + sb.Append(": "); + sb.Append(loaderException.Message); + sb.AppendLine(); + } + + #endregion } diff --git a/src/Umbraco.Core/Composing/TypeFinderConfig.cs b/src/Umbraco.Core/Composing/TypeFinderConfig.cs index 4b5271039f..2fd9283500 100644 --- a/src/Umbraco.Core/Composing/TypeFinderConfig.cs +++ b/src/Umbraco.Core/Composing/TypeFinderConfig.cs @@ -1,36 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// TypeFinder config via appSettings +/// +public class TypeFinderConfig : ITypeFinderConfig { - /// - /// TypeFinder config via appSettings - /// - public class TypeFinderConfig : ITypeFinderConfig + private readonly TypeFinderSettings _settings; + private IEnumerable? _assembliesAcceptingLoadExceptions; + + public TypeFinderConfig(IOptions settings) => _settings = settings.Value; + + public IEnumerable AssembliesAcceptingLoadExceptions { - private readonly TypeFinderSettings _settings; - private IEnumerable? _assembliesAcceptingLoadExceptions; - - public TypeFinderConfig(IOptions settings) => _settings = settings.Value; - - public IEnumerable AssembliesAcceptingLoadExceptions + get { - get + if (_assembliesAcceptingLoadExceptions != null) { - if (_assembliesAcceptingLoadExceptions != null) - { - return _assembliesAcceptingLoadExceptions; - } - - var s = _settings.AssembliesAcceptingLoadExceptions; - return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s) - ? Array.Empty() - : s.Split(',').Select(x => x.Trim()).ToArray(); + return _assembliesAcceptingLoadExceptions; } + + var s = _settings.AssembliesAcceptingLoadExceptions; + return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s) + ? Array.Empty() + : s.Split(',').Select(x => x.Trim()).ToArray(); } } } diff --git a/src/Umbraco.Core/Composing/TypeFinderExtensions.cs b/src/Umbraco.Core/Composing/TypeFinderExtensions.cs index adb920b64a..c67d935716 100644 --- a/src/Umbraco.Core/Composing/TypeFinderExtensions.cs +++ b/src/Umbraco.Core/Composing/TypeFinderExtensions.cs @@ -1,46 +1,52 @@ -using System; -using System.Collections.Generic; using System.Reflection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TypeFinderExtensions { - public static class TypeFinderExtensions - { - /// - /// Finds any classes derived from the type T that contain the attribute TAttribute - /// - /// - /// - /// - /// - /// - /// - public static IEnumerable FindClassesOfTypeWithAttribute(this ITypeFinder typeFinder, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - where TAttribute : Attribute - => typeFinder.FindClassesOfTypeWithAttribute(typeof(T), typeof(TAttribute), assemblies, onlyConcreteClasses); + /// + /// Finds any classes derived from the type T that contain the attribute TAttribute + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable FindClassesOfTypeWithAttribute( + this ITypeFinder typeFinder, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + where TAttribute : Attribute + => typeFinder.FindClassesOfTypeWithAttribute(typeof(T), typeof(TAttribute), assemblies, onlyConcreteClasses); - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - /// - /// - public static IEnumerable FindClassesOfType(this ITypeFinder typeFinder, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - => typeFinder.FindClassesOfType(typeof(T), assemblies, onlyConcreteClasses); + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + /// + public static IEnumerable FindClassesOfType( + this ITypeFinder typeFinder, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + => typeFinder.FindClassesOfType(typeof(T), assemblies, onlyConcreteClasses); - /// - /// Finds the classes with attribute. - /// - /// - /// - /// The assemblies. - /// if set to true only concrete classes. - /// - public static IEnumerable FindClassesWithAttribute(this ITypeFinder typeFinder, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - where T : Attribute - => typeFinder.FindClassesWithAttribute(typeof(T), assemblies, onlyConcreteClasses); - } + /// + /// Finds the classes with attribute. + /// + /// + /// + /// The assemblies. + /// if set to true only concrete classes. + /// + public static IEnumerable FindClassesWithAttribute( + this ITypeFinder typeFinder, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + where T : Attribute + => typeFinder.FindClassesWithAttribute(typeof(T), assemblies, onlyConcreteClasses); } diff --git a/src/Umbraco.Core/Composing/TypeHelper.cs b/src/Umbraco.Core/Composing/TypeHelper.cs index 08893732a8..6cb5426f77 100644 --- a/src/Umbraco.Core/Composing/TypeHelper.cs +++ b/src/Umbraco.Core/Composing/TypeHelper.cs @@ -1,395 +1,414 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Reflection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// A utility class for type checking, this provides internal caching so that calls to these methods will be faster +/// than doing a manual type check in c# +/// +public static class TypeHelper { + private static readonly ConcurrentDictionary, PropertyInfo[]> GetPropertiesCache + = new(); + + private static readonly ConcurrentDictionary GetFieldsCache = new(); + + private static readonly Assembly[] EmptyAssemblies = new Assembly[0]; /// - /// A utility class for type checking, this provides internal caching so that calls to these methods will be faster - /// than doing a manual type check in c# + /// Based on a type we'll check if it is IEnumerable{T} (or similar) and if so we'll return a List{T}, this will also + /// deal with array types and return List{T} for those too. + /// If it cannot be done, null is returned. /// - public static class TypeHelper + public static IList? CreateGenericEnumerableFromObject(object? obj) { - private static readonly ConcurrentDictionary, PropertyInfo[]> GetPropertiesCache - = new ConcurrentDictionary, PropertyInfo[]>(); - private static readonly ConcurrentDictionary GetFieldsCache - = new ConcurrentDictionary(); - - private static readonly Assembly[] EmptyAssemblies = new Assembly[0]; - - - - /// - /// Based on a type we'll check if it is IEnumerable{T} (or similar) and if so we'll return a List{T}, this will also deal with array types and return List{T} for those too. - /// If it cannot be done, null is returned. - /// - public static IList? CreateGenericEnumerableFromObject(object? obj) + if (obj is null) { - if (obj is null) - { - return null; - } - - var type = obj.GetType(); - - if (type.IsGenericType) - { - var genericTypeDef = type.GetGenericTypeDefinition(); - - if (genericTypeDef == typeof(IEnumerable<>) - || genericTypeDef == typeof(ICollection<>) - || genericTypeDef == typeof(Collection<>) - || genericTypeDef == typeof(IList<>) - || genericTypeDef == typeof(List<>) - //this will occur when Linq is used and we get the odd WhereIterator or DistinctIterators since those are special iterator types - || obj is IEnumerable) - { - //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> - var genericType = typeof(List<>).MakeGenericType(type.GetGenericArguments()); - //pass in obj to fill the list - return (IList?)Activator.CreateInstance(genericType, obj); - } - } - - if (type.IsArray) - { - //if its an array, we'll use a List<> - var typeArguments = type.GetElementType(); - if (typeArguments is not null) - { - Type genericType = typeof(List<>).MakeGenericType(typeArguments); - //pass in obj to fill the list - return (IList?)Activator.CreateInstance(genericType, obj); - } - } - return null; } - /// - /// Checks if the method is actually overriding a base method - /// - /// - /// - public static bool IsOverride(MethodInfo m) + Type type = obj.GetType(); + + if (type.IsGenericType) { - return m.GetBaseDefinition().DeclaringType != m.DeclaringType; - } + Type genericTypeDef = type.GetGenericTypeDefinition(); - /// - /// Find all assembly references that are referencing the assignTypeFrom Type's assembly found in the assemblyList - /// - /// The referenced assembly. - /// A list of assemblies. - /// - /// - /// If the assembly of the assignTypeFrom Type is in the App_Code assembly, then we return nothing since things cannot - /// reference that assembly, same with the global.asax assembly. - /// - public static IReadOnlyList GetReferencingAssemblies(Assembly assembly, IEnumerable assemblies) - { - if (assembly.IsDynamic || assembly.IsAppCodeAssembly() || assembly.IsGlobalAsaxAssembly()) - return EmptyAssemblies; + if (genericTypeDef == typeof(IEnumerable<>) + || genericTypeDef == typeof(ICollection<>) + || genericTypeDef == typeof(Collection<>) + || genericTypeDef == typeof(IList<>) + || genericTypeDef == typeof(List<>) - - // find all assembly references that are referencing the current type's assembly since we - // should only be scanning those assemblies because any other assembly will definitely not - // contain sub type's of the one we're currently looking for - var name = assembly.GetName().Name; - return assemblies.Where(x => x == assembly || name is not null ? HasReference(x, name!) : false).ToList(); - } - - /// - /// Determines if an assembly references another assembly. - /// - /// - /// - /// - public static bool HasReference(Assembly assembly, string name) - { - // ReSharper disable once LoopCanBeConvertedToQuery - no! - foreach (var a in assembly.GetReferencedAssemblies()) + // this will occur when Linq is used and we get the odd WhereIterator or DistinctIterators since those are special iterator types + || obj is IEnumerable) { - if (string.Equals(a.Name, name, StringComparison.Ordinal)) return true; + // if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + Type genericType = typeof(List<>).MakeGenericType(type.GetGenericArguments()); + + // pass in obj to fill the list + return (IList?)Activator.CreateInstance(genericType, obj); } - return false; } - /// - /// Returns true if the type is a class and is not static - /// - /// - /// - public static bool IsNonStaticClass(Type t) + if (type.IsArray) { - return t.IsClass && IsStaticClass(t) == false; - } - - /// - /// Returns true if the type is a static class - /// - /// - /// - /// - /// In IL a static class is abstract and sealed - /// see: http://stackoverflow.com/questions/1175888/determine-if-a-type-is-static - /// - public static bool IsStaticClass(Type type) - { - return type.IsAbstract && type.IsSealed; - } - - /// - /// Finds a lowest base class amongst a collection of types - /// - /// - /// - /// - /// The term 'lowest' refers to the most base class of the type collection. - /// If a base type is not found amongst the type collection then an invalid attempt is returned. - /// - public static Attempt GetLowestBaseType(params Type[] types) - { - if (types.Length == 0) - return Attempt.Fail(); - - if (types.Length == 1) - return Attempt.Succeed(types[0]); - - foreach (var curr in types) + // if its an array, we'll use a List<> + Type? typeArguments = type.GetElementType(); + if (typeArguments is not null) { - var others = types.Except(new[] {curr}); + Type genericType = typeof(List<>).MakeGenericType(typeArguments); - //is the current type a common denominator for all others ? - var isBase = others.All(curr.IsAssignableFrom); - - //if this type is the base for all others - if (isBase) - { - return Attempt.Succeed(curr); - } + // pass in obj to fill the list + return (IList?)Activator.CreateInstance(genericType, obj); } + } + return null; + } + + /// + /// Checks if the method is actually overriding a base method + /// + /// + /// + public static bool IsOverride(MethodInfo m) => m.GetBaseDefinition().DeclaringType != m.DeclaringType; + + /// + /// Find all assembly references that are referencing the assignTypeFrom Type's assembly found in the assemblyList + /// + /// The referenced assembly. + /// A list of assemblies. + /// + /// + /// If the assembly of the assignTypeFrom Type is in the App_Code assembly, then we return nothing since things cannot + /// reference that assembly, same with the global.asax assembly. + /// + public static IReadOnlyList GetReferencingAssemblies(Assembly assembly, IEnumerable assemblies) + { + if (assembly.IsDynamic || assembly.IsAppCodeAssembly() || assembly.IsGlobalAsaxAssembly()) + { + return EmptyAssemblies; + } + + // find all assembly references that are referencing the current type's assembly since we + // should only be scanning those assemblies because any other assembly will definitely not + // contain sub type's of the one we're currently looking for + var name = assembly.GetName().Name; + return assemblies.Where(x => x == assembly || name is not null ? HasReference(x, name!) : false).ToList(); + } + + /// + /// Determines if an assembly references another assembly. + /// + /// + /// + /// + public static bool HasReference(Assembly assembly, string name) + { + // ReSharper disable once LoopCanBeConvertedToQuery - no! + foreach (AssemblyName a in assembly.GetReferencedAssemblies()) + { + if (string.Equals(a.Name, name, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// + /// Returns true if the type is a class and is not static + /// + /// + /// + public static bool IsNonStaticClass(Type t) => t.IsClass && IsStaticClass(t) == false; + + /// + /// Returns true if the type is a static class + /// + /// + /// + /// + /// In IL a static class is abstract and sealed + /// see: http://stackoverflow.com/questions/1175888/determine-if-a-type-is-static + /// + public static bool IsStaticClass(Type type) => type.IsAbstract && type.IsSealed; + + /// + /// Finds a lowest base class amongst a collection of types + /// + /// + /// + /// + /// The term 'lowest' refers to the most base class of the type collection. + /// If a base type is not found amongst the type collection then an invalid attempt is returned. + /// + public static Attempt GetLowestBaseType(params Type[] types) + { + if (types.Length == 0) + { return Attempt.Fail(); } - /// - /// Determines whether the type is assignable from the specified implementation, - /// and caches the result across the application using a . - /// - /// The type of the contract. - /// The implementation. - /// - /// true if [is type assignable from] [the specified contract]; otherwise, false. - /// - public static bool IsTypeAssignableFrom(Type contract, Type? implementation) + if (types.Length == 1) { - return contract.IsAssignableFrom(implementation); + return Attempt.Succeed(types[0]); } - /// - /// Determines whether the type is assignable from the specified implementation , - /// and caches the result across the application using a . - /// - /// The type of the contract. - /// The implementation. - public static bool IsTypeAssignableFrom(Type implementation) + foreach (Type curr in types) { - return IsTypeAssignableFrom(typeof(TContract), implementation); - } + IEnumerable others = types.Except(new[] { curr }); - /// - /// Determines whether the object instance is assignable from the specified implementation , - /// and caches the result across the application using a . - /// - /// The type of the contract. - /// The implementation. - public static bool IsTypeAssignableFrom(object implementation) - { - if (implementation == null) throw new ArgumentNullException(nameof(implementation)); - return IsTypeAssignableFrom(implementation.GetType()); - } + // is the current type a common denominator for all others ? + var isBase = others.All(curr.IsAssignableFrom); - /// - /// A method to determine whether represents a value type. - /// - /// The implementation. - public static bool IsValueType(Type implementation) - { - return implementation.IsValueType || implementation.IsPrimitive; - } - - /// - /// A method to determine whether is an implied value type (, or a string). - /// - /// The implementation. - public static bool IsImplicitValueType(Type implementation) - { - return IsValueType(implementation) || implementation.IsEnum || implementation == typeof (string); - } - - /// - /// Returns (and caches) a PropertyInfo from a type - /// - /// - /// - /// - /// - /// - /// - /// - public static PropertyInfo? GetProperty(Type type, string name, - bool mustRead = true, - bool mustWrite = true, - bool includeIndexed = false, - bool caseSensitive = true) - { - return CachedDiscoverableProperties(type, mustRead, mustWrite, includeIndexed) - .FirstOrDefault(x => caseSensitive ? (x.Name == name) : x.Name.InvariantEquals(name)); - } - - /// - /// Gets (and caches) discoverable in the current for a given . - /// - /// The source. - /// - public static FieldInfo[] CachedDiscoverableFields(Type type) - { - return GetFieldsCache.GetOrAdd( - type, - x => type - .GetFields(BindingFlags.Public | BindingFlags.Instance) - .Where(y => y.IsInitOnly == false) - .ToArray()); - } - - /// - /// Gets (and caches) discoverable in the current for a given . - /// - /// The source. - /// true if the properties discovered are readable - /// true if the properties discovered are writable - /// true if the properties discovered are indexable - /// - public static PropertyInfo[] CachedDiscoverableProperties(Type type, bool mustRead = true, bool mustWrite = true, bool includeIndexed = false) - { - return GetPropertiesCache.GetOrAdd( - new Tuple(type, mustRead, mustWrite, includeIndexed), - x => type - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(y => (mustRead == false || y.CanRead) - && (mustWrite == false || y.CanWrite) - && (includeIndexed || y.GetIndexParameters().Any() == false)) - .ToArray()); - } - - #region Match Type - - // TODO: Need to determine if these methods should replace/combine/merge etc with IsTypeAssignableFrom, IsAssignableFromGeneric - - // readings: - // http://stackoverflow.com/questions/2033912/c-sharp-variance-problem-assigning-listderived-as-listbase - // http://stackoverflow.com/questions/2208043/generic-variance-in-c-sharp-4-0 - // http://stackoverflow.com/questions/8401738/c-sharp-casting-generics-covariance-and-contravariance - // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class - // http://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059 - - private static bool MatchGeneric(Type implementation, Type contract, IDictionary bindings) - { - // trying to match eg List with List - // or List>> with List>> - // classes are NOT invariant so List does not match List - - if (implementation.IsGenericType == false) return false; - - // must have the same generic type definition - var implDef = implementation.GetGenericTypeDefinition(); - var contDef = contract.GetGenericTypeDefinition(); - if (implDef != contDef) return false; - - // must have the same number of generic arguments - var implArgs = implementation.GetGenericArguments(); - var contArgs = contract.GetGenericArguments(); - if (implArgs.Length != contArgs.Length) return false; - - // generic arguments must match - // in insta we should have actual types (eg int, string...) - // in typea we can have generic parameters (eg ) - for (var i = 0; i < implArgs.Length; i++) + // if this type is the base for all others + if (isBase) { - const bool variance = false; // classes are NOT invariant - if (MatchType(implArgs[i], contArgs[i], bindings, variance) == false) - return false; + return Attempt.Succeed(curr); } - - return true; } - public static bool MatchType(Type implementation, Type contract) + return Attempt.Fail(); + } + + /// + /// Determines whether the type is assignable from the specified implementation, + /// and caches the result across the application using a . + /// + /// The type of the contract. + /// The implementation. + /// + /// true if [is type assignable from] [the specified contract]; otherwise, false. + /// + public static bool IsTypeAssignableFrom(Type contract, Type? implementation) => + contract.IsAssignableFrom(implementation); + + /// + /// Determines whether the type is assignable from the specified implementation + /// , + /// and caches the result across the application using a . + /// + /// The type of the contract. + /// The implementation. + public static bool IsTypeAssignableFrom(Type implementation) => + IsTypeAssignableFrom(typeof(TContract), implementation); + + /// + /// Determines whether the object instance is assignable from the specified + /// implementation , + /// and caches the result across the application using a . + /// + /// The type of the contract. + /// The implementation. + public static bool IsTypeAssignableFrom(object implementation) + { + if (implementation == null) { - return MatchType(implementation, contract, new Dictionary()); + throw new ArgumentNullException(nameof(implementation)); } - public static bool MatchType(Type implementation, Type contract, IDictionary bindings, bool variance = true) + return IsTypeAssignableFrom(implementation.GetType()); + } + + /// + /// A method to determine whether represents a value type. + /// + /// The implementation. + public static bool IsValueType(Type implementation) => implementation.IsValueType || implementation.IsPrimitive; + + /// + /// A method to determine whether is an implied value type ( + /// , or a string). + /// + /// The implementation. + public static bool IsImplicitValueType(Type implementation) => + IsValueType(implementation) || implementation.IsEnum || implementation == typeof(string); + + /// + /// Returns (and caches) a PropertyInfo from a type + /// + /// + /// + /// + /// + /// + /// + /// + public static PropertyInfo? GetProperty( + Type type, + string name, + bool mustRead = true, + bool mustWrite = true, + bool includeIndexed = false, + bool caseSensitive = true) => + CachedDiscoverableProperties(type, mustRead, mustWrite, includeIndexed) + .FirstOrDefault(x => caseSensitive ? x.Name == name : x.Name.InvariantEquals(name)); + + /// + /// Gets (and caches) discoverable in the current for a given + /// . + /// + /// The source. + /// + public static FieldInfo[] CachedDiscoverableFields(Type type) => + GetFieldsCache.GetOrAdd( + type, + x => type + .GetFields(BindingFlags.Public | BindingFlags.Instance) + .Where(y => y.IsInitOnly == false) + .ToArray()); + + /// + /// Gets (and caches) discoverable in the current for a given + /// . + /// + /// The source. + /// true if the properties discovered are readable + /// true if the properties discovered are writable + /// true if the properties discovered are indexable + /// + public static PropertyInfo[] CachedDiscoverableProperties(Type type, bool mustRead = true, bool mustWrite = true, bool includeIndexed = false) => + GetPropertiesCache.GetOrAdd( + new Tuple(type, mustRead, mustWrite, includeIndexed), + x => type + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(y => (mustRead == false || y.CanRead) + && (mustWrite == false || y.CanWrite) + && (includeIndexed || y.GetIndexParameters().Any() == false)) + .ToArray()); + + public static bool MatchType(Type implementation, Type contract) => + MatchType(implementation, contract, new Dictionary()); + + #region Match Type + + // TODO: Need to determine if these methods should replace/combine/merge etc with IsTypeAssignableFrom, IsAssignableFromGeneric + + // readings: + // http://stackoverflow.com/questions/2033912/c-sharp-variance-problem-assigning-listderived-as-listbase + // http://stackoverflow.com/questions/2208043/generic-variance-in-c-sharp-4-0 + // http://stackoverflow.com/questions/8401738/c-sharp-casting-generics-covariance-and-contravariance + // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class + // http://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059 + private static bool MatchGeneric(Type implementation, Type contract, IDictionary bindings) + { + // trying to match eg List with List + // or List>> with List>> + // classes are NOT invariant so List does not match List + if (implementation.IsGenericType == false) { - if (contract.IsGenericType) - { - // eg type is List or List - // if we have variance then List can match IList - // if we don't have variance it can't - must have exact type - - // try to match implementation against contract - if (MatchGeneric(implementation, contract, bindings)) return true; - - // if no variance, fail - if (variance == false) return false; - - // try to match an ancestor of implementation against contract - var t = implementation.BaseType; - while (t != null) - { - if (MatchGeneric(t, contract, bindings)) return true; - t = t.BaseType; - } - - // try to match an interface of implementation against contract - return implementation.GetInterfaces().Any(i => MatchGeneric(i, contract, bindings)); - } - - if (contract.IsGenericParameter) - { - // eg - - if (bindings.ContainsKey(contract.Name)) - { - // already bound: ensure it's compatible - return bindings[contract.Name] == implementation; - } - - // not already bound: bind - bindings[contract.Name] = implementation; - return true; - } - - // not a generic type, not a generic parameter - // so normal class or interface - // about primitive types, value types, etc: - // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class - // if it's a primitive type... it needs to be == - - if (implementation == contract) return true; - if (contract.IsClass && implementation.IsClass && implementation.IsSubclassOf(contract)) return true; - if (contract.IsInterface && implementation.GetInterfaces().Contains(contract)) return true; - return false; } - #endregion + // must have the same generic type definition + Type implDef = implementation.GetGenericTypeDefinition(); + Type contDef = contract.GetGenericTypeDefinition(); + if (implDef != contDef) + { + return false; + } + + // must have the same number of generic arguments + Type[] implArgs = implementation.GetGenericArguments(); + Type[] contArgs = contract.GetGenericArguments(); + if (implArgs.Length != contArgs.Length) + { + return false; + } + + // generic arguments must match + // in insta we should have actual types (eg int, string...) + // in typea we can have generic parameters (eg ) + for (var i = 0; i < implArgs.Length; i++) + { + const bool variance = false; // classes are NOT invariant + if (MatchType(implArgs[i], contArgs[i], bindings, variance) == false) + { + return false; + } + } + + return true; } + + public static bool MatchType(Type implementation, Type contract, IDictionary bindings, bool variance = true) + { + if (contract.IsGenericType) + { + // eg type is List or List + // if we have variance then List can match IList + // if we don't have variance it can't - must have exact type + + // try to match implementation against contract + if (MatchGeneric(implementation, contract, bindings)) + { + return true; + } + + // if no variance, fail + if (variance == false) + { + return false; + } + + // try to match an ancestor of implementation against contract + Type? t = implementation.BaseType; + while (t != null) + { + if (MatchGeneric(t, contract, bindings)) + { + return true; + } + + t = t.BaseType; + } + + // try to match an interface of implementation against contract + return implementation.GetInterfaces().Any(i => MatchGeneric(i, contract, bindings)); + } + + if (contract.IsGenericParameter) + { + // eg + if (bindings.ContainsKey(contract.Name)) + { + // already bound: ensure it's compatible + return bindings[contract.Name] == implementation; + } + + // not already bound: bind + bindings[contract.Name] = implementation; + return true; + } + + // not a generic type, not a generic parameter + // so normal class or interface + // about primitive types, value types, etc: + // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class + // if it's a primitive type... it needs to be == + if (implementation == contract) + { + return true; + } + + if (contract.IsClass && implementation.IsClass && implementation.IsSubclassOf(contract)) + { + return true; + } + + if (contract.IsInterface && implementation.GetInterfaces().Contains(contract)) + { + return true; + } + + return false; + } + + #endregion } diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 6f4d81fc34..7fadd102da 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; @@ -10,458 +6,506 @@ using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides methods to find and instantiate types. +/// +/// +/// +/// This class should be used to get all types, the class should never be used +/// directly. +/// +/// In most cases this class is not used directly but through extension methods that retrieve specific types. +/// +public sealed class TypeLoader { + private readonly object _locko = new(); + private readonly ILogger _logger; + + private readonly Dictionary _types = new(); + + private IEnumerable? _assemblies; + /// - /// Provides methods to find and instantiate types. + /// Initializes a new instance of the class. + /// + [Obsolete("Please use an alternative constructor.")] + public TypeLoader( + ITypeFinder typeFinder, + IRuntimeHash runtimeHash, + IAppPolicyCache runtimeCache, + DirectoryInfo localTempPath, + ILogger logger, + IProfiler profiler, + IEnumerable? assembliesToScan = null) + : this(typeFinder, logger, assembliesToScan) + { + } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Please use an alternative constructor.")] + public TypeLoader( + ITypeFinder typeFinder, + IRuntimeHash runtimeHash, + IAppPolicyCache runtimeCache, + DirectoryInfo localTempPath, + ILogger logger, + IProfiler profiler, + bool detectChanges, + IEnumerable? assembliesToScan = null) + : this(typeFinder, logger, assembliesToScan) + { + } + + public TypeLoader( + ITypeFinder typeFinder, + ILogger logger, + IEnumerable? assembliesToScan = null) + { + TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _assemblies = assembliesToScan; + } + + /// + /// Returns the underlying + /// + // ReSharper disable once MemberCanBePrivate.Global + public ITypeFinder TypeFinder { get; } + + /// + /// Gets or sets the set of assemblies to scan. /// /// - /// This class should be used to get all types, the class should never be used directly. - /// In most cases this class is not used directly but through extension methods that retrieve specific types. + /// + /// If not explicitly set, defaults to all assemblies except those that are know to not have any of the + /// types we might scan. Because we only scan for application types, this means we can safely exclude GAC + /// assemblies + /// for example. + /// + /// This is for unit tests. /// - public sealed class TypeLoader + // internal for tests + [Obsolete("This will be removed in a future version.")] + public IEnumerable AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan; + + /// + /// Gets the type lists. + /// + /// For unit tests. + // internal for tests + [Obsolete("This will be removed in a future version.")] + public IEnumerable TypeLists => _types.Values; + + /// + /// Sets a type list. + /// + /// For unit tests. + // internal for tests + [Obsolete("This will be removed in a future version.")] + public void AddTypeList(TypeList typeList) { - private readonly ILogger _logger; + Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values + _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; + } - private readonly Dictionary _types = new (); - private readonly object _locko = new (); + #region Get Assembly Attributes - private IEnumerable? _assemblies; - - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Please use an alternative constructor.")] - public TypeLoader( - ITypeFinder typeFinder, - IRuntimeHash runtimeHash, - IAppPolicyCache runtimeCache, - DirectoryInfo localTempPath, - ILogger logger, - IProfiler profiler, - IEnumerable? assembliesToScan = null) - : this(typeFinder, logger, assembliesToScan) + /// + /// Gets the assembly attributes of the specified . + /// + /// The attribute types. + /// + /// The assembly attributes of the specified types. + /// + /// attributeTypes + public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes) + { + if (attributeTypes == null) { + throw new ArgumentNullException(nameof(attributeTypes)); } - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Please use an alternative constructor.")] - public TypeLoader( - ITypeFinder typeFinder, - IRuntimeHash runtimeHash, - IAppPolicyCache runtimeCache, - DirectoryInfo localTempPath, - ILogger logger, - IProfiler profiler, - bool detectChanges, - IEnumerable? assembliesToScan = null) - : this(typeFinder, logger, assembliesToScan) + return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); + } + + #endregion + + #region Cache + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public Attempt> TryGetCached(Type baseType, Type attributeType) => + Attempt>.Fail(); + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public Dictionary<(string, string), IEnumerable>? ReadCache() => null; + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public string? GetTypesListFilePath() => null; + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public void WriteCache() + { + } + + /// + /// Clears cache. + /// + /// Generally only used for resetting cache, for example during the install process. + [Obsolete("This will be removed in a future version.")] + public void ClearTypesCache() + { + } + + #endregion + + #region Get Types + + /// + /// Gets class types inheriting from or implementing the specified type + /// + /// The type to inherit from or implement. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetTypes(bool cache = true, IEnumerable? specificAssemblies = null) + { + if (_logger == null) { + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); } - public TypeLoader( - ITypeFinder typeFinder, - ILogger logger, - IEnumerable? assembliesToScan = null) + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _assemblies = assembliesToScan; - } - - /// - /// Returns the underlying - /// - // ReSharper disable once MemberCanBePrivate.Global - public ITypeFinder TypeFinder { get; } - - /// - /// Gets or sets the set of assemblies to scan. - /// - /// - /// If not explicitly set, defaults to all assemblies except those that are know to not have any of the - /// types we might scan. Because we only scan for application types, this means we can safely exclude GAC assemblies - /// for example. - /// This is for unit tests. - /// - // internal for tests - [Obsolete("This will be removed in a future version.")] - public IEnumerable AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan; - - /// - /// Gets the type lists. - /// - /// For unit tests. - // internal for tests - [Obsolete("This will be removed in a future version.")] - public IEnumerable TypeLists => _types.Values; - - /// - /// Sets a type list. - /// - /// For unit tests. - // internal for tests - [Obsolete("This will be removed in a future version.")] - public void AddTypeList(TypeList typeList) - { - var tobject = typeof(object); // CompositeTypeTypeKey does not support null values - _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; - } - - #region Cache - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public Attempt> TryGetCached(Type baseType, Type attributeType) - { - return Attempt>.Fail(); - } - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public Dictionary<(string, string), IEnumerable>? ReadCache() => null; - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public string? GetTypesListFilePath() => null; - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public void WriteCache() - { - } - - /// - /// Clears cache. - /// - /// Generally only used for resetting cache, for example during the install process. - [Obsolete("This will be removed in a future version.")] - public void ClearTypesCache() - { - } - - #endregion - - #region Get Assembly Attributes - - /// - /// Gets the assembly attributes of the specified . - /// - /// The attribute types. - /// - /// The assembly attributes of the specified types. - /// - /// attributeTypes - public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes) - { - if (attributeTypes == null) - throw new ArgumentNullException(nameof(attributeTypes)); - - return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); - } - - #endregion - - #region Get Types - - /// - /// Gets class types inheriting from or implementing the specified type - /// - /// The type to inherit from or implement. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types inheriting from or implementing the specified type. - /// Caching is disabled when using specific assemblies. - public IEnumerable GetTypes(bool cache = true, IEnumerable? specificAssemblies = null) - { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } - - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - // if not IDiscoverable, directly get types - if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) - { - // warn - _logger.LogDebug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} (slow).", typeof(T).FullName); - - return GetTypesInternal( - typeof(T), null, - () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), - "scanning assemblies", - cache); - } - - // get IDiscoverable and always cache - var discovered = GetTypesInternal( - typeof(IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - "scanning assemblies", - true); - // warn - if (!cache) - { - _logger.LogDebug("Running a non-cached, filter for discoverable type {TypeName} (slowish).", typeof(T).FullName); - } - - // filter the cached discovered types (and maybe cache the result) - return GetTypesInternal( - typeof(T), null, - () => discovered - .Where(x => typeof(T).IsAssignableFrom(x)), - "filtering IDiscoverable", - cache); - } - - /// - /// Gets class types inheriting from or implementing the specified type and marked with the specified attribute. - /// - /// The type to inherit from or implement. - /// The type of the attribute. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types inheriting from or implementing the specified type and marked with the specified attribute. - /// Caching is disabled when using specific assemblies. - public IEnumerable GetTypesWithAttribute(bool cache = true, IEnumerable? specificAssemblies = null) - where TAttribute : Attribute - { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } - - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - // if not IDiscoverable, directly get types - if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) - { - _logger.LogDebug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); - - return GetTypesInternal( - typeof(T), typeof(TAttribute), - () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), - "scanning assemblies", - cache); - } - - // get IDiscoverable and always cache - var discovered = GetTypesInternal( - typeof(IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - "scanning assemblies", - true); - - // warn - if (!cache) - { - _logger.LogDebug("Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", typeof(T).FullName, typeof(TAttribute).FullName); - } - - // filter the cached discovered types (and maybe cache the result) - return GetTypesInternal( - typeof(T), typeof(TAttribute), - () => discovered - .Where(x => typeof(T).IsAssignableFrom(x)) - .Where(x => x.GetCustomAttributes(false).Any()), - "filtering IDiscoverable", - cache); - } - - /// - /// Gets class types marked with the specified attribute. - /// - /// The type of the attribute. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types marked with the specified attribute. - /// Caching is disabled when using specific assemblies. - public IEnumerable GetAttributedTypes(bool cache = true, IEnumerable? specificAssemblies = null) - where TAttribute : Attribute - { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } - - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - if (!cache) - { - _logger.LogDebug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); - } + _logger.LogDebug( + "Running a full, " + (cache ? string.Empty : "non-") + + "cached, scan for non-discoverable type {TypeName} (slow).", + typeof(T).FullName); return GetTypesInternal( - typeof(object), typeof(TAttribute), - () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + typeof(T), + null, + () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), "scanning assemblies", cache); } - private IEnumerable GetTypesInternal( - Type baseType, - Type? attributeType, - Func> finder, - string action, - bool cache) - { - // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable - // lock at a time, and we don't have non-upgradeable readers, and quite probably the type - // loader is mostly not going to be used in any kind of massively multi-threaded scenario - so, - // a plain lock is enough + // get IDiscoverable and always cache + IEnumerable discovered = GetTypesInternal( + typeof(IDiscoverable), + null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", + true); - lock (_locko) - { - return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); - } + // warn + if (!cache) + { + _logger.LogDebug( + "Running a non-cached, filter for discoverable type {TypeName} (slowish).", + typeof(T).FullName); } - private static string GetName(Type? baseType, Type? attributeType) + // filter the cached discovered types (and maybe cache the result) + return GetTypesInternal( + typeof(T), + null, + () => discovered.Where(x => typeof(T).IsAssignableFrom(x)), + "filtering IDiscoverable", + cache); + } + + /// + /// Gets class types inheriting from or implementing the specified type and marked with the specified attribute. + /// + /// The type to inherit from or implement. + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type and marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetTypesWithAttribute( + bool cache = true, + IEnumerable? specificAssemblies = null) + where TAttribute : Attribute + { + if (_logger == null) { - var s = attributeType == null ? string.Empty : ("[" + attributeType + "]"); - s += baseType; - return s; + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); } - private IEnumerable GetTypesInternalLocked( - Type? baseType, - Type? attributeType, - Func> finder, - string action, - bool cache) + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - // check if the TypeList already exists, if so return it, if not we'll create it - var tobject = typeof(object); // CompositeTypeTypeKey does not support null values - var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); - TypeList? typeList = null; + _logger.LogDebug( + "Running a full, " + (cache ? string.Empty : "non-") + + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", + typeof(T).FullName, + typeof(TAttribute).FullName); - if (cache) - { - _types.TryGetValue(listKey, out typeList); // else null - } + return GetTypesInternal( + typeof(T), + typeof(TAttribute), + () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", + cache); + } - // if caching and found, return - if (typeList != null) - { - // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 - _logger.LogDebug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); - return typeList.Types; - } + // get IDiscoverable and always cache + IEnumerable discovered = GetTypesInternal( + typeof(IDiscoverable), + null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", + true); - // else proceed, - typeList = new TypeList(baseType, attributeType); + // warn + if (!cache) + { + _logger.LogDebug( + "Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", + typeof(T).FullName, + typeof(TAttribute).FullName); + } - // either we had to scan, or we could not get the types from the cache file - scan now - _logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); + // filter the cached discovered types (and maybe cache the result) + return GetTypesInternal( + typeof(T), + typeof(TAttribute), + () => discovered + .Where(x => typeof(T).IsAssignableFrom(x)) + .Where(x => x.GetCustomAttributes(false).Any()), + "filtering IDiscoverable", + cache); + } - foreach (var t in finder()) - { - typeList.Add(t); - } + /// + /// Gets class types marked with the specified attribute. + /// + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetAttributedTypes( + bool cache = true, + IEnumerable? specificAssemblies = null) + where TAttribute : Attribute + { + if (_logger == null) + { + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } - // if we are to cache the results, do so - if (cache) - { - var added = _types.ContainsKey(listKey) == false; - if (added) - { - _types[listKey] = typeList; - } + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; - _logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); - } - else - { - _logger.LogDebug("Got {TypeName}.", GetName(baseType, attributeType)); - } + if (!cache) + { + _logger.LogDebug( + "Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", + typeof(TAttribute).FullName); + } + return GetTypesInternal( + typeof(object), + typeof(TAttribute), + () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", + cache); + } + + private static string GetName(Type? baseType, Type? attributeType) + { + var s = attributeType == null ? string.Empty : "[" + attributeType + "]"; + s += baseType; + return s; + } + + private IEnumerable GetTypesInternal( + Type baseType, + Type? attributeType, + Func> finder, + string action, + bool cache) + { + // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable + // lock at a time, and we don't have non-upgradeable readers, and quite probably the type + // loader is mostly not going to be used in any kind of massively multi-threaded scenario - so, + // a plain lock is enough + lock (_locko) + { + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + } + } + + private IEnumerable GetTypesInternalLocked( + Type? baseType, + Type? attributeType, + Func> finder, + string action, + bool cache) + { + // check if the TypeList already exists, if so return it, if not we'll create it + Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values + var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); + TypeList? typeList = null; + + if (cache) + { + _types.TryGetValue(listKey, out typeList); // else null + } + + // if caching and found, return + if (typeList != null) + { + // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 + _logger.LogDebug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); return typeList.Types; } - #endregion + // else proceed, + typeList = new TypeList(baseType, attributeType); - #region Nested classes and stuff + // either we had to scan, or we could not get the types from the cache file - scan now + _logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); - /// - /// Represents a list of types obtained by looking for types inheriting/implementing a - /// specified type, and/or marked with a specified attribute type. - /// - public sealed class TypeList + foreach (Type t in finder()) { - private readonly HashSet _types = new HashSet(); - - public TypeList(Type? baseType, Type? attributeType) - { - BaseType = baseType; - AttributeType = attributeType; - } - - public Type? BaseType { get; } - public Type? AttributeType { get; } - - /// - /// Adds a type. - /// - public void Add(Type type) - { - if (BaseType?.IsAssignableFrom(type) == false) - throw new ArgumentException("Base type " + BaseType + " is not assignable from type " + type + ".", nameof(type)); - _types.Add(type); - } - - /// - /// Gets the types. - /// - public IEnumerable Types => _types; + typeList.Add(t); } - /// - /// Represents the error that occurs when a type was not found in the cache type list with the specified TypeResolutionKind. - /// - /// - [Serializable] - internal class CachedTypeNotFoundInFileException : Exception + // if we are to cache the results, do so + if (cache) { - /// - /// Initializes a new instance of the class. - /// - public CachedTypeNotFoundInFileException() - { } + var added = _types.ContainsKey(listKey) == false; + if (added) + { + _types[listKey] = typeList; + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public CachedTypeNotFoundInFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public CachedTypeNotFoundInFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected CachedTypeNotFoundInFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + _logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); + } + else + { + _logger.LogDebug("Got {TypeName}.", GetName(baseType, attributeType)); } - #endregion + return typeList.Types; } + + #endregion + + #region Nested classes and stuff + + /// + /// Represents a list of types obtained by looking for types inheriting/implementing a + /// specified type, and/or marked with a specified attribute type. + /// + public sealed class TypeList + { + private readonly HashSet _types = new(); + + public TypeList(Type? baseType, Type? attributeType) + { + BaseType = baseType; + AttributeType = attributeType; + } + + public Type? BaseType { get; } + + public Type? AttributeType { get; } + + /// + /// Gets the types. + /// + public IEnumerable Types => _types; + + /// + /// Adds a type. + /// + public void Add(Type type) + { + if (BaseType?.IsAssignableFrom(type) == false) + { + throw new ArgumentException( + "Base type " + BaseType + " is not assignable from type " + type + ".", + nameof(type)); + } + + _types.Add(type); + } + } + + /// + /// Represents the error that occurs when a type was not found in the cache type list with the specified + /// TypeResolutionKind. + /// + /// + [Serializable] + internal class CachedTypeNotFoundInFileException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public CachedTypeNotFoundInFileException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public CachedTypeNotFoundInFileException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public CachedTypeNotFoundInFileException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected CachedTypeNotFoundInFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + #endregion } diff --git a/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs b/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs index eec2adc637..740921974d 100644 --- a/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs +++ b/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs @@ -1,19 +1,13 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// A runtime hash this is always different on each app startup +/// +public sealed class VaryingRuntimeHash : IRuntimeHash { - /// - /// A runtime hash this is always different on each app startup - /// - public sealed class VaryingRuntimeHash : IRuntimeHash - { - private readonly string _hash; + private readonly string _hash; - public VaryingRuntimeHash() - { - _hash = DateTime.Now.Ticks.ToString(); - } + public VaryingRuntimeHash() => _hash = DateTime.Now.Ticks.ToString(); - public string GetHashValue() => _hash; - } + public string GetHashValue() => _hash; } diff --git a/src/Umbraco.Core/Composing/WeightAttribute.cs b/src/Umbraco.Core/Composing/WeightAttribute.cs index 1225abca0c..a69ca4636e 100644 --- a/src/Umbraco.Core/Composing/WeightAttribute.cs +++ b/src/Umbraco.Core/Composing/WeightAttribute.cs @@ -1,25 +1,19 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Specifies the weight of pretty much anything. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class WeightAttribute : Attribute { /// - /// Specifies the weight of pretty much anything. + /// Initializes a new instance of the class with a weight. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class WeightAttribute : Attribute - { - /// - /// Initializes a new instance of the class with a weight. - /// - /// - public WeightAttribute(int weight) - { - Weight = weight; - } + /// + public WeightAttribute(int weight) => Weight = weight; - /// - /// Gets the weight value. - /// - public int Weight { get; } - } + /// + /// Gets the weight value. + /// + public int Weight { get; } } diff --git a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs index 1eafcce9e0..56b714d35a 100644 --- a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs @@ -1,141 +1,156 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements a weighted collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +public abstract class WeightedCollectionBuilderBase : CollectionBuilderBase + where TBuilder : WeightedCollectionBuilderBase + where TCollection : class, IBuilderCollection { + private readonly Dictionary _customWeights = new(); + + public virtual int DefaultWeight { get; set; } = 100; + + protected abstract TBuilder This { get; } + /// - /// Implements a weighted collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - public abstract class WeightedCollectionBuilderBase : CollectionBuilderBase - where TBuilder : WeightedCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - protected abstract TBuilder This { get; } + Configure(types => types.Clear()); + return This; + } - private readonly Dictionary _customWeights = new Dictionary(); - - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => { - Configure(types => types.Clear()); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add() - where T : TItem - { - Configure(types => + Type type = typeof(T); + if (types.Contains(type) == false) { - var type = typeof(T); - if (types.Contains(type) == false) types.Add(type); - }); - return This; - } + types.Add(type); + } + }); + return This; + } - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add(Type type) + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "register"); + if (types.Contains(type) == false) { + types.Add(type); + } + }); + return This; + } + + /// + /// Adds types to the collection. + /// + /// The types to add. + /// The builder. + public TBuilder Add(IEnumerable types) + { + Configure(list => + { + foreach (Type type in types) + { + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast EnsureType(type, "register"); - if (types.Contains(type) == false) types.Add(type); - }); - return This; - } - - /// - /// Adds types to the collection. - /// - /// The types to add. - /// The builder. - public TBuilder Add(IEnumerable types) - { - Configure(list => - { - foreach (var type in types) + if (list.Contains(type) == false) { - // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast - EnsureType(type, "register"); - if (list.Contains(type) == false) list.Add(type); + list.Add(type); } - }); - return This; - } + } + }); + return This; + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove() + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "remove"); + if (types.Contains(type)) { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Changes the default weight of an item - /// - /// The type of item - /// The new weight - /// - public TBuilder SetWeight(int weight) where T : TItem + /// + /// Changes the default weight of an item + /// + /// The type of item + /// The new weight + /// + public TBuilder SetWeight(int weight) + where T : TItem + { + _customWeights[typeof(T)] = weight; + return This; + } + + protected override IEnumerable GetRegisteringTypes(IEnumerable types) + { + var list = types.ToList(); + list.Sort((t1, t2) => GetWeight(t1).CompareTo(GetWeight(t2))); + return list; + } + + protected virtual int GetWeight(Type type) + { + if (_customWeights.ContainsKey(type)) { - _customWeights[typeof(T)] = weight; - return This; + return _customWeights[type]; } - protected override IEnumerable GetRegisteringTypes(IEnumerable types) - { - var list = types.ToList(); - list.Sort((t1, t2) => GetWeight(t1).CompareTo(GetWeight(t2))); - return list; - } - - public virtual int DefaultWeight { get; set; } = 100; - - protected virtual int GetWeight(Type type) - { - if (_customWeights.ContainsKey(type)) - return _customWeights[type]; - var attr = type.GetCustomAttributes(typeof(WeightAttribute), false).OfType().SingleOrDefault(); - return attr?.Weight ?? DefaultWeight; - } + WeightAttribute? attr = type.GetCustomAttributes(typeof(WeightAttribute), false).OfType() + .SingleOrDefault(); + return attr?.Weight ?? DefaultWeight; } } diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index e69de29bb2..8b13789179 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -0,0 +1 @@ + diff --git a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs index 829d19bb53..cd256e1b45 100644 --- a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs @@ -6,14 +6,14 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Configuration; /// -/// Configures the named option. +/// Configures the named option. /// public class ConfigureConnectionStrings : IConfigureNamedOptions { private readonly IConfiguration _configuration; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. public ConfigureConnectionStrings(IConfiguration configuration) @@ -38,6 +38,7 @@ public class ConfigureConnectionStrings : IConfigureNamedOptions - /// Determines if file extension is allowed for upload based on (optional) white list and black list - /// held in settings. - /// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted. - /// - public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) - { - return contentSettings.AllowedUploadFiles.Any(x => x.InvariantEquals(extension)) || - (contentSettings.AllowedUploadFiles.Any() == false && - contentSettings.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)) == false); - } +namespace Umbraco.Extensions; - /// - /// Gets the auto-fill configuration for a specified property alias. - /// - /// - /// The property type alias. - /// The auto-fill configuration for the specified property alias, or null. - public static ImagingAutoFillUploadField? GetConfig(this ContentSettings contentSettings, string propertyTypeAlias) - { - var autoFillConfigs = contentSettings.Imaging.AutoFillImageProperties; - return autoFillConfigs?.FirstOrDefault(x => x.Alias == propertyTypeAlias); - } +public static class ContentSettingsExtensions +{ + /// + /// Determines if file extension is allowed for upload based on (optional) white list and black list + /// held in settings. + /// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted. + /// + public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) => + contentSettings.AllowedUploadFiles.Any(x => x.InvariantEquals(extension)) || + (contentSettings.AllowedUploadFiles.Any() == false && + contentSettings.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)) == false); + + /// + /// Gets the auto-fill configuration for a specified property alias. + /// + /// + /// The property type alias. + /// The auto-fill configuration for the specified property alias, or null. + public static ImagingAutoFillUploadField? GetConfig(this ContentSettings contentSettings, string propertyTypeAlias) + { + ImagingAutoFillUploadField[] autoFillConfigs = contentSettings.Imaging.AutoFillImageProperties; + return autoFillConfigs?.FirstOrDefault(x => x.Alias == propertyTypeAlias); } } diff --git a/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs index b6b9f067b9..096eac6fe0 100644 --- a/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs +++ b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs @@ -1,24 +1,23 @@ using System.Reflection; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +internal class EntryAssemblyMetadata : IEntryAssemblyMetadata { - internal class EntryAssemblyMetadata : IEntryAssemblyMetadata + public EntryAssemblyMetadata() { - public EntryAssemblyMetadata() - { - var entryAssembly = Assembly.GetEntryAssembly(); + var entryAssembly = Assembly.GetEntryAssembly(); - Name = entryAssembly - ?.GetName() - ?.Name ?? string.Empty; + Name = entryAssembly + ?.GetName() + ?.Name ?? string.Empty; - InformationalVersion = entryAssembly - ?.GetCustomAttribute() - ?.InformationalVersion ?? string.Empty; - } - - public string Name { get; } - - public string InformationalVersion { get; } + InformationalVersion = entryAssembly + ?.GetCustomAttribute() + ?.InformationalVersion ?? string.Empty; } + + public string Name { get; } + + public string InformationalVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs b/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs deleted file mode 100644 index 7655252981..0000000000 --- a/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Configuration.Models; - -namespace Umbraco.Extensions -{ - public static class HealthCheckSettingsExtensions - { - public static TimeSpan GetNotificationDelay(this HealthChecksSettings settings, ICronTabParser cronTabParser, DateTime now, TimeSpan defaultDelay) - { - // If first run time not set, start with just small delay after application start. - var firstRunTime = settings.Notification.FirstRunTime; - if (string.IsNullOrEmpty(firstRunTime)) - { - return defaultDelay; - } - else - { - // Otherwise start at scheduled time according to cron expression, unless within the default delay period. - var firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now); - var delay = firstRunOccurance - now; - return delay < defaultDelay - ? defaultDelay - : delay; - } - } - } -} diff --git a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs index 2b22b0f28b..2f49bfd146 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs @@ -1,60 +1,75 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Extensions -{ - public static class GlobalSettingsExtensions - { - private static string? _mvcArea; - private static string? _backOfficePath; +namespace Umbraco.Extensions; - /// - /// Returns the absolute path for the Umbraco back office - /// - /// - /// - /// - public static string GetBackOfficePath(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) +public static class GlobalSettingsExtensions +{ + private static string? _mvcArea; + private static string? _backOfficePath; + + /// + /// Returns the absolute path for the Umbraco back office + /// + /// + /// + /// + public static string GetBackOfficePath(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + { + if (_backOfficePath != null) { - if (_backOfficePath != null) return _backOfficePath; - _backOfficePath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); return _backOfficePath; } - /// - /// This returns the string of the MVC Area route. - /// - /// - /// This will return the MVC area that we will route all custom routes through like surface controllers, etc... - /// We will use the 'Path' (default ~/umbraco) to create it but since it cannot contain '/' and people may specify a path of ~/asdf/asdf/admin - /// we will convert the '/' to '-' and use that as the path. its a bit lame but will work. - /// - /// We also make sure that the virtual directory (SystemDirectories.Root) is stripped off first, otherwise we'd end up with something - /// like "MyVirtualDirectory-Umbraco" instead of just "Umbraco". - /// - public static string GetUmbracoMvcArea(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + _backOfficePath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + return _backOfficePath; + } + + /// + /// This returns the string of the MVC Area route. + /// + /// + /// This will return the MVC area that we will route all custom routes through like surface controllers, etc... + /// We will use the 'Path' (default ~/umbraco) to create it but since it cannot contain '/' and people may specify a + /// path of ~/asdf/asdf/admin + /// we will convert the '/' to '-' and use that as the path. its a bit lame but will work. + /// We also make sure that the virtual directory (SystemDirectories.Root) is stripped off first, otherwise we'd end up + /// with something + /// like "MyVirtualDirectory-Umbraco" instead of just "Umbraco". + /// + public static string GetUmbracoMvcArea(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + { + if (_mvcArea != null) { - if (_mvcArea != null) return _mvcArea; - - _mvcArea = globalSettings.GetUmbracoMvcAreaNoCache(hostingEnvironment); - return _mvcArea; } - internal static string GetUmbracoMvcAreaNoCache(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + _mvcArea = globalSettings.GetUmbracoMvcAreaNoCache(hostingEnvironment); + + return _mvcArea; + } + + internal static string GetUmbracoMvcAreaNoCache( + this GlobalSettings globalSettings, + IHostingEnvironment hostingEnvironment) + { + var path = string.IsNullOrEmpty(globalSettings.UmbracoPath) + ? string.Empty + : hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + + if (path.IsNullOrWhiteSpace()) { - var path = string.IsNullOrEmpty(globalSettings.UmbracoPath) - ? string.Empty - : hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); - - if (path.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified"); - - if (path.StartsWith(hostingEnvironment.ApplicationVirtualPath)) // beware of TrimStart, see U4-2518 - path = path.Substring(hostingEnvironment.ApplicationVirtualPath.Length); - return path.TrimStart(Constants.CharArrays.Tilde).TrimStart(Constants.CharArrays.ForwardSlash).Replace('/', '-').Trim().ToLower(); + throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified"); } + + // beware of TrimStart, see U4-2518 + if (path.StartsWith(hostingEnvironment.ApplicationVirtualPath)) + { + path = path[hostingEnvironment.ApplicationVirtualPath.Length..]; + } + + return path.TrimStart(Constants.CharArrays.Tilde).TrimStart(Constants.CharArrays.ForwardSlash).Replace('/', '-') + .Trim().ToLower(); } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs index 27d6820399..44c9c37dfd 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs @@ -1,18 +1,21 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Configuration.Grid -{ - public class GridConfig : IGridConfig - { - public GridConfig(AppCaches appCaches, IManifestParser manifestParser, IJsonSerializer jsonSerializer, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory) - { - EditorsConfig = new GridEditorsConfig(appCaches, hostingEnvironment, manifestParser, jsonSerializer, loggerFactory.CreateLogger()); - } +namespace Umbraco.Cms.Core.Configuration.Grid; - public IGridEditorsConfig EditorsConfig { get; } - } +public class GridConfig : IGridConfig +{ + public GridConfig( + AppCaches appCaches, + IManifestParser manifestParser, + IJsonSerializer jsonSerializer, + IHostingEnvironment hostingEnvironment, + ILoggerFactory loggerFactory) + => EditorsConfig = + new GridEditorsConfig(appCaches, hostingEnvironment, manifestParser, jsonSerializer, loggerFactory.CreateLogger()); + + public IGridEditorsConfig EditorsConfig { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index db5d669ce9..11ae329192 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; @@ -10,78 +8,91 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Grid +namespace Umbraco.Cms.Core.Configuration.Grid; + +internal class GridEditorsConfig : IGridEditorsConfig { - internal class GridEditorsConfig : IGridEditorsConfig + private readonly AppCaches _appCaches; + private readonly IHostingEnvironment _hostingEnvironment; + + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private readonly IManifestParser _manifestParser; + + public GridEditorsConfig( + AppCaches appCaches, + IHostingEnvironment hostingEnvironment, + IManifestParser manifestParser, + IJsonSerializer jsonSerializer, + ILogger logger) { - private readonly AppCaches _appCaches; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IManifestParser _manifestParser; + _appCaches = appCaches; + _hostingEnvironment = hostingEnvironment; + _manifestParser = manifestParser; + _jsonSerializer = jsonSerializer; + _logger = logger; + } - private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger _logger; - - public GridEditorsConfig(AppCaches appCaches, IHostingEnvironment hostingEnvironment, IManifestParser manifestParser,IJsonSerializer jsonSerializer, ILogger logger) + public IEnumerable Editors + { + get { - _appCaches = appCaches; - _hostingEnvironment = hostingEnvironment; - _manifestParser = manifestParser; - _jsonSerializer = jsonSerializer; - _logger = logger; - } - - public IEnumerable Editors - { - get + List GetResult() { - List GetResult() + var configFolder = + new DirectoryInfo(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); + var editors = new List(); + var gridConfig = Path.Combine(configFolder.FullName, "grid.editors.config.js"); + if (File.Exists(gridConfig)) { - var configFolder = new DirectoryInfo(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); - var editors = new List(); - var gridConfig = Path.Combine(configFolder.FullName, "grid.editors.config.js"); - if (File.Exists(gridConfig)) + var sourceString = File.ReadAllText(gridConfig); + + try { - var sourceString = File.ReadAllText(gridConfig); - - try - { - editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", sourceString); - } + editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); } - else// Read default from embedded file + catch (Exception ex) { - var assembly = GetType().Assembly; - var resourceStream = assembly.GetManifestResourceStream( - "Umbraco.Cms.Core.EmbeddedResources.Grid.grid.editors.config.js"); - - if (resourceStream is not null) - { - using var reader = new StreamReader(resourceStream, Encoding.UTF8); - var sourceString = reader.ReadToEnd(); - editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); - } + _logger.LogError( + ex, + "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", + sourceString); } - - // add manifest editors, skip duplicates - foreach (var gridEditor in _manifestParser.CombinedManifest.GridEditors) - { - if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); - } - - return editors; } - //cache the result if debugging is disabled - var result = _hostingEnvironment.IsDebugMode - ? GetResult() - : _appCaches.RuntimeCache.GetCacheItem>(typeof(GridEditorsConfig) + ".Editors",GetResult, TimeSpan.FromMinutes(10)); + // Read default from embedded file + else + { + Assembly assembly = GetType().Assembly; + Stream? resourceStream = assembly.GetManifestResourceStream( + "Umbraco.Cms.Core.EmbeddedResources.Grid.grid.editors.config.js"); - return result!; + if (resourceStream is not null) + { + using var reader = new StreamReader(resourceStream, Encoding.UTF8); + var sourceString = reader.ReadToEnd(); + editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); + } + } + + // add manifest editors, skip duplicates + foreach (GridEditor gridEditor in _manifestParser.CombinedManifest.GridEditors) + { + if (editors.Contains(gridEditor) == false) + { + editors.Add(gridEditor); + } + } + + return editors; } + + // cache the result if debugging is disabled + List? result = _hostingEnvironment.IsDebugMode + ? GetResult() + : _appCaches.RuntimeCache.GetCacheItem(typeof(GridEditorsConfig) + ".Editors", GetResult, TimeSpan.FromMinutes(10)); + + return result!; } } } diff --git a/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs index d009eddd25..4dd11ee1fc 100644 --- a/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs @@ -1,9 +1,6 @@ -namespace Umbraco.Cms.Core.Configuration.Grid +namespace Umbraco.Cms.Core.Configuration.Grid; + +public interface IGridConfig { - public interface IGridConfig - { - - IGridEditorsConfig EditorsConfig { get; } - - } + IGridEditorsConfig EditorsConfig { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs index bfd3f17cbf..5103e7a328 100644 --- a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Grid; -namespace Umbraco.Cms.Core.Configuration.Grid +public interface IGridEditorConfig { - public interface IGridEditorConfig - { - string? Name { get; } - string? NameTemplate { get; } - string Alias { get; } - string? View { get; } - string? Render { get; } - string? Icon { get; } - IDictionary Config { get; } - } + string? Name { get; } + + string? NameTemplate { get; } + + string Alias { get; } + + string? View { get; } + + string? Render { get; } + + string? Icon { get; } + + IDictionary Config { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs index a49ae41d6c..e0d8c8f8d4 100644 --- a/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Grid; -namespace Umbraco.Cms.Core.Configuration.Grid +public interface IGridEditorsConfig { - public interface IGridEditorsConfig - { - IEnumerable Editors { get; } - } + IEnumerable Editors { get; } } diff --git a/src/Umbraco.Core/Configuration/IConfigManipulator.cs b/src/Umbraco.Core/Configuration/IConfigManipulator.cs index c99f90e5c9..18ce8a5eca 100644 --- a/src/Umbraco.Core/Configuration/IConfigManipulator.cs +++ b/src/Umbraco.Core/Configuration/IConfigManipulator.cs @@ -1,11 +1,14 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public interface IConfigManipulator { - public interface IConfigManipulator - { - void RemoveConnectionString(); - void SaveConnectionString(string connectionString, string? providerName); - void SaveConfigValue(string itemPath, object value); - void SaveDisableRedirectUrlTracking(bool disable); - void SetGlobalId(string id); - } + void RemoveConnectionString(); + + void SaveConnectionString(string connectionString, string? providerName); + + void SaveConfigValue(string itemPath, object value); + + void SaveDisableRedirectUrlTracking(bool disable); + + void SetGlobalId(string id); } diff --git a/src/Umbraco.Core/Configuration/ICronTabParser.cs b/src/Umbraco.Core/Configuration/ICronTabParser.cs index 565d9fa47b..bd3808ecd1 100644 --- a/src/Umbraco.Core/Configuration/ICronTabParser.cs +++ b/src/Umbraco.Core/Configuration/ICronTabParser.cs @@ -1,28 +1,25 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Configuration +/// +/// Defines the contract for that allows the parsing of chrontab expressions. +/// +public interface ICronTabParser { /// - /// Defines the contract for that allows the parsing of chrontab expressions. + /// Returns a value indicating whether a given chrontab expression is valid. /// - public interface ICronTabParser - { - /// - /// Returns a value indicating whether a given chrontab expression is valid. - /// - /// The chrontab expression to parse. - /// The result. - bool IsValidCronTab(string cronTab); + /// The chrontab expression to parse. + /// The result. + bool IsValidCronTab(string cronTab); - /// - /// Returns the next occurence for the given chrontab expression from the given time. - /// - /// The chrontab expression to parse. - /// The date and time to start from. - /// The representing the next occurence. - DateTime GetNextOccurrence(string cronTab, DateTime time); - } + /// + /// Returns the next occurence for the given chrontab expression from the given time. + /// + /// The chrontab expression to parse. + /// The date and time to start from. + /// The representing the next occurence. + DateTime GetNextOccurrence(string cronTab, DateTime time); } diff --git a/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs index 09ea5058df..857b62bb26 100644 --- a/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs +++ b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Provides metadata about the entry assembly. +/// +public interface IEntryAssemblyMetadata { /// - /// Provides metadata about the entry assembly. + /// Gets the Name of entry assembly. /// - public interface IEntryAssemblyMetadata - { - /// - /// Gets the Name of entry assembly. - /// - public string Name { get; } + public string Name { get; } - /// - /// Gets the InformationalVersion string for entry assembly. - /// - public string InformationalVersion { get; } - } + /// + /// Gets the InformationalVersion string for entry assembly. + /// + public string InformationalVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs index 7bd8ab9ef2..451cf51bc3 100644 --- a/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for members +/// +public interface IMemberPasswordConfiguration : IPasswordConfiguration { - /// - /// The password configuration for members - /// - public interface IMemberPasswordConfiguration : IPasswordConfiguration - { - } } diff --git a/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs index acfe81ece9..e0e934f550 100644 --- a/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs @@ -1,50 +1,48 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Password configuration +/// +public interface IPasswordConfiguration { + /// + /// Gets a value for the minimum required length for the password. + /// + int RequiredLength { get; } /// - /// Password configuration + /// Gets a value indicating whether at least one non-letter or digit is required for the password. /// - public interface IPasswordConfiguration - { - /// - /// Gets a value for the minimum required length for the password. - /// - int RequiredLength { get; } + bool RequireNonLetterOrDigit { get; } - /// - /// Gets a value indicating whether at least one non-letter or digit is required for the password. - /// - bool RequireNonLetterOrDigit { get; } + /// + /// Gets a value indicating whether at least one digit is required for the password. + /// + bool RequireDigit { get; } - /// - /// Gets a value indicating whether at least one digit is required for the password. - /// - bool RequireDigit { get; } + /// + /// Gets a value indicating whether at least one lower-case character is required for the password. + /// + bool RequireLowercase { get; } - /// - /// Gets a value indicating whether at least one lower-case character is required for the password. - /// - bool RequireLowercase { get; } + /// + /// Gets a value indicating whether at least one upper-case character is required for the password. + /// + bool RequireUppercase { get; } - /// - /// Gets a value indicating whether at least one upper-case character is required for the password. - /// - bool RequireUppercase { get; } + /// + /// Gets a value for the password hash algorithm type. + /// + string HashAlgorithmType { get; } - /// - /// Gets a value for the password hash algorithm type. - /// - string HashAlgorithmType { get; } - - /// - /// Gets a value for the maximum failed access attempts before lockout. - /// - /// - /// TODO: This doesn't really belong here - /// - int MaxFailedAccessAttemptsBeforeLockout { get; } - } + /// + /// Gets a value for the maximum failed access attempts before lockout. + /// + /// + /// TODO: This doesn't really belong here + /// + int MaxFailedAccessAttemptsBeforeLockout { get; } } diff --git a/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs b/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs index 9dc5805423..4acbd1dbd8 100644 --- a/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs +++ b/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +[Obsolete("Not used anymore, will be removed in Umbraco 12")]public interface ITypeFinderSettings { - public interface ITypeFinderSettings - { - string AssembliesAcceptingLoadExceptions { get; } - } + string AssembliesAcceptingLoadExceptions { get; } } diff --git a/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs b/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs index 4a1e65f13f..5547639b11 100644 --- a/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs +++ b/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs @@ -1,10 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration -{ - /// - /// Represents an Umbraco configuration section which can be used to pass to UmbracoConfiguration.For{T} - /// - public interface IUmbracoConfigurationSection - { +namespace Umbraco.Cms.Core.Configuration; - } +/// +/// Represents an Umbraco configuration section which can be used to pass to UmbracoConfiguration.For{T} +/// +public interface IUmbracoConfigurationSection +{ } diff --git a/src/Umbraco.Core/Configuration/IUmbracoVersion.cs b/src/Umbraco.Core/Configuration/IUmbracoVersion.cs index 2758d9dabf..3672f28dae 100644 --- a/src/Umbraco.Core/Configuration/IUmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/IUmbracoVersion.cs @@ -1,46 +1,45 @@ -using System; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public interface IUmbracoVersion { - public interface IUmbracoVersion - { - /// - /// Gets the non-semantic version of the Umbraco code. - /// - Version Version { get; } + /// + /// Gets the non-semantic version of the Umbraco code. + /// + Version Version { get; } - /// - /// Gets the semantic version comments of the Umbraco code. - /// - string Comment { get; } + /// + /// Gets the semantic version comments of the Umbraco code. + /// + string Comment { get; } - /// - /// Gets the assembly version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// Is the one that the CLR checks for compatibility. Therefore, it changes only on - /// hard-breaking changes (for instance, on new major versions). - /// - Version? AssemblyVersion { get; } + /// + /// Gets the assembly version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + /// Is the one that the CLR checks for compatibility. Therefore, it changes only on + /// hard-breaking changes (for instance, on new major versions). + /// + /// + Version? AssemblyVersion { get; } - /// - /// Gets the assembly file version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// - Version? AssemblyFileVersion { get; } + /// + /// Gets the assembly file version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + Version? AssemblyFileVersion { get; } - /// - /// Gets the semantic version of the Umbraco code. - /// - /// - /// The semantic version is the value of the . - /// It is the full version of Umbraco, including comments. - /// - SemVersion SemanticVersion { get; } - - } + /// + /// Gets the semantic version of the Umbraco code. + /// + /// + /// The semantic version is the value of the . + /// It is the full version of Umbraco, including comments. + /// + SemVersion SemanticVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs index db27103a67..c4f86232d3 100644 --- a/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for back office users +/// +public interface IUserPasswordConfiguration : IPasswordConfiguration { - /// - /// The password configuration for back office users - /// - public interface IUserPasswordConfiguration : IPasswordConfiguration - { - } } diff --git a/src/Umbraco.Core/Configuration/LocalTempStorage.cs b/src/Umbraco.Core/Configuration/LocalTempStorage.cs index 696ec7900e..8be409fc2b 100644 --- a/src/Umbraco.Core/Configuration/LocalTempStorage.cs +++ b/src/Umbraco.Core/Configuration/LocalTempStorage.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public enum LocalTempStorage { - public enum LocalTempStorage - { - Unknown = 0, - Default, - EnvironmentTemp - } + Unknown = 0, + Default, + EnvironmentTemp, } diff --git a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs index c7ce20454f..33471ced16 100644 --- a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for members +/// +public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration { - /// - /// The password configuration for members - /// - public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration + public MemberPasswordConfiguration(IMemberPasswordConfiguration configSettings) + : base(configSettings) { - public MemberPasswordConfiguration(IMemberPasswordConfiguration configSettings) - : base(configSettings) - { - } } } diff --git a/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs b/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs index 5e4f371c73..3373b7a778 100644 --- a/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs @@ -1,17 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for active directory settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigActiveDirectory)] +[Obsolete("This is not used anymore. Will be removed in Umbraco 12")]public class ActiveDirectorySettings { /// - /// Typed configuration options for active directory settings. + /// Gets or sets a value for the Active Directory domain. /// - [UmbracoOptions(Constants.Configuration.ConfigActiveDirectory)] - public class ActiveDirectorySettings - { - /// - /// Gets or sets a value for the Active Directory domain. - /// - public string? Domain { get; set; } - } + public string? Domain { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs b/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs index 211b6b3d83..5f42aac545 100644 --- a/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs +++ b/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs @@ -1,16 +1,11 @@ -using System; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +[AttributeUsage(AttributeTargets.Class)] +public class UmbracoOptionsAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class UmbracoOptionsAttribute : Attribute - { - public string ConfigurationKey { get; } - public bool BindNonPublicProperties { get; set; } + public UmbracoOptionsAttribute(string configurationKey) => ConfigurationKey = configurationKey; - public UmbracoOptionsAttribute(string configurationKey) - { - ConfigurationKey = configurationKey; - } - } + public string ConfigurationKey { get; } + + public bool BindNonPublicProperties { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs index 054619d843..b743fdcdd2 100644 --- a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs @@ -1,27 +1,37 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -using System.Net; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for basic authentication settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigBasicAuth)] +public class BasicAuthSettings { + private const bool StaticEnabled = false; + /// - /// Typed configuration options for basic authentication settings. + /// Gets or sets a value indicating whether to keep the user logged in. /// - [UmbracoOptions(Constants.Configuration.ConfigBasicAuth)] - public class BasicAuthSettings - { - private const bool StaticEnabled = false; - - /// - /// Gets or sets a value indicating whether to keep the user logged in. - /// - [DefaultValue(StaticEnabled)] - public bool Enabled { get; set; } = StaticEnabled; + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; - public string[] AllowedIPs { get; set; } = Array.Empty(); - } + public string[] AllowedIPs { get; set; } = Array.Empty(); + public SharedSecret SharedSecret { get; set; } = new SharedSecret(); + + public bool RedirectToLoginPage { get; set; } = false; + +} + +public class SharedSecret +{ + private const string StaticHeaderName = "X-Authentication-Shared-Secret"; + + [DefaultValue(StaticHeaderName)] + public string? HeaderName { get; set; } = StaticHeaderName; + public string? Value { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/CharItem.cs b/src/Umbraco.Core/Configuration/Models/CharItem.cs index a74b0c0a8b..625033a82a 100644 --- a/src/Umbraco.Core/Configuration/Models/CharItem.cs +++ b/src/Umbraco.Core/Configuration/Models/CharItem.cs @@ -1,17 +1,16 @@ using Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Configuration.Models -{ - public class CharItem : IChar - { - /// - /// The character to replace - /// - public string Char { get; set; } = null!; +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// The replacement character - /// - public string Replacement { get; set; } = null!; - } +public class CharItem : IChar +{ + /// + /// The character to replace + /// + public string Char { get; set; } = null!; + + /// + /// The replacement character + /// + public string Replacement { get; set; } = null!; } diff --git a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs index 333b655626..a5161eca86 100644 --- a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs @@ -7,20 +7,18 @@ namespace Umbraco.Cms.Core.Configuration.Models; /// public class ConnectionStrings // TODO: Rename to [Umbraco]ConnectionString (since v10 this only contains a single connection string) { - private string? _connectionString; - /// - /// The default provider name when not present in configuration. + /// The default provider name when not present in configuration. /// public const string DefaultProviderName = "Microsoft.Data.SqlClient"; /// - /// The DataDirectory placeholder. + /// The DataDirectory placeholder. /// public const string DataDirectoryPlaceholder = ConfigurationExtensions.DataDirectoryPlaceholder; /// - /// The postfix used to identify a connection strings provider setting. + /// The postfix used to identify a connection strings provider setting. /// public const string ProviderNamePostfix = ConfigurationExtensions.ProviderNamePostfix; @@ -39,14 +37,7 @@ public class ConnectionStrings // TODO: Rename to [Umbraco]ConnectionString (sin /// /// The connection string. /// - /// - /// When set, the will be replaced with the actual physical path. - /// - public string? ConnectionString - { - get => _connectionString; - set => _connectionString = ConfigurationExtensions.ReplaceDataDirectoryPlaceholder(value); - } + public string? ConnectionString { get; set; } /// /// Gets or sets the name of the provider. diff --git a/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs index 19d636ed34..74376a3ed2 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs @@ -1,36 +1,35 @@ using System.ComponentModel; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Typed configuration options for content dashboard settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigContentDashboard)] +public class ContentDashboardSettings { + private const string DefaultContentDashboardPath = "cms"; + /// - /// Typed configuration options for content dashboard settings. + /// Gets a value indicating whether the content dashboard should be available to all users. /// - [UmbracoOptions(Constants.Configuration.ConfigContentDashboard)] - public class ContentDashboardSettings - { - private const string DefaultContentDashboardPath = "cms"; + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers { get; set; } = true; - /// - /// Gets a value indicating whether the content dashboard should be available to all users. - /// - /// - /// true if the dashboard is visible for all user groups; otherwise, false - /// and the default access rules for that dashboard will be in use. - /// - public bool AllowContentDashboardAccessToAllUsers { get; set; } = true; + /// + /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. + /// + /// The URL path. + [DefaultValue(DefaultContentDashboardPath)] + public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; - /// - /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. - /// - /// The URL path. - [DefaultValue(DefaultContentDashboardPath)] - public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; - - /// - /// Gets the allowed addresses to retrieve data for the content dashboard. - /// - /// The URLs. - public string[]? ContentDashboardUrlAllowlist { get; set; } - } + /// + /// Gets the allowed addresses to retrieve data for the content dashboard. + /// + /// The URLs. + public string[]? ContentDashboardUrlAllowlist { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs b/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs index 6a6d3a8e61..415240e017 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs @@ -1,55 +1,53 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Configuration.Models.Validation; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration for a content error page. +/// +public class ContentErrorPage : ValidatableEntryBase { /// - /// Typed configuration for a content error page. + /// Gets or sets a value for the content Id. /// - public class ContentErrorPage : ValidatableEntryBase - { - /// - /// Gets or sets a value for the content Id. - /// - public int ContentId { get; set; } + public int ContentId { get; set; } - /// - /// Gets or sets a value for the content key. - /// - public Guid ContentKey { get; set; } + /// + /// Gets or sets a value for the content key. + /// + public Guid ContentKey { get; set; } - /// - /// Gets or sets a value for the content XPath. - /// - public string? ContentXPath { get; set; } + /// + /// Gets or sets a value for the content XPath. + /// + public string? ContentXPath { get; set; } - /// - /// Gets a value indicating whether the field is populated. - /// - public bool HasContentId => ContentId != 0; + /// + /// Gets a value indicating whether the field is populated. + /// + public bool HasContentId => ContentId != 0; - /// - /// Gets a value indicating whether the field is populated. - /// - public bool HasContentKey => ContentKey != Guid.Empty; + /// + /// Gets a value indicating whether the field is populated. + /// + public bool HasContentKey => ContentKey != Guid.Empty; - /// - /// Gets a value indicating whether the field is populated. - /// - public bool HasContentXPath => !string.IsNullOrEmpty(ContentXPath); + /// + /// Gets a value indicating whether the field is populated. + /// + public bool HasContentXPath => !string.IsNullOrEmpty(ContentXPath); - /// - /// Gets or sets a value for the content culture. - /// - [Required] - public string Culture { get; set; } = null!; + /// + /// Gets or sets a value for the content culture. + /// + [Required] + public string Culture { get; set; } = null!; - internal override bool IsValid() => - base.IsValid() && - ((HasContentId ? 1 : 0) + (HasContentKey ? 1 : 0) + (HasContentXPath ? 1 : 0) == 1); - } + internal override bool IsValid() => + base.IsValid() && + (HasContentId ? 1 : 0) + (HasContentKey ? 1 : 0) + (HasContentXPath ? 1 : 0) == 1; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs index 2e109fe310..4634f6efb9 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs @@ -1,39 +1,37 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for content imaging settings. +/// +public class ContentImagingSettings { - /// - /// Typed configuration options for content imaging settings. - /// - public class ContentImagingSettings + internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; + + private static readonly ImagingAutoFillUploadField[] DefaultImagingAutoFillUploadField = { - private static readonly ImagingAutoFillUploadField[] s_defaultImagingAutoFillUploadField = + new() { - new ImagingAutoFillUploadField - { - Alias = Constants.Conventions.Media.File, - WidthFieldAlias = Constants.Conventions.Media.Width, - HeightFieldAlias = Constants.Conventions.Media.Height, - ExtensionFieldAlias = Constants.Conventions.Media.Extension, - LengthFieldAlias = Constants.Conventions.Media.Bytes, - } - }; + Alias = Constants.Conventions.Media.File, + WidthFieldAlias = Constants.Conventions.Media.Width, + HeightFieldAlias = Constants.Conventions.Media.Height, + ExtensionFieldAlias = Constants.Conventions.Media.Extension, + LengthFieldAlias = Constants.Conventions.Media.Bytes, + }, + }; - internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; + /// + /// Gets or sets a value for the collection of accepted image file extensions. + /// + [DefaultValue(StaticImageFileTypes)] + public string[] ImageFileTypes { get; set; } = StaticImageFileTypes.Split(','); - /// - /// Gets or sets a value for the collection of accepted image file extensions. - /// - [DefaultValue(StaticImageFileTypes)] - public string[] ImageFileTypes { get; set; } = StaticImageFileTypes.Split(','); - - /// - /// Gets or sets a value for the imaging autofill following media file upload fields. - /// - public ImagingAutoFillUploadField[] AutoFillImageProperties { get; set; } = s_defaultImagingAutoFillUploadField; - } + /// + /// Gets or sets a value for the imaging autofill following media file upload fields. + /// + public ImagingAutoFillUploadField[] AutoFillImageProperties { get; set; } = DefaultImagingAutoFillUploadField; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs index c23eac75b2..ce5c3aebf3 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs @@ -3,24 +3,23 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for content notification settings. +/// +public class ContentNotificationSettings { + internal const bool StaticDisableHtmlEmail = false; + /// - /// Typed configuration options for content notification settings. + /// Gets or sets a value for the email address for notifications. /// - public class ContentNotificationSettings - { - internal const bool StaticDisableHtmlEmail = false; + public string? Email { get; set; } - /// - /// Gets or sets a value for the email address for notifications. - /// - public string? Email { get; set; } - - /// - /// Gets or sets a value indicating whether HTML email notifications should be disabled. - /// - [DefaultValue(StaticDisableHtmlEmail)] - public bool DisableHtmlEmail { get; set; } = StaticDisableHtmlEmail; - } + /// + /// Gets or sets a value indicating whether HTML email notifications should be disabled. + /// + [DefaultValue(StaticDisableHtmlEmail)] + public bool DisableHtmlEmail { get; set; } = StaticDisableHtmlEmail; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index e6e5c7006f..f0532a7203 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -1,23 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Macros; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for content settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigContent)] - public class ContentSettings - { +namespace Umbraco.Cms.Core.Configuration.Models; - internal const bool StaticResolveUrlsFromTextString = false; - internal const string StaticDefaultPreviewBadge = - @" +/// +/// Typed configuration options for content settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigContent)] +public class ContentSettings +{ + internal const bool StaticResolveUrlsFromTextString = false; + + internal const string StaticDefaultPreviewBadge = + @"
Preview mode @@ -151,98 +149,97 @@ namespace Umbraco.Cms.Core.Configuration.Models "; - internal const string StaticMacroErrors = "Inline"; - internal const string StaticDisallowedUploadFiles = "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx"; - internal const bool StaticShowDeprecatedPropertyEditors = false; - internal const string StaticLoginBackgroundImage = "assets/img/login.jpg"; - internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg"; - internal const bool StaticHideBackOfficeLogo = false; - internal const bool StaticDisableDeleteWhenReferenced = false; - internal const bool StaticDisableUnpublishWhenReferenced = false; + internal const string StaticMacroErrors = "Inline"; + internal const string StaticDisallowedUploadFiles = "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx"; + internal const bool StaticShowDeprecatedPropertyEditors = false; + internal const string StaticLoginBackgroundImage = "assets/img/login.jpg"; + internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg"; + internal const bool StaticHideBackOfficeLogo = false; + internal const bool StaticDisableDeleteWhenReferenced = false; + internal const bool StaticDisableUnpublishWhenReferenced = false; - /// - /// Gets or sets a value for the content notification settings. - /// - public ContentNotificationSettings Notifications { get; set; } = new ContentNotificationSettings(); + /// + /// Gets or sets a value for the content notification settings. + /// + public ContentNotificationSettings Notifications { get; set; } = new(); - /// - /// Gets or sets a value for the content imaging settings. - /// - public ContentImagingSettings Imaging { get; set; } = new ContentImagingSettings(); + /// + /// Gets or sets a value for the content imaging settings. + /// + public ContentImagingSettings Imaging { get; set; } = new(); - /// - /// Gets or sets a value indicating whether URLs should be resolved from text strings. - /// - [DefaultValue(StaticResolveUrlsFromTextString)] - public bool ResolveUrlsFromTextString { get; set; } = StaticResolveUrlsFromTextString; + /// + /// Gets or sets a value indicating whether URLs should be resolved from text strings. + /// + [DefaultValue(StaticResolveUrlsFromTextString)] + public bool ResolveUrlsFromTextString { get; set; } = StaticResolveUrlsFromTextString; - /// - /// Gets or sets a value for the collection of error pages. - /// - public ContentErrorPage[] Error404Collection { get; set; } = Array.Empty(); + /// + /// Gets or sets a value for the collection of error pages. + /// + public ContentErrorPage[] Error404Collection { get; set; } = Array.Empty(); - /// - /// Gets or sets a value for the preview badge mark-up. - /// - [DefaultValue(StaticDefaultPreviewBadge)] - public string PreviewBadge { get; set; } = StaticDefaultPreviewBadge; + /// + /// Gets or sets a value for the preview badge mark-up. + /// + [DefaultValue(StaticDefaultPreviewBadge)] + public string PreviewBadge { get; set; } = StaticDefaultPreviewBadge; - /// - /// Gets or sets a value for the macro error behaviour. - /// - [DefaultValue(StaticMacroErrors)] - public MacroErrorBehaviour MacroErrors { get; set; } = Enum.Parse(StaticMacroErrors); + /// + /// Gets or sets a value for the macro error behaviour. + /// + [DefaultValue(StaticMacroErrors)] + public MacroErrorBehaviour MacroErrors { get; set; } = Enum.Parse(StaticMacroErrors); - /// - /// Gets or sets a value for the collection of file extensions that are disallowed for upload. - /// - [DefaultValue(StaticDisallowedUploadFiles)] - public IEnumerable DisallowedUploadFiles { get; set; } = StaticDisallowedUploadFiles.Split(','); + /// + /// Gets or sets a value for the collection of file extensions that are disallowed for upload. + /// + [DefaultValue(StaticDisallowedUploadFiles)] + public IEnumerable DisallowedUploadFiles { get; set; } = StaticDisallowedUploadFiles.Split(','); - /// - /// Gets or sets a value for the collection of file extensions that are allowed for upload. - /// - public IEnumerable AllowedUploadFiles { get; set; } = Array.Empty(); + /// + /// Gets or sets a value for the collection of file extensions that are allowed for upload. + /// + public IEnumerable AllowedUploadFiles { get; set; } = Array.Empty(); - /// - /// Gets or sets a value indicating whether deprecated property editors should be shown. - /// - [DefaultValue(StaticShowDeprecatedPropertyEditors)] - public bool ShowDeprecatedPropertyEditors { get; set; } = StaticShowDeprecatedPropertyEditors; + /// + /// Gets or sets a value indicating whether deprecated property editors should be shown. + /// + [DefaultValue(StaticShowDeprecatedPropertyEditors)] + public bool ShowDeprecatedPropertyEditors { get; set; } = StaticShowDeprecatedPropertyEditors; - /// - /// Gets or sets a value for the path to the login screen background image. - /// - [DefaultValue(StaticLoginBackgroundImage)] - public string LoginBackgroundImage { get; set; } = StaticLoginBackgroundImage; + /// + /// Gets or sets a value for the path to the login screen background image. + /// + [DefaultValue(StaticLoginBackgroundImage)] + public string LoginBackgroundImage { get; set; } = StaticLoginBackgroundImage; - /// - /// Gets or sets a value for the path to the login screen logo image. - /// - [DefaultValue(StaticLoginLogoImage)] - public string LoginLogoImage { get; set; } = StaticLoginLogoImage; + /// + /// Gets or sets a value for the path to the login screen logo image. + /// + [DefaultValue(StaticLoginLogoImage)] + public string LoginLogoImage { get; set; } = StaticLoginLogoImage; - /// - /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not. - /// - [DefaultValue(StaticHideBackOfficeLogo)] - public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo; + /// + /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not. + /// + [DefaultValue(StaticHideBackOfficeLogo)] + public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo; - /// - /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items. - /// - [DefaultValue(StaticDisableDeleteWhenReferenced)] - public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced; + /// + /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items. + /// + [DefaultValue(StaticDisableDeleteWhenReferenced)] + public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced; - /// - /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items. - /// - [DefaultValue(StaticDisableUnpublishWhenReferenced)] - public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced; + /// + /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items. + /// + [DefaultValue(StaticDisableUnpublishWhenReferenced)] + public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced; - /// - /// Get or sets the model representing the global content version cleanup policy - /// - public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new ContentVersionCleanupPolicySettings(); - } + /// + /// Get or sets the model representing the global content version cleanup policy + /// + public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new(); } diff --git a/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs index bd460058eb..ed721382a9 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs @@ -1,33 +1,31 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Model representing the global content version cleanup policy +/// +public class ContentVersionCleanupPolicySettings { + private const bool StaticEnableCleanup = false; + private const int StaticKeepAllVersionsNewerThanDays = 7; + private const int StaticKeepLatestVersionPerDayForDays = 90; + /// - /// Model representing the global content version cleanup policy + /// Gets or sets a value indicating whether or not the cleanup job should be executed. /// - public class ContentVersionCleanupPolicySettings - { - private const bool StaticEnableCleanup = false; - private const int StaticKeepAllVersionsNewerThanDays = 7; - private const int StaticKeepLatestVersionPerDayForDays = 90; + [DefaultValue(StaticEnableCleanup)] + public bool EnableCleanup { get; set; } = StaticEnableCleanup; - /// - /// Gets or sets a value indicating whether or not the cleanup job should be executed. - /// - [DefaultValue(StaticEnableCleanup)] - public bool EnableCleanup { get; set; } = StaticEnableCleanup; + /// + /// Gets or sets the number of days where all historical content versions are kept. + /// + [DefaultValue(StaticKeepAllVersionsNewerThanDays)] + public int KeepAllVersionsNewerThanDays { get; set; } = StaticKeepAllVersionsNewerThanDays; - /// - /// Gets or sets the number of days where all historical content versions are kept. - /// - [DefaultValue(StaticKeepAllVersionsNewerThanDays)] - public int KeepAllVersionsNewerThanDays { get; set; } = StaticKeepAllVersionsNewerThanDays; - - /// - /// Gets or sets the number of days where the latest historical content version for that day are kept. - /// - [DefaultValue(StaticKeepLatestVersionPerDayForDays)] - public int KeepLatestVersionPerDayForDays { get; set; } = StaticKeepLatestVersionPerDayForDays; - - } + /// + /// Gets or sets the number of days where the latest historical content version for that day are kept. + /// + [DefaultValue(StaticKeepLatestVersionPerDayForDays)] + public int KeepLatestVersionPerDayForDays { get; set; } = StaticKeepLatestVersionPerDayForDays; } diff --git a/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs b/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs index 58810a3462..052d37c5fe 100644 --- a/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs @@ -3,27 +3,26 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for core debug settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigCoreDebug)] +public class CoreDebugSettings { + internal const bool StaticLogIncompletedScopes = false; + internal const bool StaticDumpOnTimeoutThreadAbort = false; + /// - /// Typed configuration options for core debug settings. + /// Gets or sets a value indicating whether incompleted scopes should be logged. /// - [UmbracoOptions(Constants.Configuration.ConfigCoreDebug)] - public class CoreDebugSettings - { - internal const bool StaticLogIncompletedScopes = false; - internal const bool StaticDumpOnTimeoutThreadAbort = false; + [DefaultValue(StaticLogIncompletedScopes)] + public bool LogIncompletedScopes { get; set; } = StaticLogIncompletedScopes; - /// - /// Gets or sets a value indicating whether incompleted scopes should be logged. - /// - [DefaultValue(StaticLogIncompletedScopes)] - public bool LogIncompletedScopes { get; set; } = StaticLogIncompletedScopes; - - /// - /// Gets or sets a value indicating whether memory dumps on thread abort should be taken. - /// - [DefaultValue(StaticDumpOnTimeoutThreadAbort)] - public bool DumpOnTimeoutThreadAbort { get; set; } = StaticDumpOnTimeoutThreadAbort; - } + /// + /// Gets or sets a value indicating whether memory dumps on thread abort should be taken. + /// + [DefaultValue(StaticDumpOnTimeoutThreadAbort)] + public bool DumpOnTimeoutThreadAbort { get; set; } = StaticDumpOnTimeoutThreadAbort; } diff --git a/src/Umbraco.Core/Configuration/Models/DataTypeChangeMode.cs b/src/Umbraco.Core/Configuration/Models/DataTypeChangeMode.cs new file mode 100644 index 0000000000..870c24568a --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/DataTypeChangeMode.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Configuration.Models; + +public enum DataTypeChangeMode +{ + True, + False, + FalseWithHelpText +} diff --git a/src/Umbraco.Core/Configuration/Models/DataTypesSettings.cs b/src/Umbraco.Core/Configuration/Models/DataTypesSettings.cs new file mode 100644 index 0000000000..337abc36b4 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/DataTypesSettings.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigDataTypes)] +public class DataTypesSettings +{ + internal const DataTypeChangeMode StaticDataTypeChangeMode = DataTypeChangeMode.True; + + /// + /// Gets or sets a value indicating if data types can be changed after they've been used. + /// + [DefaultValue(StaticDataTypeChangeMode)] + public DataTypeChangeMode CanBeChanged { get; set; } = StaticDataTypeChangeMode; +} diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs index f1320a199f..a083b164a8 100644 --- a/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs @@ -1,43 +1,43 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for database server messaging settings. +/// +public class DatabaseServerMessengerSettings { + internal const int StaticMaxProcessingInstructionCount = 1000; + internal const string StaticTimeToRetainInstructions = "2.00:00:00"; // TimeSpan.FromDays(2); + internal const string StaticTimeBetweenSyncOperations = "00:00:05"; // TimeSpan.FromSeconds(5); + internal const string StaticTimeBetweenPruneOperations = "00:01:00"; // TimeSpan.FromMinutes(1); + /// - /// Typed configuration options for database server messaging settings. + /// Gets or sets a value for the maximum number of instructions that can be processed at startup; otherwise the server + /// cold-boots (rebuilds its caches). /// - public class DatabaseServerMessengerSettings - { - internal const int StaticMaxProcessingInstructionCount = 1000; - internal const string StaticTimeToRetainInstructions = "2.00:00:00"; // TimeSpan.FromDays(2); - internal const string StaticTimeBetweenSyncOperations = "00:00:05"; // TimeSpan.FromSeconds(5); - internal const string StaticTimeBetweenPruneOperations = "00:01:00"; // TimeSpan.FromMinutes(1); + [DefaultValue(StaticMaxProcessingInstructionCount)] + public int MaxProcessingInstructionCount { get; set; } = StaticMaxProcessingInstructionCount; - /// - /// Gets or sets a value for the maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches). - /// - [DefaultValue(StaticMaxProcessingInstructionCount)] - public int MaxProcessingInstructionCount { get; set; } = StaticMaxProcessingInstructionCount; + /// + /// Gets or sets a value for the time to keep instructions in the database; records older than this number will be + /// pruned. + /// + [DefaultValue(StaticTimeToRetainInstructions)] + public TimeSpan TimeToRetainInstructions { get; set; } = TimeSpan.Parse(StaticTimeToRetainInstructions); - /// - /// Gets or sets a value for the time to keep instructions in the database; records older than this number will be pruned. - /// - [DefaultValue(StaticTimeToRetainInstructions)] - public TimeSpan TimeToRetainInstructions { get; set; } = TimeSpan.Parse(StaticTimeToRetainInstructions); + /// + /// Gets or sets a value for the time to wait between each sync operations. + /// + [DefaultValue(StaticTimeBetweenSyncOperations)] + public TimeSpan TimeBetweenSyncOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenSyncOperations); - /// - /// Gets or sets a value for the time to wait between each sync operations. - /// - [DefaultValue(StaticTimeBetweenSyncOperations)] - public TimeSpan TimeBetweenSyncOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenSyncOperations); - - /// - /// Gets or sets a value for the time to wait between each prune operations. - /// - [DefaultValue(StaticTimeBetweenPruneOperations)] - public TimeSpan TimeBetweenPruneOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenPruneOperations); - } + /// + /// Gets or sets a value for the time to wait between each prune operations. + /// + [DefaultValue(StaticTimeBetweenPruneOperations)] + public TimeSpan TimeBetweenPruneOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenPruneOperations); } diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs index 91d293f272..80aefeae6e 100644 --- a/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs @@ -1,29 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for database server registrar settings. +/// +public class DatabaseServerRegistrarSettings { + internal const string StaticWaitTimeBetweenCalls = "00:01:00"; + internal const string StaticStaleServerTimeout = "00:02:00"; + /// - /// Typed configuration options for database server registrar settings. + /// Gets or sets a value for the amount of time to wait between calls to the database on the background thread. /// - public class DatabaseServerRegistrarSettings - { - internal const string StaticWaitTimeBetweenCalls = "00:01:00"; - internal const string StaticStaleServerTimeout = "00:02:00"; + [DefaultValue(StaticWaitTimeBetweenCalls)] + public TimeSpan WaitTimeBetweenCalls { get; set; } = TimeSpan.Parse(StaticWaitTimeBetweenCalls); - /// - /// Gets or sets a value for the amount of time to wait between calls to the database on the background thread. - /// - [DefaultValue(StaticWaitTimeBetweenCalls)] - public TimeSpan WaitTimeBetweenCalls { get; set; } = TimeSpan.Parse(StaticWaitTimeBetweenCalls); - - /// - /// Gets or sets a value for the time span to wait before considering a server stale, after it has last been accessed. - /// - [DefaultValue(StaticStaleServerTimeout)] - public TimeSpan StaleServerTimeout { get; set; } = TimeSpan.Parse(StaticStaleServerTimeout); - } + /// + /// Gets or sets a value for the time span to wait before considering a server stale, after it has last been accessed. + /// + [DefaultValue(StaticStaleServerTimeout)] + public TimeSpan StaleServerTimeout { get; set; } = TimeSpan.Parse(StaticStaleServerTimeout); } diff --git a/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs index a24ec5b923..f055aca7ab 100644 --- a/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs @@ -1,28 +1,25 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for disabled healthcheck settings. +/// +public class DisabledHealthCheckSettings { /// - /// Typed configuration options for disabled healthcheck settings. + /// Gets or sets a value for the healthcheck Id to disable. /// - public class DisabledHealthCheckSettings - { - /// - /// Gets or sets a value for the healthcheck Id to disable. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Gets or sets a value for the date the healthcheck was disabled. - /// - public DateTime DisabledOn { get; set; } + /// + /// Gets or sets a value for the date the healthcheck was disabled. + /// + public DateTime DisabledOn { get; set; } - /// - /// Gets or sets a value for Id of the user that disabled the healthcheck. - /// - public int DisabledBy { get; set; } - } + /// + /// Gets or sets a value for Id of the user that disabled the healthcheck. + /// + public int DisabledBy { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs b/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs index 0d48453071..ebf99c03dd 100644 --- a/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs @@ -3,20 +3,19 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for exception filter settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigExceptionFilter)] - public class ExceptionFilterSettings - { - internal const bool StaticDisabled = false; +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// Gets or sets a value indicating whether the exception filter is disabled. - /// - [DefaultValue(StaticDisabled)] - public bool Disabled { get; set; } = StaticDisabled; - } +/// +/// Typed configuration options for exception filter settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigExceptionFilter)] +public class ExceptionFilterSettings +{ + internal const bool StaticDisabled = false; + + /// + /// Gets or sets a value indicating whether the exception filter is disabled. + /// + [DefaultValue(StaticDisabled)] + public bool Disabled { get; set; } = StaticDisabled; } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 8d00d58198..2665c0738f 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -1,227 +1,251 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for global settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigGlobal)] +public class GlobalSettings { + internal const string + StaticReservedPaths = + "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; // must end with a comma! + + internal const string StaticReservedUrls = "~/.well-known,"; // must end with a comma! + internal const string StaticTimeOut = "00:20:00"; + internal const string StaticDefaultUILanguage = "en-US"; + internal const bool StaticHideTopLevelNodeFromPath = true; + internal const bool StaticUseHttps = false; + internal const int StaticVersionCheckPeriod = 7; + internal const string StaticUmbracoPath = Constants.System.DefaultUmbracoPath; + internal const string StaticIconsPath = "umbraco/assets/icons"; + internal const string StaticUmbracoCssPath = "~/css"; + internal const string StaticUmbracoScriptsPath = "~/scripts"; + internal const string StaticUmbracoMediaPath = "~/media"; + internal const bool StaticInstallMissingDatabase = false; + internal const bool StaticDisableElectionForSingleServer = false; + internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; + internal const string StaticDistributedLockingReadLockDefaultTimeout = "00:01:00"; + internal const string StaticDistributedLockingWriteLockDefaultTimeout = "00:00:05"; + internal const bool StaticSanitizeTinyMce = false; + internal const int StaticMainDomReleaseSignalPollingInterval = 2000; + private const bool StaticForceCombineUrlPathLeftToRight = true; + /// - /// Typed configuration options for global settings. + /// Gets or sets a value for the reserved URLs (must end with a comma). /// - [UmbracoOptions(Constants.Configuration.ConfigGlobal)] - public class GlobalSettings - { - internal const string StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; // must end with a comma! - internal const string StaticReservedUrls = "~/.well-known,"; // must end with a comma! - internal const string StaticTimeOut = "00:20:00"; - internal const string StaticDefaultUILanguage = "en-US"; - internal const bool StaticHideTopLevelNodeFromPath = true; - internal const bool StaticUseHttps = false; - internal const int StaticVersionCheckPeriod = 7; - internal const string StaticUmbracoPath = Constants.System.DefaultUmbracoPath; - internal const string StaticIconsPath = "umbraco/assets/icons"; - internal const string StaticUmbracoCssPath = "~/css"; - internal const string StaticUmbracoScriptsPath = "~/scripts"; - internal const string StaticUmbracoMediaPath = "~/media"; - internal const bool StaticInstallMissingDatabase = false; - internal const bool StaticDisableElectionForSingleServer = false; - internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; - internal const string StaticDistributedLockingReadLockDefaultTimeout = "00:01:00"; - internal const string StaticDistributedLockingWriteLockDefaultTimeout = "00:00:05"; - internal const bool StaticSanitizeTinyMce = false; - internal const int StaticMainDomReleaseSignalPollingInterval = 2000; + [DefaultValue(StaticReservedUrls)] + public string ReservedUrls { get; set; } = StaticReservedUrls; - /// - /// Gets or sets a value for the reserved URLs (must end with a comma). - /// - [DefaultValue(StaticReservedUrls)] - public string ReservedUrls { get; set; } = StaticReservedUrls; + /// + /// Gets or sets a value for the reserved paths (must end with a comma). + /// + [DefaultValue(StaticReservedPaths)] + public string ReservedPaths { get; set; } = StaticReservedPaths; - /// - /// Gets or sets a value for the reserved paths (must end with a comma). - /// - [DefaultValue(StaticReservedPaths)] - public string ReservedPaths { get; set; } = StaticReservedPaths; + /// + /// Gets or sets a value for the back-office login timeout. + /// + [DefaultValue(StaticTimeOut)] + public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut); - /// - /// Gets or sets a value for the back-office login timeout. - /// - [DefaultValue(StaticTimeOut)] - public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut); + /// + /// Gets or sets a value for the default UI language. + /// + [DefaultValue(StaticDefaultUILanguage)] + public string DefaultUILanguage { get; set; } = StaticDefaultUILanguage; - /// - /// Gets or sets a value for the default UI language. - /// - [DefaultValue(StaticDefaultUILanguage)] - public string DefaultUILanguage { get; set; } = StaticDefaultUILanguage; + /// + /// Gets or sets a value indicating whether to hide the top level node from the path. + /// + [DefaultValue(StaticHideTopLevelNodeFromPath)] + public bool HideTopLevelNodeFromPath { get; set; } = StaticHideTopLevelNodeFromPath; - /// - /// Gets or sets a value indicating whether to hide the top level node from the path. - /// - [DefaultValue(StaticHideTopLevelNodeFromPath)] - public bool HideTopLevelNodeFromPath { get; set; } = StaticHideTopLevelNodeFromPath; + /// + /// Gets or sets a value indicating whether HTTPS should be used. + /// + [DefaultValue(StaticUseHttps)] + public bool UseHttps { get; set; } = StaticUseHttps; - /// - /// Gets or sets a value indicating whether HTTPS should be used. - /// - [DefaultValue(StaticUseHttps)] - public bool UseHttps { get; set; } = StaticUseHttps; + /// + /// Gets or sets a value for the version check period in days. + /// + [DefaultValue(StaticVersionCheckPeriod)] + public int VersionCheckPeriod { get; set; } = StaticVersionCheckPeriod; - /// - /// Gets or sets a value for the version check period in days. - /// - [DefaultValue(StaticVersionCheckPeriod)] - public int VersionCheckPeriod { get; set; } = StaticVersionCheckPeriod; + /// + /// Gets or sets a value for the Umbraco back-office path. + /// + [DefaultValue(StaticUmbracoPath)] + public string UmbracoPath { get; set; } = StaticUmbracoPath; - /// - /// Gets or sets a value for the Umbraco back-office path. - /// - [DefaultValue(StaticUmbracoPath)] - public string UmbracoPath { get; set; } = StaticUmbracoPath; + /// + /// Gets or sets a value for the Umbraco icons path. + /// + /// + /// TODO: Umbraco cannot be hard coded here that is what UmbracoPath is for + /// so this should not be a normal get set it has to have dynamic ability to return the correct + /// path given UmbracoPath if this hasn't been explicitly set. + /// + [DefaultValue(StaticIconsPath)] + public string IconsPath { get; set; } = StaticIconsPath; - /// - /// Gets or sets a value for the Umbraco icons path. - /// - /// - /// TODO: Umbraco cannot be hard coded here that is what UmbracoPath is for - /// so this should not be a normal get set it has to have dynamic ability to return the correct - /// path given UmbracoPath if this hasn't been explicitly set. - /// - [DefaultValue(StaticIconsPath)] - public string IconsPath { get; set; } = StaticIconsPath; + /// + /// Gets or sets a value for the Umbraco CSS path. + /// + [DefaultValue(StaticUmbracoCssPath)] + public string UmbracoCssPath { get; set; } = StaticUmbracoCssPath; - /// - /// Gets or sets a value for the Umbraco CSS path. - /// - [DefaultValue(StaticUmbracoCssPath)] - public string UmbracoCssPath { get; set; } = StaticUmbracoCssPath; + /// + /// Gets or sets a value for the Umbraco scripts path. + /// + [DefaultValue(StaticUmbracoScriptsPath)] + public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath; - /// - /// Gets or sets a value for the Umbraco scripts path. - /// - [DefaultValue(StaticUmbracoScriptsPath)] - public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath; + /// + /// Gets or sets a value for the Umbraco media request path. + /// + [DefaultValue(StaticUmbracoMediaPath)] + public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath; - /// - /// Gets or sets a value for the Umbraco media request path. - /// - [DefaultValue(StaticUmbracoMediaPath)] - public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath; + /// + /// Gets or sets a value for the physical Umbraco media root path (falls back to when + /// empty). + /// + /// + /// If the value is a virtual path, it's resolved relative to the webroot. + /// + public string UmbracoMediaPhysicalRootPath { get; set; } = null!; - /// - /// Gets or sets a value for the physical Umbraco media root path (falls back to when empty). - /// - /// - /// If the value is a virtual path, it's resolved relative to the webroot. - /// - public string UmbracoMediaPhysicalRootPath { get; set; } = null!; + /// + /// Gets or sets a value indicating whether to install the database when it is missing. + /// + [DefaultValue(StaticInstallMissingDatabase)] + public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase; - /// - /// Gets or sets a value indicating whether to install the database when it is missing. - /// - [DefaultValue(StaticInstallMissingDatabase)] - public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase; + /// + /// Gets or sets a value indicating whether to disable the election for a single server. + /// + [DefaultValue(StaticDisableElectionForSingleServer)] + public bool DisableElectionForSingleServer { get; set; } = StaticDisableElectionForSingleServer; - /// - /// Gets or sets a value indicating whether to disable the election for a single server. - /// - [DefaultValue(StaticDisableElectionForSingleServer)] - public bool DisableElectionForSingleServer { get; set; } = StaticDisableElectionForSingleServer; + /// + /// Gets or sets a value for the database factory server version. + /// + public string DatabaseFactoryServerVersion { get; set; } = string.Empty; - /// - /// Gets or sets a value for the database factory server version. - /// - public string DatabaseFactoryServerVersion { get; set; } = string.Empty; + /// + /// Gets or sets a value for the main dom lock. + /// + public string MainDomLock { get; set; } = string.Empty; - /// - /// Gets or sets a value for the main dom lock. - /// - public string MainDomLock { get; set; } = string.Empty; + /// + /// Gets or sets a value to discriminate MainDom boundaries. + /// + /// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero + /// downtime deployments. + /// + /// + public string MainDomKeyDiscriminator { get; set; } = string.Empty; - /// - /// Gets or sets a value to discriminate MainDom boundaries. - /// - /// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero downtime deployments. - /// - /// - public string MainDomKeyDiscriminator { get; set; } = string.Empty; + /// + /// Gets or sets the duration (in milliseconds) for which the MainDomLock release signal polling task should sleep. + /// + /// + /// Doesn't apply to MainDomSemaphoreLock. + /// + /// The default value is 2000ms. + /// + /// + [DefaultValue(StaticMainDomReleaseSignalPollingInterval)] + public int MainDomReleaseSignalPollingInterval { get; set; } = StaticMainDomReleaseSignalPollingInterval; - /// - /// Gets or sets the duration (in milliseconds) for which the MainDomLock release signal polling task should sleep. - /// - /// - /// Doesn't apply to MainDomSemaphoreLock. - /// - /// The default value is 2000ms. - /// - /// - [DefaultValue(StaticMainDomReleaseSignalPollingInterval)] - public int MainDomReleaseSignalPollingInterval { get; set; } = StaticMainDomReleaseSignalPollingInterval; + /// + /// Gets or sets the telemetry ID. + /// + public string Id { get; set; } = string.Empty; - /// - /// Gets or sets the telemetry ID. - /// - public string Id { get; set; } = string.Empty; + /// + /// Gets or sets a value for the path to the no content view. + /// + [DefaultValue(StaticNoNodesViewPath)] + public string NoNodesViewPath { get; set; } = StaticNoNodesViewPath; - /// - /// Gets or sets a value for the path to the no content view. - /// - [DefaultValue(StaticNoNodesViewPath)] - public string NoNodesViewPath { get; set; } = StaticNoNodesViewPath; + /// + /// Gets or sets a value for the database server registrar settings. + /// + public DatabaseServerRegistrarSettings DatabaseServerRegistrar { get; set; } = new(); - /// - /// Gets or sets a value for the database server registrar settings. - /// - public DatabaseServerRegistrarSettings DatabaseServerRegistrar { get; set; } = new DatabaseServerRegistrarSettings(); + /// + /// Gets or sets a value for the database server messenger settings. + /// + public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new(); - /// - /// Gets or sets a value for the database server messenger settings. - /// - public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new DatabaseServerMessengerSettings(); + /// + /// Gets or sets a value for the SMTP settings. + /// + public SmtpSettings? Smtp { get; set; } - /// - /// Gets or sets a value for the SMTP settings. - /// - public SmtpSettings? Smtp { get; set; } + /// + /// Gets a value indicating whether SMTP is configured. + /// + public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); - /// - /// Gets a value indicating whether SMTP is configured. - /// - public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); + /// + /// Gets a value indicating whether there is a physical pickup directory configured. + /// + public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); - /// - /// Gets a value indicating whether there is a physical pickup directory configured. - /// - public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); + /// + /// Gets or sets a value indicating whether TinyMCE scripting sanitization should be applied. + /// + [DefaultValue(StaticSanitizeTinyMce)] + public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce; - /// - /// Gets or sets a value indicating whether TinyMCE scripting sanitization should be applied. - /// - [DefaultValue(StaticSanitizeTinyMce)] - public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce; + /// + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed read lock. + /// + /// + /// The default value is 60 seconds. + /// + [DefaultValue(StaticDistributedLockingReadLockDefaultTimeout)] + public TimeSpan DistributedLockingReadLockDefaultTimeout { get; set; } = + TimeSpan.Parse(StaticDistributedLockingReadLockDefaultTimeout); - /// - /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed read lock. - /// - /// - /// The default value is 60 seconds. - /// - [DefaultValue(StaticDistributedLockingReadLockDefaultTimeout)] - public TimeSpan DistributedLockingReadLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingReadLockDefaultTimeout); + /// + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed write lock. + /// + /// + /// The default value is 5 seconds. + /// + [DefaultValue(StaticDistributedLockingWriteLockDefaultTimeout)] + public TimeSpan DistributedLockingWriteLockDefaultTimeout { get; set; } = + TimeSpan.Parse(StaticDistributedLockingWriteLockDefaultTimeout); - /// - /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed write lock. - /// - /// - /// The default value is 5 seconds. - /// - [DefaultValue(StaticDistributedLockingWriteLockDefaultTimeout)] - public TimeSpan DistributedLockingWriteLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingWriteLockDefaultTimeout); + /// + /// Gets or sets a value representing the DistributedLockingMechanism to use. + /// + public string DistributedLockingMechanism { get; set; } = string.Empty; - /// - /// Gets or sets a value representing the DistributedLockingMechanism to use. - /// - public string DistributedLockingMechanism { get; set; } = string.Empty; - } + /// + /// Force url paths to be left to right, even when the culture has right to left text + /// + /// + /// For the following hierarchy + /// - Root (/ar) + /// - 1 (/ar/1) + /// - 2 (/ar/1/2) + /// - 3 (/ar/1/2/3) + /// - 3 (/ar/1/2/3/4) + /// When forced + /// - https://www.umbraco.com/ar/1/2/3/4 + /// when not + /// - https://www.umbraco.com/ar/4/3/2/1 + /// + [DefaultValue(StaticForceCombineUrlPathLeftToRight)] + public bool ForceCombineUrlPathLeftToRight { get; set; } = StaticForceCombineUrlPathLeftToRight; } diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs index 2fc621a482..c973f59025 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs @@ -1,42 +1,41 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.HealthChecks; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for healthcheck notification method settings. +/// +public class HealthChecksNotificationMethodSettings { + internal const bool StaticEnabled = false; + internal const string StaticVerbosity = "Summary"; // Enum + internal const bool StaticFailureOnly = false; + /// - /// Typed configuration options for healthcheck notification method settings. + /// Gets or sets a value indicating whether the health check notification method is enabled. /// - public class HealthChecksNotificationMethodSettings - { - internal const bool StaticEnabled = false; - internal const string StaticVerbosity = "Summary"; // Enum - internal const bool StaticFailureOnly = false; + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; - /// - /// Gets or sets a value indicating whether the health check notification method is enabled. - /// - [DefaultValue(StaticEnabled)] - public bool Enabled { get; set; } = StaticEnabled; + /// + /// Gets or sets a value for the health check notifications reporting verbosity. + /// + [DefaultValue(StaticVerbosity)] + public HealthCheckNotificationVerbosity Verbosity { get; set; } = + Enum.Parse(StaticVerbosity); - /// - /// Gets or sets a value for the health check notifications reporting verbosity. - /// - [DefaultValue(StaticVerbosity)] - public HealthCheckNotificationVerbosity Verbosity { get; set; } = Enum.Parse(StaticVerbosity); + /// + /// Gets or sets a value indicating whether the health check notifications should occur on failures only. + /// + [DefaultValue(StaticFailureOnly)] + public bool FailureOnly { get; set; } = StaticFailureOnly; - /// - /// Gets or sets a value indicating whether the health check notifications should occur on failures only. - /// - [DefaultValue(StaticFailureOnly)] - public bool FailureOnly { get; set; } = StaticFailureOnly; - - /// - /// Gets or sets a value providing provider specific settings for the health check notification method. - /// - public IDictionary Settings { get; set; } = new Dictionary(); - } + /// + /// Gets or sets a value providing provider specific settings for the health check notification method. + /// + public IDictionary Settings { get; set; } = new Dictionary(); } diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs index 6e082da19f..6e81c48c7c 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs @@ -1,46 +1,44 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for healthcheck notification settings. +/// +public class HealthChecksNotificationSettings { + internal const bool StaticEnabled = false; + internal const string StaticPeriod = "1.00:00:00"; // TimeSpan.FromHours(24); + /// - /// Typed configuration options for healthcheck notification settings. + /// Gets or sets a value indicating whether health check notifications are enabled. /// - public class HealthChecksNotificationSettings - { - internal const bool StaticEnabled = false; - internal const string StaticPeriod = "1.00:00:00"; //TimeSpan.FromHours(24); + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; - /// - /// Gets or sets a value indicating whether health check notifications are enabled. - /// - [DefaultValue(StaticEnabled)] - public bool Enabled { get; set; } = StaticEnabled; + /// + /// Gets or sets a value for the first run time of a healthcheck notification in crontab format. + /// + public string FirstRunTime { get; set; } = string.Empty; - /// - /// Gets or sets a value for the first run time of a healthcheck notification in crontab format. - /// - public string FirstRunTime { get; set; } = string.Empty; + /// + /// Gets or sets a value for the period of the healthcheck notification. + /// + [DefaultValue(StaticPeriod)] + public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod); - /// - /// Gets or sets a value for the period of the healthcheck notification. - /// - [DefaultValue(StaticPeriod)] - public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod); + /// + /// Gets or sets a value for the collection of health check notification methods. + /// + public IDictionary NotificationMethods { get; set; } = + new Dictionary(); - /// - /// Gets or sets a value for the collection of health check notification methods. - /// - public IDictionary NotificationMethods { get; set; } = new Dictionary(); - - /// - /// Gets or sets a value for the collection of health checks that are disabled for notifications. - /// - public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); - } + /// + /// Gets or sets a value for the collection of health checks that are disabled for notifications. + /// + public IEnumerable DisabledChecks { get; set; } = + Enumerable.Empty(); } diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs index 0d232b9a9b..6ae79e9743 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs @@ -1,25 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for healthchecks settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigHealthChecks)] +public class HealthChecksSettings { /// - /// Typed configuration options for healthchecks settings. + /// Gets or sets a value for the collection of healthchecks that are disabled. /// - [UmbracoOptions(Constants.Configuration.ConfigHealthChecks)] - public class HealthChecksSettings - { - /// - /// Gets or sets a value for the collection of healthchecks that are disabled. - /// - public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); + public IEnumerable DisabledChecks { get; set; } = + Enumerable.Empty(); - /// - /// Gets or sets a value for the healthcheck notification settings. - /// - public HealthChecksNotificationSettings Notification { get; set; } = new HealthChecksNotificationSettings(); - } + /// + /// Gets or sets a value for the healthcheck notification settings. + /// + public HealthChecksNotificationSettings Notification { get; set; } = new(); } diff --git a/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs index b608b5c155..01d028f883 100644 --- a/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigHelpPage)] +public class HelpPageSettings { - [UmbracoOptions(Constants.Configuration.ConfigHelpPage)] - public class HelpPageSettings - { - /// - /// Gets or sets the allowed addresses to retrieve data for the content dashboard. - /// - public string[]? HelpPageUrlAllowList { get; set; } - } + /// + /// Gets or sets the allowed addresses to retrieve data for the content dashboard. + /// + public string[]? HelpPageUrlAllowList { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs index 8f5f47a566..2329c73d66 100644 --- a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs @@ -3,38 +3,38 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for hosting settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigHosting)] +public class HostingSettings { + internal const string StaticLocalTempStorageLocation = "Default"; + internal const bool StaticDebug = false; + /// - /// Typed configuration options for hosting settings. + /// Gets or sets a value for the application virtual path. /// - [UmbracoOptions(Constants.Configuration.ConfigHosting)] - public class HostingSettings - { - internal const string StaticLocalTempStorageLocation = "Default"; - internal const bool StaticDebug = false; + public string? ApplicationVirtualPath { get; set; } - /// - /// Gets or sets a value for the application virtual path. - /// - public string? ApplicationVirtualPath { get; set; } + /// + /// Gets or sets a value for the location of temporary files. + /// + [DefaultValue(StaticLocalTempStorageLocation)] + public LocalTempStorage LocalTempStorageLocation { get; set; } = + Enum.Parse(StaticLocalTempStorageLocation); - /// - /// Gets or sets a value for the location of temporary files. - /// - [DefaultValue(StaticLocalTempStorageLocation)] - public LocalTempStorage LocalTempStorageLocation { get; set; } = Enum.Parse(StaticLocalTempStorageLocation); + /// + /// Gets or sets a value indicating whether umbraco is running in [debug mode]. + /// + /// true if [debug mode]; otherwise, false. + [DefaultValue(StaticDebug)] + public bool Debug { get; set; } = StaticDebug; - /// - /// Gets or sets a value indicating whether umbraco is running in [debug mode]. - /// - /// true if [debug mode]; otherwise, false. - [DefaultValue(StaticDebug)] - public bool Debug { get; set; } = StaticDebug; - - /// - /// Gets or sets a value specifying the name of the site. - /// - public string? SiteName { get; set; } - } + /// + /// Gets or sets a value specifying the name of the site. + /// + public string? SiteName { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs b/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs index 8a0a1658b2..3bcf91b0be 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs @@ -4,41 +4,40 @@ using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Configuration.Models.Validation; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for image autofill upload settings. +/// +public class ImagingAutoFillUploadField : ValidatableEntryBase { /// - /// Typed configuration options for image autofill upload settings. + /// Gets or sets a value for the alias of the image upload property. /// - public class ImagingAutoFillUploadField : ValidatableEntryBase - { - /// - /// Gets or sets a value for the alias of the image upload property. - /// - [Required] - public string Alias { get; set; } = null!; + [Required] + public string Alias { get; set; } = null!; - /// - /// Gets or sets a value for the width field alias of the image upload property. - /// - [Required] - public string WidthFieldAlias { get; set; } = null!; + /// + /// Gets or sets a value for the width field alias of the image upload property. + /// + [Required] + public string WidthFieldAlias { get; set; } = null!; - /// - /// Gets or sets a value for the height field alias of the image upload property. - /// - [Required] - public string HeightFieldAlias { get; set; } = null!; + /// + /// Gets or sets a value for the height field alias of the image upload property. + /// + [Required] + public string HeightFieldAlias { get; set; } = null!; - /// - /// Gets or sets a value for the length field alias of the image upload property. - /// - [Required] - public string LengthFieldAlias { get; set; } = null!; + /// + /// Gets or sets a value for the length field alias of the image upload property. + /// + [Required] + public string LengthFieldAlias { get; set; } = null!; - /// - /// Gets or sets a value for the extension field alias of the image upload property. - /// - [Required] - public string ExtensionFieldAlias { get; set; } = null!; - } + /// + /// Gets or sets a value for the extension field alias of the image upload property. + /// + [Required] + public string ExtensionFieldAlias { get; set; } = null!; } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs index b3bdddc211..a433c5d300 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs @@ -1,51 +1,48 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -using System.IO; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for image cache settings. +/// +public class ImagingCacheSettings { + internal const string StaticBrowserMaxAge = "7.00:00:00"; + internal const string StaticCacheMaxAge = "365.00:00:00"; + internal const int StaticCacheHashLength = 12; + internal const int StaticCacheFolderDepth = 8; + internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; + /// - /// Typed configuration options for image cache settings. + /// Gets or sets a value for the browser image cache maximum age. /// - public class ImagingCacheSettings - { - internal const string StaticBrowserMaxAge = "7.00:00:00"; - internal const string StaticCacheMaxAge = "365.00:00:00"; - internal const int StaticCacheHashLength = 12; - internal const int StaticCacheFolderDepth = 8; - internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; + [DefaultValue(StaticBrowserMaxAge)] + public TimeSpan BrowserMaxAge { get; set; } = TimeSpan.Parse(StaticBrowserMaxAge); - /// - /// Gets or sets a value for the browser image cache maximum age. - /// - [DefaultValue(StaticBrowserMaxAge)] - public TimeSpan BrowserMaxAge { get; set; } = TimeSpan.Parse(StaticBrowserMaxAge); + /// + /// Gets or sets a value for the image cache maximum age. + /// + [DefaultValue(StaticCacheMaxAge)] + public TimeSpan CacheMaxAge { get; set; } = TimeSpan.Parse(StaticCacheMaxAge); - /// - /// Gets or sets a value for the image cache maximum age. - /// - [DefaultValue(StaticCacheMaxAge)] - public TimeSpan CacheMaxAge { get; set; } = TimeSpan.Parse(StaticCacheMaxAge); + /// + /// Gets or sets a value for the image cache hash length. + /// + [DefaultValue(StaticCacheHashLength)] + public uint CacheHashLength { get; set; } = StaticCacheHashLength; - /// - /// Gets or sets a value for the image cache hash length. - /// - [DefaultValue(StaticCacheHashLength)] - public uint CacheHashLength { get; set; } = StaticCacheHashLength; + /// + /// Gets or sets a value for the image cache folder depth. + /// + [DefaultValue(StaticCacheFolderDepth)] + public uint CacheFolderDepth { get; set; } = StaticCacheFolderDepth; - /// - /// Gets or sets a value for the image cache folder depth. - /// - [DefaultValue(StaticCacheFolderDepth)] - public uint CacheFolderDepth { get; set; } = StaticCacheFolderDepth; - - /// - /// Gets or sets a value for the image cache folder. - /// - [DefaultValue(StaticCacheFolder)] - public string CacheFolder { get; set; } = StaticCacheFolder; - } + /// + /// Gets or sets a value for the image cache folder. + /// + [DefaultValue(StaticCacheFolder)] + public string CacheFolder { get; set; } = StaticCacheFolder; } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs index ff02fdc522..dc4585bf9c 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs @@ -3,26 +3,25 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for image resize settings. +/// +public class ImagingResizeSettings { + internal const int StaticMaxWidth = 5000; + internal const int StaticMaxHeight = 5000; + /// - /// Typed configuration options for image resize settings. + /// Gets or sets a value for the maximim resize width. /// - public class ImagingResizeSettings - { - internal const int StaticMaxWidth = 5000; - internal const int StaticMaxHeight = 5000; + [DefaultValue(StaticMaxWidth)] + public int MaxWidth { get; set; } = StaticMaxWidth; - /// - /// Gets or sets a value for the maximim resize width. - /// - [DefaultValue(StaticMaxWidth)] - public int MaxWidth { get; set; } = StaticMaxWidth; - - /// - /// Gets or sets a value for the maximim resize height. - /// - [DefaultValue(StaticMaxHeight)] - public int MaxHeight { get; set; } = StaticMaxHeight; - } + /// + /// Gets or sets a value for the maximim resize height. + /// + [DefaultValue(StaticMaxHeight)] + public int MaxHeight { get; set; } = StaticMaxHeight; } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs index fde303343c..8232746ead 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs @@ -1,22 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for imaging settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigImaging)] +public class ImagingSettings { /// - /// Typed configuration options for imaging settings. + /// Gets or sets a value for imaging cache settings. /// - [UmbracoOptions(Constants.Configuration.ConfigImaging)] - public class ImagingSettings - { - /// - /// Gets or sets a value for imaging cache settings. - /// - public ImagingCacheSettings Cache { get; set; } = new ImagingCacheSettings(); + public ImagingCacheSettings Cache { get; set; } = new(); - /// - /// Gets or sets a value for imaging resize settings. - /// - public ImagingResizeSettings Resize { get; set; } = new ImagingResizeSettings(); - } + /// + /// Gets or sets a value for imaging resize settings. + /// + public ImagingResizeSettings Resize { get; set; } = new(); } diff --git a/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs b/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs index c140463b4a..8c18495d55 100644 --- a/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs @@ -1,20 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for index creator settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigExamine)] +public class IndexCreatorSettings { /// - /// Typed configuration options for index creator settings. + /// Gets or sets a value for lucene directory factory type. /// - [UmbracoOptions(Constants.Configuration.ConfigExamine)] - public class IndexCreatorSettings - { - /// - /// Gets or sets a value for lucene directory factory type. - /// - public LuceneDirectoryFactory LuceneDirectoryFactory { get; set; } - - } + public LuceneDirectoryFactory LuceneDirectoryFactory { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs b/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs index 377e893bbf..25789b397b 100644 --- a/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs @@ -1,73 +1,74 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// An enumeration of options available for control over installation of default Umbraco data. +/// +public enum InstallDefaultDataOption { /// - /// An enumeration of options available for control over installation of default Umbraco data. + /// Do not install any items of this type (other than Umbraco defined essential ones). /// - public enum InstallDefaultDataOption - { - /// - /// Do not install any items of this type (other than Umbraco defined essential ones). - /// - None, - - /// - /// Only install the default data specified in the - /// - Values, - - /// - /// Install all default data, except that specified in the - /// - ExceptValues, - - /// - /// Install all default data. - /// - All - } + None, /// - /// Typed configuration options for installation of default data. + /// Only install the default data specified in the /// - public class InstallDefaultDataSettings - { - /// - /// Gets or sets a value indicating whether to create default data on installation. - /// - public InstallDefaultDataOption InstallData { get; set; } = InstallDefaultDataOption.All; + Values, - /// - /// Gets or sets a value indicating which default data (languages, data types, etc.) should be created when is - /// set to or . - /// - /// - /// - /// For languages, the values provided should be the ISO codes for the languages to be included or excluded, e.g. "en-US". - /// If removing the single default language, ensure that a different one is created via some other means (such - /// as a restore from Umbraco Deploy schema data). - /// - /// - /// For data types, the values provided should be the Guid values used by Umbraco for the data type, listed at: - /// - /// Some data types - such as the string label - cannot be excluded from install as they are required for core Umbraco - /// functionality. - /// Otherwise take care not to remove data types required for default Umbraco media and member types, unless you also - /// choose to exclude them. - /// - /// - /// For media types, the values provided should be the Guid values used by Umbraco for the media type, listed at: - /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. - /// - /// - /// For member types, the values provided should be the Guid values used by Umbraco for the member type, listed at: - /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. - /// - /// - public IList Values { get; set; } = new List(); - } + /// + /// Install all default data, except that specified in the + /// + ExceptValues, + + /// + /// Install all default data. + /// + All, +} + +/// +/// Typed configuration options for installation of default data. +/// +public class InstallDefaultDataSettings +{ + /// + /// Gets or sets a value indicating whether to create default data on installation. + /// + public InstallDefaultDataOption InstallData { get; set; } = InstallDefaultDataOption.All; + + /// + /// Gets or sets a value indicating which default data (languages, data types, etc.) should be created when + /// is + /// set to or . + /// + /// + /// + /// For languages, the values provided should be the ISO codes for the languages to be included or excluded, e.g. + /// "en-US". + /// If removing the single default language, ensure that a different one is created via some other means (such + /// as a restore from Umbraco Deploy schema data). + /// + /// + /// For data types, the values provided should be the Guid values used by Umbraco for the data type, listed at: + /// + /// Some data types - such as the string label - cannot be excluded from install as they are required for core + /// Umbraco + /// functionality. + /// Otherwise take care not to remove data types required for default Umbraco media and member types, unless you + /// also + /// choose to exclude them. + /// + /// + /// For media types, the values provided should be the Guid values used by Umbraco for the media type, listed at: + /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. + /// + /// + /// For member types, the values provided should be the Guid values used by Umbraco for the member type, listed at: + /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. + /// + /// + public IList Values { get; set; } = new List(); } diff --git a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs index 297e1dff87..64cd61ad26 100644 --- a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs @@ -3,27 +3,26 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for keep alive settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigKeepAlive)] +public class KeepAliveSettings { + internal const bool StaticDisableKeepAliveTask = false; + internal const string StaticKeepAlivePingUrl = "~/api/keepalive/ping"; + /// - /// Typed configuration options for keep alive settings. + /// Gets or sets a value indicating whether the keep alive task is disabled. /// - [UmbracoOptions(Constants.Configuration.ConfigKeepAlive)] - public class KeepAliveSettings - { - internal const bool StaticDisableKeepAliveTask = false; - internal const string StaticKeepAlivePingUrl = "~/api/keepalive/ping"; + [DefaultValue(StaticDisableKeepAliveTask)] + public bool DisableKeepAliveTask { get; set; } = StaticDisableKeepAliveTask; - /// - /// Gets or sets a value indicating whether the keep alive task is disabled. - /// - [DefaultValue(StaticDisableKeepAliveTask)] - public bool DisableKeepAliveTask { get; set; } = StaticDisableKeepAliveTask; - - /// - /// Gets or sets a value for the keep alive ping URL. - /// - [DefaultValue(StaticKeepAlivePingUrl)] - public string KeepAlivePingUrl { get; set; } = StaticKeepAlivePingUrl; - } + /// + /// Gets or sets a value for the keep alive ping URL. + /// + [DefaultValue(StaticKeepAlivePingUrl)] + public string KeepAlivePingUrl { get; set; } = StaticKeepAlivePingUrl; } diff --git a/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs b/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs index c3909ed619..b44d70a46a 100644 --- a/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs @@ -3,28 +3,27 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for legacy machine key settings used for migration of members from a v8 solution. +/// +[UmbracoOptions(Constants.Configuration.ConfigLegacyPasswordMigration)] +public class LegacyPasswordMigrationSettings { + private const string StaticDecryptionKey = ""; + /// - /// Typed configuration options for legacy machine key settings used for migration of members from a v8 solution. + /// Gets the decryption algorithm. /// - [UmbracoOptions(Constants.Configuration.ConfigLegacyPasswordMigration)] - public class LegacyPasswordMigrationSettings - { - private const string StaticDecryptionKey = ""; + /// + /// Currently only AES is supported. This should include all machine keys generated by Umbraco. + /// + public string MachineKeyDecryption => "AES"; - /// - /// Gets the decryption algorithm. - /// - /// - /// Currently only AES is supported. This should include all machine keys generated by Umbraco. - /// - public string MachineKeyDecryption => "AES"; - - /// - /// Gets or sets the decryption hex-formatted string key found in legacy web.config machineKey configuration-element. - /// - [DefaultValue(StaticDecryptionKey)] - public string MachineKeyDecryptionKey { get; set; } = StaticDecryptionKey; - } + /// + /// Gets or sets the decryption hex-formatted string key found in legacy web.config machineKey configuration-element. + /// + [DefaultValue(StaticDecryptionKey)] + public string MachineKeyDecryptionKey { get; set; } = StaticDecryptionKey; } diff --git a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs index 2075921c3f..37b671926c 100644 --- a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs @@ -1,23 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for logging settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigLogging)] - public class LoggingSettings - { - internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24); +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// Gets or sets a value for the maximum age of a log file. - /// - [DefaultValue(StaticMaxLogAge)] - public TimeSpan MaxLogAge { get; set; } = TimeSpan.Parse(StaticMaxLogAge); - } +/// +/// Typed configuration options for logging settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigLogging)] +public class LoggingSettings +{ + internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24); + + /// + /// Gets or sets a value for the maximum age of a log file. + /// + [DefaultValue(StaticMaxLogAge)] + public TimeSpan MaxLogAge { get; set; } = TimeSpan.Parse(StaticMaxLogAge); } diff --git a/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs b/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs index 5f06a850f1..3b0e974c08 100644 --- a/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs +++ b/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs @@ -1,24 +1,23 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +public enum LuceneDirectoryFactory { - public enum LuceneDirectoryFactory - { - /// - /// The index will operate from the default location: Umbraco/Data/Temp/ExamineIndexes - /// - Default, + /// + /// The index will operate from the default location: Umbraco/Data/Temp/ExamineIndexes + /// + Default, - /// - /// The index will operate on a local index created in the processes %temp% location and - /// will replicate back to main storage in Umbraco/Data/Temp/ExamineIndexes - /// - SyncedTempFileSystemDirectoryFactory, + /// + /// The index will operate on a local index created in the processes %temp% location and + /// will replicate back to main storage in Umbraco/Data/Temp/ExamineIndexes + /// + SyncedTempFileSystemDirectoryFactory, - /// - /// The index will operate only in the processes %temp% directory location - /// - TempFileSystemDirectoryFactory - } + /// + /// The index will operate only in the processes %temp% directory location + /// + TempFileSystemDirectoryFactory, } diff --git a/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs b/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs index fa4f0725f7..1e884a150f 100644 --- a/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs @@ -3,47 +3,46 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for member password settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigMemberPassword)] +public class MemberPasswordConfigurationSettings : IPasswordConfiguration { - /// - /// Typed configuration options for member password settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigMemberPassword)] - public class MemberPasswordConfigurationSettings : IPasswordConfiguration - { - internal const int StaticRequiredLength = 10; - internal const bool StaticRequireNonLetterOrDigit = false; - internal const bool StaticRequireDigit = false; - internal const bool StaticRequireLowercase = false; - internal const bool StaticRequireUppercase = false; - internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; + internal const int StaticRequiredLength = 10; + internal const bool StaticRequireNonLetterOrDigit = false; + internal const bool StaticRequireDigit = false; + internal const bool StaticRequireLowercase = false; + internal const bool StaticRequireUppercase = false; + internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; - /// - [DefaultValue(StaticRequiredLength)] - public int RequiredLength { get; set; } = StaticRequiredLength; + /// + [DefaultValue(StaticRequiredLength)] + public int RequiredLength { get; set; } = StaticRequiredLength; - /// - [DefaultValue(StaticRequireNonLetterOrDigit)] - public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; + /// + [DefaultValue(StaticRequireNonLetterOrDigit)] + public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; - /// - [DefaultValue(StaticRequireDigit)] - public bool RequireDigit { get; set; } = StaticRequireDigit; + /// + [DefaultValue(StaticRequireDigit)] + public bool RequireDigit { get; set; } = StaticRequireDigit; - /// - [DefaultValue(StaticRequireLowercase)] - public bool RequireLowercase { get; set; } = StaticRequireLowercase; + /// + [DefaultValue(StaticRequireLowercase)] + public bool RequireLowercase { get; set; } = StaticRequireLowercase; - /// - [DefaultValue(StaticRequireUppercase)] - public bool RequireUppercase { get; set; } = StaticRequireUppercase; + /// + [DefaultValue(StaticRequireUppercase)] + public bool RequireUppercase { get; set; } = StaticRequireUppercase; - /// - [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] - public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; + /// + [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] + public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; - /// - [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] - public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; - } + /// + [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] + public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; } diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index 73d046de32..0e7e1812c6 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -2,83 +2,81 @@ // See LICENSE for more details. using System.ComponentModel; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for models builder settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigModelsBuilder, BindNonPublicProperties = true)] +public class ModelsBuilderSettings { + internal const string StaticModelsMode = "InMemoryAuto"; + internal const string StaticModelsDirectory = "~/umbraco/models"; + internal const bool StaticAcceptUnsafeModelsDirectory = false; + internal const int StaticDebugLevel = 0; + private bool _flagOutOfDateModels = true; + /// - /// Typed configuration options for models builder settings. + /// Gets or sets a value for the models mode. /// - [UmbracoOptions(Constants.Configuration.ConfigModelsBuilder, BindNonPublicProperties = true)] - public class ModelsBuilderSettings + [DefaultValue(StaticModelsMode)] + public ModelsMode ModelsMode { get; set; } = Enum.Parse(StaticModelsMode); + + /// + /// Gets or sets a value for models namespace. + /// + /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. + [DefaultValue(Constants.ModelsBuilder.DefaultModelsNamespace)] + public string ModelsNamespace { get; set; } = Constants.ModelsBuilder.DefaultModelsNamespace; + + /// + /// Gets or sets a value indicating whether we should flag out-of-date models. + /// + /// + /// Models become out-of-date when data types or content types are updated. When this + /// setting is activated the ~/umbraco/models/PureLive/ood.txt file is then created. When models are + /// generated through the dashboard, the files is cleared. Default value is false. + /// + public bool FlagOutOfDateModels { - private bool _flagOutOfDateModels = true; - internal const string StaticModelsMode = "InMemoryAuto"; - internal const string StaticModelsDirectory = "~/umbraco/models"; - internal const bool StaticAcceptUnsafeModelsDirectory = false; - internal const int StaticDebugLevel = 0; - - /// - /// Gets or sets a value for the models mode. - /// - [DefaultValue(StaticModelsMode)] - public ModelsMode ModelsMode { get; set; } = Enum.Parse(StaticModelsMode); - - /// - /// Gets or sets a value for models namespace. - /// - /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. - [DefaultValue(Constants.ModelsBuilder.DefaultModelsNamespace)] - public string ModelsNamespace { get; set; } = Constants.ModelsBuilder.DefaultModelsNamespace; - - /// - /// Gets or sets a value indicating whether we should flag out-of-date models. - /// - /// - /// Models become out-of-date when data types or content types are updated. When this - /// setting is activated the ~/umbraco/models/PureLive/ood.txt file is then created. When models are - /// generated through the dashboard, the files is cleared. Default value is false. - /// - public bool FlagOutOfDateModels + get { - get => _flagOutOfDateModels; - - set + if (ModelsMode == ModelsMode.Nothing ||ModelsMode.IsAuto()) { - if (!ModelsMode.IsAuto()) - { - _flagOutOfDateModels = false; - return; - } + return false; - _flagOutOfDateModels = value; } + + return _flagOutOfDateModels; } - /// - /// Gets or sets a value for the models directory. - /// - /// Default is ~/umbraco/models but that can be changed. - [DefaultValue(StaticModelsDirectory)] - public string ModelsDirectory { get; set; } = StaticModelsDirectory; - - - /// - /// Gets or sets a value indicating whether to accept an unsafe value for ModelsDirectory. - /// - /// - /// An unsafe value is an absolute path, or a relative path pointing outside - /// of the website root. - /// - [DefaultValue(StaticAcceptUnsafeModelsDirectory)] - public bool AcceptUnsafeModelsDirectory { get; set; } = StaticAcceptUnsafeModelsDirectory; - - /// - /// Gets or sets a value indicating the debug log level. - /// - /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). - [DefaultValue(StaticDebugLevel)] - public int DebugLevel { get; set; } = StaticDebugLevel; + set => _flagOutOfDateModels = value; } + + + /// + /// Gets or sets a value for the models directory. + /// + /// Default is ~/umbraco/models but that can be changed. + [DefaultValue(StaticModelsDirectory)] + public string ModelsDirectory { get; set; } = StaticModelsDirectory; + + /// + /// Gets or sets a value indicating whether to accept an unsafe value for ModelsDirectory. + /// + /// + /// An unsafe value is an absolute path, or a relative path pointing outside + /// of the website root. + /// + [DefaultValue(StaticAcceptUnsafeModelsDirectory)] + public bool AcceptUnsafeModelsDirectory { get; set; } = StaticAcceptUnsafeModelsDirectory; + + /// + /// Gets or sets a value indicating the debug log level. + /// + /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). + [DefaultValue(StaticDebugLevel)] + public int DebugLevel { get; set; } = StaticDebugLevel; } diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs index 8f889b10c3..0506ddb98b 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs @@ -1,14 +1,13 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// The serializer type that nucache uses to persist documents in the database. +/// +public enum NuCacheSerializerType { - /// - /// The serializer type that nucache uses to persist documents in the database. - /// - public enum NuCacheSerializerType - { - MessagePack = 1, // Default - JSON = 2 - } + MessagePack = 1, // Default + JSON = 2, } diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index ee41fc32d3..b88dbb5d0d 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -3,41 +3,41 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for NuCache settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigNuCache)] +public class NuCacheSettings { + internal const string StaticNuCacheSerializerType = "MessagePack"; + internal const int StaticSqlPageSize = 1000; + internal const int StaticKitBatchSize = 1; + /// - /// Typed configuration options for NuCache settings. + /// Gets or sets a value defining the BTree block size. /// - [UmbracoOptions(Constants.Configuration.ConfigNuCache)] - public class NuCacheSettings - { - internal const string StaticNuCacheSerializerType = "MessagePack"; - internal const int StaticSqlPageSize = 1000; - internal const int StaticKitBatchSize = 1; + public int? BTreeBlockSize { get; set; } - /// - /// Gets or sets a value defining the BTree block size. - /// - public int? BTreeBlockSize { get; set; } + /// + /// The serializer type that nucache uses to persist documents in the database. + /// + [DefaultValue(StaticNuCacheSerializerType)] + public NuCacheSerializerType NuCacheSerializerType { get; set; } = + Enum.Parse(StaticNuCacheSerializerType); - /// - /// The serializer type that nucache uses to persist documents in the database. - /// - [DefaultValue(StaticNuCacheSerializerType)] - public NuCacheSerializerType NuCacheSerializerType { get; set; } = Enum.Parse(StaticNuCacheSerializerType); + /// + /// The paging size to use for nucache SQL queries. + /// + [DefaultValue(StaticSqlPageSize)] + public int SqlPageSize { get; set; } = StaticSqlPageSize; - /// - /// The paging size to use for nucache SQL queries. - /// - [DefaultValue(StaticSqlPageSize)] - public int SqlPageSize { get; set; } = StaticSqlPageSize; + /// + /// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time. + /// + [DefaultValue(StaticKitBatchSize)] + public int KitBatchSize { get; set; } = StaticKitBatchSize; - /// - /// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time. - /// - [DefaultValue(StaticKitBatchSize)] - public int KitBatchSize { get; set; } = StaticKitBatchSize; - - public bool UnPublishedContentCompression { get; set; } = false; - } + public bool UnPublishedContentCompression { get; set; } = false; } diff --git a/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs b/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs index 27968fdcd2..ee48d5a642 100644 --- a/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs @@ -3,38 +3,41 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for package migration settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigPackageMigration)] +public class PackageMigrationSettings { + private const bool StaticRunSchemaAndContentMigrations = true; + private const bool StaticAllowComponentOverrideOfRunSchemaAndContentMigrations = true; + /// - /// Typed configuration options for package migration settings. + /// Gets or sets a value indicating whether package migration steps that install schema and content should run. /// - [UmbracoOptions(Constants.Configuration.ConfigPackageMigration)] - public class PackageMigrationSettings - { - private const bool StaticRunSchemaAndContentMigrations = true; - private const bool StaticAllowComponentOverrideOfRunSchemaAndContentMigrations = true; + /// + /// By default this is true and schema and content defined in a package migration are installed. + /// Using configuration, administrators can optionally switch this off in certain environments. + /// Deployment tools such as Umbraco Deploy can also configure this option to run or not run these migration + /// steps as is appropriate for normal use of the tool. + /// + [DefaultValue(StaticRunSchemaAndContentMigrations)] + public bool RunSchemaAndContentMigrations { get; set; } = StaticRunSchemaAndContentMigrations; - /// - /// Gets or sets a value indicating whether package migration steps that install schema and content should run. - /// - /// - /// By default this is true and schema and content defined in a package migration are installed. - /// Using configuration, administrators can optionally switch this off in certain environments. - /// Deployment tools such as Umbraco Deploy can also configure this option to run or not run these migration - /// steps as is appropriate for normal use of the tool. - /// - [DefaultValue(StaticRunSchemaAndContentMigrations)] - public bool RunSchemaAndContentMigrations { get; set; } = StaticRunSchemaAndContentMigrations; - - /// - /// Gets or sets a value indicating whether components can override the configured value for . - /// - /// - /// By default this is true and components can override the configured setting for . - /// If an administrator wants explicit control over which environments migration steps installing schema and content can run, - /// they can set this to false. Components should respect this and not override the configuration. - /// - [DefaultValue(StaticAllowComponentOverrideOfRunSchemaAndContentMigrations)] - public bool AllowComponentOverrideOfRunSchemaAndContentMigrations { get; set; } = StaticAllowComponentOverrideOfRunSchemaAndContentMigrations; - } + /// + /// Gets or sets a value indicating whether components can override the configured value for + /// . + /// + /// + /// By default this is true and components can override the configured setting for + /// . + /// If an administrator wants explicit control over which environments migration steps installing schema and content + /// can run, + /// they can set this to false. Components should respect this and not override the configuration. + /// + [DefaultValue(StaticAllowComponentOverrideOfRunSchemaAndContentMigrations)] + public bool AllowComponentOverrideOfRunSchemaAndContentMigrations { get; set; } = + StaticAllowComponentOverrideOfRunSchemaAndContentMigrations; } diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs index 49f07f0bdd..7fb12ffbd3 100644 --- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs @@ -1,27 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for request handler settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigRequestHandler)] - public class RequestHandlerSettings - { - internal const bool StaticAddTrailingSlash = true; - internal const string StaticConvertUrlsToAscii = "try"; - internal const bool StaticEnableDefaultCharReplacements = true; +namespace Umbraco.Cms.Core.Configuration.Models; - internal static readonly Umbraco.Cms.Core.Configuration.Models.CharItem[] DefaultCharCollection = - { +/// +/// Typed configuration options for request handler settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigRequestHandler)] +public class RequestHandlerSettings +{ + internal const bool StaticAddTrailingSlash = true; + internal const string StaticConvertUrlsToAscii = "try"; + internal const bool StaticEnableDefaultCharReplacements = true; + + internal static readonly CharItem[] DefaultCharCollection = + { new () { Char = " ", Replacement = "-" }, new () { Char = "\"", Replacement = string.Empty }, new () { Char = "'", Replacement = string.Empty }, @@ -45,40 +42,39 @@ namespace Umbraco.Cms.Core.Configuration.Models new () { Char = "ß", Replacement = "ss" }, new () { Char = "|", Replacement = "-" }, new () { Char = "<", Replacement = string.Empty }, - new () { Char = ">", Replacement = string.Empty } - }; + new () { Char = ">", Replacement = string.Empty }, + }; - /// - /// Gets or sets a value indicating whether to add a trailing slash to URLs. - /// - [DefaultValue(StaticAddTrailingSlash)] - public bool AddTrailingSlash { get; set; } = StaticAddTrailingSlash; + /// + /// Gets or sets a value indicating whether to add a trailing slash to URLs. + /// + [DefaultValue(StaticAddTrailingSlash)] + public bool AddTrailingSlash { get; set; } = StaticAddTrailingSlash; - /// - /// Gets or sets a value indicating whether to convert URLs to ASCII (valid values: "true", "try" or "false"). - /// - [DefaultValue(StaticConvertUrlsToAscii)] - public string ConvertUrlsToAscii { get; set; } = StaticConvertUrlsToAscii; + /// + /// Gets or sets a value indicating whether to convert URLs to ASCII (valid values: "true", "try" or "false"). + /// + [DefaultValue(StaticConvertUrlsToAscii)] + public string ConvertUrlsToAscii { get; set; } = StaticConvertUrlsToAscii; - /// - /// Gets a value indicating whether URLs should be converted to ASCII. - /// - public bool ShouldConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("true"); + /// + /// Gets a value indicating whether URLs should be converted to ASCII. + /// + public bool ShouldConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("true"); - /// - /// Gets a value indicating whether URLs should be tried to be converted to ASCII. - /// - public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try"); + /// + /// Gets a value indicating whether URLs should be tried to be converted to ASCII. + /// + public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try"); - /// - /// Disable all default character replacements - /// - [DefaultValue(StaticEnableDefaultCharReplacements)] - public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements; + /// + /// Disable all default character replacements + /// + [DefaultValue(StaticEnableDefaultCharReplacements)] + public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements; - /// - /// Add additional character replacements, or override defaults - /// - public IEnumerable? UserDefinedCharCollection { get; set; } - } + /// + /// Add additional character replacements, or override defaults + /// + public IEnumerable? UserDefinedCharCollection { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs index cd82376c57..55fa7b2c5f 100644 --- a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs @@ -1,111 +1,157 @@ -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigRichTextEditor)] +public class RichTextEditorSettings { - [UmbracoOptions(Constants.Configuration.ConfigRichTextEditor)] - public class RichTextEditorSettings + internal const string StaticValidElements = + "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption"; + + internal const string StaticInvalidElements = "font"; + + private static readonly string[] Default_plugins = { - internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption"; - internal const string StaticInvalidElements = "font"; + "paste", "anchor", "charmap", "table", "lists", "advlist", "hr", "autolink", "directionality", "tabfocus", + "searchreplace", + }; - private static readonly string[] s_default_plugins = new[] + private static readonly RichTextEditorCommand[] Default_commands = + { + new RichTextEditorCommand { - "paste", - "anchor", - "charmap", - "table", - "lists", - "advlist", - "hr", - "autolink", - "directionality", - "tabfocus", - "searchreplace" - }; - private static readonly RichTextEditorCommand[] s_default_commands = new [] + Alias = "ace", Name = "Source code editor", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand { - new RichTextEditorCommand(){Alias = "ace" , Name = "Source code editor" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "removeformat" , Name = "Remove format" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "undo" , Name = "Undo" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "redo" , Name = "Redo" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "cut" , Name = "Cut" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "copy" , Name = "Copy" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "paste" , Name = "Paste" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "styleselect" , Name = "Style select" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "bold" , Name = "Bold" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "italic" , Name = "Italic" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "underline" , Name = "Underline" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "strikethrough" , Name = "Strikethrough" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "alignleft" , Name = "Justify left" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "aligncenter" , Name = "Justify center" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "alignright" , Name = "Justify right" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "alignjustify" , Name = "Justify full" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "bullist" , Name = "Bullet list" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "numlist" , Name = "Numbered list" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "outdent" , Name = "Decrease indent" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "indent" , Name = "Increase indent" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "link" , Name = "Insert/edit link" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "unlink" , Name = "Remove link" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "anchor" , Name = "Anchor" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "umbmediapicker" , Name = "Image" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "umbmacro" , Name = "Macro" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "table" , Name = "Table" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "umbembeddialog" , Name = "Embed" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "hr" , Name = "Horizontal rule" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "subscript" , Name = "Subscript" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "superscript" , Name = "Superscript" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "charmap" , Name = "Character map" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "rtl" , Name = "Right to left" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "ltr" , Name = "Left to right" , Mode = RichTextEditorCommandMode.Selection}, - }; - - private static readonly IDictionary s_default_custom_config = new Dictionary() + Alias = "removeformat", Name = "Remove format", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand { Alias = "undo", Name = "Undo", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand { Alias = "redo", Name = "Redo", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand { Alias = "cut", Name = "Cut", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { Alias = "copy", Name = "Copy", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { Alias = "paste", Name = "Paste", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand { - ["entity_encoding"] = "raw" - }; - - /// - /// HTML RichText Editor TinyMCE Commands - /// - /// WB-TODO Custom Array of objects - public RichTextEditorCommand[] Commands { get; set; } = s_default_commands; - - /// - /// HTML RichText Editor TinyMCE Plugins - /// - public string[] Plugins { get; set; } = s_default_plugins; - - /// - /// HTML RichText Editor TinyMCE Custom Config - /// - /// WB-TODO Custom Dictionary - public IDictionary CustomConfig { get; set; } = s_default_custom_config; - - /// - /// - /// - [DefaultValue(StaticValidElements)] - public string ValidElements { get; set; } = StaticValidElements; - - /// - /// Invalid HTML elements for RichText Editor - /// - [DefaultValue(StaticInvalidElements)] - public string InvalidElements { get; set; } = StaticInvalidElements; - - public class RichTextEditorCommand + Alias = "styleselect", Name = "Style select", Mode = RichTextEditorCommandMode.All, + }, + new RichTextEditorCommand { Alias = "bold", Name = "Bold", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { Alias = "italic", Name = "Italic", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { - [Required] - public string Alias { get; set; } = null!; + Alias = "underline", Name = "Underline", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "strikethrough", Name = "Strikethrough", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "alignleft", Name = "Justify left", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "aligncenter", Name = "Justify center", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "alignright", Name = "Justify right", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "alignjustify", Name = "Justify full", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand { Alias = "bullist", Name = "Bullet list", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand { Alias = "numlist", Name = "Numbered list", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand + { + Alias = "outdent", Name = "Decrease indent", Mode = RichTextEditorCommandMode.All, + }, + new RichTextEditorCommand + { + Alias = "indent", Name = "Increase indent", Mode = RichTextEditorCommandMode.All, + }, + new RichTextEditorCommand { Alias = "link", Name = "Insert/edit link", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand + { + Alias = "unlink", Name = "Remove link", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand { Alias = "anchor", Name = "Anchor", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand + { + Alias = "umbmediapicker", Name = "Image", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand { Alias = "umbmacro", Name = "Macro", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand { Alias = "table", Name = "Table", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand + { + Alias = "umbembeddialog", Name = "Embed", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand { Alias = "hr", Name = "Horizontal rule", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand + { + Alias = "subscript", Name = "Subscript", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "superscript", Name = "Superscript", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "charmap", Name = "Character map", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand + { + Alias = "rtl", Name = "Right to left", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "ltr", Name = "Left to right", Mode = RichTextEditorCommandMode.Selection, + }, + }; - [Required] - public string Name { get; set; } = null!; + private static readonly IDictionary Default_custom_config = + new Dictionary { ["entity_encoding"] = "raw" }; - [Required] - public RichTextEditorCommandMode Mode { get; set; } - } + /// + /// HTML RichText Editor TinyMCE Commands + /// + /// WB-TODO Custom Array of objects + public RichTextEditorCommand[] Commands { get; set; } = Default_commands; + + /// + /// HTML RichText Editor TinyMCE Plugins + /// + public string[] Plugins { get; set; } = Default_plugins; + + /// + /// HTML RichText Editor TinyMCE Custom Config + /// + /// WB-TODO Custom Dictionary + public IDictionary CustomConfig { get; set; } = Default_custom_config; + + /// + /// + [DefaultValue(StaticValidElements)] + public string ValidElements { get; set; } = StaticValidElements; + + /// + /// Invalid HTML elements for RichText Editor + /// + [DefaultValue(StaticInvalidElements)] + public string InvalidElements { get; set; } = StaticInvalidElements; + + public class RichTextEditorCommand + { + [Required] + public string Alias { get; set; } = null!; + + [Required] + public string Name { get; set; } = null!; + + [Required] + public RichTextEditorCommandMode Mode { get; set; } } } diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs index db1e1526e5..37426fa84f 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +public enum RuntimeMinificationCacheBuster { - public enum RuntimeMinificationCacheBuster - { - Version, - AppDomain, - Timestamp - } + Version, + AppDomain, + Timestamp, } diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs index 643e83bcac..09c55c784b 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs @@ -1,30 +1,30 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigRuntimeMinification)] +public class RuntimeMinificationSettings { - [UmbracoOptions(Constants.Configuration.ConfigRuntimeMinification)] - public class RuntimeMinificationSettings - { - internal const bool StaticUseInMemoryCache = false; - internal const string StaticCacheBuster = "Version"; - internal const string? StaticVersion = null; + internal const bool StaticUseInMemoryCache = false; + internal const string StaticCacheBuster = "Version"; + internal const string? StaticVersion = null; - /// - /// Use in memory cache - /// - [DefaultValue(StaticUseInMemoryCache)] - public bool UseInMemoryCache { get; set; } = StaticUseInMemoryCache; + /// + /// Use in memory cache + /// + [DefaultValue(StaticUseInMemoryCache)] + public bool UseInMemoryCache { get; set; } = StaticUseInMemoryCache; - /// - /// The cache buster type to use - /// - [DefaultValue(StaticCacheBuster)] - public RuntimeMinificationCacheBuster CacheBuster { get; set; } = Enum.Parse(StaticCacheBuster); + /// + /// The cache buster type to use + /// + [DefaultValue(StaticCacheBuster)] + public RuntimeMinificationCacheBuster CacheBuster { get; set; } = + Enum.Parse(StaticCacheBuster); - /// - /// The unique version string used if CacheBuster is 'Version'. - /// - [DefaultValue(StaticVersion)] - public string? Version { get; set; } = StaticVersion; - } + /// + /// The unique version string used if CacheBuster is 'Version'. + /// + [DefaultValue(StaticVersion)] + public string? Version { get; set; } = StaticVersion; } diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs new file mode 100644 index 0000000000..3f38167af8 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Represents the configured Umbraco runtime mode. +/// +public enum RuntimeMode +{ + /// + /// The backoffice development mode ensures the runtime is configured for rapidly applying changes within the backoffice. + /// + BackofficeDevelopment = 0, + + /// + /// The development mode ensures the runtime is configured for rapidly applying changes. + /// + Development = 1, + + /// + /// The production mode ensures optimal performance settings are configured and denies any changes that would require recompilations. + /// + Production = 2 +} diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs index ef67d40102..7f31c9319b 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs @@ -1,22 +1,29 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for runtime settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigRuntime)] +public class RuntimeSettings { /// - /// Typed configuration options for runtime settings. + /// Gets or sets the runtime mode. /// - [UmbracoOptions(Constants.Configuration.ConfigRuntime)] - public class RuntimeSettings - { - /// - /// Gets or sets a value for the maximum query string length. - /// - public int? MaxQueryStringLength { get; set; } + [DefaultValue(RuntimeMode.BackofficeDevelopment)] + public RuntimeMode Mode { get; set; } = RuntimeMode.BackofficeDevelopment; - /// - /// Gets or sets a value for the maximum request length in kb. - /// - public int? MaxRequestLength { get; set; } - } + /// + /// Gets or sets a value for the maximum query string length. + /// + public int? MaxQueryStringLength { get; set; } + + /// + /// Gets or sets a value for the maximum request length in kb. + /// + public int? MaxRequestLength { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 5ec94381b4..586b3955c2 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -3,81 +3,92 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for security settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigSecurity)] +public class SecuritySettings { + internal const bool StaticMemberBypassTwoFactorForExternalLogins = true; + internal const bool StaticUserBypassTwoFactorForExternalLogins = true; + internal const bool StaticKeepUserLoggedIn = false; + internal const bool StaticHideDisabledUsersInBackOffice = false; + internal const bool StaticAllowPasswordReset = true; + internal const bool StaticAllowEditInvariantFromNonDefault = false; + internal const string StaticAuthCookieName = "UMB_UCONTEXT"; + + internal const string StaticAllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\"; + /// - /// Typed configuration options for security settings. + /// Gets or sets a value indicating whether to keep the user logged in. /// - [UmbracoOptions(Constants.Configuration.ConfigSecurity)] - public class SecuritySettings - { - internal const bool StaticMemberBypassTwoFactorForExternalLogins = true; - internal const bool StaticUserBypassTwoFactorForExternalLogins = true; - internal const bool StaticKeepUserLoggedIn = false; - internal const bool StaticHideDisabledUsersInBackOffice = false; - internal const bool StaticAllowPasswordReset = true; - internal const string StaticAuthCookieName = "UMB_UCONTEXT"; - internal const string StaticAllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\"; + [DefaultValue(StaticKeepUserLoggedIn)] + public bool KeepUserLoggedIn { get; set; } = StaticKeepUserLoggedIn; + + /// + /// Gets or sets a value indicating whether to hide disabled users in the back-office. + /// + [DefaultValue(StaticHideDisabledUsersInBackOffice)] + public bool HideDisabledUsersInBackOffice { get; set; } = StaticHideDisabledUsersInBackOffice; + + /// + /// Gets or sets a value indicating whether to allow user password reset. + /// + [DefaultValue(StaticAllowPasswordReset)] + public bool AllowPasswordReset { get; set; } = StaticAllowPasswordReset; + + /// + /// Gets or sets a value for the authorization cookie name. + /// + [DefaultValue(StaticAuthCookieName)] + public string AuthCookieName { get; set; } = StaticAuthCookieName; + + /// + /// Gets or sets a value for the authorization cookie domain. + /// + public string? AuthCookieDomain { get; set; } + + /// + /// Gets or sets a value indicating whether the user's email address is to be considered as their username. + /// + public bool UsernameIsEmail { get; set; } = true; + + /// + /// Gets or sets the set of allowed characters for a username + /// + [DefaultValue(StaticAllowedUserNameCharacters)] + public string AllowedUserNameCharacters { get; set; } = StaticAllowedUserNameCharacters; + + /// + /// Gets or sets a value for the user password settings. + /// + public UserPasswordConfigurationSettings? UserPassword { get; set; } + + /// + /// Gets or sets a value for the member password settings. + /// + public MemberPasswordConfigurationSettings? MemberPassword { get; set; } + + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login + /// for members. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticMemberBypassTwoFactorForExternalLogins)] + public bool MemberBypassTwoFactorForExternalLogins { get; set; } = StaticMemberBypassTwoFactorForExternalLogins; + + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login + /// for users. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] + public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; /// - /// Gets or sets a value indicating whether to keep the user logged in. + /// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation. /// - [DefaultValue(StaticKeepUserLoggedIn)] - public bool KeepUserLoggedIn { get; set; } = StaticKeepUserLoggedIn; - - /// - /// Gets or sets a value indicating whether to hide disabled users in the back-office. - /// - [DefaultValue(StaticHideDisabledUsersInBackOffice)] - public bool HideDisabledUsersInBackOffice { get; set; } = StaticHideDisabledUsersInBackOffice; - - /// - /// Gets or sets a value indicating whether to allow user password reset. - /// - [DefaultValue(StaticAllowPasswordReset)] - public bool AllowPasswordReset { get; set; } = StaticAllowPasswordReset; - - /// - /// Gets or sets a value for the authorization cookie name. - /// - [DefaultValue(StaticAuthCookieName)] - public string AuthCookieName { get; set; } = StaticAuthCookieName; - - /// - /// Gets or sets a value for the authorization cookie domain. - /// - public string? AuthCookieDomain { get; set; } - - /// - /// Gets or sets a value indicating whether the user's email address is to be considered as their username. - /// - public bool UsernameIsEmail { get; set; } = true; - - /// - /// Gets or sets the set of allowed characters for a username - /// - [DefaultValue(StaticAllowedUserNameCharacters)] - public string AllowedUserNameCharacters { get; set; } = StaticAllowedUserNameCharacters; - - /// - /// Gets or sets a value for the user password settings. - /// - public UserPasswordConfigurationSettings? UserPassword { get; set; } - - /// - /// Gets or sets a value for the member password settings. - /// - public MemberPasswordConfigurationSettings? MemberPassword { get; set; } - /// - /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for members. Thereby rely on the External login and potential 2FA at that provider. - /// - [DefaultValue(StaticMemberBypassTwoFactorForExternalLogins)] - public bool MemberBypassTwoFactorForExternalLogins { get; set; } = StaticMemberBypassTwoFactorForExternalLogins; - - /// - /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for users. Thereby rely on the External login and potential 2FA at that provider. - /// - [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] - public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; - } + [DefaultValue(StaticAllowEditInvariantFromNonDefault)] + public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault; } diff --git a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs index 54b9ad6c84..5a9ec1b94f 100644 --- a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs @@ -6,91 +6,95 @@ using System.ComponentModel.DataAnnotations; using System.Net.Mail; using Umbraco.Cms.Core.Configuration.Models.Validation; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Matches MailKit.Security.SecureSocketOptions and defined locally to avoid having to take +/// a dependency on this external library into Umbraco.Core. +/// +/// +public enum SecureSocketOptions { /// - /// Matches MailKit.Security.SecureSocketOptions and defined locally to avoid having to take - /// a dependency on this external library into Umbraco.Core. + /// No SSL or TLS encryption should be used. /// - /// - public enum SecureSocketOptions - { - /// - /// No SSL or TLS encryption should be used. - /// - None = 0, - - /// - /// Allow the IMailService to decide which SSL or TLS options to use (default). If the server does not support SSL or TLS, then the connection will continue without any encryption. - /// - Auto = 1, - - /// - /// The connection should use SSL or TLS encryption immediately. - /// - SslOnConnect = 2, - - /// - /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the server. If the server does not support the STARTTLS extension, then the connection will fail and a NotSupportedException will be thrown. - /// - StartTls = 3, - - /// - /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the server, but only if the server supports the STARTTLS extension. - /// - StartTlsWhenAvailable = 4 - } + None = 0, /// - /// Typed configuration options for SMTP settings. + /// Allow the IMailService to decide which SSL or TLS options to use (default). If the server does not support SSL or + /// TLS, then the connection will continue without any encryption. /// - public class SmtpSettings : ValidatableEntryBase - { - internal const string StaticSecureSocketOptions = "Auto"; - internal const string StaticDeliveryMethod = "Network"; + Auto = 1, - /// - /// Gets or sets a value for the SMTP from address to use for messages. - /// - [Required] - [EmailAddress] - public string From { get; set; } = null!; + /// + /// The connection should use SSL or TLS encryption immediately. + /// + SslOnConnect = 2, - /// - /// Gets or sets a value for the SMTP host. - /// - public string? Host { get; set; } + /// + /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the + /// server. If the server does not support the STARTTLS extension, then the connection will fail and a + /// NotSupportedException will be thrown. + /// + StartTls = 3, - /// - /// Gets or sets a value for the SMTP port. - /// - public int Port { get; set; } - - /// - /// Gets or sets a value for the secure socket options. - /// - [DefaultValue(StaticSecureSocketOptions)] - public SecureSocketOptions SecureSocketOptions { get; set; } = Enum.Parse(StaticSecureSocketOptions); - - /// - /// Gets or sets a value for the SMTP pick-up directory. - /// - public string? PickupDirectoryLocation { get; set; } - - /// - /// Gets or sets a value for the SMTP delivery method. - /// - [DefaultValue(StaticDeliveryMethod)] - public SmtpDeliveryMethod DeliveryMethod { get; set; } = Enum.Parse(StaticDeliveryMethod); - - /// - /// Gets or sets a value for the SMTP user name. - /// - public string? Username { get; set; } - - /// - /// Gets or sets a value for the SMTP password. - /// - public string? Password { get; set; } - } + /// + /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the + /// server, but only if the server supports the STARTTLS extension. + /// + StartTlsWhenAvailable = 4, +} + +/// +/// Typed configuration options for SMTP settings. +/// +public class SmtpSettings : ValidatableEntryBase +{ + internal const string StaticSecureSocketOptions = "Auto"; + internal const string StaticDeliveryMethod = "Network"; + + /// + /// Gets or sets a value for the SMTP from address to use for messages. + /// + [Required] + [EmailAddress] + public string From { get; set; } = null!; + + /// + /// Gets or sets a value for the SMTP host. + /// + public string? Host { get; set; } + + /// + /// Gets or sets a value for the SMTP port. + /// + public int Port { get; set; } + + /// + /// Gets or sets a value for the secure socket options. + /// + [DefaultValue(StaticSecureSocketOptions)] + public SecureSocketOptions SecureSocketOptions { get; set; } = + Enum.Parse(StaticSecureSocketOptions); + + /// + /// Gets or sets a value for the SMTP pick-up directory. + /// + public string? PickupDirectoryLocation { get; set; } + + /// + /// Gets or sets a value for the SMTP delivery method. + /// + [DefaultValue(StaticDeliveryMethod)] + public SmtpDeliveryMethod DeliveryMethod { get; set; } = Enum.Parse(StaticDeliveryMethod); + + /// + /// Gets or sets a value for the SMTP user name. + /// + public string? Username { get; set; } + + /// + /// Gets or sets a value for the SMTP password. + /// + public string? Password { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/TourSettings.cs b/src/Umbraco.Core/Configuration/Models/TourSettings.cs index cdc54dfe1f..aaf2063c64 100644 --- a/src/Umbraco.Core/Configuration/Models/TourSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/TourSettings.cs @@ -3,20 +3,19 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for tour settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigTours)] - public class TourSettings - { - internal const bool StaticEnableTours = true; +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// Gets or sets a value indicating whether back-office tours are enabled. - /// - [DefaultValue(StaticEnableTours)] - public bool EnableTours { get; set; } = StaticEnableTours; - } +/// +/// Typed configuration options for tour settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigTours)] +public class TourSettings +{ + internal const bool StaticEnableTours = true; + + /// + /// Gets or sets a value indicating whether back-office tours are enabled. + /// + [DefaultValue(StaticEnableTours)] + public bool EnableTours { get; set; } = StaticEnableTours; } diff --git a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs index 30ef3718f4..f281bbc31a 100644 --- a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs @@ -1,28 +1,25 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for type finder settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigTypeFinder)] +public class TypeFinderSettings { /// - /// Typed configuration options for type finder settings. + /// Gets or sets a value for the assemblies that accept load exceptions during type finder operations. /// - [UmbracoOptions(Constants.Configuration.ConfigTypeFinder)] - public class TypeFinderSettings - { - /// - /// Gets or sets a value for the assemblies that accept load exceptions during type finder operations. - /// - [Required] - public string AssembliesAcceptingLoadExceptions { get; set; } = null!; + [Required] + public string AssembliesAcceptingLoadExceptions { get; set; } = null!; - /// - /// By default the entry assemblies for scanning plugin types is the Umbraco DLLs. If you require - /// scanning for plugins based on different root referenced assemblies you can add the assembly name to this list. - /// - public IEnumerable? AdditionalEntryAssemblies { get; set; } - } + /// + /// By default the entry assemblies for scanning plugin types is the Umbraco DLLs. If you require + /// scanning for plugins based on different root referenced assemblies you can add the assembly name to this list. + /// + public IEnumerable? AdditionalEntryAssemblies { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs index d016e3547b..bec6d77bfb 100644 --- a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs @@ -1,30 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for the plugins. +/// +[UmbracoOptions(Constants.Configuration.ConfigPlugins)] +public class UmbracoPluginSettings { /// - /// Typed configuration options for the plugins. + /// Gets or sets the allowed file extensions (including the period ".") that should be accessible from the browser. /// - [UmbracoOptions(Constants.Configuration.ConfigPlugins)] - public class UmbracoPluginSettings + /// WB-TODO + public ISet BrowsableFileExtensions { get; set; } = new HashSet(new[] { - /// - /// Gets or sets the allowed file extensions (including the period ".") that should be accessible from the browser. - /// - /// WB-TODO - public ISet BrowsableFileExtensions { get; set; } = new HashSet(new[] - { - ".html", // markup - ".css", // styles - ".js", // scripts - ".jpg", ".jpeg", ".gif", ".png", ".svg", // images - ".eot", ".ttf", ".woff", // fonts - ".xml", ".json", ".config", // configurations - ".lic", // license - ".map" // js map files - }); - } + ".html", // markup + ".css", // styles + ".js", // scripts + ".jpg", ".jpeg", ".gif", ".png", ".svg", // images + ".eot", ".ttf", ".woff", // fonts + ".xml", ".json", ".config", // configurations + ".lic", // license + ".map", // js map files + }); } diff --git a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs index 08a4af5667..577fb9a2d9 100644 --- a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs @@ -4,57 +4,58 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for unattended settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigUnattended)] +public class UnattendedSettings { + private const bool StaticInstallUnattended = false; + private const bool StaticUpgradeUnattended = false; + /// - /// Typed configuration options for unattended settings. + /// Gets or sets a value indicating whether unattended installs are enabled. /// - [UmbracoOptions(Constants.Configuration.ConfigUnattended)] - public class UnattendedSettings - { - private const bool StaticInstallUnattended = false; - private const bool StaticUpgradeUnattended = false; + /// + /// + /// By default, when a database connection string is configured and it is possible to connect to + /// the database, but the database is empty, the runtime enters the Install level. + /// If this option is set to true an unattended install will be performed and the runtime enters + /// the Run level. + /// + /// + [DefaultValue(StaticInstallUnattended)] + public bool InstallUnattended { get; set; } = StaticInstallUnattended; - /// - /// Gets or sets a value indicating whether unattended installs are enabled. - /// - /// - /// By default, when a database connection string is configured and it is possible to connect to - /// the database, but the database is empty, the runtime enters the Install level. - /// If this option is set to true an unattended install will be performed and the runtime enters - /// the Run level. - /// - [DefaultValue(StaticInstallUnattended)] - public bool InstallUnattended { get; set; } = StaticInstallUnattended; + /// + /// Gets or sets a value indicating whether unattended upgrades are enabled. + /// + [DefaultValue(StaticUpgradeUnattended)] + public bool UpgradeUnattended { get; set; } = StaticUpgradeUnattended; - /// - /// Gets or sets a value indicating whether unattended upgrades are enabled. - /// - [DefaultValue(StaticUpgradeUnattended)] - public bool UpgradeUnattended { get; set; } = StaticUpgradeUnattended; + /// + /// Gets or sets a value indicating whether unattended package migrations are enabled. + /// + /// + /// This is true by default. + /// + public bool PackageMigrationsUnattended { get; set; } = true; - /// - /// Gets or sets a value indicating whether unattended package migrations are enabled. - /// - /// - /// This is true by default. - /// - public bool PackageMigrationsUnattended { get; set; } = true; + /// + /// Gets or sets a value to use for creating a user with a name for Unattended Installs + /// + public string? UnattendedUserName { get; set; } = null; - /// - /// Gets or sets a value to use for creating a user with a name for Unattended Installs - /// - public string? UnattendedUserName { get; set; } = null; + /// + /// Gets or sets a value to use for creating a user with an email for Unattended Installs + /// + [EmailAddress] + public string? UnattendedUserEmail { get; set; } = null; - /// - /// Gets or sets a value to use for creating a user with an email for Unattended Installs - /// - [EmailAddress] - public string? UnattendedUserEmail { get; set; } = null; - - /// - /// Gets or sets a value to use for creating a user with a password for Unattended Installs - /// - public string? UnattendedUserPassword { get; set; } = null; - } + /// + /// Gets or sets a value to use for creating a user with a password for Unattended Installs + /// + public string? UnattendedUserPassword { get; set; } = null; } diff --git a/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs b/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs index b53e98f712..156f90419c 100644 --- a/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs @@ -3,47 +3,46 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for user password settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigUserPassword)] +public class UserPasswordConfigurationSettings : IPasswordConfiguration { - /// - /// Typed configuration options for user password settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigUserPassword)] - public class UserPasswordConfigurationSettings : IPasswordConfiguration - { - internal const int StaticRequiredLength = 10; - internal const bool StaticRequireNonLetterOrDigit = false; - internal const bool StaticRequireDigit = false; - internal const bool StaticRequireLowercase = false; - internal const bool StaticRequireUppercase = false; - internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; + internal const int StaticRequiredLength = 10; + internal const bool StaticRequireNonLetterOrDigit = false; + internal const bool StaticRequireDigit = false; + internal const bool StaticRequireLowercase = false; + internal const bool StaticRequireUppercase = false; + internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; - /// - [DefaultValue(StaticRequiredLength)] - public int RequiredLength { get; set; } = StaticRequiredLength; + /// + [DefaultValue(StaticRequiredLength)] + public int RequiredLength { get; set; } = StaticRequiredLength; - /// - [DefaultValue(StaticRequireNonLetterOrDigit)] - public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; + /// + [DefaultValue(StaticRequireNonLetterOrDigit)] + public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; - /// - [DefaultValue(StaticRequireDigit)] - public bool RequireDigit { get; set; } = StaticRequireDigit; + /// + [DefaultValue(StaticRequireDigit)] + public bool RequireDigit { get; set; } = StaticRequireDigit; - /// - [DefaultValue(StaticRequireLowercase)] - public bool RequireLowercase { get; set; } = StaticRequireLowercase; + /// + [DefaultValue(StaticRequireLowercase)] + public bool RequireLowercase { get; set; } = StaticRequireLowercase; - /// - [DefaultValue(StaticRequireUppercase)] - public bool RequireUppercase { get; set; } = StaticRequireUppercase; + /// + [DefaultValue(StaticRequireUppercase)] + public bool RequireUppercase { get; set; } = StaticRequireUppercase; - /// - [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] - public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; + /// + [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] + public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; - /// - [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] - public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; - } + /// + [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] + public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs index ca5d4d11e5..447a27f026 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs @@ -1,75 +1,73 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Base class for configuration validators. +/// +public abstract class ConfigurationValidatorBase { /// - /// Base class for configuration validators. + /// Validates that a string is one of a set of valid values. /// - public abstract class ConfigurationValidatorBase + /// Configuration path from where the setting is found. + /// The value to check. + /// The set of valid values. + /// A message to output if the value does not match. + /// True if valid, false if not. + public bool ValidateStringIsOneOfValidValues(string configPath, string value, IEnumerable validValues, out string message) { - /// - /// Validates that a string is one of a set of valid values. - /// - /// Configuration path from where the setting is found. - /// The value to check. - /// The set of valid values. - /// A message to output if the value does not match. - /// True if valid, false if not. - public bool ValidateStringIsOneOfValidValues(string configPath, string value, IEnumerable validValues, out string message) + if (!validValues.InvariantContains(value)) { - if (!validValues.InvariantContains(value)) - { - message = $"Configuration entry {configPath} contains an invalid value '{value}', it should be one of the following: '{string.Join(", ", validValues)}'."; - return false; - } - - message = string.Empty; - return true; + message = + $"Configuration entry {configPath} contains an invalid value '{value}', it should be one of the following: '{string.Join(", ", validValues)}'."; + return false; } - /// - /// Validates that a collection of objects are all valid based on their data annotations. - /// - /// Configuration path from where the setting is found. - /// The values to check. - /// Description of validation appended to message if validation fails. - /// A message to output if the value does not match. - /// True if valid, false if not. - public bool ValidateCollection(string configPath, IEnumerable values, string validationDescription, out string message) - { - if (values.Any(x => !x.IsValid())) - { - message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; - return false; - } + message = string.Empty; + return true; + } - message = string.Empty; - return true; + /// + /// Validates that a collection of objects are all valid based on their data annotations. + /// + /// Configuration path from where the setting is found. + /// The values to check. + /// Description of validation appended to message if validation fails. + /// A message to output if the value does not match. + /// True if valid, false if not. + public bool ValidateCollection(string configPath, IEnumerable values, string validationDescription, out string message) + { + if (values.Any(x => !x.IsValid())) + { + message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; + return false; } - /// - /// Validates a configuration entry is valid if provided. - /// - /// Configuration path from where the setting is found. - /// The value to check. - /// Description of validation appended to message if validation fails. - /// A message to output if the value does not match. - /// True if valid, false if not. - public bool ValidateOptionalEntry(string configPath, ValidatableEntryBase? value, string validationDescription, out string message) - { - if (value != null && !value.IsValid()) - { - message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; - return false; - } + message = string.Empty; + return true; + } - message = string.Empty; - return true; + /// + /// Validates a configuration entry is valid if provided. + /// + /// Configuration path from where the setting is found. + /// The value to check. + /// Description of validation appended to message if validation fails. + /// A message to output if the value does not match. + /// True if valid, false if not. + public bool ValidateOptionalEntry(string configPath, ValidatableEntryBase? value, string validationDescription, out string message) + { + if (value != null && !value.IsValid()) + { + message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; + return false; } + + message = string.Empty; + return true; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs index d21d6277bf..0798014600 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs @@ -1,36 +1,42 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class ContentSettingsValidator : ConfigurationValidatorBase, IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class ContentSettingsValidator : ConfigurationValidatorBase, IValidateOptions + /// + public ValidateOptionsResult Validate(string name, ContentSettings options) { - /// - public ValidateOptionsResult Validate(string name, ContentSettings options) + if (!ValidateError404Collection(options.Error404Collection, out var message)) { - if (!ValidateError404Collection(options.Error404Collection, out string message)) - { - return ValidateOptionsResult.Fail(message); - } - - if (!ValidateAutoFillImageProperties(options.Imaging.AutoFillImageProperties, out message)) - { - return ValidateOptionsResult.Fail(message); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateError404Collection(IEnumerable values, out string message) => - ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Error404Collection)}", values, "Culture and one and only one of ContentId, ContentKey and ContentXPath must be specified for each entry", out message); + if (!ValidateAutoFillImageProperties(options.Imaging.AutoFillImageProperties, out message)) + { + return ValidateOptionsResult.Fail(message); + } - private bool ValidateAutoFillImageProperties(IEnumerable values, out string message) => - ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Imaging)}:{nameof(ContentSettings.Imaging.AutoFillImageProperties)}", values, "Alias, WidthFieldAlias, HeightFieldAlias, LengthFieldAlias and ExtensionFieldAlias must be specified for each entry", out message); + return ValidateOptionsResult.Success; } + + private bool ValidateError404Collection(IEnumerable values, out string message) => + ValidateCollection( + $"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Error404Collection)}", + values, + "Culture and one and only one of ContentId, ContentKey and ContentXPath must be specified for each entry", + out message); + + private bool ValidateAutoFillImageProperties(IEnumerable values, out string message) => + ValidateCollection( + $"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Imaging)}:{nameof(ContentSettings.Imaging.AutoFillImageProperties)}", + values, + "Alias, WidthFieldAlias, HeightFieldAlias, LengthFieldAlias and ExtensionFieldAlias must be specified for each entry", + out message); } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs index 31d0779626..32ad130c33 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs @@ -1,48 +1,51 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class GlobalSettingsValidator + : ConfigurationValidatorBase, IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class GlobalSettingsValidator - : ConfigurationValidatorBase, IValidateOptions + /// + public ValidateOptionsResult Validate(string name, GlobalSettings options) { - /// - public ValidateOptionsResult Validate(string name, GlobalSettings options) + if (!ValidateSmtpSetting(options.Smtp, out var message)) { - if (!ValidateSmtpSetting(options.Smtp, out var message)) - { - return ValidateOptionsResult.Fail(message); - } - - if (!ValidateSqlWriteLockTimeOutSetting(options.DistributedLockingWriteLockDefaultTimeout, out var message2)) - { - return ValidateOptionsResult.Fail(message2); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateSmtpSetting(SmtpSettings? value, out string message) => - ValidateOptionalEntry($"{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.Smtp)}", value, "A valid From email address is required", out message); - - private bool ValidateSqlWriteLockTimeOutSetting(TimeSpan configuredTimeOut, out string message) { - // Only apply this setting if it's not excessively high or low - const int minimumTimeOut = 100; - const int maximumTimeOut = 20000; - if (configuredTimeOut.TotalMilliseconds < minimumTimeOut || configuredTimeOut.TotalMilliseconds > maximumTimeOut) // between 0.1 and 20 seconds - { - message = $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.DistributedLockingWriteLockDefaultTimeout)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; - return false; - } - - message = string.Empty; - return true; + if (!ValidateSqlWriteLockTimeOutSetting(options.DistributedLockingWriteLockDefaultTimeout, out var message2)) + { + return ValidateOptionsResult.Fail(message2); } + + return ValidateOptionsResult.Success; + } + + private bool ValidateSmtpSetting(SmtpSettings? value, out string message) => + ValidateOptionalEntry($"{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.Smtp)}", value, "A valid From email address is required", out message); + + private bool ValidateSqlWriteLockTimeOutSetting(TimeSpan configuredTimeOut, out string message) + { + // Only apply this setting if it's not excessively high or low + const int minimumTimeOut = 100; + const int maximumTimeOut = 20000; + + // between 0.1 and 20 seconds + if (configuredTimeOut.TotalMilliseconds < minimumTimeOut || + configuredTimeOut.TotalMilliseconds > maximumTimeOut) + { + message = + $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.DistributedLockingWriteLockDefaultTimeout)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; + return false; + } + + message = string.Empty; + return true; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs index a8b63f39a0..ac0e1651ea 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs @@ -3,45 +3,47 @@ using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class HealthChecksSettingsValidator : ConfigurationValidatorBase, IValidateOptions { + private readonly ICronTabParser _cronTabParser; + /// - /// Validator for configuration representated as . + /// Initializes a new instance of the class. /// - public class HealthChecksSettingsValidator : ConfigurationValidatorBase, IValidateOptions + /// Helper for parsing crontab expressions. + public HealthChecksSettingsValidator(ICronTabParser cronTabParser) => _cronTabParser = cronTabParser; + + /// + public ValidateOptionsResult Validate(string name, HealthChecksSettings options) { - private readonly ICronTabParser _cronTabParser; - - /// - /// Initializes a new instance of the class. - /// - /// Helper for parsing crontab expressions. - public HealthChecksSettingsValidator(ICronTabParser cronTabParser) => _cronTabParser = cronTabParser; - - /// - public ValidateOptionsResult Validate(string name, HealthChecksSettings options) + if (!ValidateNotificationFirstRunTime(options.Notification.FirstRunTime, out var message)) { - if (!ValidateNotificationFirstRunTime(options.Notification.FirstRunTime, out var message)) - { - return ValidateOptionsResult.Fail(message); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateNotificationFirstRunTime(string value, out string message) => - ValidateOptionalCronTab($"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", value, out message); + return ValidateOptionsResult.Success; + } - private bool ValidateOptionalCronTab(string configPath, string value, out string message) + private bool ValidateNotificationFirstRunTime(string value, out string message) => + ValidateOptionalCronTab( + $"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", + value, + out message); + + private bool ValidateOptionalCronTab(string configPath, string value, out string message) + { + if (!string.IsNullOrEmpty(value) && !_cronTabParser.IsValidCronTab(value)) { - if (!string.IsNullOrEmpty(value) && !_cronTabParser.IsValidCronTab(value)) - { - message = $"Configuration entry {configPath} contains an invalid cron expression."; - return false; - } - - message = string.Empty; - return true; + message = $"Configuration entry {configPath} contains an invalid cron expression."; + return false; } + + message = string.Empty; + return true; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs index 6260341c18..4a1872cf30 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs @@ -3,28 +3,28 @@ using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class RequestHandlerSettingsValidator : ConfigurationValidatorBase, IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class RequestHandlerSettingsValidator : ConfigurationValidatorBase, IValidateOptions + /// + public ValidateOptionsResult Validate(string name, RequestHandlerSettings options) { - /// - public ValidateOptionsResult Validate(string name, RequestHandlerSettings options) + if (!ValidateConvertUrlsToAscii(options.ConvertUrlsToAscii, out var message)) { - if (!ValidateConvertUrlsToAscii(options.ConvertUrlsToAscii, out var message)) - { - return ValidateOptionsResult.Fail(message); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateConvertUrlsToAscii(string value, out string message) - { - var validValues = new[] { "try", "true", "false" }; - return ValidateStringIsOneOfValidValues($"{Constants.Configuration.ConfigRequestHandler}:{nameof(RequestHandlerSettings.ConvertUrlsToAscii)}", value, validValues, out message); - } + return ValidateOptionsResult.Success; + } + + private bool ValidateConvertUrlsToAscii(string value, out string message) + { + var validValues = new[] { "try", "true", "false" }; + return ValidateStringIsOneOfValidValues( + $"{Constants.Configuration.ConfigRequestHandler}:{nameof(RequestHandlerSettings.ConvertUrlsToAscii)}", value, validValues, out message); } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs index 3c073ac100..e262de76e7 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs @@ -1,44 +1,44 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class UnattendedSettingsValidator + : IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class UnattendedSettingsValidator - : IValidateOptions + /// + public ValidateOptionsResult Validate(string name, UnattendedSettings options) { - /// - public ValidateOptionsResult Validate(string name, UnattendedSettings options) + if (options.InstallUnattended) { - if (options.InstallUnattended) + var setValues = 0; + if (!string.IsNullOrEmpty(options.UnattendedUserName)) { - int setValues = 0; - if (!string.IsNullOrEmpty(options.UnattendedUserName)) - { - setValues++; - } - - if (!string.IsNullOrEmpty(options.UnattendedUserEmail)) - { - setValues++; - } - - if (!string.IsNullOrEmpty(options.UnattendedUserPassword)) - { - setValues++; - } - - if (0 < setValues && setValues < 3) - { - return ValidateOptionsResult.Fail($"Configuration entry {Constants.Configuration.ConfigUnattended} contains invalid values.\nIf any of the {nameof(options.UnattendedUserName)}, {nameof(options.UnattendedUserEmail)}, {nameof(options.UnattendedUserPassword)} are set, all of them are required."); - } + setValues++; } - return ValidateOptionsResult.Success; + if (!string.IsNullOrEmpty(options.UnattendedUserEmail)) + { + setValues++; + } + + if (!string.IsNullOrEmpty(options.UnattendedUserPassword)) + { + setValues++; + } + + if (setValues > 0 && setValues < 3) + { + return ValidateOptionsResult.Fail( + $"Configuration entry {Constants.Configuration.ConfigUnattended} contains invalid values.\nIf any of the {nameof(options.UnattendedUserName)}, {nameof(options.UnattendedUserEmail)}, {nameof(options.UnattendedUserPassword)} are set, all of them are required."); + } } + + return ValidateOptionsResult.Success; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs b/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs index 970146a27e..ff858943ac 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs @@ -1,21 +1,19 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Provides a base class for configuration models that can be validated based on data annotations. +/// +public abstract class ValidatableEntryBase { - /// - /// Provides a base class for configuration models that can be validated based on data annotations. - /// - public abstract class ValidatableEntryBase + internal virtual bool IsValid() { - internal virtual bool IsValid() - { - var ctx = new ValidationContext(this); - var results = new List(); - return Validator.TryValidateObject(this, ctx, results, true); - } + var ctx = new ValidationContext(this); + var results = new List(); + return Validator.TryValidateObject(this, ctx, results, true); } } diff --git a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs index deb7c64a9f..c4dff7a542 100644 --- a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs @@ -4,81 +4,81 @@ using System.ComponentModel; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for web routing settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigWebRouting)] +public class WebRoutingSettings { + internal const bool StaticTryMatchingEndpointsForAllPages = false; + internal const bool StaticTrySkipIisCustomErrors = false; + internal const bool StaticInternalRedirectPreservesTemplate = false; + internal const bool StaticDisableAlternativeTemplates = false; + internal const bool StaticValidateAlternativeTemplates = false; + internal const bool StaticDisableFindContentByIdPath = false; + internal const bool StaticDisableRedirectUrlTracking = false; + internal const string StaticUrlProviderMode = "Auto"; + /// - /// Typed configuration options for web routing settings. + /// Gets or sets a value indicating whether to check if any routed endpoints match a front-end request before + /// the Umbraco dynamic router tries to map the request to an Umbraco content item. /// - [UmbracoOptions(Constants.Configuration.ConfigWebRouting)] - public class WebRoutingSettings - { + /// + /// This should not be necessary if the Umbraco catch-all/dynamic route is registered last like it's supposed to be. In + /// that case + /// ASP.NET Core will automatically handle this in all cases. This is more of a backward compatible option since this + /// is what v7/v8 used + /// to do. + /// + [DefaultValue(StaticTryMatchingEndpointsForAllPages)] + public bool TryMatchingEndpointsForAllPages { get; set; } = StaticTryMatchingEndpointsForAllPages; - internal const bool StaticTryMatchingEndpointsForAllPages = false; - internal const bool StaticTrySkipIisCustomErrors = false; - internal const bool StaticInternalRedirectPreservesTemplate = false; - internal const bool StaticDisableAlternativeTemplates = false; - internal const bool StaticValidateAlternativeTemplates = false; - internal const bool StaticDisableFindContentByIdPath = false; - internal const bool StaticDisableRedirectUrlTracking = false; - internal const string StaticUrlProviderMode = "Auto"; + /// + /// Gets or sets a value indicating whether IIS custom errors should be skipped. + /// + [DefaultValue(StaticTrySkipIisCustomErrors)] + public bool TrySkipIisCustomErrors { get; set; } = StaticTrySkipIisCustomErrors; - /// - /// Gets or sets a value indicating whether to check if any routed endpoints match a front-end request before - /// the Umbraco dynamic router tries to map the request to an Umbraco content item. - /// - /// - /// This should not be necessary if the Umbraco catch-all/dynamic route is registered last like it's supposed to be. In that case - /// ASP.NET Core will automatically handle this in all cases. This is more of a backward compatible option since this is what v7/v8 used - /// to do. - /// - [DefaultValue(StaticTryMatchingEndpointsForAllPages)] - public bool TryMatchingEndpointsForAllPages { get; set; } = StaticTryMatchingEndpointsForAllPages; + /// + /// Gets or sets a value indicating whether an internal redirect should preserve the template. + /// + [DefaultValue(StaticInternalRedirectPreservesTemplate)] + public bool InternalRedirectPreservesTemplate { get; set; } = StaticInternalRedirectPreservesTemplate; - /// - /// Gets or sets a value indicating whether IIS custom errors should be skipped. - /// - [DefaultValue(StaticTrySkipIisCustomErrors)] - public bool TrySkipIisCustomErrors { get; set; } = StaticTrySkipIisCustomErrors; + /// + /// Gets or sets a value indicating whether the use of alternative templates are disabled. + /// + [DefaultValue(StaticDisableAlternativeTemplates)] + public bool DisableAlternativeTemplates { get; set; } = StaticDisableAlternativeTemplates; - /// - /// Gets or sets a value indicating whether an internal redirect should preserve the template. - /// - [DefaultValue(StaticInternalRedirectPreservesTemplate)] - public bool InternalRedirectPreservesTemplate { get; set; } = StaticInternalRedirectPreservesTemplate; + /// + /// Gets or sets a value indicating whether the use of alternative templates should be validated. + /// + [DefaultValue(StaticValidateAlternativeTemplates)] + public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates; - /// - /// Gets or sets a value indicating whether the use of alternative templates are disabled. - /// - [DefaultValue(StaticDisableAlternativeTemplates)] - public bool DisableAlternativeTemplates { get; set; } = StaticDisableAlternativeTemplates; + /// + /// Gets or sets a value indicating whether find content ID by path is disabled. + /// + [DefaultValue(StaticDisableFindContentByIdPath)] + public bool DisableFindContentByIdPath { get; set; } = StaticDisableFindContentByIdPath; - /// - /// Gets or sets a value indicating whether the use of alternative templates should be validated. - /// - [DefaultValue(StaticValidateAlternativeTemplates)] - public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates; + /// + /// Gets or sets a value indicating whether redirect URL tracking is disabled. + /// + [DefaultValue(StaticDisableRedirectUrlTracking)] + public bool DisableRedirectUrlTracking { get; set; } = StaticDisableRedirectUrlTracking; - /// - /// Gets or sets a value indicating whether find content ID by path is disabled. - /// - [DefaultValue(StaticDisableFindContentByIdPath)] - public bool DisableFindContentByIdPath { get; set; } = StaticDisableFindContentByIdPath; + /// + /// Gets or sets a value for the URL provider mode (). + /// + [DefaultValue(StaticUrlProviderMode)] + public UrlMode UrlProviderMode { get; set; } = Enum.Parse(StaticUrlProviderMode); - /// - /// Gets or sets a value indicating whether redirect URL tracking is disabled. - /// - [DefaultValue(StaticDisableRedirectUrlTracking)] - public bool DisableRedirectUrlTracking { get; set; } = StaticDisableRedirectUrlTracking; - - /// - /// Gets or sets a value for the URL provider mode (). - /// - [DefaultValue(StaticUrlProviderMode)] - public UrlMode UrlProviderMode { get; set; } = Enum.Parse(StaticUrlProviderMode); - - /// - /// Gets or sets a value for the Umbraco application URL. - /// - public string UmbracoApplicationUrl { get; set; } = null!; - } + /// + /// Gets or sets a value for the Umbraco application URL. + /// + public string UmbracoApplicationUrl { get; set; } = null!; } diff --git a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs index 1b1ebc6af5..bcd659c734 100644 --- a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs @@ -1,57 +1,61 @@ -using System.IO; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ModelsBuilderConfigExtensions { - public static class ModelsBuilderConfigExtensions + private static string? _modelsDirectoryAbsolute; + + public static string ModelsDirectoryAbsolute( + this ModelsBuilderSettings modelsBuilderConfig, + IHostingEnvironment hostingEnvironment) { - private static string? _modelsDirectoryAbsolute = null; - - public static string ModelsDirectoryAbsolute(this ModelsBuilderSettings modelsBuilderConfig, IHostingEnvironment hostingEnvironment) + if (_modelsDirectoryAbsolute is null) { - if (_modelsDirectoryAbsolute is null) - { - var modelsDirectory = modelsBuilderConfig.ModelsDirectory; - var root = hostingEnvironment.MapPathContentRoot("~/"); + var modelsDirectory = modelsBuilderConfig.ModelsDirectory; + var root = hostingEnvironment.MapPathContentRoot("~/"); - _modelsDirectoryAbsolute = GetModelsDirectory(root, modelsDirectory, - modelsBuilderConfig.AcceptUnsafeModelsDirectory); - } - - return _modelsDirectoryAbsolute; + _modelsDirectoryAbsolute = GetModelsDirectory(root, modelsDirectory, modelsBuilderConfig.AcceptUnsafeModelsDirectory); } - // internal for tests - internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) + return _modelsDirectoryAbsolute; + } + + // internal for tests + internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) + { + // making sure it is safe, ie under the website root, + // unless AcceptUnsafeModelsDirectory and then everything is OK. + if (!Path.IsPathRooted(root)) { - // making sure it is safe, ie under the website root, - // unless AcceptUnsafeModelsDirectory and then everything is OK. + throw new ConfigurationException($"Root is not rooted \"{root}\"."); + } - if (!Path.IsPathRooted(root)) - throw new ConfigurationException($"Root is not rooted \"{root}\"."); + if (config.StartsWith("~/")) + { + var dir = Path.Combine(root, config.TrimStart("~/")); - if (config.StartsWith("~/")) + // sanitize - GetFullPath will take care of any relative + // segments in path, eg '../../foo.tmp' - it may throw a SecurityException + // if the combined path reaches illegal parts of the filesystem + dir = Path.GetFullPath(dir); + root = Path.GetFullPath(root); + + if (!dir.StartsWith(root) && !acceptUnsafe) { - var dir = Path.Combine(root, config.TrimStart("~/")); - - // sanitize - GetFullPath will take care of any relative - // segments in path, eg '../../foo.tmp' - it may throw a SecurityException - // if the combined path reaches illegal parts of the filesystem - dir = Path.GetFullPath(dir); - root = Path.GetFullPath(root); - - if (!dir.StartsWith(root) && !acceptUnsafe) - throw new ConfigurationException($"Invalid models directory \"{config}\"."); - - return dir; + throw new ConfigurationException($"Invalid models directory \"{config}\"."); } - if (acceptUnsafe) - return Path.GetFullPath(config); - - throw new ConfigurationException($"Invalid models directory \"{config}\"."); + return dir; } + + if (acceptUnsafe) + { + return Path.GetFullPath(config); + } + + throw new ConfigurationException($"Invalid models directory \"{config}\"."); } } diff --git a/src/Umbraco.Core/Configuration/ModelsMode.cs b/src/Umbraco.Core/Configuration/ModelsMode.cs index 064e035892..9e76710e2b 100644 --- a/src/Umbraco.Core/Configuration/ModelsMode.cs +++ b/src/Umbraco.Core/Configuration/ModelsMode.cs @@ -1,39 +1,42 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Defines the models generation modes. +/// +public enum ModelsMode { /// - /// Defines the models generation modes. + /// Do not generate strongly typed models. /// - public enum ModelsMode - { - /// - /// Do not generate strongly typed models. - /// - /// - /// This means that only IPublishedContent instances will be used. - /// - Nothing = 0, + /// + /// This means that only IPublishedContent instances will be used. + /// + Nothing = 0, - /// - /// Generate models in memory. - /// When: a content type change occurs. - /// - /// The app does not restart. Models are available in views exclusively. - InMemoryAuto, + /// + /// Generate models in memory. + /// When: a content type change occurs. + /// + /// The app does not restart. Models are available in views exclusively. + InMemoryAuto, - /// - /// Generate models as *.cs files. - /// When: generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does not restart. - /// Models are not compiled and thus are not available to the project. - SourceCodeManual, + /// + /// Generate models as *.cs files. + /// When: generation is triggered. + /// + /// + /// Generation can be triggered from the dashboard. The app does not restart. + /// Models are not compiled and thus are not available to the project. + /// + SourceCodeManual, - /// - /// Generate models as *.cs files. - /// When: a content type change occurs, or generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does not restart. - /// Models are not compiled and thus are not available to the project. - SourceCodeAuto - } + /// + /// Generate models as *.cs files. + /// When: a content type change occurs, or generation is triggered. + /// + /// + /// Generation can be triggered from the dashboard. The app does not restart. + /// Models are not compiled and thus are not available to the project. + /// + SourceCodeAuto, } diff --git a/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs b/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs index f27d54b55d..52256a29f0 100644 --- a/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs @@ -1,28 +1,27 @@ using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions for the enumeration. +/// +public static class ModelsModeExtensions { /// - /// Provides extensions for the enumeration. + /// Gets a value indicating whether the mode is *Auto. /// - public static class ModelsModeExtensions - { - /// - /// Gets a value indicating whether the mode is *Auto. - /// - public static bool IsAuto(this ModelsMode modelsMode) - => modelsMode == ModelsMode.InMemoryAuto || modelsMode == ModelsMode.SourceCodeAuto; + public static bool IsAuto(this ModelsMode modelsMode) + => modelsMode == ModelsMode.InMemoryAuto || modelsMode == ModelsMode.SourceCodeAuto; - /// - /// Gets a value indicating whether the mode is *Auto but not InMemory. - /// - public static bool IsAutoNotInMemory(this ModelsMode modelsMode) - => modelsMode == ModelsMode.SourceCodeAuto; + /// + /// Gets a value indicating whether the mode is *Auto but not InMemory. + /// + public static bool IsAutoNotInMemory(this ModelsMode modelsMode) + => modelsMode == ModelsMode.SourceCodeAuto; - /// - /// Gets a value indicating whether the mode supports explicit manual generation. - /// - public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) - => modelsMode == ModelsMode.SourceCodeManual || modelsMode == ModelsMode.SourceCodeAuto; - } + /// + /// Gets a value indicating whether the mode supports explicit manual generation. + /// + public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) + => modelsMode == ModelsMode.SourceCodeManual || modelsMode == ModelsMode.SourceCodeAuto; } diff --git a/src/Umbraco.Core/Configuration/PasswordConfiguration.cs b/src/Umbraco.Core/Configuration/PasswordConfiguration.cs index 506821df6d..4c74720860 100644 --- a/src/Umbraco.Core/Configuration/PasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/PasswordConfiguration.cs @@ -1,37 +1,34 @@ -using System; +namespace Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Configuration +public abstract class PasswordConfiguration : IPasswordConfiguration { - public abstract class PasswordConfiguration : IPasswordConfiguration + protected PasswordConfiguration(IPasswordConfiguration configSettings) { - protected PasswordConfiguration(IPasswordConfiguration configSettings) + if (configSettings == null) { - if (configSettings == null) - { - throw new ArgumentNullException(nameof(configSettings)); - } - - RequiredLength = configSettings.RequiredLength; - RequireNonLetterOrDigit = configSettings.RequireNonLetterOrDigit; - RequireDigit = configSettings.RequireDigit; - RequireLowercase = configSettings.RequireLowercase; - RequireUppercase = configSettings.RequireUppercase; - HashAlgorithmType = configSettings.HashAlgorithmType; - MaxFailedAccessAttemptsBeforeLockout = configSettings.MaxFailedAccessAttemptsBeforeLockout; + throw new ArgumentNullException(nameof(configSettings)); } - public int RequiredLength { get; } - - public bool RequireNonLetterOrDigit { get; } - - public bool RequireDigit { get; } - - public bool RequireLowercase { get; } - - public bool RequireUppercase { get; } - - public string HashAlgorithmType { get; } - - public int MaxFailedAccessAttemptsBeforeLockout { get; } + RequiredLength = configSettings.RequiredLength; + RequireNonLetterOrDigit = configSettings.RequireNonLetterOrDigit; + RequireDigit = configSettings.RequireDigit; + RequireLowercase = configSettings.RequireLowercase; + RequireUppercase = configSettings.RequireUppercase; + HashAlgorithmType = configSettings.HashAlgorithmType; + MaxFailedAccessAttemptsBeforeLockout = configSettings.MaxFailedAccessAttemptsBeforeLockout; } + + public int RequiredLength { get; } + + public bool RequireNonLetterOrDigit { get; } + + public bool RequireDigit { get; } + + public bool RequireLowercase { get; } + + public bool RequireUppercase { get; } + + public string HashAlgorithmType { get; } + + public int MaxFailedAccessAttemptsBeforeLockout { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs index b8049fe650..8740b81cb5 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs @@ -1,41 +1,38 @@ -using System.Collections.Generic; -using Umbraco.Cms.Core.Configuration.Models; +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +public class CharacterReplacementEqualityComparer : IEqualityComparer { - public class CharacterReplacementEqualityComparer : IEqualityComparer + public bool Equals(IChar? x, IChar? y) { - public bool Equals(IChar? x, IChar? y) + if (ReferenceEquals(x, y)) { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return false; - } - - if (y is null) - { - return false; - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - return x.Char == y.Char && x.Replacement == y.Replacement; + return true; } - public int GetHashCode(IChar obj) + if (x is null) { - unchecked - { - return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0); - } + return false; + } + + if (y is null) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.Char == y.Char && x.Replacement == y.Replacement; + } + + public int GetHashCode(IChar obj) + { + unchecked + { + return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ + (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs index 61e840245c..a2ba30b776 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings -{ - public interface IChar - { - string Char { get; } +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; - string Replacement { get; } - } +public interface IChar +{ + string Char { get; } + + string Replacement { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs index c7d91a6d0a..f6431dd77a 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; + +public interface IImagingAutoFillUploadField { - public interface IImagingAutoFillUploadField - { - /// - /// Allow setting internally so we can create a default - /// - string Alias { get; } + /// + /// Allow setting internally so we can create a default + /// + string Alias { get; } - string WidthFieldAlias { get; } + string WidthFieldAlias { get; } - string HeightFieldAlias { get; } + string HeightFieldAlias { get; } - string LengthFieldAlias { get; } + string LengthFieldAlias { get; } - string ExtensionFieldAlias { get; } - } + string ExtensionFieldAlias { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs index d79d8940c3..7a309d6fe3 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs @@ -1,21 +1,20 @@ -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; + +public interface IPasswordConfigurationSection : IUmbracoConfigurationSection { - public interface IPasswordConfigurationSection : IUmbracoConfigurationSection - { - int RequiredLength { get; } + int RequiredLength { get; } - bool RequireNonLetterOrDigit { get; } + bool RequireNonLetterOrDigit { get; } - bool RequireDigit { get; } + bool RequireDigit { get; } - bool RequireLowercase { get; } + bool RequireLowercase { get; } - bool RequireUppercase { get; } + bool RequireUppercase { get; } - bool UseLegacyEncoding { get; } + bool UseLegacyEncoding { get; } - string HashAlgorithmType { get; } + string HashAlgorithmType { get; } - int MaxFailedAccessAttemptsBeforeLockout { get; } - } + int MaxFailedAccessAttemptsBeforeLockout { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs index 903f21f21a..1dfde6414f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +public interface ITypeFinderConfig { - public interface ITypeFinderConfig - { - IEnumerable AssembliesAcceptingLoadExceptions { get; } - } + IEnumerable AssembliesAcceptingLoadExceptions { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 4b4ad87801..9664d7cb73 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -1,68 +1,72 @@ -using System; using System.Reflection; using Umbraco.Cms.Core.Semver; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Represents the version of the executing code. +/// +public class UmbracoVersion : IUmbracoVersion { - /// - /// Represents the version of the executing code. - /// - public class UmbracoVersion : IUmbracoVersion + public UmbracoVersion() { - public UmbracoVersion() - { - var umbracoCoreAssembly = typeof(SemVersion).Assembly; + Assembly umbracoCoreAssembly = typeof(SemVersion).Assembly; - // gets the value indicated by the AssemblyVersion attribute - AssemblyVersion = umbracoCoreAssembly.GetName().Version; + // gets the value indicated by the AssemblyVersion attribute + AssemblyVersion = umbracoCoreAssembly.GetName().Version; - // gets the value indicated by the AssemblyFileVersion attribute - AssemblyFileVersion = System.Version.Parse(umbracoCoreAssembly.GetCustomAttribute()?.Version ?? string.Empty); + // gets the value indicated by the AssemblyFileVersion attribute + AssemblyFileVersion = + Version.Parse(umbracoCoreAssembly.GetCustomAttribute()?.Version ?? + string.Empty); - // gets the value indicated by the AssemblyInformationalVersion attribute - // this is the true semantic version of the Umbraco Cms - SemanticVersion = SemVersion.Parse(umbracoCoreAssembly.GetCustomAttribute()?.InformationalVersion ?? string.Empty); + // gets the value indicated by the AssemblyInformationalVersion attribute + // this is the true semantic version of the Umbraco Cms + SemanticVersion = + SemVersion.Parse(umbracoCoreAssembly.GetCustomAttribute() + ?.InformationalVersion ?? string.Empty); - // gets the non-semantic version - Version = SemanticVersion.GetVersion(3); - } - - /// - /// Gets the non-semantic version of the Umbraco code. - /// - public Version Version { get; } - - /// - /// Gets the semantic version comments of the Umbraco code. - /// - public string Comment => SemanticVersion.Prerelease; - - /// - /// Gets the assembly version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// Is the one that the CLR checks for compatibility. Therefore, it changes only on - /// hard-breaking changes (for instance, on new major versions). - /// - public Version? AssemblyVersion { get; } - - /// - /// Gets the assembly file version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// - public Version? AssemblyFileVersion { get; } - - /// - /// Gets the semantic version of the Umbraco code. - /// - /// - /// The semantic version is the value of the . - /// It is the full version of Umbraco, including comments. - /// - public SemVersion SemanticVersion { get; } + // gets the non-semantic version + Version = SemanticVersion.GetVersion(3); } + + /// + /// Gets the non-semantic version of the Umbraco code. + /// + public Version Version { get; } + + /// + /// Gets the semantic version comments of the Umbraco code. + /// + public string Comment => SemanticVersion.Prerelease; + + /// + /// Gets the assembly version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + /// Is the one that the CLR checks for compatibility. Therefore, it changes only on + /// hard-breaking changes (for instance, on new major versions). + /// + /// + public Version? AssemblyVersion { get; } + + /// + /// Gets the assembly file version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + public Version? AssemblyFileVersion { get; } + + /// + /// Gets the semantic version of the Umbraco code. + /// + /// + /// The semantic version is the value of the . + /// It is the full version of Umbraco, including comments. + /// + public SemVersion SemanticVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs index 6c30fbba71..47b950de9c 100644 --- a/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for back office users +/// +public class UserPasswordConfiguration : PasswordConfiguration, IUserPasswordConfiguration { - /// - /// The password configuration for back office users - /// - public class UserPasswordConfiguration : PasswordConfiguration, IUserPasswordConfiguration + public UserPasswordConfiguration(IUserPasswordConfiguration configSettings) + : base(configSettings) { - public UserPasswordConfiguration(IUserPasswordConfiguration configSettings) - : base(configSettings) - { - } } } diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index da945731af..dc36715585 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -1,162 +1,161 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the alias identifiers for Umbraco's core application sections. + /// + public static class Applications { /// - /// Defines the alias identifiers for Umbraco's core application sections. + /// Application alias for the content section. /// - public static class Applications - { - /// - /// Application alias for the content section. - /// - public const string Content = "content"; - - /// - /// Application alias for the packages section. - /// - public const string Packages = "packages"; - - /// - /// Application alias for the media section. - /// - public const string Media = "media"; - - /// - /// Application alias for the members section. - /// - public const string Members = "member"; - - /// - /// Application alias for the settings section. - /// - public const string Settings = "settings"; - - /// - /// Application alias for the translation section. - /// - public const string Translation = "translation"; - - /// - /// Application alias for the users section. - /// - public const string Users = "users"; - - /// - /// Application alias for the forms section. - /// - public const string Forms = "forms"; - } + public const string Content = "content"; /// - /// Defines the alias identifiers for Umbraco's core trees. + /// Application alias for the packages section. /// - public static class Trees + public const string Packages = "packages"; + + /// + /// Application alias for the media section. + /// + public const string Media = "media"; + + /// + /// Application alias for the members section. + /// + public const string Members = "member"; + + /// + /// Application alias for the settings section. + /// + public const string Settings = "settings"; + + /// + /// Application alias for the translation section. + /// + public const string Translation = "translation"; + + /// + /// Application alias for the users section. + /// + public const string Users = "users"; + + /// + /// Application alias for the forms section. + /// + public const string Forms = "forms"; + } + + /// + /// Defines the alias identifiers for Umbraco's core trees. + /// + public static class Trees + { + /// + /// alias for the content tree. + /// + public const string Content = "content"; + + /// + /// alias for the content blueprint tree. + /// + public const string ContentBlueprints = "contentBlueprints"; + + /// + /// alias for the member tree. + /// + public const string Members = "member"; + + /// + /// alias for the media tree. + /// + public const string Media = "media"; + + /// + /// alias for the macro tree. + /// + public const string Macros = "macros"; + + /// + /// alias for the datatype tree. + /// + public const string DataTypes = "dataTypes"; + + /// + /// alias for the packages tree + /// + public const string Packages = "packages"; + + /// + /// alias for the dictionary tree. + /// + public const string Dictionary = "dictionary"; + + public const string Stylesheets = "stylesheets"; + + /// + /// alias for the document type tree. + /// + public const string DocumentTypes = "documentTypes"; + + /// + /// alias for the media type tree. + /// + public const string MediaTypes = "mediaTypes"; + + /// + /// alias for the member type tree. + /// + public const string MemberTypes = "memberTypes"; + + /// + /// alias for the member group tree. + /// + public const string MemberGroups = "memberGroups"; + + /// + /// alias for the template tree. + /// + public const string Templates = "templates"; + + public const string RelationTypes = "relationTypes"; + + public const string Languages = "languages"; + + /// + /// alias for the user types tree. + /// + public const string UserTypes = "userTypes"; + + /// + /// alias for the user permissions tree. + /// + public const string UserPermissions = "userPermissions"; + + /// + /// alias for the users tree. + /// + public const string Users = "users"; + + public const string Scripts = "scripts"; + + public const string PartialViews = "partialViews"; + + public const string PartialViewMacros = "partialViewMacros"; + + public const string LogViewer = "logViewer"; + + public static class Groups { - /// - /// alias for the content tree. - /// - public const string Content = "content"; + public const string Settings = "settingsGroup"; - /// - /// alias for the content blueprint tree. - /// - public const string ContentBlueprints = "contentBlueprints"; + public const string Templating = "templatingGroup"; - /// - /// alias for the member tree. - /// - public const string Members = "member"; - - /// - /// alias for the media tree. - /// - public const string Media = "media"; - - /// - /// alias for the macro tree. - /// - public const string Macros = "macros"; - - /// - /// alias for the datatype tree. - /// - public const string DataTypes = "dataTypes"; - - /// - /// alias for the packages tree - /// - public const string Packages = "packages"; - - /// - /// alias for the dictionary tree. - /// - public const string Dictionary = "dictionary"; - - public const string Stylesheets = "stylesheets"; - - /// - /// alias for the document type tree. - /// - public const string DocumentTypes = "documentTypes"; - - /// - /// alias for the media type tree. - /// - public const string MediaTypes = "mediaTypes"; - - /// - /// alias for the member type tree. - /// - public const string MemberTypes = "memberTypes"; - - /// - /// alias for the member group tree. - /// - public const string MemberGroups = "memberGroups"; - - /// - /// alias for the template tree. - /// - public const string Templates = "templates"; - - public const string RelationTypes = "relationTypes"; - - public const string Languages = "languages"; - - /// - /// alias for the user types tree. - /// - public const string UserTypes = "userTypes"; - - /// - /// alias for the user permissions tree. - /// - public const string UserPermissions = "userPermissions"; - - /// - /// alias for the users tree. - /// - public const string Users = "users"; - - public const string Scripts = "scripts"; - - public const string PartialViews = "partialViews"; - - public const string PartialViewMacros = "partialViewMacros"; - - public const string LogViewer = "logViewer"; - - public static class Groups - { - public const string Settings = "settingsGroup"; - - public const string Templating = "templatingGroup"; - - public const string ThirdParty = "thirdPartyGroup"; - } - - // TODO: Fill in the rest! + public const string ThirdParty = "thirdPartyGroup"; } + + // TODO: Fill in the rest! } } diff --git a/src/Umbraco.Core/Constants-Audit.cs b/src/Umbraco.Core/Constants-Audit.cs index 54c51c95ff..f795a25974 100644 --- a/src/Umbraco.Core/Constants-Audit.cs +++ b/src/Umbraco.Core/Constants-Audit.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core; +namespace Umbraco.Cms.Core; public static partial class Constants { diff --git a/src/Umbraco.Core/Constants-CharArrays.cs b/src/Umbraco.Core/Constants-CharArrays.cs index 4be5ecba04..832cac00e6 100644 --- a/src/Umbraco.Core/Constants-CharArrays.cs +++ b/src/Umbraco.Core/Constants-CharArrays.cs @@ -1,138 +1,135 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Char Arrays to avoid allocations + /// + public static class CharArrays { /// - /// Char Arrays to avoid allocations + /// Char array containing only / /// - public static class CharArrays - { - /// - /// Char array containing only / - /// - public static readonly char[] ForwardSlash = new char[] { '/' }; + public static readonly char[] ForwardSlash = { '/' }; - /// - /// Char array containing only \ - /// - public static readonly char[] Backslash = new char[] { '\\' }; + /// + /// Char array containing only \ + /// + public static readonly char[] Backslash = { '\\' }; - /// - /// Char array containing only ' - /// - public static readonly char[] SingleQuote = new char[] { '\'' }; + /// + /// Char array containing only ' + /// + public static readonly char[] SingleQuote = { '\'' }; - /// - /// Char array containing only " - /// - public static readonly char[] DoubleQuote = new char[] { '\"' }; + /// + /// Char array containing only " + /// + public static readonly char[] DoubleQuote = { '\"' }; + /// + /// Char array containing ' " + /// + public static readonly char[] DoubleQuoteSingleQuote = { '\"', '\'' }; - /// - /// Char array containing ' " - /// - public static readonly char[] DoubleQuoteSingleQuote = new char[] { '\"', '\'' }; + /// + /// Char array containing only _ + /// + public static readonly char[] Underscore = { '_' }; - /// - /// Char array containing only _ - /// - public static readonly char[] Underscore = new char[] { '_' }; + /// + /// Char array containing \n \r + /// + public static readonly char[] LineFeedCarriageReturn = { '\n', '\r' }; - /// - /// Char array containing \n \r - /// - public static readonly char[] LineFeedCarriageReturn = new char[] { '\n', '\r' }; + /// + /// Char array containing \n + /// + public static readonly char[] LineFeed = { '\n' }; + /// + /// Char array containing only , + /// + public static readonly char[] Comma = { ',' }; - /// - /// Char array containing \n - /// - public static readonly char[] LineFeed = new char[] { '\n' }; + /// + /// Char array containing only & + /// + public static readonly char[] Ampersand = { '&' }; - /// - /// Char array containing only , - /// - public static readonly char[] Comma = new char[] { ',' }; + /// + /// Char array containing only \0 + /// + public static readonly char[] NullTerminator = { '\0' }; - /// - /// Char array containing only & - /// - public static readonly char[] Ampersand = new char[] { '&' }; + /// + /// Char array containing only . + /// + public static readonly char[] Period = { '.' }; - /// - /// Char array containing only \0 - /// - public static readonly char[] NullTerminator = new char[] { '\0' }; + /// + /// Char array containing only ~ + /// + public static readonly char[] Tilde = { '~' }; - /// - /// Char array containing only . - /// - public static readonly char[] Period = new char[] { '.' }; + /// + /// Char array containing ~ / + /// + public static readonly char[] TildeForwardSlash = { '~', '/' }; - /// - /// Char array containing only ~ - /// - public static readonly char[] Tilde = new char[] { '~' }; - /// - /// Char array containing ~ / - /// - public static readonly char[] TildeForwardSlash = new char[] { '~', '/' }; + /// + /// Char array containing ~ / \ + /// + public static readonly char[] TildeForwardSlashBackSlash = { '~', '/', '\\' }; + /// + /// Char array containing only ? + /// + public static readonly char[] QuestionMark = { '?' }; - /// - /// Char array containing ~ / \ - /// - public static readonly char[] TildeForwardSlashBackSlash = new char[] { '~', '/', '\\' }; + /// + /// Char array containing ? & + /// + public static readonly char[] QuestionMarkAmpersand = { '?', '&' }; - /// - /// Char array containing only ? - /// - public static readonly char[] QuestionMark = new char[] { '?' }; + /// + /// Char array containing XML 1.1 whitespace chars + /// + public static readonly char[] XmlWhitespaceChars = { ' ', '\t', '\r', '\n' }; - /// - /// Char array containing ? & - /// - public static readonly char[] QuestionMarkAmpersand = new char[] { '?', '&' }; + /// + /// Char array containing only the Space char + /// + public static readonly char[] Space = { ' ' }; - /// - /// Char array containing XML 1.1 whitespace chars - /// - public static readonly char[] XmlWhitespaceChars = new char[] { ' ', '\t', '\r', '\n' }; + /// + /// Char array containing only ; + /// + public static readonly char[] Semicolon = { ';' }; - /// - /// Char array containing only the Space char - /// - public static readonly char[] Space = new char[] { ' ' }; + /// + /// Char array containing a comma and a space + /// + public static readonly char[] CommaSpace = { ',', ' ' }; - /// - /// Char array containing only ; - /// - public static readonly char[] Semicolon = new char[] { ';' }; + /// + /// Char array containing _ - + /// + public static readonly char[] UnderscoreDash = { '_', '-' }; - /// - /// Char array containing a comma and a space - /// - public static readonly char[] CommaSpace = new char[] { ',', ' ' }; + /// + /// Char array containing = + /// + public static readonly char[] EqualsChar = { '=' }; - /// - /// Char array containing _ - - /// - public static readonly char[] UnderscoreDash = new char[] { '_', '-' }; + /// + /// Char array containing > + /// + public static readonly char[] GreaterThan = { '>' }; - /// - /// Char array containing = - /// - public static readonly char[] EqualsChar = new char[] { '=' }; - - /// - /// Char array containing > - /// - public static readonly char[] GreaterThan = new char[] { '>' }; - - /// - /// Char array containing | - /// - public static readonly char[] VerticalTab = new char[] { '|' }; - } + /// + /// Char array containing | + /// + public static readonly char[] VerticalTab = { '|' }; } } diff --git a/src/Umbraco.Core/Constants-Composing.cs b/src/Umbraco.Core/Constants-Composing.cs index 747a74b8d8..e55c32d01a 100644 --- a/src/Umbraco.Core/Constants-Composing.cs +++ b/src/Umbraco.Core/Constants-Composing.cs @@ -1,25 +1,19 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for composition. /// - public static partial class Constants + public static class Composing { - /// - /// Defines constants for composition. - /// - public static class Composing + public static readonly string[] UmbracoCoreAssemblyNames = { - public static readonly string[] UmbracoCoreAssemblyNames = new[] - { - "Umbraco.Core", - "Umbraco.Infrastructure", - "Umbraco.PublishedCache.NuCache", - "Umbraco.Examine.Lucene", - "Umbraco.Web.Common", - "Umbraco.Web.BackOffice", - "Umbraco.Web.Website", - }; - } + "Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", "Umbraco.Examine.Lucene", + "Umbraco.Web.Common", "Umbraco.Web.BackOffice", "Umbraco.Web.Website", + }; } } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index b9375c2619..11694fa5c0 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -1,76 +1,77 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Configuration { - public static class Configuration + /// + /// Case insensitive prefix for all configurations. + /// + /// + /// ":" is used as marker for nested objects in JSON, e.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{...}}. + /// + public const string ConfigPrefix = "Umbraco:CMS:"; + public const string ConfigContentPrefix = ConfigPrefix + "Content:"; + public const string ConfigContentNotificationsPrefix = ConfigContentPrefix + "Notifications:"; + public const string ConfigCorePrefix = ConfigPrefix + "Core:"; + public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; + public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; + public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; + public const string ConfigGlobalDistributedLockingMechanism = ConfigGlobalPrefix + "DistributedLockingMechanism"; + public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; + public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; + public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; + public const string ConfigContentNotificationsEmail = ConfigContentNotificationsPrefix + "Email"; + public const string ConfigContentMacroErrors = ConfigContentPrefix + "MacroErrors"; + public const string ConfigGlobalUseHttps = ConfigGlobalPrefix + "UseHttps"; + public const string ConfigHostingDebug = ConfigHostingPrefix + "Debug"; + public const string ConfigCustomErrorsMode = ConfigCustomErrorsPrefix + "Mode"; + public const string ConfigActiveDirectory = ConfigPrefix + "ActiveDirectory"; + public const string ConfigLegacyPasswordMigration = ConfigPrefix + "LegacyPasswordMigration"; + public const string ConfigContent = ConfigPrefix + "Content"; + public const string ConfigCoreDebug = ConfigCorePrefix + "Debug"; + public const string ConfigExceptionFilter = ConfigPrefix + "ExceptionFilter"; + public const string ConfigGlobal = ConfigPrefix + "Global"; + public const string ConfigUnattended = ConfigPrefix + "Unattended"; + public const string ConfigHealthChecks = ConfigPrefix + "HealthChecks"; + public const string ConfigHosting = ConfigPrefix + "Hosting"; + public const string ConfigImaging = ConfigPrefix + "Imaging"; + public const string ConfigExamine = ConfigPrefix + "Examine"; + public const string ConfigKeepAlive = ConfigPrefix + "KeepAlive"; + public const string ConfigLogging = ConfigPrefix + "Logging"; + public const string ConfigMemberPassword = ConfigPrefix + "Security:MemberPassword"; + public const string ConfigModelsBuilder = ConfigPrefix + "ModelsBuilder"; + public const string ConfigNuCache = ConfigPrefix + "NuCache"; + public const string ConfigPlugins = ConfigPrefix + "Plugins"; + public const string ConfigRequestHandler = ConfigPrefix + "RequestHandler"; + public const string ConfigRuntime = ConfigPrefix + "Runtime"; + public const string ConfigRuntimeMode = ConfigRuntime + ":Mode"; + public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; + public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; + public const string ConfigSecurity = ConfigPrefix + "Security"; + public const string ConfigBasicAuth = ConfigPrefix + "BasicAuth"; + public const string ConfigTours = ConfigPrefix + "Tours"; + public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; + public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; + public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; + public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; + public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; + public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; + public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; + public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; + public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; + + public static class NamedOptions { - /// - /// Case insensitive prefix for all configurations - /// - /// - /// ":" is used as marker for nested objects in json. E.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{....}} - /// - public const string ConfigPrefix = "Umbraco:CMS:"; - public const string ConfigContentPrefix = ConfigPrefix + "Content:"; - public const string ConfigContentNotificationsPrefix = ConfigContentPrefix + "Notifications:"; - public const string ConfigCorePrefix = ConfigPrefix + "Core:"; - public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; - public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; - public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; - public const string ConfigGlobalDistributedLockingMechanism = ConfigGlobalPrefix + "DistributedLockingMechanism"; - public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; - public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; - public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; - public const string ConfigContentNotificationsEmail = ConfigContentNotificationsPrefix + "Email"; - public const string ConfigContentMacroErrors = ConfigContentPrefix + "MacroErrors"; - public const string ConfigGlobalUseHttps = ConfigGlobalPrefix + "UseHttps"; - public const string ConfigHostingDebug = ConfigHostingPrefix + "Debug"; - public const string ConfigCustomErrorsMode = ConfigCustomErrorsPrefix + "Mode"; - public const string ConfigActiveDirectory = ConfigPrefix + "ActiveDirectory"; - public const string ConfigLegacyPasswordMigration = ConfigPrefix + "LegacyPasswordMigration"; - public const string ConfigContent = ConfigPrefix + "Content"; - public const string ConfigCoreDebug = ConfigCorePrefix + "Debug"; - public const string ConfigExceptionFilter = ConfigPrefix + "ExceptionFilter"; - public const string ConfigGlobal = ConfigPrefix + "Global"; - public const string ConfigUnattended = ConfigPrefix + "Unattended"; - public const string ConfigHealthChecks = ConfigPrefix + "HealthChecks"; - public const string ConfigHosting = ConfigPrefix + "Hosting"; - public const string ConfigImaging = ConfigPrefix + "Imaging"; - public const string ConfigExamine = ConfigPrefix + "Examine"; - public const string ConfigKeepAlive = ConfigPrefix + "KeepAlive"; - public const string ConfigLogging = ConfigPrefix + "Logging"; - public const string ConfigMemberPassword = ConfigPrefix + "Security:MemberPassword"; - public const string ConfigModelsBuilder = ConfigPrefix + "ModelsBuilder"; - public const string ConfigNuCache = ConfigPrefix + "NuCache"; - public const string ConfigPlugins = ConfigPrefix + "Plugins"; - public const string ConfigRequestHandler = ConfigPrefix + "RequestHandler"; - public const string ConfigRuntime = ConfigPrefix + "Runtime"; - public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; - public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; - public const string ConfigSecurity = ConfigPrefix + "Security"; - public const string ConfigBasicAuth = ConfigPrefix + "BasicAuth"; - public const string ConfigTours = ConfigPrefix + "Tours"; - public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; - public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; - public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; - public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; - public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; - public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; - public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; - public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; - - public static class NamedOptions + public static class InstallDefaultData { - public static class InstallDefaultData - { - public const string Languages = "Languages"; + public const string Languages = "Languages"; - public const string DataTypes = "DataTypes"; + public const string DataTypes = "DataTypes"; - public const string MediaTypes = "MediaTypes"; + public const string MediaTypes = "MediaTypes"; - public const string MemberTypes = "MemberTypes"; - } + public const string MemberTypes = "MemberTypes"; } } } diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 80b285aef4..22acf8cc4d 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -1,295 +1,290 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for property-type alias conventions that are used within the Umbraco core. + /// + public static class Conventions { - /// - /// Defines the identifiers for property-type alias conventions that are used within the Umbraco core. - /// - public static class Conventions + public static class Migrations { - public static class Migrations - { - public const string UmbracoUpgradePlanName = "Umbraco.Core"; - public const string KeyValuePrefix = "Umbraco.Core.Upgrader.State+"; - public const string UmbracoUpgradePlanKey = KeyValuePrefix + UmbracoUpgradePlanName; - } + public const string UmbracoUpgradePlanName = "Umbraco.Core"; + public const string KeyValuePrefix = "Umbraco.Core.Upgrader.State+"; + public const string UmbracoUpgradePlanKey = KeyValuePrefix + UmbracoUpgradePlanName; + } - public static class PermissionCategories - { - public const string ContentCategory = "content"; - public const string AdministrationCategory = "administration"; - public const string StructureCategory = "structure"; - public const string OtherCategory = "other"; - } + public static class PermissionCategories + { + public const string ContentCategory = "content"; + public const string AdministrationCategory = "administration"; + public const string StructureCategory = "structure"; + public const string OtherCategory = "other"; + } - public static class PublicAccess - { - public const string MemberUsernameRuleType = "MemberUsername"; - public const string MemberRoleRuleType = "MemberRole"; - } + public static class PublicAccess + { + public const string MemberUsernameRuleType = "MemberUsername"; + public const string MemberRoleRuleType = "MemberRole"; + } + public static class DataTypes + { + public const string ListViewPrefix = "List View - "; + } - public static class DataTypes - { - public const string ListViewPrefix = "List View - "; - } + /// + /// Constants for Umbraco Content property aliases. + /// + public static class Content + { + /// + /// Property alias for the Content's Url (internal) redirect. + /// + public const string InternalRedirectId = "umbracoInternalRedirectId"; /// - /// Constants for Umbraco Content property aliases. + /// Property alias for the Content's navigational hide, (not actually used in core code). /// - public static class Content - { - /// - /// Property alias for the Content's Url (internal) redirect. - /// - public const string InternalRedirectId = "umbracoInternalRedirectId"; - - /// - /// Property alias for the Content's navigational hide, (not actually used in core code). - /// - public const string NaviHide = "umbracoNaviHide"; - - /// - /// Property alias for the Content's Url redirect. - /// - public const string Redirect = "umbracoRedirect"; - - /// - /// Property alias for the Content's Url alias. - /// - public const string UrlAlias = "umbracoUrlAlias"; - - /// - /// Property alias for the Content's Url name. - /// - public const string UrlName = "umbracoUrlName"; - } + public const string NaviHide = "umbracoNaviHide"; /// - /// Constants for Umbraco Media property aliases. + /// Property alias for the Content's Url redirect. /// - public static class Media - { - /// - /// Property alias for the Media's file name. - /// - public const string File = "umbracoFile"; - - /// - /// Property alias for the Media's width. - /// - public const string Width = "umbracoWidth"; - - /// - /// Property alias for the Media's height. - /// - public const string Height = "umbracoHeight"; - - /// - /// Property alias for the Media's file size (in bytes). - /// - public const string Bytes = "umbracoBytes"; - - /// - /// Property alias for the Media's file extension. - /// - public const string Extension = "umbracoExtension"; - - /// - /// The default height/width of an image file if the size can't be determined from the metadata - /// - public const int DefaultSize = 200; - } + public const string Redirect = "umbracoRedirect"; /// - /// Defines the alias identifiers for Umbraco media types. + /// Property alias for the Content's Url alias. /// - public static class MediaTypes - { - /// - /// MediaType alias for a file. - /// - public const string File = "File"; - - /// - /// MediaType alias for a folder. - /// - public const string Folder = "Folder"; - - /// - /// MediaType alias for an image. - /// - public const string Image = "Image"; - - /// - /// MediaType name for a video. - /// - public const string Video = "Video"; - - /// - /// MediaType name for an audio. - /// - public const string Audio = "Audio"; - - /// - /// MediaType name for an article. - /// - public const string Article = "Article"; - - /// - /// MediaType name for vector graphics. - /// - public const string VectorGraphics = "VectorGraphics"; - - /// - /// MediaType alias for a video. - /// - public const string VideoAlias = "umbracoMediaVideo"; - - /// - /// MediaType alias for an audio. - /// - public const string AudioAlias = "umbracoMediaAudio"; - - /// - /// MediaType alias for an article. - /// - public const string ArticleAlias = "umbracoMediaArticle"; - - /// - /// MediaType alias for vector graphics. - /// - public const string VectorGraphicsAlias = "umbracoMediaVectorGraphics"; - - /// - /// MediaType alias indicating allowing auto-selection. - /// - public const string AutoSelect = "umbracoAutoSelect"; - } + public const string UrlAlias = "umbracoUrlAlias"; /// - /// Constants for Umbraco Member property aliases. + /// Property alias for the Content's Url name. /// - public static class Member - { - /// - /// if a role starts with __umbracoRole we won't show it as it's an internal role used for public access - /// - public static readonly string InternalRolePrefix = "__umbracoRole"; + public const string UrlName = "umbracoUrlName"; + } - /// - /// Property alias for the Comments on a Member - /// - public const string Comments = "umbracoMemberComments"; - - public const string CommentsLabel = "Comments"; - - /// - /// The standard properties group alias for membership properties. - /// - public const string StandardPropertiesGroupAlias = "membership"; - - /// - /// The standard properties group name for membership properties. - /// - public const string StandardPropertiesGroupName = "Membership"; - } + /// + /// Constants for Umbraco Media property aliases. + /// + public static class Media + { + /// + /// Property alias for the Media's file name. + /// + public const string File = "umbracoFile"; /// - /// Defines the alias identifiers for Umbraco member types. + /// Property alias for the Media's width. /// - public static class MemberTypes - { - /// - /// MemberType alias for default member type. - /// - public const string DefaultAlias = "Member"; - - public const string SystemDefaultProtectType = "_umbracoSystemDefaultProtectType"; - - public const string AllMembersListId = "all-members"; - } + public const string Width = "umbracoWidth"; /// - /// Constants for Umbraco URLs/Querystrings. + /// Property alias for the Media's height. /// - public static class Url - { - /// - /// Querystring parameter name used for Umbraco's alternative template functionality. - /// - public const string AltTemplate = "altTemplate"; - } + public const string Height = "umbracoHeight"; /// - /// Defines the alias identifiers for built-in Umbraco relation types. + /// Property alias for the Media's file size (in bytes). /// - public static class RelationTypes - { - /// - /// Name for default relation type "Related Media". - /// - public const string RelatedMediaName = "Related Media"; + public const string Bytes = "umbracoBytes"; - /// - /// Alias for default relation type "Related Media" - /// - public const string RelatedMediaAlias = "umbMedia"; + /// + /// Property alias for the Media's file extension. + /// + public const string Extension = "umbracoExtension"; - /// - /// Name for default relation type "Related Document". - /// - public const string RelatedDocumentName = "Related Document"; + /// + /// The default height/width of an image file if the size can't be determined from the metadata + /// + public const int DefaultSize = 200; + } - /// - /// Alias for default relation type "Related Document" - /// - public const string RelatedDocumentAlias = "umbDocument"; + /// + /// Defines the alias identifiers for Umbraco media types. + /// + public static class MediaTypes + { + /// + /// MediaType alias for a file. + /// + public const string File = "File"; - /// - /// Name for default relation type "Relate Document On Copy". - /// - public const string RelateDocumentOnCopyName = "Relate Document On Copy"; + /// + /// MediaType alias for a folder. + /// + public const string Folder = "Folder"; - /// - /// Alias for default relation type "Relate Document On Copy". - /// - public const string RelateDocumentOnCopyAlias = "relateDocumentOnCopy"; + /// + /// MediaType alias for an image. + /// + public const string Image = "Image"; - /// - /// Name for default relation type "Relate Parent Document On Delete". - /// - public const string RelateParentDocumentOnDeleteName = "Relate Parent Document On Delete"; + /// + /// MediaType name for a video. + /// + public const string Video = "Video"; - /// - /// Alias for default relation type "Relate Parent Document On Delete". - /// - public const string RelateParentDocumentOnDeleteAlias = "relateParentDocumentOnDelete"; + /// + /// MediaType name for an audio. + /// + public const string Audio = "Audio"; - /// - /// Name for default relation type "Relate Parent Media Folder On Delete". - /// - public const string RelateParentMediaFolderOnDeleteName = "Relate Parent Media Folder On Delete"; + /// + /// MediaType name for an article. + /// + public const string Article = "Article"; - /// - /// Alias for default relation type "Relate Parent Media Folder On Delete". - /// - public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; + /// + /// MediaType name for vector graphics. + /// + public const string VectorGraphics = "VectorGraphics"; - /// - /// Returns the types of relations that are automatically tracked - /// - /// - /// Developers should not manually use these relation types since they will all be cleared whenever an entity - /// (content, media or member) is saved since they are auto-populated based on property values. - /// - public static string[] AutomaticRelationTypes { get; } = new[] { RelatedMediaAlias, RelatedDocumentAlias }; + /// + /// MediaType alias for a video. + /// + public const string VideoAlias = "umbracoMediaVideo"; - //TODO: return a list of built in types so we can use that to prevent deletion in the uI - } + /// + /// MediaType alias for an audio. + /// + public const string AudioAlias = "umbracoMediaAudio"; + /// + /// MediaType alias for an article. + /// + public const string ArticleAlias = "umbracoMediaArticle"; + + /// + /// MediaType alias for vector graphics. + /// + public const string VectorGraphicsAlias = "umbracoMediaVectorGraphics"; + + /// + /// MediaType alias indicating allowing auto-selection. + /// + public const string AutoSelect = "umbracoAutoSelect"; + } + + /// + /// Constants for Umbraco Member property aliases. + /// + public static class Member + { + /// + /// Property alias for the Comments on a Member + /// + public const string Comments = "umbracoMemberComments"; + + public const string CommentsLabel = "Comments"; + + /// + /// The standard properties group alias for membership properties. + /// + public const string StandardPropertiesGroupAlias = "membership"; + + /// + /// The standard properties group name for membership properties. + /// + public const string StandardPropertiesGroupName = "Membership"; + + /// + /// if a role starts with __umbracoRole we won't show it as it's an internal role used for public access + /// + public static readonly string InternalRolePrefix = "__umbracoRole"; + } + + /// + /// Defines the alias identifiers for Umbraco member types. + /// + public static class MemberTypes + { + /// + /// MemberType alias for default member type. + /// + public const string DefaultAlias = "Member"; + + public const string SystemDefaultProtectType = "_umbracoSystemDefaultProtectType"; + + public const string AllMembersListId = "all-members"; + } + + /// + /// Constants for Umbraco URLs/Querystrings. + /// + public static class Url + { + /// + /// Querystring parameter name used for Umbraco's alternative template functionality. + /// + public const string AltTemplate = "altTemplate"; + } + + /// + /// Defines the alias identifiers for built-in Umbraco relation types. + /// + public static class RelationTypes + { + /// + /// Name for default relation type "Related Media". + /// + public const string RelatedMediaName = "Related Media"; + + /// + /// Alias for default relation type "Related Media" + /// + public const string RelatedMediaAlias = "umbMedia"; + + /// + /// Name for default relation type "Related Document". + /// + public const string RelatedDocumentName = "Related Document"; + + /// + /// Alias for default relation type "Related Document" + /// + public const string RelatedDocumentAlias = "umbDocument"; + + /// + /// Name for default relation type "Relate Document On Copy". + /// + public const string RelateDocumentOnCopyName = "Relate Document On Copy"; + + /// + /// Alias for default relation type "Relate Document On Copy". + /// + public const string RelateDocumentOnCopyAlias = "relateDocumentOnCopy"; + + /// + /// Name for default relation type "Relate Parent Document On Delete". + /// + public const string RelateParentDocumentOnDeleteName = "Relate Parent Document On Delete"; + + /// + /// Alias for default relation type "Relate Parent Document On Delete". + /// + public const string RelateParentDocumentOnDeleteAlias = "relateParentDocumentOnDelete"; + + /// + /// Name for default relation type "Relate Parent Media Folder On Delete". + /// + public const string RelateParentMediaFolderOnDeleteName = "Relate Parent Media Folder On Delete"; + + /// + /// Alias for default relation type "Relate Parent Media Folder On Delete". + /// + public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; + + /// + /// Returns the types of relations that are automatically tracked + /// + /// + /// Developers should not manually use these relation types since they will all be cleared whenever an entity + /// (content, media or member) is saved since they are auto-populated based on property values. + /// + public static string[] AutomaticRelationTypes { get; } = { RelatedMediaAlias, RelatedDocumentAlias }; + + // TODO: return a list of built in types so we can use that to prevent deletion in the uI } } } diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index ba8827cd26..a3e2dbc4c5 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -1,461 +1,428 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + public static class DataTypes { - public static class DataTypes + // NOTE: unfortunately due to backwards compat we can't move/rename these, with the addition of the GUID + // constants, it would make more sense to have these suffixed with "ID" or in a Subclass called "INT", for + // now all we can do is make a subclass called Guids to put the GUID IDs. + public const int LabelString = System.DefaultLabelDataTypeId; + public const int LabelInt = -91; + public const int LabelBigint = -93; + public const int LabelDateTime = -94; + public const int LabelTime = -98; + public const int LabelDecimal = -99; + + public const int Textarea = -89; + public const int Textbox = -88; + public const int RichtextEditor = -87; + public const int Boolean = -49; + public const int DateTime = -36; + public const int DropDownSingle = -39; + public const int DropDownMultiple = -42; + public const int Upload = -90; + public const int UploadVideo = -100; + public const int UploadAudio = -101; + public const int UploadArticle = -102; + public const int UploadVectorGraphics = -103; + + public const int DefaultContentListView = -95; + public const int DefaultMediaListView = -96; + public const int DefaultMembersListView = -97; + + public const int ImageCropper = 1043; + public const int Tags = 1041; + + public static class ReservedPreValueKeys { - //NOTE: unfortunately due to backwards compat we can't move/rename these, with the addition of the GUID - //constants, it would make more sense to have these suffixed with "ID" or in a Subclass called "INT", for - //now all we can do is make a subclass called Guids to put the GUID IDs. + public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; + } - public const int LabelString = System.DefaultLabelDataTypeId; - public const int LabelInt = -91; - public const int LabelBigint = -93; - public const int LabelDateTime = -94; - public const int LabelTime = -98; - public const int LabelDecimal = -99; - - public const int Textarea = -89; - public const int Textbox = -88; - public const int RichtextEditor = -87; - public const int Boolean = -49; - public const int DateTime = -36; - public const int DropDownSingle = -39; - public const int DropDownMultiple = -42; - public const int Upload = -90; - public const int UploadVideo = -100; - public const int UploadAudio = -101; - public const int UploadArticle = -102; - public const int UploadVectorGraphics = -103; - - public const int DefaultContentListView = -95; - public const int DefaultMediaListView = -96; - public const int DefaultMembersListView = -97; - - public const int ImageCropper = 1043; - public const int Tags = 1041; - - public static class ReservedPreValueKeys - { - public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; - } + /// + /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. + /// + public static class Guids + { + /// + /// Guid for Content Picker as string + /// + public const string ContentPicker = "FD1E0DA5-5606-4862-B679-5D0CF3A52A59"; /// - /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. + /// Guid for Member Picker as string /// - public static class Guids - { - - /// - /// Guid for Content Picker as string - /// - public const string ContentPicker = "FD1E0DA5-5606-4862-B679-5D0CF3A52A59"; - - /// - /// Guid for Content Picker - /// - public static readonly Guid ContentPickerGuid = new Guid(ContentPicker); - - - /// - /// Guid for Member Picker as string - /// - public const string MemberPicker = "1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"; - - /// - /// Guid for Member Picker - /// - public static readonly Guid MemberPickerGuid = new Guid(MemberPicker); - - - /// - /// Guid for Media Picker as string - /// - public const string MediaPicker = "135D60E0-64D9-49ED-AB08-893C9BA44AE5"; - - /// - /// Guid for Media Picker - /// - public static readonly Guid MediaPickerGuid = new Guid(MediaPicker); - - - /// - /// Guid for Multiple Media Picker as string - /// - public const string MultipleMediaPicker = "9DBBCBBB-2327-434A-B355-AF1B84E5010A"; - - /// - /// Guid for Multiple Media Picker - /// - public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); - - - /// - /// Guid for Media Picker v3 as string - /// - public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; - - /// - /// Guid for Media Picker v3 - /// - public static readonly Guid MediaPicker3Guid = new Guid(MediaPicker3); - - /// - /// Guid for Media Picker v3 multiple as string - /// - public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; - - /// - /// Guid for Media Picker v3 multiple - /// - public static readonly Guid MediaPicker3MultipleGuid = new Guid(MediaPicker3Multiple); - - - /// - /// Guid for Media Picker v3 single-image as string - /// - public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; - - /// - /// Guid for Media Picker v3 single-image - /// - public static readonly Guid MediaPicker3SingleImageGuid = new Guid(MediaPicker3SingleImage); - - - /// - /// Guid for Media Picker v3 multi-image as string - /// - public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; - - /// - /// Guid for Media Picker v3 multi-image - /// - public static readonly Guid MediaPicker3MultipleImagesGuid = new Guid(MediaPicker3MultipleImages); - - - /// - /// Guid for Related Links as string - /// - public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; - - /// - /// Guid for Related Links - /// - public static readonly Guid RelatedLinksGuid = new Guid(RelatedLinks); - - - /// - /// Guid for Member as string - /// - public const string Member = "d59be02f-1df9-4228-aa1e-01917d806cda"; - - /// - /// Guid for Member - /// - public static readonly Guid MemberGuid = new Guid(Member); - - - /// - /// Guid for Image Cropper as string - /// - public const string ImageCropper = "1df9f033-e6d4-451f-b8d2-e0cbc50a836f"; - - /// - /// Guid for Image Cropper - /// - public static readonly Guid ImageCropperGuid = new Guid(ImageCropper); - - - /// - /// Guid for Tags as string - /// - public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; - - /// - /// Guid for Tags - /// - public static readonly Guid TagsGuid = new Guid(Tags); - - - /// - /// Guid for List View - Content as string - /// - public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; - - /// - /// Guid for List View - Content - /// - public static readonly Guid ListViewContentGuid = new Guid(ListViewContent); - - - /// - /// Guid for List View - Media as string - /// - public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; - - /// - /// Guid for List View - Media - /// - public static readonly Guid ListViewMediaGuid = new Guid(ListViewMedia); - - - /// - /// Guid for List View - Members as string - /// - public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D"; - - /// - /// Guid for List View - Members - /// - public static readonly Guid ListViewMembersGuid = new Guid(ListViewMembers); - - /// - /// Guid for Date Picker with time as string - /// - public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a"; - - /// - /// Guid for Date Picker with time - /// - public static readonly Guid DatePickerWithTimeGuid = new Guid(DatePickerWithTime); - - - /// - /// Guid for Approved Color as string - /// - public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; - - /// - /// Guid for Approved Color - /// - public static readonly Guid ApprovedColorGuid = new Guid(ApprovedColor); - - - /// - /// Guid for Dropdown multiple as string - /// - public const string DropdownMultiple = "f38f0ac7-1d27-439c-9f3f-089cd8825a53"; - - /// - /// Guid for Dropdown multiple - /// - public static readonly Guid DropdownMultipleGuid = new Guid(DropdownMultiple); - - - /// - /// Guid for Radiobox as string - /// - public const string Radiobox = "bb5f57c9-ce2b-4bb9-b697-4caca783a805"; - - /// - /// Guid for Radiobox - /// - public static readonly Guid RadioboxGuid = new Guid(Radiobox); - - - /// - /// Guid for Date Picker as string - /// - public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; - - /// - /// Guid for Date Picker - /// - public static readonly Guid DatePickerGuid = new Guid(DatePicker); - - - /// - /// Guid for Dropdown as string - /// - public const string Dropdown = "0b6a45e7-44ba-430d-9da5-4e46060b9e03"; - - /// - /// Guid for Dropdown - /// - public static readonly Guid DropdownGuid = new Guid(Dropdown); - - - /// - /// Guid for Checkbox list as string - /// - public const string CheckboxList = "fbaf13a8-4036-41f2-93a3-974f678c312a"; - - /// - /// Guid for Checkbox list - /// - public static readonly Guid CheckboxListGuid = new Guid(CheckboxList); - - - /// - /// Guid for Checkbox as string - /// - public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; - - /// - /// Guid for Checkbox - /// - public static readonly Guid CheckboxGuid = new Guid(Checkbox); - - - /// - /// Guid for Numeric as string - /// - public const string Numeric = "2e6d3631-066e-44b8-aec4-96f09099b2b5"; - - /// - /// Guid for Dropdown - /// - public static readonly Guid NumericGuid = new Guid(Numeric); - - - /// - /// Guid for Richtext editor as string - /// - public const string RichtextEditor = "ca90c950-0aff-4e72-b976-a30b1ac57dad"; - - /// - /// Guid for Richtext editor - /// - public static readonly Guid RichtextEditorGuid = new Guid(RichtextEditor); - - - /// - /// Guid for Textstring as string - /// - public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; - - /// - /// Guid for Textstring - /// - public static readonly Guid TextstringGuid = new Guid(Textstring); - - - /// - /// Guid for Textarea as string - /// - public const string Textarea = "c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"; - - /// - /// Guid for Dropdown - /// - public static readonly Guid TextareaGuid = new Guid(Textarea); - - - /// - /// Guid for Upload as string - /// - public const string Upload = "84c6b441-31df-4ffe-b67e-67d5bc3ae65a"; - - /// - /// Guid for Upload - /// - public static readonly Guid UploadGuid = new Guid(Upload); - - /// - /// Guid for UploadVideo as string - /// - public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; - - /// - /// Guid for UploadVideo - /// - public static readonly Guid UploadVideoGuid = new Guid(UploadVideo); - - /// - /// Guid for UploadAudio as string - /// - public const string UploadAudio = "8f430dd6-4e96-447e-9dc0-cb552c8cd1f3"; - - /// - /// Guid for UploadAudio - /// - public static readonly Guid UploadAudioGuid = new Guid(UploadAudio); - - /// - /// Guid for UploadArticle as string - /// - public const string UploadArticle = "bc1e266c-dac4-4164-bf08-8a1ec6a7143d"; - - /// - /// Guid for UploadArticle - /// - public static readonly Guid UploadArticleGuid = new Guid(UploadArticle); - - /// - /// Guid for UploadVectorGraphics as string - /// - public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; - - /// - /// Guid for UploadVectorGraphics - /// - public static readonly Guid UploadVectorGraphicsGuid = new Guid(UploadVectorGraphics); - - - /// - /// Guid for Label as string - /// - public const string LabelString = "f0bc4bfb-b499-40d6-ba86-058885a5178c"; - - /// - /// Guid for Label string - /// - public static readonly Guid LabelStringGuid = new Guid(LabelString); - - /// - /// Guid for Label as int - /// - public const string LabelInt = "8e7f995c-bd81-4627-9932-c40e568ec788"; - - /// - /// Guid for Label int - /// - public static readonly Guid LabelIntGuid = new Guid(LabelInt); - - /// - /// Guid for Label as big int - /// - public const string LabelBigInt = "930861bf-e262-4ead-a704-f99453565708"; - - /// - /// Guid for Label big int - /// - public static readonly Guid LabelBigIntGuid = new Guid(LabelBigInt); - - /// - /// Guid for Label as date time - /// - public const string LabelDateTime = "0e9794eb-f9b5-4f20-a788-93acd233a7e4"; - - /// - /// Guid for Label date time - /// - public static readonly Guid LabelDateTimeGuid = new Guid(LabelDateTime); - - /// - /// Guid for Label as time - /// - public const string LabelTime = "a97cec69-9b71-4c30-8b12-ec398860d7e8"; - - /// - /// Guid for Label time - /// - public static readonly Guid LabelTimeGuid = new Guid(LabelTime); - - /// - /// Guid for Label as decimal - /// - public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; - - /// - /// Guid for Label decimal - /// - public static readonly Guid LabelDecimalGuid = new Guid(LabelDecimal); - - - } + public const string MemberPicker = "1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"; + + /// + /// Guid for Media Picker as string + /// + public const string MediaPicker = "135D60E0-64D9-49ED-AB08-893C9BA44AE5"; + + /// + /// Guid for Multiple Media Picker as string + /// + public const string MultipleMediaPicker = "9DBBCBBB-2327-434A-B355-AF1B84E5010A"; + + /// + /// Guid for Media Picker v3 as string + /// + public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; + + /// + /// Guid for Media Picker v3 multiple as string + /// + public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; + + /// + /// Guid for Media Picker v3 single-image as string + /// + public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; + + /// + /// Guid for Media Picker v3 multi-image as string + /// + public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; + + /// + /// Guid for Related Links as string + /// + public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; + + /// + /// Guid for Member as string + /// + public const string Member = "d59be02f-1df9-4228-aa1e-01917d806cda"; + + /// + /// Guid for Image Cropper as string + /// + public const string ImageCropper = "1df9f033-e6d4-451f-b8d2-e0cbc50a836f"; + + /// + /// Guid for Tags as string + /// + public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; + + /// + /// Guid for List View - Content as string + /// + public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; + + /// + /// Guid for List View - Media as string + /// + public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; + + /// + /// Guid for List View - Members as string + /// + public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D"; + + /// + /// Guid for Date Picker with time as string + /// + public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a"; + + /// + /// Guid for Approved Color as string + /// + public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; + + /// + /// Guid for Dropdown multiple as string + /// + public const string DropdownMultiple = "f38f0ac7-1d27-439c-9f3f-089cd8825a53"; + + /// + /// Guid for Radiobox as string + /// + public const string Radiobox = "bb5f57c9-ce2b-4bb9-b697-4caca783a805"; + + /// + /// Guid for Date Picker as string + /// + public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; + + /// + /// Guid for Dropdown as string + /// + public const string Dropdown = "0b6a45e7-44ba-430d-9da5-4e46060b9e03"; + + /// + /// Guid for Checkbox list as string + /// + public const string CheckboxList = "fbaf13a8-4036-41f2-93a3-974f678c312a"; + + /// + /// Guid for Checkbox as string + /// + public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; + + /// + /// Guid for Numeric as string + /// + public const string Numeric = "2e6d3631-066e-44b8-aec4-96f09099b2b5"; + + /// + /// Guid for Richtext editor as string + /// + public const string RichtextEditor = "ca90c950-0aff-4e72-b976-a30b1ac57dad"; + + /// + /// Guid for Textstring as string + /// + public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; + + /// + /// Guid for Textarea as string + /// + public const string Textarea = "c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"; + + /// + /// Guid for Upload as string + /// + public const string Upload = "84c6b441-31df-4ffe-b67e-67d5bc3ae65a"; + + /// + /// Guid for UploadVideo as string + /// + public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; + + /// + /// Guid for UploadAudio as string + /// + public const string UploadAudio = "8f430dd6-4e96-447e-9dc0-cb552c8cd1f3"; + + /// + /// Guid for UploadArticle as string + /// + public const string UploadArticle = "bc1e266c-dac4-4164-bf08-8a1ec6a7143d"; + + /// + /// Guid for UploadVectorGraphics as string + /// + public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; + + /// + /// Guid for Label as string + /// + public const string LabelString = "f0bc4bfb-b499-40d6-ba86-058885a5178c"; + + /// + /// Guid for Label as int + /// + public const string LabelInt = "8e7f995c-bd81-4627-9932-c40e568ec788"; + + /// + /// Guid for Label as big int + /// + public const string LabelBigInt = "930861bf-e262-4ead-a704-f99453565708"; + + /// + /// Guid for Label as date time + /// + public const string LabelDateTime = "0e9794eb-f9b5-4f20-a788-93acd233a7e4"; + + /// + /// Guid for Label as time + /// + public const string LabelTime = "a97cec69-9b71-4c30-8b12-ec398860d7e8"; + + /// + /// Guid for Label as decimal + /// + public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; + + /// + /// Guid for Content Picker + /// + public static readonly Guid ContentPickerGuid = new(ContentPicker); + + /// + /// Guid for Member Picker + /// + public static readonly Guid MemberPickerGuid = new(MemberPicker); + + /// + /// Guid for Media Picker + /// + public static readonly Guid MediaPickerGuid = new(MediaPicker); + + /// + /// Guid for Multiple Media Picker + /// + public static readonly Guid MultipleMediaPickerGuid = new(MultipleMediaPicker); + + /// + /// Guid for Media Picker v3 + /// + public static readonly Guid MediaPicker3Guid = new(MediaPicker3); + + /// + /// Guid for Media Picker v3 multiple + /// + public static readonly Guid MediaPicker3MultipleGuid = new(MediaPicker3Multiple); + + /// + /// Guid for Media Picker v3 single-image + /// + public static readonly Guid MediaPicker3SingleImageGuid = new(MediaPicker3SingleImage); + + /// + /// Guid for Media Picker v3 multi-image + /// + public static readonly Guid MediaPicker3MultipleImagesGuid = new(MediaPicker3MultipleImages); + + /// + /// Guid for Related Links + /// + public static readonly Guid RelatedLinksGuid = new(RelatedLinks); + + /// + /// Guid for Member + /// + public static readonly Guid MemberGuid = new(Member); + + /// + /// Guid for Image Cropper + /// + public static readonly Guid ImageCropperGuid = new(ImageCropper); + + /// + /// Guid for Tags + /// + public static readonly Guid TagsGuid = new(Tags); + + /// + /// Guid for List View - Content + /// + public static readonly Guid ListViewContentGuid = new(ListViewContent); + + /// + /// Guid for List View - Media + /// + public static readonly Guid ListViewMediaGuid = new(ListViewMedia); + + /// + /// Guid for List View - Members + /// + public static readonly Guid ListViewMembersGuid = new(ListViewMembers); + + /// + /// Guid for Date Picker with time + /// + public static readonly Guid DatePickerWithTimeGuid = new(DatePickerWithTime); + + /// + /// Guid for Approved Color + /// + public static readonly Guid ApprovedColorGuid = new(ApprovedColor); + + /// + /// Guid for Dropdown multiple + /// + public static readonly Guid DropdownMultipleGuid = new(DropdownMultiple); + + /// + /// Guid for Radiobox + /// + public static readonly Guid RadioboxGuid = new(Radiobox); + + /// + /// Guid for Date Picker + /// + public static readonly Guid DatePickerGuid = new(DatePicker); + + /// + /// Guid for Dropdown + /// + public static readonly Guid DropdownGuid = new(Dropdown); + + /// + /// Guid for Checkbox list + /// + public static readonly Guid CheckboxListGuid = new(CheckboxList); + + /// + /// Guid for Checkbox + /// + public static readonly Guid CheckboxGuid = new(Checkbox); + + /// + /// Guid for Dropdown + /// + public static readonly Guid NumericGuid = new(Numeric); + + /// + /// Guid for Richtext editor + /// + public static readonly Guid RichtextEditorGuid = new(RichtextEditor); + + /// + /// Guid for Textstring + /// + public static readonly Guid TextstringGuid = new(Textstring); + + /// + /// Guid for Dropdown + /// + public static readonly Guid TextareaGuid = new(Textarea); + + /// + /// Guid for Upload + /// + public static readonly Guid UploadGuid = new(Upload); + + /// + /// Guid for UploadVideo + /// + public static readonly Guid UploadVideoGuid = new(UploadVideo); + + /// + /// Guid for UploadAudio + /// + public static readonly Guid UploadAudioGuid = new(UploadAudio); + + /// + /// Guid for UploadArticle + /// + public static readonly Guid UploadArticleGuid = new(UploadArticle); + + /// + /// Guid for UploadVectorGraphics + /// + public static readonly Guid UploadVectorGraphicsGuid = new(UploadVectorGraphics); + + /// + /// Guid for Label string + /// + public static readonly Guid LabelStringGuid = new(LabelString); + + /// + /// Guid for Label int + /// + public static readonly Guid LabelIntGuid = new(LabelInt); + + /// + /// Guid for Label big int + /// + public static readonly Guid LabelBigIntGuid = new(LabelBigInt); + + /// + /// Guid for Label date time + /// + public static readonly Guid LabelDateTimeGuid = new(LabelDateTime); + + /// + /// Guid for Label time + /// + public static readonly Guid LabelTimeGuid = new(LabelTime); + + /// + /// Guid for Label decimal + /// + public static readonly Guid LabelDecimalGuid = new(LabelDecimal); } } } diff --git a/src/Umbraco.Core/Constants-DeploySelector.cs b/src/Umbraco.Core/Constants-DeploySelector.cs index 30daacf42b..0f552e8a82 100644 --- a/src/Umbraco.Core/Constants-DeploySelector.cs +++ b/src/Umbraco.Core/Constants-DeploySelector.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Contains the valid selector values. + /// + public static class DeploySelector { - /// - /// Contains the valid selector values. - /// - public static class DeploySelector - { - public const string This = "this"; - public const string ThisAndChildren = "this-and-children"; - public const string ThisAndDescendants = "this-and-descendants"; - public const string ChildrenOfThis = "children"; - public const string DescendantsOfThis = "descendants"; - } + public const string This = "this"; + public const string ThisAndChildren = "this-and-children"; + public const string ThisAndDescendants = "this-and-descendants"; + public const string ChildrenOfThis = "children"; + public const string DescendantsOfThis = "descendants"; } } diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 5a8ea401cb..2980a59457 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -1,56 +1,58 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for ModelsBuilder. /// - public static partial class Constants + public static class HealthChecks { - /// - /// Defines constants for ModelsBuilder. - /// - public static class HealthChecks + public static class DocumentationLinks { + public const string SmtpCheck = "https://umbra.co/healthchecks-smtp"; - public static class DocumentationLinks + public static class LiveEnvironment { - public const string SmtpCheck = "https://umbra.co/healthchecks-smtp"; + public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; + } - public static class LiveEnvironment + public static class Configuration + { + public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; + + public const string TrySkipIisCustomErrorsCheck = + "https://umbra.co/healthchecks-skip-iis-custom-errors"; + + public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; + } + + public static class FolderAndFilePermissionsCheck + { + public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; + public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; + public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; + public const string MediaFolderCreation = "https://umbra.co/healthchecks-media-folder-creation"; + } + + public static class Security + { + public const string UmbracoApplicationUrlCheck = + "https://umbra.co/healthchecks-umbraco-application-url"; + + public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; + public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; + public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; + public const string XssProtectionCheck = "https://umbra.co/healthchecks-xss-protection"; + public const string ExcessiveHeadersCheck = "https://umbra.co/healthchecks-excessive-headers"; + + public static class HttpsCheck { - - public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; - } - - public static class Configuration - { - public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; - public const string TrySkipIisCustomErrorsCheck = "https://umbra.co/healthchecks-skip-iis-custom-errors"; - public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; - } - - public static class FolderAndFilePermissionsCheck - { - public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; - public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; - public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; - public const string MediaFolderCreation = "https://umbra.co/healthchecks-media-folder-creation"; - } - - public static class Security - { - public const string UmbracoApplicationUrlCheck = "https://umbra.co/healthchecks-umbraco-application-url"; - public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; - public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; - public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; - public const string XssProtectionCheck = "https://umbra.co/healthchecks-xss-protection"; - public const string ExcessiveHeadersCheck = "https://umbra.co/healthchecks-excessive-headers"; - - public static class HttpsCheck - { - public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; - public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; - public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; - } + public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; + public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; + public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; } } } diff --git a/src/Umbraco.Core/Constants-HttpClients.cs b/src/Umbraco.Core/Constants-HttpClients.cs index 474ec49a50..677f442085 100644 --- a/src/Umbraco.Core/Constants-HttpClients.cs +++ b/src/Umbraco.Core/Constants-HttpClients.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for named http clients. /// - public static partial class Constants + public static class HttpClients { /// - /// Defines constants for named http clients. + /// Name for http client which ignores certificate errors. /// - public static class HttpClients - { - /// - /// Name for http client which ignores certificate errors. - /// - public const string IgnoreCertificateErrors = "Umbraco:HttpClients:IgnoreCertificateErrors"; - } + public const string IgnoreCertificateErrors = "Umbraco:HttpClients:IgnoreCertificateErrors"; } } diff --git a/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs index 7be1fbd140..a89bfc2553 100644 --- a/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs +++ b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class HttpContext { - public static class HttpContext + /// + /// Defines keys for items stored in HttpContext.Items + /// + public static class Items { /// - /// Defines keys for items stored in HttpContext.Items + /// Key for current requests body deserialized as JObject. /// - public static class Items - { - /// - /// Key for current requests body deserialized as JObject. - /// - public const string RequestBodyAsJObject = "RequestBodyAsJObject"; - } + public const string RequestBodyAsJObject = "RequestBodyAsJObject"; } } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 39980f116a..40ab52aaa5 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -1,143 +1,142 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Icons { - public static class Icons - { - /// - /// System default icon - /// - public const string DefaultIcon = Content; + /// + /// System default icon + /// + public const string DefaultIcon = Content; - /// - /// System blueprint icon - /// - public const string Blueprint = "icon-blueprint"; + /// + /// System blueprint icon + /// + public const string Blueprint = "icon-blueprint"; - /// - /// System content icon - /// - public const string Content = "icon-document"; + /// + /// System content icon + /// + public const string Content = "icon-document"; - /// - /// System content type icon - /// - public const string ContentType = "icon-item-arrangement"; + /// + /// System content type icon + /// + public const string ContentType = "icon-item-arrangement"; - /// - /// System data type icon - /// - public const string DataType = "icon-autofill"; + /// + /// System data type icon + /// + public const string DataType = "icon-autofill"; - /// - /// System dictionary icon - /// - public const string Dictionary = "icon-book-alt"; + /// + /// System dictionary icon + /// + public const string Dictionary = "icon-book-alt"; - /// - /// System generic folder icon - /// - public const string Folder = "icon-folder"; + /// + /// System generic folder icon + /// + public const string Folder = "icon-folder"; - /// - /// System language icon - /// - public const string Language = "icon-globe"; + /// + /// System language icon + /// + public const string Language = "icon-globe"; - /// - /// System logviewer icon - /// - public const string LogViewer = "icon-box-alt"; + /// + /// System logviewer icon + /// + public const string LogViewer = "icon-box-alt"; - /// - /// System list view icon - /// - public const string ListView = "icon-thumbnail-list"; + /// + /// System list view icon + /// + public const string ListView = "icon-thumbnail-list"; - /// - /// System macro icon - /// - public const string Macro = "icon-settings-alt"; + /// + /// System macro icon + /// + public const string Macro = "icon-settings-alt"; - /// - /// System media file icon - /// - public const string MediaFile = "icon-document"; + /// + /// System media file icon + /// + public const string MediaFile = "icon-document"; - /// - /// System media video icon - /// - public const string MediaVideo = "icon-video"; + /// + /// System media video icon + /// + public const string MediaVideo = "icon-video"; - /// - /// System media audio icon - /// - public const string MediaAudio = "icon-sound-waves"; + /// + /// System media audio icon + /// + public const string MediaAudio = "icon-sound-waves"; - /// - /// System media article icon - /// - public const string MediaArticle = "icon-article"; + /// + /// System media article icon + /// + public const string MediaArticle = "icon-article"; - /// - /// System media vector icon - /// - public const string MediaVectorGraphics = "icon-picture"; + /// + /// System media vector icon + /// + public const string MediaVectorGraphics = "icon-picture"; - /// - /// System media folder icon - /// - public const string MediaFolder = "icon-folder"; + /// + /// System media folder icon + /// + public const string MediaFolder = "icon-folder"; - /// - /// System media image icon - /// - public const string MediaImage = "icon-picture"; + /// + /// System media image icon + /// + public const string MediaImage = "icon-picture"; - /// - /// System media type icon - /// - public const string MediaType = "icon-thumbnails"; + /// + /// System media type icon + /// + public const string MediaType = "icon-thumbnails"; - /// - /// System member icon - /// - public const string Member = "icon-user"; + /// + /// System member icon + /// + public const string Member = "icon-user"; - /// - /// System member group icon - /// - public const string MemberGroup = "icon-users-alt"; + /// + /// System member group icon + /// + public const string MemberGroup = "icon-users-alt"; - /// - /// System member type icon - /// - public const string MemberType = "icon-users"; + /// + /// System member type icon + /// + public const string MemberType = "icon-users"; - /// - /// System packages icon - /// - public const string Packages = "icon-box"; + /// + /// System packages icon + /// + public const string Packages = "icon-box"; - /// - /// System property editor icon - /// - public const string PropertyEditor = "icon-autofill"; + /// + /// System property editor icon + /// + public const string PropertyEditor = "icon-autofill"; - /// - /// System member icon - /// - public const string Template = "icon-layout"; + /// + /// System member icon + /// + public const string Template = "icon-layout"; - /// - /// System user icon - /// - public const string User = "icon-user"; + /// + /// System user icon + /// + public const string User = "icon-user"; - /// - /// System user group icon - /// - public const string UserGroup = "icon-users"; - } + /// + /// System user group icon + /// + public const string UserGroup = "icon-users"; } } diff --git a/src/Umbraco.Core/Constants-Indexes.cs b/src/Umbraco.Core/Constants-Indexes.cs index fcf2e7ed14..9c5d9ca48e 100644 --- a/src/Umbraco.Core/Constants-Indexes.cs +++ b/src/Umbraco.Core/Constants-Indexes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class UmbracoIndexes { - public static class UmbracoIndexes - { - public const string InternalIndexName = "InternalIndex"; - public const string ExternalIndexName = "ExternalIndex"; - public const string MembersIndexName = "MembersIndex"; - } + public const string InternalIndexName = "InternalIndex"; + public const string ExternalIndexName = "ExternalIndex"; + public const string MembersIndexName = "MembersIndex"; } } diff --git a/src/Umbraco.Core/Constants-ModelStateErrorKeys.cs b/src/Umbraco.Core/Constants-ModelStateErrorKeys.cs new file mode 100644 index 0000000000..c5bee395aa --- /dev/null +++ b/src/Umbraco.Core/Constants-ModelStateErrorKeys.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + public class ModelStateErrorKeys + { + public const string PermissionError = "PermissionError"; + } +} diff --git a/src/Umbraco.Core/Constants-ModelsBuilder.cs b/src/Umbraco.Core/Constants-ModelsBuilder.cs index 289c0355a8..63b852a600 100644 --- a/src/Umbraco.Core/Constants-ModelsBuilder.cs +++ b/src/Umbraco.Core/Constants-ModelsBuilder.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for ModelsBuilder. /// - public static partial class Constants + public static class ModelsBuilder { - /// - /// Defines constants for ModelsBuilder. - /// - public static class ModelsBuilder - { - public const string DefaultModelsNamespace = "Umbraco.Cms.Web.Common.PublishedModels"; - } + public const string DefaultModelsNamespace = "Umbraco.Cms.Web.Common.PublishedModels"; } } diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 0a9847b848..049a536690 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -1,125 +1,123 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + /// + /// Defines the Umbraco object type unique identifiers. + /// + public static class ObjectTypes { + public static readonly Guid SystemRoot = new(Strings.SystemRoot); + + public static readonly Guid ContentRecycleBin = new(Strings.ContentRecycleBin); + + public static readonly Guid MediaRecycleBin = new(Strings.MediaRecycleBin); + + public static readonly Guid DataTypeContainer = new(Strings.DataTypeContainer); + + public static readonly Guid DocumentTypeContainer = new(Strings.DocumentTypeContainer); + + public static readonly Guid MediaTypeContainer = new(Strings.MediaTypeContainer); + + public static readonly Guid DataType = new(Strings.DataType); + + public static readonly Guid Document = new(Strings.Document); + + public static readonly Guid DocumentBlueprint = new(Strings.DocumentBlueprint); + + public static readonly Guid DocumentType = new(Strings.DocumentType); + + public static readonly Guid Media = new(Strings.Media); + + public static readonly Guid MediaType = new(Strings.MediaType); + + public static readonly Guid Member = new(Strings.Member); + + public static readonly Guid MemberGroup = new(Strings.MemberGroup); + + public static readonly Guid MemberType = new(Strings.MemberType); + + public static readonly Guid TemplateType = new(Strings.Template); + + public static readonly Guid LockObject = new(Strings.LockObject); + + public static readonly Guid RelationType = new(Strings.RelationType); + + public static readonly Guid FormsForm = new(Strings.FormsForm); + + public static readonly Guid FormsPreValue = new(Strings.FormsPreValue); + + public static readonly Guid FormsDataSource = new(Strings.FormsDataSource); + + public static readonly Guid Language = new(Strings.Language); + + public static readonly Guid IdReservation = new(Strings.IdReservation); + + public static readonly Guid Template = new(Strings.Template); + + public static readonly Guid ContentItem = new(Strings.ContentItem); + /// - /// Defines the Umbraco object type unique identifiers. + /// Defines the Umbraco object type unique identifiers as string. /// - public static class ObjectTypes + /// + /// Should be used only when it's not possible to use the corresponding + /// readonly Guid value, e.g. in attributes (where only consts can be used). + /// + public static class Strings { - /// - /// Defines the Umbraco object type unique identifiers as string. - /// - /// Should be used only when it's not possible to use the corresponding - /// readonly Guid value, e.g. in attributes (where only consts can be used). - public static class Strings - { - // ReSharper disable MemberHidesStaticFromOuterClass + // ReSharper disable MemberHidesStaticFromOuterClass + public const string DataTypeContainer = "521231E3-8B37-469C-9F9D-51AFC91FEB7B"; - public const string DataTypeContainer = "521231E3-8B37-469C-9F9D-51AFC91FEB7B"; + public const string DocumentTypeContainer = "2F7A2769-6B0B-4468-90DD-AF42D64F7F16"; - public const string DocumentTypeContainer = "2F7A2769-6B0B-4468-90DD-AF42D64F7F16"; + public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; - public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; + public const string ContentItem = "10E2B09F-C28B-476D-B77A-AA686435E44A"; - public const string ContentItem = "10E2B09F-C28B-476D-B77A-AA686435E44A"; + public const string ContentItemType = "7A333C54-6F43-40A4-86A2-18688DC7E532"; - public const string ContentItemType = "7A333C54-6F43-40A4-86A2-18688DC7E532"; + public const string ContentRecycleBin = "01BB7FF2-24DC-4C0C-95A2-C24EF72BBAC8"; - public const string ContentRecycleBin = "01BB7FF2-24DC-4C0C-95A2-C24EF72BBAC8"; + public const string DataType = "30A2A501-1978-4DDB-A57B-F7EFED43BA3C"; - public const string DataType = "30A2A501-1978-4DDB-A57B-F7EFED43BA3C"; + public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; - public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; + public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; - public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; + public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB"; - public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB"; + public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; - public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; + public const string MediaRecycleBin = "CF3D8E34-1C1C-41e9-AE56-878B57B32113"; - public const string MediaRecycleBin = "CF3D8E34-1C1C-41e9-AE56-878B57B32113"; + public const string MediaType = "4EA4382B-2F5A-4C2B-9587-AE9B3CF3602E"; - public const string MediaType = "4EA4382B-2F5A-4C2B-9587-AE9B3CF3602E"; + public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; - public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; + public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; - public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; + public const string MemberType = "9B5416FB-E72F-45A9-A07B-5A9A2709CE43"; - public const string MemberType = "9B5416FB-E72F-45A9-A07B-5A9A2709CE43"; + public const string SystemRoot = "EA7D8624-4CFE-4578-A871-24AA946BF34D"; - public const string SystemRoot = "EA7D8624-4CFE-4578-A871-24AA946BF34D"; + public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; - public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; + public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; - public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; + public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; - public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; + public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; - public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; + public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; - public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; + public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; - public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; + public const string Language = "6B05D05B-EC78-49BE-A4E4-79E274F07A77"; - public const string Language = "6B05D05B-EC78-49BE-A4E4-79E274F07A77"; + public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; - public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; - - // ReSharper restore MemberHidesStaticFromOuterClass - } - - public static readonly Guid SystemRoot = new Guid(Strings.SystemRoot); - - public static readonly Guid ContentRecycleBin = new Guid(Strings.ContentRecycleBin); - - public static readonly Guid MediaRecycleBin = new Guid(Strings.MediaRecycleBin); - - public static readonly Guid DataTypeContainer = new Guid(Strings.DataTypeContainer); - - public static readonly Guid DocumentTypeContainer = new Guid(Strings.DocumentTypeContainer); - - public static readonly Guid MediaTypeContainer = new Guid(Strings.MediaTypeContainer); - - public static readonly Guid DataType = new Guid(Strings.DataType); - - public static readonly Guid Document = new Guid(Strings.Document); - - public static readonly Guid DocumentBlueprint = new Guid(Strings.DocumentBlueprint); - - public static readonly Guid DocumentType = new Guid(Strings.DocumentType); - - public static readonly Guid Media = new Guid(Strings.Media); - - public static readonly Guid MediaType = new Guid(Strings.MediaType); - - public static readonly Guid Member = new Guid(Strings.Member); - - public static readonly Guid MemberGroup = new Guid(Strings.MemberGroup); - - public static readonly Guid MemberType = new Guid(Strings.MemberType); - - public static readonly Guid TemplateType = new Guid(Strings.Template); - - public static readonly Guid LockObject = new Guid(Strings.LockObject); - - public static readonly Guid RelationType = new Guid(Strings.RelationType); - - public static readonly Guid FormsForm = new Guid(Strings.FormsForm); - - public static readonly Guid FormsPreValue = new Guid(Strings.FormsPreValue); - - public static readonly Guid FormsDataSource = new Guid(Strings.FormsDataSource); - - public static readonly Guid Language = new Guid(Strings.Language); - - public static readonly Guid IdReservation = new Guid(Strings.IdReservation); - - public static readonly Guid Template = new Guid(Strings.Template); - - public static readonly Guid ContentItem = new Guid(Strings.ContentItem); + // ReSharper restore MemberHidesStaticFromOuterClass } } } diff --git a/src/Umbraco.Core/Constants-PackageRepository.cs b/src/Umbraco.Core/Constants-PackageRepository.cs index 96ef39b7c1..96746adb49 100644 --- a/src/Umbraco.Core/Constants-PackageRepository.cs +++ b/src/Umbraco.Core/Constants-PackageRepository.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the constants used for the Umbraco package repository + /// + public static class PackageRepository { - /// - /// Defines the constants used for the Umbraco package repository - /// - public static class PackageRepository - { - public const string RestApiBaseUrl = "https://our.umbraco.com/webapi/packages/v1"; - public const string DefaultRepositoryName = "Umbraco package Repository"; - public const string DefaultRepositoryId = "65194810-1f85-11dd-bd0b-0800200c9a66"; - } + public const string RestApiBaseUrl = "https://our.umbraco.com/webapi/packages/v1"; + public const string DefaultRepositoryName = "Umbraco package Repository"; + public const string DefaultRepositoryId = "65194810-1f85-11dd-bd0b-0800200c9a66"; } } diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index b34351d902..2bb53b3299 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -1,241 +1,239 @@ using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines property editors constants. + /// + public static class PropertyEditors { /// - /// Defines property editors constants. + /// Used to prefix generic properties that are internal content properties /// - public static class PropertyEditors + public const string InternalGenericPropertiesPrefix = "_umb_"; + + public static class Legacy { - /// - /// Used to prefix generic properties that are internal content properties - /// - public const string InternalGenericPropertiesPrefix = "_umb_"; - - public static class Legacy - { - public static class Aliases - { - public const string Textbox = "Umbraco.Textbox"; - public const string Date = "Umbraco.Date"; - public const string ContentPicker2 = "Umbraco.ContentPicker2"; - public const string MediaPicker2 = "Umbraco.MediaPicker2"; - public const string MemberPicker2 = "Umbraco.MemberPicker2"; - public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; - public const string TextboxMultiple = "Umbraco.TextboxMultiple"; - public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; - public const string RelatedLinks = "Umbraco.RelatedLinks"; - - } - } - - /// - /// Defines Umbraco built-in property editor aliases. - /// public static class Aliases { - /// - /// Block List. - /// - public const string BlockList = "Umbraco.BlockList"; - - /// - /// CheckBox List. - /// - public const string CheckBoxList = "Umbraco.CheckBoxList"; - - /// - /// Color Picker. - /// - public const string ColorPicker = "Umbraco.ColorPicker"; - - /// - /// Eye Dropper Color Picker. - /// - public const string ColorPickerEyeDropper = "Umbraco.ColorPicker.EyeDropper"; - - /// - /// Content Picker. - /// - public const string ContentPicker = "Umbraco.ContentPicker"; - - /// - /// DateTime. - /// - public const string DateTime = "Umbraco.DateTime"; - - /// - /// DropDown List. - /// - public const string DropDownListFlexible = "Umbraco.DropDown.Flexible"; - - /// - /// Grid. - /// - public const string Grid = "Umbraco.Grid"; - - /// - /// Image Cropper. - /// - public const string ImageCropper = "Umbraco.ImageCropper"; - - /// - /// Integer. - /// - public const string Integer = "Umbraco.Integer"; - - /// - /// Decimal. - /// - public const string Decimal = "Umbraco.Decimal"; - - /// - /// ListView. - /// - public const string ListView = "Umbraco.ListView"; - - /// - /// Media Picker. - /// - public const string MediaPicker = "Umbraco.MediaPicker"; - - /// - /// Media Picker v.3. - /// - public const string MediaPicker3 = "Umbraco.MediaPicker3"; - - /// - /// Multiple Media Picker. - /// - public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; - - /// - /// Member Picker. - /// - public const string MemberPicker = "Umbraco.MemberPicker"; - - /// - /// Member Group Picker. - /// - public const string MemberGroupPicker = "Umbraco.MemberGroupPicker"; - - /// - /// MultiNode Tree Picker. - /// - public const string MultiNodeTreePicker = "Umbraco.MultiNodeTreePicker"; - - /// - /// Multiple TextString. - /// - public const string MultipleTextstring = "Umbraco.MultipleTextstring"; - - /// - /// Label. - /// - public const string Label = "Umbraco.Label"; - - /// - /// Picker Relations. - /// - public const string PickerRelations = "Umbraco.PickerRelations"; - - /// - /// RadioButton list. - /// - public const string RadioButtonList = "Umbraco.RadioButtonList"; - - /// - /// Slider. - /// - public const string Slider = "Umbraco.Slider"; - - /// - /// Tags. - /// - public const string Tags = "Umbraco.Tags"; - - /// - /// Textbox. - /// - public const string TextBox = "Umbraco.TextBox"; - - /// - /// Textbox Multiple. - /// - public const string TextArea = "Umbraco.TextArea"; - - /// - /// TinyMCE - /// - public const string TinyMce = "Umbraco.TinyMCE"; - - /// - /// Boolean. - /// - public const string Boolean = "Umbraco.TrueFalse"; - - /// - /// Markdown Editor. - /// - public const string MarkdownEditor = "Umbraco.MarkdownEditor"; - - /// - /// User Picker. - /// - public const string UserPicker = "Umbraco.UserPicker"; - - /// - /// Upload Field. - /// - public const string UploadField = "Umbraco.UploadField"; - - /// - /// Email Address. - /// - public const string EmailAddress = "Umbraco.EmailAddress"; - - /// - /// Nested Content. - /// - public const string NestedContent = "Umbraco.NestedContent"; - - /// - /// Alias for the multi URL picker editor. - /// - public const string MultiUrlPicker = "Umbraco.MultiUrlPicker"; + public const string Textbox = "Umbraco.Textbox"; + public const string Date = "Umbraco.Date"; + public const string ContentPicker2 = "Umbraco.ContentPicker2"; + public const string MediaPicker2 = "Umbraco.MediaPicker2"; + public const string MemberPicker2 = "Umbraco.MemberPicker2"; + public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; + public const string TextboxMultiple = "Umbraco.TextboxMultiple"; + public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; + public const string RelatedLinks = "Umbraco.RelatedLinks"; } + } + + /// + /// Defines Umbraco built-in property editor aliases. + /// + public static class Aliases + { + /// + /// Block List. + /// + public const string BlockList = "Umbraco.BlockList"; /// - /// Defines Umbraco build-in datatype configuration keys. + /// CheckBox List. /// - public static class ConfigurationKeys - { - /// - /// The value type of property data (i.e., string, integer, etc) - /// - /// Must be a valid value. - public const string DataValueType = "umbracoDataValueType"; - } + public const string CheckBoxList = "Umbraco.CheckBoxList"; /// - /// Defines Umbraco's built-in property editor groups. + /// Color Picker. /// - public static class Groups - { - public const string Common = "Common"; + public const string ColorPicker = "Umbraco.ColorPicker"; - public const string Lists = "Lists"; + /// + /// Eye Dropper Color Picker. + /// + public const string ColorPickerEyeDropper = "Umbraco.ColorPicker.EyeDropper"; - public const string Media = "Media"; + /// + /// Content Picker. + /// + public const string ContentPicker = "Umbraco.ContentPicker"; - public const string People = "People"; + /// + /// DateTime. + /// + public const string DateTime = "Umbraco.DateTime"; - public const string Pickers = "Pickers"; + /// + /// DropDown List. + /// + public const string DropDownListFlexible = "Umbraco.DropDown.Flexible"; - public const string RichContent = "Rich Content"; - } + /// + /// Grid. + /// + public const string Grid = "Umbraco.Grid"; + + /// + /// Image Cropper. + /// + public const string ImageCropper = "Umbraco.ImageCropper"; + + /// + /// Integer. + /// + public const string Integer = "Umbraco.Integer"; + + /// + /// Decimal. + /// + public const string Decimal = "Umbraco.Decimal"; + + /// + /// ListView. + /// + public const string ListView = "Umbraco.ListView"; + + /// + /// Media Picker. + /// + public const string MediaPicker = "Umbraco.MediaPicker"; + + /// + /// Media Picker v.3. + /// + public const string MediaPicker3 = "Umbraco.MediaPicker3"; + + /// + /// Multiple Media Picker. + /// + public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; + + /// + /// Member Picker. + /// + public const string MemberPicker = "Umbraco.MemberPicker"; + + /// + /// Member Group Picker. + /// + public const string MemberGroupPicker = "Umbraco.MemberGroupPicker"; + + /// + /// MultiNode Tree Picker. + /// + public const string MultiNodeTreePicker = "Umbraco.MultiNodeTreePicker"; + + /// + /// Multiple TextString. + /// + public const string MultipleTextstring = "Umbraco.MultipleTextstring"; + + /// + /// Label. + /// + public const string Label = "Umbraco.Label"; + + /// + /// Picker Relations. + /// + public const string PickerRelations = "Umbraco.PickerRelations"; + + /// + /// RadioButton list. + /// + public const string RadioButtonList = "Umbraco.RadioButtonList"; + + /// + /// Slider. + /// + public const string Slider = "Umbraco.Slider"; + + /// + /// Tags. + /// + public const string Tags = "Umbraco.Tags"; + + /// + /// Textbox. + /// + public const string TextBox = "Umbraco.TextBox"; + + /// + /// Textbox Multiple. + /// + public const string TextArea = "Umbraco.TextArea"; + + /// + /// TinyMCE + /// + public const string TinyMce = "Umbraco.TinyMCE"; + + /// + /// Boolean. + /// + public const string Boolean = "Umbraco.TrueFalse"; + + /// + /// Markdown Editor. + /// + public const string MarkdownEditor = "Umbraco.MarkdownEditor"; + + /// + /// User Picker. + /// + public const string UserPicker = "Umbraco.UserPicker"; + + /// + /// Upload Field. + /// + public const string UploadField = "Umbraco.UploadField"; + + /// + /// Email Address. + /// + public const string EmailAddress = "Umbraco.EmailAddress"; + + /// + /// Nested Content. + /// + public const string NestedContent = "Umbraco.NestedContent"; + + /// + /// Alias for the multi URL picker editor. + /// + public const string MultiUrlPicker = "Umbraco.MultiUrlPicker"; + } + + /// + /// Defines Umbraco build-in datatype configuration keys. + /// + public static class ConfigurationKeys + { + /// + /// The value type of property data (i.e., string, integer, etc) + /// + /// Must be a valid value. + public const string DataValueType = "umbracoDataValueType"; + } + + /// + /// Defines Umbraco's built-in property editor groups. + /// + public static class Groups + { + public const string Common = "Common"; + + public const string Lists = "Lists"; + + public const string Media = "Media"; + + public const string People = "People"; + + public const string Pickers = "Pickers"; + + public const string RichContent = "Rich Content"; } } } diff --git a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs index 46b41ea233..a713b279b1 100644 --- a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs +++ b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs @@ -1,46 +1,45 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for property-type groups conventions that are used within the Umbraco core. + /// + public static class PropertyTypeGroups { /// - /// Defines the identifiers for property-type groups conventions that are used within the Umbraco core. + /// Guid for an Image PropertyTypeGroup object. /// - public static class PropertyTypeGroups - { - /// - /// Guid for an Image PropertyTypeGroup object. - /// - public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; + public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; - /// - /// Guid for a File PropertyTypeGroup object. - /// - public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; + /// + /// Guid for a File PropertyTypeGroup object. + /// + public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; - /// - /// Guid for a Video PropertyTypeGroup object. - /// - public const string Video = "2F0A61B6-CF92-4FF4-B437-751AB35EB254"; + /// + /// Guid for a Video PropertyTypeGroup object. + /// + public const string Video = "2F0A61B6-CF92-4FF4-B437-751AB35EB254"; - /// - /// Guid for an Audio PropertyTypeGroup object. - /// - public const string Audio = "335FB495-0A87-4E82-B902-30EB367B767C"; + /// + /// Guid for an Audio PropertyTypeGroup object. + /// + public const string Audio = "335FB495-0A87-4E82-B902-30EB367B767C"; - /// - /// Guid for an Article PropertyTypeGroup object. - /// - public const string Article = "9AF3BD65-F687-4453-9518-5F180D1898EC"; + /// + /// Guid for an Article PropertyTypeGroup object. + /// + public const string Article = "9AF3BD65-F687-4453-9518-5F180D1898EC"; - /// - /// Guid for a VectorGraphics PropertyTypeGroup object. - /// - public const string VectorGraphics = "F199B4D7-9E84-439F-8531-F87D9AF37711"; + /// + /// Guid for a VectorGraphics PropertyTypeGroup object. + /// + public const string VectorGraphics = "F199B4D7-9E84-439F-8531-F87D9AF37711"; - /// - /// Guid for a Membership PropertyTypeGroup object. - /// - public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; - } + /// + /// Guid for a Membership PropertyTypeGroup object. + /// + public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; } } diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 68601a78b0..26e26804ae 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -1,73 +1,82 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Security { - public static class Security - { - /// - /// Gets the identifier of the 'super' user. - /// - public const int SuperUserId = -1; + /// + /// Gets the identifier of the 'super' user. + /// + public const int SuperUserId = -1; - public const string SuperUserIdAsString = "-1"; + public const string SuperUserIdAsString = "-1"; - /// - /// The id for the 'unknown' user. - /// - /// - /// This is a user row that exists in the DB only for referential integrity but the user is never returned from any of the services - /// - public const int UnknownUserId = 0; + /// + /// The id for the 'unknown' user. + /// + /// + /// This is a user row that exists in the DB only for referential integrity but the user is never returned from any of + /// the services + /// + public const int UnknownUserId = 0; - /// - /// The name of the 'unknown' user. - /// - public const string UnknownUserName = "SYSTEM"; + /// + /// The name of the 'unknown' user. + /// + public const string UnknownUserName = "SYSTEM"; - public const string AdminGroupAlias = "admin"; - public const string EditorGroupAlias = "editor"; - public const string SensitiveDataGroupAlias = "sensitiveData"; - public const string TranslatorGroupAlias = "translator"; - public const string WriterGroupAlias = "writer"; + public const string AdminGroupAlias = "admin"; + public const string EditorGroupAlias = "editor"; + public const string SensitiveDataGroupAlias = "sensitiveData"; + public const string TranslatorGroupAlias = "translator"; + public const string WriterGroupAlias = "writer"; - public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; - public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; - public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; - public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; - public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; - public const string BackOfficeTwoFactorRememberMeAuthenticationType = "UmbracoTwoFactorRememberMeCookie"; + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; + public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; + public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; + public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; + public const string BackOfficeTwoFactorRememberMeAuthenticationType = "UmbracoTwoFactorRememberMeCookie"; - public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; + public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; - public const string DefaultMemberTypeAlias = "Member"; + public const string DefaultMemberTypeAlias = "Member"; + /// + /// The prefix used for external identity providers for their authentication type + /// + /// + /// By default we don't want to interfere with front-end external providers and their default setup, for back office + /// the + /// providers need to be setup differently and each auth type for the back office will be prefixed with this value + /// + public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; - /// - /// The prefix used for external identity providers for their authentication type - /// - /// - /// By default we don't want to interfere with front-end external providers and their default setup, for back office the - /// providers need to be setup differently and each auth type for the back office will be prefixed with this value - /// - public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; - public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers."; + public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers."; - public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; - public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; - public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; - public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; - public const string TicketExpiresClaimType = "http://umbraco.org/2020/06/identity/claims/backoffice/ticketexpires"; + public const string StartContentNodeIdClaimType = + "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; - /// - /// The claim type for the ASP.NET Identity security stamp - /// - public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp"; + public const string StartMediaNodeIdClaimType = + "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; - public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3"; - public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2"; - public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256"; - public const string AspNetUmbraco4PasswordHashAlgorithmName = "HMACSHA1"; - public const string UnknownPasswordConfigJson = "{\"hashAlgorithm\":\"Unknown\"}"; - } + public const string AllowedApplicationsClaimType = + "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; + + public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + + public const string TicketExpiresClaimType = + "http://umbraco.org/2020/06/identity/claims/backoffice/ticketexpires"; + + /// + /// The claim type for the ASP.NET Identity security stamp + /// + public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp"; + + public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3"; + public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2"; + public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256"; + public const string AspNetUmbraco4PasswordHashAlgorithmName = "HMACSHA1"; + public const string UnknownPasswordConfigJson = "{\"hashAlgorithm\":\"Unknown\"}"; } } diff --git a/src/Umbraco.Core/Constants-Sql.cs b/src/Umbraco.Core/Constants-Sql.cs index b57861c92a..f893680465 100644 --- a/src/Umbraco.Core/Constants-Sql.cs +++ b/src/Umbraco.Core/Constants-Sql.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Sql { - public static class Sql - { - /// - /// The maximum amount of parameters that can be used in a query. - /// - /// - /// The actual limit is 2100 - /// (https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server), - /// but we want to ensure there's room for additional parameters if this value is used to create groups/batches. - /// - public const int MaxParameterCount = 2000; - } + /// + /// The maximum amount of parameters that can be used in a query. + /// + /// + /// The actual limit is 2100 + /// (https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server), + /// but we want to ensure there's room for additional parameters if this value is used to create groups/batches. + /// + public const int MaxParameterCount = 2000; } } diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index a2fe501ab3..549dae5bd6 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -1,43 +1,53 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class SqlTemplates { - public static class SqlTemplates + public static class VersionableRepository { - public static class VersionableRepository - { - public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds"; - public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion"; - public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions"; - public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName"; - public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder"; - public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode"; - public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; - } - public static class RelationRepository - { - public const string DeleteByParentAll = "Umbraco.Core.RelationRepository.DeleteByParent"; - public const string DeleteByParentIn = "Umbraco.Core.RelationRepository.DeleteByParentIn"; - } + public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds"; + public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion"; + public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions"; + public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName"; + public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder"; + public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode"; + public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; + } - public static class DataTypeRepository - { - public const string EnsureUniqueNodeName = "Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName"; - } + public static class RelationRepository + { + public const string DeleteByParentAll = "Umbraco.Core.RelationRepository.DeleteByParent"; + public const string DeleteByParentIn = "Umbraco.Core.RelationRepository.DeleteByParentIn"; + } - public static class NuCacheDatabaseDataSource - { - public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; - public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; - public const string SourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin"; - public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; - public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; - public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; - public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; - public const string ObjectTypeNotTrashedFilter = "Umbraco.Web.PublishedCache.NuCache.DataSource.ObjectTypeNotTrashedFilter"; - public const string OrderByLevelIdSortOrder = "Umbraco.Web.PublishedCache.NuCache.DataSource.OrderByLevelIdSortOrder"; + public static class DataTypeRepository + { + public const string EnsureUniqueNodeName = "Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName"; + } - } + public static class NuCacheDatabaseDataSource + { + public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; + public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; + + public const string SourcesSelectUmbracoNodeJoin = + "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin"; + + public const string ContentSourcesSelect = + "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; + + public const string ContentSourcesCount = + "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; + + public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; + public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; + + public const string ObjectTypeNotTrashedFilter = + "Umbraco.Web.PublishedCache.NuCache.DataSource.ObjectTypeNotTrashedFilter"; + + public const string OrderByLevelIdSortOrder = + "Umbraco.Web.PublishedCache.NuCache.DataSource.OrderByLevelIdSortOrder"; } } } diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 0ad9852671..43de01995b 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -1,70 +1,68 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for Umbraco system nodes. + /// + public static class System { /// - /// Defines the identifiers for Umbraco system nodes. + /// The integer identifier for global system root node. /// - public static class System - { - /// - /// The integer identifier for global system root node. - /// - public const int Root = -1; + public const int Root = -1; - /// - /// The string identifier for global system root node. - /// - /// Use this instead of re-creating the string everywhere. - public const string RootString = "-1"; + /// + /// The string identifier for global system root node. + /// + /// Use this instead of re-creating the string everywhere. + public const string RootString = "-1"; - /// - /// The integer identifier for content's recycle bin. - /// - public const int RecycleBinContent = -20; + /// + /// The integer identifier for content's recycle bin. + /// + public const int RecycleBinContent = -20; - /// - /// The string identifier for content's recycle bin. - /// - /// Use this instead of re-creating the string everywhere. - public const string RecycleBinContentString = "-20"; + /// + /// The string identifier for content's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinContentString = "-20"; - /// - /// The string path prefix of the content's recycle bin. - /// - /// - /// Everything that is in the content recycle bin, has a path that starts with the prefix. - /// Use this instead of re-creating the string everywhere. - /// - public const string RecycleBinContentPathPrefix = "-1,-20,"; + /// + /// The string path prefix of the content's recycle bin. + /// + /// + /// Everything that is in the content recycle bin, has a path that starts with the prefix. + /// Use this instead of re-creating the string everywhere. + /// + public const string RecycleBinContentPathPrefix = "-1,-20,"; - /// - /// The integer identifier for media's recycle bin. - /// - public const int RecycleBinMedia = -21; + /// + /// The integer identifier for media's recycle bin. + /// + public const int RecycleBinMedia = -21; - /// - /// The string identifier for media's recycle bin. - /// - /// Use this instead of re-creating the string everywhere. - public const string RecycleBinMediaString = "-21"; + /// + /// The string identifier for media's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinMediaString = "-21"; - /// - /// The string path prefix of the media's recycle bin. - /// - /// - /// Everything that is in the media recycle bin, has a path that starts with the prefix. - /// Use this instead of re-creating the string everywhere. - /// - public const string RecycleBinMediaPathPrefix = "-1,-21,"; + /// + /// The string path prefix of the media's recycle bin. + /// + /// + /// Everything that is in the media recycle bin, has a path that starts with the prefix. + /// Use this instead of re-creating the string everywhere. + /// + public const string RecycleBinMediaPathPrefix = "-1,-21,"; - public const int DefaultLabelDataTypeId = -92; + public const int DefaultLabelDataTypeId = -92; - public const string UmbracoDefaultDatabaseName = "Umbraco"; + public const string UmbracoDefaultDatabaseName = "Umbraco"; - public const string UmbracoConnectionName = "umbracoDbDSN"; - - public const string DefaultUmbracoPath = "~/umbraco"; - } + public const string UmbracoConnectionName = "umbracoDbDSN"; + public const string DefaultUmbracoPath = "~/umbraco"; } } diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index f70dd199fc..85375390ac 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -1,71 +1,68 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + public static class SystemDirectories { - public static class SystemDirectories - { - /// - /// The aspnet bin folder - /// - public const string Bin = "~/bin"; + /// + /// The aspnet bin folder + /// + public const string Bin = "~/bin"; - // TODO: Shouldn't this exist underneath /Umbraco in the content root? - public const string Config = "~/config"; + // TODO: Shouldn't this exist underneath /Umbraco in the content root? + public const string Config = "~/config"; - /// - /// The Umbraco folder that exists at the content root. - /// - /// - /// This is not the same as the Umbraco web folder which is configurable for serving front-end files. - /// - public const string Umbraco = "~/umbraco"; + /// + /// The Umbraco folder that exists at the content root. + /// + /// + /// This is not the same as the Umbraco web folder which is configurable for serving front-end files. + /// + public const string Umbraco = "~/umbraco"; - /// - /// The Umbraco data folder in the content root. - /// - public const string Data = Umbraco + "/Data"; + /// + /// The Umbraco data folder in the content root. + /// + public const string Data = Umbraco + "/Data"; - /// - /// The Umbraco licenses folder in the content root. - /// - public const string Licenses = Umbraco + "/Licenses"; + /// + /// The Umbraco licenses folder in the content root. + /// + public const string Licenses = Umbraco + "/Licenses"; - /// - /// The Umbraco temp data folder in the content root. - /// - public const string TempData = Data + "/TEMP"; + /// + /// The Umbraco temp data folder in the content root. + /// + public const string TempData = Data + "/TEMP"; - public const string TempFileUploads = TempData + "/FileUploads"; + public const string TempFileUploads = TempData + "/FileUploads"; - public const string TempImageUploads = TempFileUploads + "/rte"; + public const string TempImageUploads = TempFileUploads + "/rte"; - public const string Install = "~/install"; + public const string Install = "~/install"; - public const string AppPlugins = "/App_Plugins"; + public const string AppPlugins = "/App_Plugins"; - [Obsolete("Use PluginIcons instead")] - public static string AppPluginIcons => "/Backoffice/Icons"; + public const string PluginIcons = "/backoffice/icons"; - public const string PluginIcons = "/backoffice/icons"; + public const string MvcViews = "~/Views"; - public const string MvcViews = "~/Views"; + public const string PartialViews = MvcViews + "/Partials/"; - public const string PartialViews = MvcViews + "/Partials/"; + public const string MacroPartials = MvcViews + "/MacroPartials/"; - public const string MacroPartials = MvcViews + "/MacroPartials/"; + public const string Packages = Data + "/packages"; - public const string Packages = Data + "/packages"; + public const string CreatedPackages = Data + "/CreatedPackages"; - public const string CreatedPackages = Data + "/CreatedPackages"; + public const string Preview = Data + "/preview"; - public const string Preview = Data + "/preview"; + /// + /// The default folder where Umbraco log files are stored + /// + public const string LogFiles = Umbraco + "/Logs"; - /// - /// The default folder where Umbraco log files are stored - /// - public const string LogFiles = Umbraco + "/Logs"; - } + [Obsolete("Use PluginIcons instead")] + public static string AppPluginIcons => "/Backoffice/Icons"; } } diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index 6fc474d9ae..c40df0f8bd 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -1,32 +1,31 @@ -namespace Umbraco.Cms.Core -{ - public static partial class Constants - { - public static class Telemetry - { +namespace Umbraco.Cms.Core; - public static string RootCount = "RootCount"; - public static string DomainCount = "DomainCount"; - public static string ExamineIndexCount = "ExamineIndexCount"; - public static string LanguageCount = "LanguageCount"; - public static string MacroCount = "MacroCount"; - public static string MediaCount = "MediaCount"; - public static string MemberCount = "MemberCount"; - public static string TemplateCount = "TemplateCount"; - public static string ContentCount = "ContentCount"; - public static string DocumentTypeCount = "DocumentTypeCount"; - public static string Properties = "Properties"; - public static string UserCount = "UserCount"; - public static string UserGroupCount = "UserGroupCount"; - public static string ServerOs = "ServerOs"; - public static string ServerFramework = "ServerFramework"; - public static string OsLanguage = "OsLanguage"; - public static string WebServer = "WebServer"; - public static string ModelsBuilderMode = "ModelBuilderMode"; - public static string CustomUmbracoPath = "CustomUmbracoPath"; - public static string AspEnvironment = "AspEnvironment"; - public static string IsDebug = "IsDebug"; - public static string DatabaseProvider = "DatabaseProvider"; - } +public static partial class Constants +{ + public static class Telemetry + { + public static string RootCount = "RootCount"; + public static string DomainCount = "DomainCount"; + public static string ExamineIndexCount = "ExamineIndexCount"; + public static string LanguageCount = "LanguageCount"; + public static string MacroCount = "MacroCount"; + public static string MediaCount = "MediaCount"; + public static string MemberCount = "MemberCount"; + public static string TemplateCount = "TemplateCount"; + public static string ContentCount = "ContentCount"; + public static string DocumentTypeCount = "DocumentTypeCount"; + public static string Properties = "Properties"; + public static string UserCount = "UserCount"; + public static string UserGroupCount = "UserGroupCount"; + public static string ServerOs = "ServerOs"; + public static string ServerFramework = "ServerFramework"; + public static string OsLanguage = "OsLanguage"; + public static string WebServer = "WebServer"; + public static string ModelsBuilderMode = "ModelBuilderMode"; + public static string CustomUmbracoPath = "CustomUmbracoPath"; + public static string AspEnvironment = "AspEnvironment"; + public static string IsDebug = "IsDebug"; + public static string DatabaseProvider = "DatabaseProvider"; + public static string CurrentServerRole = "CurrentServerRole"; } } diff --git a/src/Umbraco.Core/Constants-UdiEntityType.cs b/src/Umbraco.Core/Constants-UdiEntityType.cs index 01e9ca213d..f65c290516 100644 --- a/src/Umbraco.Core/Constants-UdiEntityType.cs +++ b/src/Umbraco.Core/Constants-UdiEntityType.cs @@ -1,74 +1,66 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines well-known entity types. + /// + /// + /// Well-known entity types are those that Deploy already knows about, + /// but entity types are strings and so can be extended beyond what is defined here. + /// + public static class UdiEntityType { - /// - /// Defines well-known entity types. - /// - /// Well-known entity types are those that Deploy already knows about, - /// but entity types are strings and so can be extended beyond what is defined here. - public static class UdiEntityType - { - // note: const fields in this class MUST be consistent with what GetTypes returns - // this is validated by UdiTests.ValidateUdiEntityType - // also, this is used exclusively in Udi static ctor, only once, so there is no - // need to keep it around in a field nor to make it readonly + // note: const fields in this class MUST be consistent with what GetTypes returns + // this is validated by UdiTests.ValidateUdiEntityType + // also, this is used exclusively in Udi static ctor, only once, so there is no + // need to keep it around in a field nor to make it readonly + public const string Unknown = "unknown"; + // guid entity types + public const string AnyGuid = "any-guid"; // that one is for tests - public const string Unknown = "unknown"; + public const string Element = "element"; + public const string Document = "document"; - // guid entity types + public const string DocumentBlueprint = "document-blueprint"; - public const string AnyGuid = "any-guid"; // that one is for tests + public const string Media = "media"; + public const string Member = "member"; - public const string Element = "element"; - public const string Document = "document"; + public const string DictionaryItem = "dictionary-item"; + public const string Macro = "macro"; + public const string Template = "template"; - public const string DocumentBlueprint = "document-blueprint"; + public const string DocumentType = "document-type"; + public const string DocumentTypeContainer = "document-type-container"; - public const string Media = "media"; - public const string Member = "member"; + // TODO: What is this? This alias is only used for the blue print tree to render the blueprint's document type, it's not a real udi type + public const string DocumentTypeBluePrints = "document-type-blueprints"; + public const string MediaType = "media-type"; + public const string MediaTypeContainer = "media-type-container"; + public const string DataType = "data-type"; + public const string DataTypeContainer = "data-type-container"; + public const string MemberType = "member-type"; + public const string MemberGroup = "member-group"; - public const string DictionaryItem = "dictionary-item"; - public const string Macro = "macro"; - public const string Template = "template"; + public const string RelationType = "relation-type"; - public const string DocumentType = "document-type"; - public const string DocumentTypeContainer = "document-type-container"; + // forms + public const string FormsForm = "forms-form"; + public const string FormsPreValue = "forms-prevalue"; + public const string FormsDataSource = "forms-datasource"; - // TODO: What is this? This alias is only used for the blue print tree to render the blueprint's document type, it's not a real udi type - public const string DocumentTypeBluePrints = "document-type-blueprints"; - public const string MediaType = "media-type"; - public const string MediaTypeContainer = "media-type-container"; - public const string DataType = "data-type"; - public const string DataTypeContainer = "data-type-container"; - public const string MemberType = "member-type"; - public const string MemberGroup = "member-group"; + // string entity types + public const string AnyString = "any-string"; // that one is for tests - public const string RelationType = "relation-type"; - - // forms - - public const string FormsForm = "forms-form"; - public const string FormsPreValue = "forms-prevalue"; - public const string FormsDataSource = "forms-datasource"; - - // string entity types - - public const string AnyString = "any-string"; // that one is for tests - - public const string Language = "language"; - public const string MacroScript = "macroscript"; - public const string MediaFile = "media-file"; - public const string TemplateFile = "template-file"; - public const string Script = "script"; - public const string Stylesheet = "stylesheet"; - public const string PartialView = "partial-view"; - public const string PartialViewMacro = "partial-view-macro"; - - - - - } + public const string Language = "language"; + public const string MacroScript = "macroscript"; + public const string MediaFile = "media-file"; + public const string TemplateFile = "template-file"; + public const string Script = "script"; + public const string Stylesheet = "stylesheet"; + public const string PartialView = "partial-view"; + public const string PartialViewMacro = "partial-view-macro"; } } diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f6a8c00970..bfbe4e56d5 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -1,73 +1,75 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for Umbraco system nodes. + /// + public static class Web { /// - /// Defines the identifiers for Umbraco system nodes. + /// The preview cookie name /// - public static class Web + public const string PreviewCookieName = "UMB_PREVIEW"; + + /// + /// Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. + /// + public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; + + public const string InstallerCookieName = "umb_installId"; + + /// + /// The cookie name that is used to store the validation value + /// + public const string CsrfValidationCookieName = "UMB-XSRF-V"; + + /// + /// The cookie name that is set for angular to use to pass in to the header value for "X-UMB-XSRF-TOKEN" + /// + public const string AngularCookieName = "UMB-XSRF-TOKEN"; + + /// + /// The header name that angular uses to pass in the token to validate the cookie + /// + public const string AngularHeadername = "X-UMB-XSRF-TOKEN"; + + /// + /// The route name of the page shown when Umbraco has no published content. + /// + public const string NoContentRouteName = "umbraco-no-content"; + + /// + /// The default authentication type used for remembering that 2FA is not needed on next login + /// + public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser"; + + public static class Mvc { - /// - /// The preview cookie name - /// - public const string PreviewCookieName = "UMB_PREVIEW"; + public const string InstallArea = "UmbracoInstall"; - /// - /// Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. - /// - public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; + public const string + BackOfficePathSegment = "BackOffice"; // The path segment prefix for all back office controllers - public const string InstallerCookieName = "umb_installId"; + public const string BackOfficeArea = "UmbracoBackOffice"; // Used for area routes of non-api controllers + public const string BackOfficeApiArea = "UmbracoApi"; // Same name as v8 so all routing remains the same + public const string BackOfficeTreeArea = "UmbracoTrees"; // Same name as v8 so all routing remains the same + } - /// - /// The cookie name that is used to store the validation value - /// - public const string CsrfValidationCookieName = "UMB-XSRF-V"; + public static class Routing + { + public const string ControllerToken = "controller"; + public const string ActionToken = "action"; + public const string AreaToken = "area"; + } - /// - /// The cookie name that is set for angular to use to pass in to the header value for "X-UMB-XSRF-TOKEN" - /// - public const string AngularCookieName = "UMB-XSRF-TOKEN"; - - /// - /// The header name that angular uses to pass in the token to validate the cookie - /// - public const string AngularHeadername = "X-UMB-XSRF-TOKEN"; - - /// - /// The route name of the page shown when Umbraco has no published content. - /// - public const string NoContentRouteName = "umbraco-no-content"; - - /// - /// The default authentication type used for remembering that 2FA is not needed on next login - /// - public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser"; - - public static class Mvc - { - public const string InstallArea = "UmbracoInstall"; - public const string BackOfficePathSegment = "BackOffice"; // The path segment prefix for all back office controllers - public const string BackOfficeArea = "UmbracoBackOffice"; // Used for area routes of non-api controllers - public const string BackOfficeApiArea = "UmbracoApi"; // Same name as v8 so all routing remains the same - public const string BackOfficeTreeArea = "UmbracoTrees"; // Same name as v8 so all routing remains the same - } - - public static class Routing - { - public const string ControllerToken = "controller"; - public const string ActionToken = "action"; - public const string AreaToken = "area"; - } - - public static class EmailTypes - { - public const string HealthCheck = "HealthCheck"; - public const string Notification = "Notification"; - public const string PasswordReset = "PasswordReset"; - public const string TwoFactorAuth = "2FA"; - public const string UserInvite = "UserInvite"; - } + public static class EmailTypes + { + public const string HealthCheck = "HealthCheck"; + public const string Notification = "Notification"; + public const string PasswordReset = "PasswordReset"; + public const string TwoFactorAuth = "2FA"; + public const string UserInvite = "UserInvite"; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs index e4a5eedf18..09a3e410fd 100644 --- a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs +++ b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.ContentEditing; @@ -8,59 +5,63 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentAppFactoryCollection : BuilderCollectionBase { - public class ContentAppFactoryCollection : BuilderCollectionBase + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly ILogger _logger; + + public ContentAppFactoryCollection( + Func> items, + ILogger logger, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + : base(items) { - private readonly ILogger _logger; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + _logger = logger; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } - public ContentAppFactoryCollection(Func> items, ILogger logger, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) - : base(items) + public IEnumerable GetContentAppsFor(object o, IEnumerable? userGroups = null) + { + IEnumerable roles = GetCurrentUserGroups(); + + var apps = this.Select(x => x.GetContentAppFor(o, roles)).WhereNotNull().OrderBy(x => x.Weight).ToList(); + + var aliases = new HashSet(); + List? dups = null; + + foreach (ContentApp app in apps) { - _logger = logger; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - } - - private IEnumerable GetCurrentUserGroups() - { - var currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - return currentUser == null - ? Enumerable.Empty() - : currentUser.Groups; - - } - - public IEnumerable GetContentAppsFor(object o, IEnumerable? userGroups = null) - { - var roles = GetCurrentUserGroups(); - - var apps = this.Select(x => x.GetContentAppFor(o, roles)).WhereNotNull().OrderBy(x => x.Weight).ToList(); - - var aliases = new HashSet(); - List? dups = null; - - foreach (var app in apps) + if (app.Alias is not null) { - if (app.Alias is not null) + if (aliases.Contains(app.Alias)) { - - if (aliases.Contains(app.Alias)) - (dups ?? (dups = new List())).Add(app.Alias); - else - aliases.Add(app.Alias); + (dups ??= new List()).Add(app.Alias); + } + else + { + aliases.Add(app.Alias); } } - - if (dups != null) - { - // dying is not user-friendly, so let's write to log instead, and wish people read logs... - - //throw new InvalidOperationException($"Duplicate content app aliases found: {string.Join(",", dups)}"); - _logger.LogWarning("Duplicate content app aliases found: {DuplicateAliases}", string.Join(",", dups)); - } - - return apps; } + + if (dups != null) + { + // dying is not user-friendly, so let's write to log instead, and wish people read logs... + + // throw new InvalidOperationException($"Duplicate content app aliases found: {string.Join(",", dups)}"); + _logger.LogWarning("Duplicate content app aliases found: {DuplicateAliases}", string.Join(",", dups)); + } + + return apps; + } + + private IEnumerable GetCurrentUserGroups() + { + IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + return currentUser == null + ? Enumerable.Empty() + : currentUser.Groups; } } diff --git a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs index a80c79a3ef..fe6fdd423a 100644 --- a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; @@ -9,31 +6,31 @@ using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentAppFactoryCollectionBuilder : OrderedCollectionBuilderBase { - public class ContentAppFactoryCollectionBuilder : OrderedCollectionBuilderBase + protected override ContentAppFactoryCollectionBuilder This => this; + + // need to inject dependencies in the collection, so override creation + public override ContentAppFactoryCollection CreateCollection(IServiceProvider factory) { - protected override ContentAppFactoryCollectionBuilder This => this; + // get the logger factory just-in-time - see note below for manifest parser + ILoggerFactory loggerFactory = factory.GetRequiredService(); + IBackOfficeSecurityAccessor backOfficeSecurityAccessor = + factory.GetRequiredService(); + return new ContentAppFactoryCollection(() => CreateItems(factory), loggerFactory.CreateLogger(), backOfficeSecurityAccessor); + } - // need to inject dependencies in the collection, so override creation - public override ContentAppFactoryCollection CreateCollection(IServiceProvider factory) - { - // get the logger factory just-in-time - see note below for manifest parser - var loggerFactory = factory.GetRequiredService(); - var backOfficeSecurityAccessor = factory.GetRequiredService(); - return new ContentAppFactoryCollection( - () => CreateItems(factory), - loggerFactory.CreateLogger(), backOfficeSecurityAccessor); - } - - protected override IEnumerable CreateItems(IServiceProvider factory) - { - // get the manifest parser just-in-time - injecting it in the ctor would mean that - // simply getting the builder in order to configure the collection, would require - // its dependencies too, and that can create cycles or other oddities - var manifestParser = factory.GetRequiredService(); - var ioHelper = factory.GetRequiredService(); - return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.ContentApps.Select(x => new ManifestContentAppFactory(x, ioHelper))); - } + protected override IEnumerable CreateItems(IServiceProvider factory) + { + // get the manifest parser just-in-time - injecting it in the ctor would mean that + // simply getting the builder in order to configure the collection, would require + // its dependencies too, and that can create cycles or other oddities + IManifestParser manifestParser = factory.GetRequiredService(); + IIOHelper ioHelper = factory.GetRequiredService(); + return base.CreateItems(factory) + .Concat(manifestParser.CombinedManifest.ContentApps.Select(x => + new ManifestContentAppFactory(x, ioHelper))); } } diff --git a/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs index 948c563ea9..ac8b3a2061 100644 --- a/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs @@ -1,56 +1,54 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentEditorContentAppFactory : IContentAppFactory { - public class ContentEditorContentAppFactory : IContentAppFactory + // see note on ContentApp + internal const int Weight = -100; + + private ContentApp? _contentApp; + private ContentApp? _mediaApp; + private ContentApp? _memberApp; + + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) { - // see note on ContentApp - internal const int Weight = -100; - - private ContentApp? _contentApp; - private ContentApp? _mediaApp; - private ContentApp? _memberApp; - - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + switch (o) { - switch (o) - { - case IContent content when content.Properties.Count > 0: - return _contentApp ?? (_contentApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = Constants.Icons.Content, - View = "views/content/apps/content/content.html", - Weight = Weight - }); + case IContent content when content.Properties.Count > 0: + return _contentApp ??= new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/content/apps/content/content.html", + Weight = Weight, + }; - case IMedia media when !media.ContentType.IsContainer || media.Properties.Count > 0: - return _mediaApp ?? (_mediaApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = Constants.Icons.Content, - View = "views/media/apps/content/content.html", - Weight = Weight - }); + case IMedia media when !media.ContentType.IsContainer || media.Properties.Count > 0: + return _mediaApp ??= new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/media/apps/content/content.html", + Weight = Weight, + }; - case IMember _: - return _memberApp ?? (_memberApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = Constants.Icons.Content, - View = "views/member/apps/content/content.html", - Weight = Weight - }); + case IMember _: + return _memberApp ??= new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/member/apps/content/content.html", + Weight = Weight, + }; - default: - return null; - } + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs index 3e068750c4..1e318e380e 100644 --- a/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs @@ -1,55 +1,53 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentInfoContentAppFactory : IContentAppFactory { - public class ContentInfoContentAppFactory : IContentAppFactory + // see note on ContentApp + private const int Weight = +100; + + private ContentApp? _contentApp; + private ContentApp? _mediaApp; + private ContentApp? _memberApp; + + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) { - // see note on ContentApp - private const int Weight = +100; - - private ContentApp? _contentApp; - private ContentApp? _mediaApp; - private ContentApp? _memberApp; - - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + switch (o) { - switch (o) - { - case IContent _: - return _contentApp ??= new ContentApp - { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/content/apps/info/info.html", - Weight = Weight - }; + case IContent _: + return _contentApp ??= new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/content/apps/info/info.html", + Weight = Weight, + }; - case IMedia _: - return _mediaApp ??= new ContentApp - { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/media/apps/info/info.html", - Weight = Weight - }; - case IMember _: - return _memberApp ??= new ContentApp - { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/member/apps/info/info.html", - Weight = Weight - }; + case IMedia _: + return _mediaApp ??= new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/media/apps/info/info.html", + Weight = Weight, + }; + case IMember _: + return _memberApp ??= new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/member/apps/info/info.html", + Weight = Weight, + }; - default: - return null; - } + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs index 0fe482e7d4..5e4f6a7a88 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypeDesignContentAppFactory : IContentAppFactory { - public class ContentTypeDesignContentAppFactory : IContentAppFactory + private const int Weight = -200; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -200; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "design", - Name = "Design", - Icon = "icon-document-dashed-line", - View = "views/documentTypes/views/design/design.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "design", + Name = "Design", + Icon = "icon-document-dashed-line", + View = "views/documentTypes/views/design/design.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs index 6ddf98e132..8aed04050f 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypeListViewContentAppFactory : IContentAppFactory { - public class ContentTypeListViewContentAppFactory : IContentAppFactory + private const int Weight = -180; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -180; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "listView", - Name = "List view", - Icon = "icon-list", - View = "views/documentTypes/views/listview/listview.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "listView", + Name = "List view", + Icon = "icon-list", + View = "views/documentTypes/views/listview/listview.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs index 98b82d24e7..b585a7db4d 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypePermissionsContentAppFactory : IContentAppFactory { - public class ContentTypePermissionsContentAppFactory : IContentAppFactory + private const int Weight = -160; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -160; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "permissions", - Name = "Permissions", - Icon = "icon-keychain", - View = "views/documentTypes/views/permissions/permissions.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "permissions", + Name = "Permissions", + Icon = "icon-keychain", + View = "views/documentTypes/views/permissions/permissions.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs index 74e57d76c9..712e1e7c1e 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypeTemplatesContentAppFactory : IContentAppFactory { - public class ContentTypeTemplatesContentAppFactory : IContentAppFactory + private const int Weight = -140; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -140; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "templates", - Name = "Templates", - Icon = "icon-layout", - View = "views/documentTypes/views/templates/templates.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "templates", + Name = "Templates", + Icon = "icon-layout", + View = "views/documentTypes/views/templates/templates.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs index ae8a957df7..21bfcfcef0 100644 --- a/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +internal class DictionaryContentAppFactory : IContentAppFactory { - internal class DictionaryContentAppFactory : IContentAppFactory + private const int Weight = -100; + + private ContentApp? _dictionaryApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -100; - - private ContentApp? _dictionaryApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IDictionaryItem _: - return _dictionaryApp ??= new ContentApp - { - Alias = "dictionaryContent", - Name = "Content", - Icon = "icon-document", - View = "views/dictionary/views/content/content.html", - Weight = Weight - }; - default: - return null; - } + case IDictionaryItem _: + return _dictionaryApp ??= new ContentApp + { + Alias = "dictionaryContent", + Name = "Content", + Icon = "icon-document", + View = "views/dictionary/views/content/content.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs index d33c50499f..87c755fdc9 100644 --- a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs @@ -1,135 +1,160 @@ -using System; -using System.Collections.Generic; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ListViewContentAppFactory : IContentAppFactory { - public class ListViewContentAppFactory : IContentAppFactory + // see note on ContentApp + private const int Weight = -666; + + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + + public ListViewContentAppFactory(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors) { - // see note on ContentApp - private const int Weight = -666; + _dataTypeService = dataTypeService; + _propertyEditors = propertyEditors; + } - private readonly IDataTypeService _dataTypeService; - private readonly PropertyEditorCollection _propertyEditors; - - public ListViewContentAppFactory(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors) + public static ContentApp CreateContentApp( + IDataTypeService dataTypeService, + PropertyEditorCollection propertyEditors, + string entityType, + string contentTypeAlias, + int defaultListViewDataType) + { + if (dataTypeService == null) { - _dataTypeService = dataTypeService; - _propertyEditors = propertyEditors; + throw new ArgumentNullException(nameof(dataTypeService)); } - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + if (propertyEditors == null) { - string contentTypeAlias, entityType; - int dtdId; - - switch (o) - { - case IContent content when !content.ContentType.IsContainer: - return null; - case IContent content: - contentTypeAlias = content.ContentType.Alias; - entityType = "content"; - dtdId = Constants.DataTypes.DefaultContentListView; - break; - case IMedia media when !media.ContentType.IsContainer && media.ContentType.Alias != Constants.Conventions.MediaTypes.Folder: - return null; - case IMedia media: - contentTypeAlias = media.ContentType.Alias; - entityType = "media"; - dtdId = Constants.DataTypes.DefaultMediaListView; - break; - default: - return null; - } - - return CreateContentApp(_dataTypeService, _propertyEditors, entityType, contentTypeAlias, dtdId); + throw new ArgumentNullException(nameof(propertyEditors)); } - public static ContentApp CreateContentApp(IDataTypeService dataTypeService, - PropertyEditorCollection propertyEditors, - string entityType, string contentTypeAlias, - int defaultListViewDataType) + if (string.IsNullOrWhiteSpace(entityType)) { - if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); - if (propertyEditors == null) throw new ArgumentNullException(nameof(propertyEditors)); - if (string.IsNullOrWhiteSpace(entityType)) throw new ArgumentException("message", nameof(entityType)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("message", nameof(contentTypeAlias)); - if (defaultListViewDataType == default) throw new ArgumentException("defaultListViewDataType", nameof(defaultListViewDataType)); - - var contentApp = new ContentApp - { - Alias = "umbListView", - Name = "Child items", - Icon = "icon-list", - View = "views/content/apps/listview/listview.html", - Weight = Weight - }; - - var customDtdName = Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias; - - //first try to get the custom one if there is one - var dt = dataTypeService.GetDataType(customDtdName) - ?? dataTypeService.GetDataType(defaultListViewDataType); - - if (dt == null) - { - throw new InvalidOperationException("No list view data type was found for this document type, ensure that the default list view data types exists and/or that your custom list view data type exists"); - } - - var editor = propertyEditors[dt.EditorAlias]; - if (editor == null) - { - throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); - } - - var listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditor(dt.Configuration); - //add the entity type to the config - listViewConfig["entityType"] = entityType; - - //Override Tab Label if tabName is provided - if (listViewConfig.ContainsKey("tabName")) - { - var configTabName = listViewConfig["tabName"]; - if (configTabName != null && String.IsNullOrWhiteSpace(configTabName.ToString()) == false) - contentApp.Name = configTabName.ToString(); - } - - //Override Icon if icon is provided - if (listViewConfig.ContainsKey("icon")) - { - var configIcon = listViewConfig["icon"]; - if (configIcon != null && String.IsNullOrWhiteSpace(configIcon.ToString()) == false) - contentApp.Icon = configIcon.ToString(); - } - - // if the list view is configured to show umbContent first, update the list view content app weight accordingly - if(listViewConfig.ContainsKey("showContentFirst") && - listViewConfig["showContentFirst"]?.ToString().TryConvertTo().Result == true) - { - contentApp.Weight = ContentEditorContentAppFactory.Weight + 1; - } - - //This is the view model used for the list view app - contentApp.ViewModel = new List - { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}containerView", - Label = "", - Value = null, - View = editor.GetValueEditor().View, - HideLabel = true, - Config = listViewConfig - } - }; - - return contentApp; + throw new ArgumentException("message", nameof(entityType)); } + + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException("message", nameof(contentTypeAlias)); + } + + if (defaultListViewDataType == default) + { + throw new ArgumentException("defaultListViewDataType", nameof(defaultListViewDataType)); + } + + var contentApp = new ContentApp + { + Alias = "umbListView", + Name = "Child items", + Icon = "icon-list", + View = "views/content/apps/listview/listview.html", + Weight = Weight, + }; + + var customDtdName = Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias; + + // first try to get the custom one if there is one + IDataType? dt = dataTypeService.GetDataType(customDtdName) + ?? dataTypeService.GetDataType(defaultListViewDataType); + + if (dt == null) + { + throw new InvalidOperationException( + "No list view data type was found for this document type, ensure that the default list view data types exists and/or that your custom list view data type exists"); + } + + IDataEditor? editor = propertyEditors[dt.EditorAlias]; + if (editor == null) + { + throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); + } + + IDictionary listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditorNullable(dt.Configuration); + + // add the entity type to the config + listViewConfig["entityType"] = entityType; + + // Override Tab Label if tabName is provided + if (listViewConfig.ContainsKey("tabName")) + { + var configTabName = listViewConfig["tabName"]; + if (string.IsNullOrWhiteSpace(configTabName?.ToString()) == false) + { + contentApp.Name = configTabName.ToString(); + } + } + + // Override Icon if icon is provided + if (listViewConfig.ContainsKey("icon")) + { + var configIcon = listViewConfig["icon"]; + if (string.IsNullOrWhiteSpace(configIcon?.ToString()) == false) + { + contentApp.Icon = configIcon.ToString(); + } + } + + // if the list view is configured to show umbContent first, update the list view content app weight accordingly + if (listViewConfig.ContainsKey("showContentFirst") && + listViewConfig["showContentFirst"]?.ToString().TryConvertTo().Result == true) + { + contentApp.Weight = ContentEditorContentAppFactory.Weight + 1; + } + + // This is the view model used for the list view app + contentApp.ViewModel = new List + { + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}containerView", + Label = string.Empty, + Value = null, + View = editor.GetValueEditor().View, + HideLabel = true, + ConfigNullable = listViewConfig, + }, + }; + + return contentApp; + } + + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + { + string contentTypeAlias, entityType; + int dtdId; + + switch (o) + { + case IContent content when !content.ContentType.IsContainer: + return null; + case IContent content: + contentTypeAlias = content.ContentType.Alias; + entityType = "content"; + dtdId = Constants.DataTypes.DefaultContentListView; + break; + case IMedia media when !media.ContentType.IsContainer && + media.ContentType.Alias != Constants.Conventions.MediaTypes.Folder: + return null; + case IMedia media: + contentTypeAlias = media.ContentType.Alias; + entityType = "media"; + dtdId = Constants.DataTypes.DefaultMediaListView; + break; + default: + return null; + } + + return CreateContentApp(_dataTypeService, _propertyEditors, entityType, contentTypeAlias, dtdId); } } diff --git a/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs b/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs index ae5e783bbc..5ba19cabb0 100644 --- a/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs @@ -1,34 +1,32 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +internal class MemberEditorContentAppFactory : IContentAppFactory { - internal class MemberEditorContentAppFactory : IContentAppFactory + // see note on ContentApp + internal const int Weight = +50; + + private ContentApp? _memberApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - // see note on ContentApp - internal const int Weight = +50; - - private ContentApp? _memberApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IMember _: - return _memberApp ??= new ContentApp - { - Alias = "umbMembership", - Name = "Member", - Icon = "icon-user", - View = "views/member/apps/membership/membership.html", - Weight = Weight - }; + case IMember _: + return _memberApp ??= new ContentApp + { + Alias = "umbMembership", + Name = "Member", + Icon = "icon-user", + View = "views/member/apps/membership/membership.html", + Weight = Weight, + }; - default: - return null; - } + default: + return null; } } } diff --git a/src/Umbraco.Core/ConventionsHelper.cs b/src/Umbraco.Core/ConventionsHelper.cs index 2f9203ef92..7d79338142 100644 --- a/src/Umbraco.Core/ConventionsHelper.cs +++ b/src/Umbraco.Core/ConventionsHelper.cs @@ -1,26 +1,22 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class ConventionsHelper { - public static class ConventionsHelper - { - public static Dictionary GetStandardPropertyTypeStubs(IShortStringHelper shortStringHelper) => - new Dictionary + public static Dictionary GetStandardPropertyTypeStubs(IShortStringHelper shortStringHelper) => + new() + { { - { - Constants.Conventions.Member.Comments, - new PropertyType( - shortStringHelper, - Constants.PropertyEditors.Aliases.TextArea, - ValueStorageType.Ntext, - true, - Constants.Conventions.Member.Comments) - { - Name = Constants.Conventions.Member.CommentsLabel, - } - }, - }; - } + Constants.Conventions.Member.Comments, + new PropertyType( + shortStringHelper, + Constants.PropertyEditors.Aliases.TextArea, + ValueStorageType.Ntext, + true, + Constants.Conventions.Member.Comments) + { Name = Constants.Conventions.Member.CommentsLabel } + }, + }; } diff --git a/src/Umbraco.Core/CustomBooleanTypeConverter.cs b/src/Umbraco.Core/CustomBooleanTypeConverter.cs index 253f070b40..bacfec7ef9 100644 --- a/src/Umbraco.Core/CustomBooleanTypeConverter.cs +++ b/src/Umbraco.Core/CustomBooleanTypeConverter.cs @@ -1,34 +1,48 @@ -using System; using System.ComponentModel; +using System.Globalization; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Allows for converting string representations of 0 and 1 to boolean +/// +public class CustomBooleanTypeConverter : BooleanConverter { - /// - /// Allows for converting string representations of 0 and 1 to boolean - /// - public class CustomBooleanTypeConverter : BooleanConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + if (sourceType == typeof(string)) { - if (sourceType == typeof(string)) + return true; + } + + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string str) + { + if (str == null || str.Length == 0 || str == "0") + { + return false; + } + + if (str == "1") { return true; } - return base.CanConvertFrom(context, sourceType); - } - public override object? ConvertFrom(ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) - { - if (value is string) + if (str.Equals("Yes", StringComparison.OrdinalIgnoreCase)) { - var str = (string)value; - if (str == null || str.Length == 0 || str == "0") return false; - if (str == "1") return true; - if (str.Equals("Yes", StringComparison.OrdinalIgnoreCase)) return true; - if (str.Equals("No", StringComparison.OrdinalIgnoreCase)) return false; + return true; } - return base.ConvertFrom(context, culture, value); + if (str.Equals("No", StringComparison.OrdinalIgnoreCase)) + { + return false; + } } + + return base.ConvertFrom(context, culture, value); } } diff --git a/src/Umbraco.Core/Dashboards/AccessRule.cs b/src/Umbraco.Core/Dashboards/AccessRule.cs index 070659518e..eb7383f601 100644 --- a/src/Umbraco.Core/Dashboards/AccessRule.cs +++ b/src/Umbraco.Core/Dashboards/AccessRule.cs @@ -1,13 +1,13 @@ -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Implements . +/// +public class AccessRule : IAccessRule { - /// - /// Implements . - /// - public class AccessRule : IAccessRule - { - /// - public AccessRuleType Type { get; set; } = AccessRuleType.Unknown; - /// - public string? Value { get; set; } - } + /// + public AccessRuleType Type { get; set; } = AccessRuleType.Unknown; + + /// + public string? Value { get; set; } } diff --git a/src/Umbraco.Core/Dashboards/AccessRuleType.cs b/src/Umbraco.Core/Dashboards/AccessRuleType.cs index 103d944de8..63d92fc38a 100644 --- a/src/Umbraco.Core/Dashboards/AccessRuleType.cs +++ b/src/Umbraco.Core/Dashboards/AccessRuleType.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Defines dashboard access rules type. +/// +public enum AccessRuleType { /// - /// Defines dashboard access rules type. + /// Unknown (default value). /// - public enum AccessRuleType - { - /// - /// Unknown (default value). - /// - Unknown = 0, + Unknown = 0, - /// - /// Grant access to the dashboard if user belongs to the specified user group. - /// - Grant, + /// + /// Grant access to the dashboard if user belongs to the specified user group. + /// + Grant, - /// - /// Deny access to the dashboard if user belongs to the specified user group. - /// - Deny, + /// + /// Deny access to the dashboard if user belongs to the specified user group. + /// + Deny, - /// - /// Grant access to the dashboard if user has access to the specified section. - /// - GrantBySection - } + /// + /// Grant access to the dashboard if user has access to the specified section. + /// + GrantBySection, } diff --git a/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs index 1be6e045d0..07688832f6 100644 --- a/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs +++ b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs @@ -1,15 +1,12 @@ -using System; +namespace Umbraco.Cms.Core.Dashboards; -namespace Umbraco.Cms.Core.Dashboards +public class AnalyticsDashboard : IDashboard { - public class AnalyticsDashboard : IDashboard - { - public string Alias => "settingsAnalytics"; + public string Alias => "settingsAnalytics"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/analytics.html"; + public string View => "views/dashboard/settings/analytics.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/ContentDashboard.cs b/src/Umbraco.Core/Dashboards/ContentDashboard.cs index 135fe4304d..ff3a0031b3 100644 --- a/src/Umbraco.Core/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ContentDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class ContentDashboard : IDashboard { - [Weight(10)] - public class ContentDashboard : IDashboard - { - public string Alias => "contentIntro"; + public string Alias => "contentIntro"; - public string[] Sections => new[] { "content" }; + public string[] Sections => new[] { "content" }; - public string View => "views/dashboard/default/startupdashboardintro.html"; + public string View => "views/dashboard/default/startupdashboardintro.html"; - public IAccessRule[] AccessRules { get; } = Array.Empty(); - } + public IAccessRule[] AccessRules { get; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollection.cs b/src/Umbraco.Core/Dashboards/DashboardCollection.cs index e5c8378139..ebcf79fc7f 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollection.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +public class DashboardCollection : BuilderCollectionBase { - public class DashboardCollection : BuilderCollectionBase + public DashboardCollection(Func> items) + : base(items) { - public DashboardCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs index 348e81e383..50867c90f4 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs @@ -1,46 +1,42 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +public class DashboardCollectionBuilder : WeightedCollectionBuilderBase { - public class DashboardCollectionBuilder : WeightedCollectionBuilderBase + protected override DashboardCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) { - protected override DashboardCollectionBuilder This => this; + // get the manifest parser just-in-time - injecting it in the ctor would mean that + // simply getting the builder in order to configure the collection, would require + // its dependencies too, and that can create cycles or other oddities + IManifestParser manifestParser = factory.GetRequiredService(); - protected override IEnumerable CreateItems(IServiceProvider factory) + IEnumerable dashboardSections = + Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); + + return dashboardSections; + } + + private IEnumerable Merge( + IEnumerable dashboardsFromCode, + IReadOnlyList dashboardFromManifest) => + dashboardsFromCode.Concat(dashboardFromManifest) + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(GetWeight); + + private int GetWeight(IDashboard dashboard) + { + switch (dashboard) { - // get the manifest parser just-in-time - injecting it in the ctor would mean that - // simply getting the builder in order to configure the collection, would require - // its dependencies too, and that can create cycles or other oddities - var manifestParser = factory.GetRequiredService(); + case ManifestDashboard manifestDashboardDefinition: + return manifestDashboardDefinition.Weight; - var dashboardSections = Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); - - return dashboardSections; - } - - private IEnumerable Merge(IEnumerable dashboardsFromCode, IReadOnlyList dashboardFromManifest) - { - return dashboardsFromCode.Concat(dashboardFromManifest) - .Where(x => !string.IsNullOrEmpty(x.Alias)) - .OrderBy(GetWeight); - } - - private int GetWeight(IDashboard dashboard) - { - switch (dashboard) - { - case ManifestDashboard manifestDashboardDefinition: - return manifestDashboardDefinition.Weight; - - default: - return GetWeight(dashboard.GetType()); - } + default: + return GetWeight(dashboard.GetType()); } } } diff --git a/src/Umbraco.Core/Dashboards/DashboardSlim.cs b/src/Umbraco.Core/Dashboards/DashboardSlim.cs index 9ff2b51baf..a79392c0d0 100644 --- a/src/Umbraco.Core/Dashboards/DashboardSlim.cs +++ b/src/Umbraco.Core/Dashboards/DashboardSlim.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[DataContract(IsReference = true)] +public class DashboardSlim : IDashboardSlim { - [DataContract(IsReference = true)] - public class DashboardSlim : IDashboardSlim - { - public string? Alias { get; set; } + public string? Alias { get; set; } - public string? View { get; set; } - } + public string? View { get; set; } } diff --git a/src/Umbraco.Core/Dashboards/ExamineDashboard.cs b/src/Umbraco.Core/Dashboards/ExamineDashboard.cs index 5411f1d3ce..ddd048c99e 100644 --- a/src/Umbraco.Core/Dashboards/ExamineDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ExamineDashboard.cs @@ -1,19 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(20)] +public class ExamineDashboard : IDashboard { - [Weight(20)] - public class ExamineDashboard : IDashboard - { - public string Alias => "settingsExamine"; + public string Alias => "settingsExamine"; - public string[] Sections => new [] { "settings" }; - - public string View => "views/dashboard/settings/examinemanagement.html"; - - public IAccessRule[] AccessRules => Array.Empty(); - } + public string[] Sections => new[] { "settings" }; + public string View => "views/dashboard/settings/examinemanagement.html"; + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/FormsDashboard.cs b/src/Umbraco.Core/Dashboards/FormsDashboard.cs index c56ad7c51a..4146553484 100644 --- a/src/Umbraco.Core/Dashboards/FormsDashboard.cs +++ b/src/Umbraco.Core/Dashboards/FormsDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class FormsDashboard : IDashboard { - [Weight(10)] - public class FormsDashboard : IDashboard - { - public string Alias => "formsInstall"; + public string Alias => "formsInstall"; - public string[] Sections => new [] { Constants.Applications.Forms }; + public string[] Sections => new[] { Constants.Applications.Forms }; - public string View => "views/dashboard/forms/formsdashboardintro.html"; + public string View => "views/dashboard/forms/formsdashboardintro.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs b/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs index 24b4efaf6d..85c2053450 100644 --- a/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs +++ b/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs @@ -1,19 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(50)] +public class HealthCheckDashboard : IDashboard { - [Weight(50)] - public class HealthCheckDashboard : IDashboard - { - public string Alias => "settingsHealthCheck"; + public string Alias => "settingsHealthCheck"; - public string[] Sections => new [] { "settings" }; - - public string View => "views/dashboard/settings/healthcheck.html"; - - public IAccessRule[] AccessRules => Array.Empty(); - } + public string[] Sections => new[] { "settings" }; + public string View => "views/dashboard/settings/healthcheck.html"; + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/IAccessRule.cs b/src/Umbraco.Core/Dashboards/IAccessRule.cs index 9f8c120910..fcd78ebc9b 100644 --- a/src/Umbraco.Core/Dashboards/IAccessRule.cs +++ b/src/Umbraco.Core/Dashboards/IAccessRule.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Represents an access rule. +/// +public interface IAccessRule { /// - /// Represents an access rule. + /// Gets or sets the rule type. /// - public interface IAccessRule - { - /// - /// Gets or sets the rule type. - /// - AccessRuleType Type { get; set; } + AccessRuleType Type { get; set; } - /// - /// Gets or sets the value for the rule. - /// - string? Value { get; set; } - } + /// + /// Gets or sets the value for the rule. + /// + string? Value { get; set; } } diff --git a/src/Umbraco.Core/Dashboards/IDashboard.cs b/src/Umbraco.Core/Dashboards/IDashboard.cs index 41a60cb518..96e29d0539 100644 --- a/src/Umbraco.Core/Dashboards/IDashboard.cs +++ b/src/Umbraco.Core/Dashboards/IDashboard.cs @@ -1,37 +1,43 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Represents a dashboard. +/// +public interface IDashboard : IDashboardSlim { /// - /// Represents a dashboard. + /// Gets the aliases of sections/applications where this dashboard appears. /// - public interface IDashboard : IDashboardSlim - { - /// - /// Gets the aliases of sections/applications where this dashboard appears. - /// - /// - /// This field is *not* needed by the UI and therefore we want to exclude - /// it from serialization, but it is deserialized as part of the manifest, - /// therefore we cannot plainly ignore it. - /// So, it has to remain a data member, plus we use our special - /// JsonDontSerialize attribute (see attribute for more details). - /// - [DataMember(Name = "sections")] - string[] Sections { get; } + /// + /// + /// This field is *not* needed by the UI and therefore we want to exclude + /// it from serialization, but it is deserialized as part of the manifest, + /// therefore we cannot plainly ignore it. + /// + /// + /// So, it has to remain a data member, plus we use our special + /// JsonDontSerialize attribute (see attribute for more details). + /// + /// + [DataMember(Name = "sections")] + string[] Sections { get; } - - /// - /// Gets the access rule determining the visibility of the dashboard. - /// - /// - /// This field is *not* needed by the UI and therefore we want to exclude - /// it from serialization, but it is deserialized as part of the manifest, - /// therefore we cannot plainly ignore it. - /// So, it has to remain a data member, plus we use our special - /// JsonDontSerialize attribute (see attribute for more details). - /// - [DataMember(Name = "access")] - IAccessRule[] AccessRules { get; } - } + /// + /// Gets the access rule determining the visibility of the dashboard. + /// + /// + /// + /// This field is *not* needed by the UI and therefore we want to exclude + /// it from serialization, but it is deserialized as part of the manifest, + /// therefore we cannot plainly ignore it. + /// + /// + /// So, it has to remain a data member, plus we use our special + /// JsonDontSerialize attribute (see attribute for more details). + /// + /// + [DataMember(Name = "access")] + IAccessRule[] AccessRules { get; } } diff --git a/src/Umbraco.Core/Dashboards/IDashboardSlim.cs b/src/Umbraco.Core/Dashboards/IDashboardSlim.cs index 4859f5dc84..c3907b1af4 100644 --- a/src/Umbraco.Core/Dashboards/IDashboardSlim.cs +++ b/src/Umbraco.Core/Dashboards/IDashboardSlim.cs @@ -1,22 +1,21 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Represents a dashboard with only minimal data. +/// +public interface IDashboardSlim { /// - /// Represents a dashboard with only minimal data. + /// Gets the alias of the dashboard. /// - public interface IDashboardSlim - { - /// - /// Gets the alias of the dashboard. - /// - [DataMember(Name = "alias")] - string? Alias { get; } + [DataMember(Name = "alias")] + string? Alias { get; } - /// - /// Gets the view used to render the dashboard. - /// - [DataMember(Name = "view")] - string? View { get; } - } + /// + /// Gets the view used to render the dashboard. + /// + [DataMember(Name = "view")] + string? View { get; } } diff --git a/src/Umbraco.Core/Dashboards/MediaDashboard.cs b/src/Umbraco.Core/Dashboards/MediaDashboard.cs index acbad0bc2a..47e45c4270 100644 --- a/src/Umbraco.Core/Dashboards/MediaDashboard.cs +++ b/src/Umbraco.Core/Dashboards/MediaDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class MediaDashboard : IDashboard { - [Weight(10)] - public class MediaDashboard : IDashboard - { - public string Alias => "mediaFolderBrowser"; + public string Alias => "mediaFolderBrowser"; - public string[] Sections => new [] { "media" }; + public string[] Sections => new[] { "media" }; - public string View => "views/dashboard/media/mediafolderbrowser.html"; + public string View => "views/dashboard/media/mediafolderbrowser.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/MembersDashboard.cs b/src/Umbraco.Core/Dashboards/MembersDashboard.cs index 3023d63b8a..f69d0a1ed0 100644 --- a/src/Umbraco.Core/Dashboards/MembersDashboard.cs +++ b/src/Umbraco.Core/Dashboards/MembersDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class MembersDashboard : IDashboard { - [Weight(10)] - public class MembersDashboard : IDashboard - { - public string Alias => "memberIntro"; + public string Alias => "memberIntro"; - public string[] Sections => new [] { "member" }; + public string[] Sections => new[] { "member" }; - public string View => "views/dashboard/members/membersdashboardvideos.html"; + public string View => "views/dashboard/members/membersdashboardvideos.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs b/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs index 9ba5c9dd0c..640f6daf6e 100644 --- a/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(40)] +public class ModelsBuilderDashboard : IDashboard { - [Weight(40)] - public class ModelsBuilderDashboard : IDashboard - { - public string Alias => "settingsModelsBuilder"; + public string Alias => "settingsModelsBuilder"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/modelsbuildermanagement.html"; + public string View => "views/dashboard/settings/modelsbuildermanagement.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs b/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs index 7a3829209f..b84b1529c3 100644 --- a/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(60)] +public class ProfilerDashboard : IDashboard { - [Weight(60)] - public class ProfilerDashboard : IDashboard - { - public string Alias => "settingsProfiler"; + public string Alias => "settingsProfiler"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/profiler.html"; + public string View => "views/dashboard/settings/profiler.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs b/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs index 5cae4594f7..49709436ab 100644 --- a/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs +++ b/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs @@ -1,19 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(30)] +public class PublishedStatusDashboard : IDashboard { - [Weight(30)] - public class PublishedStatusDashboard : IDashboard - { - public string Alias => "settingsPublishedStatus"; + public string Alias => "settingsPublishedStatus"; - public string[] Sections => new [] { "settings" }; - - public string View => "views/dashboard/settings/publishedstatus.html"; - - public IAccessRule[] AccessRules => Array.Empty(); - } + public string[] Sections => new[] { "settings" }; + public string View => "views/dashboard/settings/publishedstatus.html"; + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs b/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs index 15eb883697..25b064154b 100644 --- a/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs +++ b/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(20)] +public class RedirectUrlDashboard : IDashboard { - [Weight(20)] - public class RedirectUrlDashboard : IDashboard - { - public string Alias => "contentRedirectManager"; + public string Alias => "contentRedirectManager"; - public string[] Sections => new [] { "content" }; + public string[] Sections => new[] { "content" }; - public string View => "views/dashboard/content/redirecturls.html"; + public string View => "views/dashboard/content/redirecturls.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/SettingsDashboards.cs b/src/Umbraco.Core/Dashboards/SettingsDashboards.cs index e5f37fd5a3..b9cb572240 100644 --- a/src/Umbraco.Core/Dashboards/SettingsDashboards.cs +++ b/src/Umbraco.Core/Dashboards/SettingsDashboards.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class SettingsDashboard : IDashboard { - [Weight(10)] - public class SettingsDashboard : IDashboard - { - public string Alias => "settingsWelcome"; + public string Alias => "settingsWelcome"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/settingsdashboardintro.html"; + public string View => "views/dashboard/settings/settingsdashboardintro.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/DefaultEventMessagesFactory.cs b/src/Umbraco.Core/DefaultEventMessagesFactory.cs index 544299b03a..9648e76fca 100644 --- a/src/Umbraco.Core/DefaultEventMessagesFactory.cs +++ b/src/Umbraco.Core/DefaultEventMessagesFactory.cs @@ -1,29 +1,26 @@ -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public class DefaultEventMessagesFactory : IEventMessagesFactory { - public class DefaultEventMessagesFactory : IEventMessagesFactory + private readonly IEventMessagesAccessor _eventMessagesAccessor; + + public DefaultEventMessagesFactory(IEventMessagesAccessor eventMessagesAccessor) { - private readonly IEventMessagesAccessor _eventMessagesAccessor; - - public DefaultEventMessagesFactory(IEventMessagesAccessor eventMessagesAccessor) - { - if (eventMessagesAccessor == null) throw new ArgumentNullException(nameof(eventMessagesAccessor)); - _eventMessagesAccessor = eventMessagesAccessor; - } - - public EventMessages Get() - { - var eventMessages = _eventMessagesAccessor.EventMessages; - if (eventMessages == null) - _eventMessagesAccessor.EventMessages = eventMessages = new EventMessages(); - return eventMessages; - } - - public EventMessages? GetOrDefault() - { - return _eventMessagesAccessor.EventMessages; - } + _eventMessagesAccessor = eventMessagesAccessor ?? throw new ArgumentNullException(nameof(eventMessagesAccessor)); } + + public EventMessages Get() + { + EventMessages? eventMessages = _eventMessagesAccessor.EventMessages; + if (eventMessages == null) + { + _eventMessagesAccessor.EventMessages = eventMessages = new EventMessages(); + } + + return eventMessages; + } + + public EventMessages? GetOrDefault() => _eventMessagesAccessor.EventMessages; } diff --git a/src/Umbraco.Core/DelegateEqualityComparer.cs b/src/Umbraco.Core/DelegateEqualityComparer.cs index 64d715c838..8a442e8f85 100644 --- a/src/Umbraco.Core/DelegateEqualityComparer.cs +++ b/src/Umbraco.Core/DelegateEqualityComparer.cs @@ -1,60 +1,54 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// A custom equality comparer that excepts a delegate to do the comparison operation +/// +/// +public class DelegateEqualityComparer : IEqualityComparer { - /// - /// A custom equality comparer that excepts a delegate to do the comparison operation - /// - /// - public class DelegateEqualityComparer : IEqualityComparer + private readonly Func _equals; + private readonly Func _getHashcode; + + #region Implementation of IEqualityComparer + + public DelegateEqualityComparer(Func equals, Func getHashcode) { - private readonly Func _equals; - private readonly Func _getHashcode; - - #region Implementation of IEqualityComparer - - public DelegateEqualityComparer(Func equals, Func getHashcode) - { - _getHashcode = getHashcode; - _equals = equals; - } - - public static DelegateEqualityComparer CompareMember(Func memberExpression) where TMember : IEquatable - { - return new DelegateEqualityComparer( - (x, y) => memberExpression.Invoke(x).Equals((TMember)memberExpression.Invoke(y)), - x => - { - var invoked = memberExpression.Invoke(x); - return !ReferenceEquals(invoked, default(TMember)) ? invoked.GetHashCode() : 0; - }); - } - - /// - /// Determines whether the specified objects are equal. - /// - /// - /// true if the specified objects are equal; otherwise, false. - /// - /// The first object of type to compare.The second object of type to compare. - public bool Equals(T? x, T? y) - { - return _equals.Invoke(x, y); - } - - /// - /// Returns a hash code for the specified object. - /// - /// - /// A hash code for the specified object. - /// - /// The for which a hash code is to be returned.The type of is a reference type and is null. - public int GetHashCode(T obj) - { - return _getHashcode.Invoke(obj); - } - - #endregion + _getHashcode = getHashcode; + _equals = equals; } + + public static DelegateEqualityComparer CompareMember(Func memberExpression) + where TMember : IEquatable => + new DelegateEqualityComparer( + (x, y) => memberExpression.Invoke(x).Equals(memberExpression.Invoke(y)), + x => + { + TMember invoked = memberExpression.Invoke(x); + return !ReferenceEquals(invoked, default(TMember)) ? invoked.GetHashCode() : 0; + }); + + /// + /// Determines whether the specified objects are equal. + /// + /// + /// true if the specified objects are equal; otherwise, false. + /// + /// The first object of type to compare. + /// The second object of type to compare. + public bool Equals(T? x, T? y) => _equals.Invoke(x, y); + + /// + /// Returns a hash code for the specified object. + /// + /// + /// A hash code for the specified object. + /// + /// The for which a hash code is to be returned. + /// + /// The type of is a reference type and + /// is null. + /// + public int GetHashCode(T obj) => _getHashcode.Invoke(obj); + + #endregion } diff --git a/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs index d1fabe26db..939315cd86 100644 --- a/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs +++ b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs @@ -1,19 +1,16 @@ -using System; +namespace Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.DependencyInjection +/// +/// Provides access to a request scoped service provider when available for cases where +/// IHttpContextAccessor is not available. e.g. No reference to AspNetCore.Http in core. +/// +public interface IScopedServiceProvider { /// - /// Provides access to a request scoped service provider when available for cases where - /// IHttpContextAccessor is not available. e.g. No reference to AspNetCore.Http in core. + /// Gets a request scoped service provider when available. /// - public interface IScopedServiceProvider - { - /// - /// Gets a request scoped service provider when available. - /// - /// - /// Can be null. - /// - IServiceProvider? ServiceProvider { get; } - } + /// + /// Can be null. + /// + IServiceProvider? ServiceProvider { get; } } diff --git a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs index 59f06801ff..2629aceb6f 100644 --- a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -7,33 +6,38 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +public interface IUmbracoBuilder { - public interface IUmbracoBuilder - { - IServiceCollection Services { get; } - IConfiguration Config { get; } - TypeLoader TypeLoader { get; } + IServiceCollection Services { get; } - /// - /// A Logger factory created specifically for the . This is NOT the same - /// instance that will be resolved from DI. Use only if required during configuration. - /// - ILoggerFactory BuilderLoggerFactory { get; } + IConfiguration Config { get; } - /// - /// A hosting environment created specifically for the . This is NOT the same - /// instance that will be resolved from DI. Use only if required during configuration. - /// - /// - /// This may be null. - /// - [Obsolete("This property will be removed in a future version, please find an alternative approach.")] - IHostingEnvironment? BuilderHostingEnvironment { get; } + TypeLoader TypeLoader { get; } - IProfiler Profiler { get; } - AppCaches AppCaches { get; } - TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder; - void Build(); - } + /// + /// A Logger factory created specifically for the . This is NOT the same + /// instance that will be resolved from DI. Use only if required during configuration. + /// + ILoggerFactory BuilderLoggerFactory { get; } + + /// + /// A hosting environment created specifically for the . This is NOT the same + /// instance that will be resolved from DI. Use only if required during configuration. + /// + /// + /// This may be null. + /// + [Obsolete("This property will be removed in a future version, please find an alternative approach.")] + IHostingEnvironment? BuilderHostingEnvironment { get; } + + IProfiler Profiler { get; } + + AppCaches AppCaches { get; } + + TBuilder WithCollectionBuilder() + where TBuilder : ICollectionBuilder; + + void Build(); } diff --git a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs index 6a3d020671..579a34894a 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,104 +1,132 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + /// + /// Adds a service of type with an implementation type of + /// to the specified . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services) + where TService : class + where TImplementing : class, TService => + AddUnique(services, ServiceLifetime.Singleton); + + /// + /// Adds a service of type with an implementation type of + /// to the specified . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + ServiceLifetime lifetime) + where TService : class + where TImplementing : class, TService { - /// - /// Adds a service of type with an implementation type of to the specified . - /// - /// - /// Removes all previous registrations for the type . - /// - public static void AddUnique( - this IServiceCollection services) - where TService : class - where TImplementing : class, TService - { - AddUnique(services, ServiceLifetime.Singleton); - } + services.RemoveAll(); + services.Add(ServiceDescriptor.Describe(typeof(TService), typeof(TImplementing), lifetime)); + } + + /// + /// Adds services of types & with a shared + /// implementation type of to the specified . + /// + /// + /// Removes all previous registrations for the types & + /// . + /// + public static void AddMultipleUnique( + this IServiceCollection services) + where TService1 : class + where TService2 : class + where TImplementing : class, TService1, TService2 + => services.AddMultipleUnique(ServiceLifetime.Singleton); + + /// + /// Adds services of types & with a shared implementation type of to the specified . + /// + /// + /// Removes all previous registrations for the types & . + /// + public static void AddMultipleUnique( + this IServiceCollection services, + ServiceLifetime lifetime) + where TService1 : class + where TService2 : class + where TImplementing : class, TService1, TService2 + { + services.AddUnique(lifetime); + services.AddUnique(factory => (TImplementing)factory.GetRequiredService(), lifetime); + } /// - /// Adds a service of type with an implementation type of to the specified . - /// - /// - /// Removes all previous registrations for the type . - /// - public static void AddUnique( - this IServiceCollection services, - ServiceLifetime lifetime) - where TService : class - where TImplementing : class, TService - { - services.RemoveAll(); - services.Add(ServiceDescriptor.Describe(typeof(TService), typeof(TImplementing), lifetime)); - } + /// Adds a service of type with an implementation factory method to the specified + /// . + ///
+ /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + Func factory) + where TService : class + => services.AddUnique(factory, ServiceLifetime.Singleton); - /// - /// Adds services of types & with a shared implementation type of to the specified . - /// - /// - /// Removes all previous registrations for the types & . - /// - public static void AddMultipleUnique( - this IServiceCollection services, - ServiceLifetime lifetime = ServiceLifetime.Singleton) - where TService1 : class - where TService2 : class - where TImplementing : class, TService1, TService2 - { - services.AddUnique(lifetime); - services.AddUnique(factory => (TImplementing)factory.GetRequiredService(), lifetime); - } + /// + /// Adds a service of type with an implementation factory method to the specified . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + Func factory, + ServiceLifetime lifetime) + where TService : class + { + services.RemoveAll(); + services.Add(ServiceDescriptor.Describe(typeof(TService), factory, lifetime)); + } - /// - /// Adds a service of type with an implementation factory method to the specified . - /// - /// - /// Removes all previous registrations for the type . - /// - public static void AddUnique( - this IServiceCollection services, - Func factory, - ServiceLifetime lifetime = ServiceLifetime.Singleton) - where TService : class - { - services.RemoveAll(); - services.Add(ServiceDescriptor.Describe(typeof(TService), factory, lifetime)); - } + /// + /// Adds a singleton service of the type specified by to the specified + /// . + /// + /// + /// Removes all previous registrations for the type specified by . + /// + public static void AddUnique(this IServiceCollection services, Type serviceType, object instance) + { + services.RemoveAll(serviceType); + services.AddSingleton(serviceType, instance); + } - /// - /// Adds a singleton service of the type specified by to the specified . - /// - /// - /// Removes all previous registrations for the type specified by . - /// - public static void AddUnique(this IServiceCollection services, Type serviceType, object instance) - { - services.RemoveAll(serviceType); - services.AddSingleton(serviceType, instance); - } + /// + /// Adds a singleton service of type to the specified + /// . + /// + /// + /// Removes all previous registrations for the type type . + /// + public static void AddUnique(this IServiceCollection services, TService instance) + where TService : class + { + services.RemoveAll(); + services.AddSingleton(instance); + } - /// - /// Adds a singleton service of type to the specified . - /// - /// - /// Removes all previous registrations for the type type . - /// - public static void AddUnique(this IServiceCollection services, TService instance) - where TService : class - { - services.RemoveAll(); - services.AddSingleton(instance); - } - - internal static IServiceCollection AddLazySupport(this IServiceCollection services) - { - services.Replace(ServiceDescriptor.Transient(typeof(Lazy<>), typeof(LazyResolve<>))); - return services; - } + internal static IServiceCollection AddLazySupport(this IServiceCollection services) + { + services.Replace(ServiceDescriptor.Transient(typeof(Lazy<>), typeof(LazyResolve<>))); + return services; } } diff --git a/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs index 9bcc0cf7f8..9c2202e2aa 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs @@ -1,57 +1,55 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to the class. +/// +public static class ServiceProviderExtensions { /// - /// Provides extension methods to the class. + /// Creates an instance with arguments. /// - public static class ServiceProviderExtensions + /// The type of the instance. + /// The factory. + /// Arguments. + /// An instance of the specified type. + /// + /// Throws an exception if the factory failed to get an instance of the specified type. + /// The arguments are used as dependencies by the factory. + /// + public static T CreateInstance(this IServiceProvider serviceProvider, params object[] args) + where T : class + => (T)serviceProvider.CreateInstance(typeof(T), args); + + /// + /// Creates an instance of a service, with arguments. + /// + /// The + /// The type of the instance. + /// Named arguments. + /// An instance of the specified type. + /// + /// The instance type does not need to be registered into the factory. + /// + /// The arguments are used as dependencies by the factory. Other dependencies + /// are retrieved from the factory. + /// + /// + public static object CreateInstance(this IServiceProvider serviceProvider, Type type, params object[] args) + => ActivatorUtilities.CreateInstance(serviceProvider, type, args); + + [EditorBrowsable(EditorBrowsableState.Never)] + public static PublishedModelFactory CreateDefaultPublishedModelFactory(this IServiceProvider factory) { - /// - /// Creates an instance with arguments. - /// - /// The type of the instance. - /// The factory. - /// Arguments. - /// An instance of the specified type. - /// - /// Throws an exception if the factory failed to get an instance of the specified type. - /// The arguments are used as dependencies by the factory. - /// - public static T CreateInstance(this IServiceProvider serviceProvider, params object[] args) - where T : class - => (T)serviceProvider.CreateInstance(typeof(T), args); - - /// - /// Creates an instance of a service, with arguments. - /// - /// The - /// The type of the instance. - /// Named arguments. - /// An instance of the specified type. - /// - /// The instance type does not need to be registered into the factory. - /// The arguments are used as dependencies by the factory. Other dependencies - /// are retrieved from the factory. - /// - public static object CreateInstance(this IServiceProvider serviceProvider, Type type, params object[] args) - => ActivatorUtilities.CreateInstance(serviceProvider, type, args); - - [EditorBrowsable(EditorBrowsableState.Never)] - public static PublishedModelFactory CreateDefaultPublishedModelFactory(this IServiceProvider factory) - { - TypeLoader typeLoader = factory.GetRequiredService(); - IPublishedValueFallback publishedValueFallback = factory.GetRequiredService(); - IEnumerable types = typeLoader - .GetTypes() // element models - .Concat(typeLoader.GetTypes()); // content models - return new PublishedModelFactory(types, publishedValueFallback); - } + TypeLoader typeLoader = factory.GetRequiredService(); + IPublishedValueFallback publishedValueFallback = factory.GetRequiredService(); + IEnumerable types = typeLoader + .GetTypes() // element models + .Concat(typeLoader.GetTypes()); // content models + return new PublishedModelFactory(types, publishedValueFallback); } } diff --git a/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs index fdc4e3f622..6f8e4a2173 100644 --- a/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs +++ b/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs @@ -1,25 +1,25 @@ -using System; using System.ComponentModel; -namespace Umbraco.Cms.Web.Common.DependencyInjection +namespace Umbraco.Cms.Web.Common.DependencyInjection; + +/// +/// Service locator for internal (umbraco cms) only purposes. Should only be used if no other ways exist. +/// +/// +/// It is created with only two goals in mind +/// 1) Continue to have the same extension methods on IPublishedContent and IPublishedElement as in V8. To make +/// migration easier. +/// 2) To have a tool to avoid breaking changes in minor and patch versions. All methods using this should in theory be +/// obsolete. +/// Keep in mind, every time this is used, the code becomes basically untestable. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class StaticServiceProvider { /// - /// Service locator for internal (umbraco cms) only purposes. Should only be used if no other ways exist. + /// The service locator. /// - /// - /// It is created with only two goals in mind - /// 1) Continue to have the same extension methods on IPublishedContent and IPublishedElement as in V8. To make migration easier. - /// 2) To have a tool to avoid breaking changes in minor and patch versions. All methods using this should in theory be obsolete. - /// - /// Keep in mind, every time this is used, the code becomes basically untestable. - /// [EditorBrowsable(EditorBrowsableState.Never)] - public static class StaticServiceProvider - { - /// - /// The service locator. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static IServiceProvider Instance { get; set; } = null!; // This is set doing startup and will always exists after that - } + public static IServiceProvider Instance { get; set; } = + null!; // This is set doing startup and will always exists after that } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs index 5259c2a8a9..fc78d985f7 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs @@ -5,107 +5,106 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Sections; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Contains extensions methods for used for registering content apps. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Contains extensions methods for used for registering content apps. + /// Register a component. /// - public static partial class UmbracoBuilderExtensions + /// The type of the component. + /// The builder. + public static IUmbracoBuilder AddComponent(this IUmbracoBuilder builder) + where T : class, IComponent { - /// - /// Register a component. - /// - /// The type of the component. - /// The builder. - public static IUmbracoBuilder AddComponent(this IUmbracoBuilder builder) - where T : class, IComponent - { - builder.Components().Append(); - return builder; - } + builder.Components().Append(); + return builder; + } - /// - /// Register a content app. - /// - /// The type of the content app. - /// The builder. - public static IUmbracoBuilder AddContentApp(this IUmbracoBuilder builder) - where T : class, IContentAppFactory - { - builder.ContentApps().Append(); - return builder; - } + /// + /// Register a content app. + /// + /// The type of the content app. + /// The builder. + public static IUmbracoBuilder AddContentApp(this IUmbracoBuilder builder) + where T : class, IContentAppFactory + { + builder.ContentApps().Append(); + return builder; + } - /// - /// Register a content finder. - /// - /// The type of the content finder. - /// The builder. - public static IUmbracoBuilder AddContentFinder(this IUmbracoBuilder builder) - where T : class, IContentFinder - { - builder.ContentFinders().Append(); - return builder; - } + /// + /// Register a content finder. + /// + /// The type of the content finder. + /// The builder. + public static IUmbracoBuilder AddContentFinder(this IUmbracoBuilder builder) + where T : class, IContentFinder + { + builder.ContentFinders().Append(); + return builder; + } - /// - /// Register a dashboard. - /// - /// The type of the dashboard. - /// The builder. - public static IUmbracoBuilder AddDashboard(this IUmbracoBuilder builder) - where T : class, IDashboard - { - builder.Dashboards().Add(); - return builder; - } + /// + /// Register a dashboard. + /// + /// The type of the dashboard. + /// The builder. + public static IUmbracoBuilder AddDashboard(this IUmbracoBuilder builder) + where T : class, IDashboard + { + builder.Dashboards().Add(); + return builder; + } - /// - /// Register a media url provider. - /// - /// The type of the media url provider. - /// The builder. - public static IUmbracoBuilder AddMediaUrlProvider(this IUmbracoBuilder builder) - where T : class, IMediaUrlProvider - { - builder.MediaUrlProviders().Append(); - return builder; - } + /// + /// Register a media url provider. + /// + /// The type of the media url provider. + /// The builder. + public static IUmbracoBuilder AddMediaUrlProvider(this IUmbracoBuilder builder) + where T : class, IMediaUrlProvider + { + builder.MediaUrlProviders().Append(); + return builder; + } - /// - /// Register a embed provider. - /// - /// The type of the embed provider. - /// The builder. - public static IUmbracoBuilder AddEmbedProvider(this IUmbracoBuilder builder) - where T : class, IEmbedProvider - { - builder.EmbedProviders().Append(); - return builder; - } + /// + /// Register a embed provider. + /// + /// The type of the embed provider. + /// The builder. + public static IUmbracoBuilder AddEmbedProvider(this IUmbracoBuilder builder) + where T : class, IEmbedProvider + { + builder.EmbedProviders().Append(); + return builder; + } - /// - /// Register a section. - /// - /// The type of the section. - /// The builder. - public static IUmbracoBuilder AddSection(this IUmbracoBuilder builder) - where T : class, ISection - { - builder.Sections().Append(); - return builder; - } + /// + /// Register a section. + /// + /// The type of the section. + /// The builder. + public static IUmbracoBuilder AddSection(this IUmbracoBuilder builder) + where T : class, ISection + { + builder.Sections().Append(); + return builder; + } - /// - /// Register a url provider. - /// - /// The type of the url provider. - /// The Builder. - public static IUmbracoBuilder AddUrlProvider(this IUmbracoBuilder builder) - where T : class, IUrlProvider - { - builder.UrlProviders().Append(); - return builder; - } + /// + /// Register a url provider. + /// + /// The type of the url provider. + /// The Builder. + public static IUmbracoBuilder AddUrlProvider(this IUmbracoBuilder builder) + where T : class, IUrlProvider + { + builder.UrlProviders().Append(); + return builder; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 9133405f52..280d7ce492 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -13,273 +13,281 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Sections; +using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Tour; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.WebAssets; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Extension methods for +/// +public static partial class UmbracoBuilderExtensions { /// - /// Extension methods for + /// Adds all core collection builders /// - public static partial class UmbracoBuilderExtensions + internal static void AddAllCoreCollectionBuilders(this IUmbracoBuilder builder) { - /// - /// Adds all core collection builders - /// - internal static void AddAllCoreCollectionBuilders(this IUmbracoBuilder builder) - { - builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers()); - builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors()); - builder.Actions().Add(() => builder .TypeLoader.GetActions()); + builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers()); + builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors()); + builder.Actions().Add(() => builder .TypeLoader.GetActions()); - // register known content apps - builder.ContentApps() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); + // register known content apps + builder.ContentApps() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); - // all built-in finders in the correct order, - // devs can then modify this list on application startup - builder.ContentFinders() - .Append() - .Append() - .Append() - /*.Append() // disabled, this is an odd finder */ - .Append() - .Append(); - builder.EditorValidators().Add(() => builder.TypeLoader.GetTypes()); - builder.HealthChecks().Add(() => builder.TypeLoader.GetTypes()); - builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes()); - builder.TourFilters(); - builder.UrlProviders() - .Append() - .Append(); - builder.MediaUrlProviders() - .Append(); - // register back office sections in the order we want them rendered - builder.Sections() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - builder.Components(); - // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards - builder.Dashboards() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(builder.TypeLoader.GetTypes()); - builder.DataValueReferenceFactories(); - builder.PropertyValueConverters().Append(builder.TypeLoader.GetTypes()); - builder.UrlSegmentProviders().Append(); - builder.ManifestValueValidators() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(); - builder.ManifestFilters(); - builder.MediaUrlGenerators(); - // register OEmbed providers - no type scanning - all explicit opt-in of adding types, IEmbedProvider is not IDiscoverable - builder.EmbedProviders() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - builder.SearchableTrees().Add(() => builder.TypeLoader.GetTypes()); - builder.BackOfficeAssets(); - } - - /// - /// Gets the actions collection builder. - /// - /// The builder. - public static ActionCollectionBuilder Actions(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the content apps collection builder. - /// - /// The builder. - public static ContentAppFactoryCollectionBuilder ContentApps(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the content finders collection builder. - /// - /// The builder. - public static ContentFinderCollectionBuilder ContentFinders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the editor validators collection builder. - /// - /// The builder. - public static EditorValidatorCollectionBuilder EditorValidators(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the health checks collection builder. - /// - /// The builder. - public static HealthCheckCollectionBuilder HealthChecks(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - public static HealthCheckNotificationMethodCollectionBuilder HealthCheckNotificationMethods(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the TourFilters collection builder. - /// - public static TourFilterCollectionBuilder TourFilters(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the URL providers collection builder. - /// - /// The builder. - public static UrlProviderCollectionBuilder UrlProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the media url providers collection builder. - /// - /// The builder. - public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the backoffice sections/applications collection builder. - /// - /// The builder. - public static SectionCollectionBuilder Sections(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the components collection builder. - /// - public static ComponentCollectionBuilder Components(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the backoffice dashboards collection builder. - /// - /// The builder. - public static DashboardCollectionBuilder Dashboards(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the cache refreshers collection builder. - /// - /// The builder. - public static CacheRefresherCollectionBuilder CacheRefreshers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the map definitions collection builder. - /// - /// The builder. - public static MapDefinitionCollectionBuilder MapDefinitions(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the data editor collection builder. - /// - /// The builder. - public static DataEditorCollectionBuilder DataEditors(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the data value reference factory collection builder. - /// - /// The builder. - public static DataValueReferenceFactoryCollectionBuilder DataValueReferenceFactories(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the property value converters collection builder. - /// - /// The builder. - public static PropertyValueConverterCollectionBuilder PropertyValueConverters(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the url segment providers collection builder. - /// - /// The builder. - public static UrlSegmentProviderCollectionBuilder UrlSegmentProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the validators collection builder. - /// - /// The builder. - internal static ManifestValueValidatorCollectionBuilder ManifestValueValidators(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the manifest filter collection builder. - /// - /// The builder. - public static ManifestFilterCollectionBuilder ManifestFilters(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the content finders collection builder. - /// - /// The builder. - public static MediaUrlGeneratorCollectionBuilder MediaUrlGenerators(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the backoffice Embed Providers collection builder. - /// - /// The builder. - public static EmbedProvidersCollectionBuilder EmbedProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the back office searchable tree collection builder - /// - public static SearchableTreeCollectionBuilder SearchableTrees(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the back office custom assets collection builder - /// - public static CustomBackOfficeAssetsCollectionBuilder BackOfficeAssets(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); + // all built-in finders in the correct order, + // devs can then modify this list on application startup + builder.ContentFinders() + .Append() + .Append() + .Append() + /*.Append() // disabled, this is an odd finder */ + .Append() + .Append(); + builder.EditorValidators().Add(() => builder.TypeLoader.GetTypes()); + builder.HealthChecks().Add(() => builder.TypeLoader.GetTypes()); + builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes()); + builder.TourFilters(); + builder.UrlProviders() + .Append() + .Append(); + builder.MediaUrlProviders() + .Append(); + // register back office sections in the order we want them rendered + builder.Sections() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + builder.Components(); + // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards + builder.Dashboards() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(builder.TypeLoader.GetTypes()); + builder.PartialViewSnippets(); + builder.PartialViewMacroSnippets(); + builder.DataValueReferenceFactories(); + builder.PropertyValueConverters().Append(builder.TypeLoader.GetTypes()); + builder.UrlSegmentProviders().Append(); + builder.ManifestValueValidators() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); + builder.ManifestFilters(); + builder.MediaUrlGenerators(); + // register OEmbed providers - no type scanning - all explicit opt-in of adding types, IEmbedProvider is not IDiscoverable + builder.EmbedProviders() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + builder.SearchableTrees().Add(() => builder.TypeLoader.GetTypes()); + builder.BackOfficeAssets(); } + + /// + /// Gets the actions collection builder. + /// + /// The builder. + public static ActionCollectionBuilder Actions(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the content apps collection builder. + /// + /// The builder. + public static ContentAppFactoryCollectionBuilder ContentApps(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the content finders collection builder. + /// + /// The builder. + public static ContentFinderCollectionBuilder ContentFinders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the editor validators collection builder. + /// + /// The builder. + public static EditorValidatorCollectionBuilder EditorValidators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the health checks collection builder. + /// + /// The builder. + public static HealthCheckCollectionBuilder HealthChecks(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + public static HealthCheckNotificationMethodCollectionBuilder HealthCheckNotificationMethods(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the TourFilters collection builder. + /// + public static TourFilterCollectionBuilder TourFilters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the URL providers collection builder. + /// + /// The builder. + public static UrlProviderCollectionBuilder UrlProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the media url providers collection builder. + /// + /// The builder. + public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the backoffice sections/applications collection builder. + /// + /// The builder. + public static SectionCollectionBuilder Sections(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the components collection builder. + /// + public static ComponentCollectionBuilder Components(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the backoffice dashboards collection builder. + /// + /// The builder. + public static DashboardCollectionBuilder Dashboards(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + public static PartialViewSnippetCollectionBuilder? PartialViewSnippets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + public static PartialViewMacroSnippetCollectionBuilder? PartialViewMacroSnippets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// Gets the cache refreshers collection builder. + /// + /// The builder. + public static CacheRefresherCollectionBuilder CacheRefreshers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the map definitions collection builder. + /// + /// The builder. + public static MapDefinitionCollectionBuilder MapDefinitions(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the data editor collection builder. + /// + /// The builder. + public static DataEditorCollectionBuilder DataEditors(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the data value reference factory collection builder. + /// + /// The builder. + public static DataValueReferenceFactoryCollectionBuilder DataValueReferenceFactories(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the property value converters collection builder. + /// + /// The builder. + public static PropertyValueConverterCollectionBuilder PropertyValueConverters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the url segment providers collection builder. + /// + /// The builder. + public static UrlSegmentProviderCollectionBuilder UrlSegmentProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the validators collection builder. + /// + /// The builder. + internal static ManifestValueValidatorCollectionBuilder ManifestValueValidators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the manifest filter collection builder. + /// + /// The builder. + public static ManifestFilterCollectionBuilder ManifestFilters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the content finders collection builder. + /// + /// The builder. + public static MediaUrlGeneratorCollectionBuilder MediaUrlGenerators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the backoffice Embed Providers collection builder. + /// + /// The builder. + public static EmbedProvidersCollectionBuilder EmbedProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the back office searchable tree collection builder + /// + public static SearchableTreeCollectionBuilder SearchableTrees(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the back office custom assets collection builder + /// + public static CustomBackOfficeAssetsCollectionBuilder BackOfficeAssets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs index 81a1bbac32..e3a659056b 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs @@ -1,26 +1,24 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Extension methods for +/// +public static partial class UmbracoBuilderExtensions { /// - /// Extension methods for + /// Adds Umbraco composers for plugins /// - public static partial class UmbracoBuilderExtensions + public static IUmbracoBuilder AddComposers(this IUmbracoBuilder builder) { - /// - /// Adds Umbraco composers for plugins - /// - public static IUmbracoBuilder AddComposers(this IUmbracoBuilder builder) - { - IEnumerable composerTypes = builder.TypeLoader.GetTypes(); - IEnumerable enableDisable = builder.TypeLoader.GetAssemblyAttributes(typeof(EnableComposerAttribute), typeof(DisableComposerAttribute)); + IEnumerable composerTypes = builder.TypeLoader.GetTypes(); + IEnumerable enableDisable = + builder.TypeLoader.GetAssemblyAttributes(typeof(EnableComposerAttribute), typeof(DisableComposerAttribute)); - new ComposerGraph(builder, composerTypes, enableDisable, builder.BuilderLoggerFactory.CreateLogger()).Compose(); + new ComposerGraph(builder, composerTypes, enableDisable, builder.BuilderLoggerFactory.CreateLogger()).Compose(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 6586230081..4c7d6490e0 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -1,5 +1,4 @@ using System.Reflection; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; @@ -17,13 +16,13 @@ public static partial class UmbracoBuilderExtensions private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action>? configure = null) where TOptions : class { - var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); + UmbracoOptionsAttribute? umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); if (umbracoOptionsAttribute is null) { throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute."); } - var optionsBuilder = builder.Services.AddOptions() + OptionsBuilder? optionsBuilder = builder.Services.AddOptions() .Bind( builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties) @@ -83,9 +82,12 @@ public static partial class UmbracoBuilderExtensions .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); + // Configure connection string and ensure it's updated when the configuration changes builder.Services.AddSingleton, ConfigureConnectionStrings>(); + builder.Services.AddSingleton, ConfigurationChangeTokenSource>(); builder.Services.Configure( Constants.Configuration.NamedOptions.InstallDefaultData.Languages, diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs index 441bc836da..844c52a5ab 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -5,72 +5,80 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Contains extensions methods for used for registering event handlers. +/// +public static partial class UmbracoBuilderExtensions { + /// + /// Registers a notification handler against the Umbraco service collection. + /// + /// The type of notification. + /// The type of notificiation handler. + /// The Umbraco builder. + /// The . + public static IUmbracoBuilder AddNotificationHandler( + this IUmbracoBuilder builder) + where TNotificationHandler : INotificationHandler + where TNotification : INotification + { + builder.Services.AddNotificationHandler(); + return builder; + } /// - /// Contains extensions methods for used for registering event handlers. + /// Registers a notification async handler against the Umbraco service collection. /// - public static partial class UmbracoBuilderExtensions + /// The type of notification. + /// The type of notification async handler. + /// The Umbraco builder. + /// The . + public static IUmbracoBuilder AddNotificationAsyncHandler( + this IUmbracoBuilder builder) + where TNotificationAsyncHandler : INotificationAsyncHandler + where TNotification : INotification { - /// - /// Registers a notification handler against the Umbraco service collection. - /// - /// The type of notification. - /// The type of notificiation handler. - /// The Umbraco builder. - /// The . - public static IUmbracoBuilder AddNotificationHandler(this IUmbracoBuilder builder) - where TNotificationHandler : INotificationHandler - where TNotification : INotification + builder.Services.AddNotificationAsyncHandler(); + return builder; + } + + internal static IServiceCollection AddNotificationHandler( + this IServiceCollection services) + where TNotificationHandler : INotificationHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + var descriptor = new UniqueServiceDescriptor( + typeof(INotificationHandler), + typeof(TNotificationHandler), + ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) { - builder.Services.AddNotificationHandler(); - return builder; + services.Add(descriptor); } - /// - /// Registers a notification async handler against the Umbraco service collection. - /// - /// The type of notification. - /// The type of notification async handler. - /// The Umbraco builder. - /// The . - public static IUmbracoBuilder AddNotificationAsyncHandler(this IUmbracoBuilder builder) - where TNotificationAsyncHandler : INotificationAsyncHandler - where TNotification : INotification + return services; + } + + internal static IServiceCollection AddNotificationAsyncHandler( + this IServiceCollection services) + where TNotificationAsyncHandler : INotificationAsyncHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + var descriptor = new ServiceDescriptor( + typeof(INotificationAsyncHandler), + typeof(TNotificationAsyncHandler), + ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) { - builder.Services.AddNotificationAsyncHandler(); - return builder; + services.Add(descriptor); } - internal static IServiceCollection AddNotificationHandler(this IServiceCollection services) - where TNotificationHandler : INotificationHandler - where TNotification : INotification - { - // Register the handler as transient. This ensures that anything can be injected into it. - var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); - - if (!services.Contains(descriptor)) - { - services.Add(descriptor); - } - - return services; - } - - internal static IServiceCollection AddNotificationAsyncHandler(this IServiceCollection services) - where TNotificationAsyncHandler : INotificationAsyncHandler - where TNotification : INotification - { - // Register the handler as transient. This ensures that anything can be injected into it. - var descriptor = new ServiceDescriptor(typeof(INotificationAsyncHandler), typeof(TNotificationAsyncHandler), ServiceLifetime.Transient); - - if (!services.Contains(descriptor)) - { - services.Add(descriptor); - } - - return services; - } + return services; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 84ade2837d..9d314526a9 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -35,6 +35,7 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; @@ -186,9 +187,6 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); - // by default, register a noop factory - Services.AddUnique(); - Services.AddUnique(); Services.AddSingleton(f => f.GetRequiredService().CreateDictionary()); @@ -321,6 +319,11 @@ namespace Umbraco.Cms.Core.DependencyInjection // Register a noop IHtmlSanitizer to be replaced Services.AddUnique(); + + Services.AddUnique(); + Services.AddUnique(); + + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs b/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs index 538f3f1dda..57a8bcfe99 100644 --- a/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs +++ b/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs @@ -1,58 +1,68 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// A custom that supports unique checking +/// +/// +/// This is required because the default implementation doesn't implement Equals or GetHashCode. +/// see: https://github.com/dotnet/runtime/issues/47262 +/// +public sealed class UniqueServiceDescriptor : ServiceDescriptor, IEquatable { /// - /// A custom that supports unique checking + /// Initializes a new instance of the class. /// - /// - /// This is required because the default implementation doesn't implement Equals or GetHashCode. - /// see: https://github.com/dotnet/runtime/issues/47262 - /// - public sealed class UniqueServiceDescriptor : ServiceDescriptor, IEquatable + public UniqueServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime) + : base(serviceType, implementationType, lifetime) { - /// - /// Initializes a new instance of the class. - /// - public UniqueServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime) - : base(serviceType, implementationType, lifetime) + } + + /// + public bool Equals(UniqueServiceDescriptor? other) => other != null && Lifetime == other.Lifetime && + EqualityComparer.Default.Equals( + ServiceType, + other.ServiceType) && + EqualityComparer.Default.Equals( + ImplementationType, + other.ImplementationType) && + EqualityComparer.Default.Equals( + ImplementationInstance, other.ImplementationInstance) && + EqualityComparer?>.Default + .Equals( + ImplementationFactory, + other.ImplementationFactory); + + /// + public override bool Equals(object? obj) => Equals(obj as UniqueServiceDescriptor); + + /// + public override int GetHashCode() + { + var hashCode = 493849952; + hashCode = (hashCode * -1521134295) + Lifetime.GetHashCode(); + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ServiceType); + + if (ImplementationType is not null) { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationType); } - /// - public override bool Equals(object? obj) => Equals(obj as UniqueServiceDescriptor); - - /// - public bool Equals(UniqueServiceDescriptor? other) => other != null && Lifetime == other.Lifetime && EqualityComparer.Default.Equals(ServiceType, other.ServiceType) && EqualityComparer.Default.Equals(ImplementationType, other.ImplementationType) && EqualityComparer.Default.Equals(ImplementationInstance, other.ImplementationInstance) && EqualityComparer?>.Default.Equals(ImplementationFactory, other.ImplementationFactory); - - /// - public override int GetHashCode() + if (ImplementationInstance is not null) { - int hashCode = 493849952; - hashCode = (hashCode * -1521134295) + Lifetime.GetHashCode(); - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ServiceType); - - if (ImplementationType is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationType); - } - - if (ImplementationInstance is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationInstance); - } - - if (ImplementationFactory is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer?>.Default.GetHashCode(ImplementationFactory); - } - - return hashCode; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationInstance); } + + if (ImplementationFactory is not null) + { + hashCode = (hashCode * -1521134295) + + EqualityComparer?>.Default.GetHashCode(ImplementationFactory); + } + + return hashCode; } } diff --git a/src/Umbraco.Core/Deploy/ArtifactBase.cs b/src/Umbraco.Core/Deploy/ArtifactBase.cs index 200b47096d..cc2415f4cd 100644 --- a/src/Umbraco.Core/Deploy/ArtifactBase.cs +++ b/src/Umbraco.Core/Deploy/ArtifactBase.cs @@ -21,8 +21,6 @@ namespace Umbraco.Cms.Core.Deploy protected abstract string GetChecksum(); - #region Abstract implementation of IArtifactSignature - Udi IArtifactSignature.Udi => Udi; public TUdi Udi { get; set; } @@ -45,8 +43,6 @@ namespace Umbraco.Cms.Core.Deploy set => _dependencies = value.OrderBy(x => x.Udi); } - #endregion - public string Name { get; set; } public string Alias { get; set; } = string.Empty; diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs index 618400e395..07ba917dc2 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependency.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -1,40 +1,42 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represents an artifact dependency. +/// +/// +/// Dependencies have an order property which indicates whether it must be respected when ordering artifacts. +/// +/// Dependencies have a mode which can be Match or Exist depending on whether the checksum should +/// match. +/// +/// +public class ArtifactDependency { /// - /// Represents an artifact dependency. + /// Initializes a new instance of the ArtifactDependency class with an entity identifier and a mode. /// - /// - /// Dependencies have an order property which indicates whether it must be respected when ordering artifacts. - /// Dependencies have a mode which can be Match or Exist depending on whether the checksum should match. - /// - public class ArtifactDependency + /// The entity identifier of the artifact that is a dependency. + /// A value indicating whether the dependency is ordering. + /// The dependency mode. + public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode) { - /// - /// Initializes a new instance of the ArtifactDependency class with an entity identifier and a mode. - /// - /// The entity identifier of the artifact that is a dependency. - /// A value indicating whether the dependency is ordering. - /// The dependency mode. - public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode) - { - Udi = udi; - Ordering = ordering; - Mode = mode; - } - - /// - /// Gets the entity id of the artifact that is a dependency. - /// - public Udi Udi { get; private set; } - - /// - /// Gets a value indicating whether the dependency is ordering. - /// - public bool Ordering { get; private set; } - - /// - /// Gets the dependency mode. - /// - public ArtifactDependencyMode Mode { get; private set; } + Udi = udi; + Ordering = ordering; + Mode = mode; } + + /// + /// Gets the entity id of the artifact that is a dependency. + /// + public Udi Udi { get; } + + /// + /// Gets a value indicating whether the dependency is ordering. + /// + public bool Ordering { get; } + + /// + /// Gets the dependency mode. + /// + public ArtifactDependencyMode Mode { get; } } diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs index a5fff53800..1be524c86f 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -1,69 +1,44 @@ -using System; using System.Collections; -using System.Collections.Generic; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represents a collection of distinct . +/// +/// The collection cannot contain duplicates and modes are properly managed. +public class ArtifactDependencyCollection : ICollection { - /// - /// Represents a collection of distinct . - /// - /// The collection cannot contain duplicates and modes are properly managed. - public class ArtifactDependencyCollection : ICollection + private readonly Dictionary _dependencies = new(); + + public int Count => _dependencies.Count; + + public IEnumerator GetEnumerator() => _dependencies.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(ArtifactDependency item) { - private readonly Dictionary _dependencies - = new Dictionary(); - - public IEnumerator GetEnumerator() + if (_dependencies.ContainsKey(item.Udi)) { - return _dependencies.Values.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public void Add(ArtifactDependency item) - { - if (_dependencies.ContainsKey(item.Udi)) + ArtifactDependency exist = _dependencies[item.Udi]; + if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode) { - var exist = _dependencies[item.Udi]; - if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode) - return; + return; } - - _dependencies[item.Udi] = item; } - public void Clear() - { - _dependencies.Clear(); - } - - public bool Contains(ArtifactDependency item) - { - return _dependencies.ContainsKey(item.Udi) && - (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); - } - - public void CopyTo(ArtifactDependency[] array, int arrayIndex) - { - _dependencies.Values.CopyTo(array, arrayIndex); - } - - public bool Remove(ArtifactDependency item) - { - throw new NotSupportedException(); - } - - public int Count - { - get { return _dependencies.Count; } - } - - public bool IsReadOnly - { - get { return false; } - } + _dependencies[item.Udi] = item; } + + public void Clear() => _dependencies.Clear(); + + public bool Contains(ArtifactDependency item) => + _dependencies.ContainsKey(item.Udi) && + (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); + + public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex); + + public bool Remove(ArtifactDependency item) => throw new NotSupportedException(); + + public bool IsReadOnly => false; } diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs index 7a2d108a13..b997b9c759 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Indicates the mode of the dependency. +/// +public enum ArtifactDependencyMode { /// - /// Indicates the mode of the dependency. + /// The dependency must match exactly. /// - public enum ArtifactDependencyMode - { - /// - /// The dependency must match exactly. - /// - Match, + Match, - /// - /// The dependency must exist. - /// - Exist - } + /// + /// The dependency must exist. + /// + Exist, } diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs index 0849f3526f..1b75fe11c0 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs @@ -1,47 +1,46 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represent the state of an artifact being deployed. +/// +public abstract class ArtifactDeployState { /// - /// Represent the state of an artifact being deployed. + /// Gets the artifact. /// - public abstract class ArtifactDeployState - { - /// - /// Creates a new instance of the class from an artifact and an entity. - /// - /// The type of the artifact. - /// The type of the entity. - /// The artifact. - /// The entity. - /// The service connector deploying the artifact. - /// The next pass number. - /// A deploying artifact. - public static ArtifactDeployState Create(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) - where TArtifact : IArtifact - { - return new ArtifactDeployState(art, entity, connector, nextPass); - } + public IArtifact Artifact => GetArtifactAsIArtifact(); - /// - /// Gets the artifact. - /// - public IArtifact Artifact => GetArtifactAsIArtifact(); + /// + /// Gets or sets the service connector in charge of deploying the artifact. + /// + public IServiceConnector? Connector { get; set; } - /// - /// Gets the artifact as an . - /// - /// The artifact, as an . - /// This is because classes that inherit from this class cannot override the Artifact property - /// with a property that specializes the return type, and so they need to 'new' the property. - protected abstract IArtifact GetArtifactAsIArtifact(); + /// + /// Gets or sets the next pass number. + /// + public int NextPass { get; set; } - /// - /// Gets or sets the service connector in charge of deploying the artifact. - /// - public IServiceConnector? Connector { get; set; } + /// + /// Creates a new instance of the class from an artifact and an entity. + /// + /// The type of the artifact. + /// The type of the entity. + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + /// A deploying artifact. + public static ArtifactDeployState Create(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) + where TArtifact : IArtifact => + new ArtifactDeployState(art, entity, connector, nextPass); - /// - /// Gets or sets the next pass number. - /// - public int NextPass { get; set; } - } + /// + /// Gets the artifact as an . + /// + /// The artifact, as an . + /// + /// This is because classes that inherit from this class cannot override the Artifact property + /// with a property that specializes the return type, and so they need to 'new' the property. + /// + protected abstract IArtifact GetArtifactAsIArtifact(); } diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs index 72724ee57b..0ff1e20e87 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs @@ -1,42 +1,38 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represent the state of an artifact being deployed. +/// +/// The type of the artifact. +/// The type of the entity. +public class ArtifactDeployState : ArtifactDeployState + where TArtifact : IArtifact { /// - /// Represent the state of an artifact being deployed. + /// Initializes a new instance of the class. /// - /// The type of the artifact. - /// The type of the entity. - public class ArtifactDeployState : ArtifactDeployState - where TArtifact : IArtifact + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + public ArtifactDeployState(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) { - /// - /// Initializes a new instance of the class. - /// - /// The artifact. - /// The entity. - /// The service connector deploying the artifact. - /// The next pass number. - public ArtifactDeployState(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) - { - Artifact = art; - Entity = entity; - Connector = connector; - NextPass = nextPass; - } - - /// - /// Gets or sets the artifact. - /// - public new TArtifact Artifact { get; set; } - - /// - /// Gets or sets the entity. - /// - public TEntity? Entity { get; set; } - - /// - protected sealed override IArtifact GetArtifactAsIArtifact() - { - return Artifact; - } + Artifact = art; + Entity = entity; + Connector = connector; + NextPass = nextPass; } + + /// + /// Gets or sets the artifact. + /// + public new TArtifact Artifact { get; set; } + + /// + /// Gets or sets the entity. + /// + public TEntity? Entity { get; set; } + + /// + protected sealed override IArtifact GetArtifactAsIArtifact() => Artifact; } diff --git a/src/Umbraco.Core/Deploy/ArtifactSignature.cs b/src/Umbraco.Core/Deploy/ArtifactSignature.cs index 629d65593c..3dccddba29 100644 --- a/src/Umbraco.Core/Deploy/ArtifactSignature.cs +++ b/src/Umbraco.Core/Deploy/ArtifactSignature.cs @@ -1,21 +1,17 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +public sealed class ArtifactSignature : IArtifactSignature { - public sealed class ArtifactSignature : IArtifactSignature + public ArtifactSignature(Udi udi, string checksum, IEnumerable? dependencies = null) { - public ArtifactSignature(Udi udi, string checksum, IEnumerable? dependencies = null) - { - Udi = udi; - Checksum = checksum; - Dependencies = dependencies ?? Enumerable.Empty(); - } - - public Udi Udi { get; private set; } - - public string Checksum { get; private set; } - - public IEnumerable Dependencies { get; private set; } + Udi = udi; + Checksum = checksum; + Dependencies = dependencies ?? Enumerable.Empty(); } + + public Udi Udi { get; } + + public string Checksum { get; } + + public IEnumerable Dependencies { get; } } diff --git a/src/Umbraco.Core/Deploy/Difference.cs b/src/Umbraco.Core/Deploy/Difference.cs index be0c086c0b..d704642a9f 100644 --- a/src/Umbraco.Core/Deploy/Difference.cs +++ b/src/Umbraco.Core/Deploy/Difference.cs @@ -1,28 +1,38 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +public class Difference { - public class Difference + public Difference(string title, string? text = null, string? category = null) { - public Difference(string title, string? text = null, string? category = null) + Title = title; + Text = text; + Category = category; + } + + public string Title { get; set; } + + public string? Text { get; set; } + + public string? Category { get; set; } + + public override string ToString() + { + var s = Title; + if (!string.IsNullOrWhiteSpace(Category)) { - Title = title; - Text = text; - Category = category; + s += string.Format("[{0}]", Category); } - public string Title { get; set; } - public string? Text { get; set; } - public string? Category { get; set; } - - public override string ToString() + if (!string.IsNullOrWhiteSpace(Text)) { - var s = Title; - if (!string.IsNullOrWhiteSpace(Category)) s += string.Format("[{0}]", Category); - if (!string.IsNullOrWhiteSpace(Text)) + if (s.Length > 0) { - if (s.Length > 0) s += ":"; - s += Text; + s += ":"; } - return s; + + s += Text; } + + return s; } } diff --git a/src/Umbraco.Core/Deploy/Direction.cs b/src/Umbraco.Core/Deploy/Direction.cs index 7a6ee5ae09..30439380f2 100644 --- a/src/Umbraco.Core/Deploy/Direction.cs +++ b/src/Umbraco.Core/Deploy/Direction.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +public enum Direction { - public enum Direction - { - ToArtifact, - FromArtifact - } + ToArtifact, + FromArtifact, } diff --git a/src/Umbraco.Core/Deploy/IArtifact.cs b/src/Umbraco.Core/Deploy/IArtifact.cs index 5eb9c079f3..faea983dee 100644 --- a/src/Umbraco.Core/Deploy/IArtifact.cs +++ b/src/Umbraco.Core/Deploy/IArtifact.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represents an artifact ie an object that can be transfered between environments. +/// +public interface IArtifact : IArtifactSignature { - /// - /// Represents an artifact ie an object that can be transfered between environments. - /// - public interface IArtifact : IArtifactSignature - { - string Name { get; } - string? Alias { get; } - } + string Name { get; } + + string? Alias { get; } } diff --git a/src/Umbraco.Core/Deploy/IArtifactSignature.cs b/src/Umbraco.Core/Deploy/IArtifactSignature.cs index 695624cd86..f1dd35295f 100644 --- a/src/Umbraco.Core/Deploy/IArtifactSignature.cs +++ b/src/Umbraco.Core/Deploy/IArtifactSignature.cs @@ -1,41 +1,46 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +/// +/// Represents the signature of an artifact. +/// +public interface IArtifactSignature { /// - /// Represents the signature of an artifact. + /// Gets the entity unique identifier of this artifact. /// - public interface IArtifactSignature - { - /// - /// Gets the entity unique identifier of this artifact. - /// - /// - /// The project identifier is independent from the state of the artifact, its data - /// values, dependencies, anything. It never changes and fully identifies the artifact. - /// What an entity uses as a unique identifier will influence what we can transfer - /// between environments. Eg content type "Foo" on one environment is not necessarily the - /// same as "Foo" on another environment, if guids are used as unique identifiers. What is - /// used should be documented for each entity, along with the consequences of the choice. - /// - Udi Udi { get; } + /// + /// + /// The project identifier is independent from the state of the artifact, its data + /// values, dependencies, anything. It never changes and fully identifies the artifact. + /// + /// + /// What an entity uses as a unique identifier will influence what we can transfer + /// between environments. Eg content type "Foo" on one environment is not necessarily the + /// same as "Foo" on another environment, if guids are used as unique identifiers. What is + /// used should be documented for each entity, along with the consequences of the choice. + /// + /// + Udi Udi { get; } - /// - /// Gets the checksum of this artifact. - /// - /// - /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies, - /// but not on their checksums. So the checksum changes when any of the artifact's properties changes, - /// or when the list of dependencies changes. But not if one of these dependencies change. - /// It is assumed that checksum collisions cannot happen ie that no two different artifact's - /// states will ever produce the same checksum, so that if two artifacts have the same checksum then - /// they are identical. - /// - string Checksum { get; } + /// + /// Gets the checksum of this artifact. + /// + /// + /// + /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies, + /// but not on their checksums. So the checksum changes when any of the artifact's properties changes, + /// or when the list of dependencies changes. But not if one of these dependencies change. + /// + /// + /// It is assumed that checksum collisions cannot happen ie that no two different artifact's + /// states will ever produce the same checksum, so that if two artifacts have the same checksum then + /// they are identical. + /// + /// + string Checksum { get; } - /// - /// Gets the dependencies of this artifact. - /// - IEnumerable Dependencies { get; } - } + /// + /// Gets the dependencies of this artifact. + /// + IEnumerable Dependencies { get; } } diff --git a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs index 87a00e7969..6b91926b57 100644 --- a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs +++ b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs @@ -1,34 +1,37 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Defines methods that can convert data type configuration to / from an environment-agnostic string. +/// +/// +/// Configuration may contain values such as content identifiers, that would be local +/// to one environment, and need to be converted in order to be deployed. +/// +[SuppressMessage( + "ReSharper", + "UnusedMember.Global", + Justification = "This is actual only used by Deploy, but we don't want third parties to have references on deploy, that's why this interface is part of core.")] +public interface IDataTypeConfigurationConnector { /// - /// Defines methods that can convert data type configuration to / from an environment-agnostic string. + /// Gets the property editor aliases that the value converter supports by default. /// - /// Configuration may contain values such as content identifiers, that would be local - /// to one environment, and need to be converted in order to be deployed. - [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is actual only used by Deploy, but we don't want third parties to have references on deploy, that's why this interface is part of core.")] - public interface IDataTypeConfigurationConnector - { - /// - /// Gets the property editor aliases that the value converter supports by default. - /// - IEnumerable PropertyEditorAliases { get; } + IEnumerable PropertyEditorAliases { get; } - /// - /// Gets the artifact datatype configuration corresponding to the actual datatype configuration. - /// - /// The datatype. - /// The dependencies. - string? ToArtifact(IDataType dataType, ICollection dependencies); + /// + /// Gets the artifact datatype configuration corresponding to the actual datatype configuration. + /// + /// The datatype. + /// The dependencies. + string? ToArtifact(IDataType dataType, ICollection dependencies); - /// - /// Gets the actual datatype configuration corresponding to the artifact configuration. - /// - /// The datatype. - /// The artifact configuration. - object? FromArtifact(IDataType dataType, string? configuration); - } + /// + /// Gets the actual datatype configuration corresponding to the artifact configuration. + /// + /// The datatype. + /// The artifact configuration. + object? FromArtifact(IDataType dataType, string? configuration); } diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs index c6e2da997b..bdc8fd8d61 100644 --- a/src/Umbraco.Core/Deploy/IDeployContext.cs +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -1,47 +1,44 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +/// +/// Represents a deployment context. +/// +public interface IDeployContext { /// - /// Represents a deployment context. + /// Gets the unique identifier of the deployment. /// - public interface IDeployContext - { - /// - /// Gets the unique identifier of the deployment. - /// - Guid SessionId { get; } + Guid SessionId { get; } - /// - /// Gets the file source. - /// - /// The file source is used to obtain files from the source environment. - IFileSource FileSource { get; } + /// + /// Gets the file source. + /// + /// The file source is used to obtain files from the source environment. + IFileSource FileSource { get; } - /// - /// Gets the next number in a numerical sequence. - /// - /// The next sequence number. - /// Can be used to uniquely number things during a deployment. - int NextSeq(); + /// + /// Gets items. + /// + IDictionary Items { get; } - /// - /// Gets items. - /// - IDictionary Items { get; } + /// + /// Gets the next number in a numerical sequence. + /// + /// The next sequence number. + /// Can be used to uniquely number things during a deployment. + int NextSeq(); - /// - /// Gets item. - /// - /// The type of the item. - /// The key of the item. - /// The item with the specified key and type, if any, else null. - T? Item(string key) where T : class; + /// + /// Gets item. + /// + /// The type of the item. + /// The key of the item. + /// The item with the specified key and type, if any, else null. + T? Item(string key) + where T : class; - ///// - ///// Gets the global deployment cancellation token. - ///// - //CancellationToken CancellationToken { get; } - } + ///// + ///// Gets the global deployment cancellation token. + ///// + // CancellationToken CancellationToken { get; } } diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs index 6e582803a2..ed169b9df5 100644 --- a/src/Umbraco.Core/Deploy/IFileSource.cs +++ b/src/Umbraco.Core/Deploy/IFileSource.cs @@ -1,91 +1,85 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +/// +/// Represents a file source, ie a mean for a target environment involved in a +/// deployment to obtain the content of files being deployed. +/// +public interface IFileSource { /// - /// Represents a file source, ie a mean for a target environment involved in a - /// deployment to obtain the content of files being deployed. + /// Gets the content of a file as a stream. /// - public interface IFileSource - { - /// - /// Gets the content of a file as a stream. - /// - /// A file entity identifier. - /// A stream with read access to the file content. - /// - /// Returns null if no content could be read. - /// The caller should ensure that the stream is properly closed/disposed. - /// - Stream GetFileStream(StringUdi udi); + /// A file entity identifier. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Stream GetFileStream(StringUdi udi); - /// - /// Gets the content of a file as a stream. - /// - /// A file entity identifier. - /// A cancellation token. - /// A stream with read access to the file content. - /// - /// Returns null if no content could be read. - /// The caller should ensure that the stream is properly closed/disposed. - /// - Task GetFileStreamAsync(StringUdi udi, CancellationToken token); + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A cancellation token. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Task GetFileStreamAsync(StringUdi udi, CancellationToken token); - /// - /// Gets the content of a file as a string. - /// - /// A file entity identifier. - /// A string containing the file content. - /// Returns null if no content could be read. - string GetFileContent(StringUdi udi); + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A string containing the file content. + /// Returns null if no content could be read. + string GetFileContent(StringUdi udi); - /// - /// Gets the content of a file as a string. - /// - /// A file entity identifier. - /// A cancellation token. - /// A string containing the file content. - /// Returns null if no content could be read. - Task GetFileContentAsync(StringUdi udi, CancellationToken token); + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A cancellation token. + /// A string containing the file content. + /// Returns null if no content could be read. + Task GetFileContentAsync(StringUdi udi, CancellationToken token); - /// - /// Gets the length of a file. - /// - /// A file entity identifier. - /// The length of the file, or -1 if the file does not exist. - long GetFileLength(StringUdi udi); + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// The length of the file, or -1 if the file does not exist. + long GetFileLength(StringUdi udi); - /// - /// Gets the length of a file. - /// - /// A file entity identifier. - /// A cancellation token. - /// The length of the file, or -1 if the file does not exist. - Task GetFileLengthAsync(StringUdi udi, CancellationToken token); + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// A cancellation token. + /// The length of the file, or -1 if the file does not exist. + Task GetFileLengthAsync(StringUdi udi, CancellationToken token); - /// - /// Gets files and store them using a file store. - /// - /// The udis of the files to get. - /// A collection of file types which can store the files. - void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes); + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes); - /// - /// Gets files and store them using a file store. - /// - /// The udis of the files to get. - /// A collection of file types which can store the files. - /// A cancellation token. - Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token); + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + /// A cancellation token. + Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token); - ///// - ///// Gets the content of a file as a bytes array. - ///// - ///// A file entity identifier. - ///// A byte array containing the file content. - //byte[] GetFileBytes(StringUdi Udi); - } + ///// + ///// Gets the content of a file as a bytes array. + ///// + ///// A file entity identifier. + ///// A byte array containing the file content. + // byte[] GetFileBytes(StringUdi Udi); } diff --git a/src/Umbraco.Core/Deploy/IFileType.cs b/src/Umbraco.Core/Deploy/IFileType.cs index ef6c44e1e6..466c87a3ed 100644 --- a/src/Umbraco.Core/Deploy/IFileType.cs +++ b/src/Umbraco.Core/Deploy/IFileType.cs @@ -1,32 +1,27 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +public interface IFileType { - public interface IFileType - { - Stream GetStream(StringUdi udi); + bool CanSetPhysical { get; } - Task GetStreamAsync(StringUdi udi, CancellationToken token); + Stream GetStream(StringUdi udi); - Stream GetChecksumStream(StringUdi udi); + Task GetStreamAsync(StringUdi udi, CancellationToken token); - long GetLength(StringUdi udi); + Stream GetChecksumStream(StringUdi udi); - void SetStream(StringUdi udi, Stream stream); + long GetLength(StringUdi udi); - Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); + void SetStream(StringUdi udi, Stream stream); - bool CanSetPhysical { get; } + Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); - void Set(StringUdi udi, string physicalPath, bool copy = false); + void Set(StringUdi udi, string physicalPath, bool copy = false); - // this is not pretty as *everywhere* in Deploy we take care of ignoring - // the physical path and always rely on Core's virtual IFileSystem but - // Cloud wants to add some of these files to Git and needs the path... - string GetPhysicalPath(StringUdi udi); + // this is not pretty as *everywhere* in Deploy we take care of ignoring + // the physical path and always rely on Core's virtual IFileSystem but + // Cloud wants to add some of these files to Git and needs the path... + string GetPhysicalPath(StringUdi udi); - string GetVirtualPath(StringUdi udi); - } + string GetVirtualPath(StringUdi udi); } diff --git a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs index d19d2ad64a..2ae2bb4bb9 100644 --- a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs +++ b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Deploy -{ - public interface IFileTypeCollection - { - IFileType this[string entityType] { get; } +namespace Umbraco.Cms.Core.Deploy; - bool Contains(string entityType); - } +public interface IFileTypeCollection +{ + IFileType this[string entityType] { get; } + + bool Contains(string entityType); } diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs index 084ba1b118..7b9e3f5e96 100644 --- a/src/Umbraco.Core/Deploy/IImageSourceParser.cs +++ b/src/Umbraco.Core/Deploy/IImageSourceParser.cs @@ -1,25 +1,24 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Provides methods to parse image tag sources in property values. +/// +public interface IImageSourceParser { /// - /// Provides methods to parse image tag sources in property values. + /// Parses an Umbraco property value and produces an artifact property value. /// - public interface IImageSourceParser - { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// The property value. - /// A list of dependencies. - /// The parsed value. - /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. - string? ToArtifact(string? value, ICollection dependencies); + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. + string? ToArtifact(string? value, ICollection dependencies); - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// The artifact property value. - /// The parsed value. - /// Turns umb://media/... into /media/.... - string? FromArtifact(string? value); - } + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns umb://media/... into /media/.... + string? FromArtifact(string? value); } diff --git a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs index 5883f73217..7ec3fff0fa 100644 --- a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs +++ b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Provides methods to parse local link tags in property values. +/// +public interface ILocalLinkParser { /// - /// Provides methods to parse local link tags in property values. + /// Parses an Umbraco property value and produces an artifact property value. /// - public interface ILocalLinkParser - { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// The property value. - /// A list of dependencies. - /// The parsed value. - /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the dependencies. - string ToArtifact(string value, ICollection dependencies); + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// + /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the + /// dependencies. + /// + string ToArtifact(string value, ICollection dependencies); - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// The artifact property value. - /// The parsed value. - /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. - string FromArtifact(string value); - } + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. + string FromArtifact(string value); } diff --git a/src/Umbraco.Core/Deploy/IMacroParser.cs b/src/Umbraco.Core/Deploy/IMacroParser.cs index 81b014c1cc..1945b2bdb3 100644 --- a/src/Umbraco.Core/Deploy/IMacroParser.cs +++ b/src/Umbraco.Core/Deploy/IMacroParser.cs @@ -1,32 +1,29 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +public interface IMacroParser { - public interface IMacroParser - { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// Property value. - /// A list of dependencies. - /// Parsed value. - string? ToArtifact(string? value, ICollection dependencies); + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// Property value. + /// A list of dependencies. + /// Parsed value. + string? ToArtifact(string? value, ICollection dependencies); - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// Artifact property value. - /// Parsed value. - string? FromArtifact(string? value); + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// Artifact property value. + /// Parsed value. + string? FromArtifact(string? value); - /// - /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier. - /// - /// Value to attempt to convert - /// Alias of the editor used for the parameter - /// Collection to add dependencies to when performing ToArtifact - /// Indicates which action is being performed (to or from artifact) - /// Value with converted identifiers - string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction); - } + /// + /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier. + /// + /// Value to attempt to convert + /// Alias of the editor used for the parameter + /// Collection to add dependencies to when performing ToArtifact + /// Indicates which action is being performed (to or from artifact) + /// Value with converted identifiers + string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction); } diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs index 3f789e2e38..f6cd7c8002 100644 --- a/src/Umbraco.Core/Deploy/IServiceConnector.cs +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -1,84 +1,83 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Connects to an Umbraco service. +/// +public interface IServiceConnector : IDiscoverable { /// - /// Connects to an Umbraco service. + /// Gets an artifact. /// - public interface IServiceConnector : IDiscoverable - { - /// - /// Gets an artifact. - /// - /// The entity identifier of the artifact. - /// The corresponding artifact, or null. - IArtifact? GetArtifact(Udi udi); + /// The entity identifier of the artifact. + /// The corresponding artifact, or null. + IArtifact? GetArtifact(Udi udi); - /// - /// Gets an artifact. - /// - /// The entity. - /// The corresponding artifact. - IArtifact GetArtifact(object entity); + /// + /// Gets an artifact. + /// + /// The entity. + /// The corresponding artifact. + IArtifact GetArtifact(object entity); - /// - /// Initializes processing for an artifact. - /// - /// The artifact. - /// The deploy context. - /// The mapped artifact. - ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); + /// + /// Initializes processing for an artifact. + /// + /// The artifact. + /// The deploy context. + /// The mapped artifact. + ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); - /// - /// Processes an artifact. - /// - /// The mapped artifact. - /// The deploy context. - /// The processing pass number. - void Process(ArtifactDeployState dart, IDeployContext context, int pass); + /// + /// Processes an artifact. + /// + /// The mapped artifact. + /// The deploy context. + /// The processing pass number. + void Process(ArtifactDeployState dart, IDeployContext context, int pass); - /// - /// Explodes a range into udis. - /// - /// The range. - /// The list of udis where to add the new udis. - /// Also, it's cool to have a method named Explode. Kaboom! - void Explode(UdiRange range, List udis); + /// + /// Explodes a range into udis. + /// + /// The range. + /// The list of udis where to add the new udis. + /// Also, it's cool to have a method named Explode. Kaboom! + void Explode(UdiRange range, List udis); - /// - /// Gets a named range for a specified udi and selector. - /// - /// The udi. - /// The selector. - /// The named range for the specified udi and selector. - NamedUdiRange GetRange(Udi udi, string selector); + /// + /// Gets a named range for a specified udi and selector. + /// + /// The udi. + /// The selector. + /// The named range for the specified udi and selector. + NamedUdiRange GetRange(Udi udi, string selector); - /// - /// Gets a named range for specified entity type, identifier and selector. - /// - /// The entity type. - /// The identifier. - /// The selector. - /// The named range for the specified entity type, identifier and selector. - /// - /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? - /// At the moment our UI has a hard time returning proper udis, mainly because Core's tree do - /// not manage guids but only ints... so we have to provide a way to support it. The string id here - /// can be either a real string (for string udis) or an "integer as a string", using the value "-1" to - /// indicate the "root" i.e. an open udi. - /// - NamedUdiRange GetRange(string entityType, string sid, string selector); - - /// - /// Compares two artifacts. - /// - /// The first artifact. - /// The second artifact. - /// A collection of differences to append to, if not null. - /// A boolean value indicating whether the artifacts are identical. - /// ServiceConnectorBase{TArtifact} provides a very basic default implementation. - bool Compare(IArtifact? art1, IArtifact? art2, ICollection? differences = null); - } + /// + /// Gets a named range for specified entity type, identifier and selector. + /// + /// The entity type. + /// The identifier. + /// The selector. + /// The named range for the specified entity type, identifier and selector. + /// + /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? + /// + /// At the moment our UI has a hard time returning proper udis, mainly because Core's tree do + /// not manage guids but only ints... so we have to provide a way to support it. The string id here + /// can be either a real string (for string udis) or an "integer as a string", using the value "-1" to + /// indicate the "root" i.e. an open udi. + /// + /// + NamedUdiRange GetRange(string entityType, string sid, string selector); + /// + /// Compares two artifacts. + /// + /// The first artifact. + /// The second artifact. + /// A collection of differences to append to, if not null. + /// A boolean value indicating whether the artifacts are identical. + /// ServiceConnectorBase{TArtifact} provides a very basic default implementation. + bool Compare(IArtifact? art1, IArtifact? art2, ICollection? differences = null); } diff --git a/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs index 66364a08f3..c68906bbbf 100644 --- a/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs +++ b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs @@ -1,25 +1,24 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Provides a method to retrieve an artifact's unique identifier. +/// +/// +/// Artifacts are uniquely identified by their , however they represent +/// elements in Umbraco that may be uniquely identified by another value. For example, +/// a content type is uniquely identified by its alias. If someone creates a new content +/// type, and tries to deploy it to a remote environment where a content type with the +/// same alias already exists, both content types end up having different +/// but the same alias. By default, Deploy would fail and throw when trying to save the +/// new content type (duplicate alias). However, if the connector also implements this +/// interface, the situation can be detected beforehand and reported in a nicer way. +/// +public interface IUniqueIdentifyingServiceConnector { /// - /// Provides a method to retrieve an artifact's unique identifier. + /// Gets the unique identifier of the specified artifact. /// - /// - /// Artifacts are uniquely identified by their , however they represent - /// elements in Umbraco that may be uniquely identified by another value. For example, - /// a content type is uniquely identified by its alias. If someone creates a new content - /// type, and tries to deploy it to a remote environment where a content type with the - /// same alias already exists, both content types end up having different - /// but the same alias. By default, Deploy would fail and throw when trying to save the - /// new content type (duplicate alias). However, if the connector also implements this - /// interface, the situation can be detected beforehand and reported in a nicer way. - /// - public interface IUniqueIdentifyingServiceConnector - { - /// - /// Gets the unique identifier of the specified artifact. - /// - /// The artifact. - /// The unique identifier. - string GetUniqueIdentifier(IArtifact artifact); - } + /// The artifact. + /// The unique identifier. + string GetUniqueIdentifier(IArtifact artifact); } diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 2c684f2ccd..f2a776c7ca 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -1,37 +1,37 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Defines methods that can convert a property value to / from an environment-agnostic string. +/// +/// +/// Property values may contain values such as content identifiers, that would be local +/// to one environment, and need to be converted in order to be deployed. Connectors also deal +/// with serializing to / from string. +/// +public interface IValueConnector { /// - /// Defines methods that can convert a property value to / from an environment-agnostic string. + /// Gets the property editor aliases that the value converter supports by default. /// - /// Property values may contain values such as content identifiers, that would be local - /// to one environment, and need to be converted in order to be deployed. Connectors also deal - /// with serializing to / from string. - public interface IValueConnector - { - /// - /// Gets the property editor aliases that the value converter supports by default. - /// - IEnumerable PropertyEditorAliases { get; } + IEnumerable PropertyEditorAliases { get; } - /// - /// Gets the deploy property value corresponding to a content property value, and gather dependencies. - /// - /// The content property value. - /// The value property type - /// The content dependencies. - /// The deploy property value. - string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies); + /// + /// Gets the deploy property value corresponding to a content property value, and gather dependencies. + /// + /// The content property value. + /// The value property type + /// The content dependencies. + /// The deploy property value. + string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies); - /// - /// Gets the content property value corresponding to a deploy property value. - /// - /// The deploy property value. - /// The value property type< - /// The current content property value. - /// The content property value. - object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue); - } + /// + /// Gets the content property value corresponding to a deploy property value. + /// + /// The deploy property value. + /// The value property type + /// The current content property value. + /// The content property value. + object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue); } diff --git a/src/Umbraco.Core/Diagnostics/IMarchal.cs b/src/Umbraco.Core/Diagnostics/IMarchal.cs index 988eaca78c..304ff22c5a 100644 --- a/src/Umbraco.Core/Diagnostics/IMarchal.cs +++ b/src/Umbraco.Core/Diagnostics/IMarchal.cs @@ -1,16 +1,15 @@ -using System; +namespace Umbraco.Cms.Core.Diagnostics; -namespace Umbraco.Cms.Core.Diagnostics +/// +/// Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting +/// managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code. +/// +public interface IMarchal { /// - /// Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code. + /// Retrieves a computer-independent description of an exception, and information about the state that existed for the + /// thread when the exception occurred. /// - public interface IMarchal - { - /// - /// Retrieves a computer-independent description of an exception, and information about the state that existed for the thread when the exception occurred. - /// - /// A pointer to an EXCEPTION_POINTERS structure. - IntPtr GetExceptionPointers(); - } + /// A pointer to an EXCEPTION_POINTERS structure. + IntPtr GetExceptionPointers(); } diff --git a/src/Umbraco.Core/Diagnostics/MiniDump.cs b/src/Umbraco.Core/Diagnostics/MiniDump.cs index 25f6e530e1..ac37c69f12 100644 --- a/src/Umbraco.Core/Diagnostics/MiniDump.cs +++ b/src/Umbraco.Core/Diagnostics/MiniDump.cs @@ -1,145 +1,158 @@ -using System; using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Diagnostics +namespace Umbraco.Cms.Core.Diagnostics; + +// taken from https://blogs.msdn.microsoft.com/dondu/2010/10/24/writing-minidumps-in-c/ +// and https://blogs.msdn.microsoft.com/dondu/2010/10/31/writing-minidumps-from-exceptions-in-c/ +// which itself got it from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/ +public static class MiniDump { - // taken from https://blogs.msdn.microsoft.com/dondu/2010/10/24/writing-minidumps-in-c/ - // and https://blogs.msdn.microsoft.com/dondu/2010/10/31/writing-minidumps-from-exceptions-in-c/ - // which itself got it from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/ + private static readonly object LockO = new(); - public static class MiniDump + [Flags] + public enum Option : uint { - private static readonly object LockO = new object(); + // From dbghelp.h: + Normal = 0x00000000, + WithDataSegs = 0x00000001, + WithFullMemory = 0x00000002, + WithHandleData = 0x00000004, + FilterMemory = 0x00000008, + ScanMemory = 0x00000010, + WithUnloadedModules = 0x00000020, + WithIndirectlyReferencedMemory = 0x00000040, + FilterModulePaths = 0x00000080, + WithProcessThreadData = 0x00000100, + WithPrivateReadWriteMemory = 0x00000200, + WithoutOptionalData = 0x00000400, + WithFullMemoryInfo = 0x00000800, + WithThreadInfo = 0x00001000, + WithCodeSegs = 0x00002000, + WithoutAuxiliaryState = 0x00004000, + WithFullAuxiliaryState = 0x00008000, + WithPrivateWriteCopyMemory = 0x00010000, + IgnoreInaccessibleMemory = 0x00020000, + ValidTypeFlags = 0x0003ffff, + } - [Flags] - public enum Option : uint + public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false) + { + lock (LockO) { - // From dbghelp.h: - Normal = 0x00000000, - WithDataSegs = 0x00000001, - WithFullMemory = 0x00000002, - WithHandleData = 0x00000004, - FilterMemory = 0x00000008, - ScanMemory = 0x00000010, - WithUnloadedModules = 0x00000020, - WithIndirectlyReferencedMemory = 0x00000040, - FilterModulePaths = 0x00000080, - WithProcessThreadData = 0x00000100, - WithPrivateReadWriteMemory = 0x00000200, - WithoutOptionalData = 0x00000400, - WithFullMemoryInfo = 0x00000800, - WithThreadInfo = 0x00001000, - WithCodeSegs = 0x00002000, - WithoutAuxiliaryState = 0x00004000, - WithFullAuxiliaryState = 0x00008000, - WithPrivateWriteCopyMemory = 0x00010000, - IgnoreInaccessibleMemory = 0x00020000, - ValidTypeFlags = 0x0003ffff, - } + // work around "stack trace is not available while minidump debugging", + // by making sure a local var (that we can inspect) contains the stack trace. + // getting the call stack before it is unwound would require a special exception + // filter everywhere in our code = not! + var stacktrace = withException ? Environment.StackTrace : string.Empty; - //typedef struct _MINIDUMP_EXCEPTION_INFORMATION { - // DWORD ThreadId; - // PEXCEPTION_POINTERS ExceptionPointers; - // BOOL ClientPointers; - //} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION; - [StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64! - public struct MiniDumpExceptionInformation - { - public uint ThreadId; - public IntPtr ExceptionPointers; - [MarshalAs(UnmanagedType.Bool)] - public bool ClientPointers; - } + var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); - //BOOL - //WINAPI - //MiniDumpWriteDump( - // __in HANDLE hProcess, - // __in DWORD ProcessId, - // __in HANDLE hFile, - // __in MINIDUMP_TYPE DumpType, - // __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, - // __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, - // __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam - // ); - - // Overload requiring MiniDumpExceptionInformation - [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] - private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam); - - // Overload supporting MiniDumpExceptionInformation == NULL - [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] - private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam); - - [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)] - private static extern uint GetCurrentThreadId(); - - private static bool Write(IMarchal marchal, SafeHandle fileHandle, Option options, bool withException = false) - { - using (var currentProcess = Process.GetCurrentProcess()) + if (Directory.Exists(directory) == false) { - var currentProcessHandle = currentProcess.Handle; - var currentProcessId = (uint)currentProcess.Id; - - MiniDumpExceptionInformation exp; - - exp.ThreadId = GetCurrentThreadId(); - exp.ClientPointers = false; - exp.ExceptionPointers = IntPtr.Zero; - - if (withException) - { - exp.ExceptionPointers = marchal.GetExceptionPointers(); - } - - var bRet = exp.ExceptionPointers == IntPtr.Zero - ? MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero) - : MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, ref exp, IntPtr.Zero, IntPtr.Zero); - - return bRet; + Directory.CreateDirectory(directory); } - } - public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false) - { - lock (LockO) + var filename = Path.Combine( + directory, + $"{DateTime.UtcNow:yyyyMMddTHHmmss}.{Guid.NewGuid().ToString("N")[..4]}.dmp"); + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Write)) { - // work around "stack trace is not available while minidump debugging", - // by making sure a local var (that we can inspect) contains the stack trace. - // getting the call stack before it is unwound would require a special exception - // filter everywhere in our code = not! - var stacktrace = withException ? Environment.StackTrace : string.Empty; - - var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); - - if (Directory.Exists(directory) == false) - { - Directory.CreateDirectory(directory); - } - - var filename = Path.Combine(directory, $"{DateTime.UtcNow:yyyyMMddTHHmmss}.{Guid.NewGuid().ToString("N").Substring(0, 4)}.dmp"); - using (var stream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Write)) - { - return Write(marchal, stream.SafeFileHandle, options, withException); - } - } - } - - public static bool OkToDump(IHostingEnvironment hostingEnvironment) - { - lock (LockO) - { - var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); - if (Directory.Exists(directory) == false) - { - return true; - } - var count = Directory.GetFiles(directory, "*.dmp").Length; - return count < 8; + return Write(marchal, stream.SafeFileHandle, options, withException); } } } + + // BOOL + // WINAPI + // MiniDumpWriteDump( + // __in HANDLE hProcess, + // __in DWORD ProcessId, + // __in HANDLE hFile, + // __in MINIDUMP_TYPE DumpType, + // __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + // __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + // __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam + // ); + + // Overload requiring MiniDumpExceptionInformation + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam); + + // Overload supporting MiniDumpExceptionInformation == NULL + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam); + + [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)] + private static extern uint GetCurrentThreadId(); + + private static bool Write(IMarchal marchal, SafeHandle fileHandle, Option options, bool withException = false) + { + using (var currentProcess = Process.GetCurrentProcess()) + { + IntPtr currentProcessHandle = currentProcess.Handle; + var currentProcessId = (uint)currentProcess.Id; + + MiniDumpExceptionInformation exp; + + exp.ThreadId = GetCurrentThreadId(); + exp.ClientPointers = false; + exp.ExceptionPointers = IntPtr.Zero; + + if (withException) + { + exp.ExceptionPointers = marchal.GetExceptionPointers(); + } + + var bRet = exp.ExceptionPointers == IntPtr.Zero + ? MiniDumpWriteDump( + currentProcessHandle, + currentProcessId, + fileHandle, + (uint)options, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero) + : MiniDumpWriteDump( + currentProcessHandle, + currentProcessId, + fileHandle, + (uint)options, + ref exp, + IntPtr.Zero, + IntPtr.Zero); + + return bRet; + } + } + + public static bool OkToDump(IHostingEnvironment hostingEnvironment) + { + lock (LockO) + { + var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); + if (Directory.Exists(directory) == false) + { + return true; + } + + var count = Directory.GetFiles(directory, "*.dmp").Length; + return count < 8; + } + } + + // typedef struct _MINIDUMP_EXCEPTION_INFORMATION { + // DWORD ThreadId; + // PEXCEPTION_POINTERS ExceptionPointers; + // BOOL ClientPointers; + // } MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION; + [StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64! + public struct MiniDumpExceptionInformation + { + public uint ThreadId; + public IntPtr ExceptionPointers; + [MarshalAs(UnmanagedType.Bool)] + public bool ClientPointers; + } } diff --git a/src/Umbraco.Core/Diagnostics/NoopMarchal.cs b/src/Umbraco.Core/Diagnostics/NoopMarchal.cs index 273a4fb32c..770aefd50f 100644 --- a/src/Umbraco.Core/Diagnostics/NoopMarchal.cs +++ b/src/Umbraco.Core/Diagnostics/NoopMarchal.cs @@ -1,9 +1,6 @@ -using System; +namespace Umbraco.Cms.Core.Diagnostics; -namespace Umbraco.Cms.Core.Diagnostics +internal class NoopMarchal : IMarchal { - internal class NoopMarchal : IMarchal - { - public IntPtr GetExceptionPointers() => IntPtr.Zero; - } + public IntPtr GetExceptionPointers() => IntPtr.Zero; } diff --git a/src/Umbraco.Core/Dictionary/ICultureDictionary.cs b/src/Umbraco.Core/Dictionary/ICultureDictionary.cs index e8e3c62050..380f7ee287 100644 --- a/src/Umbraco.Core/Dictionary/ICultureDictionary.cs +++ b/src/Umbraco.Core/Dictionary/ICultureDictionary.cs @@ -1,30 +1,28 @@ -using System.Collections.Generic; using System.Globalization; -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +/// +/// Represents a dictionary based on a specific culture +/// +public interface ICultureDictionary { /// - /// Represents a dictionary based on a specific culture + /// Returns the current culture /// - public interface ICultureDictionary - { - /// - /// Returns the dictionary value based on the key supplied - /// - /// - /// - string? this[string key] { get; } + CultureInfo Culture { get; } - /// - /// Returns the current culture - /// - CultureInfo Culture { get; } + /// + /// Returns the dictionary value based on the key supplied + /// + /// + /// + string? this[string key] { get; } - /// - /// Returns the child dictionary entries for a given key - /// - /// - /// - IDictionary GetChildren(string key); - } + /// + /// Returns the child dictionary entries for a given key + /// + /// + /// + IDictionary GetChildren(string key); } diff --git a/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs b/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs index 40fbb1bad8..6cb2642b15 100644 --- a/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs +++ b/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +public interface ICultureDictionaryFactory { - public interface ICultureDictionaryFactory - { - ICultureDictionary CreateDictionary(); - } + ICultureDictionary CreateDictionary(); } diff --git a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs index 44cc15033f..de968f1676 100644 --- a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs +++ b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs @@ -1,142 +1,141 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +/// +/// A culture dictionary that uses the Umbraco ILocalizationService +/// +/// +/// TODO: The ICultureDictionary needs to represent the 'fast' way to do dictionary item retrieval - for front-end and +/// back office. +/// The ILocalizationService is the service used for interacting with this data from the database which isn't all that +/// fast +/// (even though there is caching involved, if there's lots of dictionary items the caching is not great) +/// +internal class DefaultCultureDictionary : ICultureDictionary { + private readonly ILocalizationService _localizationService; + private readonly IAppCache _requestCache; + private readonly CultureInfo? _specificCulture; + /// - /// A culture dictionary that uses the Umbraco ILocalizationService + /// Default constructor which will use the current thread's culture /// - /// - /// TODO: The ICultureDictionary needs to represent the 'fast' way to do dictionary item retrieval - for front-end and back office. - /// The ILocalizationService is the service used for interacting with this data from the database which isn't all that fast - /// (even though there is caching involved, if there's lots of dictionary items the caching is not great) - /// - internal class DefaultCultureDictionary : ICultureDictionary + /// + /// + public DefaultCultureDictionary(ILocalizationService localizationService, IAppCache requestCache) { - private readonly ILocalizationService _localizationService; - private readonly IAppCache _requestCache; - private readonly CultureInfo? _specificCulture; + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + } - /// - /// Default constructor which will use the current thread's culture - /// - /// - /// - public DefaultCultureDictionary(ILocalizationService localizationService, IAppCache requestCache) - { - _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - } + /// + /// Constructor for testing to specify a static culture + /// + /// + /// + /// + public DefaultCultureDictionary(CultureInfo specificCulture, ILocalizationService localizationService, IAppCache requestCache) + { + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + _specificCulture = specificCulture ?? throw new ArgumentNullException(nameof(specificCulture)); + } - /// - /// Constructor for testing to specify a static culture - /// - /// - /// - /// - public DefaultCultureDictionary(CultureInfo specificCulture, ILocalizationService localizationService, IAppCache requestCache) - { - _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - _specificCulture = specificCulture ?? throw new ArgumentNullException(nameof(specificCulture)); - } + /// + /// Returns the current culture + /// + public CultureInfo Culture => _specificCulture ?? Thread.CurrentThread.CurrentUICulture; - /// - /// Returns the dictionary value based on the key supplied - /// - /// - /// - public string? this[string key] - { - get + private ILanguage? Language => + + // ensure it's stored/retrieved from request cache + // NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now. + _requestCache.GetCacheItem( + typeof(DefaultCultureDictionary).Name + "Culture" + Culture.Name, + () => { - var found = _localizationService.GetDictionaryItemByKey(key); - if (found == null) + // find a language that matches the current culture or any of its parent cultures + CultureInfo culture = Culture; + while (culture != CultureInfo.InvariantCulture) { - return string.Empty; + ILanguage? language = _localizationService.GetLanguageByIsoCode(culture.Name); + if (language != null) + { + return language; + } + + culture = culture.Parent; } - var byLang = found.Translations?.FirstOrDefault(x => x.Language?.Equals(Language) ?? false); - if (byLang == null) - { - return string.Empty; - } + return null; + }); - return byLang.Value; - } - } - - /// - /// Returns the current culture - /// - public CultureInfo Culture => _specificCulture ?? System.Threading.Thread.CurrentThread.CurrentUICulture; - - /// - /// Returns the child dictionary entries for a given key - /// - /// - /// - /// - /// NOTE: The result of this is not cached anywhere - the underlying repository does not cache - /// the child lookups because that is done by a query lookup. This method isn't used in our codebase - /// so I don't think this is a performance issue but if devs are using this it could be optimized here. - /// - public IDictionary GetChildren(string key) + /// + /// Returns the dictionary value based on the key supplied + /// + /// + /// + public string? this[string key] + { + get { - var result = new Dictionary(); - - var found = _localizationService.GetDictionaryItemByKey(key); + IDictionaryItem? found = _localizationService.GetDictionaryItemByKey(key); if (found == null) { - return result; + return string.Empty; } - var children = _localizationService.GetDictionaryItemChildren(found.Key); - if (children == null) + IDictionaryTranslation? byLang = + found.Translations.FirstOrDefault(x => x.Language?.Equals(Language) ?? false); + if (byLang == null) { - return result; + return string.Empty; } - foreach (var dictionaryItem in children) - { - var byLang = dictionaryItem.Translations?.FirstOrDefault((x => x.Language?.Equals(Language) ?? false)); - if (byLang != null && dictionaryItem.ItemKey is not null && byLang.Value is not null) - { - result.Add(dictionaryItem.ItemKey, byLang.Value); - } - } + return byLang.Value; + } + } + /// + /// Returns the child dictionary entries for a given key + /// + /// + /// + /// + /// NOTE: The result of this is not cached anywhere - the underlying repository does not cache + /// the child lookups because that is done by a query lookup. This method isn't used in our codebase + /// so I don't think this is a performance issue but if devs are using this it could be optimized here. + /// + public IDictionary GetChildren(string key) + { + var result = new Dictionary(); + + IDictionaryItem? found = _localizationService.GetDictionaryItemByKey(key); + if (found == null) + { return result; } - private ILanguage? Language + IEnumerable? children = _localizationService.GetDictionaryItemChildren(found.Key); + if (children == null) { - get + return result; + } + + foreach (IDictionaryItem dictionaryItem in children) + { + IDictionaryTranslation? byLang = dictionaryItem.Translations.FirstOrDefault(x => x.Language?.Equals(Language) ?? false); + if (byLang != null && dictionaryItem.ItemKey is not null && byLang.Value is not null) { - //ensure it's stored/retrieved from request cache - //NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now. - return _requestCache.GetCacheItem(typeof (DefaultCultureDictionary).Name + "Culture" + Culture.Name, - () => { - // find a language that matches the current culture or any of its parent cultures - var culture = Culture; - while(culture != CultureInfo.InvariantCulture) - { - var language = _localizationService.GetLanguageByIsoCode(culture.Name); - if(language != null) - { - return language; - } - culture = culture.Parent; - } - return null; - }); + result.Add(dictionaryItem.ItemKey, byLang.Value); } } + + return result; } } diff --git a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs index 8713e338ea..4c4eb030cc 100644 --- a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs +++ b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs @@ -1,28 +1,26 @@ -using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +/// +/// A culture dictionary factory used to create an Umbraco.Core.Dictionary.ICultureDictionary. +/// +/// +/// In the future this will allow use to potentially store dictionary items elsewhere and allows for maximum +/// flexibility. +/// +public class DefaultCultureDictionaryFactory : ICultureDictionaryFactory { - /// - /// A culture dictionary factory used to create an Umbraco.Core.Dictionary.ICultureDictionary. - /// - /// - /// In the future this will allow use to potentially store dictionary items elsewhere and allows for maximum flexibility. - /// - public class DefaultCultureDictionaryFactory : ICultureDictionaryFactory + private readonly AppCaches _appCaches; + private readonly ILocalizationService _localizationService; + + public DefaultCultureDictionaryFactory(ILocalizationService localizationService, AppCaches appCaches) { - private readonly ILocalizationService _localizationService; - private readonly AppCaches _appCaches; - - public DefaultCultureDictionaryFactory(ILocalizationService localizationService, AppCaches appCaches) - { - _localizationService = localizationService; - _appCaches = appCaches; - } - - public ICultureDictionary CreateDictionary() - { - return new DefaultCultureDictionary(_localizationService, _appCaches.RequestCache); - } + _localizationService = localizationService; + _appCaches = appCaches; } + + public ICultureDictionary CreateDictionary() => + new DefaultCultureDictionary(_localizationService, _appCaches.RequestCache); } diff --git a/src/Umbraco.Core/Direction.cs b/src/Umbraco.Core/Direction.cs index 152a3663fd..874a00a4ac 100644 --- a/src/Umbraco.Core/Direction.cs +++ b/src/Umbraco.Core/Direction.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public enum Direction { - public enum Direction - { - Ascending = 0, - Descending = 1 - } + Ascending = 0, + Descending = 1, } diff --git a/src/Umbraco.Core/DisposableObjectSlim.cs b/src/Umbraco.Core/DisposableObjectSlim.cs index 4304098324..6cc7f38d91 100644 --- a/src/Umbraco.Core/DisposableObjectSlim.cs +++ b/src/Umbraco.Core/DisposableObjectSlim.cs @@ -1,56 +1,50 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Abstract implementation of managed IDisposable. +/// +/// +/// This is for objects that do NOT have unmanaged resources. +/// Can also be used as a pattern for when inheriting is not possible. +/// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx +/// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/ +/// Note: if an object's ctor throws, it will never be disposed, and so if that ctor +/// has allocated disposable objects, it should take care of disposing them. +/// +public abstract class DisposableObjectSlim : IDisposable { /// - /// Abstract implementation of managed IDisposable. + /// Gets a value indicating whether this instance is disposed. /// /// - /// This is for objects that do NOT have unmanaged resources. - /// - /// Can also be used as a pattern for when inheriting is not possible. - /// - /// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx - /// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/ - /// - /// Note: if an object's ctor throws, it will never be disposed, and so if that ctor - /// has allocated disposable objects, it should take care of disposing them. + /// for internal tests only (not thread safe) /// - public abstract class DisposableObjectSlim : IDisposable - { - /// - /// Gets a value indicating whether this instance is disposed. - /// - /// - /// for internal tests only (not thread safe) - /// - public bool Disposed { get; private set; } + public bool Disposed { get; private set; } - /// - /// Disposes managed resources - /// - protected abstract void DisposeResources(); - - /// - /// Disposes managed resources - /// - /// True if disposing via Dispose method and not a finalizer. Always true for this class. - protected virtual void Dispose(bool disposing) - { - if (!Disposed) - { - if (disposing) - { - DisposeResources(); - } - - Disposed = true; - } - } - - /// + /// #pragma warning disable CA1063 // Implement IDisposable Correctly - public void Dispose() => Dispose(disposing: true); // We do not use GC.SuppressFinalize because this has no finalizer + public void Dispose() => Dispose(true); // We do not use GC.SuppressFinalize because this has no finalizer #pragma warning restore CA1063 // Implement IDisposable Correctly + + /// + /// Disposes managed resources + /// + protected abstract void DisposeResources(); + + /// + /// Disposes managed resources + /// + /// True if disposing via Dispose method and not a finalizer. Always true for this class. + protected virtual void Dispose(bool disposing) + { + if (!Disposed) + { + if (disposing) + { + DisposeResources(); + } + + Disposed = true; + } } } diff --git a/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs index 01acd02c10..8ae47fce08 100644 --- a/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs +++ b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs @@ -1,10 +1,10 @@ namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Represents the type of distributed lock. +/// Represents the type of distributed lock. /// public enum DistributedLockType { ReadLock, - WriteLock + WriteLock, } diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs index 2f27929a6c..570af005b5 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs @@ -1,14 +1,12 @@ -using System; - namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Base class for all DistributedLockingExceptions. +/// Base class for all DistributedLockingExceptions. /// public class DistributedLockingException : ApplicationException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DistributedLockingException(string message) : base(message) @@ -16,7 +14,7 @@ public class DistributedLockingException : ApplicationException } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// // ReSharper disable once UnusedMember.Global public DistributedLockingException(string message, Exception innerException) diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs index 9d65023790..064a046803 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Base class for all DistributedLocking timeout related exceptions. +/// Base class for all DistributedLocking timeout related exceptions. /// public abstract class DistributedLockingTimeoutException : DistributedLockingException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// protected DistributedLockingTimeoutException(int lockId, bool isWrite) : base($"Failed to acquire {(isWrite ? "write" : "read")} lock for id: {lockId}.") diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs index 4d37238c0d..8e21004cec 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Exception thrown when a read lock could not be obtained in a timely manner. +/// Exception thrown when a read lock could not be obtained in a timely manner. /// public class DistributedReadLockTimeoutException : DistributedLockingTimeoutException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DistributedReadLockTimeoutException(int lockId) : base(lockId, false) diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs index abf84470e0..068684f310 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Exception thrown when a write lock could not be obtained in a timely manner. +/// Exception thrown when a write lock could not be obtained in a timely manner. /// public class DistributedWriteLockTimeoutException : DistributedLockingTimeoutException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DistributedWriteLockTimeoutException(int lockId) : base(lockId, true) diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs index 202bb594bc..261bd802e3 100644 --- a/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs @@ -1,19 +1,17 @@ -using System; - namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Interface representing a DistributedLock. +/// Interface representing a DistributedLock. /// public interface IDistributedLock : IDisposable { /// - /// Gets the LockId. + /// Gets the LockId. /// int LockId { get; } /// - /// Gets the DistributedLockType. + /// Gets the DistributedLockType. /// DistributedLockType LockType { get; } } diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs index 5df8a23650..57252364d3 100644 --- a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs @@ -1,50 +1,52 @@ -using System; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking.Exceptions; namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Represents a class responsible for managing distributed locks. +/// Represents a class responsible for managing distributed locks. /// /// -/// In general the rules for distributed locks are as follows. -/// -/// -/// Cannot obtain a write lock if a read lock exists for same lock id (except during an upgrade from reader -> writer) -/// -/// -/// Cannot obtain a write lock if a write lock exists for same lock id. -/// -/// -/// Cannot obtain a read lock if a write lock exists for same lock id. -/// -/// -/// Can obtain a read lock if a read lock exists for same lock id. -/// -/// +/// In general the rules for distributed locks are as follows. +/// +/// +/// Cannot obtain a write lock if a read lock exists for same lock id (except during an upgrade from +/// reader -> writer) +/// +/// +/// Cannot obtain a write lock if a write lock exists for same lock id. +/// +/// +/// Cannot obtain a read lock if a write lock exists for same lock id. +/// +/// +/// Can obtain a read lock if a read lock exists for same lock id. +/// +/// /// public interface IDistributedLockingMechanism { /// - /// Gets a value indicating whether this distributed locking mechanism can be used. + /// Gets a value indicating whether this distributed locking mechanism can be used. /// bool Enabled { get; } /// - /// Obtains a distributed read lock. + /// Obtains a distributed read lock. /// /// - /// When timeout is null, implementations should use . + /// When timeout is null, implementations should use + /// . /// /// Failed to obtain distributed read lock in time. IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null); /// - /// Obtains a distributed read lock. + /// Obtains a distributed read lock. /// /// - /// When timeout is null, implementations should use . + /// When timeout is null, implementations should use + /// . /// /// Failed to obtain distributed write lock in time. IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null); diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs index 1bd1cfe206..ecc1c99cfa 100644 --- a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs @@ -1,7 +1,7 @@ namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Picks an appropriate IDistributedLockingMechanism when multiple are registered +/// Picks an appropriate IDistributedLockingMechanism when multiple are registered /// public interface IDistributedLockingMechanismFactory { diff --git a/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs b/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs index d8bd73aca9..6ab0b76e33 100644 --- a/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs +++ b/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs @@ -1,21 +1,21 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Features; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +public class BackOfficePreviewModel { - public class BackOfficePreviewModel + private readonly UmbracoFeatures _features; + + public BackOfficePreviewModel(UmbracoFeatures features, IEnumerable languages) { - private readonly UmbracoFeatures _features; - - public BackOfficePreviewModel(UmbracoFeatures features, IEnumerable languages) - { - _features = features; - Languages = languages; - } - - public IEnumerable Languages { get; } - public bool DisableDevicePreview => _features.Disabled.DisableDevicePreview; - public string? PreviewExtendedHeaderView => _features.Enabled.PreviewExtendedView; + _features = features; + Languages = languages; } + + public IEnumerable Languages { get; } + + public bool DisableDevicePreview => _features.Disabled.DisableDevicePreview; + + public string? PreviewExtendedHeaderView => _features.Enabled.PreviewExtendedView; } diff --git a/src/Umbraco.Core/Editors/EditorValidatorCollection.cs b/src/Umbraco.Core/Editors/EditorValidatorCollection.cs index 91bc3e191b..a1c46cdb57 100644 --- a/src/Umbraco.Core/Editors/EditorValidatorCollection.cs +++ b/src/Umbraco.Core/Editors/EditorValidatorCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +public class EditorValidatorCollection : BuilderCollectionBase { - public class EditorValidatorCollection : BuilderCollectionBase + public EditorValidatorCollection(Func> items) + : base(items) { - public EditorValidatorCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs b/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs index 223778b79d..b7b5269ee7 100644 --- a/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs +++ b/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase { - public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase - { - protected override EditorValidatorCollectionBuilder This => this; - } + protected override EditorValidatorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Editors/EditorValidatorOfT.cs b/src/Umbraco.Core/Editors/EditorValidatorOfT.cs index a70509237a..3e2b899519 100644 --- a/src/Umbraco.Core/Editors/EditorValidatorOfT.cs +++ b/src/Umbraco.Core/Editors/EditorValidatorOfT.cs @@ -1,19 +1,16 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +/// +/// Provides a base class for implementations. +/// +/// The validated object type. +public abstract class EditorValidator : IEditorValidator { - /// - /// Provides a base class for implementations. - /// - /// The validated object type. - public abstract class EditorValidator : IEditorValidator - { - public Type ModelType => typeof (T); + public Type ModelType => typeof(T); - public IEnumerable Validate(object model) => Validate((T) model); + public IEnumerable Validate(object model) => Validate((T)model); - protected abstract IEnumerable Validate(T model); - } + protected abstract IEnumerable Validate(T model); } diff --git a/src/Umbraco.Core/Editors/IEditorValidator.cs b/src/Umbraco.Core/Editors/IEditorValidator.cs index 17bb195e4b..2f6bc9f110 100644 --- a/src/Umbraco.Core/Editors/IEditorValidator.cs +++ b/src/Umbraco.Core/Editors/IEditorValidator.cs @@ -1,34 +1,31 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +// note - about IEditorValidator +// +// interface: IEditorValidator +// base class: EditorValidator +// static validation: EditorValidator.Validate() +// composition: via EditorValidationCollection and builder +// initialized with all IEditorValidator instances +// +// validation is used exclusively in ContentTypeControllerBase +// currently the only implementations are for Models Builder. + +/// +/// Provides a general object validator. +/// +public interface IEditorValidator : IDiscoverable { - // note - about IEditorValidator - // - // interface: IEditorValidator - // base class: EditorValidator - // static validation: EditorValidator.Validate() - // composition: via EditorValidationCollection and builder - // initialized with all IEditorValidator instances - // - // validation is used exclusively in ContentTypeControllerBase - // currently the only implementations are for Models Builder. + /// + /// Gets the object type validated by this validator. + /// + Type ModelType { get; } /// - /// Provides a general object validator. + /// Validates an object. /// - public interface IEditorValidator : IDiscoverable - { - /// - /// Gets the object type validated by this validator. - /// - Type ModelType { get; } - - /// - /// Validates an object. - /// - IEnumerable Validate(object model); - } + IEnumerable Validate(object model); } diff --git a/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs index 23fc59da24..be9b05230f 100644 --- a/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs +++ b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs @@ -1,8 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -10,161 +8,191 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Editors -{ - public class UserEditorAuthorizationHelper - { - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; +namespace Umbraco.Cms.Core.Editors; - public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) +public class UserEditorAuthorizationHelper +{ + private readonly AppCaches _appCaches; + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + private readonly IMediaService _mediaService; + + public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) + { + _contentService = contentService; + _mediaService = mediaService; + _entityService = entityService; + _appCaches = appCaches; + } + + /// + /// Checks if the current user has access to save the user data + /// + /// The current user trying to save user data + /// The user instance being saved (can be null if it's a new user) + /// The start content ids of the user being saved (can be null or empty) + /// The start media ids of the user being saved (can be null or empty) + /// The user aliases of the user being saved (can be null or empty) + /// + public Attempt IsAuthorized( + IUser? currentUser, + IUser? savingUser, + IEnumerable? startContentIds, + IEnumerable? startMediaIds, + IEnumerable? userGroupAliases) + { + var currentIsAdmin = currentUser?.IsAdmin() ?? false; + + // a) A non-admin cannot save an admin + if (savingUser != null) { - _contentService = contentService; - _mediaService = mediaService; - _entityService = entityService; - _appCaches = appCaches; + if (savingUser.IsAdmin() && currentIsAdmin == false) + { + return Attempt.Fail("The current user is not an administrator so cannot save another administrator"); + } } - /// - /// Checks if the current user has access to save the user data - /// - /// The current user trying to save user data - /// The user instance being saved (can be null if it's a new user) - /// The start content ids of the user being saved (can be null or empty) - /// The start media ids of the user being saved (can be null or empty) - /// The user aliases of the user being saved (can be null or empty) - /// - public Attempt IsAuthorized(IUser? currentUser, - IUser? savingUser, - IEnumerable? startContentIds, IEnumerable? startMediaIds, - IEnumerable? userGroupAliases) + // b) If a start node is changing, a user cannot set a start node on another user that they don't have access to, this even goes for admins + + // only validate any start nodes that have changed. + // a user can remove any start nodes and add start nodes that they have access to + // but they cannot add a start node that they do not have access to + IEnumerable? changedStartContentIds = savingUser == null + ? startContentIds + : startContentIds == null || savingUser.StartContentIds is null + ? null + : startContentIds.Except(savingUser.StartContentIds).ToArray(); + IEnumerable? changedStartMediaIds = savingUser == null + ? startMediaIds + : startMediaIds == null || savingUser.StartMediaIds is null + ? null + : startMediaIds.Except(savingUser.StartMediaIds).ToArray(); + Attempt pathResult = currentUser is null + ? Attempt.Fail() + : AuthorizePath(currentUser, changedStartContentIds, changedStartMediaIds); + if (pathResult == false) { - var currentIsAdmin = currentUser?.IsAdmin() ?? false; + return pathResult; + } - // a) A non-admin cannot save an admin + // c) an admin can manage any group or section access + if (currentIsAdmin) + { + return Attempt.Succeed(); + } - if (savingUser != null) - { - if (savingUser.IsAdmin() && currentIsAdmin == false) - return Attempt.Fail("The current user is not an administrator so cannot save another administrator"); - } - - // b) If a start node is changing, a user cannot set a start node on another user that they don't have access to, this even goes for admins - - //only validate any start nodes that have changed. - //a user can remove any start nodes and add start nodes that they have access to - //but they cannot add a start node that they do not have access to - - var changedStartContentIds = savingUser == null - ? startContentIds - : startContentIds == null || savingUser.StartContentIds is null - ? null - : startContentIds.Except(savingUser.StartContentIds).ToArray(); - var changedStartMediaIds = savingUser == null - ? startMediaIds - : startMediaIds == null || savingUser.StartMediaIds is null - ? null - : startMediaIds.Except(savingUser.StartMediaIds).ToArray(); - var pathResult = currentUser is null ? Attempt.Fail() : AuthorizePath(currentUser, changedStartContentIds, changedStartMediaIds); - if (pathResult == false) - return pathResult; - - // c) an admin can manage any group or section access - - if (currentIsAdmin) - return Attempt.Succeed(); - - if (userGroupAliases != null) - { - var savingGroupAliases = userGroupAliases.ToArray(); - var existingGroupAliases = savingUser == null + if (userGroupAliases != null) + { + var savingGroupAliases = userGroupAliases.ToArray(); + var existingGroupAliases = savingUser == null ? new string[0] : savingUser.Groups.Select(x => x.Alias).ToArray(); - var addedGroupAliases = savingGroupAliases.Except(existingGroupAliases); + IEnumerable addedGroupAliases = savingGroupAliases.Except(existingGroupAliases); - // As we know the current user is not admin, it is only allowed to use groups that the user do have themselves. - var savingGroupAliasesNotAllowed = addedGroupAliases.Except(currentUser?.Groups.Select(x=> x.Alias) ?? Enumerable.Empty()).ToArray(); - if (savingGroupAliasesNotAllowed.Any()) + // As we know the current user is not admin, it is only allowed to use groups that the user do have themselves. + var savingGroupAliasesNotAllowed = addedGroupAliases + .Except(currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty()).ToArray(); + if (savingGroupAliasesNotAllowed.Any()) + { + return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) + + "', the current user is not part of them or admin"); + } + + // only validate any groups that have changed. + // a non-admin user can remove groups and add groups that they have access to + // but they cannot add a group that they do not have access to or that grants them + // path or section access that they don't have access to. + var newGroups = savingUser == null + ? savingGroupAliases + : savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray(); + + var userGroupsChanged = savingUser != null && newGroups.Length > 0; + + if (userGroupsChanged) + { + // d) A user cannot assign a group to another user that they do not belong to + var currentUserGroups = currentUser?.Groups.Select(x => x.Alias).ToArray(); + foreach (var group in newGroups) { - return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) + "', the current user is not part of them or admin"); - } - - //only validate any groups that have changed. - //a non-admin user can remove groups and add groups that they have access to - //but they cannot add a group that they do not have access to or that grants them - //path or section access that they don't have access to. - - var newGroups = savingUser == null - ? savingGroupAliases - : savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray(); - - var userGroupsChanged = savingUser != null && newGroups.Length > 0; - - if (userGroupsChanged) - { - // d) A user cannot assign a group to another user that they do not belong to - var currentUserGroups = currentUser?.Groups.Select(x => x.Alias).ToArray(); - foreach (var group in newGroups) + if (currentUserGroups?.Contains(group) == false) { - if (currentUserGroups?.Contains(group) == false) - { - return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member"); - } + return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member"); } } } - - return Attempt.Succeed(); } - private Attempt AuthorizePath(IUser currentUser, IEnumerable? startContentIds, IEnumerable? startMediaIds) + return Attempt.Succeed(); + } + + private Attempt AuthorizePath(IUser currentUser, IEnumerable? startContentIds, IEnumerable? startMediaIds) + { + if (startContentIds != null) { - if (startContentIds != null) + foreach (var contentId in startContentIds) { - foreach (var contentId in startContentIds) + if (contentId == Constants.System.Root) { - if (contentId == Constants.System.Root) + var hasAccess = ContentPermissions.HasPathAccess( + "-1", + currentUser.CalculateContentStartNodeIds(_entityService, _appCaches), + Constants.System.RecycleBinContent); + if (hasAccess == false) { - var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinContent); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the content root"); + return Attempt.Fail("The current user does not have access to the content root"); } - else + } + else + { + IContent? content = _contentService.GetById(contentId); + if (content == null) { - var content = _contentService.GetById(contentId); - if (content == null) continue; - var hasAccess = currentUser.HasPathAccess(content, _entityService, _appCaches); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the content path " + content.Path); + continue; + } + + var hasAccess = currentUser.HasPathAccess(content, _entityService, _appCaches); + if (hasAccess == false) + { + return Attempt.Fail("The current user does not have access to the content path " + + content.Path); } } } - - if (startMediaIds != null) - { - foreach (var mediaId in startMediaIds) - { - if (mediaId == Constants.System.Root) - { - var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinMedia); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the media root"); - } - else - { - var media = _mediaService.GetById(mediaId); - if (media == null) continue; - var hasAccess = currentUser.HasPathAccess(media, _entityService, _appCaches); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the media path " + media.Path); - } - } - } - - return Attempt.Succeed(); } + + if (startMediaIds != null) + { + foreach (var mediaId in startMediaIds) + { + if (mediaId == Constants.System.Root) + { + var hasAccess = ContentPermissions.HasPathAccess( + "-1", + currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches), + Constants.System.RecycleBinMedia); + if (hasAccess == false) + { + return Attempt.Fail("The current user does not have access to the media root"); + } + } + else + { + IMedia? media = _mediaService.GetById(mediaId); + if (media == null) + { + continue; + } + + var hasAccess = currentUser.HasPathAccess(media, _entityService, _appCaches); + if (hasAccess == false) + { + return Attempt.Fail("The current user does not have access to the media path " + media.Path); + } + } + } + } + + return Attempt.Succeed(); } } diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index ea28892f49..b03fa9d884 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -536,6 +536,7 @@ Vælg medlemsgruppe Vælg medlemstype Vælg node + Vælg sprog Vælg sektioner Vælg bruger Vælg brugere @@ -1028,12 +1029,14 @@ Umbraco: Nulstil adgangskode Dit brugernavn til at logge på Umbraco backoffice er: %0%

Klik her for at nulstille din adgangskode eller kopier/indsæt denne URL i din browser:

%1%

]]> + Umbraco: Sikkerhedskode + Din sikkerhedskode er: %0% Sidste skridt - Det er påkrævet at du verificerer din identitet. - Vælg venligst en autentificeringsmetode + Det er påkrævet at du bekræfter din identitet. + Vælg venligst en godkendelsesmetode Kode - Indtast venligst koden fra dit device - Koden kunne ikke genkendes + Indtast venligst bekræftelseskoden + Ugyldig kode indtastet Skrivebord @@ -1867,6 +1870,7 @@ Mange hilsner fra Umbraco robotten Sæt rettigheder for specifikke noder Profil Søg alle 'børn' + Tilføj sprog for at give brugerne adgang til at redigere Tilføj sektioner for at give brugerne adgang Vælg brugergrupper Ingen startnode valgt @@ -2181,18 +2185,19 @@ Mange hilsner fra Umbraco robotten være muligt. Indholdet vil blive vist som ikke understøttet indhold. + Kan ikke redigeres fordi elementtypen ikke eksisterer. Billede Tilføj billede Opret ny Udklipsholder Indstillinger Avanceret - Skjuld indholds editoren + Skjul indholdseditoren Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem? Annuller oprettelse? - Error! - The ElementType of this block does not exist anymore + Fejl! + Elementtypen for denne blok eksisterer ikke længere Tilføj indhold Tilføj %0% Feltet %0% bruger editor %1% som ikke er supporteret for blokke. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 1b93cacf01..e6ba39eb17 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -36,6 +36,7 @@ Restore Choose where to copy Choose where to move + Choose where to import to in the tree structure below Choose where to copy the selected item(s) Choose where to move the selected item(s) @@ -544,6 +545,7 @@ Select member group Select member type Select node + Select languages Select sections Select user Select users @@ -569,9 +571,15 @@ Modifying layout will result in loss of data for any existing content that is based on this configuration. + + To import a dictionary item, find the ".udt" file on your computer by clicking the + "Import" button (you'll be asked for confirmation on the next screen) + Dictionary item does not exist. Parent item does not exist. There are no dictionary items. + There are no dictionary items in this file. + There were no dictionary items found. Create dictionary item @@ -1041,7 +1049,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Happy super Sunday - Happy manic Monday + Happy marvelous Monday Happy tubular Tuesday Happy wonderful Wednesday Happy thunderous Thursday @@ -1592,6 +1600,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Invitation has been re-sent to %0% Document Type was exported to file An error occurred while exporting the Document Type + Dictionary item(s) was exported to file + An error occurred while exporting the dictionary item(s) + The following dictionary item(s) has been imported! Domains are not configured for multilingual site, please contact an administrator, see log for more information @@ -1621,6 +1632,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Editor + Production.]]> Failed to delete template with ID %0% Edit template Sections @@ -2080,6 +2092,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Set permissions for specific nodes Profile Search all children + Limit the languages users have access to edit Add sections to give users access Select user groups No start node selected @@ -2634,6 +2647,11 @@ To manage your website, simply open the Umbraco backoffice and start adding cont

Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

]]> + + Want to master Umbraco? Spend a few minutes learning some best practices by visiting the Umbraco Learning Base Youtube channel. Here you can find a bunch of video material covering many aspects of Umbraco.

+ ]]> +
To get you started @@ -2714,6 +2732,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont will no longer be available and will be shown as unsupported content. + Cannot be edited cause ElementType does not exist. Thumbnail Add thumbnail Create empty diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index b1dd754861..70aa1c2d5c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -37,6 +37,7 @@ Restore Choose where to copy Choose where to move + Choose where to import to in the tree structure below Choose where to copy the selected item(s) Choose where to move the selected item(s) @@ -319,6 +320,11 @@ Create new Paste from clipboard This item is in the Recycle Bin + Save is not allowed + Publish is not allowed + Send for approval is not allowed + Schedule is not allowed + Unpublish is not allowed %0%]]> @@ -553,6 +559,7 @@ Select member group Select member type Select node + Select languages Select sections Select user Select users @@ -579,9 +586,15 @@ Modifying layout will result in loss of data for any existing content that is based on this configuration. + + To import a dictionary item, find the ".udt" file on your computer by clicking the + "Import" button (you'll be asked for confirmation on the next screen) + Dictionary item does not exist. Parent item does not exist. There are no dictionary items. + There are no dictionary items in this file. + There were no dictionary items found. Create dictionary item @@ -662,6 +675,7 @@ %0% was renamed to %1% + Changing a property editor on a data type with stored values is disabled. To allow this you can change the Umbraco:CMS:DataTypes:CanBeChanged setting in appsettings.json. Add prevalue Database datatype Property editor GUID @@ -844,6 +858,7 @@ No items have been added Server Settings + Shared Show Show page on Send Size @@ -1061,7 +1076,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Happy super Sunday - Happy manic Monday + Happy marvelous Monday Happy tubular Tuesday Happy wonderful Wednesday Happy thunderous Thursday @@ -1451,6 +1466,10 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Not allowed Open media picker + + Select Property Editor + Select Property Editor + enter external link choose internal page @@ -1629,6 +1648,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Validation failed for language '%0%' Document Type was exported to file An error occurred while exporting the Document Type + Dictionary item(s) was exported to file + An error occurred while exporting the dictionary item(s) + The following dictionary item(s) has been imported! The release date cannot be in the past Cannot schedule the document for publishing since the required '%0%' is not published @@ -1669,6 +1691,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Rich Text Editor + Production.]]> Failed to delete template with ID %0% Edit template Sections @@ -1925,6 +1948,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Keep latest version per day for days Prevent cleanup NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]> + Changing a data type with stored values is disabled. To allow this you can change the Umbraco:CMS:DataTypes:CanBeChanged setting in appsettings.json. Add language @@ -1943,6 +1967,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Fall back language none + %0% is shared across all languages.]]> Add parameter @@ -2151,6 +2176,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Set permissions for specific nodes Profile Search all children + Limit the languages users have access to edit + Allow access to all languages Add sections to give users access Select user groups No start node selected @@ -2512,7 +2539,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Published Status Models Builder Health Check - Analytics + Telemetry data Profiling Getting Started Install Umbraco Forms @@ -2802,6 +2829,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont will no longer be available and will be shown as unsupported content. + Cannot be edited cause ElementType does not exist. Thumbnail Add thumbnail Create empty @@ -2869,8 +2897,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont items returned - Consent for analytics - Analytics level saved! + Consent for telemetry data + Telemetry level saved! We will send an anonymized site ID, umbraco version, and packages installed We will send: -
- Anonymized site ID, umbraco version, and packages installed. -
- Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use. -
- System information: Webserver, server OS, server framework, server OS language, and database provider. -
- Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode. -
-
We might change what we send on the Detailed level in the future. If so, it will be listed above. + We will send: +
    +
  • Anonymized site ID, umbraco version, and packages installed.
  • +
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use.
  • +
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • +
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.
  • +
+ We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymized information being collected.
]]>
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index 8f94e8694d..163fd14199 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -996,6 +996,14 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Umbraco: Wachtwoord Reset De gebruikersnaam om in te loggen bij jouw Umbraco omgeving is: %0%

Klik hier om je wachtwoord te resetten of knip/plak deze URL in je browser:

%1%

]]>
+ Umbraco: Beveiligingscode + Jouw beveiligingscode is: %0% + Laatste stap + Je hebt tweestapsverificatie ingeschakeld en moet je identiteit verifiëren. + Kies een tweestapsverificatie aanbieder + Verificatiecode + Vul de verificatiecode in + Ongeldige code ingevoerd Dashboard @@ -1436,6 +1444,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Rich Text Editor + Production.]]> Kan sjabloon met ID %0% niet verwijderen Sjabloon aanpassen Secties @@ -1934,6 +1943,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Wijzig je foto zodat andere gebruikers je makkelijk kunnen herkennen. Auteur + Configureer tweestapsverificatie Wijzig Je profiel Je recente historie @@ -2287,6 +2297,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Gebruikt in Mediatypes Gebruikt in Ledentypes Gebruikt door + Heeft verwijzingen vanuit de volgende items Gebruikt in Documenten Gebruikt in Leden Gebruikt in Media @@ -2501,6 +2512,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Fout! Het Elementtype van dit blok bestaat niet meer Inhoud toevoegen + Voeg %0% toe Eigenschap '%0%' gebruikt editor '%1%' die niet ondersteund wordt in blokken. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml index ad6f18b4cd..af3f157bf4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml @@ -123,6 +123,7 @@ Punktlista Numrerad lista Infoga macro + Skapa en ny dokumenttyp Infoga bild Publicera och stäng Ändra relation diff --git a/src/Umbraco.Core/Enum.cs b/src/Umbraco.Core/Enum.cs index 9ca1111a30..6084dfe971 100644 --- a/src/Umbraco.Core/Enum.cs +++ b/src/Umbraco.Core/Enum.cs @@ -1,110 +1,103 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Provides utility methods for handling enumerations. +/// +/// +/// Taken from http://damieng.com/blog/2010/10/17/enums-better-syntax-improved-performance-and-tryparse-in-net-3-5 +/// +public static class Enum + where T : struct { - /// - /// Provides utility methods for handling enumerations. - /// - /// - /// Taken from http://damieng.com/blog/2010/10/17/enums-better-syntax-improved-performance-and-tryparse-in-net-3-5 - /// - public static class Enum - where T : struct + private static readonly List Values; + private static readonly Dictionary InsensitiveNameToValue; + private static readonly Dictionary SensitiveNameToValue; + private static readonly Dictionary IntToValue; + private static readonly Dictionary ValueToName; + + static Enum() { - private static readonly List Values; - private static readonly Dictionary InsensitiveNameToValue; - private static readonly Dictionary SensitiveNameToValue; - private static readonly Dictionary IntToValue; - private static readonly Dictionary ValueToName; + Values = Enum.GetValues(typeof(T)).Cast().ToList(); - static Enum() + IntToValue = new Dictionary(); + ValueToName = new Dictionary(); + SensitiveNameToValue = new Dictionary(); + InsensitiveNameToValue = new Dictionary(); + + foreach (T value in Values) { - Values = Enum.GetValues(typeof(T)).Cast().ToList(); + var name = value.ToString(); - IntToValue = new Dictionary(); - ValueToName = new Dictionary(); - SensitiveNameToValue = new Dictionary(); - InsensitiveNameToValue = new Dictionary(); - - foreach (var value in Values) - { - var name = value.ToString(); - - IntToValue[Convert.ToInt32(value)] = value; - ValueToName[value] = name!; - SensitiveNameToValue[name!] = value; - InsensitiveNameToValue[name!.ToLowerInvariant()] = value; - } - } - - public static bool IsDefined(T value) - { - return ValueToName.Keys.Contains(value); - } - - public static bool IsDefined(string value) - { - return SensitiveNameToValue.Keys.Contains(value); - } - - public static bool IsDefined(int value) - { - return IntToValue.Keys.Contains(value); - } - - public static IEnumerable GetValues() - { - return Values; - } - - public static string[] GetNames() - { - return ValueToName.Values.ToArray(); - } - - public static string? GetName(T value) - { - return ValueToName.TryGetValue(value, out var name) ? name : null; - } - - public static T Parse(string value, bool ignoreCase = false) - { - var names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; - if (ignoreCase) value = value.ToLowerInvariant(); - - if (names.TryGetValue(value, out var parsed)) - return parsed; - - throw new ArgumentException($"Value \"{value}\"is not a valid {typeof(T).Name} enumeration value.", nameof(value)); - } - - public static bool TryParse(string value, out T returnValue, bool ignoreCase = false) - { - var names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; - if (ignoreCase) value = value.ToLowerInvariant(); - return names.TryGetValue(value, out returnValue); - } - - public static T? ParseOrNull(string value) - { - if (string.IsNullOrWhiteSpace(value)) - return null; - - if (InsensitiveNameToValue.TryGetValue(value.ToLowerInvariant(), out var parsed)) - return parsed; - - return null; - } - - public static T? CastOrNull(int value) - { - if (IntToValue.TryGetValue(value, out var foundValue)) - return foundValue; - - return null; + IntToValue[Convert.ToInt32(value)] = value; + ValueToName[value] = name!; + SensitiveNameToValue[name!] = value; + InsensitiveNameToValue[name!.ToLowerInvariant()] = value; } } + + public static bool IsDefined(T value) => ValueToName.Keys.Contains(value); + + public static bool IsDefined(string value) => SensitiveNameToValue.Keys.Contains(value); + + public static bool IsDefined(int value) => IntToValue.Keys.Contains(value); + + public static IEnumerable GetValues() => Values; + + public static string[] GetNames() => ValueToName.Values.ToArray(); + + public static string? GetName(T value) => ValueToName.TryGetValue(value, out var name) ? name : null; + + public static T Parse(string value, bool ignoreCase = false) + { + Dictionary names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; + if (ignoreCase) + { + value = value.ToLowerInvariant(); + } + + if (names.TryGetValue(value, out T parsed)) + { + return parsed; + } + + throw new ArgumentException( + $"Value \"{value}\"is not a valid {typeof(T).Name} enumeration value.", + nameof(value)); + } + + public static bool TryParse(string value, out T returnValue, bool ignoreCase = false) + { + Dictionary names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; + if (ignoreCase) + { + value = value.ToLowerInvariant(); + } + + return names.TryGetValue(value, out returnValue); + } + + public static T? ParseOrNull(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + if (InsensitiveNameToValue.TryGetValue(value.ToLowerInvariant(), out T parsed)) + { + return parsed; + } + + return null; + } + + public static T? CastOrNull(int value) + { + if (IntToValue.TryGetValue(value, out T foundValue)) + { + return foundValue; + } + + return null; + } } diff --git a/src/Umbraco.Core/EnvironmentHelper.cs b/src/Umbraco.Core/EnvironmentHelper.cs index 097ffc9629..04b3bc91ff 100644 --- a/src/Umbraco.Core/EnvironmentHelper.cs +++ b/src/Umbraco.Core/EnvironmentHelper.cs @@ -1,17 +1,14 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Currently just used to get the machine name for use with file names +/// +internal class EnvironmentHelper { /// - /// Currently just used to get the machine name for use with file names + /// Returns the machine name that is safe to use in file paths. /// - internal class EnvironmentHelper - { - /// - /// Returns the machine name that is safe to use in file paths. - /// - public static string FileSafeMachineName => Environment.MachineName.ReplaceNonAlphanumericChars('-'); - - } + public static string FileSafeMachineName => Environment.MachineName.ReplaceNonAlphanumericChars('-'); } diff --git a/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs index c9958a5fc9..22c7ef4c7e 100644 --- a/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs @@ -1,59 +1,79 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Represents event data, for events that support cancellation, and expose impacted objects. +/// +/// The type of the exposed, impacted objects. +public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, + IEquatable> { - /// - /// Represents event data, for events that support cancellation, and expose impacted objects. - /// - /// The type of the exposed, impacted objects. - public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, IEquatable> + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) { - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) - { } + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) - : base(eventObject, canCancel) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) - : base(eventObject) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) + : base(eventObject) + { + } - public bool Equals(CancellableEnumerableObjectEventArgs? other) + public bool Equals(CancellableEnumerableObjectEventArgs? other) + { + if (other is null || other.EventObject is null) { - if (other is null || other.EventObject is null) return false; - if (ReferenceEquals(this, other)) return true; - - return EventObject?.SequenceEqual(other.EventObject) ?? false; + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, other)) { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CancellableEnumerableObjectEventArgs)obj); + return true; } - public override int GetHashCode() - { - if (EventObject is not null) - { - return HashCodeHelper.GetHashCode(EventObject); - } + return EventObject?.SequenceEqual(other.EventObject) ?? false; + } - return base.GetHashCode(); + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((CancellableEnumerableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + if (EventObject is not null) + { + return HashCodeHelper.GetHashCode(EventObject); + } + + return base.GetHashCode(); } } diff --git a/src/Umbraco.Core/Events/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs index a991f6532b..7768da05f5 100644 --- a/src/Umbraco.Core/Events/CancellableEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs @@ -1,141 +1,157 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Represents event data for events that support cancellation. +/// +public class CancellableEventArgs : EventArgs, IEquatable { - /// - /// Represents event data for events that support cancellation. - /// - public class CancellableEventArgs : EventArgs, IEquatable + private static readonly ReadOnlyDictionary EmptyAdditionalData = new(new Dictionary()); + + private bool _cancel; + private IDictionary? _eventState; + + public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) { - private bool _cancel; - private IDictionary? _eventState; + CanCancel = canCancel; + Messages = messages; + AdditionalData = new ReadOnlyDictionary(additionalData); + } - private static readonly ReadOnlyDictionary EmptyAdditionalData = new ReadOnlyDictionary(new Dictionary()); + public CancellableEventArgs(bool canCancel, EventMessages eventMessages) + { + CanCancel = canCancel; + Messages = eventMessages ?? throw new ArgumentNullException("eventMessages"); + AdditionalData = EmptyAdditionalData; + } - public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) + public CancellableEventArgs(bool canCancel) + { + CanCancel = canCancel; + + // create a standalone messages + Messages = new EventMessages(); + AdditionalData = EmptyAdditionalData; + } + + public CancellableEventArgs(EventMessages eventMessages) + : this(true, eventMessages) + { + } + + public CancellableEventArgs() + : this(true) + { + } + + /// + /// Flag to determine if this instance will support being cancellable + /// + public bool CanCancel { get; set; } + + /// + /// If this instance supports cancellation, this gets/sets the cancel value + /// + public bool Cancel + { + get { - CanCancel = canCancel; - Messages = messages; - AdditionalData = new ReadOnlyDictionary(additionalData); - } - - public CancellableEventArgs(bool canCancel, EventMessages eventMessages) - { - if (eventMessages == null) throw new ArgumentNullException("eventMessages"); - CanCancel = canCancel; - Messages = eventMessages; - AdditionalData = EmptyAdditionalData; - } - - public CancellableEventArgs(bool canCancel) - { - CanCancel = canCancel; - //create a standalone messages - Messages = new EventMessages(); - AdditionalData = EmptyAdditionalData; - } - - public CancellableEventArgs(EventMessages eventMessages) - : this(true, eventMessages) - { } - - public CancellableEventArgs() - : this(true) - { } - - /// - /// Flag to determine if this instance will support being cancellable - /// - public bool CanCancel { get; set; } - - /// - /// If this instance supports cancellation, this gets/sets the cancel value - /// - public bool Cancel - { - get + if (CanCancel == false) { - if (CanCancel == false) - { - throw new InvalidOperationException("This event argument class does not support canceling."); - } - return _cancel; + throw new InvalidOperationException("This event argument class does not support canceling."); } - set + + return _cancel; + } + + set + { + if (CanCancel == false) { - if (CanCancel == false) - { - throw new InvalidOperationException("This event argument class does not support canceling."); - } - _cancel = value; + throw new InvalidOperationException("This event argument class does not support canceling."); } - } - /// - /// if this instance supports cancellation, this will set Cancel to true with an affiliated cancellation message - /// - /// - public void CancelOperation(EventMessage cancelationMessage) - { - Cancel = true; - cancelationMessage.IsDefaultEventMessage = true; - Messages.Add(cancelationMessage); - } - - /// - /// Returns the EventMessages object which is used to add messages to the message collection for this event - /// - public EventMessages Messages { get; } - - /// - /// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event subscribers - /// - /// - /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility - /// so we cannot change the strongly typed nature for some events. - /// - public ReadOnlyDictionary AdditionalData { get; set; } - - /// - /// This can be used by event subscribers to store state in the event args so they easily deal with custom state data between a starting ("ing") - /// event and an ending ("ed") event - /// - public IDictionary EventState - { - get => _eventState ?? (_eventState = new Dictionary()); - set => _eventState = value; - } - - public bool Equals(CancellableEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(AdditionalData, other.AdditionalData); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((CancellableEventArgs) obj); - } - - public override int GetHashCode() - { - return AdditionalData != null ? AdditionalData.GetHashCode() : 0; - } - - public static bool operator ==(CancellableEventArgs? left, CancellableEventArgs? right) - { - return Equals(left, right); - } - - public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right) - { - return Equals(left, right) == false; + _cancel = value; } } + + /// + /// Returns the EventMessages object which is used to add messages to the message collection for this event + /// + public EventMessages Messages { get; } + + /// + /// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event + /// subscribers + /// + /// + /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards + /// compatibility + /// so we cannot change the strongly typed nature for some events. + /// + public ReadOnlyDictionary AdditionalData { get; set; } + + /// + /// This can be used by event subscribers to store state in the event args so they easily deal with custom state data + /// between a starting ("ing") + /// event and an ending ("ed") event + /// + public IDictionary EventState + { + get => _eventState ??= new Dictionary(); + set => _eventState = value; + } + + public static bool operator ==(CancellableEventArgs? left, CancellableEventArgs? right) => Equals(left, right); + + public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right) => Equals(left, right) == false; + + public bool Equals(CancellableEventArgs? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Equals(AdditionalData, other.AdditionalData); + } + + /// + /// if this instance supports cancellation, this will set Cancel to true with an affiliated cancellation message + /// + /// + public void CancelOperation(EventMessage cancelationMessage) + { + Cancel = true; + cancelationMessage.IsDefaultEventMessage = true; + Messages.Add(cancelationMessage); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((CancellableEventArgs)obj); + } + + public override int GetHashCode() => AdditionalData != null ? AdditionalData.GetHashCode() : 0; } diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index 2697b773c2..26aa61b67a 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,46 +1,38 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Provides a base class for classes representing event data, for events that support cancellation, and expose an +/// impacted object. +/// +public abstract class CancellableObjectEventArgs : CancellableEventArgs { - /// - /// Provides a base class for classes representing event data, for events that support cancellation, and expose an impacted object. - /// - public abstract class CancellableObjectEventArgs : CancellableEventArgs + protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(canCancel, messages, additionalData) => + EventObject = eventObject; + + protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages eventMessages) + : base(canCancel, eventMessages) => + EventObject = eventObject; + + protected CancellableObjectEventArgs(object? eventObject, EventMessages eventMessages) + : this(eventObject, true, eventMessages) { - protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(canCancel, messages, additionalData) - { - EventObject = eventObject; - } - - protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages eventMessages) - : base(canCancel, eventMessages) - { - EventObject = eventObject; - } - - protected CancellableObjectEventArgs(object? eventObject, EventMessages eventMessages) - : this(eventObject, true, eventMessages) - { - } - - protected CancellableObjectEventArgs(object? eventObject, bool canCancel) - : base(canCancel) - { - EventObject = eventObject; - } - - protected CancellableObjectEventArgs(object? eventObject) - : this(eventObject, true) - { - } - - /// - /// Gets or sets the impacted object. - /// - /// - /// This is protected so that inheritors can expose it with their own name - /// - public object? EventObject { get; set; } } + + protected CancellableObjectEventArgs(object? eventObject, bool canCancel) + : base(canCancel) => + EventObject = eventObject; + + protected CancellableObjectEventArgs(object? eventObject) + : this(eventObject, true) + { + } + + /// + /// Gets or sets the impacted object. + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + public object? EventObject { get; set; } } diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs index 939fd8e11b..5d9865c253 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs @@ -1,87 +1,102 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Represent event data, for events that support cancellation, and expose an impacted object. +/// +/// The type of the exposed, impacted object. +public class CancellableObjectEventArgs : CancellableObjectEventArgs, + IEquatable> { - /// - /// Represent event data, for events that support cancellation, and expose an impacted object. - /// - /// The type of the exposed, impacted object. - public class CancellableObjectEventArgs : CancellableObjectEventArgs, IEquatable> + public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) { - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) + } + + public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } + + public CancellableObjectEventArgs(TEventObject? eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } + + public CancellableObjectEventArgs(TEventObject? eventObject) + : base(eventObject) + { + } + + /// + /// Gets or sets the impacted object. + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + protected new TEventObject? EventObject + { + get => (TEventObject?)base.EventObject; + set => base.EventObject = value; + } + + public static bool operator ==( + CancellableObjectEventArgs left, + CancellableObjectEventArgs right) => Equals(left, right); + + public static bool operator !=( + CancellableObjectEventArgs left, + CancellableObjectEventArgs right) => !Equals(left, right); + + public bool Equals(CancellableObjectEventArgs? other) + { + if (other is null) { + return false; } - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + if (ReferenceEquals(this, other)) { + return true; } - public CancellableObjectEventArgs(TEventObject? eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) + return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); + } + + public override bool Equals(object? obj) + { + if (obj is null) { + return false; } - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel) - : base(eventObject, canCancel) + if (ReferenceEquals(this, obj)) { + return true; } - public CancellableObjectEventArgs(TEventObject? eventObject) - : base(eventObject) + if (obj.GetType() != GetType()) { + return false; } - /// - /// Gets or sets the impacted object. - /// - /// - /// This is protected so that inheritors can expose it with their own name - /// - protected new TEventObject? EventObject - { - get => (TEventObject?) base.EventObject; - set => base.EventObject = value; - } + return Equals((CancellableObjectEventArgs)obj); + } - public bool Equals(CancellableObjectEventArgs? other) + public override int GetHashCode() + { + unchecked { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); - } - - public override bool Equals(object? obj) - { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CancellableObjectEventArgs)obj); - } - - public override int GetHashCode() - { - unchecked + if (EventObject is not null) { - if (EventObject is not null) - { - return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); - } - - return base.GetHashCode() * 397; + return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); } - } - public static bool operator ==(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return !Equals(left, right); + return base.GetHashCode() * 397; } } } diff --git a/src/Umbraco.Core/Events/ContentCacheEventArgs.cs b/src/Umbraco.Core/Events/ContentCacheEventArgs.cs index 78f714f754..732e6f2452 100644 --- a/src/Umbraco.Core/Events/ContentCacheEventArgs.cs +++ b/src/Umbraco.Core/Events/ContentCacheEventArgs.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Cms.Core.Events +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Events; + +public class ContentCacheEventArgs : CancelEventArgs { - public class ContentCacheEventArgs : System.ComponentModel.CancelEventArgs { } } diff --git a/src/Umbraco.Core/Events/CopyEventArgs.cs b/src/Umbraco.Core/Events/CopyEventArgs.cs index 6a4969710a..bead8213b6 100644 --- a/src/Umbraco.Core/Events/CopyEventArgs.cs +++ b/src/Umbraco.Core/Events/CopyEventArgs.cs @@ -1,91 +1,99 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class CopyEventArgs : CancellableObjectEventArgs, IEquatable> { - public class CopyEventArgs : CancellableObjectEventArgs, IEquatable> + public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) + : base(original, canCancel) { - public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) - : base(original, canCancel) + Copy = copy; + ParentId = parentId; + } + + public CopyEventArgs(TEntity eventObject, TEntity copy, int parentId) + : base(eventObject) + { + Copy = copy; + ParentId = parentId; + } + + public CopyEventArgs(TEntity eventObject, TEntity copy, bool canCancel, int parentId, bool relateToOriginal) + : base(eventObject, canCancel) + { + Copy = copy; + ParentId = parentId; + RelateToOriginal = relateToOriginal; + } + + /// + /// The copied entity + /// + public TEntity Copy { get; set; } + + /// + /// The original entity + /// + public TEntity? Original => EventObject; + + /// + /// Gets or Sets the Id of the objects new parent. + /// + public int ParentId { get; } + + public bool RelateToOriginal { get; set; } + + public static bool operator ==(CopyEventArgs left, CopyEventArgs right) => Equals(left, right); + + public bool Equals(CopyEventArgs? other) + { + if (ReferenceEquals(null, other)) { - Copy = copy; - ParentId = parentId; + return false; } - public CopyEventArgs(TEntity eventObject, TEntity copy, int parentId) - : base(eventObject) + if (ReferenceEquals(this, other)) { - Copy = copy; - ParentId = parentId; + return true; } - public CopyEventArgs(TEntity eventObject, TEntity copy, bool canCancel, int parentId, bool relateToOriginal) - : base(eventObject, canCancel) + return base.Equals(other) && EqualityComparer.Default.Equals(Copy, other.Copy) && + ParentId == other.ParentId && RelateToOriginal == other.RelateToOriginal; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - Copy = copy; - ParentId = parentId; - RelateToOriginal = relateToOriginal; + return false; } - /// - /// The copied entity - /// - public TEntity Copy { get; set; } - - /// - /// The original entity - /// - public TEntity? Original + if (ReferenceEquals(this, obj)) { - get { return EventObject; } + return true; } - /// - /// Gets or Sets the Id of the objects new parent. - /// - public int ParentId { get; private set; } - - public bool RelateToOriginal { get; set; } - - public bool Equals(CopyEventArgs? other) + if (obj.GetType() != GetType()) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && EqualityComparer.Default.Equals(Copy, other.Copy) && ParentId == other.ParentId && RelateToOriginal == other.RelateToOriginal; + return false; } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CopyEventArgs) obj); - } + return Equals((CopyEventArgs)obj); + } - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked + var hashCode = base.GetHashCode(); + if (Copy is not null) { - int hashCode = base.GetHashCode(); - if (Copy is not null) - { - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Copy); - } - - hashCode = (hashCode * 397) ^ ParentId; - hashCode = (hashCode * 397) ^ RelateToOriginal.GetHashCode(); - return hashCode; + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Copy); } - } - public static bool operator ==(CopyEventArgs left, CopyEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(CopyEventArgs left, CopyEventArgs right) - { - return !Equals(left, right); + hashCode = (hashCode * 397) ^ ParentId; + hashCode = (hashCode * 397) ^ RelateToOriginal.GetHashCode(); + return hashCode; } } + + public static bool operator !=(CopyEventArgs left, CopyEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs index 1696e07ec6..3ca366834f 100644 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs @@ -1,202 +1,209 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +[SupersedeEvent(typeof(SaveEventArgs<>))] +[SupersedeEvent(typeof(PublishEventArgs<>))] +[SupersedeEvent(typeof(MoveEventArgs<>))] +[SupersedeEvent(typeof(CopyEventArgs<>))] +public class DeleteEventArgs : CancellableEnumerableObjectEventArgs, + IEquatable>, IDeletingMediaFilesEventArgs { - [SupersedeEvent(typeof(SaveEventArgs<>))] - [SupersedeEvent(typeof(PublishEventArgs<>))] - [SupersedeEvent(typeof(MoveEventArgs<>))] - [SupersedeEvent(typeof(CopyEventArgs<>))] - public class DeleteEventArgs : CancellableEnumerableObjectEventArgs, IEquatable>, IDeletingMediaFilesEventArgs + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) => MediaFilesToDelete = new List(); + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base( + eventObject, + eventMessages) => MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + public DeleteEventArgs(IEnumerable eventObject) + : base(eventObject) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + public DeleteEventArgs(TEntity eventObject) + : base(new List { eventObject }) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, bool canCancel) + : base(new List { eventObject }, canCancel) => + MediaFilesToDelete = new List(); + + /// + /// Returns all entities that were deleted during the operation + /// + public IEnumerable DeletedEntities { - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) : base(eventObject, canCancel, eventMessages) + get => EventObject ?? Enumerable.Empty(); + set => EventObject = value; + } + + /// + /// A list of media files that can be added to during a deleted operation for which Umbraco will ensure are removed + /// + public List MediaFilesToDelete { get; } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) => + Equals(left, right); + + public bool Equals(DeleteEventArgs? other) + { + if (ReferenceEquals(null, other)) { - MediaFilesToDelete = new List(); + return false; } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) + if (ReferenceEquals(this, other)) { - MediaFilesToDelete = new List(); + return true; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) + return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - MediaFilesToDelete = new List(); + return false; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) + if (ReferenceEquals(this, obj)) { - MediaFilesToDelete = new List(); + return true; } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, bool canCancel) : base(eventObject, canCancel) + if (obj.GetType() != GetType()) { - MediaFilesToDelete = new List(); + return false; } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - public DeleteEventArgs(IEnumerable eventObject) : base(eventObject) - { - MediaFilesToDelete = new List(); - } + return Equals((DeleteEventArgs)obj); + } - /// - /// Constructor accepting a single entity instance - /// - /// - public DeleteEventArgs(TEntity eventObject) - : base(new List { eventObject }) + public override int GetHashCode() + { + unchecked { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, bool canCancel) - : base(new List { eventObject }, canCancel) - { - MediaFilesToDelete = new List(); - } - - /// - /// Returns all entities that were deleted during the operation - /// - public IEnumerable DeletedEntities - { - get => EventObject ?? Enumerable.Empty(); - set => EventObject = value; - } - - /// - /// A list of media files that can be added to during a deleted operation for which Umbraco will ensure are removed - /// - public List MediaFilesToDelete { get; private set; } - - public bool Equals(DeleteEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((DeleteEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); - } - } - - public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) - { - return !Equals(left, right); + return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); } } - public class DeleteEventArgs : CancellableEventArgs, IEquatable - { - public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) - : base(canCancel, eventMessages) - { - Id = id; - } - - public DeleteEventArgs(int id, bool canCancel) - : base(canCancel) - { - Id = id; - } - - public DeleteEventArgs(int id) - { - Id = id; - } - - /// - /// Gets the Id of the object being deleted. - /// - public int Id { get; private set; } - - public bool Equals(DeleteEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && Id == other.Id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((DeleteEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ Id; - } - } - - public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) - { - return !Equals(left, right); - } - } + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) => + !Equals(left, right); +} + +public class DeleteEventArgs : CancellableEventArgs, IEquatable +{ + public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) + : base(canCancel, eventMessages) => + Id = id; + + public DeleteEventArgs(int id, bool canCancel) + : base(canCancel) => + Id = id; + + public DeleteEventArgs(int id) => Id = id; + + /// + /// Gets the Id of the object being deleted. + /// + public int Id { get; } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) => Equals(left, right); + + public bool Equals(DeleteEventArgs? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((DeleteEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ Id; + } + } + + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs index e27c155ec4..d298f5bbec 100644 --- a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs +++ b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs @@ -1,183 +1,188 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Contains types and methods that allow publishing general notifications. +/// +public partial class EventAggregator : IEventAggregator { - /// - /// Contains types and methods that allow publishing general notifications. - /// - public partial class EventAggregator : IEventAggregator + private static readonly ConcurrentDictionary NotificationAsyncHandlers + = new(); + + private static readonly ConcurrentDictionary NotificationHandlers = new(); + + private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default) { - private static readonly ConcurrentDictionary s_notificationAsyncHandlers - = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary s_notificationHandlers - = new ConcurrentDictionary(); - - private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default) - { - Type notificationType = notification.GetType(); - NotificationAsyncHandlerWrapper asyncHandler = s_notificationAsyncHandlers.GetOrAdd( - notificationType, - t => - { - var value = Activator.CreateInstance( - typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(notificationType)); - return value is not null - ? (NotificationAsyncHandlerWrapper)value - : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); - }); - - return asyncHandler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync); - } - - private void PublishNotification(INotification notification) - { - Type notificationType = notification.GetType(); - NotificationHandlerWrapper? asyncHandler = s_notificationHandlers.GetOrAdd( - notificationType, - t => - { - var value = Activator.CreateInstance( - typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(notificationType)); - return value is not null ? (NotificationHandlerWrapper)value : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); - }); - - asyncHandler?.Handle(notification, _serviceFactory, PublishCore); - } - - private async Task PublishCoreAsync( - IEnumerable> allHandlers, - INotification notification, - CancellationToken cancellationToken) - { - foreach (Func handler in allHandlers) + Type notificationType = notification.GetType(); + NotificationAsyncHandlerWrapper asyncHandler = NotificationAsyncHandlers.GetOrAdd( + notificationType, + t => { - await handler(notification, cancellationToken).ConfigureAwait(false); - } - } + var value = Activator.CreateInstance( + typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(notificationType)); + return value is not null + ? (NotificationAsyncHandlerWrapper)value + : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); + }); - private void PublishCore( - IEnumerable> allHandlers, - INotification notification) - { - foreach (Action handler in allHandlers) + return asyncHandler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync); + } + + private void PublishNotification(INotification notification) + { + Type notificationType = notification.GetType(); + NotificationHandlerWrapper? asyncHandler = NotificationHandlers.GetOrAdd( + notificationType, + t => { - handler(notification); - } + var value = Activator.CreateInstance( + typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(notificationType)); + return value is not null + ? (NotificationHandlerWrapper)value + : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); + }); + + asyncHandler?.Handle(notification, _serviceFactory, PublishCore); + } + + private async Task PublishCoreAsync( + IEnumerable> allHandlers, + INotification notification, + CancellationToken cancellationToken) + { + foreach (Func handler in allHandlers) + { + await handler(notification, cancellationToken).ConfigureAwait(false); } } - internal abstract class NotificationHandlerWrapper + private void PublishCore( + IEnumerable> allHandlers, + INotification notification) { - public abstract void Handle( - INotification notification, - ServiceFactory serviceFactory, - Action>, INotification> publish); - } - - internal abstract class NotificationAsyncHandlerWrapper - { - public abstract Task HandleAsync( - INotification notification, - CancellationToken cancellationToken, - ServiceFactory serviceFactory, - Func>, INotification, CancellationToken, Task> publish); - } - - internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper - where TNotification : INotification - { - /// - /// - /// Background - During v9 build we wanted an in-process message bus to facilitate removal of the old static event handlers.
- /// Instead of taking a dependency on MediatR we (the community) implemented our own using MediatR as inspiration. - ///
- /// - /// - /// Some things worth knowing about MediatR. - /// - /// All handlers are by default registered with transient lifetime, but can easily depend on services with state. - /// Both the Mediatr instance and its handler resolver are registered transient and as such it is always possible to depend on scoped services in a handler. - /// - /// - /// - /// - /// Our EventAggregator started out registered with a transient lifetime but later (before initial release) the registration was changed to singleton, presumably - /// because there are a lot of singleton services in Umbraco which like to publish notifications and it's a pain to use scoped services from a singleton. - ///
- /// The problem with a singleton EventAggregator is it forces handlers to create a service scope and service locate any scoped services - /// they wish to make use of e.g. a unit of work (think entity framework DBContext). - ///
- /// - /// - /// Moving forwards it probably makes more sense to register EventAggregator transient but doing so now would mean an awful lot of service location to avoid breaking changes. - ///
- /// For now we can do the next best thing which is to create a scope for each published notification, thus enabling the transient handlers to take a dependency on a scoped service. - ///
- /// - /// - /// Did discuss using HttpContextAccessor/IScopedServiceProvider to enable sharing of scopes when publisher has http context, - /// but decided against because it's inconsistent with what happens in background threads and will just cause confusion. - /// - ///
- public override Task HandleAsync( - INotification notification, - CancellationToken cancellationToken, - ServiceFactory serviceFactory, - Func>, INotification, CancellationToken, Task> publish) + foreach (Action handler in allHandlers) { - // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. - // TODO: go back to using ServiceFactory to resolve - IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); - using IServiceScope scope = scopeFactory.CreateScope(); - IServiceProvider container = scope.ServiceProvider; - - IEnumerable> handlers = container - .GetServices>() - .Select(x => new Func( - (theNotification, theToken) => - x.HandleAsync((TNotification)theNotification, theToken))); - - return publish(handlers, notification, cancellationToken); - } - } - - internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper - where TNotification : INotification - { - /// - /// See remarks on for explanation on - /// what's going on with the IServiceProvider stuff here. - /// - public override void Handle( - INotification notification, - ServiceFactory serviceFactory, - Action>, INotification> publish) - { - // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. - // TODO: go back to using ServiceFactory to resolve - IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); - using IServiceScope scope = scopeFactory.CreateScope(); - IServiceProvider container = scope.ServiceProvider; - - IEnumerable> handlers = container - .GetServices>() - .Select(x => new Action( - (theNotification) => - x.Handle((TNotification)theNotification))); - - publish(handlers, notification); + handler(notification); } } } + +internal abstract class NotificationHandlerWrapper +{ + public abstract void Handle( + INotification notification, + ServiceFactory serviceFactory, + Action>, INotification> publish); +} + +internal abstract class NotificationAsyncHandlerWrapper +{ + public abstract Task HandleAsync( + INotification notification, + CancellationToken cancellationToken, + ServiceFactory serviceFactory, + Func>, INotification, CancellationToken, Task> + publish); +} + +internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper + where TNotification : INotification +{ + /// + /// + /// Background - During v9 build we wanted an in-process message bus to facilitate removal of the old static event + /// handlers.
+ /// Instead of taking a dependency on MediatR we (the community) implemented our own using MediatR as inspiration. + ///
+ /// + /// Some things worth knowing about MediatR. + /// + /// + /// All handlers are by default registered with transient lifetime, but can easily depend on services + /// with state. + /// + /// + /// Both the Mediatr instance and its handler resolver are registered transient and as such it is always + /// possible to depend on scoped services in a handler. + /// + /// + /// + /// + /// Our EventAggregator started out registered with a transient lifetime but later (before initial release) the + /// registration was changed to singleton, presumably + /// because there are a lot of singleton services in Umbraco which like to publish notifications and it's a pain to + /// use scoped services from a singleton. + ///
+ /// The problem with a singleton EventAggregator is it forces handlers to create a service scope and service locate + /// any scoped services + /// they wish to make use of e.g. a unit of work (think entity framework DBContext). + ///
+ /// + /// Moving forwards it probably makes more sense to register EventAggregator transient but doing so now would mean + /// an awful lot of service location to avoid breaking changes. + ///
+ /// For now we can do the next best thing which is to create a scope for each published notification, thus enabling + /// the transient handlers to take a dependency on a scoped service. + ///
+ /// + /// Did discuss using HttpContextAccessor/IScopedServiceProvider to enable sharing of scopes when publisher has + /// http context, + /// but decided against because it's inconsistent with what happens in background threads and will just cause + /// confusion. + /// + ///
+ public override Task HandleAsync( + INotification notification, + CancellationToken cancellationToken, + ServiceFactory serviceFactory, + Func>, INotification, CancellationToken, Task> publish) + { + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() + .Select(x => new Func( + (theNotification, theToken) => + x.HandleAsync((TNotification)theNotification, theToken))); + + return publish(handlers, notification, cancellationToken); + } +} + +internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper + where TNotification : INotification +{ + /// + /// See remarks on for explanation on + /// what's going on with the IServiceProvider stuff here. + /// + public override void Handle( + INotification notification, + ServiceFactory serviceFactory, + Action>, INotification> publish) + { + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() + .Select(x => new Action( + theNotification => + x.Handle((TNotification)theNotification))); + + publish(handlers, notification); + } +} diff --git a/src/Umbraco.Core/Events/EventAggregator.cs b/src/Umbraco.Core/Events/EventAggregator.cs index 5bf54b516a..277b24eb06 100644 --- a/src/Umbraco.Core/Events/EventAggregator.cs +++ b/src/Umbraco.Core/Events/EventAggregator.cs @@ -1,117 +1,112 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// A factory method used to resolve all services. +/// For multiple instances, it will resolve against . +/// +/// Type of service to resolve. +/// An instance of type . +public delegate object ServiceFactory(Type serviceType); + +/// +/// Extensions for . +/// +public static class ServiceFactoryExtensions { /// - /// A factory method used to resolve all services. - /// For multiple instances, it will resolve against . + /// Gets an instance of . /// - /// Type of service to resolve. - /// An instance of type . - public delegate object ServiceFactory(Type serviceType); - - /// - public partial class EventAggregator : IEventAggregator - { - private readonly ServiceFactory _serviceFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The service instance factory. - public EventAggregator(ServiceFactory serviceFactory) - => _serviceFactory = serviceFactory; - - /// - public Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) - where TNotification : INotification - { - // TODO: Introduce codegen efficient Guard classes to reduce noise. - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - PublishNotification(notification); - return PublishNotificationAsync(notification, cancellationToken); - } - - /// - public void Publish(TNotification notification) - where TNotification : INotification - { - // TODO: Introduce codegen efficient Guard classes to reduce noise. - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - PublishNotification(notification); - var task = PublishNotificationAsync(notification); - if (task is not null) - { - Task.WaitAll(task); - } - } - - public bool PublishCancelable(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification - { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - Publish(notification); - return notification.Cancel; - } - - public async Task PublishCancelableAsync(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification - { - if (notification is null) - { - throw new ArgumentNullException(nameof(notification)); - } - - Task? task = PublishAsync(notification); - if (task is not null) - { - await task; - } - - return notification.Cancel; - } - } + /// The type to return. + /// The service factory. + /// The new instance. + public static T GetInstance(this ServiceFactory factory) + => (T)factory(typeof(T)); /// - /// Extensions for . + /// Gets a collection of instances of . /// - public static class ServiceFactoryExtensions - { - /// - /// Gets an instance of . - /// - /// The type to return. - /// The service factory. - /// The new instance. - public static T GetInstance(this ServiceFactory factory) - => (T)factory(typeof(T)); + /// The collection item type to return. + /// The service factory. + /// The new instance collection. + public static IEnumerable GetInstances(this ServiceFactory factory) + => (IEnumerable)factory(typeof(IEnumerable)); +} - /// - /// Gets a collection of instances of . - /// - /// The collection item type to return. - /// The service factory. - /// The new instance collection. - public static IEnumerable GetInstances(this ServiceFactory factory) - => (IEnumerable)factory(typeof(IEnumerable)); +/// +public partial class EventAggregator : IEventAggregator +{ + private readonly ServiceFactory _serviceFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The service instance factory. + public EventAggregator(ServiceFactory serviceFactory) + => _serviceFactory = serviceFactory; + + /// + public Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification + { + // TODO: Introduce codegen efficient Guard classes to reduce noise. + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + PublishNotification(notification); + return PublishNotificationAsync(notification, cancellationToken); + } + + /// + public void Publish(TNotification notification) + where TNotification : INotification + { + // TODO: Introduce codegen efficient Guard classes to reduce noise. + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + PublishNotification(notification); + Task task = PublishNotificationAsync(notification); + if (task is not null) + { + Task.WaitAll(task); + } + } + + public bool PublishCancelable(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + Publish(notification); + return notification.Cancel; + } + + public async Task PublishCancelableAsync(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification + { + if (notification is null) + { + throw new ArgumentNullException(nameof(notification)); + } + + Task? task = PublishAsync(notification); + if (task is not null) + { + await task; + } + + return notification.Cancel; } } diff --git a/src/Umbraco.Core/Events/EventDefinition.cs b/src/Umbraco.Core/Events/EventDefinition.cs index aa6f2899cd..3f7cd382ed 100644 --- a/src/Umbraco.Core/Events/EventDefinition.cs +++ b/src/Umbraco.Core/Events/EventDefinition.cs @@ -1,73 +1,61 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class EventDefinition : EventDefinitionBase { - public class EventDefinition : EventDefinitionBase + private readonly EventArgs _args; + private readonly object _sender; + private readonly EventHandler _trackedEvent; + + public EventDefinition(EventHandler trackedEvent, object sender, EventArgs args, string? eventName = null) + : base(sender, args, eventName) { - private readonly EventHandler _trackedEvent; - private readonly object _sender; - private readonly EventArgs _args; - - public EventDefinition(EventHandler trackedEvent, object sender, EventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } - - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; } - public class EventDefinition : EventDefinitionBase + public override void RaiseEvent() { - private readonly EventHandler _trackedEvent; - private readonly object _sender; - private readonly TEventArgs _args; - - public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } - - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } - } - - public class EventDefinition : EventDefinitionBase - { - private readonly TypedEventHandler _trackedEvent; - private readonly TSender _sender; - private readonly TEventArgs _args; - - public EventDefinition(TypedEventHandler trackedEvent, TSender sender, TEventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } - - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } + _trackedEvent?.Invoke(_sender, _args); + } +} + +public class EventDefinition : EventDefinitionBase +{ + private readonly TEventArgs _args; + private readonly object _sender; + private readonly EventHandler _trackedEvent; + + public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string? eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + _trackedEvent?.Invoke(_sender, _args); + } +} + +public class EventDefinition : EventDefinitionBase +{ + private readonly TEventArgs _args; + private readonly TSender _sender; + private readonly TypedEventHandler _trackedEvent; + + public EventDefinition(TypedEventHandler trackedEvent, TSender sender, TEventArgs args, string? eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + _trackedEvent?.Invoke(_sender, _args); } } diff --git a/src/Umbraco.Core/Events/EventDefinitionBase.cs b/src/Umbraco.Core/Events/EventDefinitionBase.cs index 4223924234..8ac84c470d 100644 --- a/src/Umbraco.Core/Events/EventDefinitionBase.cs +++ b/src/Umbraco.Core/Events/EventDefinitionBase.cs @@ -1,73 +1,93 @@ -using System; using System.Reflection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public abstract class EventDefinitionBase : IEventDefinition, IEquatable { - public abstract class EventDefinitionBase : IEventDefinition, IEquatable + protected EventDefinitionBase(object? sender, object? args, string? eventName = null) { - protected EventDefinitionBase(object? sender, object? args, string? eventName = null) - { - Sender = sender ?? throw new ArgumentNullException(nameof(sender)); - Args = args ?? throw new ArgumentNullException(nameof(args)); - EventName = eventName; + Sender = sender ?? throw new ArgumentNullException(nameof(sender)); + Args = args ?? throw new ArgumentNullException(nameof(args)); + EventName = eventName; - if (EventName.IsNullOrWhiteSpace()) + if (EventName.IsNullOrWhiteSpace()) + { + // don't match "Ing" suffixed names + Attempt findResult = + EventNameExtractor.FindEvent(sender, args, EventNameExtractor.MatchIngNames); + + if (findResult.Success == false) { - // don't match "Ing" suffixed names - var findResult = EventNameExtractor.FindEvent(sender, args, exclude:EventNameExtractor.MatchIngNames); - - if (findResult.Success == false) - throw new AmbiguousMatchException("Could not automatically find the event name, the event name will need to be explicitly registered for this event definition. " - + $"Sender: {sender.GetType()} Args: {args.GetType()}" - + " Error: " + findResult.Result?.Error); - EventName = findResult.Result?.Name; + throw new AmbiguousMatchException( + "Could not automatically find the event name, the event name will need to be explicitly registered for this event definition. " + + $"Sender: {sender.GetType()} Args: {args.GetType()}" + + " Error: " + findResult.Result?.Error); } - } - public object Sender { get; } - public object Args { get; } - public string? EventName { get; } - - public abstract void RaiseEvent(); - - public bool Equals(EventDefinitionBase? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Args.Equals(other.Args) && string.Equals(EventName, other.EventName) && Sender.Equals(other.Sender); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((EventDefinitionBase) obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = Args.GetHashCode(); - if (EventName is not null) - { - hashCode = (hashCode * 397) ^ EventName.GetHashCode(); - } - hashCode = (hashCode * 397) ^ Sender.GetHashCode(); - return hashCode; - } - } - - public static bool operator ==(EventDefinitionBase left, EventDefinitionBase right) - { - return Equals(left, right); - } - - public static bool operator !=(EventDefinitionBase left, EventDefinitionBase right) - { - return Equals(left, right) == false; + EventName = findResult.Result?.Name; } } + + public object Sender { get; } + + public bool Equals(EventDefinitionBase? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Args.Equals(other.Args) && string.Equals(EventName, other.EventName) && Sender.Equals(other.Sender); + } + + public object Args { get; } + + public string? EventName { get; } + + public static bool operator ==(EventDefinitionBase left, EventDefinitionBase right) => Equals(left, right); + + public abstract void RaiseEvent(); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((EventDefinitionBase)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Args.GetHashCode(); + if (EventName is not null) + { + hashCode = (hashCode * 397) ^ EventName.GetHashCode(); + } + + hashCode = (hashCode * 397) ^ Sender.GetHashCode(); + return hashCode; + } + } + + public static bool operator !=(EventDefinitionBase left, EventDefinitionBase right) => Equals(left, right) == false; } diff --git a/src/Umbraco.Core/Events/EventDefinitionFilter.cs b/src/Umbraco.Core/Events/EventDefinitionFilter.cs index 47b0f9a44e..4872b23e8b 100644 --- a/src/Umbraco.Core/Events/EventDefinitionFilter.cs +++ b/src/Umbraco.Core/Events/EventDefinitionFilter.cs @@ -1,24 +1,23 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// The filter used in the GetEvents method which determines +/// how the result list is filtered +/// +public enum EventDefinitionFilter { /// - /// The filter used in the GetEvents method which determines - /// how the result list is filtered + /// Returns all events tracked /// - public enum EventDefinitionFilter - { - /// - /// Returns all events tracked - /// - All, + All, - /// - /// Deduplicates events and only returns the first duplicate instance tracked - /// - FirstIn, + /// + /// Deduplicates events and only returns the first duplicate instance tracked + /// + FirstIn, - /// - /// Deduplicates events and only returns the last duplicate instance tracked - /// - LastIn - } + /// + /// Deduplicates events and only returns the last duplicate instance tracked + /// + LastIn, } diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index 4d98cbbcca..6d9fd8103b 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -1,46 +1,51 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Extension methods for cancellable event operations +/// +public static class EventExtensions { - /// - /// Extension methods for cancellable event operations - /// - public static class EventExtensions - { - // keep these two for backward compatibility reasons but understand that - // they are *not* part of any scope / event dispatcher / anything... + // keep these two for backward compatibility reasons but understand that + // they are *not* part of any scope / event dispatcher / anything... - /// - /// Raises a cancelable event and returns a value indicating whether the event should be cancelled. - /// - /// The type of the event source. - /// The type of the event data. - /// The event handler. - /// The event source. - /// The event data. - /// A value indicating whether the cancelable event should be cancelled - /// A cancelable event is raised by a component when it is about to perform an action that can be canceled. - public static bool IsRaisedEventCancelled(this TypedEventHandler eventHandler, TArgs args, TSender sender) - where TArgs : CancellableEventArgs + /// + /// Raises a cancelable event and returns a value indicating whether the event should be cancelled. + /// + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + /// A value indicating whether the cancelable event should be cancelled + /// A cancelable event is raised by a component when it is about to perform an action that can be canceled. + public static bool IsRaisedEventCancelled(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - /// - /// Raises an event. - /// - /// The type of the event source. - /// The type of the event data. - /// The event handler. - /// The event source. - /// The event data. - public static void RaiseEvent(this TypedEventHandler eventHandler, TArgs args, TSender sender) - where TArgs : EventArgs + eventHandler(sender, args); + return args.Cancel; + } + + /// + /// Raises an event. + /// + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + public static void RaiseEvent(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : EventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return; - eventHandler(sender, args); + return; } + + eventHandler(sender, args); } } diff --git a/src/Umbraco.Core/Events/EventMessage.cs b/src/Umbraco.Core/Events/EventMessage.cs index eef0985c23..8ba2c98bf8 100644 --- a/src/Umbraco.Core/Events/EventMessage.cs +++ b/src/Umbraco.Core/Events/EventMessage.cs @@ -1,27 +1,29 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// An event message +/// +public sealed class EventMessage { /// - /// An event message + /// Initializes a new instance of the class. /// - public sealed class EventMessage + public EventMessage(string category, string message, EventMessageType messageType = EventMessageType.Default) { - /// - /// Initializes a new instance of the class. - /// - public EventMessage(string category, string message, EventMessageType messageType = EventMessageType.Default) - { - Category = category; - Message = message; - MessageType = messageType; - } - - public string Category { get; private set; } - public string Message { get; private set; } - public EventMessageType MessageType { get; private set; } - - /// - /// This is used to track if this message should be used as a default message so that Umbraco doesn't also append it's own default messages - /// - public bool IsDefaultEventMessage { get; set; } + Category = category; + Message = message; + MessageType = messageType; } + + public string Category { get; } + + public string Message { get; } + + public EventMessageType MessageType { get; } + + /// + /// This is used to track if this message should be used as a default message so that Umbraco doesn't also append it's + /// own default messages + /// + public bool IsDefaultEventMessage { get; set; } } diff --git a/src/Umbraco.Core/Events/EventMessageType.cs b/src/Umbraco.Core/Events/EventMessageType.cs index afbed0d590..a3c6ebf2f9 100644 --- a/src/Umbraco.Core/Events/EventMessageType.cs +++ b/src/Umbraco.Core/Events/EventMessageType.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// The type of event message +/// +public enum EventMessageType { - /// - /// The type of event message - /// - public enum EventMessageType - { - Default = 0, - Info = 1, - Error = 2, - Success = 3, - Warning = 4 - } + Default = 0, + Info = 1, + Error = 2, + Success = 3, + Warning = 4, } diff --git a/src/Umbraco.Core/Events/EventMessages.cs b/src/Umbraco.Core/Events/EventMessages.cs index 23b40118c7..68d19f27fd 100644 --- a/src/Umbraco.Core/Events/EventMessages.cs +++ b/src/Umbraco.Core/Events/EventMessages.cs @@ -1,29 +1,17 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Event messages collection +/// +public sealed class EventMessages : DisposableObjectSlim { - /// - /// Event messages collection - /// - public sealed class EventMessages : DisposableObjectSlim - { - private readonly List _msgs = new List(); + private readonly List _msgs = new(); - public void Add(EventMessage msg) - { - _msgs.Add(msg); - } + public int Count => _msgs.Count; - public int Count => _msgs.Count; + public void Add(EventMessage msg) => _msgs.Add(msg); - public IEnumerable GetAll() - { - return _msgs; - } + public IEnumerable GetAll() => _msgs; - protected override void DisposeResources() - { - _msgs.Clear(); - } - } + protected override void DisposeResources() => _msgs.Clear(); } diff --git a/src/Umbraco.Core/Events/EventNameExtractor.cs b/src/Umbraco.Core/Events/EventNameExtractor.cs index c74d2e293e..16f772dcb2 100644 --- a/src/Umbraco.Core/Events/EventNameExtractor.cs +++ b/src/Umbraco.Core/Events/EventNameExtractor.cs @@ -1,168 +1,184 @@ -using System; using System.Collections.Concurrent; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// There is actually no way to discover an event name in c# at the time of raising the event. It is possible +/// to get the event name from the handler that is being executed based on the event being raised, however that is not +/// what we want in this case. We need to find the event name before it is being raised - you would think that it's +/// possible +/// with reflection or anything but that is not the case, the delegate that defines an event has no info attached to +/// it, it +/// is literally just an event. +/// So what this does is take the sender and event args objects, looks up all public/static events on the sender that +/// have +/// a generic event handler with generic arguments (but only) one, then we match the type of event arguments with the +/// ones +/// being passed in. As it turns out, in our services this will work for the majority of our events! In some cases it +/// may not +/// work and we'll have to supply a string but hopefully this saves a bit of magic strings. +/// We can also write tests to validate these are all working correctly for all services. +/// +public class EventNameExtractor { /// - /// There is actually no way to discover an event name in c# at the time of raising the event. It is possible - /// to get the event name from the handler that is being executed based on the event being raised, however that is not - /// what we want in this case. We need to find the event name before it is being raised - you would think that it's possible - /// with reflection or anything but that is not the case, the delegate that defines an event has no info attached to it, it - /// is literally just an event. - /// - /// So what this does is take the sender and event args objects, looks up all public/static events on the sender that have - /// a generic event handler with generic arguments (but only) one, then we match the type of event arguments with the ones - /// being passed in. As it turns out, in our services this will work for the majority of our events! In some cases it may not - /// work and we'll have to supply a string but hopefully this saves a bit of magic strings. - /// - /// We can also write tests to validate these are all working correctly for all services. + /// Used to cache all candidate events for a given type so we don't re-look them up /// - public class EventNameExtractor + private static readonly ConcurrentDictionary CandidateEvents = new(); + + /// + /// Used to cache all matched event names by (sender type + arg type) so we don't re-look them up + /// + private static readonly ConcurrentDictionary, string[]> MatchedEventNames = new(); + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being + /// matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) { + var events = FindEvents(senderType, argsType, exclude); - /// - /// Finds the event name on the sender that matches the args type - /// - /// - /// - /// - /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched - /// - /// - /// null if not found or an ambiguous match - /// - public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) + switch (events.Length) { - var events = FindEvents(senderType, argsType, exclude); + case 0: + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); - switch (events.Length) - { - case 0: - return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); + case 1: + return Attempt.Succeed(new EventNameExtractorResult(events[0])); - case 1: - return Attempt.Succeed(new EventNameExtractorResult(events[0])); - - default: - //there's more than one left so it's ambiguous! - return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); - } + default: + // there's more than one left so it's ambiguous! + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); } + } - public static string[] FindEvents(Type senderType, Type argsType, Func exclude) + public static string[] FindEvents(Type senderType, Type argsType, Func exclude) + { + var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => { - var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => + EventInfoArgs[] events = CandidateEvents.GetOrAdd(senderType, t => { - var events = CandidateEvents.GetOrAdd(senderType, t => - { - return t.GetEvents(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) - //we can only look for events handlers with generic types because that is the only - // way that we can try to find a matching event based on the arg type passed in - .Where(x => x.EventHandlerType?.IsGenericType ?? false) - .Select(x => new EventInfoArgs(x, x.EventHandlerType!.GetGenericArguments())) - //we are only looking for event handlers that have more than one generic argument - .Where(x => - { - if (x.GenericArgs.Length == 1) return true; + return t.GetEvents(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.FlattenHierarchy) - //special case for our own TypedEventHandler - if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && x.GenericArgs.Length == 2) - { - return true; - } + // we can only look for events handlers with generic types because that is the only + // way that we can try to find a matching event based on the arg type passed in + .Where(x => x.EventHandlerType?.IsGenericType ?? false) + .Select(x => new EventInfoArgs(x, x.EventHandlerType!.GetGenericArguments())) - return false; - }) - .ToArray(); - }); - - return events.Where(x => - { - if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) - return true; - - //special case for our own TypedEventHandler - if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) - && x.GenericArgs.Length == 2 - && x.GenericArgs[1] == tuple.Item2) + // we are only looking for event handlers that have more than one generic argument + .Where(x => { - return true; - } + if (x.GenericArgs.Length == 1) + { + return true; + } - return false; - }).Select(x => x.EventInfo.Name).ToArray(); + // special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && + x.GenericArgs.Length == 2) + { + return true; + } + + return false; + }) + .ToArray(); }); - return found.Where(x => exclude(x) == false).ToArray(); - } - - /// - /// Finds the event name on the sender that matches the args type - /// - /// - /// - /// - /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched - /// - /// - /// null if not found or an ambiguous match - /// - public static Attempt FindEvent(object sender, object args, Func exclude) - { - return FindEvent(sender.GetType(), args.GetType(), exclude); - } - - /// - /// Return true if the event is named with an ING name such as "Saving" or "RollingBack" - /// - /// - /// - public static bool MatchIngNames(string eventName) - { - var splitter = new Regex(@"(? - /// Return true if the event is not named with an ING name such as "Saving" or "RollingBack" - /// - /// - /// - public static bool MatchNonIngNames(string eventName) - { - var splitter = new Regex(@"(? { - EventInfo = eventInfo; - GenericArgs = genericArgs; - } + if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) + { + return true; + } + + // special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) + && x.GenericArgs.Length == 2 + && x.GenericArgs[1] == tuple.Item2) + { + return true; + } + + return false; + }).Select(x => x.EventInfo.Name).ToArray(); + }); + + return found.Where(x => exclude(x) == false).ToArray(); + } + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being + /// matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt + FindEvent(object sender, object args, Func exclude) => + FindEvent(sender.GetType(), args.GetType(), exclude); + + /// + /// Return true if the event is named with an ING name such as "Saving" or "RollingBack" + /// + /// + /// + public static bool MatchIngNames(string eventName) + { + var splitter = new Regex(@"(? - /// Used to cache all candidate events for a given type so we don't re-look them up - /// - private static readonly ConcurrentDictionary CandidateEvents = new ConcurrentDictionary(); + return words[0].EndsWith("ing"); + } - /// - /// Used to cache all matched event names by (sender type + arg type) so we don't re-look them up - /// - private static readonly ConcurrentDictionary, string[]> MatchedEventNames = new ConcurrentDictionary, string[]>(); + /// + /// Return true if the event is not named with an ING name such as "Saving" or "RollingBack" + /// + /// + /// + public static bool MatchNonIngNames(string eventName) + { + var splitter = new Regex(@"(? Name = name; - public EventNameExtractorResult(string? name) - { - Name = name; - } + public EventNameExtractorResult(EventNameExtractorError? error) => Error = error; - public EventNameExtractorResult(EventNameExtractorError? error) - { - Error = error; - } - } + public EventNameExtractorError? Error { get; } + + public string? Name { get; } } diff --git a/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs index 2026f41ff3..06b7ff81f4 100644 --- a/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs +++ b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs @@ -1,18 +1,17 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Events -{ - public class ExportedMemberEventArgs : EventArgs - { - public IMember Member { get; } - public MemberExportModel Exported { get; } +namespace Umbraco.Cms.Core.Events; - public ExportedMemberEventArgs(IMember member, MemberExportModel exported) - { - Member = member; - Exported = exported; - } +public class ExportedMemberEventArgs : EventArgs +{ + public ExportedMemberEventArgs(IMember member, MemberExportModel exported) + { + Member = member; + Exported = exported; } + + public IMember Member { get; } + + public MemberExportModel Exported { get; } } diff --git a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs index 9a6a4357e0..4aaeeac29d 100644 --- a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs +++ b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public interface IDeletingMediaFilesEventArgs { - public interface IDeletingMediaFilesEventArgs - { - List MediaFilesToDelete { get; } - } + List MediaFilesToDelete { get; } } diff --git a/src/Umbraco.Core/Events/IEventAggregator.cs b/src/Umbraco.Core/Events/IEventAggregator.cs index c654bb6c86..379f532be2 100644 --- a/src/Umbraco.Core/Events/IEventAggregator.cs +++ b/src/Umbraco.Core/Events/IEventAggregator.cs @@ -1,52 +1,49 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Threading; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Defines an object that channels events from multiple objects into a single object +/// to simplify registration for clients. +/// +public interface IEventAggregator { /// - /// Defines an object that channels events from multiple objects into a single object - /// to simplify registration for clients. + /// Asynchronously send a notification to multiple handlers of both sync and async /// - public interface IEventAggregator - { - /// - /// Asynchronously send a notification to multiple handlers of both sync and async - /// - /// The type of notification being handled. - /// The notification object. - /// An optional cancellation token. - /// A task that represents the publish operation. - Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) - where TNotification : INotification; + /// The type of notification being handled. + /// The notification object. + /// An optional cancellation token. + /// A task that represents the publish operation. + Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification; - /// - /// Synchronously send a notification to multiple handlers of both sync and async - /// - /// The type of notification being handled. - /// The notification object. - void Publish(TNotification notification) - where TNotification : INotification; + /// + /// Synchronously send a notification to multiple handlers of both sync and async + /// + /// The type of notification being handled. + /// The notification object. + void Publish(TNotification notification) + where TNotification : INotification; - /// - /// Publishes a cancelable notification to the notification subscribers - /// - /// The type of notification being handled. - /// - /// True if the notification was cancelled by a subscriber, false otherwise - bool PublishCancelable(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification; + /// + /// Publishes a cancelable notification to the notification subscribers + /// + /// The type of notification being handled. + /// + /// True if the notification was cancelled by a subscriber, false otherwise + bool PublishCancelable(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification; - /// - /// Publishes a cancelable notification async to the notification subscribers - /// - /// The type of notification being handled. - /// - /// True if the notification was cancelled by a subscriber, false otherwise - Task PublishCancelableAsync(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification; - } + /// + /// Publishes a cancelable notification async to the notification subscribers + /// + /// The type of notification being handled. + /// + /// True if the notification was cancelled by a subscriber, false otherwise + Task PublishCancelableAsync(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification; } diff --git a/src/Umbraco.Core/Events/IEventDefinition.cs b/src/Umbraco.Core/Events/IEventDefinition.cs index e3918113e1..d10b931548 100644 --- a/src/Umbraco.Core/Events/IEventDefinition.cs +++ b/src/Umbraco.Core/Events/IEventDefinition.cs @@ -1,11 +1,12 @@ -namespace Umbraco.Cms.Core.Events -{ - public interface IEventDefinition - { - object Sender { get; } - object Args { get; } - string? EventName { get; } +namespace Umbraco.Cms.Core.Events; - void RaiseEvent(); - } +public interface IEventDefinition +{ + object Sender { get; } + + object Args { get; } + + string? EventName { get; } + + void RaiseEvent(); } diff --git a/src/Umbraco.Core/Events/IEventDispatcher.cs b/src/Umbraco.Core/Events/IEventDispatcher.cs index bef94b6d4a..9d15a74c02 100644 --- a/src/Umbraco.Core/Events/IEventDispatcher.cs +++ b/src/Umbraco.Core/Events/IEventDispatcher.cs @@ -1,98 +1,98 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Dispatches events from within a scope. +/// +/// +/// +/// The name of the event is auto-magically discovered by matching the sender type, args type, and +/// eventHandler type. If the match is not unique, then the name parameter must be used to specify the +/// name in an explicit way. +/// +/// +/// What happens when an event is dispatched depends on the scope settings. It can be anything from +/// "trigger immediately" to "just ignore". Refer to the scope documentation for more details. +/// +/// +public interface IEventDispatcher { + // not sure about the Dispatch & DispatchCancelable signatures at all for now + // nor about the event name thing, etc - but let's keep it like this + /// - /// Dispatches events from within a scope. + /// Dispatches a cancelable event. /// - /// - /// The name of the event is auto-magically discovered by matching the sender type, args type, and - /// eventHandler type. If the match is not unique, then the name parameter must be used to specify the - /// name in an explicit way. - /// What happens when an event is dispatched depends on the scope settings. It can be anything from - /// "trigger immediately" to "just ignore". Refer to the scope documentation for more details. - /// - public interface IEventDispatcher - { - // not sure about the Dispatch & DispatchCancelable signatures at all for now - // nor about the event name thing, etc - but let's keep it like this + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? name = null); - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? name = null); + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? name = null) + where TArgs : CancellableEventArgs; - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? name = null) - where TArgs : CancellableEventArgs; + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null) + where TArgs : CancellableEventArgs; - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null) - where TArgs : CancellableEventArgs; + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? name = null); - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? name = null); + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? name = null); - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? name = null); + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null); - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null); + /// + /// Notifies the dispatcher that the scope is exiting. + /// + /// A value indicating whether the scope completed. + void ScopeExit(bool completed); - /// - /// Notifies the dispatcher that the scope is exiting. - /// - /// A value indicating whether the scope completed. - void ScopeExit(bool completed); - - /// - /// Gets the collected events. - /// - /// The collected events. - IEnumerable GetEvents(EventDefinitionFilter filter); - } + /// + /// Gets the collected events. + /// + /// The collected events. + IEnumerable GetEvents(EventDefinitionFilter filter); } diff --git a/src/Umbraco.Core/Events/IEventMessagesAccessor.cs b/src/Umbraco.Core/Events/IEventMessagesAccessor.cs index cffff705da..e88ba73dee 100644 --- a/src/Umbraco.Core/Events/IEventMessagesAccessor.cs +++ b/src/Umbraco.Core/Events/IEventMessagesAccessor.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public interface IEventMessagesAccessor { - public interface IEventMessagesAccessor - { - EventMessages? EventMessages { get; set; } - } + EventMessages? EventMessages { get; set; } } diff --git a/src/Umbraco.Core/Events/IEventMessagesFactory.cs b/src/Umbraco.Core/Events/IEventMessagesFactory.cs index 6abf6e8d41..9ade74d20a 100644 --- a/src/Umbraco.Core/Events/IEventMessagesFactory.cs +++ b/src/Umbraco.Core/Events/IEventMessagesFactory.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Events -{ - /// - /// Event messages factory - /// - public interface IEventMessagesFactory - { - EventMessages Get(); +namespace Umbraco.Cms.Core.Events; - EventMessages? GetOrDefault(); - } +/// +/// Event messages factory +/// +public interface IEventMessagesFactory +{ + EventMessages Get(); + + EventMessages? GetOrDefault(); } diff --git a/src/Umbraco.Core/Events/INotificationAsyncHandler.cs b/src/Umbraco.Core/Events/INotificationAsyncHandler.cs index cdcc21542f..25a46ed250 100644 --- a/src/Umbraco.Core/Events/INotificationAsyncHandler.cs +++ b/src/Umbraco.Core/Events/INotificationAsyncHandler.cs @@ -1,25 +1,22 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Threading; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Defines a handler for a async notification. +/// +/// The type of notification being handled. +public interface INotificationAsyncHandler + where TNotification : INotification { /// - /// Defines a handler for a async notification. + /// Handles a notification /// - /// The type of notification being handled. - public interface INotificationAsyncHandler - where TNotification : INotification - { - /// - /// Handles a notification - /// - /// The notification - /// The cancellation token. - /// A representing the asynchronous operation. - Task HandleAsync(TNotification notification, CancellationToken cancellationToken); - } + /// The notification + /// The cancellation token. + /// A representing the asynchronous operation. + Task HandleAsync(TNotification notification, CancellationToken cancellationToken); } diff --git a/src/Umbraco.Core/Events/INotificationHandler.cs b/src/Umbraco.Core/Events/INotificationHandler.cs index 548bec39b8..2111009faa 100644 --- a/src/Umbraco.Core/Events/INotificationHandler.cs +++ b/src/Umbraco.Core/Events/INotificationHandler.cs @@ -3,19 +3,18 @@ using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Defines a handler for a notification. +/// +/// The type of notification being handled. +public interface INotificationHandler + where TNotification : INotification { /// - /// Defines a handler for a notification. + /// Handles a notification /// - /// The type of notification being handled. - public interface INotificationHandler - where TNotification : INotification - { - /// - /// Handles a notification - /// - /// The notification - void Handle(TNotification notification); - } + /// The notification + void Handle(TNotification notification); } diff --git a/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs b/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs index 58fdafc341..89962bbb9c 100644 --- a/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs +++ b/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs @@ -1,45 +1,45 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public interface IScopedNotificationPublisher { - public interface IScopedNotificationPublisher - { - /// - /// Suppresses all notifications from being added/created until the result object is disposed. - /// - /// - IDisposable Suppress(); + /// + /// Suppresses all notifications from being added/created until the result object is disposed. + /// + /// + IDisposable Suppress(); - /// - /// Publishes a cancelable notification to the notification subscribers - /// - /// - /// True if the notification was cancelled by a subscriber, false otherwise - bool PublishCancelable(ICancelableNotification notification); + /// + /// Publishes a cancelable notification to the notification subscribers + /// + /// + /// True if the notification was cancelled by a subscriber, false otherwise + bool PublishCancelable(ICancelableNotification notification); - /// - /// Publishes a cancelable notification to the notification subscribers - /// - /// - /// True if the notification was cancelled by a subscriber, false otherwise - Task PublishCancelableAsync(ICancelableNotification notification); + /// + /// Publishes a cancelable notification to the notification subscribers + /// + /// + /// True if the notification was cancelled by a subscriber, false otherwise + Task PublishCancelableAsync(ICancelableNotification notification); - /// - /// Publishes a notification to the notification subscribers - /// - /// - /// The notification is published upon successful completion of the current scope, i.e. when things have been saved/published/deleted etc. - void Publish(INotification notification); + /// + /// Publishes a notification to the notification subscribers + /// + /// + /// + /// The notification is published upon successful completion of the current scope, i.e. when things have been + /// saved/published/deleted etc. + /// + void Publish(INotification notification); - /// - /// Invokes publishing of all pending notifications within the current scope - /// - /// - void ScopeExit(bool completed); - } + /// + /// Invokes publishing of all pending notifications within the current scope + /// + /// + void ScopeExit(bool completed); } diff --git a/src/Umbraco.Core/Events/MacroErrorEventArgs.cs b/src/Umbraco.Core/Events/MacroErrorEventArgs.cs index 8d0e8dbfe1..876f7b99eb 100644 --- a/src/Umbraco.Core/Events/MacroErrorEventArgs.cs +++ b/src/Umbraco.Core/Events/MacroErrorEventArgs.cs @@ -1,42 +1,41 @@ -using System; using Umbraco.Cms.Core.Macros; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +// Provides information on the macro that caused an error +public class MacroErrorEventArgs : EventArgs { - // Provides information on the macro that caused an error - public class MacroErrorEventArgs : EventArgs - { - /// - /// Name of the faulting macro. - /// - public string? Name { get; set; } + /// + /// Name of the faulting macro. + /// + public string? Name { get; set; } - /// - /// Alias of the faulting macro. - /// - public string? Alias { get; set; } + /// + /// Alias of the faulting macro. + /// + public string? Alias { get; set; } - /// - /// Filename, file path, fully qualified class name, or other key used by the macro engine to do it's processing of the faulting macro. - /// - public string? MacroSource { get; set; } + /// + /// Filename, file path, fully qualified class name, or other key used by the macro engine to do it's processing of the + /// faulting macro. + /// + public string? MacroSource { get; set; } - /// - /// Exception raised. - /// - public Exception? Exception { get; set; } + /// + /// Exception raised. + /// + public Exception? Exception { get; set; } - /// - /// Gets or sets the desired behaviour when a matching macro causes an error. See - /// for definitions. By setting this in your event - /// you can override the default behaviour defined in UmbracoSettings.config. - /// - /// Macro error behaviour enum. - public MacroErrorBehaviour Behaviour { get; set; } + /// + /// Gets or sets the desired behaviour when a matching macro causes an error. See + /// for definitions. By setting this in your event + /// you can override the default behaviour defined in UmbracoSettings.config. + /// + /// Macro error behaviour enum. + public MacroErrorBehaviour Behaviour { get; set; } - /// - /// The HTML code to display when Behavior is Content. - /// - public string? Html { get; set; } - } + /// + /// The HTML code to display when Behavior is Content. + /// + public string? Html { get; set; } } diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs index 2f65056353..312f1b8146 100644 --- a/src/Umbraco.Core/Events/MoveEventArgs.cs +++ b/src/Umbraco.Core/Events/MoveEventArgs.cs @@ -1,151 +1,163 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class MoveEventArgs : CancellableObjectEventArgs, IEquatable> { - public class MoveEventArgs : CancellableObjectEventArgs, IEquatable> + private IEnumerable>? _moveInfoCollection; + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(bool canCancel, EventMessages eventMessages, params MoveEventInfo[] moveInfo) + : base(default, canCancel, eventMessages) { - private IEnumerable>? _moveInfoCollection; - - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(bool canCancel, EventMessages eventMessages, params MoveEventInfo[] moveInfo) - : base(default, canCancel, eventMessages) + if (moveInfo.FirstOrDefault() is null) { - if (moveInfo.FirstOrDefault() is null) + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(EventMessages eventMessages, params MoveEventInfo[] moveInfo) + : base(default, eventMessages) + { + if (moveInfo.FirstOrDefault() is null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(bool canCancel, params MoveEventInfo[] moveInfo) + : base(default, canCancel) + { + if (moveInfo.FirstOrDefault() is null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(params MoveEventInfo[] moveInfo) + : base(default) + { + if (moveInfo.FirstOrDefault() is null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Gets all MoveEventInfo objects used to create the object + /// + public IEnumerable>? MoveInfoCollection + { + get => _moveInfoCollection; + set + { + MoveEventInfo? first = value?.FirstOrDefault(); + if (first is null) { - throw new ArgumentException("moveInfo argument must contain at least one item"); + throw new InvalidOperationException("MoveInfoCollection must have at least one item"); } - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } + _moveInfoCollection = value; - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(EventMessages eventMessages, params MoveEventInfo[] moveInfo) - : base(default, eventMessages) - { - if (moveInfo.FirstOrDefault() is null) - { - throw new ArgumentException("moveInfo argument must contain at least one item"); - } - - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } - - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(bool canCancel, params MoveEventInfo[] moveInfo) - : base(default, canCancel) - { - if (moveInfo.FirstOrDefault() is null) - { - throw new ArgumentException("moveInfo argument must contain at least one item"); - } - - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } - - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(params MoveEventInfo[] moveInfo) - : base(default) - { - if (moveInfo.FirstOrDefault() is null) - { - throw new ArgumentException("moveInfo argument must contain at least one item"); - } - - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } - - - /// - /// Gets all MoveEventInfo objects used to create the object - /// - public IEnumerable>? MoveInfoCollection - { - get { return _moveInfoCollection; } - set - { - var first = value?.FirstOrDefault(); - if (first is null) - { - throw new InvalidOperationException("MoveInfoCollection must have at least one item"); - } - - _moveInfoCollection = value; - - //assign the legacy props - EventObject = first.Entity; - } - } - - public bool Equals(MoveEventArgs? other) - { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && (MoveInfoCollection?.Equals(other.MoveInfoCollection) ?? false); - } - - public override bool Equals(object? obj) - { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MoveEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - if (MoveInfoCollection is not null) - { - return (base.GetHashCode() * 397) ^ MoveInfoCollection.GetHashCode(); - } - - return base.GetHashCode() * 397; - } - } - - public static bool operator ==(MoveEventArgs left, MoveEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(MoveEventArgs left, MoveEventArgs right) - { - return !Equals(left, right); + // assign the legacy props + EventObject = first.Entity; } } + + public static bool operator ==(MoveEventArgs left, MoveEventArgs right) => Equals(left, right); + + public bool Equals(MoveEventArgs? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && (MoveInfoCollection?.Equals(other.MoveInfoCollection) ?? false); + } + + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((MoveEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + if (MoveInfoCollection is not null) + { + return (base.GetHashCode() * 397) ^ MoveInfoCollection.GetHashCode(); + } + + return base.GetHashCode() * 397; + } + } + + public static bool operator !=(MoveEventArgs left, MoveEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/MoveEventInfo.cs b/src/Umbraco.Core/Events/MoveEventInfo.cs index 126a3fd230..92c09c92a8 100644 --- a/src/Umbraco.Core/Events/MoveEventInfo.cs +++ b/src/Umbraco.Core/Events/MoveEventInfo.cs @@ -1,55 +1,70 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class MoveEventInfo : IEquatable> { - public class MoveEventInfo : IEquatable> + public MoveEventInfo(TEntity entity, string originalPath, int newParentId) { - public MoveEventInfo(TEntity entity, string originalPath, int newParentId) + Entity = entity; + OriginalPath = originalPath; + NewParentId = newParentId; + } + + public TEntity Entity { get; set; } + + public string OriginalPath { get; set; } + + public int NewParentId { get; set; } + + public static bool operator ==(MoveEventInfo left, MoveEventInfo right) => Equals(left, right); + + public bool Equals(MoveEventInfo? other) + { + if (ReferenceEquals(null, other)) { - Entity = entity; - OriginalPath = originalPath; - NewParentId = newParentId; + return false; } - public TEntity Entity { get; set; } - public string OriginalPath { get; set; } - public int NewParentId { get; set; } - - public bool Equals(MoveEventInfo? other) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return EqualityComparer.Default.Equals(Entity, other.Entity) && NewParentId == other.NewParentId && string.Equals(OriginalPath, other.OriginalPath); + return true; } - public override bool Equals(object? obj) + return EqualityComparer.Default.Equals(Entity, other.Entity) && NewParentId == other.NewParentId && + string.Equals(OriginalPath, other.OriginalPath); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MoveEventInfo) obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - unchecked - { - var hashCode = Entity is not null ? EqualityComparer.Default.GetHashCode(Entity) : base.GetHashCode(); - hashCode = (hashCode * 397) ^ NewParentId; - hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode(); - return hashCode; - } + return true; } - public static bool operator ==(MoveEventInfo left, MoveEventInfo right) + if (obj.GetType() != GetType()) { - return Equals(left, right); + return false; } - public static bool operator !=(MoveEventInfo left, MoveEventInfo right) + return Equals((MoveEventInfo)obj); + } + + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + var hashCode = Entity is not null + ? EqualityComparer.Default.GetHashCode(Entity) + : base.GetHashCode(); + hashCode = (hashCode * 397) ^ NewParentId; + hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode(); + return hashCode; } } + + public static bool operator !=(MoveEventInfo left, MoveEventInfo right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/NewEventArgs.cs b/src/Umbraco.Core/Events/NewEventArgs.cs index d3e8436d0e..0db72488aa 100644 --- a/src/Umbraco.Core/Events/NewEventArgs.cs +++ b/src/Umbraco.Core/Events/NewEventArgs.cs @@ -1,130 +1,136 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class NewEventArgs : CancellableObjectEventArgs, IEquatable> { - public class NewEventArgs : CancellableObjectEventArgs, IEquatable> + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, int parentId, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) { + Alias = alias; + ParentId = parentId; + } + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, TEntity? parent, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + Alias = alias; + Parent = parent; + } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, int parentId, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + public NewEventArgs(TEntity eventObject, string alias, int parentId, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + Alias = alias; + ParentId = parentId; + } + + public NewEventArgs(TEntity eventObject, string alias, TEntity? parent, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + Alias = alias; + Parent = parent; + } + + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, int parentId) + : base(eventObject, canCancel) + { + Alias = alias; + ParentId = parentId; + } + + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, TEntity? parent) + : base(eventObject, canCancel) + { + Alias = alias; + Parent = parent; + } + + public NewEventArgs(TEntity eventObject, string alias, int parentId) + : base(eventObject) + { + Alias = alias; + ParentId = parentId; + } + + public NewEventArgs(TEntity eventObject, string alias, TEntity? parent) + : base(eventObject) + { + Alias = alias; + Parent = parent; + } + + /// + /// The entity being created + /// + public TEntity? Entity => EventObject; + + /// + /// Gets or Sets the Alias. + /// + public string Alias { get; } + + /// + /// Gets or Sets the Id of the parent. + /// + public int ParentId { get; } + + /// + /// Gets or Sets the parent IContent object. + /// + public TEntity? Parent { get; } + + public static bool operator ==(NewEventArgs left, NewEventArgs right) => Equals(left, right); + + public bool Equals(NewEventArgs? other) + { + if (ReferenceEquals(null, other)) { - Alias = alias; - ParentId = parentId; + return false; } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, TEntity? parent, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + if (ReferenceEquals(this, other)) { - Alias = alias; - Parent = parent; + return true; } - public NewEventArgs(TEntity eventObject, string @alias, int parentId, EventMessages eventMessages) - : base(eventObject, eventMessages) + return base.Equals(other) && string.Equals(Alias, other.Alias) && + EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - Alias = alias; - ParentId = parentId; + return false; } - public NewEventArgs(TEntity eventObject, string @alias, TEntity? parent, EventMessages eventMessages) - : base(eventObject, eventMessages) + if (ReferenceEquals(this, obj)) { - Alias = alias; - Parent = parent; + return true; } - - - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, int parentId) : base(eventObject, canCancel) + if (obj.GetType() != GetType()) { - Alias = alias; - ParentId = parentId; + return false; } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, TEntity? parent) - : base(eventObject, canCancel) + return Equals((NewEventArgs?)obj); + } + + public override int GetHashCode() + { + unchecked { - Alias = alias; - Parent = parent; - } - - public NewEventArgs(TEntity eventObject, string @alias, int parentId) : base(eventObject) - { - Alias = alias; - ParentId = parentId; - } - - public NewEventArgs(TEntity eventObject, string @alias, TEntity? parent) - : base(eventObject) - { - Alias = alias; - Parent = parent; - } - - /// - /// The entity being created - /// - public TEntity? Entity - { - get { return EventObject; } - } - - /// - /// Gets or Sets the Alias. - /// - public string Alias { get; private set; } - - /// - /// Gets or Sets the Id of the parent. - /// - public int ParentId { get; private set; } - - /// - /// Gets or Sets the parent IContent object. - /// - public TEntity? Parent { get; private set; } - - public bool Equals(NewEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && string.Equals(Alias, other.Alias) && EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((NewEventArgs?) obj); - } - - public override int GetHashCode() - { - unchecked + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ Alias.GetHashCode(); + if (Parent is not null) { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ Alias.GetHashCode(); - if (Parent is not null) - { - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Parent); - } - - hashCode = (hashCode * 397) ^ ParentId; - return hashCode; + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Parent); } - } - public static bool operator ==(NewEventArgs left, NewEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(NewEventArgs left, NewEventArgs right) - { - return !Equals(left, right); + hashCode = (hashCode * 397) ^ ParentId; + return hashCode; } } + + public static bool operator !=(NewEventArgs left, NewEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs index a36368ea54..20398502a1 100644 --- a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs +++ b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs @@ -1,60 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// An IEventDispatcher that immediately raise all events. +/// +/// +/// This means that events will be raised during the scope transaction, +/// whatever happens, and the transaction could roll back in the end. +/// +internal class PassThroughEventDispatcher : IEventDispatcher { - /// - /// An IEventDispatcher that immediately raise all events. - /// - /// This means that events will be raised during the scope transaction, - /// whatever happens, and the transaction could roll back in the end. - internal class PassThroughEventDispatcher : IEventDispatcher + public bool DispatchCancelable(EventHandler? eventHandler, object sender, CancellableEventArgs args, string? eventName = null) { - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler? eventHandler, object sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler? eventHandler, TSender sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } + eventHandler(sender, args); + return args.Cancel; + } - public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } + public void Dispatch(EventHandler? eventHandler, object sender, EventArgs args, string? eventName = null) => + eventHandler?.Invoke(sender, args); - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } + public void Dispatch(EventHandler? eventHandler, object sender, TArgs args, string? eventName = null) => eventHandler?.Invoke(sender, args); - public IEnumerable GetEvents(EventDefinitionFilter filter) - { - return Enumerable.Empty(); - } + public void Dispatch(TypedEventHandler? eventHandler, TSender sender, TArgs args, string? eventName = null) => eventHandler?.Invoke(sender, args); - public void ScopeExit(bool completed) - { } + public IEnumerable GetEvents(EventDefinitionFilter filter) => + Enumerable.Empty(); + + public void ScopeExit(bool completed) + { } } diff --git a/src/Umbraco.Core/Events/PublishEventArgs.cs b/src/Umbraco.Core/Events/PublishEventArgs.cs index 80b6dcd8c7..8a48a0cfa9 100644 --- a/src/Umbraco.Core/Events/PublishEventArgs.cs +++ b/src/Umbraco.Core/Events/PublishEventArgs.cs @@ -1,128 +1,141 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class PublishEventArgs : CancellableEnumerableObjectEventArgs, + IEquatable> { - public class PublishEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) { - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - /// - /// - public PublishEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + } + + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public PublishEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public PublishEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) + { + } + + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, bool canCancel, bool isAllPublished) + : base(eventObject, canCancel) + { + } + + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + public PublishEventArgs(IEnumerable eventObject) + : base(eventObject) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + public PublishEventArgs(TEntity eventObject) + : base(new List { eventObject }) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished) + : base(new List { eventObject }, canCancel) + { + } + + /// + /// Returns all entities that were published during the operation + /// + public IEnumerable? PublishedEntities => EventObject; + + public static bool operator ==(PublishEventArgs left, PublishEventArgs right) => + Equals(left, right); + + public bool Equals(PublishEventArgs? other) + { + if (ReferenceEquals(null, other)) { + return false; } - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - /// - public PublishEventArgs(IEnumerable eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) + if (ReferenceEquals(this, other)) { + return true; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public PublishEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) + return base.Equals(other); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { + return false; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public PublishEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) + if (ReferenceEquals(this, obj)) { + return true; } - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - /// - /// - public PublishEventArgs(IEnumerable eventObject, bool canCancel, bool isAllPublished) - : base(eventObject, canCancel) + if (obj.GetType() != GetType()) { + return false; } - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - public PublishEventArgs(IEnumerable eventObject) - : base(eventObject) - { - } + return Equals((PublishEventArgs)obj); + } - /// - /// Constructor accepting a single entity instance - /// - /// - public PublishEventArgs(TEntity eventObject) - : base(new List { eventObject }) + public override int GetHashCode() + { + unchecked { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished) - : base(new List { eventObject }, canCancel) - { - } - - /// - /// Returns all entities that were published during the operation - /// - public IEnumerable? PublishedEntities => EventObject; - - public bool Equals(PublishEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((PublishEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397); - } - } - - public static bool operator ==(PublishEventArgs left, PublishEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(PublishEventArgs left, PublishEventArgs right) - { - return !Equals(left, right); + return base.GetHashCode() * 397; } } + + public static bool operator !=(PublishEventArgs left, PublishEventArgs right) => + !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs index e79cd67cd8..bc8eac29a1 100644 --- a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs +++ b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs @@ -1,43 +1,38 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// An IEventDispatcher that queues events, and raise them when the scope +/// exits and has been completed. +/// +public class QueuingEventDispatcher : QueuingEventDispatcherBase { - /// - /// An IEventDispatcher that queues events, and raise them when the scope - /// exits and has been completed. - /// - public class QueuingEventDispatcher : QueuingEventDispatcherBase + private readonly MediaFileManager _mediaFileManager; + + public QueuingEventDispatcher(MediaFileManager mediaFileManager) + : base(true) => + _mediaFileManager = mediaFileManager; + + protected override void ScopeExitCompleted() { - private readonly MediaFileManager _mediaFileManager; - public QueuingEventDispatcher(MediaFileManager mediaFileManager) - : base(true) + // processing only the last instance of each event... + // this is probably far from perfect, because if eg a content is saved in a list + // and then as a single content, the two events will probably not be de-duplicated, + // but it's better than nothing + foreach (IEventDefinition e in GetEvents(EventDefinitionFilter.LastIn)) { - _mediaFileManager = mediaFileManager; - } + e.RaiseEvent(); - protected override void ScopeExitCompleted() - { - // processing only the last instance of each event... - // this is probably far from perfect, because if eg a content is saved in a list - // and then as a single content, the two events will probably not be de-duplicated, - // but it's better than nothing - - foreach (var e in GetEvents(EventDefinitionFilter.LastIn)) + // separating concerns means that this should probably not be here, + // but then where should it be (without making things too complicated)? + if (e.Args is IDeletingMediaFilesEventArgs delete && delete.MediaFilesToDelete.Count > 0) { - e.RaiseEvent(); - - // separating concerns means that this should probably not be here, - // but then where should it be (without making things too complicated)? - var delete = e.Args as IDeletingMediaFilesEventArgs; - if (delete != null && delete.MediaFilesToDelete.Count > 0) - _mediaFileManager.DeleteMediaFiles(delete.MediaFilesToDelete); + _mediaFileManager.DeleteMediaFiles(delete.MediaFilesToDelete); } } - - - } } diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs index 71b7647b4f..c259e271e5 100644 --- a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs +++ b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs @@ -1,344 +1,423 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// An IEventDispatcher that queues events. +/// +/// +/// Can raise, or ignore, cancelable events, depending on option. +/// +/// Implementations must override ScopeExitCompleted to define what +/// to do with the events when the scope exits and has been completed. +/// +/// If the scope exits without being completed, events are ignored. +/// +public abstract class QueuingEventDispatcherBase : IEventDispatcher { - /// - /// An IEventDispatcher that queues events. - /// - /// - /// Can raise, or ignore, cancelable events, depending on option. - /// Implementations must override ScopeExitCompleted to define what - /// to do with the events when the scope exits and has been completed. - /// If the scope exits without being completed, events are ignored. - /// - public abstract class QueuingEventDispatcherBase : IEventDispatcher + private readonly bool _raiseCancelable; + + // events will be enlisted in the order they are raised + private List? _events; + + protected QueuingEventDispatcherBase(bool raiseCancelable) => _raiseCancelable = raiseCancelable; + + private List Events => _events ??= new List(); + + public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) { - //events will be enlisted in the order they are raised - private List? _events; - private readonly bool _raiseCancelable; - - protected QueuingEventDispatcherBase(bool raiseCancelable) + if (eventHandler == null) { - _raiseCancelable = raiseCancelable; - } - - private List Events => _events ?? (_events = new List()); - - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) - { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + if (_raiseCancelable == false) { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) + if (_raiseCancelable == false) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return args.Cancel; } - public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return args.Cancel; } - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + if (_raiseCancelable == false) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return args.Cancel; } - public IEnumerable GetEvents(EventDefinitionFilter filter) + eventHandler(sender, args); + return args.Cancel; + } + + public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) + { + if (eventHandler == null) { - if (_events == null) - return Enumerable.Empty(); - - IReadOnlyList events; - switch (filter) - { - case EventDefinitionFilter.All: - events = _events; - break; - case EventDefinitionFilter.FirstIn: - var l1 = new OrderedHashSet(); - foreach (var e in _events) - l1.Add(e); - events = l1; - break; - case EventDefinitionFilter.LastIn: - var l2 = new OrderedHashSet(keepOldest: false); - foreach (var e in _events) - l2.Add(e); - events = l2; - break; - default: - throw new ArgumentOutOfRangeException("filter", filter, null); - } - - return FilterSupersededAndUpdateToLatestEntity(events); + return; } - private class EventDefinitionInfos + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) + { + if (eventHandler == null) { - public IEventDefinition? EventDefinition { get; set; } - public Type[]? SupersedeTypes { get; set; } + return; } - // this is way too convoluted, the supersede attribute is used only on DeleteEventargs to specify - // that it supersedes save, publish, move and copy - BUT - publish event args is also used for - // unpublishing and should NOT be superseded - so really it should not be managed at event args - // level but at event level - // - // what we want is: - // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should - // not trigger for the entity - and even though, does it make any sense? making a copy of an entity - // should ... trigger? - // - // not going to refactor it all - we probably want to *always* trigger event but tell people that - // due to scopes, they should not expected eg a saved entity to still be around - however, now, - // going to write a ugly condition to deal with U4-10764 + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } - // iterates over the events (latest first) and filter out any events or entities in event args that are included - // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want - // to raise the Saved event (well actually we just don't want to include it in the args for that saved event) - internal static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events) + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + { + if (eventHandler == null) { - // keeps the 'latest' entity and associated event data - var entities = new List>(); + return; + } - // collects the event definitions - // collects the arguments in result, that require their entities to be updated - var result = new List(); - var resultArgs = new List(); + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } - // eagerly fetch superseded arg types for each arg type - var argTypeSuperceeding = events.Select(x => x.Args.GetType()) - .Distinct() - .ToDictionary(x => x, x => x.GetCustomAttributes(false).Select(y => y.SupersededEventArgsType).ToArray()); + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + if (_events == null) + { + return Enumerable.Empty(); + } - // iterate over all events and filter - // - // process the list in reverse, because events are added in the order they are raised and we want to keep - // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity - // is Deleted after being Saved, we want to filter out the Saved event - for (var index = events.Count - 1; index >= 0; index--) - { - var def = events[index]; - - var infos = new EventDefinitionInfos + IReadOnlyList events; + switch (filter) + { + case EventDefinitionFilter.All: + events = _events; + break; + case EventDefinitionFilter.FirstIn: + var l1 = new OrderedHashSet(); + foreach (IEventDefinition e in _events) { - EventDefinition = def, - SupersedeTypes = argTypeSuperceeding[def.Args.GetType()] - }; - - var args = def.Args as CancellableObjectEventArgs; - if (args == null) - { - // not a cancellable event arg, include event definition in result - result.Add(def); + l1.Add(e); } - else + + events = l1; + break; + case EventDefinitionFilter.LastIn: + var l2 = new OrderedHashSet(false); + foreach (IEventDefinition e in _events) { - // event object can either be a single object or an enumerable of objects - // try to get as an enumerable, get null if it's not - var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); - if (eventObjects == null) - { - // single object, cast as an IEntity - // if cannot cast, cannot filter, nothing - just include event definition in result - var eventEntity = args.EventObject as IEntity; - if (eventEntity == null) - { - result.Add(def); - continue; - } - - // look for this entity in superseding event args - // found = must be removed (ie not added), else track - if (IsSuperceeded(eventEntity, infos, entities) == false) - { - // track - entities.Add(Tuple.Create(eventEntity, infos)); - - // track result arguments - // include event definition in result - resultArgs.Add(args); - result.Add(def); - } - } - else - { - // enumerable of objects - var toRemove = new List(); - foreach (var eventObject in eventObjects) - { - // extract the event object, cast as an IEntity - // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue - var eventEntity = eventObject as IEntity; - if (eventEntity == null) - continue; - - // look for this entity in superseding event args - // found = must be removed, else track - if (IsSuperceeded(eventEntity, infos, entities)) - toRemove.Add(eventEntity); - else - entities.Add(Tuple.Create(eventEntity, infos)); - } - - // remove superseded entities - foreach (var entity in toRemove) - eventObjects.Remove(entity); - - // if there are still entities in the list, keep the event definition - if (eventObjects.Count > 0) - { - if (toRemove.Count > 0) - { - // re-assign if changed - args.EventObject = eventObjects; - } - - // track result arguments - // include event definition in result - resultArgs.Add(args); - result.Add(def); - } - } + l2.Add(e); } - } - // go over all args in result, and update them with the latest instanceof each entity - UpdateToLatestEntities(entities, resultArgs); - - // reverse, since we processed the list in reverse - result.Reverse(); - - return result; + events = l2; + break; + default: + throw new ArgumentOutOfRangeException("filter", filter, null); } - // edits event args to use the latest instance of each entity - private static void UpdateToLatestEntities(IEnumerable> entities, IEnumerable args) - { - // get the latest entities - // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates) - var latestEntities = new OrderedHashSet(keepOldest: true); - foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate)) - latestEntities.Add(entity.Item1); + return FilterSupersededAndUpdateToLatestEntity(events); + } - foreach (var arg in args) + public void ScopeExit(bool completed) + { + if (_events == null) + { + return; + } + + if (completed) + { + ScopeExitCompleted(); + } + + _events.Clear(); + } + + // this is way too convoluted, the supersede attribute is used only on DeleteEventargs to specify + // that it supersedes save, publish, move and copy - BUT - publish event args is also used for + // unpublishing and should NOT be superseded - so really it should not be managed at event args + // level but at event level + // + // what we want is: + // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should + // not trigger for the entity - and even though, does it make any sense? making a copy of an entity + // should ... trigger? + // + // not going to refactor it all - we probably want to *always* trigger event but tell people that + // due to scopes, they should not expected eg a saved entity to still be around - however, now, + // going to write a ugly condition to deal with U4-10764 + + // iterates over the events (latest first) and filter out any events or entities in event args that are included + // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want + // to raise the Saved event (well actually we just don't want to include it in the args for that saved event) + internal static IEnumerable FilterSupersededAndUpdateToLatestEntity( + IReadOnlyList events) + { + // keeps the 'latest' entity and associated event data + var entities = new List>(); + + // collects the event definitions + // collects the arguments in result, that require their entities to be updated + var result = new List(); + var resultArgs = new List(); + + // eagerly fetch superseded arg types for each arg type + var argTypeSuperceeding = events.Select(x => x.Args.GetType()) + .Distinct() + .ToDictionary( + x => x, + x => x.GetCustomAttributes(false).Select(y => y.SupersededEventArgsType) + .ToArray()); + + // iterate over all events and filter + // + // process the list in reverse, because events are added in the order they are raised and we want to keep + // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity + // is Deleted after being Saved, we want to filter out the Saved event + for (var index = events.Count - 1; index >= 0; index--) + { + IEventDefinition def = events[index]; + + var infos = new EventDefinitionInfos + { + EventDefinition = def, + SupersedeTypes = argTypeSuperceeding[def.Args.GetType()], + }; + + var args = def.Args as CancellableObjectEventArgs; + if (args == null) + { + // not a cancellable event arg, include event definition in result + result.Add(def); + } + else { // event object can either be a single object or an enumerable of objects // try to get as an enumerable, get null if it's not - var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject); + IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); if (eventObjects == null) { - // single object - // look for a more recent entity for that object, and replace if any - // works by "equalling" entities ie the more recent one "equals" this one (though different object) - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject)); - if (foundEntity != null) - arg.EventObject = foundEntity; + // single object, cast as an IEntity + // if cannot cast, cannot filter, nothing - just include event definition in result + if (args.EventObject is not IEntity eventEntity) + { + result.Add(def); + continue; + } + + // look for this entity in superseding event args + // found = must be removed (ie not added), else track + if (IsSuperceeded(eventEntity, infos, entities) == false) + { + // track + entities.Add(Tuple.Create(eventEntity, infos)); + + // track result arguments + // include event definition in result + resultArgs.Add(args); + result.Add(def); + } } else { // enumerable of objects - // same as above but for each object - var updated = false; - for (var i = 0; i < eventObjects.Count; i++) + var toRemove = new List(); + foreach (var eventObject in eventObjects) { - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i])); - if (foundEntity == null) continue; - eventObjects[i] = foundEntity; - updated = true; + // extract the event object, cast as an IEntity + // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue + if (eventObject is not IEntity eventEntity) + { + continue; + } + + // look for this entity in superseding event args + // found = must be removed, else track + if (IsSuperceeded(eventEntity, infos, entities)) + { + toRemove.Add(eventEntity); + } + else + { + entities.Add(Tuple.Create(eventEntity, infos)); + } } - if (updated) - arg.EventObject = eventObjects; + // remove superseded entities + foreach (IEntity entity in toRemove) + { + eventObjects.Remove(entity); + } + + // if there are still entities in the list, keep the event definition + if (eventObjects.Count > 0) + { + if (toRemove.Count > 0) + { + // re-assign if changed + args.EventObject = eventObjects; + } + + // track result arguments + // include event definition in result + resultArgs.Add(args); + result.Add(def); + } } } } - // determines if a given entity, appearing in a given event definition, should be filtered out, - // considering the entities that have already been visited - an entity is filtered out if it - // appears in another even definition, which supersedes this event definition. - private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List> entities) + // go over all args in result, and update them with the latest instanceof each entity + UpdateToLatestEntities(entities, resultArgs); + + // reverse, since we processed the list in reverse + result.Reverse(); + + return result; + } + + protected abstract void ScopeExitCompleted(); + + // edits event args to use the latest instance of each entity + private static void UpdateToLatestEntities( + IEnumerable> entities, + IEnumerable args) + { + // get the latest entities + // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates) + var latestEntities = new OrderedHashSet(true); + foreach (Tuple entity in entities.OrderByDescending(entity => + entity.Item1.UpdateDate)) { - //var argType = meta.EventArgsType; - var argType = infos.EventDefinition?.Args.GetType(); + latestEntities.Add(entity.Item1); + } - // look for other instances of the same entity, coming from an event args that supersedes other event args, - // ie is marked with the attribute, and is not this event args (cannot supersede itself) - var superceeding = entities - .Where(x => x.Item2.SupersedeTypes?.Length > 0 // has the attribute - && x.Item2.EventDefinition?.Args.GetType() != argType // is not the same - && Equals(x.Item1, entity)) // same entity - .ToArray(); - - // first time we see this entity = not filtered - if (superceeding.Length == 0) - return false; - - // delete event args does NOT supersedes 'unpublished' event - if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition?.EventName == "Unpublished") - return false; - - // found occurrences, need to determine if this event args is superseded - if (argType?.IsGenericType ?? false) + foreach (CancellableObjectEventArgs arg in args) + { + // event object can either be a single object or an enumerable of objects + // try to get as an enumerable, get null if it's not + IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject); + if (eventObjects == null) { - // generic, must compare type arguments - var supercededBy = superceeding.FirstOrDefault(x => - x.Item2.SupersedeTypes?.Any(y => - // superseding a generic type which has the same generic type definition - // (but ... no matter the generic type parameters? could be different?) - y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition() - // or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic? - || y.IsGenericTypeDefinition == false && y == argType) ?? false); - return supercededBy != null; + // single object + // look for a more recent entity for that object, and replace if any + // works by "equalling" entities ie the more recent one "equals" this one (though different object) + IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject)); + if (foundEntity != null) + { + arg.EventObject = foundEntity; + } } else { - // non-generic, can compare types 1:1 - var supercededBy = superceeding.FirstOrDefault(x => - x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false); - return supercededBy != null; + // enumerable of objects + // same as above but for each object + var updated = false; + for (var i = 0; i < eventObjects.Count; i++) + { + IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i])); + if (foundEntity == null) + { + continue; + } + + eventObjects[i] = foundEntity; + updated = true; + } + + if (updated) + { + arg.EventObject = eventObjects; + } } } + } - public void ScopeExit(bool completed) + // determines if a given entity, appearing in a given event definition, should be filtered out, + // considering the entities that have already been visited - an entity is filtered out if it + // appears in another even definition, which supersedes this event definition. + private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List> entities) + { + // var argType = meta.EventArgsType; + Type? argType = infos.EventDefinition?.Args.GetType(); + + // look for other instances of the same entity, coming from an event args that supersedes other event args, + // ie is marked with the attribute, and is not this event args (cannot supersede itself) + Tuple[] superceeding = entities + .Where(x => x.Item2.SupersedeTypes?.Length > 0 // has the attribute + && x.Item2.EventDefinition?.Args.GetType() != argType // is not the same + && Equals(x.Item1, entity)) // same entity + .ToArray(); + + // first time we see this entity = not filtered + if (superceeding.Length == 0) { - if (_events == null) return; - if (completed) - ScopeExitCompleted(); - _events.Clear(); + return false; } - protected abstract void ScopeExitCompleted(); + // delete event args does NOT supersedes 'unpublished' event + if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && + infos.EventDefinition?.EventName == "Unpublished") + { + return false; + } + + // found occurrences, need to determine if this event args is superseded + if (argType?.IsGenericType ?? false) + { + // generic, must compare type arguments + Tuple? supercededBy = superceeding.FirstOrDefault(x => + x.Item2.SupersedeTypes?.Any(y => + + // superseding a generic type which has the same generic type definition + // (but ... no matter the generic type parameters? could be different?) + (y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition()) + + // or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic? + || (y.IsGenericTypeDefinition == false && y == argType)) ?? false); + return supercededBy != null; + } + else + { + // non-generic, can compare types 1:1 + Tuple? supercededBy = superceeding.FirstOrDefault(x => + x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false); + return supercededBy != null; + } + } + + private class EventDefinitionInfos + { + public IEventDefinition? EventDefinition { get; set; } + + public Type[]? SupersedeTypes { get; set; } } } diff --git a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs index ee0d43a07a..44fb13016b 100644 --- a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs +++ b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs @@ -1,77 +1,84 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class RecycleBinEventArgs : CancellableEventArgs, IEquatable { - public class RecycleBinEventArgs : CancellableEventArgs, IEquatable + public RecycleBinEventArgs(Guid nodeObjectType, EventMessages eventMessages) + : base(true, eventMessages) => + NodeObjectType = nodeObjectType; + + public RecycleBinEventArgs(Guid nodeObjectType) + : base(true) => + NodeObjectType = nodeObjectType; + + /// + /// Gets the Id of the node object type of the items + /// being deleted from the Recycle Bin. + /// + public Guid NodeObjectType { get; } + + /// + /// Boolean indicating whether the Recycle Bin was emptied successfully + /// + public bool RecycleBinEmptiedSuccessfully { get; set; } + + /// + /// Boolean indicating whether this event was fired for the Content's Recycle Bin. + /// + public bool IsContentRecycleBin => NodeObjectType == Constants.ObjectTypes.Document; + + /// + /// Boolean indicating whether this event was fired for the Media's Recycle Bin. + /// + public bool IsMediaRecycleBin => NodeObjectType == Constants.ObjectTypes.Media; + + public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) => Equals(left, right); + + public bool Equals(RecycleBinEventArgs? other) { - public RecycleBinEventArgs(Guid nodeObjectType, EventMessages eventMessages) - : base(true, eventMessages) + if (ReferenceEquals(null, other)) { - NodeObjectType = nodeObjectType; + return false; } - public RecycleBinEventArgs(Guid nodeObjectType) - : base(true) + if (ReferenceEquals(this, other)) { - NodeObjectType = nodeObjectType; - + return true; } - /// - /// Gets the Id of the node object type of the items - /// being deleted from the Recycle Bin. - /// - public Guid NodeObjectType { get; } + return base.Equals(other) && NodeObjectType.Equals(other.NodeObjectType) && + RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + } - /// - /// Boolean indicating whether the Recycle Bin was emptied successfully - /// - public bool RecycleBinEmptiedSuccessfully { get; set; } - - /// - /// Boolean indicating whether this event was fired for the Content's Recycle Bin. - /// - public bool IsContentRecycleBin => NodeObjectType == Constants.ObjectTypes.Document; - - /// - /// Boolean indicating whether this event was fired for the Media's Recycle Bin. - /// - public bool IsMediaRecycleBin => NodeObjectType == Constants.ObjectTypes.Media; - - public bool Equals(RecycleBinEventArgs? other) + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && NodeObjectType.Equals(other.NodeObjectType) && RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((RecycleBinEventArgs) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - unchecked - { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ NodeObjectType.GetHashCode(); - hashCode = (hashCode * 397) ^ RecycleBinEmptiedSuccessfully.GetHashCode(); - return hashCode; - } + return false; } - public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) - { - return Equals(left, right); - } + return Equals((RecycleBinEventArgs)obj); + } - public static bool operator !=(RecycleBinEventArgs left, RecycleBinEventArgs right) + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ NodeObjectType.GetHashCode(); + hashCode = (hashCode * 397) ^ RecycleBinEmptiedSuccessfully.GetHashCode(); + return hashCode; } } + + public static bool operator !=(RecycleBinEventArgs left, RecycleBinEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/RefreshContentEventArgs.cs b/src/Umbraco.Core/Events/RefreshContentEventArgs.cs index c41043a039..00302e9f35 100644 --- a/src/Umbraco.Core/Events/RefreshContentEventArgs.cs +++ b/src/Umbraco.Core/Events/RefreshContentEventArgs.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Events -{ - //public class RefreshContentEventArgs : System.ComponentModel.CancelEventArgs { } -} +namespace Umbraco.Cms.Core.Events; + + +// public class RefreshContentEventArgs : System.ComponentModel.CancelEventArgs { } diff --git a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs index f37d8723a7..3817f93f6f 100644 --- a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs +++ b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs @@ -5,48 +5,49 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class RelateOnCopyNotificationHandler : INotificationHandler { - public class RelateOnCopyNotificationHandler : INotificationHandler + private readonly IAuditService _auditService; + private readonly IRelationService _relationService; + + public RelateOnCopyNotificationHandler(IRelationService relationService, IAuditService auditService) { - private readonly IRelationService _relationService; - private readonly IAuditService _auditService; + _relationService = relationService; + _auditService = auditService; + } - public RelateOnCopyNotificationHandler(IRelationService relationService, IAuditService auditService) + public void Handle(ContentCopiedNotification notification) + { + if (notification.RelateToOriginal == false) { - _relationService = relationService; - _auditService = auditService; + return; } - public void Handle(ContentCopiedNotification notification) + IRelationType? relationType = _relationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias); + + if (relationType == null) { - if (notification.RelateToOriginal == false) - { - return; - } + relationType = new RelationType( + Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, + Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, + true, + Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, + false); - var relationType = _relationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias); - - if (relationType == null) - { - relationType = new RelationType(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, - Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, - true, - Constants.ObjectTypes.Document, - Constants.ObjectTypes.Document, - false); - - _relationService.Save(relationType); - } - - var relation = new Relation(notification.Original.Id, notification.Copy.Id, relationType); - _relationService.Save(relation); - - _auditService.Add( - AuditType.Copy, - notification.Copy.WriterId, - notification.Copy.Id, ObjectTypes.GetName(UmbracoObjectTypes.Document) ?? string.Empty, - $"Copied content with Id: '{notification.Copy.Id}' related to original content with Id: '{notification.Original.Id}'"); + _relationService.Save(relationType); } + + var relation = new Relation(notification.Original.Id, notification.Copy.Id, relationType); + _relationService.Save(relation); + + _auditService.Add( + AuditType.Copy, + notification.Copy.WriterId, + notification.Copy.Id, + UmbracoObjectTypes.Document.GetName() ?? string.Empty, + $"Copied content with Id: '{notification.Copy.Id}' related to original content with Id: '{notification.Original.Id}'"); } } diff --git a/src/Umbraco.Core/Events/RolesEventArgs.cs b/src/Umbraco.Core/Events/RolesEventArgs.cs index a4fb6c3d18..a96de06713 100644 --- a/src/Umbraco.Core/Events/RolesEventArgs.cs +++ b/src/Umbraco.Core/Events/RolesEventArgs.cs @@ -1,16 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class RolesEventArgs : EventArgs { - public class RolesEventArgs : EventArgs + public RolesEventArgs(int[] memberIds, string[] roles) { - public RolesEventArgs(int[] memberIds, string[] roles) - { - MemberIds = memberIds; - Roles = roles; - } - - public int[] MemberIds { get; set; } - public string[] Roles { get; set; } + MemberIds = memberIds; + Roles = roles; } + + public int[] MemberIds { get; set; } + + public string[] Roles { get; set; } } diff --git a/src/Umbraco.Core/Events/RollbackEventArgs.cs b/src/Umbraco.Core/Events/RollbackEventArgs.cs index 96b67ba769..d23ac75f9a 100644 --- a/src/Umbraco.Core/Events/RollbackEventArgs.cs +++ b/src/Umbraco.Core/Events/RollbackEventArgs.cs @@ -1,21 +1,19 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class RollbackEventArgs : CancellableObjectEventArgs { - public class RollbackEventArgs : CancellableObjectEventArgs + public RollbackEventArgs(TEntity eventObject, bool canCancel) + : base(eventObject, canCancel) { - public RollbackEventArgs(TEntity eventObject, bool canCancel) : base(eventObject, canCancel) - { - } - - public RollbackEventArgs(TEntity eventObject) : base(eventObject) - { - } - - /// - /// The entity being rolledback - /// - public TEntity? Entity - { - get { return EventObject; } - } } + + public RollbackEventArgs(TEntity eventObject) + : base(eventObject) + { + } + + /// + /// The entity being rolledback + /// + public TEntity? Entity => EventObject; } diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index 3424962a54..319a0726f2 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -1,117 +1,113 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class SaveEventArgs : CancellableEnumerableObjectEventArgs { - public class SaveEventArgs : CancellableEnumerableObjectEventArgs + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) { - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) - { - } - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) - { - } - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - /// - public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(new List { eventObject }, canCancel, messages, additionalData) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) - { - } - - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel) - : base(eventObject, canCancel) - { - } - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - public SaveEventArgs(IEnumerable eventObject) - : base(eventObject) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - public SaveEventArgs(TEntity eventObject) - : base(new List { eventObject }) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public SaveEventArgs(TEntity eventObject, bool canCancel) - : base(new List { eventObject }, canCancel) - { - } - - /// - /// Returns all entities that were saved during the operation - /// - public IEnumerable? SavedEntities => EventObject; } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(new List { eventObject }, canCancel, messages, additionalData) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) + { + } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + public SaveEventArgs(IEnumerable eventObject) + : base(eventObject) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + public SaveEventArgs(TEntity eventObject) + : base(new List { eventObject }) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel) + : base(new List { eventObject }, canCancel) + { + } + + /// + /// Returns all entities that were saved during the operation + /// + public IEnumerable? SavedEntities => EventObject; } diff --git a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs index cdd8707a79..6681d321b7 100644 --- a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs +++ b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs @@ -1,135 +1,133 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class ScopedNotificationPublisher : IScopedNotificationPublisher { - public class ScopedNotificationPublisher : IScopedNotificationPublisher + private readonly IEventAggregator _eventAggregator; + private readonly object _locker = new(); + private readonly List _notificationOnScopeCompleted; + private bool _isSuppressed; + + public ScopedNotificationPublisher(IEventAggregator eventAggregator) { - private readonly IEventAggregator _eventAggregator; - private readonly List _notificationOnScopeCompleted; - private readonly object _locker = new object(); - private bool _isSuppressed = false; + _eventAggregator = eventAggregator; + _notificationOnScopeCompleted = new List(); + } - public ScopedNotificationPublisher(IEventAggregator eventAggregator) + public bool PublishCancelable(ICancelableNotification notification) + { + if (notification == null) { - _eventAggregator = eventAggregator; - _notificationOnScopeCompleted = new List(); + throw new ArgumentNullException(nameof(notification)); } - public bool PublishCancelable(ICancelableNotification notification) + if (_isSuppressed) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - if (_isSuppressed) - { - return false; - } - - _eventAggregator.Publish(notification); - return notification.Cancel; + return false; } - public async Task PublishCancelableAsync(ICancelableNotification notification) + _eventAggregator.Publish(notification); + return notification.Cancel; + } + + public async Task PublishCancelableAsync(ICancelableNotification notification) + { + if (notification == null) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - if (_isSuppressed) - { - return false; - } - - var task = _eventAggregator.PublishAsync(notification); - if (task is not null) - { - await task; - } - - return notification.Cancel; + throw new ArgumentNullException(nameof(notification)); } - public void Publish(INotification notification) + if (_isSuppressed) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - if (_isSuppressed) - { - return; - } - - _notificationOnScopeCompleted.Add(notification); + return false; } - public void ScopeExit(bool completed) + Task task = _eventAggregator.PublishAsync(notification); + if (task is not null) { - try + await task; + } + + return notification.Cancel; + } + + public void Publish(INotification notification) + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + if (_isSuppressed) + { + return; + } + + _notificationOnScopeCompleted.Add(notification); + } + + public void ScopeExit(bool completed) + { + try + { + if (completed) { - if (completed) + foreach (INotification notification in _notificationOnScopeCompleted) { - foreach (INotification notification in _notificationOnScopeCompleted) + _eventAggregator.Publish(notification); + } + } + } + finally + { + _notificationOnScopeCompleted.Clear(); + } + } + + public IDisposable Suppress() + { + lock (_locker) + { + if (_isSuppressed) + { + throw new InvalidOperationException("Notifications are already suppressed"); + } + + return new Suppressor(this); + } + } + + private class Suppressor : IDisposable + { + private readonly ScopedNotificationPublisher _scopedNotificationPublisher; + private bool _disposedValue; + + public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) + { + _scopedNotificationPublisher = scopedNotificationPublisher; + _scopedNotificationPublisher._isSuppressed = true; + } + + public void Dispose() => Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + lock (_scopedNotificationPublisher._locker) { - _eventAggregator.Publish(notification); + _scopedNotificationPublisher._isSuppressed = false; } } - } - finally - { - _notificationOnScopeCompleted.Clear(); - } - } - public IDisposable Suppress() - { - lock(_locker) - { - if (_isSuppressed) - { - throw new InvalidOperationException("Notifications are already suppressed"); - } - return new Suppressor(this); + _disposedValue = true; } } - - private class Suppressor : IDisposable - { - private bool _disposedValue; - private readonly ScopedNotificationPublisher _scopedNotificationPublisher; - - public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) - { - _scopedNotificationPublisher = scopedNotificationPublisher; - _scopedNotificationPublisher._isSuppressed = true; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - lock (_scopedNotificationPublisher._locker) - { - _scopedNotificationPublisher._isSuppressed = false; - } - } - _disposedValue = true; - } - } - public void Dispose() => Dispose(disposing: true); - } } } diff --git a/src/Umbraco.Core/Events/SendEmailEventArgs.cs b/src/Umbraco.Core/Events/SendEmailEventArgs.cs index c1e626c6c1..2e75d1b583 100644 --- a/src/Umbraco.Core/Events/SendEmailEventArgs.cs +++ b/src/Umbraco.Core/Events/SendEmailEventArgs.cs @@ -1,15 +1,10 @@ -using System; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Events -{ - public class SendEmailEventArgs : EventArgs - { - public EmailMessage Message { get; } +namespace Umbraco.Cms.Core.Events; - public SendEmailEventArgs(EmailMessage message) - { - Message = message; - } - } +public class SendEmailEventArgs : EventArgs +{ + public SendEmailEventArgs(EmailMessage message) => Message = message; + + public EmailMessage Message { get; } } diff --git a/src/Umbraco.Core/Events/SendToPublishEventArgs.cs b/src/Umbraco.Core/Events/SendToPublishEventArgs.cs index 9b4e078149..a72cd82012 100644 --- a/src/Umbraco.Core/Events/SendToPublishEventArgs.cs +++ b/src/Umbraco.Core/Events/SendToPublishEventArgs.cs @@ -1,21 +1,19 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class SendToPublishEventArgs : CancellableObjectEventArgs { - public class SendToPublishEventArgs : CancellableObjectEventArgs + public SendToPublishEventArgs(TEntity eventObject, bool canCancel) + : base(eventObject, canCancel) { - public SendToPublishEventArgs(TEntity eventObject, bool canCancel) : base(eventObject, canCancel) - { - } - - public SendToPublishEventArgs(TEntity eventObject) : base(eventObject) - { - } - - /// - /// The entity being sent to publish - /// - public TEntity? Entity - { - get { return EventObject; } - } } + + public SendToPublishEventArgs(TEntity eventObject) + : base(eventObject) + { + } + + /// + /// The entity being sent to publish + /// + public TEntity? Entity => EventObject; } diff --git a/src/Umbraco.Core/Events/SupersedeEventAttribute.cs b/src/Umbraco.Core/Events/SupersedeEventAttribute.cs index d733f0706a..21137968f0 100644 --- a/src/Umbraco.Core/Events/SupersedeEventAttribute.cs +++ b/src/Umbraco.Core/Events/SupersedeEventAttribute.cs @@ -1,20 +1,15 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// This is used to know if the event arg attributed should supersede another event arg type when +/// tracking events for the same entity. If one event args supersedes another then the event args that have been +/// superseded +/// will mean that the event will not be dispatched or the args will be filtered to exclude the entity. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class SupersedeEventAttribute : Attribute { - /// - /// This is used to know if the event arg attributed should supersede another event arg type when - /// tracking events for the same entity. If one event args supersedes another then the event args that have been superseded - /// will mean that the event will not be dispatched or the args will be filtered to exclude the entity. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class SupersedeEventAttribute : Attribute - { - public Type SupersededEventArgsType { get; private set; } + public SupersedeEventAttribute(Type supersededEventArgsType) => SupersededEventArgsType = supersededEventArgsType; - public SupersedeEventAttribute(Type supersededEventArgsType) - { - SupersededEventArgsType = supersededEventArgsType; - } - } + public Type SupersededEventArgsType { get; } } diff --git a/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs b/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs index 2c8dde89a2..8495da25b0 100644 --- a/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs +++ b/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs @@ -1,18 +1,11 @@ -namespace Umbraco.Cms.Core.Events -{ - /// - /// A simple/default transient messages factory - /// - public class TransientEventMessagesFactory : IEventMessagesFactory - { - public EventMessages Get() - { - return new EventMessages(); - } +namespace Umbraco.Cms.Core.Events; - public EventMessages? GetOrDefault() - { - return null; - } - } +/// +/// A simple/default transient messages factory +/// +public class TransientEventMessagesFactory : IEventMessagesFactory +{ + public EventMessages Get() => new EventMessages(); + + public EventMessages? GetOrDefault() => null; } diff --git a/src/Umbraco.Core/Events/TypedEventHandler.cs b/src/Umbraco.Core/Events/TypedEventHandler.cs index 11301448e0..e359bd47f9 100644 --- a/src/Umbraco.Core/Events/TypedEventHandler.cs +++ b/src/Umbraco.Core/Events/TypedEventHandler.cs @@ -1,7 +1,4 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events -{ - [Serializable] - public delegate void TypedEventHandler(TSender sender, TEventArgs e); -} +[Serializable] +public delegate void TypedEventHandler(TSender sender, TEventArgs e); diff --git a/src/Umbraco.Core/Events/UserGroupWithUsers.cs b/src/Umbraco.Core/Events/UserGroupWithUsers.cs index 17946a781f..f3a77e22e6 100644 --- a/src/Umbraco.Core/Events/UserGroupWithUsers.cs +++ b/src/Umbraco.Core/Events/UserGroupWithUsers.cs @@ -1,18 +1,19 @@ -using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class UserGroupWithUsers { - public class UserGroupWithUsers + public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers) { - public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers) - { - UserGroup = userGroup; - AddedUsers = addedUsers; - RemovedUsers = removedUsers; - } - - public IUserGroup UserGroup { get; } - public IUser[] AddedUsers { get; } - public IUser[] RemovedUsers { get; } + UserGroup = userGroup; + AddedUsers = addedUsers; + RemovedUsers = removedUsers; } + + public IUserGroup UserGroup { get; } + + public IUser[] AddedUsers { get; } + + public IUser[] RemovedUsers { get; } } diff --git a/src/Umbraco.Core/Events/UserNotificationsHandler.cs b/src/Umbraco.Core/Events/UserNotificationsHandler.cs index 96425e644f..042355630f 100644 --- a/src/Umbraco.Core/Events/UserNotificationsHandler.cs +++ b/src/Umbraco.Core/Events/UserNotificationsHandler.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Actions; @@ -18,221 +15,242 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public sealed class UserNotificationsHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - public sealed class UserNotificationsHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly ActionCollection _actions; + private readonly IContentService _contentService; + private readonly Notifier _notifier; + + public UserNotificationsHandler(Notifier notifier, ActionCollection actions, IContentService contentService) { - private readonly Notifier _notifier; - private readonly ActionCollection _actions; - private readonly IContentService _contentService; + _notifier = notifier; + _actions = actions; + _contentService = contentService; + } - public UserNotificationsHandler(Notifier notifier, ActionCollection actions, IContentService contentService) + public void Handle(AssignedUserGroupPermissionsNotification notification) + { + IContent[]? entities = _contentService.GetByIds(notification.EntityPermissions.Select(e => e.EntityId)).ToArray(); + if (entities?.Any() == false) { - _notifier = notifier; - _actions = actions; - _contentService = contentService; + return; } - public void Handle(ContentSavedNotification notification) - { - var newEntities = new List(); - var updatedEntities = new List(); + _notifier.Notify(_actions.GetAction(), entities!); + } - //need to determine if this is updating or if it is new - foreach (var entity in notification.SavedEntities) + public void Handle(ContentCopiedNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.Original); + + public void Handle(ContentMovedNotification notification) + { + // notify about the move for all moved items + _notifier.Notify( + _actions.GetAction(), + notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); + + // for any items being moved from the recycle bin (restored), explicitly notify about that too + IContent[] restoredEntities = notification.MoveInfoCollection + .Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString)) + .Select(m => m.Entity) + .ToArray(); + if (restoredEntities.Any()) + { + _notifier.Notify(_actions.GetAction(), restoredEntities); + } + } + + public void Handle(ContentMovedToRecycleBinNotification notification) => _notifier.Notify( + _actions.GetAction(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); + + public void Handle(ContentPublishedNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.PublishedEntities.ToArray()); + + public void Handle(ContentRolledBackNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.Entity); + + public void Handle(ContentSavedNotification notification) + { + var newEntities = new List(); + var updatedEntities = new List(); + + // need to determine if this is updating or if it is new + foreach (IContent entity in notification.SavedEntities) + { + var dirty = (IRememberBeingDirty)entity; + if (dirty.WasPropertyDirty("Id")) { - var dirty = (IRememberBeingDirty)entity; - if (dirty.WasPropertyDirty("Id")) - { - //it's new - newEntities.Add(entity); - } - else - { - //it's updating - updatedEntities.Add(entity); - } + // it's new + newEntities.Add(entity); } - _notifier.Notify(_actions.GetAction(), newEntities.ToArray()); - _notifier.Notify(_actions.GetAction(), updatedEntities.ToArray()); - } - - public void Handle(ContentSortedNotification notification) - { - var parentId = notification.SortedEntities.Select(x => x.ParentId).Distinct().ToList(); - if (parentId.Count != 1) - return; // this shouldn't happen, for sorting all entities will have the same parent id - - // in this case there's nothing to report since if the root is sorted we can't report on a fake entity. - // this is how it was in v7, we can't report on root changes because you can't subscribe to root changes. - if (parentId[0] <= 0) - return; - - var parent = _contentService.GetById(parentId[0]); - if (parent == null) - return; // this shouldn't happen - - _notifier.Notify(_actions.GetAction(), new[] { parent }); - } - - public void Handle(ContentPublishedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.PublishedEntities.ToArray()); - - public void Handle(ContentMovedNotification notification) - { - // notify about the move for all moved items - _notifier.Notify(_actions.GetAction(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); - - // for any items being moved from the recycle bin (restored), explicitly notify about that too - var restoredEntities = notification.MoveInfoCollection - .Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString)) - .Select(m => m.Entity) - .ToArray(); - if (restoredEntities.Any()) + else { - _notifier.Notify(_actions.GetAction(), restoredEntities); + // it's updating + updatedEntities.Add(entity); } } - public void Handle(ContentMovedToRecycleBinNotification notification) => _notifier.Notify(_actions.GetAction(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); + _notifier.Notify(_actions.GetAction(), newEntities.ToArray()); + _notifier.Notify(_actions.GetAction(), updatedEntities.ToArray()); + } - public void Handle(ContentCopiedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Original); + public void Handle(ContentSentToPublishNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.Entity); - public void Handle(ContentRolledBackNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Entity); + public void Handle(ContentSortedNotification notification) + { + var parentId = notification.SortedEntities.Select(x => x.ParentId).Distinct().ToList(); + if (parentId.Count != 1) + { + return; // this shouldn't happen, for sorting all entities will have the same parent id + } - public void Handle(ContentSentToPublishNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Entity); + // in this case there's nothing to report since if the root is sorted we can't report on a fake entity. + // this is how it was in v7, we can't report on root changes because you can't subscribe to root changes. + if (parentId[0] <= 0) + { + return; + } - public void Handle(ContentUnpublishedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.UnpublishedEntities.ToArray()); + IContent? parent = _contentService.GetById(parentId[0]); + if (parent == null) + { + return; // this shouldn't happen + } + + _notifier.Notify(_actions.GetAction(), parent); + } + + public void Handle(ContentUnpublishedNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.UnpublishedEntities.ToArray()); + + public void Handle(PublicAccessEntrySavedNotification notification) + { + IContent[] entities = _contentService.GetByIds(notification.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray(); + if (entities.Any() == false) + { + return; + } + + _notifier.Notify(_actions.GetAction(), entities); + } + + /// + /// This class is used to send the notifications + /// + public sealed class Notifier + { + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly INotificationService _notificationService; + private readonly ILocalizedTextService _textService; + private readonly IUserService _userService; + private GlobalSettings _globalSettings; /// - /// This class is used to send the notifications + /// Initializes a new instance of the class. /// - public sealed class Notifier + public Notifier( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IHostingEnvironment hostingEnvironment, + INotificationService notificationService, + IUserService userService, + ILocalizedTextService textService, + IOptionsMonitor globalSettings, + ILogger logger) { - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly INotificationService _notificationService; - private readonly IUserService _userService; - private readonly ILocalizedTextService _textService; - private GlobalSettings _globalSettings; - private readonly ILogger _logger; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _hostingEnvironment = hostingEnvironment; + _notificationService = notificationService; + _userService = userService; + _textService = textService; + _globalSettings = globalSettings.CurrentValue; + _logger = logger; - /// - /// Initializes a new instance of the class. - /// - public Notifier( - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IHostingEnvironment hostingEnvironment, - INotificationService notificationService, - IUserService userService, - ILocalizedTextService textService, - IOptionsMonitor globalSettings, - ILogger logger) + globalSettings.OnChange(x => _globalSettings = x); + } + + public void Notify(IAction? action, params IContent[] entities) + { + IUser? user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; + + // if there is no current user, then use the admin + if (user == null) { - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _hostingEnvironment = hostingEnvironment; - _notificationService = notificationService; - _userService = userService; - _textService = textService; - _globalSettings = globalSettings.CurrentValue; - _logger = logger; - - globalSettings.OnChange(x => _globalSettings = x); - } - - public void Notify(IAction? action, params IContent[] entities) - { - var user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - - //if there is no current user, then use the admin + _logger.LogDebug( + "There is no current Umbraco user logged in, the notifications will be sent from the administrator"); + user = _userService.GetUserById(Constants.Security.SuperUserId); if (user == null) { - _logger.LogDebug("There is no current Umbraco user logged in, the notifications will be sent from the administrator"); - user = _userService.GetUserById(Constants.Security.SuperUserId); - if (user == null) - { - _logger.LogWarning("Notifications can not be sent, no admin user with id {SuperUserId} could be resolved", Constants.Security.SuperUserId); - return; - } - } - - SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl); - } - - private void SendNotification(IUser sender, IEnumerable entities, IAction? action, Uri? siteUri) - { - if (sender == null) - throw new ArgumentNullException(nameof(sender)); - if (siteUri == null) - { - _logger.LogWarning("Notifications can not be sent, no site URL is set (might be during boot process?)"); + _logger.LogWarning( + "Notifications can not be sent, no admin user with id {SuperUserId} could be resolved", + Constants.Security.SuperUserId); return; } - - //group by the content type variation since the emails will be different - foreach (var contentVariantGroup in entities.GroupBy(x => x.ContentType.Variations)) - { - _notificationService.SendNotifications( - sender, - contentVariantGroup, - action?.Letter.ToString(CultureInfo.InvariantCulture), - _textService.Localize("actions", action?.Alias), - siteUri, - ((IUser user, NotificationEmailSubjectParams subject) x) - => _textService.Localize( - "notifications", "mailSubject", - x.user.GetUserCulture(_textService, _globalSettings), - new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }), - ((IUser user, NotificationEmailBodyParams body, bool isHtml) x) - => _textService.Localize( - "notifications", x.isHtml ? "mailBodyHtml" : "mailBody", - x.user.GetUserCulture(_textService, _globalSettings), - new[] - { - x.body.RecipientName, - x.body.Action, - x.body.ItemName, - x.body.EditedUser, - x.body.SiteUrl, - x.body.ItemId, - //format the summary depending on if it's variant or not - contentVariantGroup.Key == ContentVariation.Culture - ? (x.isHtml ? _textService.Localize("notifications", "mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications","mailBodyVariantSummary", new []{ x.body.Summary })) - : x.body.Summary, - x.body.ItemUrl - })); - } } + + SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl); } - public void Handle(AssignedUserGroupPermissionsNotification notification) + private void SendNotification(IUser sender, IEnumerable entities, IAction? action, Uri? siteUri) { - var entities = _contentService.GetByIds(notification.EntityPermissions.Select(e => e.EntityId))?.ToArray(); - if (entities?.Any() == false) + if (sender == null) { - return; + throw new ArgumentNullException(nameof(sender)); } - _notifier.Notify(_actions.GetAction(), entities!); - } - public void Handle(PublicAccessEntrySavedNotification notification) - { - var entities = _contentService.GetByIds(notification.SavedEntities.Select(e => e.ProtectedNodeId))?.ToArray(); - if (entities?.Any() == false) + if (siteUri == null) { + _logger.LogWarning("Notifications can not be sent, no site URL is set (might be during boot process?)"); return; } - _notifier.Notify(_actions.GetAction(), entities!); + + // group by the content type variation since the emails will be different + foreach (IGrouping contentVariantGroup in entities.GroupBy(x => + x.ContentType.Variations)) + { + _notificationService.SendNotifications( + sender, + contentVariantGroup, + action?.Letter.ToString(CultureInfo.InvariantCulture), + _textService.Localize("actions", action?.Alias), + siteUri, + x + => _textService.Localize( + "notifications", "mailSubject", x.user.GetUserCulture(_textService, _globalSettings), new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }), + x + => _textService.Localize( + "notifications", + x.isHtml ? "mailBodyHtml" : "mailBody", + x.user.GetUserCulture(_textService, _globalSettings), + new[] + { + x.body.RecipientName, x.body.Action, x.body.ItemName, x.body.EditedUser, x.body.SiteUrl, + x.body.ItemId, + + // format the summary depending on if it's variant or not + contentVariantGroup.Key == ContentVariation.Culture + ? x.isHtml + ? _textService.Localize("notifications", "mailBodyVariantHtmlSummary", new[] { x.body.Summary }) + : _textService.Localize("notifications", "mailBodyVariantSummary", new[] { x.body.Summary }) + : x.body.Summary, + x.body.ItemUrl, + })); + } } } } diff --git a/src/Umbraco.Core/Exceptions/AuthorizationException.cs b/src/Umbraco.Core/Exceptions/AuthorizationException.cs index fa2399fc5c..fd55a94b7b 100644 --- a/src/Umbraco.Core/Exceptions/AuthorizationException.cs +++ b/src/Umbraco.Core/Exceptions/AuthorizationException.cs @@ -1,45 +1,56 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// The exception that is thrown when authorization failed. +/// +/// +[Serializable] +public class AuthorizationException : Exception { /// - /// The exception that is thrown when authorization failed. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class AuthorizationException : Exception + public AuthorizationException() { - /// - /// Initializes a new instance of the class. - /// - public AuthorizationException() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public AuthorizationException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public AuthorizationException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public AuthorizationException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public AuthorizationException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected AuthorizationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected AuthorizationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/Exceptions/BootFailedException.cs b/src/Umbraco.Core/Exceptions/BootFailedException.cs index eeac07869d..5ade44a68f 100644 --- a/src/Umbraco.Core/Exceptions/BootFailedException.cs +++ b/src/Umbraco.Core/Exceptions/BootFailedException.cs @@ -1,83 +1,96 @@ -using System; using System.Runtime.Serialization; using System.Text; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// An exception that is thrown if the Umbraco application cannot boot. +/// +/// +[Serializable] +public class BootFailedException : Exception { /// - /// An exception that is thrown if the Umbraco application cannot boot. + /// Defines the default boot failed exception message. /// - /// - [Serializable] - public class BootFailedException : Exception + public const string DefaultMessage = "Boot failed: Umbraco cannot run. See Umbraco's log file for more details."; + + /// + /// Initializes a new instance of the class. + /// + public BootFailedException() { - /// - /// Defines the default boot failed exception message. - /// - public const string DefaultMessage = "Boot failed: Umbraco cannot run. See Umbraco's log file for more details."; + } - /// - /// Initializes a new instance of the class. - /// - public BootFailedException() - { } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public BootFailedException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public BootFailedException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception which is the cause of this exception. + /// + /// The message that describes the error. + /// The inner exception, or null. + public BootFailedException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception which is the cause of this exception. - /// - /// The message that describes the error. - /// The inner exception, or null. - public BootFailedException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected BootFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected BootFailedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } - - /// - /// Rethrows a captured . - /// - /// The boot failed exception. - /// - /// - /// - /// The exception can be null, in which case a default message is used. - /// - public static void Rethrow(BootFailedException? bootFailedException) + /// + /// Rethrows a captured . + /// + /// The boot failed exception. + /// + /// + /// + /// The exception can be null, in which case a default message is used. + /// + public static void Rethrow(BootFailedException? bootFailedException) + { + if (bootFailedException == null) { - if (bootFailedException == null) - throw new BootFailedException(DefaultMessage); - - // see https://stackoverflow.com/questions/57383 - // would that be the correct way to do it? - //ExceptionDispatchInfo.Capture(bootFailedException).Throw(); - - Exception? e = bootFailedException; - var m = new StringBuilder(); - m.Append(DefaultMessage); - while (e != null) - { - m.Append($"\n\n-> {e.GetType().FullName}: {e.Message}"); - if (string.IsNullOrWhiteSpace(e.StackTrace) == false) - m.Append($"\n{e.StackTrace}"); - e = e.InnerException; - } - throw new BootFailedException(m.ToString()); + throw new BootFailedException(DefaultMessage); } + + // see https://stackoverflow.com/questions/57383 + // would that be the correct way to do it? + // ExceptionDispatchInfo.Capture(bootFailedException).Throw(); + Exception? e = bootFailedException; + var m = new StringBuilder(); + m.Append(DefaultMessage); + while (e != null) + { + m.Append($"\n\n-> {e.GetType().FullName}: {e.Message}"); + if (string.IsNullOrWhiteSpace(e.StackTrace) == false) + { + m.Append($"\n{e.StackTrace}"); + } + + e = e.InnerException; + } + + throw new BootFailedException(m.ToString()); } } diff --git a/src/Umbraco.Core/Exceptions/ConfigurationException.cs b/src/Umbraco.Core/Exceptions/ConfigurationException.cs index fe711a9823..89d8bfc01d 100644 --- a/src/Umbraco.Core/Exceptions/ConfigurationException.cs +++ b/src/Umbraco.Core/Exceptions/ConfigurationException.cs @@ -1,41 +1,47 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// An exception that is thrown if the configuration is wrong. +/// +/// +[Serializable] +public class ConfigurationException : Exception { /// - /// An exception that is thrown if the configuration is wrong. + /// Initializes a new instance of the class with a specified error message. /// - /// - [Serializable] - public class ConfigurationException : Exception + /// The message that describes the error. + public ConfigurationException(string message) + : base(message) { - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public ConfigurationException(string message) - : base(message) - { } + } - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception which is the cause of this exception. - /// - /// The message that describes the error. - /// The inner exception, or null. - public ConfigurationException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected ConfigurationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception which is the cause of this exception. + /// + /// The message that describes the error. + /// The inner exception, or null. + public ConfigurationException(string message, Exception innerException) + : base(message, innerException) + { + } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected ConfigurationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/Exceptions/DataOperationException.cs b/src/Umbraco.Core/Exceptions/DataOperationException.cs index 0b56cfb72c..9acc6ded38 100644 --- a/src/Umbraco.Core/Exceptions/DataOperationException.cs +++ b/src/Umbraco.Core/Exceptions/DataOperationException.cs @@ -1,98 +1,111 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// +/// +/// +[Serializable] +public class DataOperationException : Exception + where T : Enum { /// - /// + /// Initializes a new instance of the class. /// - /// - /// - [Serializable] - public class DataOperationException : Exception - where T : Enum + public DataOperationException() { - /// - /// Gets the operation. - /// - /// - /// The operation. - /// - /// - /// This object should be serializable to prevent a to be thrown. - /// - public T? Operation { get; private set; } + } - /// - /// Initializes a new instance of the class. - /// - public DataOperationException() - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public DataOperationException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public DataOperationException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public DataOperationException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public DataOperationException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The operation. + public DataOperationException(T operation) + : this(operation, "Data operation exception: " + operation) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The operation. - public DataOperationException(T operation) - : this(operation, "Data operation exception: " + operation) - { } + /// + /// Initializes a new instance of the class. + /// + /// The operation. + /// The message. + public DataOperationException(T operation, string message) + : base(message) => + Operation = operation; - /// - /// Initializes a new instance of the class. - /// - /// The operation. - /// The message. - public DataOperationException(T operation, string message) - : base(message) + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + protected DataOperationException(SerializationInfo info, StreamingContext context) + : base(info, context) => + Operation = (T)Enum.Parse(typeof(T), info.GetString(nameof(Operation)) ?? string.Empty); + + /// + /// Gets the operation. + /// + /// + /// The operation. + /// + /// + /// This object should be serializable to prevent a to be thrown. + /// + public T? Operation { get; private set; } + + /// + /// When overridden in a derived class, sets the with + /// information about the exception. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) { - Operation = operation; + throw new ArgumentNullException(nameof(info)); } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - protected DataOperationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Operation = (T)Enum.Parse(typeof(T), info.GetString(nameof(Operation)) ?? string.Empty); - } + info.AddValue(nameof(Operation), Operation is not null ? Enum.GetName(typeof(T), Operation) : string.Empty); - /// - /// When overridden in a derived class, sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(Operation), Operation is not null ? Enum.GetName(typeof(T), Operation) : string.Empty); - - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); } } diff --git a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs index ba8c2b6106..9bc51d7b6e 100644 --- a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs +++ b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs @@ -1,166 +1,192 @@ -using System; using System.Runtime.Serialization; -using Umbraco.Extensions; using System.Text; +using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// The exception that is thrown when a composition is invalid. +/// +/// +[Serializable] +public class InvalidCompositionException : Exception { /// - /// The exception that is thrown when a composition is invalid. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class InvalidCompositionException : Exception + public InvalidCompositionException() { - /// - /// Gets the content type alias. - /// - /// - /// The content type alias. - /// - public string? ContentTypeAlias { get; } + } - /// - /// Gets the added composition alias. - /// - /// - /// The added composition alias. - /// - public string? AddedCompositionAlias { get; } + /// + /// Initializes a new instance of the class. + /// + /// The content type alias. + /// The property type aliases. + public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliases) + : this(contentTypeAlias, null, propertyTypeAliases) + { + } - /// - /// Gets the property type aliases. - /// - /// - /// The property type aliases. - /// - public string[]? PropertyTypeAliases { get; } + /// + /// Initializes a new instance of the class. + /// + /// The content type alias. + /// The added composition alias. + /// The property type aliases. + public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases) + : this(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, new string[0]) + { + } - /// - /// Gets the property group aliases. - /// - /// - /// The property group aliases. - /// - public string[]? PropertyGroupAliases { get; } + /// + /// Initializes a new instance of the class. + /// + /// The content type alias. + /// The added composition alias. + /// The property type aliases. + /// The property group aliases. + public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) + : this(FormatMessage(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, propertyGroupAliases)) + { + ContentTypeAlias = contentTypeAlias; + AddedCompositionAlias = addedCompositionAlias; + PropertyTypeAliases = propertyTypeAliases; + PropertyGroupAliases = propertyGroupAliases; + } - /// - /// Initializes a new instance of the class. - /// - public InvalidCompositionException() - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public InvalidCompositionException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The content type alias. - /// The property type aliases. - public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliases) - : this(contentTypeAlias, null, propertyTypeAliases) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public InvalidCompositionException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The content type alias. - /// The added composition alias. - /// The property type aliases. - public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases) - : this(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, new string[0]) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected InvalidCompositionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + ContentTypeAlias = info.GetString(nameof(ContentTypeAlias)); + AddedCompositionAlias = info.GetString(nameof(AddedCompositionAlias)); + PropertyTypeAliases = (string[]?)info.GetValue(nameof(PropertyTypeAliases), typeof(string[])); + PropertyGroupAliases = (string[]?)info.GetValue(nameof(PropertyGroupAliases), typeof(string[])); + } - /// - /// Initializes a new instance of the class. - /// - /// The content type alias. - /// The added composition alias. - /// The property type aliases. - /// The property group aliases. - public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) - : this(FormatMessage(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, propertyGroupAliases)) + /// + /// Gets the content type alias. + /// + /// + /// The content type alias. + /// + public string? ContentTypeAlias { get; } + + /// + /// Gets the added composition alias. + /// + /// + /// The added composition alias. + /// + public string? AddedCompositionAlias { get; } + + /// + /// Gets the property type aliases. + /// + /// + /// The property type aliases. + /// + public string[]? PropertyTypeAliases { get; } + + /// + /// Gets the property group aliases. + /// + /// + /// The property group aliases. + /// + public string[]? PropertyGroupAliases { get; } + + /// + /// When overridden in a derived class, sets the with + /// information about the exception. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) { - ContentTypeAlias = contentTypeAlias; - AddedCompositionAlias = addedCompositionAlias; - PropertyTypeAliases = propertyTypeAliases; - PropertyGroupAliases = propertyGroupAliases; + throw new ArgumentNullException(nameof(info)); } - private static string FormatMessage(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) + info.AddValue(nameof(ContentTypeAlias), ContentTypeAlias); + info.AddValue(nameof(AddedCompositionAlias), AddedCompositionAlias); + info.AddValue(nameof(PropertyTypeAliases), PropertyTypeAliases); + info.AddValue(nameof(PropertyGroupAliases), PropertyGroupAliases); + + base.GetObjectData(info, context); + } + + private static string FormatMessage(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) + { + var sb = new StringBuilder(); + + if (addedCompositionAlias.IsNullOrWhiteSpace()) { - var sb = new StringBuilder(); - - if (addedCompositionAlias.IsNullOrWhiteSpace()) - { - sb.AppendFormat("Content type with alias '{0}' has an invalid composition.", contentTypeAlias); - } - else - { - sb.AppendFormat("Content type with alias '{0}' was added as a composition to content type with alias '{1}', but there was a conflict.", addedCompositionAlias, contentTypeAlias); - } - - if (propertyTypeAliases.Length > 0) - { - sb.AppendFormat(" Property types must have a unique alias across all compositions, these aliases are duplicate: {0}.", string.Join(", ", propertyTypeAliases)); - } - - if (propertyGroupAliases.Length > 0) - { - sb.AppendFormat(" Property groups with the same alias must also have the same type across all compositions, these aliases have different types: {0}.", string.Join(", ", propertyGroupAliases)); - } - - return sb.ToString(); + sb.AppendFormat("Content type with alias '{0}' has an invalid composition.", contentTypeAlias); + } + else + { + sb.AppendFormat( + "Content type with alias '{0}' was added as a composition to content type with alias '{1}', but there was a conflict.", + addedCompositionAlias, + contentTypeAlias); } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public InvalidCompositionException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public InvalidCompositionException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected InvalidCompositionException(SerializationInfo info, StreamingContext context) - : base(info, context) + if (propertyTypeAliases.Length > 0) { - ContentTypeAlias = info.GetString(nameof(ContentTypeAlias)); - AddedCompositionAlias = info.GetString(nameof(AddedCompositionAlias)); - PropertyTypeAliases = (string[]?)info.GetValue(nameof(PropertyTypeAliases), typeof(string[])); - PropertyGroupAliases = (string[] ?)info.GetValue(nameof(PropertyGroupAliases), typeof(string[])); + sb.AppendFormat( + " Property types must have a unique alias across all compositions, these aliases are duplicate: {0}.", + string.Join(", ", propertyTypeAliases)); } - /// - /// When overridden in a derived class, sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - public override void GetObjectData(SerializationInfo info, StreamingContext context) + if (propertyGroupAliases.Length > 0) { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(ContentTypeAlias), ContentTypeAlias); - info.AddValue(nameof(AddedCompositionAlias), AddedCompositionAlias); - info.AddValue(nameof(PropertyTypeAliases), PropertyTypeAliases); - info.AddValue(nameof(PropertyGroupAliases), PropertyGroupAliases); - - base.GetObjectData(info, context); + sb.AppendFormat( + " Property groups with the same alias must also have the same type across all compositions, these aliases have different types: {0}.", + string.Join(", ", propertyGroupAliases)); } + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs index 9ba1311e84..99ce96c273 100644 --- a/src/Umbraco.Core/Exceptions/PanicException.cs +++ b/src/Umbraco.Core/Exceptions/PanicException.cs @@ -1,45 +1,57 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// Represents an internal exception that in theory should never been thrown, it is only thrown in circumstances that +/// should never happen. +/// +/// +[Serializable] +public class PanicException : Exception { /// - /// Represents an internal exception that in theory should never been thrown, it is only thrown in circumstances that should never happen. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class PanicException : Exception + public PanicException() { - /// - /// Initializes a new instance of the class. - /// - public PanicException() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public PanicException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public PanicException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public PanicException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public PanicException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected PanicException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected PanicException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs b/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs index 2a2b97b23d..f65da50745 100644 --- a/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs +++ b/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs @@ -1,46 +1,53 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// An exception that is thrown if an unattended installation occurs. +/// +[Serializable] +public class UnattendedInstallException : Exception { /// - /// An exception that is thrown if an unattended installation occurs. + /// Initializes a new instance of the class. /// - [Serializable] - public class UnattendedInstallException : Exception + public UnattendedInstallException() { - /// - /// Initializes a new instance of the class. - /// - public UnattendedInstallException() - { - } + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public UnattendedInstallException(string message) : base(message) - { - } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public UnattendedInstallException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception which is the cause of this exception. - /// - /// The message that describes the error. - /// The inner exception, or null. - public UnattendedInstallException(string message, Exception innerException) : base(message, innerException) - { - } + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception which is the cause of this exception. + /// + /// The message that describes the error. + /// The inner exception, or null. + public UnattendedInstallException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected UnattendedInstallException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected UnattendedInstallException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/ExpressionHelper.cs b/src/Umbraco.Core/ExpressionHelper.cs index 1895364d17..79e03d7b93 100644 --- a/src/Umbraco.Core/ExpressionHelper.cs +++ b/src/Umbraco.Core/ExpressionHelper.cs @@ -1,372 +1,441 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Persistence; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// A set of helper methods for dealing with expressions +/// +/// +public static class ExpressionHelper { + private static readonly ConcurrentDictionary PropertyInfoCache = new(); + /// - /// A set of helper methods for dealing with expressions + /// Gets a object from an expression. /// + /// The type of the source. + /// The type of the property. + /// The source. + /// The property lambda. + /// /// - public static class ExpressionHelper - { - private static readonly ConcurrentDictionary PropertyInfoCache = new ConcurrentDictionary(); + public static PropertyInfo GetPropertyInfo( + this TSource source, + Expression> propertyLambda) => GetPropertyInfo(propertyLambda); - /// - /// Gets a object from an expression. - /// - /// The type of the source. - /// The type of the property. - /// The source. - /// The property lambda. - /// - /// - public static PropertyInfo GetPropertyInfo(this TSource source, Expression> propertyLambda) - { - return GetPropertyInfo(propertyLambda); - } - - /// - /// Gets a object from an expression. - /// - /// The type of the source. - /// The type of the property. - /// The property lambda. - /// - /// - public static PropertyInfo GetPropertyInfo(Expression> propertyLambda) - { - return PropertyInfoCache.GetOrAdd( - new LambdaExpressionCacheKey(propertyLambda), - x => - { - var type = typeof(TSource); - - var member = propertyLambda.Body as MemberExpression; - if (member == null) - { - if (propertyLambda.Body.GetType().Name == "UnaryExpression") - { - // The expression might be for some boxing, e.g. representing a value type like HiveId as an object - // in which case the expression will be Convert(x.MyProperty) - var unary = propertyLambda.Body as UnaryExpression; - if (unary != null) - { - var boxedMember = unary.Operand as MemberExpression; - if (boxedMember == null) - throw new ArgumentException("The type of property could not be inferred, try specifying the type parameters explicitly. This can happen if you have tried to access PropertyInfo where the property's return type is a value type, but the expression is trying to convert it to an object"); - else member = boxedMember; - } - } - else throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda)); - } - - - var propInfo = member!.Member as PropertyInfo; - if (propInfo == null) - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a field, not a property.", - propertyLambda)); - - if (type != propInfo.ReflectedType && - !type.IsSubclassOf(propInfo.ReflectedType!)) - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a property that is not from type {1}.", - propertyLambda, - type)); - - return propInfo; - }); - } - - public static (MemberInfo, string?) FindProperty(LambdaExpression lambda) - { - void Throw() + /// + /// Gets a object from an expression. + /// + /// The type of the source. + /// The type of the property. + /// The property lambda. + /// + /// + public static PropertyInfo + GetPropertyInfo(Expression> propertyLambda) => + PropertyInfoCache.GetOrAdd( + new LambdaExpressionCacheKey(propertyLambda), + x => { - throw new ArgumentException($"Expression '{lambda}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", nameof(lambda)); - } + Type type = typeof(TSource); - Expression expr = lambda; - var loop = true; - string? alias = null; - while (loop) - { - switch (expr.NodeType) + var member = propertyLambda.Body as MemberExpression; + if (member == null) { - case ExpressionType.Convert: - expr = ((UnaryExpression) expr).Operand; - break; - case ExpressionType.Lambda: - expr = ((LambdaExpression) expr).Body; - break; - case ExpressionType.Call: - var callExpr = (MethodCallExpression) expr; - var method = callExpr.Method; - if (method.DeclaringType != typeof(SqlExtensionsStatics) || method.Name != "Alias" || !(callExpr.Arguments[1] is ConstantExpression aliasExpr)) - Throw(); - expr = callExpr.Arguments[0]; - alias = aliasExpr.Value?.ToString(); - break; - case ExpressionType.MemberAccess: - var memberExpr = (MemberExpression) expr; - if (memberExpr.Expression?.NodeType != ExpressionType.Parameter && memberExpr.Expression?.NodeType != ExpressionType.Convert) - Throw(); - return (memberExpr.Member, alias); - default: - loop = false; - break; + if (propertyLambda.Body.GetType().Name == "UnaryExpression") + { + // The expression might be for some boxing, e.g. representing a value type like HiveId as an object + // in which case the expression will be Convert(x.MyProperty) + if (propertyLambda.Body is UnaryExpression unary) + { + if (unary.Operand is not MemberExpression boxedMember) + { + throw new ArgumentException( + "The type of property could not be inferred, try specifying the type parameters explicitly. This can happen if you have tried to access PropertyInfo where the property's return type is a value type, but the expression is trying to convert it to an object"); + } + + member = boxedMember; + } + } + else + { + throw new ArgumentException( + string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda)); + } } - } - throw new Exception("Configuration for members is only supported for top-level individual members on a type."); + var propInfo = member!.Member as PropertyInfo; + if (propInfo == null) + { + throw new ArgumentException(string.Format( + "Expression '{0}' refers to a field, not a property.", + propertyLambda)); + } + + if (type != propInfo.ReflectedType && + !type.IsSubclassOf(propInfo.ReflectedType!)) + { + throw new ArgumentException(string.Format( + "Expression '{0}' refers to a property that is not from type {1}.", + propertyLambda, + type)); + } + + return propInfo; + }); + + public static (MemberInfo, string?) FindProperty(LambdaExpression lambda) + { + void Throw() + { + throw new ArgumentException( + $"Expression '{lambda}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", + nameof(lambda)); } - public static IDictionary? GetMethodParams(Expression> fromExpression) + Expression expr = lambda; + var loop = true; + string? alias = null; + while (loop) { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - if (body == null) - return new Dictionary(); - - var rVal = new Dictionary(); - var parameters = body.Method.GetParameters().Select(x => x.Name).Where(x => x is not null).ToArray(); - var i = 0; - foreach (var argument in body.Arguments) - { - var lambda = Expression.Lambda(argument, fromExpression.Parameters); - var d = lambda.Compile(); - var value = d.DynamicInvoke(new object[1]); - rVal.Add(parameters[i]!, value); - i++; - } - return rVal; - } - - /// - /// Gets a from an provided it refers to a method call. - /// - /// - /// From expression. - /// The or null if is null or cannot be converted to . - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) - { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - return body != null ? body.Method : null; - } - - /// - /// Gets the method info. - /// - /// The return type of the method. - /// From expression. - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) - { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - return body != null ? body.Method : null; - } - - /// - /// Gets the method info. - /// - /// The type of the 1. - /// The type of the 2. - /// From expression. - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) - { - if (fromExpression == null) return null; - - MethodCallExpression? me; - switch (fromExpression.Body.NodeType) + switch (expr.NodeType) { case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - var ue = fromExpression.Body as UnaryExpression; - me = ((ue != null) ? ue.Operand : null) as MethodCallExpression; + expr = ((UnaryExpression)expr).Operand; break; + case ExpressionType.Lambda: + expr = ((LambdaExpression)expr).Body; + break; + case ExpressionType.Call: + var callExpr = (MethodCallExpression)expr; + MethodInfo method = callExpr.Method; + if (method.DeclaringType != typeof(SqlExtensionsStatics) || method.Name != "Alias" || + !(callExpr.Arguments[1] is ConstantExpression aliasExpr)) + { + Throw(); + } + + expr = callExpr.Arguments[0]; + alias = aliasExpr.Value?.ToString(); + break; + case ExpressionType.MemberAccess: + var memberExpr = (MemberExpression)expr; + if (memberExpr.Expression?.NodeType != ExpressionType.Parameter && + memberExpr.Expression?.NodeType != ExpressionType.Convert) + { + Throw(); + } + + return (memberExpr.Member, alias); default: - me = fromExpression.Body as MethodCallExpression; + loop = false; break; } - - return me != null ? me.Method : null; } - /// - /// Gets a from an provided it refers to a method call. - /// - /// The expression. - /// The or null if cannot be converted to . - /// - public static MethodInfo? GetMethod(Expression expression) + throw new Exception("Configuration for members is only supported for top-level individual members on a type."); + } + + public static IDictionary? GetMethodParams(Expression> fromExpression) + { + if (fromExpression == null) { - if (expression == null) return null; - return IsMethod(expression) ? (((MethodCallExpression)expression).Method) : null; + return null; } - /// - /// Gets a from an provided it refers to member access. - /// - /// - /// The type of the return. - /// From expression. - /// The or null if cannot be converted to . - /// - public static MemberInfo? GetMemberInfo(Expression> fromExpression) + if (fromExpression.Body is not MethodCallExpression body) { - if (fromExpression == null) return null; - - MemberExpression? me; - switch (fromExpression.Body.NodeType) - { - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - var ue = fromExpression.Body as UnaryExpression; - me = ((ue != null) ? ue.Operand : null) as MemberExpression; - break; - default: - me = fromExpression.Body as MemberExpression; - break; - } - - return me != null ? me.Member : null; + return new Dictionary(); } - /// - /// Determines whether the MethodInfo is the same based on signature, not based on the equality operator or HashCode. - /// - /// The left. - /// The right. - /// - /// true if [is method signature equal to] [the specified left]; otherwise, false. - /// - /// - /// This is useful for comparing Expression methods that may contain different generic types - /// - public static bool IsMethodSignatureEqualTo(this MethodInfo left, MethodInfo right) + var rVal = new Dictionary(); + var parameters = body.Method.GetParameters().Select(x => x.Name).Where(x => x is not null).ToArray(); + var i = 0; + foreach (Expression argument in body.Arguments) + { + LambdaExpression lambda = Expression.Lambda(argument, fromExpression.Parameters); + Delegate d = lambda.Compile(); + var value = d.DynamicInvoke(new object[1]); + rVal.Add(parameters[i]!, value); + i++; + } + + return rVal; + } + + /// + /// Gets a from an provided it refers to a method call. + /// + /// + /// From expression. + /// + /// The or null if is null or cannot be converted to + /// . + /// + /// + public static MethodInfo? GetMethodInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + var body = fromExpression.Body as MethodCallExpression; + return body?.Method; + } + + /// + /// Gets the method info. + /// + /// The return type of the method. + /// From expression. + /// + public static MethodInfo? GetMethodInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + var body = fromExpression.Body as MethodCallExpression; + return body?.Method; + } + + /// + /// Gets the method info. + /// + /// The type of the 1. + /// The type of the 2. + /// From expression. + /// + public static MethodInfo? GetMethodInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + MethodCallExpression? me; + switch (fromExpression.Body.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + var ue = fromExpression.Body as UnaryExpression; + me = ue?.Operand as MethodCallExpression; + break; + default: + me = fromExpression.Body as MethodCallExpression; + break; + } + + return me?.Method; + } + + /// + /// Gets a from an provided it refers to a method call. + /// + /// The expression. + /// + /// The or null if cannot be converted to + /// . + /// + /// + public static MethodInfo? GetMethod(Expression expression) + { + if (expression == null) + { + return null; + } + + return IsMethod(expression) ? ((MethodCallExpression)expression).Method : null; + } + + /// + /// Gets a from an provided it refers to member + /// access. + /// + /// + /// The type of the return. + /// From expression. + /// + /// The or null if cannot be converted to + /// . + /// + /// + public static MemberInfo? GetMemberInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + MemberExpression? me; + switch (fromExpression.Body.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + var ue = fromExpression.Body as UnaryExpression; + me = ue?.Operand as MemberExpression; + break; + default: + me = fromExpression.Body as MemberExpression; + break; + } + + return me?.Member; + } + + /// + /// Determines whether the MethodInfo is the same based on signature, not based on the equality operator or HashCode. + /// + /// The left. + /// The right. + /// + /// true if [is method signature equal to] [the specified left]; otherwise, false. + /// + /// + /// This is useful for comparing Expression methods that may contain different generic types + /// + public static bool IsMethodSignatureEqualTo(this MethodInfo left, MethodInfo right) + { + if (left.Equals(right)) { - if (left.Equals(right)) - return true; - if (left.DeclaringType != right.DeclaringType) - return false; - if (left.Name != right.Name) - return false; - var leftParams = left.GetParameters(); - var rightParams = right.GetParameters(); - if (leftParams.Length != rightParams.Length) - return false; - for (int i = 0; i < leftParams.Length; i++) - { - //if they are delegate parameters, then assume they match as they could be anything - if (typeof(Delegate).IsAssignableFrom(leftParams[i].ParameterType) && typeof(Delegate).IsAssignableFrom(rightParams[i].ParameterType)) - continue; - //if they are not delegates, then compare the types - if (leftParams[i].ParameterType != rightParams[i].ParameterType) - return false; - } - if (left.ReturnType != right.ReturnType) - return false; return true; } - /// - /// Gets a from an provided it refers to member access. - /// - /// The expression. - /// - /// - public static MemberInfo? GetMember(Expression expression) + if (left.DeclaringType != right.DeclaringType) { - if (expression == null) return null; - return IsMember(expression) ? (((MemberExpression)expression).Member) : null; + return false; } - /// - /// Gets a from a - /// - /// From method group. - /// - /// - public static MethodInfo GetStaticMethodInfo(Delegate fromMethodGroup) + if (left.Name != right.Name) { - if (fromMethodGroup == null) throw new ArgumentNullException("fromMethodGroup"); - - - return fromMethodGroup.Method; + return false; } - ///// - ///// Formats an unhandled item for representing the expression as a string. - ///// - ///// - ///// The unhandled item. - ///// - ///// - //public static string FormatUnhandledItem(T unhandledItem) where T : class - //{ - // if (unhandledItem == null) throw new ArgumentNullException("unhandledItem"); - - - // var itemAsExpression = unhandledItem as Expression; - // return itemAsExpression != null - // ? FormattingExpressionTreeVisitor.Format(itemAsExpression) - // : unhandledItem.ToString(); - //} - - /// - /// Determines whether the specified expression is a method. - /// - /// The expression. - /// true if the specified expression is method; otherwise, false. - /// - public static bool IsMethod(Expression expression) + ParameterInfo[] leftParams = left.GetParameters(); + ParameterInfo[] rightParams = right.GetParameters(); + if (leftParams.Length != rightParams.Length) { - return expression is MethodCallExpression; + return false; } - - /// - /// Determines whether the specified expression is a member. - /// - /// The expression. - /// true if the specified expression is member; otherwise, false. - /// - public static bool IsMember(Expression expression) + for (var i = 0; i < leftParams.Length; i++) { - return expression is MemberExpression; + // if they are delegate parameters, then assume they match as they could be anything + if (typeof(Delegate).IsAssignableFrom(leftParams[i].ParameterType) && + typeof(Delegate).IsAssignableFrom(rightParams[i].ParameterType)) + { + continue; + } + + // if they are not delegates, then compare the types + if (leftParams[i].ParameterType != rightParams[i].ParameterType) + { + return false; + } } - /// - /// Determines whether the specified expression is a constant. - /// - /// The expression. - /// true if the specified expression is constant; otherwise, false. - /// - public static bool IsConstant(Expression expression) + if (left.ReturnType != right.ReturnType) { - return expression is ConstantExpression; + return false; } - /// - /// Gets the first value from the supplied arguments of an expression, for those arguments that can be cast to . - /// - /// The arguments. - /// - /// - public static object? GetFirstValueFromArguments(IEnumerable arguments) + return true; + } + + /// + /// Gets a from an provided it refers to member access. + /// + /// The expression. + /// + /// + public static MemberInfo? GetMember(Expression expression) + { + if (expression == null) { - if (arguments == null) return false; - return - arguments.Where(x => x is ConstantExpression).Cast - ().Select(x => x.Value).DefaultIfEmpty(null).FirstOrDefault(); + return null; } + + return IsMember(expression) ? ((MemberExpression)expression).Member : null; + } + + /// + /// Gets a from a + /// + /// From method group. + /// + /// + public static MethodInfo GetStaticMethodInfo(Delegate fromMethodGroup) + { + if (fromMethodGroup == null) + { + throw new ArgumentNullException("fromMethodGroup"); + } + + return fromMethodGroup.Method; + } + + ///// + ///// Formats an unhandled item for representing the expression as a string. + ///// + ///// + ///// The unhandled item. + ///// + ///// + // public static string FormatUnhandledItem(T unhandledItem) where T : class + // { + // if (unhandledItem == null) throw new ArgumentNullException("unhandledItem"); + + // var itemAsExpression = unhandledItem as Expression; + // return itemAsExpression != null + // ? FormattingExpressionTreeVisitor.Format(itemAsExpression) + // : unhandledItem.ToString(); + // } + + /// + /// Determines whether the specified expression is a method. + /// + /// The expression. + /// true if the specified expression is method; otherwise, false. + /// + public static bool IsMethod(Expression expression) => expression is MethodCallExpression; + + /// + /// Determines whether the specified expression is a member. + /// + /// The expression. + /// true if the specified expression is member; otherwise, false. + /// + public static bool IsMember(Expression expression) => expression is MemberExpression; + + /// + /// Determines whether the specified expression is a constant. + /// + /// The expression. + /// true if the specified expression is constant; otherwise, false. + /// + public static bool IsConstant(Expression expression) => expression is ConstantExpression; + + /// + /// Gets the first value from the supplied arguments of an expression, for those arguments that can be cast to + /// . + /// + /// The arguments. + /// + /// + public static object? GetFirstValueFromArguments(IEnumerable arguments) + { + if (arguments == null) + { + return false; + } + + return + arguments.Where(x => x is ConstantExpression).Cast + ().Select(x => x.Value).DefaultIfEmpty(null).FirstOrDefault(); } } diff --git a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs index aea0f847ab..45ae9ceafe 100644 --- a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs +++ b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs @@ -1,106 +1,107 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; using System.Reflection; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class AssemblyExtensions { - public static class AssemblyExtensions + private static string _rootDir = string.Empty; + + /// + /// Utility method that returns the path to the root of the application, by getting the path to where the assembly + /// where this + /// method is included is present, then traversing until it's past the /bin directory. Ie. this makes it work + /// even if the assembly is in a /bin/debug or /bin/release folder + /// + /// + public static string GetRootDirectorySafe(this Assembly executingAssembly) { - private static string _rootDir = ""; - - /// - /// Utility method that returns the path to the root of the application, by getting the path to where the assembly where this - /// method is included is present, then traversing until it's past the /bin directory. Ie. this makes it work - /// even if the assembly is in a /bin/debug or /bin/release folder - /// - /// - public static string GetRootDirectorySafe(this Assembly executingAssembly) + if (string.IsNullOrEmpty(_rootDir) == false) { - if (string.IsNullOrEmpty(_rootDir) == false) - { - return _rootDir; - } - var codeBase = executingAssembly.Location; - var uri = new Uri(codeBase); - var path = uri.LocalPath; - var baseDirectory = Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(baseDirectory)) - throw new Exception("No root directory could be resolved. Please ensure that your Umbraco solution is correctly configured."); - - _rootDir = baseDirectory.Contains("bin") - ? baseDirectory.Substring(0, baseDirectory.LastIndexOf("bin", StringComparison.OrdinalIgnoreCase) - 1) - : baseDirectory; - return _rootDir; } - /// - /// Returns the file used to load the assembly - /// - /// - /// - public static FileInfo GetAssemblyFile(this Assembly assembly) + var codeBase = executingAssembly.Location; + var uri = new Uri(codeBase); + var path = uri.LocalPath; + var baseDirectory = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(baseDirectory)) + { + throw new Exception( + "No root directory could be resolved. Please ensure that your Umbraco solution is correctly configured."); + } + + _rootDir = baseDirectory.Contains("bin") + ? baseDirectory[..(baseDirectory.LastIndexOf("bin", StringComparison.OrdinalIgnoreCase) - 1)] + : baseDirectory; + + return _rootDir; + } + + /// + /// Returns the file used to load the assembly + /// + /// + /// + public static FileInfo GetAssemblyFile(this Assembly assembly) + { + var codeBase = assembly.Location; + var uri = new Uri(codeBase); + var path = uri.LocalPath; + return new FileInfo(path); + } + + /// + /// Returns true if the assembly is the App_Code assembly + /// + /// + /// + public static bool IsAppCodeAssembly(this Assembly assembly) + { + if (assembly.FullName!.StartsWith("App_Code")) + { + try + { + Assembly.Load("App_Code"); + return true; + } + catch (FileNotFoundException) + { + // this will occur if it cannot load the assembly + return false; + } + } + + return false; + } + + /// + /// Returns true if the assembly is the compiled global asax. + /// + /// + /// + public static bool IsGlobalAsaxAssembly(this Assembly assembly) => + + // only way I can figure out how to test is by the name + assembly.FullName!.StartsWith("App_global.asax"); + + /// + /// Returns the file used to load the assembly + /// + /// + /// + public static FileInfo? GetAssemblyFile(this AssemblyName assemblyName) + { + var codeBase = assemblyName.CodeBase; + if (!string.IsNullOrEmpty(codeBase)) { - var codeBase = assembly.Location; var uri = new Uri(codeBase); var path = uri.LocalPath; return new FileInfo(path); } - /// - /// Returns true if the assembly is the App_Code assembly - /// - /// - /// - public static bool IsAppCodeAssembly(this Assembly assembly) - { - if (assembly.FullName!.StartsWith("App_Code")) - { - try - { - Assembly.Load("App_Code"); - return true; - } - catch (FileNotFoundException) - { - //this will occur if it cannot load the assembly - return false; - } - } - return false; - } - - /// - /// Returns true if the assembly is the compiled global asax. - /// - /// - /// - public static bool IsGlobalAsaxAssembly(this Assembly assembly) - { - //only way I can figure out how to test is by the name - return assembly.FullName!.StartsWith("App_global.asax"); - } - - /// - /// Returns the file used to load the assembly - /// - /// - /// - public static FileInfo? GetAssemblyFile(this AssemblyName assemblyName) - { - var codeBase = assemblyName.CodeBase; - if (!string.IsNullOrEmpty(codeBase)) - { - var uri = new Uri(codeBase); - var path = uri.LocalPath; - return new FileInfo(path); - } - - return null; - } - + return null; } } diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index e3d6f7f4fd..a604b3e017 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -1,378 +1,395 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Security.Claims; using System.Security.Principal; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ClaimsIdentityExtensions { - public static class ClaimsIdentityExtensions + /// + /// Returns the required claim types for a back office identity + /// + /// + /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be + /// empty + /// + public static IEnumerable RequiredBackOfficeClaimTypes => new[] { - public static T? GetUserId(this IIdentity identity) + ClaimTypes.NameIdentifier, // id + ClaimTypes.Name, // username + ClaimTypes.GivenName, + + // Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null... + // Constants.Security.StartMediaNodeIdClaimType, + ClaimTypes.Locality, Constants.Security.SecurityStampClaimType, + }; + + public static T? GetUserId(this IIdentity identity) + { + var strId = identity.GetUserId(); + Attempt converted = strId.TryConvertTo(); + return converted.Result ?? default; + } + + /// + /// Returns the user id from the of either the claim type + /// or "sub" + /// + /// + /// + /// The string value of the user id if found otherwise null + /// + public static string? GetUserId(this IIdentity identity) + { + if (identity == null) { - var strId = identity.GetUserId(); - var converted = strId.TryConvertTo(); - return converted.Result ?? default; + throw new ArgumentNullException(nameof(identity)); } - /// - /// Returns the user id from the of either the claim type or "sub" - /// - /// - /// - /// The string value of the user id if found otherwise null - /// - public static string? GetUserId(this IIdentity identity) + string? userId = null; + if (identity is ClaimsIdentity claimsIdentity) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); - - string? userId = null; - if (identity is ClaimsIdentity claimsIdentity) - { - userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) - ?? claimsIdentity.FindFirstValue("sub"); - } - - return userId; + userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) + ?? claimsIdentity.FindFirstValue("sub"); } - /// - /// Returns the user name from the of either the claim type or "preferred_username" - /// - /// - /// - /// The string value of the user name if found otherwise null - /// - public static string? GetUserName(this IIdentity identity) + return userId; + } + + /// + /// Returns the user name from the of either the claim type or + /// "preferred_username" + /// + /// + /// + /// The string value of the user name if found otherwise null + /// + public static string? GetUserName(this IIdentity identity) + { + if (identity == null) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); - - string? username = null; - if (identity is ClaimsIdentity claimsIdentity) - { - username = claimsIdentity.FindFirstValue(ClaimTypes.Name) - ?? claimsIdentity.FindFirstValue("preferred_username"); - } - - return username; + throw new ArgumentNullException(nameof(identity)); } - public static string? GetEmail(this IIdentity identity) + string? username = null; + if (identity is ClaimsIdentity claimsIdentity) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); - - string? email = null; - if (identity is ClaimsIdentity claimsIdentity) - { - email = claimsIdentity.FindFirstValue(ClaimTypes.Email); - } - - return email; + username = claimsIdentity.FindFirstValue(ClaimTypes.Name) + ?? claimsIdentity.FindFirstValue("preferred_username"); } - /// - /// Returns the first claim value found in the for the given claimType - /// - /// - /// - /// - /// The string value of the claim if found otherwise null - /// - public static string? FindFirstValue(this ClaimsIdentity identity, string claimType) - { - if (identity == null) throw new ArgumentNullException(nameof(identity)); + return username; + } - return identity.FindFirst(claimType)?.Value; + public static string? GetEmail(this IIdentity identity) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); } - /// - /// Returns the required claim types for a back office identity - /// - /// - /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be empty - /// - public static IEnumerable RequiredBackOfficeClaimTypes => new[] + string? email = null; + if (identity is ClaimsIdentity claimsIdentity) { - ClaimTypes.NameIdentifier, //id - ClaimTypes.Name, //username - ClaimTypes.GivenName, - // Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null... - // Constants.Security.StartMediaNodeIdClaimType, - ClaimTypes.Locality, - Constants.Security.SecurityStampClaimType - }; + email = claimsIdentity.FindFirstValue(ClaimTypes.Email); + } - /// - /// Verify that a ClaimsIdentity has all the required claim types - /// - /// - /// Verified identity wrapped in a ClaimsIdentity with BackOfficeAuthentication type - /// True if ClaimsIdentity - public static bool VerifyBackOfficeIdentity(this ClaimsIdentity identity, [MaybeNullWhen(false)] out ClaimsIdentity verifiedIdentity) + return email; + } + + /// + /// Returns the first claim value found in the for the given claimType + /// + /// + /// + /// + /// The string value of the claim if found otherwise null + /// + public static string? FindFirstValue(this ClaimsIdentity identity, string claimType) + { + if (identity == null) { - if (identity is null) + throw new ArgumentNullException(nameof(identity)); + } + + return identity.FindFirst(claimType)?.Value; + } + + /// + /// Verify that a ClaimsIdentity has all the required claim types + /// + /// + /// Verified identity wrapped in a ClaimsIdentity with BackOfficeAuthentication type + /// True if ClaimsIdentity + public static bool VerifyBackOfficeIdentity( + this ClaimsIdentity identity, + [MaybeNullWhen(false)] out ClaimsIdentity verifiedIdentity) + { + if (identity is null) + { + verifiedIdentity = null; + return false; + } + + // Validate that all required claims exist + foreach (var claimType in RequiredBackOfficeClaimTypes) + { + if (identity.HasClaim(x => x.Type == claimType) == false || + identity.HasClaim(x => x.Type == claimType && x.Value.IsNullOrWhiteSpace())) { verifiedIdentity = null; return false; } - - // Validate that all required claims exist - foreach (var claimType in RequiredBackOfficeClaimTypes) - { - if (identity.HasClaim(x => x.Type == claimType) == false || - identity.HasClaim(x => x.Type == claimType && x.Value.IsNullOrWhiteSpace())) - { - verifiedIdentity = null; - return false; - } - } - - verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType); - return true; } - /// - /// Add the required claims to be a BackOffice ClaimsIdentity - /// - /// this - /// The users Id - /// Username - /// Real name - /// Start content nodes - /// Start media nodes - /// The locality of the user - /// Security stamp - /// Allowed apps - /// Roles - public static void AddRequiredClaims(this ClaimsIdentity identity, string userId, string username, - string realName, IEnumerable? startContentNodes, IEnumerable? startMediaNodes, string culture, - string securityStamp, IEnumerable allowedApps, IEnumerable roles) + verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType + ? identity + : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType); + return true; + } + + /// + /// Add the required claims to be a BackOffice ClaimsIdentity + /// + /// this + /// The users Id + /// Username + /// Real name + /// Start content nodes + /// Start media nodes + /// The locality of the user + /// Security stamp + /// Allowed apps + /// Roles + public static void AddRequiredClaims(this ClaimsIdentity identity, string userId, string username, string realName, IEnumerable? startContentNodes, IEnumerable? startMediaNodes, string culture, string securityStamp, IEnumerable allowedApps, IEnumerable roles) + { + // This is the id that 'identity' uses to check for the user id + if (identity.HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) { - //This is the id that 'identity' uses to check for the user id - if (identity.HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) + identity.AddClaim(new Claim( + ClaimTypes.NameIdentifier, + userId, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + if (identity.HasClaim(x => x.Type == ClaimTypes.Name) == false) + { + identity.AddClaim(new Claim( + ClaimTypes.Name, + username, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + if (identity.HasClaim(x => x.Type == ClaimTypes.GivenName) == false) + { + identity.AddClaim(new Claim( + ClaimTypes.GivenName, + realName, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && + startContentNodes != null) + { + foreach (var startContentNode in startContentNodes) { identity.AddClaim(new Claim( - ClaimTypes.NameIdentifier, - userId, - ClaimValueTypes.String, + Constants.Security.StartContentNodeIdClaimType, + startContentNode.ToInvariantString(), + ClaimValueTypes.Integer32, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } - - if (identity.HasClaim(x => x.Type == ClaimTypes.Name) == false) - { - identity.AddClaim(new Claim( - ClaimTypes.Name, - username, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - if (identity.HasClaim(x => x.Type == ClaimTypes.GivenName) == false) - { - identity.AddClaim(new Claim( - ClaimTypes.GivenName, - realName, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && - startContentNodes != null) - { - foreach (var startContentNode in startContentNodes) - { - identity.AddClaim(new Claim( - Constants.Security.StartContentNodeIdClaimType, - startContentNode.ToInvariantString(), - ClaimValueTypes.Integer32, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } - - if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && - startMediaNodes != null) - { - foreach (var startMediaNode in startMediaNodes) - { - identity.AddClaim(new Claim( - Constants.Security.StartMediaNodeIdClaimType, - startMediaNode.ToInvariantString(), - ClaimValueTypes.Integer32, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } - - if (identity.HasClaim(x => x.Type == ClaimTypes.Locality) == false) - { - identity.AddClaim(new Claim( - ClaimTypes.Locality, - culture, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - // The security stamp claim is also required - if (identity.HasClaim(x => x.Type == Constants.Security.SecurityStampClaimType) == false) - { - identity.AddClaim(new Claim( - Constants.Security.SecurityStampClaimType, - securityStamp, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - // Add each app as a separate claim - if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && - allowedApps != null) - { - foreach (var application in allowedApps) - { - identity.AddClaim(new Claim( - Constants.Security.AllowedApplicationsClaimType, - application, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } - - // Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might - // not be made with that factory if it was created with a different ticket so perform the check - if (identity.HasClaim(x => x.Type == ClaimsIdentity.DefaultRoleClaimType) == false && roles != null) - { - // Manually add them - foreach (var roleName in roles) - { - identity.AddClaim(new Claim( - identity.RoleClaimType, - roleName, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } } - /// - /// Get the start content nodes from a ClaimsIdentity - /// - /// - /// Array of start content nodes - public static int[] GetStartContentNodes(this ClaimsIdentity identity) => - identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType) - .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : default) - .Where(x => x != default).ToArray(); - - /// - /// Get the start media nodes from a ClaimsIdentity - /// - /// - /// Array of start media nodes - public static int[] GetStartMediaNodes(this ClaimsIdentity identity) => - identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) - .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : default) - .Where(x => x != default).ToArray(); - - /// - /// Get the allowed applications from a ClaimsIdentity - /// - /// - /// - public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity - .FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray(); - - /// - /// Get the user ID from a ClaimsIdentity - /// - /// - /// User ID as integer - public static int? GetId(this ClaimsIdentity identity) + if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && + startMediaNodes != null) { - var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier); - if (firstValue is not null) + foreach (var startMediaNode in startMediaNodes) { - return int.Parse(firstValue, CultureInfo.InvariantCulture); + identity.AddClaim(new Claim( + Constants.Security.StartMediaNodeIdClaimType, + startMediaNode.ToInvariantString(), + ClaimValueTypes.Integer32, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); } - - return null; } - /// - /// Get the real name belonging to the user from a ClaimsIdentity - /// - /// - /// Real name of the user - public static string? GetRealName(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.GivenName); - - /// - /// Get the username of the user from a ClaimsIdentity - /// - /// - /// Username of the user - public static string? GetUsername(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Name); - - /// - /// Get the culture string from a ClaimsIdentity - /// - /// - /// Culture string - public static string? GetCultureString(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Locality); - - /// - /// Get the security stamp from a ClaimsIdentity - /// - /// - /// Security stamp - public static string? GetSecurityStamp(this ClaimsIdentity identity) => identity.FindFirstValue(Constants.Security.SecurityStampClaimType); - - /// - /// Get the roles assigned to a user from a ClaimsIdentity - /// - /// - /// Array of roles - public static string[] GetRoles(this ClaimsIdentity identity) => identity - .FindAll(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(role => role.Value).ToArray(); - - - /// - /// Adds or updates and existing claim. - /// - public static void AddOrUpdateClaim(this ClaimsIdentity identity, Claim? claim) + if (identity.HasClaim(x => x.Type == ClaimTypes.Locality) == false) { - if (identity == null) + identity.AddClaim(new Claim( + ClaimTypes.Locality, + culture, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + // The security stamp claim is also required + if (identity.HasClaim(x => x.Type == Constants.Security.SecurityStampClaimType) == false) + { + identity.AddClaim(new Claim( + Constants.Security.SecurityStampClaimType, + securityStamp, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + // Add each app as a separate claim + if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null) + { + foreach (var application in allowedApps) { - throw new ArgumentNullException(nameof(identity)); + identity.AddClaim(new Claim( + Constants.Security.AllowedApplicationsClaimType, + application, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); } + } - if (claim is not null) + // Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might + // not be made with that factory if it was created with a different ticket so perform the check + if (identity.HasClaim(x => x.Type == ClaimsIdentity.DefaultRoleClaimType) == false && roles != null) + { + // Manually add them + foreach (var roleName in roles) { - Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type); - identity.TryRemoveClaim(existingClaim); - - identity.AddClaim(claim); + identity.AddClaim(new Claim( + identity.RoleClaimType, + roleName, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); } } } + + /// + /// Get the start content nodes from a ClaimsIdentity + /// + /// + /// Array of start content nodes + public static int[] GetStartContentNodes(this ClaimsIdentity identity) => + identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType) + .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? i + : default) + .Where(x => x != default).ToArray(); + + /// + /// Get the start media nodes from a ClaimsIdentity + /// + /// + /// Array of start media nodes + public static int[] GetStartMediaNodes(this ClaimsIdentity identity) => + identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) + .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? i + : default) + .Where(x => x != default).ToArray(); + + /// + /// Get the allowed applications from a ClaimsIdentity + /// + /// + /// + public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity + .FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray(); + + /// + /// Get the user ID from a ClaimsIdentity + /// + /// + /// User ID as integer + public static int? GetId(this ClaimsIdentity identity) + { + var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier); + if (firstValue is not null) + { + return int.Parse(firstValue, CultureInfo.InvariantCulture); + } + + return null; + } + + /// + /// Get the real name belonging to the user from a ClaimsIdentity + /// + /// + /// Real name of the user + public static string? GetRealName(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.GivenName); + + /// + /// Get the username of the user from a ClaimsIdentity + /// + /// + /// Username of the user + public static string? GetUsername(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Name); + + /// + /// Get the culture string from a ClaimsIdentity + /// + /// + /// Culture string + public static string? GetCultureString(this ClaimsIdentity identity) => + identity.FindFirstValue(ClaimTypes.Locality); + + /// + /// Get the security stamp from a ClaimsIdentity + /// + /// + /// Security stamp + public static string? GetSecurityStamp(this ClaimsIdentity identity) => + identity.FindFirstValue(Constants.Security.SecurityStampClaimType); + + /// + /// Get the roles assigned to a user from a ClaimsIdentity + /// + /// + /// Array of roles + public static string[] GetRoles(this ClaimsIdentity identity) => identity + .FindAll(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(role => role.Value).ToArray(); + + /// + /// Adds or updates and existing claim. + /// + public static void AddOrUpdateClaim(this ClaimsIdentity identity, Claim? claim) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (claim is not null) + { + Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type); + identity.TryRemoveClaim(existingClaim); + + identity.AddClaim(claim); + } + } } diff --git a/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs index d719876d9f..2003079736 100644 --- a/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs +++ b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs @@ -1,10 +1,11 @@ using Microsoft.Extensions.Configuration; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Extensions; /// -/// Extension methods for configuration. +/// Extensions for . /// public static class ConfigurationExtensions { @@ -74,7 +75,11 @@ public static class ConfigurationExtensions if (!string.IsNullOrEmpty(connectionString)) { // Replace data directory - connectionString = ReplaceDataDirectoryPlaceholder(connectionString); + string? dataDirectory = AppDomain.CurrentDomain.GetData(DataDirectoryName)?.ToString(); + if (!string.IsNullOrEmpty(dataDirectory)) + { + connectionString = connectionString.Replace(DataDirectoryPlaceholder, dataDirectory); + } // Get provider name providerName = configuration.GetConnectionStringProviderName(name); @@ -87,17 +92,13 @@ public static class ConfigurationExtensions return connectionString; } - internal static string? ReplaceDataDirectoryPlaceholder(string? connectionString) - { - if (!string.IsNullOrEmpty(connectionString)) - { - string? dataDirectory = AppDomain.CurrentDomain.GetData(DataDirectoryName)?.ToString(); - if (!string.IsNullOrEmpty(dataDirectory)) - { - return connectionString.Replace(DataDirectoryPlaceholder, dataDirectory); - } - } - - return connectionString; - } + /// + /// Gets the Umbraco runtime mode. + /// + /// The configuration. + /// + /// The Umbraco runtime mode. + /// + public static RuntimeMode GetRuntimeMode(this IConfiguration configuration) + => configuration.GetValue(Constants.Configuration.ConfigRuntimeMode); } diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index 0bd1e36d9e..df0e58d878 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -1,11 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Xml.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; @@ -15,395 +11,399 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ContentExtensions { - public static class ContentExtensions + /// + /// Returns the path to a media item stored in a property if the property editor is + /// + /// + /// + /// + /// + /// + /// + /// True if the file path can be resolved and the property is + public static bool TryGetMediaPath( + this IContentBase content, + string propertyTypeAlias, + MediaUrlGeneratorCollection mediaUrlGenerators, + out string? mediaFilePath, + string? culture = null, + string? segment = null) { - /// - /// Returns the path to a media item stored in a property if the property editor is - /// - /// - /// - /// - /// - /// - /// - /// True if the file path can be resolved and the property is - public static bool TryGetMediaPath( - this IContentBase content, - string propertyTypeAlias, - MediaUrlGeneratorCollection mediaUrlGenerators, - out string? mediaFilePath, - string? culture = null, - string? segment = null) + if (!content.Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) { - if (!content.Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) - { - mediaFilePath = null; - return false; - } - - if (!mediaUrlGenerators.TryGetMediaPath( - property?.PropertyType?.PropertyEditorAlias, - property?.GetValue(culture, segment), - out mediaFilePath)) - { - return false; - } - - return true; - } - - public static bool IsAnyUserPropertyDirty(this IContentBase entity) - { - return entity.Properties.Any(x => x.IsDirty()); - } - - public static bool WasAnyUserPropertyDirty(this IContentBase entity) - { - return entity.Properties.Any(x => x.WasDirty()); - } - - - public static bool IsMoving(this IContentBase entity) - { - // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below - // operations which will make this whole operation go much faster. When moving we don't need to create - // new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.IsPropertyDirty(nameof(entity.Path)) - && entity.IsPropertyDirty(nameof(entity.Level)) - && entity.IsPropertyDirty(nameof(entity.UpdateDate)); - - return isMoving; - } - - - /// - /// Removes characters that are not valid XML characters from all entity properties - /// of type string. See: http://stackoverflow.com/a/961504/5018 - /// - /// - /// - /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. - /// - /// - public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) - { - entity.Name = entity.Name?.ToValidXmlString(); - foreach (var property in entity.Properties) - { - foreach (var propertyValue in property.Values) - { - if (propertyValue.EditedValue is string editString) - propertyValue.EditedValue = editString.ToValidXmlString(); - if (propertyValue.PublishedValue is string publishedString) - propertyValue.PublishedValue = publishedString.ToValidXmlString(); - } - } - } - - /// - /// Checks if the IContentBase has children - /// - /// - /// - /// - /// - /// This is a bit of a hack because we need to type check! - /// - internal static bool? HasChildren(IContentBase content, ServiceContext services) - { - if (content is IContent) - { - return services.ContentService?.HasChildren(content.Id); - } - if (content is IMedia) - { - return services.MediaService?.HasChildren(content.Id); - } + mediaFilePath = null; return false; } - - /// - /// Returns all properties based on the editorAlias - /// - /// - /// - /// - public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) - => content.Properties.Where(x => x.PropertyType?.PropertyEditorAlias == editorAlias); - - - #region IContent - - /// - /// Gets the current status of the Content - /// - public static ContentStatus GetStatus(this IContent content, ContentScheduleCollection contentSchedule, string? culture = null) + if (!mediaUrlGenerators.TryGetMediaPath( + property?.PropertyType?.PropertyEditorAlias, + property?.GetValue(culture, segment), + out mediaFilePath)) { - if (content.Trashed) - return ContentStatus.Trashed; - - if (!content.ContentType.VariesByCulture()) - culture = string.Empty; - else if (culture.IsNullOrWhiteSpace()) - throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); - - var expires = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Expire); - if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) - return ContentStatus.Expired; - - var release = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Release); - if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) - return ContentStatus.AwaitingRelease; - - if (content.Published) - return ContentStatus.Published; - - return ContentStatus.Unpublished; + return false; } - /// - /// Gets a collection containing the ids of all ancestors. - /// - /// to retrieve ancestors for - /// An Enumerable list of integer ids - public static IEnumerable? GetAncestorIds(this IContent content) => - content.Path?.Split(Constants.CharArrays.Comma) - .Where(x => x != Constants.System.RootString && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(s => - int.Parse(s, CultureInfo.InvariantCulture)); + return true; + } - #endregion + public static bool IsAnyUserPropertyDirty(this IContentBase entity) => entity.Properties.Any(x => x.IsDirty()); + public static bool WasAnyUserPropertyDirty(this IContentBase entity) => entity.Properties.Any(x => x.WasDirty()); - /// - /// Gets the for the Creator of this content item. - /// - public static IProfile? GetCreatorProfile(this IContentBase content, IUserService userService) + public static bool IsMoving(this IContentBase entity) + { + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below + // operations which will make this whole operation go much faster. When moving we don't need to create + // new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsPropertyDirty(nameof(entity.Path)) + && entity.IsPropertyDirty(nameof(entity.Level)) + && entity.IsPropertyDirty(nameof(entity.UpdateDate)); + + return isMoving; + } + + /// + /// Removes characters that are not valid XML characters from all entity properties + /// of type string. See: http://stackoverflow.com/a/961504/5018 + /// + /// + /// + /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. + /// + /// + public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) + { + entity.Name = entity.Name?.ToValidXmlString(); + foreach (IProperty property in entity.Properties) { - return userService.GetProfileById(content.CreatorId); - } - /// - /// Gets the for the Writer of this content. - /// - public static IProfile? GetWriterProfile(this IContent content, IUserService userService) - { - return userService.GetProfileById(content.WriterId); - } - - /// - /// Gets the for the Writer of this content. - /// - public static IProfile? GetWriterProfile(this IMedia content, IUserService userService) - { - return userService.GetProfileById(content.WriterId); - } - - - #region User/Profile methods - - /// - /// Gets the for the Creator of this media item. - /// - public static IProfile? GetCreatorProfile(this IMedia media, IUserService userService) - { - return userService.GetProfileById(media.CreatorId); - } - - - #endregion - - - /// - /// Returns properties that do not belong to a group - /// - /// - /// - public static IEnumerable GetNonGroupedProperties(this IContentBase content) - { - return content.Properties - .Where(x => x.PropertyType?.PropertyGroupId == null) - .OrderBy(x => x.PropertyType?.SortOrder); - } - - /// - /// Returns the Property object for the given property group - /// - /// - /// - /// - public static IEnumerable GetPropertiesForGroup(this IContentBase content, PropertyGroup propertyGroup) - { - //get the properties for the current tab - return content.Properties - .Where(property => propertyGroup.PropertyTypes is not null && propertyGroup.PropertyTypes - .Select(propertyType => propertyType.Id) - .Contains(property.PropertyTypeId)); - } - - - #region SetValue for setting file contents - - /// - /// Sets the posted file value of a property. - /// - public static void SetValue( - this IContentBase content, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - string propertyTypeAlias, - string filename, - Stream filestream, - string? culture = null, - string? segment = null) - { - if (filename == null || filestream == null) - return; - - filename = shortStringHelper.CleanStringForSafeFileName(filename); - if (string.IsNullOrWhiteSpace(filename)) - return; - filename = filename.ToLower(); - - SetUploadFile(content, mediaFileManager, mediaUrlGenerators, contentTypeBaseServiceProvider, propertyTypeAlias, filename, filestream, culture, segment); - } - - private static void SetUploadFile( - this IContentBase content, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - string propertyTypeAlias, - string filename, - Stream filestream, - string? culture = null, - string? segment = null) - { - var property = GetProperty(content, contentTypeBaseServiceProvider, propertyTypeAlias); - - // Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an - // existing IMedia with extension SetValue causes exception 'Illegal characters in path' - string? oldpath = null; - - if (content.TryGetMediaPath(property.Alias, mediaUrlGenerators, out string? mediaFilePath, culture, segment)) + foreach (IPropertyValue propertyValue in property.Values) { - oldpath = mediaFileManager.FileSystem.GetRelativePath(mediaFilePath!); + if (propertyValue.EditedValue is string editString) + { + propertyValue.EditedValue = editString.ToValidXmlString(); + } + + if (propertyValue.PublishedValue is string publishedString) + { + propertyValue.PublishedValue = publishedString.ToValidXmlString(); + } } + } + } - var filepath = mediaFileManager.StoreFile(content, property.PropertyType, filename, filestream, oldpath); + /// + /// Returns all properties based on the editorAlias + /// + /// + /// + /// + public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) + => content.Properties.Where(x => x.PropertyType?.PropertyEditorAlias == editorAlias); - // NOTE: Here we are just setting the value to a string which means that any file based editor - // will need to handle the raw string value and save it to it's correct (i.e. JSON) - // format. I'm unsure how this works today with image cropper but it does (maybe events?) - property.SetValue(mediaFileManager.FileSystem.GetUrl(filepath), culture, segment); + /// + /// Checks if the IContentBase has children + /// + /// + /// + /// + /// + /// This is a bit of a hack because we need to type check! + /// + internal static bool? HasChildren(IContentBase content, ServiceContext services) + { + if (content is IContent) + { + return services.ContentService?.HasChildren(content.Id); } - // gets or creates a property for a content item. - private static IProperty GetProperty(IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias) + if (content is IMedia) { - var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - if (property != null) - return property; + return services.MediaService?.HasChildren(content.Id); + } - var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); - var propertyType = contentType?.CompositionPropertyTypes - .FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); - if (propertyType == null) - throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); + return false; + } - property = new Property(propertyType); - content.Properties.Add(property); + /// + /// Gets the for the Creator of this content item. + /// + public static IProfile? GetCreatorProfile(this IContentBase content, IUserService userService) => + userService.GetProfileById(content.CreatorId); + + /// + /// Gets the for the Writer of this content. + /// + public static IProfile? GetWriterProfile(this IContent content, IUserService userService) => + userService.GetProfileById(content.WriterId); + + /// + /// Gets the for the Writer of this content. + /// + public static IProfile? GetWriterProfile(this IMedia content, IUserService userService) => + userService.GetProfileById(content.WriterId); + + #region User/Profile methods + + /// + /// Gets the for the Creator of this media item. + /// + public static IProfile? GetCreatorProfile(this IMedia media, IUserService userService) => + userService.GetProfileById(media.CreatorId); + + #endregion + + /// + /// Returns properties that do not belong to a group + /// + /// + /// + public static IEnumerable GetNonGroupedProperties(this IContentBase content) => + content.Properties + .Where(x => x.PropertyType?.PropertyGroupId == null) + .OrderBy(x => x.PropertyType?.SortOrder); + + /// + /// Returns the Property object for the given property group + /// + /// + /// + /// + public static IEnumerable + GetPropertiesForGroup(this IContentBase content, PropertyGroup propertyGroup) => + + // get the properties for the current tab + content.Properties + .Where(property => propertyGroup.PropertyTypes is not null && propertyGroup.PropertyTypes + .Select(propertyType => propertyType.Id) + .Contains(property.PropertyTypeId)); + + #region Dirty + + public static IEnumerable GetDirtyUserProperties(this IContentBase entity) => + entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); + + #endregion + + /// + /// Creates the full xml representation for the object and all of it's descendants + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) => + serializer.Serialize(content, false, true); + + /// + /// Creates the xml representation for the object + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToXml(this IContent content, IEntityXmlSerializer serializer) => + serializer.Serialize(content, false); + + /// + /// Creates the xml representation for the object + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToXml(this IMedia media, IEntityXmlSerializer serializer) => serializer.Serialize(media); + + /// + /// Creates the xml representation for the object + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToXml(this IMember member, IEntityXmlSerializer serializer) => serializer.Serialize(member); + + #region IContent + + /// + /// Gets the current status of the Content + /// + public static ContentStatus GetStatus(this IContent content, ContentScheduleCollection contentSchedule, string? culture = null) + { + if (content.Trashed) + { + return ContentStatus.Trashed; + } + + if (!content.ContentType.VariesByCulture()) + { + culture = string.Empty; + } + else if (culture.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); + } + + IEnumerable expires = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Expire); + if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) + { + return ContentStatus.Expired; + } + + IEnumerable release = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Release); + if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) + { + return ContentStatus.AwaitingRelease; + } + + if (content.Published) + { + return ContentStatus.Published; + } + + return ContentStatus.Unpublished; + } + + /// + /// Gets a collection containing the ids of all ancestors. + /// + /// to retrieve ancestors for + /// An Enumerable list of integer ids + public static IEnumerable? GetAncestorIds(this IContent content) => + content.Path?.Split(Constants.CharArrays.Comma) + .Where(x => x != Constants.System.RootString && x != content.Id.ToString(CultureInfo.InvariantCulture)) + .Select(s => + int.Parse(s, CultureInfo.InvariantCulture)); + + #endregion + + #region SetValue for setting file contents + + /// + /// Sets the posted file value of a property. + /// + public static void SetValue( + this IContentBase content, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string? culture = null, + string? segment = null) + { + if (filename == null || filestream == null) + { + return; + } + + filename = shortStringHelper.CleanStringForSafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) + { + return; + } + + filename = filename.ToLower(); + + SetUploadFile(content, mediaFileManager, mediaUrlGenerators, contentTypeBaseServiceProvider, propertyTypeAlias, filename, filestream, culture, segment); + } + + /// + /// Stores a file. + /// + /// A content item. + /// The property alias. + /// The name of the file. + /// A stream containing the file data. + /// The original file path, if any. + /// The path to the file, relative to the media filesystem. + /// + /// + /// Does NOT set the property value, so one should probably store the file and then do + /// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath). + /// + /// + /// The original file path is used, in the old media file path scheme, to try and reuse + /// the "folder number" that was assigned to the previous file referenced by the property, + /// if any. + /// + /// + public static string StoreFile( + this IContentBase content, + MediaFileManager mediaFileManager, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string filepath) + { + IContentTypeComposition? contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); + IPropertyType? propertyType = contentType? + .CompositionPropertyTypes.FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); + if (propertyType == null) + { + throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); + } + + return mediaFileManager.StoreFile(content, propertyType, filename, filestream, filepath); + } + + private static void SetUploadFile( + this IContentBase content, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string? culture = null, + string? segment = null) + { + IProperty property = GetProperty(content, contentTypeBaseServiceProvider, propertyTypeAlias); + + // Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an + // existing IMedia with extension SetValue causes exception 'Illegal characters in path' + string? oldpath = null; + + if (content.TryGetMediaPath(property.Alias, mediaUrlGenerators, out var mediaFilePath, culture, segment)) + { + oldpath = mediaFileManager.FileSystem.GetRelativePath(mediaFilePath!); + } + + var filepath = mediaFileManager.StoreFile(content, property.PropertyType, filename, filestream, oldpath); + + // NOTE: Here we are just setting the value to a string which means that any file based editor + // will need to handle the raw string value and save it to it's correct (i.e. JSON) + // format. I'm unsure how this works today with image cropper but it does (maybe events?) + property.SetValue(mediaFileManager.FileSystem.GetUrl(filepath), culture, segment); + } + + // gets or creates a property for a content item. + private static IProperty GetProperty( + IContentBase content, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias) + { + IProperty? property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (property != null) + { return property; } - /// - /// Stores a file. - /// - /// A content item. - /// The property alias. - /// The name of the file. - /// A stream containing the file data. - /// The original file path, if any. - /// The path to the file, relative to the media filesystem. - /// - /// Does NOT set the property value, so one should probably store the file and then do - /// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath). - /// The original file path is used, in the old media file path scheme, to try and reuse - /// the "folder number" that was assigned to the previous file referenced by the property, - /// if any. - /// - public static string StoreFile(this IContentBase content, MediaFileManager mediaFileManager, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, Stream filestream, string filepath) + IContentTypeComposition? contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); + IPropertyType? propertyType = contentType?.CompositionPropertyTypes + .FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); + if (propertyType == null) { - var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); - var propertyType = contentType? - .CompositionPropertyTypes.FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); - if (propertyType == null) - throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); - return mediaFileManager.StoreFile(content, propertyType, filename, filestream, filepath); + throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); } - #endregion - - - #region Dirty - - public static IEnumerable GetDirtyUserProperties(this IContentBase entity) - { - return entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); - } - - - - #endregion - - - /// - /// Creates the full xml representation for the object and all of it's descendants - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) - { - return serializer.Serialize(content, false, true); - } - - /// - /// Creates the xml representation for the object - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToXml(this IContent content, IEntityXmlSerializer serializer) - { - return serializer.Serialize(content, false, false); - } - - - /// - /// Creates the xml representation for the object - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToXml(this IMedia media, IEntityXmlSerializer serializer) - { - return serializer.Serialize(media); - } - - /// - /// Creates the xml representation for the object - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToXml(this IMember member, IEntityXmlSerializer serializer) - { - return serializer.Serialize(member); - } + property = new Property(propertyType); + content.Properties.Add(property); + return property; } + + #endregion } diff --git a/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs b/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs index 4469683acb..2654bf0e1e 100644 --- a/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs @@ -1,346 +1,381 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for content variations. +/// +public static class ContentVariationExtensions { /// - /// Provides extension methods for content variations. + /// Determines whether the content type is invariant. /// - public static class ContentVariationExtensions + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// + public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the content type is invariant. + /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// + public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the content type is invariant. + /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// + public static bool VariesByNothing(this IPublishedContentType contentType) => + contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the property type is invariant. + /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// + public static bool VariesByNothing(this IPropertyType propertyType) => propertyType.Variations.VariesByNothing(); + + /// + /// Determines whether the property type is invariant. + /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// + public static bool VariesByNothing(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesByNothing(); + + /// + /// Determines whether a variation is invariant. + /// + /// The variation. + /// + /// A value indicating whether the variation is invariant. + /// + public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this IPublishedContentType contentType) => + contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by culture. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// + public static bool VariesByCulture(this IPropertyType propertyType) => propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by culture. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// + public static bool VariesByCulture(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether a variation varies by culture. + /// + /// The variation. + /// + /// A value indicating whether the variation varies by culture. + /// + public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this IPublishedContentType contentType) => + contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// + public static bool VariesBySegment(this IPropertyType propertyType) => propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// + public static bool VariesBySegment(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether a variation varies by segment. + /// + /// The variation. + /// + /// A value indicating whether the variation varies by segment. + /// + public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => + contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => + contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => + contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => + propertyType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether a variation varies by culture and segment. + /// + /// The variation. + /// + /// A value indicating whether the variation varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ContentVariation variation) => + (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; + + /// + /// Sets or removes the content type variation depending on the specified value. + /// + /// The content type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => + contentType.Variations = contentType.Variations.SetFlag(variation, value); + + /// + /// Sets or removes the property type variation depending on the specified value. + /// + /// The property type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// + public static void SetVariesBy(this IPropertyType propertyType, ContentVariation variation, bool value = true) => + propertyType.Variations = propertyType.Variations.SetFlag(variation, value); + + /// + /// Returns the variations with the variation set or removed depending on the specified value. + /// + /// The existing variations. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// The variations with the variation set or removed. + /// + /// + /// This method does not support setting the variation to nothing. + /// + public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true) => + value + ? variations | variation // Set flag using bitwise logical OR + : variations & + ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + + /// + /// Validates that a combination of culture and segment is valid for the variation. + /// + /// The variation. + /// The culture. + /// The segment. + /// A value indicating whether to perform exact validation. + /// A value indicating whether to support wildcards. + /// + /// A value indicating whether to throw a when the + /// combination is invalid. + /// + /// + /// true if the combination is valid; otherwise false. + /// + /// + /// Occurs when the combination is invalid, and + /// is true. + /// + /// + /// + /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is + /// Culture, then + /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: + /// if the variation is + /// Culture, an invariant combination is ok. + /// + /// + /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one + /// content type. + /// + /// Both and can be "*" to indicate "all of them". + /// + public static bool ValidateVariation(this ContentVariation variation, string? culture, string? segment, bool exact, bool wildcards, bool throwIfInvalid) { - /// - /// Determines whether the content type is invariant. - /// - /// The content type. - /// - /// A value indicating whether the content type is invariant. - /// - public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing(); + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); - /// - /// Determines whether the content type is invariant. - /// - /// The content type. - /// - /// A value indicating whether the content type is invariant. - /// - public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); - - /// - /// Determines whether the content type is invariant. - /// - /// The content type. - /// - /// A value indicating whether the content type is invariant. - /// - public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing(); - - /// - /// Determines whether the property type is invariant. - /// - /// The property type. - /// - /// A value indicating whether the property type is invariant. - /// - public static bool VariesByNothing(this IPropertyType propertyType) => propertyType.Variations.VariesByNothing(); - - /// - /// Determines whether the property type is invariant. - /// - /// The property type. - /// - /// A value indicating whether the property type is invariant. - /// - public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); - - /// - /// Determines whether a variation is invariant. - /// - /// The variation. - /// - /// A value indicating whether the variation is invariant. - /// - public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; - - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the property type varies by culture. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture. - /// - public static bool VariesByCulture(this IPropertyType propertyType) => propertyType.Variations.VariesByCulture(); - - /// - /// Determines whether the property type varies by culture. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture. - /// - public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); - - /// - /// Determines whether a variation varies by culture. - /// - /// The variation. - /// - /// A value indicating whether the variation varies by culture. - /// - public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the property type varies by segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by segment. - /// - public static bool VariesBySegment(this IPropertyType propertyType) => propertyType.Variations.VariesBySegment(); - - /// - /// Determines whether the property type varies by segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by segment. - /// - public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); - - /// - /// Determines whether a variation varies by segment. - /// - /// The variation. - /// - /// A value indicating whether the variation varies by segment. - /// - public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the property type varies by culture and segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the property type varies by culture and segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether a variation varies by culture and segment. - /// - /// The variation. - /// - /// A value indicating whether the variation varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; - - /// - /// Sets or removes the content type variation depending on the specified value. - /// - /// The content type. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// This method does not support setting the variation to nothing. - /// - public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetFlag(variation, value); - - /// - /// Sets or removes the property type variation depending on the specified value. - /// - /// The property type. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// This method does not support setting the variation to nothing. - /// - public static void SetVariesBy(this IPropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetFlag(variation, value); - - /// - /// Returns the variations with the variation set or removed depending on the specified value. - /// - /// The existing variations. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// The variations with the variation set or removed. - /// - /// - /// This method does not support setting the variation to nothing. - /// - public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true) + // if wildcards are disabled, do not allow "*" + if (!wildcards && (culture == "*" || segment == "*")) { - return value - ? variations | variation // Set flag using bitwise logical OR - : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + if (throwIfInvalid) + { + throw new NotSupportedException("Variation wildcards are not supported."); + } + + return false; } - /// - /// Validates that a combination of culture and segment is valid for the variation. - /// - /// The variation. - /// The culture. - /// The segment. - /// A value indicating whether to perform exact validation. - /// A value indicating whether to support wildcards. - /// A value indicating whether to throw a when the combination is invalid. - /// - /// true if the combination is valid; otherwise false. - /// - /// Occurs when the combination is invalid, and is true. - /// - /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then - /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is - /// Culture, an invariant combination is ok. - /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type. - /// Both and can be "*" to indicate "all of them". - /// - public static bool ValidateVariation(this ContentVariation variation, string? culture, string? segment, bool exact, bool wildcards, bool throwIfInvalid) + if (variation.VariesByCulture()) { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - // if wildcards are disabled, do not allow "*" - if (!wildcards && (culture == "*" || segment == "*")) + // varies by culture + // in exact mode, the culture cannot be null + if (exact && culture == null) { if (throwIfInvalid) - throw new NotSupportedException($"Variation wildcards are not supported."); - return false; - } - - if (variation.VariesByCulture()) - { - // varies by culture - // in exact mode, the culture cannot be null - if (exact && culture == null) { - if (throwIfInvalid) - throw new NotSupportedException($"Culture may not be null because culture variation is enabled."); - - return false; + throw new NotSupportedException("Culture may not be null because culture variation is enabled."); } - } - else - { - // does not vary by culture - // the culture cannot have a value - // unless wildcards and it's "*" - if (culture != null && !(wildcards && culture == "*")) - { - if (throwIfInvalid) - throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled."); - - return false; - } - } - - // if it does not vary by segment - // the segment cannot have a value - // segment may always be null, even when the ContentVariation.Segment flag is set for this variation, - // therefore the exact parameter is not used in segment validation. - if (!variation.VariesBySegment() && segment != null && !(wildcards && segment == "*")) - { - if (throwIfInvalid) - throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled."); return false; } - - return true; } + else + { + // does not vary by culture + // the culture cannot have a value + // unless wildcards and it's "*" + if (culture != null && !(wildcards && culture == "*")) + { + if (throwIfInvalid) + { + throw new NotSupportedException( + $"Culture \"{culture}\" is invalid because culture variation is disabled."); + } + + return false; + } + } + + // if it does not vary by segment + // the segment cannot have a value + // segment may always be null, even when the ContentVariation.Segment flag is set for this variation, + // therefore the exact parameter is not used in segment validation. + if (!variation.VariesBySegment() && segment != null && !(wildcards && segment == "*")) + { + if (throwIfInvalid) + { + throw new NotSupportedException( + $"Segment \"{segment}\" is invalid because segment variation is disabled."); + } + + return false; + } + + return true; } } diff --git a/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs b/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs index 8dfec45c7e..1af0b8c47a 100644 --- a/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs +++ b/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs @@ -1,24 +1,21 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Cache; -namespace Umbraco.Extensions -{ - /// - /// Extension methods for the cache helper - /// - public static class CoreCacheHelperExtensions - { - public const string PartialViewCacheKey = "Umbraco.Web.PartialViewCacheKey"; +namespace Umbraco.Extensions; - /// - /// Clears the cache for partial views - /// - /// - public static void ClearPartialViewCache(this AppCaches appCaches) - { - appCaches.RuntimeCache.ClearByKey(PartialViewCacheKey); - } - } +/// +/// Extension methods for the cache helper +/// +public static class CoreCacheHelperExtensions +{ + public const string PartialViewCacheKey = "Umbraco.Web.PartialViewCacheKey"; + + /// + /// Clears the cache for partial views + /// + /// + public static void ClearPartialViewCache(this AppCaches appCaches) => + appCaches.RuntimeCache.ClearByKey(PartialViewCacheKey); } diff --git a/src/Umbraco.Core/Extensions/DataTableExtensions.cs b/src/Umbraco.Core/Extensions/DataTableExtensions.cs index 4594709407..10fa51deaf 100644 --- a/src/Umbraco.Core/Extensions/DataTableExtensions.cs +++ b/src/Umbraco.Core/Extensions/DataTableExtensions.cs @@ -1,114 +1,112 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Static and extension methods for the DataTable object +/// +public static class DataTableExtensions { /// - /// Static and extension methods for the DataTable object + /// Creates a DataTable with the specified alias and columns and uses a callback to populate the headers. /// - public static class DataTableExtensions + /// + /// + /// + /// + /// + /// This has been migrated from the Node class and uses proper locking now. It is now used by the Node class and the + /// DynamicPublishedContent extensions for legacy reasons. + /// + public static DataTable GenerateDataTable( + string tableAlias, + Func>> getHeaders, + Func>, IEnumerable>>>> + rowData) { - /// - /// Creates a DataTable with the specified alias and columns and uses a callback to populate the headers. - /// - /// - /// - /// - /// - /// - /// This has been migrated from the Node class and uses proper locking now. It is now used by the Node class and the - /// DynamicPublishedContent extensions for legacy reasons. - /// - public static DataTable GenerateDataTable( - string tableAlias, - Func>> getHeaders, - Func>, IEnumerable>>>> rowData) + var dt = new DataTable(tableAlias); + + // get all row data + Tuple>, IEnumerable>>[] tableData = + rowData().ToArray(); + + // get all headers + IDictionary propertyHeaders = GetPropertyHeaders(tableAlias, getHeaders); + foreach (KeyValuePair h in propertyHeaders) { - var dt = new DataTable(tableAlias); - - //get all row data - var tableData = rowData().ToArray(); - - //get all headers - var propertyHeaders = GetPropertyHeaders(tableAlias, getHeaders); - foreach(var h in propertyHeaders) - { - dt.Columns.Add(new DataColumn(h.Value)); - } - - //add row data - foreach(var r in tableData) - { - dt.PopulateRow( - propertyHeaders, - r.Item1, - r.Item2); - } - - return dt; + dt.Columns.Add(new DataColumn(h.Value)); } - /// - /// Helper method to return this ugly object - /// - /// - /// - /// This is for legacy code, I didn't want to go creating custom classes for these - /// - public static List>, IEnumerable>>> CreateTableData() + // add row data + foreach (Tuple>, IEnumerable>> r in + tableData) { - return new List>, IEnumerable>>>(); + dt.PopulateRow( + propertyHeaders, + r.Item1, + r.Item2); } - /// - /// Helper method to deal with these ugly objects - /// - /// - /// - /// - /// - /// This is for legacy code, I didn't want to go creating custom classes for these - /// - public static void AddRowData( - List>, IEnumerable>>> rowData, - IEnumerable> standardVals, - IEnumerable> userVals) + return dt; + } + + /// + /// Helper method to return this ugly object + /// + /// + /// + /// This is for legacy code, I didn't want to go creating custom classes for these + /// + public static List>, IEnumerable>>> + CreateTableData() => + new List>, IEnumerable>>>(); + + /// + /// Helper method to deal with these ugly objects + /// + /// + /// + /// + /// + /// This is for legacy code, I didn't want to go creating custom classes for these + /// + public static void AddRowData( + List>, IEnumerable>>> rowData, + IEnumerable> standardVals, + IEnumerable> userVals) => + rowData.Add(new Tuple>, IEnumerable>>( + standardVals, + userVals)); + + private static IDictionary GetPropertyHeaders( + string alias, + Func>> getHeaders) + { + IEnumerable> headers = getHeaders(alias); + var def = headers.ToDictionary(pt => pt.Key, pt => pt.Value); + return def; + } + + private static void PopulateRow( + this DataTable dt, + IDictionary aliasesToNames, + IEnumerable> standardVals, + IEnumerable> userPropertyVals) + { + DataRow dr = dt.NewRow(); + foreach (KeyValuePair r in standardVals) { - rowData.Add(new System.Tuple>, IEnumerable>>( - standardVals, - userVals - )); + dr[r.Key] = r.Value; } - private static IDictionary GetPropertyHeaders(string alias, Func>> getHeaders) + foreach (KeyValuePair p in userPropertyVals.Where(p => p.Value != null)) { - var headers = getHeaders(alias); - var def = headers.ToDictionary(pt => pt.Key, pt => pt.Value); - return def; - } - - private static void PopulateRow( - this DataTable dt, - IDictionary aliasesToNames, - IEnumerable> standardVals, - IEnumerable> userPropertyVals) - { - var dr = dt.NewRow(); - foreach (var r in standardVals) - { - dr[r.Key] = r.Value; - } - foreach (var p in userPropertyVals.Where(p => p.Value != null)) - { - dr[aliasesToNames[p.Key]] = p.Value; - } - dt.Rows.Add(dr); + dr[aliasesToNames[p.Key]] = p.Value; } + dt.Rows.Add(dr); } } diff --git a/src/Umbraco.Core/Extensions/DateTimeExtensions.cs b/src/Umbraco.Core/Extensions/DateTimeExtensions.cs index e500cf86b0..35c9f600e5 100644 --- a/src/Umbraco.Core/Extensions/DateTimeExtensions.cs +++ b/src/Umbraco.Core/Extensions/DateTimeExtensions.cs @@ -1,46 +1,57 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Globalization; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class DateTimeExtensions { - public static class DateTimeExtensions + public enum DateTruncate { - /// - /// Returns the DateTime as an ISO formatted string that is globally expectable - /// - /// - /// - public static string ToIsoString(this DateTime dt) + Year, + Month, + Day, + Hour, + Minute, + Second, + } + + /// + /// Returns the DateTime as an ISO formatted string that is globally expectable + /// + /// + /// + public static string ToIsoString(this DateTime dt) => + dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + + public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) + { + if (truncateTo == DateTruncate.Year) { - return dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + return new DateTime(dt.Year, 1, 1); } - public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) + if (truncateTo == DateTruncate.Month) { - if (truncateTo == DateTruncate.Year) - return new DateTime(dt.Year, 1, 1); - if (truncateTo == DateTruncate.Month) - return new DateTime(dt.Year, dt.Month, 1); - if (truncateTo == DateTruncate.Day) - return new DateTime(dt.Year, dt.Month, dt.Day); - if (truncateTo == DateTruncate.Hour) - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0); - if (truncateTo == DateTruncate.Minute) - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0); - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second); + return new DateTime(dt.Year, dt.Month, 1); } - public enum DateTruncate + if (truncateTo == DateTruncate.Day) { - Year, - Month, - Day, - Hour, - Minute, - Second + return new DateTime(dt.Year, dt.Month, dt.Day); } + + if (truncateTo == DateTruncate.Hour) + { + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0); + } + + if (truncateTo == DateTruncate.Minute) + { + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0); + } + + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second); } } diff --git a/src/Umbraco.Core/Extensions/DecimalExtensions.cs b/src/Umbraco.Core/Extensions/DecimalExtensions.cs index fa62805841..6e70544d0e 100644 --- a/src/Umbraco.Core/Extensions/DecimalExtensions.cs +++ b/src/Umbraco.Core/Extensions/DecimalExtensions.cs @@ -1,26 +1,25 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for System.Decimal. +/// +/// +/// See System.Decimal on MSDN and also +/// http://stackoverflow.com/questions/4298719/parse-decimal-and-filter-extra-0-on-the-right/4298787#4298787. +/// +public static class DecimalExtensions { /// - /// Provides extension methods for System.Decimal. + /// Gets the normalized value. /// - /// See System.Decimal on MSDN and also - /// http://stackoverflow.com/questions/4298719/parse-decimal-and-filter-extra-0-on-the-right/4298787#4298787. + /// The value to normalize. + /// The normalized value. + /// + /// Normalizing changes the scaling factor and removes trailing zeros, + /// so 1.2500m comes out as 1.25m. /// - public static class DecimalExtensions - { - /// - /// Gets the normalized value. - /// - /// The value to normalize. - /// The normalized value. - /// Normalizing changes the scaling factor and removes trailing zeros, - /// so 1.2500m comes out as 1.25m. - public static decimal Normalize(this decimal value) - { - return value / 1.000000000000000000000000000000000m; - } - } + public static decimal Normalize(this decimal value) => value / 1.000000000000000000000000000000000m; } diff --git a/src/Umbraco.Core/Extensions/DelegateExtensions.cs b/src/Umbraco.Core/Extensions/DelegateExtensions.cs index 4cbcdd5d6a..621ef46438 100644 --- a/src/Umbraco.Core/Extensions/DelegateExtensions.cs +++ b/src/Umbraco.Core/Extensions/DelegateExtensions.cs @@ -1,48 +1,57 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Diagnostics; -using System.Threading; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class DelegateExtensions { - public static class DelegateExtensions + public static Attempt RetryUntilSuccessOrTimeout(this Func> task, TimeSpan timeout, TimeSpan pause) { - public static Attempt RetryUntilSuccessOrTimeout(this Func> task, TimeSpan timeout, TimeSpan pause) + if (pause.TotalMilliseconds < 0) { - if (pause.TotalMilliseconds < 0) - { - throw new ArgumentException("pause must be >= 0 milliseconds"); - } - var stopwatch = Stopwatch.StartNew(); - do - { - var result = task(); - if (result.Success) { return result; } - Thread.Sleep((int)pause.TotalMilliseconds); - } - while (stopwatch.Elapsed < timeout); - return Attempt.Fail(); + throw new ArgumentException("pause must be >= 0 milliseconds"); } - public static Attempt RetryUntilSuccessOrMaxAttempts(this Func> task, int totalAttempts, TimeSpan pause) + var stopwatch = Stopwatch.StartNew(); + do { - if (pause.TotalMilliseconds < 0) + Attempt result = task(); + if (result.Success) { - throw new ArgumentException("pause must be >= 0 milliseconds"); + return result; } - int attempts = 0; - do - { - attempts++; - var result = task(attempts); - if (result.Success) { return result; } - Thread.Sleep((int)pause.TotalMilliseconds); - } - while (attempts < totalAttempts); - return Attempt.Fail(); + + Thread.Sleep((int)pause.TotalMilliseconds); } + while (stopwatch.Elapsed < timeout); + + return Attempt.Fail(); + } + + public static Attempt RetryUntilSuccessOrMaxAttempts(this Func> task, int totalAttempts, TimeSpan pause) + { + if (pause.TotalMilliseconds < 0) + { + throw new ArgumentException("pause must be >= 0 milliseconds"); + } + + var attempts = 0; + do + { + attempts++; + Attempt result = task(attempts); + if (result.Success) + { + return result; + } + + Thread.Sleep((int)pause.TotalMilliseconds); + } + while (attempts < totalAttempts); + + return Attempt.Fail(); } } diff --git a/src/Umbraco.Core/Extensions/DictionaryExtensions.cs b/src/Umbraco.Core/Extensions/DictionaryExtensions.cs index 906f12282e..3bbd3bdcb9 100644 --- a/src/Umbraco.Core/Extensions/DictionaryExtensions.cs +++ b/src/Umbraco.Core/Extensions/DictionaryExtensions.cs @@ -1,306 +1,320 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using System.Net; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core; -namespace Umbraco.Extensions -{ - /// - /// Extension methods for Dictionary & ConcurrentDictionary - /// - public static class DictionaryExtensions - { +namespace Umbraco.Extensions; - /// - /// Method to Get a value by the key. If the key doesn't exist it will create a new TVal object for the key and return it. - /// - /// - /// - /// - /// - /// - public static TVal GetOrCreate(this IDictionary dict, TKey key) - where TVal : class, new() +/// +/// Extension methods for Dictionary & ConcurrentDictionary +/// +public static class DictionaryExtensions +{ + /// + /// Method to Get a value by the key. If the key doesn't exist it will create a new TVal object for the key and return + /// it. + /// + /// + /// + /// + /// + /// + public static TVal GetOrCreate(this IDictionary dict, TKey key) + where TVal : class, new() + { + if (dict.ContainsKey(key) == false) { - if (dict.ContainsKey(key) == false) - { - dict.Add(key, new TVal()); - } - return dict[key]; + dict.Add(key, new TVal()); } - /// - /// Updates an item with the specified key with the specified value - /// - /// - /// - /// - /// - /// - /// - /// - /// Taken from: http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression - /// - /// If there is an item in the dictionary with the key, it will keep trying to update it until it can - /// - public static bool TryUpdate(this ConcurrentDictionary dict, TKey key, Func updateFactory) - where TKey : notnull + return dict[key]; + } + + /// + /// Updates an item with the specified key with the specified value + /// + /// + /// + /// + /// + /// + /// + /// + /// Taken from: + /// http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression + /// If there is an item in the dictionary with the key, it will keep trying to update it until it can + /// + public static bool TryUpdate(this ConcurrentDictionary dict, TKey key, Func updateFactory) + where TKey : notnull + { + while (dict.TryGetValue(key, out TValue? curValue)) { - TValue? curValue; - while (dict.TryGetValue(key, out curValue)) + if (dict.TryUpdate(key, updateFactory(curValue), curValue)) { - if (dict.TryUpdate(key, updateFactory(curValue), curValue)) - return true; - //if we're looping either the key was removed by another thread, or another thread - //changed the value, so we start again. + return true; } + + // if we're looping either the key was removed by another thread, or another thread + // changed the value, so we start again. + } + + return false; + } + + /// + /// Updates an item with the specified key with the specified value + /// + /// + /// + /// + /// + /// + /// + /// + /// Taken from: + /// http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression + /// WARNING: If the value changes after we've retrieved it, then the item will not be updated + /// + public static bool TryUpdateOptimisitic(this ConcurrentDictionary dict, TKey key, Func updateFactory) + where TKey : notnull + { + if (!dict.TryGetValue(key, out TValue? curValue)) + { return false; } - /// - /// Updates an item with the specified key with the specified value - /// - /// - /// - /// - /// - /// - /// - /// - /// Taken from: http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression - /// - /// WARNING: If the value changes after we've retrieved it, then the item will not be updated - /// - public static bool TryUpdateOptimisitic(this ConcurrentDictionary dict, TKey key, Func updateFactory) - where TKey : notnull + dict.TryUpdate(key, updateFactory(curValue), curValue); + return true; // note we return true whether we succeed or not, see explanation below. + } + + /// + /// Converts a dictionary to another type by only using direct casting + /// + /// + /// + /// + /// + public static IDictionary ConvertTo(this IDictionary d) + where TKeyOut : notnull + { + var result = new Dictionary(); + foreach (DictionaryEntry v in d) { - TValue? curValue; - if (!dict.TryGetValue(key, out curValue)) - return false; - dict.TryUpdate(key, updateFactory(curValue), curValue); - return true;//note we return true whether we succeed or not, see explanation below. + result.Add((TKeyOut)v.Key, (TValOut)v.Value!); } - /// - /// Converts a dictionary to another type by only using direct casting - /// - /// - /// - /// - /// - public static IDictionary ConvertTo(this IDictionary d) - where TKeyOut : notnull + return result; + } + + /// + /// Converts a dictionary to another type using the specified converters + /// + /// + /// + /// + /// + /// + /// + public static IDictionary ConvertTo( + this IDictionary d, + Func keyConverter, + Func valConverter) + where TKeyOut : notnull + { + var result = new Dictionary(); + foreach (DictionaryEntry v in d) { - var result = new Dictionary(); - foreach (DictionaryEntry v in d) - { - result.Add((TKeyOut)v.Key, (TValOut)v.Value!); - } - return result; + result.Add(keyConverter(v.Key), valConverter(v.Value!)); } - /// - /// Converts a dictionary to another type using the specified converters - /// - /// - /// - /// - /// - /// - /// - public static IDictionary ConvertTo(this IDictionary d, Func keyConverter, Func valConverter) - where TKeyOut : notnull + return result; + } + + /// + /// Converts a dictionary to a NameValueCollection + /// + /// + /// + public static NameValueCollection ToNameValueCollection(this IDictionary d) + { + var n = new NameValueCollection(); + foreach (KeyValuePair i in d) { - var result = new Dictionary(); - foreach (DictionaryEntry v in d) - { - result.Add(keyConverter(v.Key), valConverter(v.Value!)); - } - return result; + n.Add(i.Key, i.Value); } - /// - /// Converts a dictionary to a NameValueCollection - /// - /// - /// - public static NameValueCollection ToNameValueCollection(this IDictionary d) + return n; + } + + /// + /// Merges all key/values from the sources dictionaries into the destination dictionary + /// + /// + /// + /// + /// The source dictionary to merge other dictionaries into + /// + /// By default all values will be retained in the destination if the same keys exist in the sources but + /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the + /// destination. Note that + /// it will just use the last found key/value if this is true. + /// + /// The other dictionaries to merge values from + public static void MergeLeft(this T destination, IEnumerable> sources, bool overwrite = false) + where T : IDictionary + { + foreach (KeyValuePair p in sources.SelectMany(src => src) + .Where(p => overwrite || destination.ContainsKey(p.Key) == false)) { - var n = new NameValueCollection(); - foreach (var i in d) - { - n.Add(i.Key, i.Value); - } - return n; - } - - - /// - /// Merges all key/values from the sources dictionaries into the destination dictionary - /// - /// - /// - /// - /// The source dictionary to merge other dictionaries into - /// - /// By default all values will be retained in the destination if the same keys exist in the sources but - /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the destination. Note that - /// it will just use the last found key/value if this is true. - /// - /// The other dictionaries to merge values from - public static void MergeLeft(this T destination, IEnumerable> sources, bool overwrite = false) - where T : IDictionary - { - foreach (var p in sources.SelectMany(src => src).Where(p => overwrite || destination.ContainsKey(p.Key) == false)) - { - destination[p.Key] = p.Value; - } - } - - /// - /// Merges all key/values from the sources dictionaries into the destination dictionary - /// - /// - /// - /// - /// The source dictionary to merge other dictionaries into - /// - /// By default all values will be retained in the destination if the same keys exist in the sources but - /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the destination. Note that - /// it will just use the last found key/value if this is true. - /// - /// The other dictionary to merge values from - public static void MergeLeft(this T destination, IDictionary source, bool overwrite = false) - where T : IDictionary - { - destination.MergeLeft(new[] {source}, overwrite); - } - - /// - /// Returns the value of the key value based on the key, if the key is not found, a null value is returned - /// - /// The type of the key. - /// The type of the val. - /// The d. - /// The key. - /// The default value. - /// - public static TVal? GetValue(this IDictionary d, TKey key, TVal? defaultValue = default(TVal)) - { - if (d.ContainsKey(key)) - { - return d[key]; - } - return defaultValue; - } - - /// - /// Returns the value of the key value based on the key as it's string value, if the key is not found, then an empty string is returned - /// - /// - /// - /// - public static string? GetValueAsString(this IDictionary d, TKey key) - => d.ContainsKey(key) ? d[key]!.ToString() : string.Empty; - - /// - /// Returns the value of the key value based on the key as it's string value, if the key is not found or is an empty string, then the provided default value is returned - /// - /// - /// - /// - /// - public static string? GetValueAsString(this IDictionary d, TKey key, string defaultValue) - { - if (d.ContainsKey(key)) - { - var value = d[key]!.ToString(); - if (value != string.Empty) - return value; - } - - return defaultValue; - } - - /// contains key ignore case. - /// The dictionary. - /// The key. - /// Value Type - /// The contains key ignore case. - public static bool ContainsKeyIgnoreCase(this IDictionary dictionary, string key) - { - return dictionary.Keys.InvariantContains(key); - } - - /// - /// Converts a dictionary object to a query string representation such as: - /// firstname=shannon&lastname=deminick - /// - /// - /// - public static string ToQueryString(this IDictionary d) - { - if (!d.Any()) return ""; - - var builder = new StringBuilder(); - foreach (var i in d) - { - builder.Append(String.Format("{0}={1}&", WebUtility.UrlEncode(i.Key), i.Value == null ? string.Empty : WebUtility.UrlEncode(i.Value.ToString()))); - } - return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); - } - - /// The get entry ignore case. - /// The dictionary. - /// The key. - /// The type - /// The entry - public static TValue? GetValueIgnoreCase(this IDictionary dictionary, string key) - => dictionary!.GetValueIgnoreCase(key, default(TValue)); - - /// The get entry ignore case. - /// The dictionary. - /// The key. - /// The default value. - /// The type - /// The entry - public static TValue GetValueIgnoreCase(this IDictionary dictionary, string? key, TValue - defaultValue) - { - key = dictionary.Keys.FirstOrDefault(i => i.InvariantEquals(key)); - - return key.IsNullOrWhiteSpace() == false - ? dictionary[key!] - : defaultValue; - } - - public static async Task> ToDictionaryAsync( - this IEnumerable enumerable, - Func syncKeySelector, - Func> asyncValueSelector) - where TKey : notnull - { - Dictionary dictionary = new Dictionary(); - - foreach (var item in enumerable) - { - var key = syncKeySelector(item); - - var value = await asyncValueSelector(item); - - dictionary.Add(key,value); - } - - return dictionary; + destination[p.Key] = p.Value; } } + + /// + /// Merges all key/values from the sources dictionaries into the destination dictionary + /// + /// + /// + /// + /// The source dictionary to merge other dictionaries into + /// + /// By default all values will be retained in the destination if the same keys exist in the sources but + /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the + /// destination. Note that + /// it will just use the last found key/value if this is true. + /// + /// The other dictionary to merge values from + public static void MergeLeft(this T destination, IDictionary source, bool overwrite = false) + where T : IDictionary => + destination.MergeLeft(new[] { source }, overwrite); + + /// + /// Returns the value of the key value based on the key, if the key is not found, a null value is returned + /// + /// The type of the key. + /// The type of the val. + /// The d. + /// The key. + /// The default value. + /// + public static TVal? GetValue(this IDictionary d, TKey key, TVal? defaultValue = default) + { + if (d.ContainsKey(key)) + { + return d[key]; + } + + return defaultValue; + } + + /// + /// Returns the value of the key value based on the key as it's string value, if the key is not found, then an empty + /// string is returned + /// + /// + /// + /// + public static string? GetValueAsString(this IDictionary d, TKey key) + => d.ContainsKey(key) ? d[key]!.ToString() : string.Empty; + + /// + /// Returns the value of the key value based on the key as it's string value, if the key is not found or is an empty + /// string, then the provided default value is returned + /// + /// + /// + /// + /// + public static string? GetValueAsString(this IDictionary d, TKey key, string defaultValue) + { + if (d.ContainsKey(key)) + { + var value = d[key]!.ToString(); + if (value != string.Empty) + { + return value; + } + } + + return defaultValue; + } + + /// contains key ignore case. + /// The dictionary. + /// The key. + /// Value Type + /// The contains key ignore case. + public static bool ContainsKeyIgnoreCase(this IDictionary dictionary, string key) => + dictionary.Keys.InvariantContains(key); + + /// + /// Converts a dictionary object to a query string representation such as: + /// firstname=shannon&lastname=deminick + /// + /// + /// + public static string ToQueryString(this IDictionary d) + { + if (!d.Any()) + { + return string.Empty; + } + + var builder = new StringBuilder(); + foreach (KeyValuePair i in d) + { + builder.Append(string.Format("{0}={1}&", WebUtility.UrlEncode(i.Key), i.Value == null ? string.Empty : WebUtility.UrlEncode(i.Value.ToString()))); + } + + return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); + } + + /// The get entry ignore case. + /// The dictionary. + /// The key. + /// The type + /// The entry + public static TValue? GetValueIgnoreCase(this IDictionary dictionary, string key) + => dictionary!.GetValueIgnoreCase(key, default); + + /// The get entry ignore case. + /// The dictionary. + /// The key. + /// The default value. + /// The type + /// The entry + public static TValue GetValueIgnoreCase(this IDictionary dictionary, string? key, TValue + defaultValue) + { + key = dictionary.Keys.FirstOrDefault(i => i.InvariantEquals(key)); + + return key.IsNullOrWhiteSpace() == false + ? dictionary[key!] + : defaultValue; + } + + public static async Task> ToDictionaryAsync( + this IEnumerable enumerable, + Func syncKeySelector, + Func> asyncValueSelector) + where TKey : notnull + { + var dictionary = new Dictionary(); + + foreach (TInput item in enumerable) + { + TKey key = syncKeySelector(item); + + TValue value = await asyncValueSelector(item); + + dictionary.Add(key, value); + } + + return dictionary; + } } diff --git a/src/Umbraco.Core/Extensions/EnumerableExtensions.cs b/src/Umbraco.Core/Extensions/EnumerableExtensions.cs index 28f4844f00..6628dc4f3d 100644 --- a/src/Umbraco.Core/Extensions/EnumerableExtensions.cs +++ b/src/Umbraco.Core/Extensions/EnumerableExtensions.cs @@ -1,351 +1,399 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extensions for enumerable sources +/// +public static class EnumerableExtensions { + public static bool IsCollectionEmpty(this IReadOnlyCollection? list) => list == null || list.Count == 0; + /// - /// Extensions for enumerable sources + /// Wraps this object instance into an IEnumerable{T} consisting of a single item. /// - public static class EnumerableExtensions + /// Type of the object. + /// The instance that will be wrapped. + /// An IEnumerable{T} consisting of a single item. + public static IEnumerable Yield(this T item) { - public static bool IsCollectionEmpty(this IReadOnlyCollection? list) => list == null || list.Count == 0; + // see EnumeratorBenchmarks - this is faster, and allocates less, than returning an array + yield return item; + } - internal static bool HasDuplicates(this IEnumerable items, bool includeNull) + internal static bool HasDuplicates(this IEnumerable items, bool includeNull) + { + var hs = new HashSet(); + foreach (T item in items) { - var hs = new HashSet(); - foreach (var item in items) + if ((item != null || includeNull) && !hs.Add(item)) { - if ((item != null || includeNull) && !hs.Add(item)) - return true; - } - return false; - } - - - /// - /// Wraps this object instance into an IEnumerable{T} consisting of a single item. - /// - /// Type of the object. - /// The instance that will be wrapped. - /// An IEnumerable{T} consisting of a single item. - public static IEnumerable Yield(this T item) - { - // see EnumeratorBenchmarks - this is faster, and allocates less, than returning an array - yield return item; - } - - public static IEnumerable> InGroupsOf(this IEnumerable? source, int groupSize) - { - if (source == null) - throw new ArgumentNullException("source"); - if (groupSize <= 0) - throw new ArgumentException("Must be greater than zero.", "groupSize"); - - - // following code derived from MoreLinq and does not allocate bazillions of tuples - - T[]? temp = null; - var count = 0; - - foreach (var item in source) - { - if (temp == null) temp = new T[groupSize]; - temp[count++] = item; - if (count != groupSize) continue; - yield return temp/*.Select(x => x)*/; - temp = null; - count = 0; - } - - if (temp != null && count > 0) - yield return temp.Take(count); - } - - public static IEnumerable SelectByGroups(this IEnumerable source, Func, IEnumerable> selector, int groupSize) - { - // don't want to use a SelectMany(x => x) here - isn't this better? - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var resultGroup in source.InGroupsOf(groupSize).Select(selector)) - foreach (var result in resultGroup) - yield return result; - } - - /// - /// Returns a sequence of length whose elements are the result of invoking . - /// - /// - /// The factory. - /// The count. - /// - public static IEnumerable Range(Func factory, int count) - { - for (int i = 1; i <= count; i++) - { - yield return factory.Invoke(i - 1); + return true; } } - /// The if not null. - /// The items. - /// The action. - /// The type - public static void IfNotNull(this IEnumerable items, Action action) where TItem : class + return false; + } + + public static IEnumerable> InGroupsOf(this IEnumerable? source, int groupSize) + { + if (source == null) { - if (items != null) + throw new ArgumentNullException("source"); + } + + if (groupSize <= 0) + { + throw new ArgumentException("Must be greater than zero.", "groupSize"); + } + + // following code derived from MoreLinq and does not allocate bazillions of tuples + T[]? temp = null; + var count = 0; + + foreach (T item in source) + { + if (temp == null) { - foreach (TItem item in items) + temp = new T[groupSize]; + } + + temp[count++] = item; + if (count != groupSize) + { + continue; + } + + yield return temp /*.Select(x => x)*/; + temp = null; + count = 0; + } + + if (temp != null && count > 0) + { + yield return temp.Take(count); + } + } + + public static IEnumerable SelectByGroups( + this IEnumerable source, + Func, IEnumerable> selector, + int groupSize) + { + // don't want to use a SelectMany(x => x) here - isn't this better? + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (IEnumerable resultGroup in source.InGroupsOf(groupSize).Select(selector)) + { + foreach (TResult result in resultGroup) + { + yield return result; + } + } + } + + /// + /// Returns a sequence of length whose elements are the result of invoking + /// . + /// + /// + /// The factory. + /// The count. + /// + public static IEnumerable Range(Func factory, int count) + { + for (var i = 1; i <= count; i++) + { + yield return factory.Invoke(i - 1); + } + } + + /// The if not null. + /// The items. + /// The action. + /// The type + public static void IfNotNull(this IEnumerable items, Action action) + where TItem : class + { + if (items != null) + { + foreach (TItem item in items) + { + item.IfNotNull(action); + } + } + } + + /// + /// Returns true if all items in the other collection exist in this collection + /// + /// + /// + /// + /// + public static bool ContainsAll(this IEnumerable source, IEnumerable other) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (other == null) + { + throw new ArgumentNullException("other"); + } + + return other.Except(source).Any() == false; + } + + /// + /// Returns true if the source contains any of the items in the other list + /// + /// + /// + /// + /// + public static bool ContainsAny(this IEnumerable source, IEnumerable other) => + other.Any(source.Contains); + + /// + /// Removes all matching items from an . + /// + /// + /// The list. + /// The predicate. + /// + public static void RemoveAll(this IList list, Func predicate) + { + for (var i = 0; i < list.Count; i++) + { + if (predicate(list[i])) + { + list.RemoveAt(i--); + } + } + } + + /// + /// Removes all matching items from an . + /// + /// + /// The list. + /// The predicate. + /// + public static void RemoveAll(this ICollection list, Func predicate) + { + T[] matches = list.Where(predicate).ToArray(); + foreach (T match in matches) + { + list.Remove(match); + } + } + + public static IEnumerable SelectRecursive( + this IEnumerable source, + Func> recursiveSelector, + int maxRecusionDepth = 100) + { + var stack = new Stack>(); + stack.Push(source.GetEnumerator()); + + try + { + while (stack.Count > 0) + { + if (stack.Count > maxRecusionDepth) { - item.IfNotNull(action); + throw new InvalidOperationException("Maximum recursion depth reached of " + maxRecusionDepth); } - } - } - - /// - /// Returns true if all items in the other collection exist in this collection - /// - /// - /// - /// - /// - public static bool ContainsAll(this IEnumerable source, IEnumerable other) - { - if (source == null) throw new ArgumentNullException("source"); - if (other == null) throw new ArgumentNullException("other"); - - return other.Except(source).Any() == false; - } - - /// - /// Returns true if the source contains any of the items in the other list - /// - /// - /// - /// - /// - public static bool ContainsAny(this IEnumerable source, IEnumerable other) - { - return other.Any(source.Contains); - } - - /// - /// Removes all matching items from an . - /// - /// - /// The list. - /// The predicate. - /// - public static void RemoveAll(this IList list, Func predicate) - { - for (var i = 0; i < list.Count; i++) - { - if (predicate(list[i])) + if (stack.Peek().MoveNext()) { - list.RemoveAt(i--); + TSource current = stack.Peek().Current; + + yield return current; + + stack.Push(recursiveSelector(current).GetEnumerator()); } - } - } - - /// - /// Removes all matching items from an . - /// - /// - /// The list. - /// The predicate. - /// - public static void RemoveAll(this ICollection list, Func predicate) - { - var matches = list.Where(predicate).ToArray(); - foreach (var match in matches) - { - list.Remove(match); - } - } - - public static IEnumerable SelectRecursive( - this IEnumerable source, - Func> recursiveSelector, int maxRecusionDepth = 100) - { - var stack = new Stack>(); - stack.Push(source.GetEnumerator()); - - try - { - while (stack.Count > 0) - { - if (stack.Count > maxRecusionDepth) - throw new InvalidOperationException("Maximum recursion depth reached of " + maxRecusionDepth); - - if (stack.Peek().MoveNext()) - { - var current = stack.Peek().Current; - - yield return current; - - stack.Push(recursiveSelector(current).GetEnumerator()); - } - else - { - stack.Pop().Dispose(); - } - } - } - finally - { - while (stack.Count > 0) + else { stack.Pop().Dispose(); } } } - - /// - /// Filters a sequence of values to ignore those which are null. - /// - /// - /// The coll. - /// - /// - public static IEnumerable WhereNotNull(this IEnumerable coll) where T : class + finally { - return coll.Where(x => x != null)!; - } - - public static IEnumerable ForAllThatAre(this IEnumerable sequence, Action projection) - where TActual : class - { - return sequence.Select( - x => - { - if (x is TActual casted) - { - projection.Invoke(casted); - } - return x; - }); - } - - /// - /// Finds the index of the first item matching an expression in an enumerable. - /// - /// The type of the enumerated objects. - /// The enumerable to search. - /// The expression to test the items against. - /// The index of the first matching item, or -1. - public static int FindIndex(this IEnumerable items, Func predicate) - { - return FindIndex(items, 0, predicate); - } - - /// - /// Finds the index of the first item matching an expression in an enumerable. - /// - /// The type of the enumerated objects. - /// The enumerable to search. - /// The index to start at. - /// The expression to test the items against. - /// The index of the first matching item, or -1. - public static int FindIndex(this IEnumerable items, int startIndex, Func predicate) - { - if (items == null) throw new ArgumentNullException("items"); - if (predicate == null) throw new ArgumentNullException("predicate"); - if (startIndex < 0) throw new ArgumentOutOfRangeException("startIndex"); - - var index = startIndex; - if (index > 0) - items = items.Skip(index); - - foreach (var item in items) + while (stack.Count > 0) { - if (predicate(item)) return index; - index++; + stack.Pop().Dispose(); } - - return -1; - } - - ///Finds the index of the first occurrence of an item in an enumerable. - ///The enumerable to search. - ///The item to find. - ///The index of the first matching item, or -1 if the item was not found. - public static int IndexOf(this IEnumerable items, T item) - { - return items.FindIndex(i => EqualityComparer.Default.Equals(item, i)); - } - - /// - /// Determines if 2 lists have equal elements within them regardless of how they are sorted - /// - /// - /// - /// - /// - /// - /// The logic for this is taken from: - /// http://stackoverflow.com/questions/4576723/test-whether-two-ienumerablet-have-the-same-values-with-the-same-frequencies - /// - /// There's a few answers, this one seems the best for it's simplicity and based on the comment of Eamon - /// - public static bool UnsortedSequenceEqual(this IEnumerable? source, IEnumerable? other) - { - if (source == null && other == null) return true; - if (source == null || other == null) return false; - - var list1Groups = source.ToLookup(i => i); - var list2Groups = other.ToLookup(i => i); - return list1Groups.Count == list2Groups.Count - && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); - } - - /// - /// Transforms an enumerable. - /// - /// - /// - /// - /// - public static IEnumerable Transform(this IEnumerable source, Func, IEnumerable> transform) - { - return transform(source); - } - - /// - /// Gets a null IEnumerable as an empty IEnumerable. - /// - /// - /// - /// - public static IEnumerable EmptyNull(this IEnumerable? items) - { - return items ?? Enumerable.Empty(); - } - - // the .OfType() filter is nice when there's only one type - // this is to support filtering with multiple types - public static IEnumerable OfTypes(this IEnumerable contents, params Type[] types) - { - return contents.Where(x => types.Contains(x?.GetType())); - } - - public static IEnumerable SkipLast(this IEnumerable source) - { - using (var e = source.GetEnumerator()) - { - if (e.MoveNext() == false) yield break; - - for (var value = e.Current; e.MoveNext(); value = e.Current) - yield return value; - } - } - - public static IOrderedEnumerable OrderBy(this IEnumerable source, Func keySelector, Direction sortOrder) - { - return sortOrder == Direction.Ascending ? source.OrderBy(keySelector) : source.OrderByDescending(keySelector); } } + + /// + /// Filters a sequence of values to ignore those which are null. + /// + /// + /// The coll. + /// + /// + public static IEnumerable WhereNotNull(this IEnumerable coll) + where T : class + => + coll.Where(x => x != null)!; + + public static IEnumerable ForAllThatAre( + this IEnumerable sequence, + Action projection) + where TActual : class => + sequence.Select( + x => + { + if (x is TActual casted) + { + projection.Invoke(casted); + } + + return x; + }); + + /// + /// Finds the index of the first item matching an expression in an enumerable. + /// + /// The type of the enumerated objects. + /// The enumerable to search. + /// The expression to test the items against. + /// The index of the first matching item, or -1. + public static int FindIndex(this IEnumerable items, Func predicate) => + FindIndex(items, 0, predicate); + + /// + /// Finds the index of the first item matching an expression in an enumerable. + /// + /// The type of the enumerated objects. + /// The enumerable to search. + /// The index to start at. + /// The expression to test the items against. + /// The index of the first matching item, or -1. + public static int FindIndex(this IEnumerable items, int startIndex, Func predicate) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + if (predicate == null) + { + throw new ArgumentNullException("predicate"); + } + + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException("startIndex"); + } + + var index = startIndex; + if (index > 0) + { + items = items.Skip(index); + } + + foreach (T item in items) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } + + /// Finds the index of the first occurrence of an item in an enumerable. + /// The enumerable to search. + /// The item to find. + /// The index of the first matching item, or -1 if the item was not found. + public static int IndexOf(this IEnumerable items, T item) => + items.FindIndex(i => EqualityComparer.Default.Equals(item, i)); + + /// + /// Determines if 2 lists have equal elements within them regardless of how they are sorted + /// + /// + /// + /// + /// + /// + /// The logic for this is taken from: + /// http://stackoverflow.com/questions/4576723/test-whether-two-ienumerablet-have-the-same-values-with-the-same-frequencies + /// There's a few answers, this one seems the best for it's simplicity and based on the comment of Eamon + /// + public static bool UnsortedSequenceEqual(this IEnumerable? source, IEnumerable? other) + { + if (source == null && other == null) + { + return true; + } + + if (source == null || other == null) + { + return false; + } + + ILookup list1Groups = source.ToLookup(i => i); + ILookup list2Groups = other.ToLookup(i => i); + return list1Groups.Count == list2Groups.Count + && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); + } + + /// + /// Transforms an enumerable. + /// + /// + /// + /// + /// + public static IEnumerable Transform( + this IEnumerable source, + Func, IEnumerable> transform) => transform(source); + + /// + /// Gets a null IEnumerable as an empty IEnumerable. + /// + /// + /// + /// + public static IEnumerable EmptyNull(this IEnumerable? items) => items ?? Enumerable.Empty(); + + // the .OfType() filter is nice when there's only one type + // this is to support filtering with multiple types + public static IEnumerable OfTypes(this IEnumerable contents, params Type[] types) => + contents.Where(x => types.Contains(x?.GetType())); + + public static IEnumerable SkipLast(this IEnumerable source) + { + using (IEnumerator e = source.GetEnumerator()) + { + if (e.MoveNext() == false) + { + yield break; + } + + for (T value = e.Current; e.MoveNext(); value = e.Current) + { + yield return value; + } + } + } + + public static IOrderedEnumerable OrderBy( + this IEnumerable source, + Func keySelector, + Direction sortOrder) => sortOrder == Direction.Ascending + ? source.OrderBy(keySelector) + : source.OrderByDescending(keySelector); } diff --git a/src/Umbraco.Core/Extensions/ExpressionExtensions.cs b/src/Umbraco.Core/Extensions/ExpressionExtensions.cs index d76f39a8de..12476c9506 100644 --- a/src/Umbraco.Core/Extensions/ExpressionExtensions.cs +++ b/src/Umbraco.Core/Extensions/ExpressionExtensions.cs @@ -1,27 +1,25 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Linq.Expressions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +internal static class ExpressionExtensions { - internal static class ExpressionExtensions + public static Expression> True() => f => true; + + public static Expression> False() => f => false; + + public static Expression> Or(this Expression> left, Expression> right) { - public static Expression> True() { return f => true; } + InvocationExpression invokedExpr = Expression.Invoke(right, left.Parameters); + return Expression.Lambda>(Expression.OrElse(left.Body, invokedExpr), left.Parameters); + } - public static Expression> False() { return f => false; } - - public static Expression> Or(this Expression> left, Expression> right) - { - var invokedExpr = Expression.Invoke(right, left.Parameters); - return Expression.Lambda>(Expression.OrElse(left.Body, invokedExpr), left.Parameters); - } - - public static Expression> And(this Expression> left, Expression> right) - { - var invokedExpr = Expression.Invoke(right, left.Parameters); - return Expression.Lambda> (Expression.AndAlso(left.Body, invokedExpr), left.Parameters); - } + public static Expression> And(this Expression> left, Expression> right) + { + InvocationExpression invokedExpr = Expression.Invoke(right, left.Parameters); + return Expression.Lambda>(Expression.AndAlso(left.Body, invokedExpr), left.Parameters); } } diff --git a/src/Umbraco.Core/Extensions/HealthCheckSettingsExtensions.cs b/src/Umbraco.Core/Extensions/HealthCheckSettingsExtensions.cs new file mode 100644 index 0000000000..a029963805 --- /dev/null +++ b/src/Umbraco.Core/Extensions/HealthCheckSettingsExtensions.cs @@ -0,0 +1,27 @@ +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Extensions; + +// TODO (V12): Remove this class that's no longer used. + +[Obsolete("Please use RecurringHostedServiceBase.GetDelay(). This class is no longer used within Umbraco and will be removed in V12.")] +public static class HealthCheckSettingsExtensions +{ + public static TimeSpan GetNotificationDelay(this HealthChecksSettings settings, ICronTabParser cronTabParser, DateTime now, TimeSpan defaultDelay) + { + // If first run time not set, start with just small delay after application start. + var firstRunTime = settings.Notification.FirstRunTime; + if (string.IsNullOrEmpty(firstRunTime)) + { + return defaultDelay; + } + + // Otherwise start at scheduled time according to cron expression, unless within the default delay period. + DateTime firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now); + TimeSpan delay = firstRunOccurance - now; + return delay < defaultDelay + ? defaultDelay + : delay; + } +} diff --git a/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs b/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs index 944c9360b4..f1b19569ff 100644 --- a/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs +++ b/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs @@ -1,53 +1,51 @@ -using System; -using System.IO; using Microsoft.Extensions.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Extensions +namespace Umbraco.Cms.Core.Extensions; + +/// +/// Contains extension methods for the interface. +/// +public static class HostEnvironmentExtensions { + private static string? _temporaryApplicationId; + /// - /// Contains extension methods for the interface. + /// Maps a virtual path to a physical path to the application's content root. /// - public static class HostEnvironmentExtensions + /// + /// Generally the content root is the parent directory of the web root directory. + /// + public static string MapPathContentRoot(this IHostEnvironment hostEnvironment, string path) { - private static string? s_temporaryApplicationId; + var root = hostEnvironment.ContentRootPath; - /// - /// Maps a virtual path to a physical path to the application's content root. - /// - /// - /// Generally the content root is the parent directory of the web root directory. - /// - public static string MapPathContentRoot(this IHostEnvironment hostEnvironment, string path) + var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); + + // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX + // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did, + // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not + // absolute in the file system. This error will help us find and fix improper uses, and should be removed once + // all those uses have been found and fixed + if (newPath.StartsWith(root)) { - var root = hostEnvironment.ContentRootPath; - - var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); - - // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX - // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did, - // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not - // absolute in the file system. This error will help us find and fix improper uses, and should be removed once - // all those uses have been found and fixed - if (newPath.StartsWith(root)) - { - throw new ArgumentException("The path appears to already be fully qualified. Please remove the call to MapPathContentRoot"); - } - - return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash)); + throw new ArgumentException( + "The path appears to already be fully qualified. Please remove the call to MapPathContentRoot"); } - /// - /// Gets a temporary application id for use before the ioc container is built. - /// - public static string GetTemporaryApplicationId(this IHostEnvironment hostEnvironment) - { - if (s_temporaryApplicationId != null) - { - return s_temporaryApplicationId; - } + return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash)); + } - return s_temporaryApplicationId = hostEnvironment.ContentRootPath.GenerateHash(); + /// + /// Gets a temporary application id for use before the ioc container is built. + /// + public static string GetTemporaryApplicationId(this IHostEnvironment hostEnvironment) + { + if (_temporaryApplicationId != null) + { + return _temporaryApplicationId; } + + return _temporaryApplicationId = hostEnvironment.ContentRootPath.GenerateHash(); } } diff --git a/src/Umbraco.Core/Extensions/IfExtensions.cs b/src/Umbraco.Core/Extensions/IfExtensions.cs index b4ef60ea57..1ab908b445 100644 --- a/src/Umbraco.Core/Extensions/IfExtensions.cs +++ b/src/Umbraco.Core/Extensions/IfExtensions.cs @@ -1,60 +1,58 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +/// +/// Extension methods for 'If' checking like checking If something is null or not null +/// +public static class IfExtensions { - /// - /// Extension methods for 'If' checking like checking If something is null or not null - /// - public static class IfExtensions + /// The if not null. + /// The item. + /// The action. + /// The type + public static void IfNotNull(this TItem item, Action action) + where TItem : class { - /// The if not null. - /// The item. - /// The action. - /// The type - public static void IfNotNull(this TItem item, Action action) where TItem : class + if (item != null) { - if (item != null) - { - action(item); - } + action(item); } - - /// The if true. - /// The predicate. - /// The action. - public static void IfTrue(this bool predicate, Action action) - { - if (predicate) - { - action(); - } - } - - /// - /// Checks if the item is not null, and if so returns an action on that item, or a default value - /// - /// the result type - /// The type - /// The item. - /// The action. - /// The default value. - /// - public static TResult? IfNotNull(this TItem? item, Func action, TResult? defaultValue = default(TResult)) - where TItem : class - => item != null ? action(item) : defaultValue; - - /// - /// Checks if the value is null, if it is it returns the value specified, otherwise returns the non-null value - /// - /// - /// - /// - /// - public static TItem IfNull(this TItem? item, Func action) - where TItem : class - => item ?? action(item!); } + + /// The if true. + /// The predicate. + /// The action. + public static void IfTrue(this bool predicate, Action action) + { + if (predicate) + { + action(); + } + } + + /// + /// Checks if the item is not null, and if so returns an action on that item, or a default value + /// + /// the result type + /// The type + /// The item. + /// The action. + /// The default value. + /// + public static TResult? IfNotNull(this TItem? item, Func action, TResult? defaultValue = default) + where TItem : class + => item != null ? action(item) : defaultValue; + + /// + /// Checks if the value is null, if it is it returns the value specified, otherwise returns the non-null value + /// + /// + /// + /// + /// + public static TItem IfNull(this TItem? item, Func action) + where TItem : class + => item ?? action(item!); } diff --git a/src/Umbraco.Core/Extensions/IntExtensions.cs b/src/Umbraco.Core/Extensions/IntExtensions.cs index 4f79baa3f5..d347993dd0 100644 --- a/src/Umbraco.Core/Extensions/IntExtensions.cs +++ b/src/Umbraco.Core/Extensions/IntExtensions.cs @@ -1,35 +1,34 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +public static class IntExtensions { - public static class IntExtensions + /// + /// Does something 'x' amount of times + /// + /// + /// + public static void Times(this int n, Action action) { - /// - /// Does something 'x' amount of times - /// - /// - /// - public static void Times(this int n, Action action) + for (var i = 0; i < n; i++) { - for (int i = 0; i < n; i++) - { - action(i); - } - } - - /// - /// Creates a Guid based on an integer value - /// - /// value to convert - /// - public static Guid ToGuid(this int value) - { - byte[] bytes = new byte[16]; - BitConverter.GetBytes(value).CopyTo(bytes, 0); - return new Guid(bytes); + action(i); } } + + /// + /// Creates a Guid based on an integer value + /// + /// value to convert + /// + /// + /// + public static Guid ToGuid(this int value) + { + var bytes = new byte[16]; + BitConverter.GetBytes(value).CopyTo(bytes, 0); + return new Guid(bytes); + } } diff --git a/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs b/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs index 73927f7a41..7189c4cc15 100644 --- a/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs +++ b/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs @@ -1,23 +1,20 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +/// +/// Provides extension methods for the struct. +/// +public static class KeyValuePairExtensions { /// - /// Provides extension methods for the struct. + /// Implements key/value pair deconstruction. /// - public static class KeyValuePairExtensions + /// Allows for foreach ((var k, var v) in ...). + public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { - /// - /// Implements key/value pair deconstruction. - /// - /// Allows for foreach ((var k, var v) in ...). - public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) - { - key = kvp.Key; - value = kvp.Value; - } + key = kvp.Key; + value = kvp.Value; } } diff --git a/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs b/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs index 2c46271964..f9aec08a61 100644 --- a/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs +++ b/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs @@ -1,16 +1,15 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class MediaTypeExtensions { - public static class MediaTypeExtensions - { - public static bool IsSystemMediaType(this IMediaType mediaType) => - mediaType.Alias == Constants.Conventions.MediaTypes.File - || mediaType.Alias == Constants.Conventions.MediaTypes.Folder - || mediaType.Alias == Constants.Conventions.MediaTypes.Image; - } + public static bool IsSystemMediaType(this IMediaType mediaType) => + mediaType.Alias == Constants.Conventions.MediaTypes.File + || mediaType.Alias == Constants.Conventions.MediaTypes.Folder + || mediaType.Alias == Constants.Conventions.MediaTypes.Image; } diff --git a/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs b/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs index a07abfbd96..f8fdcdc83f 100644 --- a/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs +++ b/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs @@ -1,43 +1,39 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; +using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class NameValueCollectionExtensions { - public static class NameValueCollectionExtensions + public static IEnumerable> AsEnumerable(this NameValueCollection nvc) { - public static IEnumerable> AsEnumerable(this NameValueCollection nvc) + foreach (var key in nvc.AllKeys) { - foreach (string? key in nvc.AllKeys) - { - yield return new KeyValuePair(key, nvc[key]); - } - } - - public static bool ContainsKey(this NameValueCollection collection, string key) - { - return collection.Keys.Cast().Any(k => (string) k == key); - } - - public static T? GetValue(this NameValueCollection collection, string key, T defaultIfNotFound) - { - if (collection.ContainsKey(key) == false) - { - return defaultIfNotFound; - } - - var val = collection[key]; - if (val == null) - { - return defaultIfNotFound; - } - - var result = val.TryConvertTo(); - - return result.Success ? result.Result : defaultIfNotFound; + yield return new KeyValuePair(key, nvc[key]); } } + + public static bool ContainsKey(this NameValueCollection collection, string key) => + collection.Keys.Cast().Any(k => (string)k == key); + + public static T? GetValue(this NameValueCollection collection, string key, T defaultIfNotFound) + { + if (collection.ContainsKey(key) == false) + { + return defaultIfNotFound; + } + + var val = collection[key]; + if (val == null) + { + return defaultIfNotFound; + } + + Attempt result = val.TryConvertTo(); + + return result.Success ? result.Result : defaultIfNotFound; + } } diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index 1ba7e0fc4d..6dc220446b 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -1,13 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.ComponentModel; -using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; @@ -15,767 +11,856 @@ using System.Xml; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides object extension methods. +/// +public static class ObjectExtensions { + private static readonly ConcurrentDictionary NullableGenericCache = new(); + private static readonly ConcurrentDictionary InputTypeConverterCache = new(); + + private static readonly ConcurrentDictionary DestinationTypeConverterCache = + new(); + + private static readonly ConcurrentDictionary AssignableTypeCache = new(); + private static readonly ConcurrentDictionary BoolConvertCache = new(); + + private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; + private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new(); + + // private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); + /// - /// Provides object extension methods. /// - public static class ObjectExtensions + /// + /// + /// + public static IEnumerable AsEnumerableOfOne(this T input) => Enumerable.Repeat(input, 1); + + /// + /// + /// + public static void DisposeIfDisposable(this object input) { - private static readonly ConcurrentDictionary NullableGenericCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary InputTypeConverterCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary DestinationTypeConverterCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary AssignableTypeCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary BoolConvertCache = new ConcurrentDictionary(); - - private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; - private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter(); - - //private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); - - /// - /// - /// - /// - /// - /// - public static IEnumerable AsEnumerableOfOne(this T input) + if (input is IDisposable disposable) { - return Enumerable.Repeat(input, 1); + disposable.Dispose(); } + } - /// - /// - /// - /// - public static void DisposeIfDisposable(this object input) + /// + /// Provides a shortcut way of safely casting an input when you cannot guarantee the is + /// an instance type (i.e., when the C# AS keyword is not applicable). + /// + /// + /// The input. + /// + public static T? SafeCast(this object input) + { + if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) { - if (input is IDisposable disposable) - disposable.Dispose(); - } - - /// - /// Provides a shortcut way of safely casting an input when you cannot guarantee the is - /// an instance type (i.e., when the C# AS keyword is not applicable). - /// - /// - /// The input. - /// - public static T? SafeCast(this object input) - { - if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default; - if (input is T variable) return variable; return default; } - /// - /// Attempts to convert the input object to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The type to convert to - /// The input. - /// The - public static Attempt TryConvertTo(this object? input) + if (input is T variable) { - Attempt result = TryConvertTo(input, typeof(T)); - - if (result.Success) - { - return Attempt.Succeed((T?)result.Result); - } - - if (input == null) - { - if (typeof(T).IsValueType) - { - // fail, cannot convert null to a value type - return Attempt.Fail(); - } - else - { - // sure, null can be any object - return Attempt.Succeed((T)input!); - } - } - - // just try to cast - try - { - return Attempt.Succeed((T)input); - } - catch (Exception e) - { - return Attempt.Fail(e); - } + return variable; } - /// - /// Attempts to convert the input object to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The input. - /// The type to convert to - /// The - public static Attempt TryConvertTo(this object? input, Type target) + return default; + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The type to convert to + /// The input. + /// The + public static Attempt TryConvertTo(this object? input) + { + Attempt result = TryConvertTo(input, typeof(T)); + + if (result.Success) { - if (target == null) + return Attempt.Succeed((T?)result.Result); + } + + if (input == null) + { + if (typeof(T).IsValueType) { - return Attempt.Fail(); + // fail, cannot convert null to a value type + return Attempt.Fail(); } - try + // sure, null can be any object + return Attempt.Succeed((T)input!); + } + + // just try to cast + try + { + return Attempt.Succeed((T)input); + } + catch (Exception e) + { + return Attempt.Fail(e); + } + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + public static Attempt TryConvertTo(this object? input, Type target) + { + if (target == null) + { + return Attempt.Fail(); + } + + try + { + if (input == null) { - if (input == null) - { - // Nullable is ok - if (target.IsGenericType && GetCachedGenericNullableType(target) != null) - { - return Attempt.Succeed(null); - } - - // Reference types are ok - return Attempt.If(target.IsValueType == false, null); - } - - var inputType = input.GetType(); - - // Easy - if (target == typeof(object) || inputType == target) - { - return Attempt.Succeed(input); - } - - // Check for string so that overloaders of ToString() can take advantage of the conversion. - if (target == typeof(string)) - { - return Attempt.Succeed(input.ToString()); - } - - // If we've got a nullable of something, we try to convert directly to that thing. - // We cache the destination type and underlying nullable types - // Any other generic types need to fall through - if (target.IsGenericType) - { - var underlying = GetCachedGenericNullableType(target); - if (underlying != null) - { - // Special case for empty strings for bools/dates which should return null if an empty string. - if (input is string inputString) - { - // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? - if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool))) - { - return Attempt.Succeed(null); - } - } - - // Recursively call into this method with the inner (not-nullable) type and handle the outcome - var inner = input.TryConvertTo(underlying); - - // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception - if (inner.Success) - { - input = inner.Result; // Now fall on through... - } - else - { - return Attempt.Fail(inner.Exception); - } - } - } - else - { - // target is not a generic type - - if (input is string inputString) - { - // Try convert from string, returns an Attempt if the string could be - // processed (either succeeded or failed), else null if we need to try - // other methods - var result = TryConvertToFromString(inputString, target); - if (result.HasValue) - { - return result.Value; - } - } - - // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with - // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. - if (GetCachedCanAssign(input, inputType, target)) - { - return Attempt.Succeed(Convert.ChangeType(input, target)); - } - } - - if (target == typeof(bool)) - { - if (GetCachedCanConvertToBoolean(inputType)) - { - return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input!)); - } - } - - var inputConverter = GetCachedSourceTypeConverter(inputType, target); - if (inputConverter != null) - { - return Attempt.Succeed(inputConverter.ConvertTo(input, target)); - } - - var outputConverter = GetCachedTargetTypeConverter(inputType, target); - if (outputConverter != null) - { - return Attempt.Succeed(outputConverter.ConvertFrom(input!)); - } - + // Nullable is ok if (target.IsGenericType && GetCachedGenericNullableType(target) != null) { - // cannot Convert.ChangeType as that does not work with nullable - // input has already been converted to the underlying type - just - // return input, there's an implicit conversion from T to T? anyways - return Attempt.Succeed(input); + return Attempt.Succeed(null); } - // Re-check convertibles since we altered the input through recursion - if (input is IConvertible convertible2) + // Reference types are ok + return Attempt.If(target.IsValueType == false, null); + } + + Type inputType = input.GetType(); + + // Easy + if (target == typeof(object) || inputType == target) + { + return Attempt.Succeed(input); + } + + // Check for string so that overloaders of ToString() can take advantage of the conversion. + if (target == typeof(string)) + { + return Attempt.Succeed(input.ToString()); + } + + // If we've got a nullable of something, we try to convert directly to that thing. + // We cache the destination type and underlying nullable types + // Any other generic types need to fall through + if (target.IsGenericType) + { + Type? underlying = GetCachedGenericNullableType(target); + if (underlying != null) { - return Attempt.Succeed(Convert.ChangeType(convertible2, target)); + // Special case for empty strings for bools/dates which should return null if an empty string. + if (input is string inputString) + { + // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? + if (string.IsNullOrEmpty(inputString) && + (underlying == typeof(DateTime) || underlying == typeof(bool))) + { + return Attempt.Succeed(null); + } + } + + // Recursively call into this method with the inner (not-nullable) type and handle the outcome + Attempt inner = input.TryConvertTo(underlying); + + // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception + if (inner.Success) + { + input = inner.Result; // Now fall on through... + } + else + { + return Attempt.Fail(inner.Exception); + } } } - catch (Exception e) + else { - return Attempt.Fail(e); + // target is not a generic type + if (input is string inputString) + { + // Try convert from string, returns an Attempt if the string could be + // processed (either succeeded or failed), else null if we need to try + // other methods + Attempt? result = TryConvertToFromString(inputString, target); + if (result.HasValue) + { + return result.Value; + } + } + + // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with + // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. + if (GetCachedCanAssign(input, inputType, target)) + { + return Attempt.Succeed(Convert.ChangeType(input, target)); + } + } + + if (target == typeof(bool)) + { + if (GetCachedCanConvertToBoolean(inputType)) + { + return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input!)); + } + } + + TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target); + if (inputConverter != null) + { + return Attempt.Succeed(inputConverter.ConvertTo(input, target)); + } + + TypeConverter? outputConverter = GetCachedTargetTypeConverter(inputType, target); + if (outputConverter != null) + { + return Attempt.Succeed(outputConverter.ConvertFrom(input!)); + } + + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) + { + // cannot Convert.ChangeType as that does not work with nullable + // input has already been converted to the underlying type - just + // return input, there's an implicit conversion from T to T? anyways + return Attempt.Succeed(input); + } + + // Re-check convertibles since we altered the input through recursion + if (input is IConvertible convertible2) + { + return Attempt.Succeed(Convert.ChangeType(convertible2, target)); + } + } + catch (Exception e) + { + return Attempt.Fail(e); + } + + return Attempt.Fail(); + } + + // public enum PropertyNamesCaseType + // { + // CamelCase, + // CaseInsensitive + // } + + ///// + ///// Convert an object to a JSON string with camelCase formatting + ///// + ///// + ///// + // public static string ToJsonString(this object obj) + // { + // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); + // } + + ///// + ///// Convert an object to a JSON string with the specified formatting + ///// + ///// The obj. + ///// Type of the property names case. + ///// + // public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) + // { + // var type = obj.GetType(); + // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; + + // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) + // { + // return obj.ToString(); + // } + + // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) + // { + // return Convert.ToDateTime(obj).ToString(dateTimeStyle); + // } + + // var serializer = new JsonSerializer(); + + // switch (propertyNamesCaseType) + // { + // case PropertyNamesCaseType.CamelCase: + // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); + // break; + // } + + // var dateTimeConverter = new IsoDateTimeConverter + // { + // DateTimeStyles = System.Globalization.DateTimeStyles.None, + // DateTimeFormat = dateTimeStyle + // }; + + // if (typeof(IDictionary).IsAssignableFrom(type)) + // { + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) + // { + // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + /// + /// Converts an object into a dictionary + /// + /// + /// + /// + /// + /// + /// + public static IDictionary? ToDictionary( + this T o, + params Expression>[] ignoreProperties) => o?.ToDictionary(ignoreProperties + .Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); + + internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) + { + // TODO: Localize this exception + if (isDisposed) + { + throw new ObjectDisposedException(objectname); + } + } + + /// + /// Attempts to convert the input string to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + private static Attempt? TryConvertToFromString(this string input, Type target) + { + // Easy + if (target == typeof(string)) + { + return Attempt.Succeed(input); + } + + // Null, empty, whitespaces + if (string.IsNullOrWhiteSpace(input)) + { + if (target == typeof(bool)) + { + // null/empty = bool false + return Attempt.Succeed(false); + } + + if (target == typeof(DateTime)) + { + // null/empty = min DateTime value + return Attempt.Succeed(DateTime.MinValue); + } + + // Cannot decide here, + // Any of the types below will fail parsing and will return a failed attempt + // but anything else will not be processed and will return null + // so even though the string is null/empty we have to proceed. + } + + // Look for type conversions in the expected order of frequency of use. + // + // By using a mixture of ordered if statements and switches we can optimize both for + // fast conditional checking for most frequently used types and the branching + // that does not depend on previous values available to switch statements. + if (target.IsPrimitive) + { + if (target == typeof(int)) + { + if (int.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Because decimal 100.01m will happily convert to integer 100, it + // makes sense that string "100.01" *also* converts to integer 100. + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); + } + + if (target == typeof(long)) + { + if (long.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Same as int + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); + } + + // TODO: Should we do the decimal trick for short, byte, unsigned? + if (target == typeof(bool)) + { + if (bool.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Don't declare failure so the CustomBooleanTypeConverter can try + return null; + } + + // Calling this method directly is faster than any attempt to cache it. + switch (Type.GetTypeCode(target)) + { + case TypeCode.Int16: + return Attempt.If(short.TryParse(input, out var value), value); + + case TypeCode.Double: + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(double.TryParse(input2, out var valueD), valueD); + + case TypeCode.Single: + var input3 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(float.TryParse(input3, out var valueF), valueF); + + case TypeCode.Char: + return Attempt.If(char.TryParse(input, out var valueC), valueC); + + case TypeCode.Byte: + return Attempt.If(byte.TryParse(input, out var valueB), valueB); + + case TypeCode.SByte: + return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); + + case TypeCode.UInt32: + return Attempt.If(uint.TryParse(input, out var valueU), valueU); + + case TypeCode.UInt16: + return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); + + case TypeCode.UInt64: + return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); + } + } + else if (target == typeof(Guid)) + { + return Attempt.If(Guid.TryParse(input, out Guid value), value); + } + else if (target == typeof(DateTime)) + { + if (DateTime.TryParse(input, out DateTime value)) + { + switch (value.Kind) + { + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + return Attempt.Succeed(value); + + case DateTimeKind.Local: + return Attempt.Succeed(value.ToUniversalTime()); + + default: + throw new ArgumentOutOfRangeException(); + } } return Attempt.Fail(); } - - /// - /// Attempts to convert the input string to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The input. - /// The type to convert to - /// The - private static Attempt? TryConvertToFromString(this string input, Type target) + else if (target == typeof(DateTimeOffset)) { - // Easy - if (target == typeof(string)) + return Attempt.If(DateTimeOffset.TryParse(input, out DateTimeOffset value), value); + } + else if (target == typeof(TimeSpan)) + { + return Attempt.If(TimeSpan.TryParse(input, out TimeSpan value), value); + } + else if (target == typeof(decimal)) + { + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value), value); + } + else if (input != null && target == typeof(Version)) + { + return Attempt.If(Version.TryParse(input, out Version? value), value); + } + + // E_NOTIMPL IPAddress, BigInteger + return null; // we can't decide... + } + + /// + /// Turns object into dictionary + /// + /// + /// Properties to ignore + /// + public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + { + if (o != null) + { + PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o); + var d = new Dictionary(); + foreach (PropertyDescriptor prop in props.Cast() + .Where(x => ignoreProperties.Contains(x.Name) == false)) { - return Attempt.Succeed(input); - } - - // Null, empty, whitespaces - if (string.IsNullOrWhiteSpace(input)) - { - if (target == typeof(bool)) + var val = prop.GetValue(o); + if (val != null) { - // null/empty = bool false - return Attempt.Succeed(false); - } - - if (target == typeof(DateTime)) - { - // null/empty = min DateTime value - return Attempt.Succeed(DateTime.MinValue); - } - - // Cannot decide here, - // Any of the types below will fail parsing and will return a failed attempt - // but anything else will not be processed and will return null - // so even though the string is null/empty we have to proceed. - } - - // Look for type conversions in the expected order of frequency of use. - // - // By using a mixture of ordered if statements and switches we can optimize both for - // fast conditional checking for most frequently used types and the branching - // that does not depend on previous values available to switch statements. - if (target.IsPrimitive) - { - if (target == typeof(int)) - { - if (int.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Because decimal 100.01m will happily convert to integer 100, it - // makes sense that string "100.01" *also* converts to integer 100. - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); - } - - if (target == typeof(long)) - { - if (long.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Same as int - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); - } - - // TODO: Should we do the decimal trick for short, byte, unsigned? - - if (target == typeof(bool)) - { - if (bool.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Don't declare failure so the CustomBooleanTypeConverter can try - return null; - } - - // Calling this method directly is faster than any attempt to cache it. - switch (Type.GetTypeCode(target)) - { - case TypeCode.Int16: - return Attempt.If(short.TryParse(input, out var value), value); - - case TypeCode.Double: - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(double.TryParse(input2, out var valueD), valueD); - - case TypeCode.Single: - var input3 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(float.TryParse(input3, out var valueF), valueF); - - case TypeCode.Char: - return Attempt.If(char.TryParse(input, out var valueC), valueC); - - case TypeCode.Byte: - return Attempt.If(byte.TryParse(input, out var valueB), valueB); - - case TypeCode.SByte: - return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); - - case TypeCode.UInt32: - return Attempt.If(uint.TryParse(input, out var valueU), valueU); - - case TypeCode.UInt16: - return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); - - case TypeCode.UInt64: - return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); + d.Add(prop.Name, (TVal)val); } } - else if (target == typeof(Guid)) - { - return Attempt.If(Guid.TryParse(input, out var value), value); - } - else if (target == typeof(DateTime)) - { - if (DateTime.TryParse(input, out var value)) - { - switch (value.Kind) - { - case DateTimeKind.Unspecified: - case DateTimeKind.Utc: - return Attempt.Succeed(value); - case DateTimeKind.Local: - return Attempt.Succeed(value.ToUniversalTime()); - - default: - throw new ArgumentOutOfRangeException(); - } - } - - return Attempt.Fail(); - } - else if (target == typeof(DateTimeOffset)) - { - return Attempt.If(DateTimeOffset.TryParse(input, out var value), value); - } - else if (target == typeof(TimeSpan)) - { - return Attempt.If(TimeSpan.TryParse(input, out var value), value); - } - else if (target == typeof(decimal)) - { - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value), value); - } - else if (input != null && target == typeof(Version)) - { - return Attempt.If(Version.TryParse(input, out var value), value); - } - - // E_NOTIMPL IPAddress, BigInteger - return null; // we can't decide... - } - internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) - { - // TODO: Localize this exception - if (isDisposed) - throw new ObjectDisposedException(objectname); + return d; } - //public enum PropertyNamesCaseType - //{ - // CamelCase, - // CaseInsensitive - //} + return new Dictionary(); + } - ///// - ///// Convert an object to a JSON string with camelCase formatting - ///// - ///// - ///// - //public static string ToJsonString(this object obj) - //{ - // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); - //} - - ///// - ///// Convert an object to a JSON string with the specified formatting - ///// - ///// The obj. - ///// Type of the property names case. - ///// - //public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) - //{ - // var type = obj.GetType(); - // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; - - // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) - // { - // return obj.ToString(); - // } - - // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) - // { - // return Convert.ToDateTime(obj).ToString(dateTimeStyle); - // } - - // var serializer = new JsonSerializer(); - - // switch (propertyNamesCaseType) - // { - // case PropertyNamesCaseType.CamelCase: - // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); - // break; - // } - - // var dateTimeConverter = new IsoDateTimeConverter - // { - // DateTimeStyles = System.Globalization.DateTimeStyles.None, - // DateTimeFormat = dateTimeStyle - // }; - - // if (typeof(IDictionary).IsAssignableFrom(type)) - // { - // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - // } - - // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) - // { - // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - // } - - // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - //} - - /// - /// Converts an object into a dictionary - /// - /// - /// - /// - /// - /// - /// - public static IDictionary? ToDictionary(this T o, params Expression>[] ignoreProperties) + /// + /// Returns an XmlSerialized safe string representation for the value + /// + /// + /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown + /// + public static string ToXmlString(this object value, Type type) + { + if (value == null) { - return o?.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); + return string.Empty; } - /// - /// Turns object into dictionary - /// - /// - /// Properties to ignore - /// - public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + if (type == typeof(string)) { - if (o != null) + return value.ToString().IsNullOrWhiteSpace() ? string.Empty : value.ToString()!; + } + + if (type == typeof(bool)) + { + return XmlConvert.ToString((bool)value); + } + + if (type == typeof(byte)) + { + return XmlConvert.ToString((byte)value); + } + + if (type == typeof(char)) + { + return XmlConvert.ToString((char)value); + } + + if (type == typeof(DateTime)) + { + return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); + } + + if (type == typeof(DateTimeOffset)) + { + return XmlConvert.ToString((DateTimeOffset)value); + } + + if (type == typeof(decimal)) + { + return XmlConvert.ToString((decimal)value); + } + + if (type == typeof(double)) + { + return XmlConvert.ToString((double)value); + } + + if (type == typeof(float)) + { + return XmlConvert.ToString((float)value); + } + + if (type == typeof(Guid)) + { + return XmlConvert.ToString((Guid)value); + } + + if (type == typeof(int)) + { + return XmlConvert.ToString((int)value); + } + + if (type == typeof(long)) + { + return XmlConvert.ToString((long)value); + } + + if (type == typeof(sbyte)) + { + return XmlConvert.ToString((sbyte)value); + } + + if (type == typeof(short)) + { + return XmlConvert.ToString((short)value); + } + + if (type == typeof(TimeSpan)) + { + return XmlConvert.ToString((TimeSpan)value); + } + + if (type == typeof(uint)) + { + return XmlConvert.ToString((uint)value); + } + + if (type == typeof(ulong)) + { + return XmlConvert.ToString((ulong)value); + } + + if (type == typeof(ushort)) + { + return XmlConvert.ToString((ushort)value); + } + + throw new NotSupportedException("Cannot convert type " + type.FullName + + " to a string using ToXmlString as it is not supported by XmlConvert"); + } + + internal static string? ToDebugString(this object? obj, int levels = 0) + { + if (obj == null) + { + return "{null}"; + } + + try + { + if (obj is string) { - var props = TypeDescriptor.GetProperties(o); - var d = new Dictionary(); - foreach (var prop in props.Cast().Where(x => ignoreProperties.Contains(x.Name) == false)) - { - var val = prop.GetValue(o); - if (val != null) - { - d.Add(prop.Name, (TVal)val); - } - } - return d; + return "\"{0}\"".InvariantFormat(obj); } - return new Dictionary(); - } - - - internal static string? ToDebugString(this object? obj, int levels = 0) - { - if (obj == null) return "{null}"; - try + if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || + obj is int? || obj is short? || obj is long? || obj is float? || obj is double? || obj is bool?) { - if (obj is string) - { - return "\"{0}\"".InvariantFormat(obj); - } - if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || obj is int? || obj is short? || obj is long? || obj is float? || obj is double? || obj is bool?) - { - return "{0}".InvariantFormat(obj); - } - if (obj is Enum) - { - return "[{0}]".InvariantFormat(obj); - } - if (obj is IEnumerable enumerable) - { - var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList(); + return "{0}".InvariantFormat(obj); + } - return items.Any() - ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) - : null; - } + if (obj is Enum) + { + return "[{0}]".InvariantFormat(obj); + } - var props = obj.GetType().GetProperties(); - if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) - { - try - { - var key = props[0].GetValue(obj, null) as string; - var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); - return "{0}={1}".InvariantFormat(key, value); - } - catch (Exception) - { - return "[KeyValuePropertyException]"; - } - } - if (levels > -1) - { - var items = - (from propertyInfo in props - let value = GetPropertyDebugString(propertyInfo, obj, levels) - where value != null - select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); + if (obj is IEnumerable enumerable) + { + var items = (from object enumItem in enumerable + let value = GetEnumPropertyDebugString(enumItem, levels) + where value != null + select value).Take(10).ToList(); - return items.Any() - ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) - : null; + return items.Any() + ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) + : null; + } + + PropertyInfo[] props = obj.GetType().GetProperties(); + if (props.Length == 2 && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) + { + try + { + var key = props[0].GetValue(obj, null) as string; + var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); + return "{0}={1}".InvariantFormat(key, value); + } + catch (Exception) + { + return "[KeyValuePropertyException]"; } } - catch (Exception ex) - { - return "[Exception:{0}]".InvariantFormat(ex.Message); - } - return null; - } - /// - /// Attempts to serialize the value to an XmlString using ToXmlString - /// - /// - /// - /// - internal static Attempt TryConvertToXmlString(this object value, Type type) + if (levels > -1) + { + var items = + (from propertyInfo in props + let value = GetPropertyDebugString(propertyInfo, obj, levels) + where value != null + select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); + + return items.Any() + ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, string.Join(", ", items)) + : null; + } + } + catch (Exception ex) { - try - { - var output = value.ToXmlString(type); - return Attempt.Succeed(output); - } - catch (NotSupportedException ex) - { - return Attempt.Fail(ex); - } + return "[Exception:{0}]".InvariantFormat(ex.Message); } - /// - /// Returns an XmlSerialized safe string representation for the value - /// - /// - /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown - /// - public static string ToXmlString(this object value, Type type) + return null; + } + + /// + /// Attempts to serialize the value to an XmlString using ToXmlString + /// + /// + /// + /// + internal static Attempt TryConvertToXmlString(this object value, Type type) + { + try { - if (value == null) return string.Empty; - if (type == typeof(string)) return (value.ToString().IsNullOrWhiteSpace() ? "" : value.ToString()!); - if (type == typeof(bool)) return XmlConvert.ToString((bool)value); - if (type == typeof(byte)) return XmlConvert.ToString((byte)value); - if (type == typeof(char)) return XmlConvert.ToString((char)value); - if (type == typeof(DateTime)) return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); - if (type == typeof(DateTimeOffset)) return XmlConvert.ToString((DateTimeOffset)value); - if (type == typeof(decimal)) return XmlConvert.ToString((decimal)value); - if (type == typeof(double)) return XmlConvert.ToString((double)value); - if (type == typeof(float)) return XmlConvert.ToString((float)value); - if (type == typeof(Guid)) return XmlConvert.ToString((Guid)value); - if (type == typeof(int)) return XmlConvert.ToString((int)value); - if (type == typeof(long)) return XmlConvert.ToString((long)value); - if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value); - if (type == typeof(short)) return XmlConvert.ToString((short)value); - if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value); - if (type == typeof(uint)) return XmlConvert.ToString((uint)value); - if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value); - if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value); - - throw new NotSupportedException("Cannot convert type " + type.FullName + " to a string using ToXmlString as it is not supported by XmlConvert"); + var output = value.ToXmlString(type); + return Attempt.Succeed(output); } - - /// - /// Returns an XmlSerialized safe string representation for the value and type - /// - /// - /// - /// - public static string ToXmlString(this object value) + catch (NotSupportedException ex) { - return value.ToXmlString(typeof (T)); + return Attempt.Fail(ex); } + } - private static string? GetEnumPropertyDebugString(object enumItem, int levels) + /// + /// Returns an XmlSerialized safe string representation for the value and type + /// + /// + /// + /// + public static string ToXmlString(this object value) => value.ToXmlString(typeof(T)); + + public static Guid AsGuid(this object value) => value is Guid guid ? guid : Guid.Empty; + + private static string? GetEnumPropertyDebugString(object enumItem, int levels) + { + try { - try - { - return enumItem.ToDebugString(levels - 1); - } - catch (Exception) - { - return "[GetEnumPartException]"; - } + return enumItem.ToDebugString(levels - 1); } - - private static string? GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) + catch (Exception) { - try - { - return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); - } - catch (Exception) - { - return "[GetPropertyValueException]"; - } + return "[GetEnumPartException]"; } + } - public static Guid AsGuid(this object value) + private static string? GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) + { + try { - return value is Guid guid ? guid : Guid.Empty; + return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string NormalizeNumberDecimalSeparator(string s) + catch (Exception) { - var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; - return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + return "[GetPropertyValueException]"; } + } - // gets a converter for source, that can convert to target, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TypeConverter? GetCachedSourceTypeConverter(Type source, Type target) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string NormalizeNumberDecimalSeparator(string s) + { + var normalized = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; + return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + } + + // gets a converter for source, that can convert to target, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter? GetCachedSourceTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (InputTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) { - var key = new CompositeTypeTypeKey(source, target); - - if (InputTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } - - var converter = TypeDescriptor.GetConverter(source); - if (converter.CanConvertTo(target)) - { - return InputTypeConverterCache[key] = converter; - } - - InputTypeConverterCache[key] = null; - return null; + return typeConverter; } - // gets a converter for target, that can convert from source, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TypeConverter? GetCachedTargetTypeConverter(Type source, Type target) + TypeConverter converter = TypeDescriptor.GetConverter(source); + if (converter.CanConvertTo(target)) { - var key = new CompositeTypeTypeKey(source, target); - - if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } - - var converter = TypeDescriptor.GetConverter(target); - if (converter.CanConvertFrom(source)) - { - return DestinationTypeConverterCache[key] = converter; - } - - DestinationTypeConverterCache[key] = null; - return null; + return InputTypeConverterCache[key] = converter; } - // gets the underlying type of a nullable type, or null if the type is not nullable - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Type? GetCachedGenericNullableType(Type type) + InputTypeConverterCache[key] = null; + return null; + } + + // gets a converter for target, that can convert from source, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter? GetCachedTargetTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (DestinationTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) { - if (NullableGenericCache.TryGetValue(type, out var underlyingType)) - { - return underlyingType; - } - - if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - Type? underlying = Nullable.GetUnderlyingType(type); - return NullableGenericCache[type] = underlying; - } - - NullableGenericCache[type] = null; - return null; + return typeConverter; } - // gets an IConvertible from source to target type, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetCachedCanAssign(object input, Type source, Type target) + TypeConverter converter = TypeDescriptor.GetConverter(target); + if (converter.CanConvertFrom(source)) { - var key = new CompositeTypeTypeKey(source, target); - if (AssignableTypeCache.TryGetValue(key, out var canConvert)) - { - return canConvert; - } - - // "object is" is faster than "Type.IsAssignableFrom. - // We can use it to very quickly determine whether true/false - if (input is IConvertible && target.IsAssignableFrom(source)) - { - return AssignableTypeCache[key] = true; - } - - return AssignableTypeCache[key] = false; + return DestinationTypeConverterCache[key] = converter; } - // determines whether a type can be converted to boolean - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetCachedCanConvertToBoolean(Type type) + DestinationTypeConverterCache[key] = null; + return null; + } + + // gets the underlying type of a nullable type, or null if the type is not nullable + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Type? GetCachedGenericNullableType(Type type) + { + if (NullableGenericCache.TryGetValue(type, out Type? underlyingType)) { - if (BoolConvertCache.TryGetValue(type, out var result)) - { - return result; - } - - if (CustomBooleanTypeConverter.CanConvertFrom(type)) - { - return BoolConvertCache[type] = true; - } - - return BoolConvertCache[type] = false; + return underlyingType; } + if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + Type? underlying = Nullable.GetUnderlyingType(type); + return NullableGenericCache[type] = underlying; + } + NullableGenericCache[type] = null; + return null; + } + + // gets an IConvertible from source to target type, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanAssign(object input, Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + if (AssignableTypeCache.TryGetValue(key, out var canConvert)) + { + return canConvert; + } + + // "object is" is faster than "Type.IsAssignableFrom. + // We can use it to very quickly determine whether true/false + if (input is IConvertible && target.IsAssignableFrom(source)) + { + return AssignableTypeCache[key] = true; + } + + return AssignableTypeCache[key] = false; + } + + // determines whether a type can be converted to boolean + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanConvertToBoolean(Type type) + { + if (BoolConvertCache.TryGetValue(type, out var result)) + { + return result; + } + + if (CustomBooleanTypeConverter.CanConvertFrom(type)) + { + return BoolConvertCache[type] = true; + } + + return BoolConvertCache[type] = false; } } diff --git a/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs b/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs index 0c15da6bdb..61b284383a 100644 --- a/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs +++ b/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs @@ -1,41 +1,34 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class PasswordConfigurationExtensions { - public static class PasswordConfigurationExtensions - { - /// - /// Returns the configuration of the membership provider used to configure change password editors - /// - /// - /// - /// - public static IDictionary GetConfiguration( - this IPasswordConfiguration passwordConfiguration, - bool allowManuallyChangingPassword = false) + /// + /// Returns the configuration of the membership provider used to configure change password editors + /// + /// + /// + /// + public static IDictionary GetConfiguration( + this IPasswordConfiguration passwordConfiguration, + bool allowManuallyChangingPassword = false) => + new Dictionary { - return new Dictionary - { - {"minPasswordLength", passwordConfiguration.RequiredLength}, + { "minPasswordLength", passwordConfiguration.RequiredLength }, - // TODO: This doesn't make a ton of sense with asp.net identity and also there's a bunch of other settings - // that we can consider with IPasswordConfiguration, but these are currently still based on how membership providers worked. - {"minNonAlphaNumericChars", passwordConfiguration.GetMinNonAlphaNumericChars()}, + // TODO: This doesn't make a ton of sense with asp.net identity and also there's a bunch of other settings + // that we can consider with IPasswordConfiguration, but these are currently still based on how membership providers worked. + { "minNonAlphaNumericChars", passwordConfiguration.GetMinNonAlphaNumericChars() }, - // A flag to indicate if the current password box should be shown or not, only a user that has access to change other user/member passwords - // doesn't have to specify the current password for the user/member. A user changing their own password must specify their current password. - {"allowManuallyChangingPassword", allowManuallyChangingPassword}, - }; - } + // A flag to indicate if the current password box should be shown or not, only a user that has access to change other user/member passwords + // doesn't have to specify the current password for the user/member. A user changing their own password must specify their current password. + { "allowManuallyChangingPassword", allowManuallyChangingPassword }, + }; - public static int GetMinNonAlphaNumericChars(this IPasswordConfiguration passwordConfiguration) - { - return passwordConfiguration.RequireNonLetterOrDigit ? 2 : 0; - } - - } + public static int GetMinNonAlphaNumericChars(this IPasswordConfiguration passwordConfiguration) => + passwordConfiguration.RequireNonLetterOrDigit ? 2 : 0; } diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index 48769bda2c..f7ad53d7d6 100644 --- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs @@ -1,1377 +1,1469 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class PublishedContentExtensions { - public static class PublishedContentExtensions + #region Name + + /// + /// Gets the name of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the name for. If null is used the current culture is used (Default is + /// null). + /// + public static string? Name(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) { - #region Name - - /// - /// Gets the name of the content item. - /// - /// The content item. - /// - /// The specific culture to get the name for. If null is used the current culture is used (Default is null). - public static string? Name(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + if (content == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture()) - return content.Cultures.TryGetValue(string.Empty, out var invariantInfos) ? invariantInfos.Name : null; - - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - - // get - return culture != string.Empty && content.Cultures.TryGetValue(culture, out var infos) ? infos.Name : null; + throw new ArgumentNullException(nameof(content)); } - #endregion - - #region Url segment - - /// - /// Gets the URL segment of the content item. - /// - /// The content item. - /// - /// The specific culture to get the URL segment for. If null is used the current culture is used (Default is null). - public static string? UrlSegment(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture()) - return content.Cultures.TryGetValue("", out var invariantInfos) ? invariantInfos.UrlSegment : null; - - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? ""; - - // get - return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.UrlSegment : null; + return content.Cultures.TryGetValue(string.Empty, out PublishedCultureInfo? invariantInfos) + ? invariantInfos.Name + : null; } - #endregion - - #region Culture - - /// - /// Determines whether the content has a culture. - /// - /// Culture is case-insensitive. - public static bool HasCulture(this IPublishedContent content, string? culture) + // handle context culture for variant + if (culture == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - return content.Cultures.ContainsKey(culture ?? string.Empty); + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - /// - /// Determines whether the content is invariant, or has a culture. - /// - /// Culture is case-insensitive. - public static bool IsInvariantOrHasCulture(this IPublishedContent content, string culture) - => !content.ContentType.VariesByCulture() || content.Cultures.ContainsKey(culture ?? ""); - - /// - /// Filters a sequence of to return invariant items, and items that are published for the specified culture. - /// - /// The content items. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is null). - internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - if (contents == null) throw new ArgumentNullException(nameof(contents)); + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.Name + : null; + } - culture = culture ?? variationContextAccessor.VariationContext?.Culture ?? ""; + #endregion - // either does not vary by culture, or has the specified culture - return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); - } + #region Url segment - /// - /// Gets the culture date of the content item. - /// - /// The content item. - /// - /// The specific culture to get the name for. If null is used the current culture is used (Default is null). - public static DateTime CultureDate(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + /// + /// Gets the URL segment of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the URL segment for. If null is used the current culture is used + /// (Default is null). + /// + public static string? UrlSegment(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + { + if (content == null) { - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture()) - return content.UpdateDate; - - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? ""; - - // get - return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.Date : DateTime.MinValue; + throw new ArgumentNullException(nameof(content)); } - #endregion - - #region IsComposedOf - - /// - /// Gets a value indicating whether the content is of a content type composed of the given alias - /// - /// The content. - /// The content type alias. - /// A value indicating whether the content is of a content type composed of a content type identified by the alias. - public static bool IsComposedOf(this IPublishedContent content, string alias) + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - return content.ContentType.CompositionAliases.InvariantContains(alias); + return content.Cultures.TryGetValue(string.Empty, out PublishedCultureInfo? invariantInfos) + ? invariantInfos.UrlSegment + : null; } - #endregion - - #region Template - - /// - /// Returns the current template Alias - /// - /// Empty string if none is set. - public static string GetTemplateAlias(this IPublishedContent content, IFileService fileService) + // handle context culture for variant + if (culture == null) { - if (content.TemplateId.HasValue == false) - { - return string.Empty; - } - - var template = fileService.GetTemplate(content.TemplateId.Value); - return template?.Alias ?? string.Empty; + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, - WebRoutingSettings webRoutingSettings, int templateId) - { - return content.IsAllowedTemplate(contentTypeService, - webRoutingSettings.DisableAlternativeTemplates, - webRoutingSettings.ValidateAlternativeTemplates, templateId); - } - - public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId) - { - if (disableAlternativeTemplates) - return content.TemplateId == templateId; + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.UrlSegment + : null; + } - if (content.TemplateId == templateId || !validateAlternativeTemplates) - return true; + #endregion - var publishedContentContentType = contentTypeService.Get(content.ContentType.Id); - if (publishedContentContentType == null) - throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); + #region IsComposedOf - return publishedContentContentType.IsAllowedTemplate(templateId); - - } - public static bool IsAllowedTemplate(this IPublishedContent content, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias) - { - var template = fileService.GetTemplate(templateAlias); - return template != null && content.IsAllowedTemplate(contentTypeService, disableAlternativeTemplates, validateAlternativeTemplates, template.Id); - } + /// + /// Gets a value indicating whether the content is of a content type composed of the given alias + /// + /// The content. + /// The content type alias. + /// + /// A value indicating whether the content is of a content type composed of a content type identified by the + /// alias. + /// + public static bool IsComposedOf(this IPublishedContent content, string alias) => + content.ContentType.CompositionAliases.InvariantContains(alias); - #endregion - - #region HasValue, Value, Value - - /// - /// Gets a value indicating whether the content has a value for a property identified by its alias. - /// - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// A value indicating whether the content has a value for the property identified by the alias. - /// Returns true if HasValue is true, or a fallback strategy can provide a value. - public static bool HasValue(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default) - { - var property = content.GetProperty(alias); + #endregion - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return true; + #region Axes: parent - // else let fallback try to get a value - return publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, null, out _, out _); - } + // Parent is native - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - public static object? Value(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) + /// + /// Gets the parent of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The parent of content, of the given content type, else null. + public static T? Parent(this IPublishedContent content) + where T : class, IPublishedContent + { + if (content == null) { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.GetValue(culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value, out property)) - return value; - - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - return property?.GetValue(culture, segment); + throw new ArgumentNullException(nameof(content)); } - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type. - /// - /// The target property type. - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type. - public static T? Value(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) - { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.Value(publishedValueFallback, culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value, out property)) - return value; + return content.Parent as T; + } - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property == null ? default : property.Value(publishedValueFallback, culture, segment); - } - - #endregion + #endregion - #region IsSomething: misc. + #region Url - /// - /// Determines whether the specified content is a specified content type. - /// - /// The content to determine content type of. - /// The alias of the content type to test against. - /// True if the content is of the specified content type; otherwise false. - public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias) + /// + /// Gets the url of the content item. + /// + /// + /// + /// If the content item is a document, then this method returns the url of the + /// document. If it is a media, then this methods return the media url for the + /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other + /// properties. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + /// + public static string Url(this IPublishedContent content, IPublishedUrlProvider publishedUrlProvider, string? culture = null, UrlMode mode = UrlMode.Default) + { + if (publishedUrlProvider == null) { - return content.ContentType.Alias.InvariantEquals(docTypeAlias); + throw new InvalidOperationException( + "Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); } - /// - /// Determines whether the specified content is a specified content type or it's derived types. - /// - /// The content to determine content type of. - /// The alias of the content type to test against. - /// When true, recurses up the content type tree to check inheritance; when false just calls IsDocumentType(this IPublishedContent content, string docTypeAlias). - /// True if the content is of the specified content type or a derived content type; otherwise false. - public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias, bool recursive) + switch (content.ContentType.ItemType) { - if (content.IsDocumentType(docTypeAlias)) - return true; - - return recursive && content.IsComposedOf(docTypeAlias); - } - - #endregion + case PublishedItemType.Content: + return publishedUrlProvider.GetUrl(content, mode, culture); - #region IsSomething: equality + case PublishedItemType.Media: + return publishedUrlProvider.GetMediaUrl(content, mode, culture); - public static bool IsEqual(this IPublishedContent content, IPublishedContent other) - { - return content.Id == other.Id; + default: + throw new NotSupportedException(); } + } - public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) - { - return content.IsEqual(other) == false; - } - - #endregion - - #region IsSomething: ancestors and descendants + #endregion - public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) - { - return other.Level < content.Level && content.Path.InvariantStartsWith(other.Path.EnsureEndsWith(',')); - } + #region Culture - public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) + /// + /// Determines whether the content has a culture. + /// + /// Culture is case-insensitive. + public static bool HasCulture(this IPublishedContent content, string? culture) + { + if (content == null) { - return content.Path.InvariantEquals(other.Path) || content.IsDescendant(other); + throw new ArgumentNullException(nameof(content)); } - public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) - { - return content.Level < other.Level && other.Path.InvariantStartsWith(content.Path.EnsureEndsWith(',')); - } + return content.Cultures.ContainsKey(culture ?? string.Empty); + } - public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) - { - return other.Path.InvariantEquals(content.Path) || content.IsAncestor(other); - } + /// + /// Determines whether the content is invariant, or has a culture. + /// + /// Culture is case-insensitive. + public static bool IsInvariantOrHasCulture(this IPublishedContent content, string culture) + => !content.ContentType.VariesByCulture() || content.Cultures.ContainsKey(culture ?? string.Empty); - #endregion - - #region Axes: ancestors, ancestors-or-self - - // as per XPath 1.0 specs �2.2, - // - the ancestor axis contains the ancestors of the context node; the ancestors of the context node consist - // of the parent of context node and the parent's parent and so on; thus, the ancestor axis will always - // include the root node, unless the context node is the root node. - // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, - // the ancestor axis will always include the root node. - // - // as per XPath 2.0 specs �3.2.1.1, - // - the ancestor axis is defined as the transitive closure of the parent axis; it contains the ancestors - // of the context node (the parent, the parent of the parent, and so on) - The ancestor axis includes the - // root node of the tree in which the context node is found, unless the context node is the root node. - // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, - // the ancestor-or-self axis will always include the root node. - // - // the ancestor and ancestor-or-self axis are reverse axes ie they contain the context node or nodes that - // are before the context node in document order. - // - // document order is defined by �2.4.1 as: - // - the root node is the first node. - // - every node occurs before all of its children and descendants. - // - the relative order of siblings is the order in which they occur in the children property of their parent node. - // - children and descendants occur before following siblings. - - /// - /// Gets the ancestors of the content. - /// - /// The content. - /// The ancestors of the content, in down-top order. - /// Does not consider the content itself. - public static IEnumerable Ancestors(this IPublishedContent content) + /// + /// Filters a sequence of to return invariant items, and items that are published for + /// the specified culture. + /// + /// The content items. + /// + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null). + /// + internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent + { + if (contents == null) { - return content.AncestorsOrSelf(false, null); + throw new ArgumentNullException(nameof(contents)); } - /// - /// Gets the ancestors of the content, at a level lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The ancestors of the content, at a level lesser or equal to the specified level, in down-top order. - /// Does not consider the content itself. Only content that are "high enough" in the tree are returned. - public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) - { - return content.AncestorsOrSelf(false, n => n.Level <= maxLevel); - } + culture = culture ?? variationContextAccessor.VariationContext?.Culture ?? string.Empty; - /// - /// Gets the ancestors of the content, of a specified content type. - /// - /// The content. - /// The content type. - /// The ancestors of the content, of the specified content type, in down-top order. - /// Does not consider the content itself. Returns all ancestors, of the specified content type. - public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) - { - return content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } + // either does not vary by culture, or has the specified culture + return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); + } - /// - /// Gets the ancestors of the content, of a specified content type. - /// - /// The content type. - /// The content. - /// The ancestors of the content, of the specified content type, in down-top order. - /// Does not consider the content itself. Returns all ancestors, of the specified content type. - public static IEnumerable Ancestors(this IPublishedContent content) - where T : class, IPublishedContent + /// + /// Gets the culture date of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the name for. If null is used the current culture is used (Default is + /// null). + /// + public static DateTime CultureDate(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - return content.Ancestors().OfType(); + return content.UpdateDate; } - /// - /// Gets the ancestors of the content, at a level lesser or equal to a specified level, and of a specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified - /// content type, in down-top order. - /// Does not consider the content itself. Only content that are "high enough" in the trees, and of the - /// specified content type, are returned. - public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent + // handle context culture for variant + if (culture == null) { - return content.Ancestors(maxLevel).OfType(); + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - /// - /// Gets the content and its ancestors. - /// - /// The content. - /// The content and its ancestors, in down-top order. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content) - { - return content.AncestorsOrSelf(true, null); - } + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.Date + : DateTime.MinValue; + } - /// - /// Gets the content and its ancestors, at a level lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The content and its ancestors, at a level lesser or equal to the specified level, - /// in down-top order. - /// Only content that are "high enough" in the tree are returned. So it may or may not begin - /// with the content itself, depending on its level. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) - { - return content.AncestorsOrSelf(true, n => n.Level <= maxLevel); - } + #endregion - /// - /// Gets the content and its ancestors, of a specified content type. - /// - /// The content. - /// The content type. - /// The content and its ancestors, of the specified content type, in down-top order. - /// May or may not begin with the content itself, depending on its content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) - { - return content.AncestorsOrSelf(true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } + #region Template - /// - /// Gets the content and its ancestors, of a specified content type. - /// - /// The content type. - /// The content. - /// The content and its ancestors, of the specified content type, in down-top order. - /// May or may not begin with the content itself, depending on its content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content) - where T : class, IPublishedContent + /// + /// Returns the current template Alias + /// + /// Empty string if none is set. + public static string GetTemplateAlias(this IPublishedContent content, IFileService fileService) + { + if (content.TemplateId.HasValue == false) { - return content.AncestorsOrSelf().OfType(); + return string.Empty; } - /// - /// Gets the content and its ancestor, at a lever lesser or equal to a specified level, and of a specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// The content and its ancestors, at a level lesser or equal to the specified level, and of the specified - /// content type, in down-top order. - /// May or may not begin with the content itself, depending on its level and content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent - { - return content.AncestorsOrSelf(maxLevel).OfType(); - } + ITemplate? template = fileService.GetTemplate(content.TemplateId.Value); + return template?.Alias ?? string.Empty; + } - /// - /// Gets the ancestor of the content, ie its parent. - /// - /// The content. - /// The ancestor of the content. - /// This method is here for consistency purposes but does not make much sense. - public static IPublishedContent? Ancestor(this IPublishedContent content) - { - return content.Parent; - } + public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, WebRoutingSettings webRoutingSettings, int templateId) => + content.IsAllowedTemplate(contentTypeService, webRoutingSettings.DisableAlternativeTemplates, webRoutingSettings.ValidateAlternativeTemplates, templateId); - /// - /// Gets the nearest ancestor of the content, at a lever lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The nearest (in down-top order) ancestor of the content, at a level lesser or equal to the specified level. - /// Does not consider the content itself. May return null. - public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel) + public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId) + { + if (disableAlternativeTemplates) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.Level <= maxLevel); + return content.TemplateId == templateId; } - /// - /// Gets the nearest ancestor of the content, of a specified content type. - /// - /// The content. - /// The content type alias. - /// The nearest (in down-top order) ancestor of the content, of the specified content type. - /// Does not consider the content itself. May return null. - public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias) + if (content.TemplateId == templateId || !validateAlternativeTemplates) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + return true; } - /// - /// Gets the nearest ancestor of the content, of a specified content type. - /// - /// The content type. - /// The content. - /// The nearest (in down-top order) ancestor of the content, of the specified content type. - /// Does not consider the content itself. May return null. - public static T? Ancestor(this IPublishedContent content) - where T : class, IPublishedContent - { - return content.Ancestors().FirstOrDefault(); - } + IContentType? publishedContentContentType = contentTypeService.Get(content.ContentType.Id); + if (publishedContentContentType == null) + { + throw new NullReferenceException("No content type returned for published content (contentType='" + + content.ContentType.Id + "')"); + } + + return publishedContentContentType.IsAllowedTemplate(templateId); + } + + public static bool IsAllowedTemplate(this IPublishedContent content, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias) + { + ITemplate? template = fileService.GetTemplate(templateAlias); + return template != null && content.IsAllowedTemplate(contentTypeService, disableAlternativeTemplates, validateAlternativeTemplates, template.Id); + } - /// - /// Gets the nearest ancestor of the content, at the specified level and of the specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// The ancestor of the content, at the specified level and of the specified content type. - /// Does not consider the content itself. If the ancestor at the specified level is - /// not of the specified type, returns null. - public static T? Ancestor(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent - { - return content.Ancestors(maxLevel).FirstOrDefault(); - } + #endregion - /// - /// Gets the content or its nearest ancestor. - /// - /// The content. - /// The content. - /// This method is here for consistency purposes but does not make much sense. - public static IPublishedContent AncestorOrSelf(this IPublishedContent content) - { - return content; - } + #region HasValue, Value, Value - /// - /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The content or its nearest (in down-top order) ancestor, at a level lesser or equal to the specified level. - /// May or may not return the content itself depending on its level. May return null. - public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, int maxLevel) - { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.Level <= maxLevel); - } + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// A value indicating whether the content has a value for the property identified by the alias. + /// Returns true if HasValue is true, or a fallback strategy can provide a value. + public static bool HasValue(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default) + { + IPublishedProperty? property = content.GetProperty(alias); - /// - /// Gets the content or its nearest ancestor, of a specified content type. - /// - /// The content. - /// The content type. - /// The content or its nearest (in down-top order) ancestor, of the specified content type. - /// May or may not return the content itself depending on its content type. May return null. - public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + return true; } - /// - /// Gets the content or its nearest ancestor, of a specified content type. - /// - /// The content type. - /// The content. - /// The content or its nearest (in down-top order) ancestor, of the specified content type. - /// May or may not return the content itself depending on its content type. May return null. - public static T? AncestorOrSelf(this IPublishedContent content) - where T : class, IPublishedContent - { - return content.AncestorsOrSelf().FirstOrDefault(); - } + // else let fallback try to get a value + return publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, null, out _, out _); + } - /// - /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level, and of a specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// - public static T? AncestorOrSelf(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent - { - return content.AncestorsOrSelf(maxLevel).FirstOrDefault(); - } + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + public static object? Value( + this IPublishedContent content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + object? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) { - var ancestorsOrSelf = content.EnumerateAncestors(orSelf); - return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); + return property.GetValue(culture, segment); } - /// - /// Enumerates ancestors of the content, bottom-up. - /// - /// The content. - /// Indicates whether the content should be included. - /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). - internal static IEnumerable EnumerateAncestors(this IPublishedContent? content, bool orSelf) + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value, out property)) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (orSelf) yield return content; - while ((content = content.Parent) != null) - yield return content; + return value; } - #endregion - - #region Axes: breadcrumbs - - /// - /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . - /// - /// The content. - /// Indicates whether the specified content should be included. - /// - /// The breadcrumbs (ancestors and self, top to bottom) for the specified . - /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) - { - return content.AncestorsOrSelf(andSelf, null).Reverse(); - } - - /// - /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . - /// - /// The content. - /// The minimum level. - /// Indicates whether the specified content should be included. - /// - /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . - /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, int minLevel, bool andSelf = true) - { - return content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); - } - - /// - /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . - /// - /// The root content type. - /// The content. - /// Indicates whether the specified content should be included. - /// - /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . - /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) - where T : class, IPublishedContent - { - static IEnumerable TakeUntil(IEnumerable source, Func predicate) - { - foreach (var item in source) - { - yield return item; - if (predicate(item)) - { - yield break; - } - } - } - - return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); - } - - #endregion - - #region Axes: descendants, descendants-or-self - - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// - public static IEnumerable DescendantsOrSelfOfType(this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, string? culture = null) - { - return parentNodes.SelectMany(x => x.DescendantsOrSelfOfType(variationContextAccessor, docTypeAlias, culture)); - } - - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// - public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return parentNodes.SelectMany(x => x.DescendantsOrSelf(variationContextAccessor, culture)); - } - - - // as per XPath 1.0 specs �2.2, - // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus - // the descendant axis never contains attribute or namespace nodes. - // - the descendant-or-self axis contains the context node and the descendants of the context node. - // - // as per XPath 2.0 specs �3.2.1.1, - // - the descendant axis is defined as the transitive closure of the child axis; it contains the descendants of the context node (the - // children, the children of the children, and so on). - // - the descendant-or-self axis contains the context node and the descendants of the context node. - // - // the descendant and descendant-or-self axis are forward axes ie they contain the context node or nodes that are after the context - // node in document order. - // - // document order is defined by �2.4.1 as: - // - the root node is the first node. - // - every node occurs before all of its children and descendants. - // - the relative order of siblings is the order in which they occur in the children property of their parent node. - // - children and descendants occur before following siblings. - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, false, null, culture); - } - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, false, p => p.Level >= level, culture); - } - - public static IEnumerable DescendantsOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - } - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Descendants(variationContextAccessor, culture).OfType(); - } - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.Descendants(variationContextAccessor, level, culture).OfType(); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, true, null, culture); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, true, p => p.Level >= level, culture); - } - - public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string ?culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.DescendantsOrSelf(variationContextAccessor, culture).OfType(); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.DescendantsOrSelf(variationContextAccessor, level, culture).OfType(); - } - - public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(); - } - - public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x.Level == level); - } - - public static IPublishedContent? DescendantOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } - - public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x is T) as T; - } - - public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.Descendant(variationContextAccessor, level, culture) as T; - } - - public static IPublishedContent DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content; - } - - public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x.Level == level); - } - - public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } - - public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x is T) as T; - } - - public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.DescendantOrSelf(variationContextAccessor, level, culture) as T; - } - - internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, Func? func, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, orSelf, culture).Where(x => func == null || func(x)); - } - - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, string? culture = null) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (orSelf) yield return content; - - var children = content.Children(variationContextAccessor, culture); - if (children is not null) - { - foreach (var desc in children.SelectMany(x => x.EnumerateDescendants(variationContextAccessor, culture))) - yield return desc; - } - } + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) + return property?.GetValue(culture, segment); + } - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + public static T? Value( + this IPublishedContent content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + T? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); + + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) + { + return property.Value(publishedValueFallback, culture, segment); + } + + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out T? value, out property)) + { + return value; + } + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(publishedValueFallback, culture, segment); + } + + #endregion + + #region IsSomething: misc. + + /// + /// Determines whether the specified content is a specified content type. + /// + /// The content to determine content type of. + /// The alias of the content type to test against. + /// True if the content is of the specified content type; otherwise false. + public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias) => + content.ContentType.Alias.InvariantEquals(docTypeAlias); + + /// + /// Determines whether the specified content is a specified content type or it's derived types. + /// + /// The content to determine content type of. + /// The alias of the content type to test against. + /// + /// When true, recurses up the content type tree to check inheritance; when false just calls + /// IsDocumentType(this IPublishedContent content, string docTypeAlias). + /// + /// True if the content is of the specified content type or a derived content type; otherwise false. + public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias, bool recursive) + { + if (content.IsDocumentType(docTypeAlias)) + { + return true; + } + + return recursive && content.IsComposedOf(docTypeAlias); + } + + #endregion + + #region IsSomething: equality + + public static bool IsEqual(this IPublishedContent content, IPublishedContent other) => content.Id == other.Id; + + public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) => + content.IsEqual(other) == false; + + #endregion + + #region IsSomething: ancestors and descendants + + public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) => + other.Level < content.Level && content.Path.InvariantStartsWith(other.Path.EnsureEndsWith(',')); + + public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) => + content.Path.InvariantEquals(other.Path) || content.IsDescendant(other); + + public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) => + content.Level < other.Level && other.Path.InvariantStartsWith(content.Path.EnsureEndsWith(',')); + + public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) => + other.Path.InvariantEquals(content.Path) || content.IsAncestor(other); + + #endregion + + #region Axes: ancestors, ancestors-or-self + + // as per XPath 1.0 specs �2.2, + // - the ancestor axis contains the ancestors of the context node; the ancestors of the context node consist + // of the parent of context node and the parent's parent and so on; thus, the ancestor axis will always + // include the root node, unless the context node is the root node. + // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, + // the ancestor axis will always include the root node. + // + // as per XPath 2.0 specs �3.2.1.1, + // - the ancestor axis is defined as the transitive closure of the parent axis; it contains the ancestors + // of the context node (the parent, the parent of the parent, and so on) - The ancestor axis includes the + // root node of the tree in which the context node is found, unless the context node is the root node. + // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, + // the ancestor-or-self axis will always include the root node. + // + // the ancestor and ancestor-or-self axis are reverse axes ie they contain the context node or nodes that + // are before the context node in document order. + // + // document order is defined by �2.4.1 as: + // - the root node is the first node. + // - every node occurs before all of its children and descendants. + // - the relative order of siblings is the order in which they occur in the children property of their parent node. + // - children and descendants occur before following siblings. + + /// + /// Gets the ancestors of the content. + /// + /// The content. + /// The ancestors of the content, in down-top order. + /// Does not consider the content itself. + public static IEnumerable Ancestors(this IPublishedContent content) => + content.AncestorsOrSelf(false, null); + + /// + /// Gets the ancestors of the content, at a level lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// The ancestors of the content, at a level lesser or equal to the specified level, in down-top order. + /// Does not consider the content itself. Only content that are "high enough" in the tree are returned. + public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) => + content.AncestorsOrSelf(false, n => n.Level <= maxLevel); + + /// + /// Gets the ancestors of the content, of a specified content type. + /// + /// The content. + /// The content type. + /// The ancestors of the content, of the specified content type, in down-top order. + /// Does not consider the content itself. Returns all ancestors, of the specified content type. + public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) => + content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the ancestors of the content, of a specified content type. + /// + /// The content type. + /// The content. + /// The ancestors of the content, of the specified content type, in down-top order. + /// Does not consider the content itself. Returns all ancestors, of the specified content type. + public static IEnumerable Ancestors(this IPublishedContent content) + where T : class, IPublishedContent => + content.Ancestors().OfType(); + + /// + /// Gets the ancestors of the content, at a level lesser or equal to a specified level, and of a specified content + /// type. + /// + /// The content type. + /// The content. + /// The level. + /// + /// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified + /// content type, in down-top order. + /// + /// + /// Does not consider the content itself. Only content that are "high enough" in the trees, and of the + /// specified content type, are returned. + /// + public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.Ancestors(maxLevel).OfType(); + + /// + /// Gets the content and its ancestors. + /// + /// The content. + /// The content and its ancestors, in down-top order. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content) => + content.AncestorsOrSelf(true, null); + + /// + /// Gets the content and its ancestors, at a level lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// + /// The content and its ancestors, at a level lesser or equal to the specified level, + /// in down-top order. + /// + /// + /// Only content that are "high enough" in the tree are returned. So it may or may not begin + /// with the content itself, depending on its level. + /// + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) => + content.AncestorsOrSelf(true, n => n.Level <= maxLevel); + + /// + /// Gets the content and its ancestors, of a specified content type. + /// + /// The content. + /// The content type. + /// The content and its ancestors, of the specified content type, in down-top order. + /// May or may not begin with the content itself, depending on its content type. + public static IEnumerable + AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) => + content.AncestorsOrSelf(true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the content and its ancestors, of a specified content type. + /// + /// The content type. + /// The content. + /// The content and its ancestors, of the specified content type, in down-top order. + /// May or may not begin with the content itself, depending on its content type. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content) + where T : class, IPublishedContent => + content.AncestorsOrSelf().OfType(); + + /// + /// Gets the content and its ancestor, at a lever lesser or equal to a specified level, and of a specified content + /// type. + /// + /// The content type. + /// The content. + /// The level. + /// + /// The content and its ancestors, at a level lesser or equal to the specified level, and of the specified + /// content type, in down-top order. + /// + /// May or may not begin with the content itself, depending on its level and content type. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.AncestorsOrSelf(maxLevel).OfType(); + + /// + /// Gets the ancestor of the content, ie its parent. + /// + /// The content. + /// The ancestor of the content. + /// This method is here for consistency purposes but does not make much sense. + public static IPublishedContent? Ancestor(this IPublishedContent content) => content.Parent; + + /// + /// Gets the nearest ancestor of the content, at a lever lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// The nearest (in down-top order) ancestor of the content, at a level lesser or equal to the specified level. + /// Does not consider the content itself. May return null. + public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel) => + content.EnumerateAncestors(false).FirstOrDefault(x => x.Level <= maxLevel); + + /// + /// Gets the nearest ancestor of the content, of a specified content type. + /// + /// The content. + /// The content type alias. + /// The nearest (in down-top order) ancestor of the content, of the specified content type. + /// Does not consider the content itself. May return null. + public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias) => content + .EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the nearest ancestor of the content, of a specified content type. + /// + /// The content type. + /// The content. + /// The nearest (in down-top order) ancestor of the content, of the specified content type. + /// Does not consider the content itself. May return null. + public static T? Ancestor(this IPublishedContent content) + where T : class, IPublishedContent => + content.Ancestors().FirstOrDefault(); + + /// + /// Gets the nearest ancestor of the content, at the specified level and of the specified content type. + /// + /// The content type. + /// The content. + /// The level. + /// The ancestor of the content, at the specified level and of the specified content type. + /// + /// Does not consider the content itself. If the ancestor at the specified level is + /// not of the specified type, returns null. + /// + public static T? Ancestor(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.Ancestors(maxLevel).FirstOrDefault(); + + /// + /// Gets the content or its nearest ancestor. + /// + /// The content. + /// The content. + /// This method is here for consistency purposes but does not make much sense. + public static IPublishedContent AncestorOrSelf(this IPublishedContent content) => content; + + /// + /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// The content or its nearest (in down-top order) ancestor, at a level lesser or equal to the specified level. + /// May or may not return the content itself depending on its level. May return null. + public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, int maxLevel) => + content.EnumerateAncestors(true).FirstOrDefault(x => x.Level <= maxLevel); + + /// + /// Gets the content or its nearest ancestor, of a specified content type. + /// + /// The content. + /// The content type. + /// The content or its nearest (in down-top order) ancestor, of the specified content type. + /// May or may not return the content itself depending on its content type. May return null. + public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) => content + .EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the content or its nearest ancestor, of a specified content type. + /// + /// The content type. + /// The content. + /// The content or its nearest (in down-top order) ancestor, of the specified content type. + /// May or may not return the content itself depending on its content type. May return null. + public static T? AncestorOrSelf(this IPublishedContent content) + where T : class, IPublishedContent => + content.AncestorsOrSelf().FirstOrDefault(); + + /// + /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level, and of a specified + /// content type. + /// + /// The content type. + /// The content. + /// The level. + /// + public static T? AncestorOrSelf(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.AncestorsOrSelf(maxLevel).FirstOrDefault(); + + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) + { + IEnumerable ancestorsOrSelf = content.EnumerateAncestors(orSelf); + return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); + } + + /// + /// Enumerates ancestors of the content, bottom-up. + /// + /// The content. + /// Indicates whether the content should be included. + /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). + internal static IEnumerable EnumerateAncestors(this IPublishedContent? content, bool orSelf) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (orSelf) { yield return content; - var children = content.Children(variationContextAccessor, culture); - if (children is not null) - { - foreach (var desc in children.SelectMany(x => x.EnumerateDescendants(variationContextAccessor, culture))) - yield return desc; - } } - #endregion - - #region Axes: children - - /// - /// Gets the children of the content item. - /// - /// The content item. - /// - /// - /// The specific culture to get the URL children for. Default is null which will use the current culture in - /// - /// - /// Gets children that are available for the specified culture. - /// Children are sorted by their sortOrder. - /// - /// For culture, - /// if null is used the current culture is used. - /// If an empty string is used only invariant children are returned. - /// If "*" is used all children are returned. - /// - /// - /// If a variant culture is specified or there is a current culture in the then the Children returned - /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. - /// However, if an empty string is specified only invariant children are returned. - /// - /// - public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + while ((content = content.Parent) != null) { - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? ""; - - var children = content.ChildrenForAllCultures; - return culture == "*" - ? children - : children?.Where(x => x.IsInvariantOrHasCulture(culture)); + yield return content; } - - /// - /// Gets the children of the content, filtered by a predicate. - /// - /// The content. - /// Published snapshot instance - /// The predicate. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content, filtered by the predicate. - /// - /// Children are sorted by their sortOrder. - /// - public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - { - return content.Children(variationContextAccessor, culture)?.Where(predicate); - } - - /// - /// Gets the children of the content, of any of the specified types. - /// - /// The content. - /// Published snapshot instance - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The children of the content, of any of the specified types. - public static IEnumerable? ChildrenOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, string? culture = null) - { - return content.Children(variationContextAccessor, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - } - - /// - /// Gets the children of the content, of a given content type. - /// - /// The content type. - /// The content. - /// Published snapshot instance - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of content, of the given content type. - /// - /// Children are sorted by their sortOrder. - /// - public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Children(variationContextAccessor, culture)?.OfType(); - } - - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(); - } - - /// - /// Gets the first child of the content, of a given content type. - /// - public static IPublishedContent? FirstChildOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture)?.FirstOrDefault(); - } - - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - { - return content.Children(variationContextAccessor, predicate, culture)?.FirstOrDefault(); - } - - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Guid uniqueId, string? culture = null) - { - return content.Children(variationContextAccessor, x => x.Key == uniqueId, culture)?.FirstOrDefault(); - } - - public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(); - } - - public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - where T : class, IPublishedContent - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(predicate); - } - - #endregion - - #region Axes: parent - - // Parent is native - - /// - /// Gets the parent of the content, of a given content type. - /// - /// The content type. - /// The content. - /// The parent of content, of the given content type, else null. - public static T? Parent(this IPublishedContent content) - where T : class, IPublishedContent - { - if (content == null) throw new ArgumentNullException(nameof(content)); - return content.Parent as T; - } - - #endregion - - #region Axes: siblings - - /// - /// Gets the siblings of the content. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? Siblings(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture)?.Where(x => x.Id != content.Id); - } - - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? SiblingsOfType(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return SiblingsAndSelfOfType(content, publishedSnapshot, variationContextAccessor, contentTypeAlias, culture)?.Where(x => x.Id != content.Id); - } - - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content type. - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? Siblings(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture)?.Where(x => x.Id != content.Id); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content including the node itself. - public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.Parent != null - ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The siblings of the content including the node itself, of the given content type. - public static IEnumerable? SiblingsAndSelfOfType(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.Parent != null - ? content.Parent.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).OfTypes(contentTypeAlias).WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content type. - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content including the node itself, of the given content type. - public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Parent != null - ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).OfType().WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - #endregion - - #region Axes: custom - - /// - /// Gets the root content (ancestor or self at level 1) for the specified . - /// - /// The content. - /// - /// The root content (ancestor or self at level 1) for the specified . - /// - /// - /// This is the same as calling with maxLevel set to 1. - /// - public static IPublishedContent? Root(this IPublishedContent content) - { - return content.AncestorOrSelf(1); - } - - /// - /// Gets the root content (ancestor or self at level 1) for the specified if it's of the specified content type . - /// - /// The content type. - /// The content. - /// - /// The root content (ancestor or self at level 1) for the specified of content type . - /// - /// - /// This is the same as calling with maxLevel set to 1. - /// - public static T? Root(this IPublishedContent content) - where T : class, IPublishedContent - { - return content.AncestorOrSelf(1); - } - - #endregion - - #region Writer and creator - - public static string? GetCreatorName(this IPublishedContent content, IUserService userService) - { - var user = userService.GetProfileById(content.CreatorId); - return user?.Name; - } - - public static string? GetWriterName(this IPublishedContent content, IUserService userService) - { - var user = userService.GetProfileById(content.WriterId); - return user?.Name; - } - - #endregion - - #region Url - - /// - /// Gets the url of the content item. - /// - /// - /// If the content item is a document, then this method returns the url of the - /// document. If it is a media, then this methods return the media url for the - /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other - /// properties. - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - public static string Url(this IPublishedContent content, IPublishedUrlProvider publishedUrlProvider, string? culture = null, UrlMode mode = UrlMode.Default) - { - if (publishedUrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); - - switch (content.ContentType.ItemType) - { - case PublishedItemType.Content: - return publishedUrlProvider.GetUrl(content, mode, culture); - - case PublishedItemType.Media: - return publishedUrlProvider.GetMediaUrl(content, mode, culture, Constants.Conventions.Media.File); - - default: - throw new NotSupportedException(); - } - } - - #endregion - - #region Axes: children - - /// - /// Gets the children of the content in a DataTable. - /// - /// The content. - /// Variation context accessor. - /// The content type service. - /// The media type service. - /// The member type service. - /// The published url provider. - /// An optional content type alias. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content. - public static DataTable ChildrenAsTable(this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, - IPublishedUrlProvider publishedUrlProvider, string contentTypeAliasFilter = "", string? culture = null) - => GenerateDataTable(content, variationContextAccessor, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); - - /// - /// Gets the children of the content in a DataTable. - /// - /// The content. - /// Variation context accessor. - /// The content type service. - /// The media type service. - /// The member type service. - /// The published url provider. - /// An optional content type alias. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content. - private static DataTable GenerateDataTable(IPublishedContent content, - IVariationContextAccessor variationContextAccessor, IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, - IPublishedUrlProvider publishedUrlProvider, string contentTypeAliasFilter = "", string? culture = null) - { - var firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() - ? content.Children(variationContextAccessor, culture)?.Any() ?? false - ? content.Children(variationContextAccessor, culture)?.ElementAt(0) - : null - : content.Children(variationContextAccessor, culture)?.FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); - if (firstNode == null) - return new DataTable(); //no children found - - //use new utility class to create table so that we don't have to maintain code in many places, just one - var dt = DataTableExtensions.GenerateDataTable( - //pass in the alias of the first child node since this is the node type we're rendering headers for - firstNode.ContentType.Alias, - //pass in the callback to extract the Dictionary of all defined aliases to their names - alias => GetPropertyAliasesAndNames(contentTypeService, mediaTypeService, memberTypeService, alias), - //pass in a callback to populate the datatable, yup its a bit ugly but it's already legacy and we just want to maintain code in one place. - () => - { - //create all row data - var tableData = DataTableExtensions.CreateTableData(); - var children = content.Children(variationContextAccessor)?.OrderBy(x => x.SortOrder); - if (children is not null) - { - //loop through each child and create row data for it - foreach (var n in children) - { - if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) - { - if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) - continue; //skip this one, it doesn't match the filter - } - - var standardVals = new Dictionary - { - { "Id", n.Id }, - { "NodeName", n.Name(variationContextAccessor) }, - { "NodeTypeAlias", n.ContentType.Alias }, - { "CreateDate", n.CreateDate }, - { "UpdateDate", n.UpdateDate }, - { "CreatorId", n.CreatorId}, - { "WriterId", n.WriterId }, - { "Url", n.Url(publishedUrlProvider) } - }; - - var userVals = new Dictionary(); - var properties = n.Properties?.Where(p => p.GetSourceValue() is not null) ?? Array.Empty(); - foreach (var p in properties) - { - // probably want the "object value" of the property here... - userVals[p.Alias] = p.GetValue(); - } - //add the row data - DataTableExtensions.AddRowData(tableData, standardVals, userVals); - } - } - - return tableData; - }); - return dt; - } - - #endregion - - #region PropertyAliasesAndNames - - private static Func>? _getPropertyAliasesAndNames; - - /// - /// This is used only for unit tests to set the delegate to look up aliases/names dictionary of a content type - /// - internal static Func> GetPropertyAliasesAndNames - { - get => _getPropertyAliasesAndNames ?? GetAliasesAndNames; - set => _getPropertyAliasesAndNames = value; - } - - private static Dictionary GetAliasesAndNames(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, string alias) - { - var type = contentTypeService.Get(alias) - ?? mediaTypeService.Get(alias) - ?? (IContentTypeBase?)memberTypeService.Get(alias); - var fields = GetAliasesAndNames(type); - - // ensure the standard fields are there - var stdFields = new Dictionary - { - {"Id", "Id"}, - {"NodeName", "NodeName"}, - {"NodeTypeAlias", "NodeTypeAlias"}, - {"CreateDate", "CreateDate"}, - {"UpdateDate", "UpdateDate"}, - {"CreatorId", "CreatorId"}, - {"WriterId", "WriterId"}, - {"Url", "Url"} - }; - - foreach (var field in stdFields.Where(x => fields.ContainsKey(x.Key) == false)) - { - fields[field.Key] = field.Value; - } - - return fields; - } - - private static Dictionary GetAliasesAndNames(IContentTypeBase? contentType) => contentType?.PropertyTypes.ToDictionary(x => x.Alias, x => x.Name) ?? new Dictionary(); - - #endregion } + + #endregion + + #region Axes: breadcrumbs + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) => + content.AncestorsOrSelf(andSelf, null).Reverse(); + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level + /// higher or equal to . + /// + /// The content. + /// The minimum level. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher + /// or equal to . + /// + public static IEnumerable Breadcrumbs( + this IPublishedContent content, + int minLevel, + bool andSelf = true) => + content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level + /// higher or equal to the specified root content type . + /// + /// The root content type. + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher + /// or equal to the specified root content type . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + where T : class, IPublishedContent + { + static IEnumerable TakeUntil(IEnumerable source, Func predicate) + { + foreach (IPublishedContent item in source) + { + yield return item; + if (predicate(item)) + { + yield break; + } + } + } + + return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); + } + + #endregion + + #region Axes: descendants, descendants-or-self + + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// Variation context accessor. + /// + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelfOfType( + this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, string? culture = null) => parentNodes.SelectMany(x => + x.DescendantsOrSelfOfType(variationContextAccessor, docTypeAlias, culture)); + + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + parentNodes.SelectMany(x => x.DescendantsOrSelf(variationContextAccessor, culture)); + + // as per XPath 1.0 specs �2.2, + // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus + // the descendant axis never contains attribute or namespace nodes. + // - the descendant-or-self axis contains the context node and the descendants of the context node. + // + // as per XPath 2.0 specs �3.2.1.1, + // - the descendant axis is defined as the transitive closure of the child axis; it contains the descendants of the context node (the + // children, the children of the children, and so on). + // - the descendant-or-self axis contains the context node and the descendants of the context node. + // + // the descendant and descendant-or-self axis are forward axes ie they contain the context node or nodes that are after the context + // node in document order. + // + // document order is defined by �2.4.1 as: + // - the root node is the first node. + // - every node occurs before all of its children and descendants. + // - the relative order of siblings is the order in which they occur in the children property of their parent node. + // - children and descendants occur before following siblings. + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, false, null, culture); + + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, false, p => p.Level >= level, culture); + + public static IEnumerable DescendantsOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.Descendants(variationContextAccessor, culture).OfType(); + + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.Descendants(variationContextAccessor, level, culture).OfType(); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, true, null, culture); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, true, p => p.Level >= level, culture); + + public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.DescendantsOrSelf(variationContextAccessor, culture).OfType(); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.DescendantsOrSelf(variationContextAccessor, level, culture).OfType(); + + public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + + public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x.Level == level); + + public static IPublishedContent? DescendantOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, false, culture) + .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x is T) as T; + + public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.Descendant(variationContextAccessor, level, culture) as T; + + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => content; + + public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x.Level == level); + + public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, true, culture) + .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x is T) as T; + + public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.DescendantOrSelf(variationContextAccessor, level, culture) as T; + + internal static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + bool orSelf, + Func? func, + string? culture = null) => + content.EnumerateDescendants(variationContextAccessor, orSelf, culture) + .Where(x => func == null || func(x)); + + internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, string? culture = null) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (orSelf) + { + yield return content; + } + + IEnumerable? children = content.Children(variationContextAccessor, culture); + if (children is not null) + { + foreach (IPublishedContent desc in children.SelectMany(x => + x.EnumerateDescendants(variationContextAccessor, culture))) + { + yield return desc; + } + } + } + + internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + { + yield return content; + IEnumerable? children = content.Children(variationContextAccessor, culture); + if (children is not null) + { + foreach (IPublishedContent desc in children.SelectMany(x => + x.EnumerateDescendants(variationContextAccessor, culture))) + { + yield return desc; + } + } + } + + #endregion + + #region Axes: children + + /// + /// Gets the children of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the URL children for. Default is null which will use the current culture in + /// + /// + /// + /// Gets children that are available for the specified culture. + /// Children are sorted by their sortOrder. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the + /// Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant + /// children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// + /// + public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + { + // handle context culture for variant + if (culture == null) + { + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; + } + + IEnumerable? children = content.ChildrenForAllCultures; + return culture == "*" + ? children + : children?.Where(x => x.IsInvariantOrHasCulture(culture)); + } + + /// + /// Gets the children of the content, filtered by a predicate. + /// + /// The content. + /// The accessor for VariationContext + /// The predicate. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content, filtered by the predicate. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable? Children( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + Func predicate, + string? culture = null) => + content.Children(variationContextAccessor, culture)?.Where(predicate); + + /// + /// Gets the children of the content, of any of the specified types. + /// + /// The content. + /// The accessor for the VariationContext + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The children of the content, of any of the specified types. + public static IEnumerable? ChildrenOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, string? culture = null) => + content.Children(variationContextAccessor, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + + /// + /// Gets the children of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The accessor for the VariationContext + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of content, of the given content type. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.Children(variationContextAccessor, culture)?.OfType(); + + public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + + /// + /// Gets the first child of the content, of a given content type. + /// + public static IPublishedContent? FirstChildOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => + content.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture)?.FirstOrDefault(); + + public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) => content.Children(variationContextAccessor, predicate, culture)?.FirstOrDefault(); + + public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Guid uniqueId, string? culture = null) => content + .Children(variationContextAccessor, x => x.Key == uniqueId, culture)?.FirstOrDefault(); + + public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + + public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) + where T : class, IPublishedContent => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(predicate); + + #endregion + + #region Axes: siblings + + /// + /// Gets the siblings of the content. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? Siblings( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string? culture = null) => + SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture)?.Where(x => x.Id != content.Id); + + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? SiblingsOfType( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string contentTypeAlias, + string? culture = null) => + SiblingsAndSelfOfType(content, publishedSnapshot, variationContextAccessor, contentTypeAlias, culture) + ?.Where(x => x.Id != content.Id); + + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content type. + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? Siblings(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture) + ?.Where(x => x.Id != content.Id); + + /// + /// Gets the siblings of the content including the node itself to indicate the position. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content including the node itself. + public static IEnumerable? SiblingsAndSelf( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string? culture = null) => + content.Parent != null + ? content.Parent.Children(variationContextAccessor, culture) + : publishedSnapshot?.Content?.GetAtRoot(culture) + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable? SiblingsAndSelfOfType( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string contentTypeAlias, + string? culture = null) => + content.Parent != null + ? content.Parent.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture) + : publishedSnapshot?.Content?.GetAtRoot(culture).OfTypes(contentTypeAlias) + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content type. + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable? SiblingsAndSelf( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string? culture = null) + where T : class, IPublishedContent => + content.Parent != null + ? content.Parent.Children(variationContextAccessor, culture) + : publishedSnapshot?.Content?.GetAtRoot(culture).OfType() + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + + #endregion + + #region Axes: custom + + /// + /// Gets the root content (ancestor or self at level 1) for the specified . + /// + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified . + /// + /// + /// This is the same as calling + /// with maxLevel + /// set to 1. + /// + public static IPublishedContent? Root(this IPublishedContent content) => content.AncestorOrSelf(1); + + /// + /// Gets the root content (ancestor or self at level 1) for the specified if it's of the + /// specified content type . + /// + /// The content type. + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified of content type + /// . + /// + /// + /// This is the same as calling + /// with + /// maxLevel set to 1. + /// + public static T? Root(this IPublishedContent content) + where T : class, IPublishedContent => + content.AncestorOrSelf(1); + + #endregion + + #region Writer and creator + + public static string? GetCreatorName(this IPublishedContent content, IUserService userService) + { + IProfile? user = userService.GetProfileById(content.CreatorId); + return user?.Name; + } + + public static string? GetWriterName(this IPublishedContent content, IUserService userService) + { + IProfile? user = userService.GetProfileById(content.WriterId); + return user?.Name; + } + + #endregion + + #region Axes: children + + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// Variation context accessor. + /// The content type service. + /// The media type service. + /// The member type service. + /// The published url provider. + /// An optional content type alias. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content. + public static DataTable ChildrenAsTable( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedUrlProvider publishedUrlProvider, + string contentTypeAliasFilter = "", + string? culture = null) + => GenerateDataTable(content, variationContextAccessor, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); + + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// Variation context accessor. + /// The content type service. + /// The media type service. + /// The member type service. + /// The published url provider. + /// An optional content type alias. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content. + private static DataTable GenerateDataTable( + IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedUrlProvider publishedUrlProvider, + string contentTypeAliasFilter = "", + string? culture = null) + { + IPublishedContent? firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() + ? content.Children(variationContextAccessor, culture)?.Any() ?? false + ? content.Children(variationContextAccessor, culture)?.ElementAt(0) + : null + : content.Children(variationContextAccessor, culture) + ?.FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); + if (firstNode == null) + { + // No children found + return new DataTable(); + } + + // use new utility class to create table so that we don't have to maintain code in many places, just one + DataTable dt = DataTableExtensions.GenerateDataTable( + + // pass in the alias of the first child node since this is the node type we're rendering headers for + firstNode.ContentType.Alias, + + // pass in the callback to extract the Dictionary of all defined aliases to their names + alias => GetPropertyAliasesAndNames(contentTypeService, mediaTypeService, memberTypeService, alias), + () => + { + // here we pass in a callback to populate the datatable, yup its a bit ugly but it's already legacy and we just want to maintain code in one place. + // create all row data + List>, IEnumerable>>> + tableData = DataTableExtensions.CreateTableData(); + IOrderedEnumerable? children = + content.Children(variationContextAccessor)?.OrderBy(x => x.SortOrder); + if (children is not null) + { + // loop through each child and create row data for it + foreach (IPublishedContent n in children) + { + if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) + { + if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) + { + continue; // skip this one, it doesn't match the filter + } + } + + var standardVals = new Dictionary + { + { "Id", n.Id }, + { "NodeName", n.Name(variationContextAccessor) }, + { "NodeTypeAlias", n.ContentType.Alias }, + { "CreateDate", n.CreateDate }, + { "UpdateDate", n.UpdateDate }, + { "CreatorId", n.CreatorId }, + { "WriterId", n.WriterId }, + { "Url", n.Url(publishedUrlProvider) }, + }; + + var userVals = new Dictionary(); + IEnumerable properties = + n.Properties?.Where(p => p.GetSourceValue() is not null) ?? + Array.Empty(); + foreach (IPublishedProperty p in properties) + { + // probably want the "object value" of the property here... + userVals[p.Alias] = p.GetValue(); + } + + // Add the row data + DataTableExtensions.AddRowData(tableData, standardVals, userVals); + } + } + + return tableData; + }); + return dt; + } + + #endregion + + #region PropertyAliasesAndNames + + private static Func>? _getPropertyAliasesAndNames; + + /// + /// This is used only for unit tests to set the delegate to look up aliases/names dictionary of a content type + /// + internal static Func> + GetPropertyAliasesAndNames + { + get => _getPropertyAliasesAndNames ?? GetAliasesAndNames; + set => _getPropertyAliasesAndNames = value; + } + + private static Dictionary GetAliasesAndNames(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, string alias) + { + IContentTypeBase? type = contentTypeService.Get(alias) + ?? mediaTypeService.Get(alias) + ?? (IContentTypeBase?)memberTypeService.Get(alias); + Dictionary fields = GetAliasesAndNames(type); + + // ensure the standard fields are there + var stdFields = new Dictionary + { + { "Id", "Id" }, + { "NodeName", "NodeName" }, + { "NodeTypeAlias", "NodeTypeAlias" }, + { "CreateDate", "CreateDate" }, + { "UpdateDate", "UpdateDate" }, + { "CreatorId", "CreatorId" }, + { "WriterId", "WriterId" }, + { "Url", "Url" }, + }; + + foreach (KeyValuePair field in stdFields.Where(x => fields.ContainsKey(x.Key) == false)) + { + fields[field.Key] = field.Value; + } + + return fields; + } + + private static Dictionary GetAliasesAndNames(IContentTypeBase? contentType) => + contentType?.PropertyTypes.ToDictionary(x => x.Alias, x => x.Name) ?? new Dictionary(); + + #endregion } diff --git a/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs b/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs index 17133cddaa..c85178c85c 100644 --- a/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs @@ -1,210 +1,267 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for IPublishedElement. +/// +public static class PublishedElementExtensions { - /// - /// Provides extension methods for IPublishedElement. - /// - public static class PublishedElementExtensions + #region OfTypes + + // the .OfType() filter is nice when there's only one type + // this is to support filtering with multiple types + public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) + where T : IPublishedElement { - #region OfTypes - - // the .OfType() filter is nice when there's only one type - // this is to support filtering with multiple types - public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) - where T : IPublishedElement + if (types == null || types.Length == 0) { - if (types == null || types.Length == 0) return Enumerable.Empty(); - - return contents.Where(x => types.InvariantContains(x.ContentType.Alias)); + return Enumerable.Empty(); } - #endregion - - #region IsComposedOf - - /// - /// Gets a value indicating whether the content is of a content type composed of the given alias - /// - /// The content. - /// The content type alias. - /// A value indicating whether the content is of a content type composed of a content type identified by the alias. - public static bool IsComposedOf(this IPublishedElement content, string alias) - { - return content.ContentType.CompositionAliases.InvariantContains(alias); - } - - #endregion - - #region HasProperty - - /// - /// Gets a value indicating whether the content has a property identified by its alias. - /// - /// The content. - /// The property alias. - /// A value indicating whether the content has the property identified by the alias. - /// The content may have a property, and that property may not have a value. - public static bool HasProperty(this IPublishedElement content, string alias) - { - return content.ContentType.GetPropertyType(alias) != null; - } - - #endregion - - #region HasValue - - /// - /// Gets a value indicating whether the content has a value for a property identified by its alias. - /// - /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is true. - public static bool HasValue(this IPublishedElement content, string alias, string? culture = null, string? segment = null) - { - var prop = content.GetProperty(alias); - return prop != null && prop.HasValue(culture, segment); - } - - #endregion - - #region Value - - /// - /// Gets the value of a content's property identified by its alias. - /// - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object? Value(this IPublishedElement content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) - { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.GetValue(culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) - return value; - - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property?.GetValue(culture, segment); - } - - #endregion - - #region Value - - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type. - /// - /// The target property type. - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static T? Value(this IPublishedElement content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) - { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.Value(publishedValueFallback, culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) - return value; - - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property == null ? default : property.Value(publishedValueFallback, culture, segment); - } - - #endregion - - #region ToIndexedArray - - public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) - where TContent : class, IPublishedElement - { - var set = source.Select((content, index) => new IndexedArrayItem(content, index)).ToArray(); - foreach (var setItem in set) setItem.TotalCount = set.Length; - return set; - } - - #endregion - - #region IsSomething - - /// - /// Gets a value indicating whether the content is visible. - /// - /// The content. - /// The published value fallback implementation. - /// A value indicating whether the content is visible. - /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, - /// the content is visible. - public static bool IsVisible(this IPublishedElement content, IPublishedValueFallback publishedValueFallback) - { - // rely on the property converter - will return default bool value, ie false, if property - // is not defined, or has no value, else will return its value. - return content.Value(publishedValueFallback, Constants.Conventions.Content.NaviHide) == false; - } - - #endregion - - #region MediaUrl - - /// - /// Gets the url for a media. - /// - /// The content item. - /// The published url provider. - /// The culture (use current culture by default). - /// The url mode (use site configuration by default). - /// The alias of the property (use 'umbracoFile' by default). - /// The url for the media. - /// - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - public static string MediaUrl(this IPublishedContent content, IPublishedUrlProvider publishedUrlProvider, string? culture = null, UrlMode mode = UrlMode.Default, string propertyAlias = Constants.Conventions.Media.File) - { - if (publishedUrlProvider == null) throw new ArgumentNullException(nameof(publishedUrlProvider)); - - return publishedUrlProvider.GetMediaUrl(content, mode, culture, propertyAlias); - } - - #endregion + return contents.Where(x => types.InvariantContains(x.ContentType.Alias)); } + + #endregion + + #region IsComposedOf + + /// + /// Gets a value indicating whether the content is of a content type composed of the given alias + /// + /// The content. + /// The content type alias. + /// + /// A value indicating whether the content is of a content type composed of a content type identified by the + /// alias. + /// + public static bool IsComposedOf(this IPublishedElement content, string alias) => + content.ContentType.CompositionAliases.InvariantContains(alias); + + #endregion + + #region HasProperty + + /// + /// Gets a value indicating whether the content has a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether the content has the property identified by the alias. + /// The content may have a property, and that property may not have a value. + public static bool HasProperty(this IPublishedElement content, string alias) => + content.ContentType.GetPropertyType(alias) != null; + + #endregion + + #region HasValue + + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// + /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is + /// true. + /// + public static bool HasValue(this IPublishedElement content, string alias, string? culture = null, string? segment = null) + { + IPublishedProperty? prop = content.GetProperty(alias); + return prop != null && prop.HasValue(culture, segment); + } + + #endregion + + #region Value + + /// + /// Gets the value of a content's property identified by its alias. + /// + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering + /// content. + /// + /// + /// If no property with the specified alias exists, or if the property has no value, returns + /// . + /// + /// + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the + /// converter. + /// + /// The alias is case-insensitive. + /// + public static object? Value( + this IPublishedElement content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + object? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); + + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) + { + return property.GetValue(culture, segment); + } + + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) + { + return value; + } + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property?.GetValue(culture, segment); + } + + #endregion + + #region Value + + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + /// + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering + /// content. + /// + /// + /// If no property with the specified alias exists, or if the property has no value, or if it could not be + /// converted, returns default(T). + /// + /// + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the + /// converter. + /// + /// The alias is case-insensitive. + /// + public static T? Value( + this IPublishedElement content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + T? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); + + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) + { + return property.Value(publishedValueFallback, culture, segment); + } + + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out T? value)) + { + return value; + } + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(publishedValueFallback, culture, segment); + } + + #endregion + + #region ToIndexedArray + + public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) + where TContent : class, IPublishedElement + { + IndexedArrayItem[] set = + source.Select((content, index) => new IndexedArrayItem(content, index)).ToArray(); + foreach (IndexedArrayItem setItem in set) + { + setItem.TotalCount = set.Length; + } + + return set; + } + + #endregion + + #region IsSomething + + /// + /// Gets a value indicating whether the content is visible. + /// + /// The content. + /// The published value fallback implementation. + /// A value indicating whether the content is visible. + /// + /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, + /// the content is visible. + /// + public static bool IsVisible(this IPublishedElement content, IPublishedValueFallback publishedValueFallback) => + + // rely on the property converter - will return default bool value, ie false, if property + // is not defined, or has no value, else will return its value. + content.Value(publishedValueFallback, Constants.Conventions.Content.NaviHide) == false; + + #endregion + + #region MediaUrl + + /// + /// Gets the url for a media. + /// + /// The content item. + /// The published url provider. + /// The culture (use current culture by default). + /// The url mode (use site configuration by default). + /// The alias of the property (use 'umbracoFile' by default). + /// The url for the media. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + /// + public static string MediaUrl( + this IPublishedContent content, + IPublishedUrlProvider publishedUrlProvider, + string? culture = null, + UrlMode mode = UrlMode.Default, + string propertyAlias = Constants.Conventions.Media.File) + { + if (publishedUrlProvider == null) + { + throw new ArgumentNullException(nameof(publishedUrlProvider)); + } + + return publishedUrlProvider.GetMediaUrl(content, mode, culture, propertyAlias); + } + + #endregion } diff --git a/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs index b4ffc40130..0d84e3268e 100644 --- a/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs @@ -1,52 +1,52 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for . +/// +public static class PublishedModelFactoryExtensions { /// - /// Provides extension methods for . + /// Returns true if the current is an implementation of + /// and is enabled /// - public static class PublishedModelFactoryExtensions + public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory) { - /// - /// Returns true if the current is an implementation of and is enabled - /// - public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory) + if (factory is IAutoPublishedModelFactory liveFactory) { - if (factory is IAutoPublishedModelFactory liveFactory) - { - return liveFactory.Enabled; - } - - // if it's not ILivePublishedModelFactory we know we're not using a live factory - return false; + return liveFactory.Enabled; } - /// - /// Sets a flag to reset the ModelsBuilder models if the is - /// - /// - /// This does not recompile the InMemory models, only sets a flag to tell models builder to recompile when they are requested. - /// - internal static void WithSafeLiveFactoryReset(this IPublishedModelFactory factory, Action action) - { - if (factory is IAutoPublishedModelFactory liveFactory) - { - lock (liveFactory.SyncRoot) - { - liveFactory.Reset(); + // if it's not ILivePublishedModelFactory we know we're not using a live factory + return false; + } - action(); - } - } - else + /// + /// Sets a flag to reset the ModelsBuilder models if the is + /// + /// + /// + /// This does not recompile the InMemory models, only sets a flag to tell models builder to recompile when they are + /// requested. + /// + internal static void WithSafeLiveFactoryReset(this IPublishedModelFactory factory, Action action) + { + if (factory is IAutoPublishedModelFactory liveFactory) + { + lock (liveFactory.SyncRoot) { + liveFactory.Reset(); + action(); } } - + else + { + action(); + } } } diff --git a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs index 3ff5c77719..267157cf7a 100644 --- a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs +++ b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs @@ -1,77 +1,80 @@ // Copyright (c) Umbraco. // See LICENSE for more details. + +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for IPublishedProperty. +/// +public static class PublishedPropertyExtension { - /// - /// Provides extension methods for IPublishedProperty. - /// - public static class PublishedPropertyExtension + #region Value + + public static object? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) { - #region Value - - public static object? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) + if (property.HasValue(culture, segment)) { - if (property.HasValue(culture, segment)) - return property.GetValue(culture, segment); - - return publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var value) - ? value - : property.GetValue(culture, segment); // give converter a chance to return it's own vision of "no value" + return property.GetValue(culture, segment); } - #endregion + return publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var value) + ? value + : property.GetValue(culture, segment); // give converter a chance to return it's own vision of "no value" + } - #region Value + #endregion - public static T? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) + #region Value + + public static T? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) + { + if (property.HasValue(culture, segment)) { - if (property.HasValue(culture, segment)) + // we have a value + // try to cast or convert it + var value = property.GetValue(culture, segment); + if (value is T valueAsT) { - // we have a value - // try to cast or convert it - var value = property.GetValue(culture, segment); - if (value is T valueAsT) - { - return valueAsT; - } - - var valueConverted = value.TryConvertTo(); - if (valueConverted.Success) - { - return valueConverted.Result; - } - - // cannot cast nor convert the value, nothing we can return but 'default' - // note: we don't want to fallback in that case - would make little sense - return default; + return valueAsT; } - // we don't have a value, try fallback - if (publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) + Attempt valueConverted = value.TryConvertTo(); + if (valueConverted.Success) { - return fallbackValue; + return valueConverted.Result; } - // we don't have a value - neither direct nor fallback - // give a chance to the converter to return something (eg empty enumerable) - var noValue = property.GetValue(culture, segment); - if (noValue is T noValueAsT) - { - return noValueAsT; - } - - var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted.Success) - { - return noValueConverted.Result; - } - - // cannot cast noValue nor convert it, nothing we can return but 'default' + // cannot cast nor convert the value, nothing we can return but 'default' + // note: we don't want to fallback in that case - would make little sense return default; } - #endregion + // we don't have a value, try fallback + if (publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out T? fallbackValue)) + { + return fallbackValue; + } + + // we don't have a value - neither direct nor fallback + // give a chance to the converter to return something (eg empty enumerable) + var noValue = property.GetValue(culture, segment); + if (noValue is T noValueAsT) + { + return noValueAsT; + } + + Attempt noValueConverted = noValue.TryConvertTo(); + if (noValueConverted.Success) + { + return noValueConverted.Result; + } + + // cannot cast noValue nor convert it, nothing we can return but 'default' + return default; } + + #endregion } diff --git a/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs b/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs index 9fd7da4640..5e6d356674 100644 --- a/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs @@ -1,18 +1,17 @@ -using System; using Umbraco.Cms.Core.PublishedCache; -namespace Umbraco.Extensions -{ - public static class PublishedSnapshotAccessorExtensions - { - public static IPublishedSnapshot GetRequiredPublishedSnapshot(this IPublishedSnapshotAccessor publishedSnapshotAccessor) - { - if (publishedSnapshotAccessor.TryGetPublishedSnapshot(out var publishedSnapshot)) - { - return publishedSnapshot!; - } +namespace Umbraco.Extensions; - throw new InvalidOperationException("Wasn't possible to a get a valid Snapshot"); +public static class PublishedSnapshotAccessorExtensions +{ + public static IPublishedSnapshot GetRequiredPublishedSnapshot( + this IPublishedSnapshotAccessor publishedSnapshotAccessor) + { + if (publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) + { + return publishedSnapshot!; } + + throw new InvalidOperationException("Wasn't possible to a get a valid Snapshot"); } } diff --git a/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs index 65c4e506ce..a083f89cc6 100644 --- a/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs +++ b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs @@ -1,76 +1,62 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Configuration; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Get concatenated user and default character replacements +/// taking into account +/// +public static class RequestHandlerSettingsExtension { /// - /// Get concatenated user and default character replacements - /// taking into account + /// Get concatenated user and default character replacements + /// taking into account /// - public static class RequestHandlerSettingsExtension + public static IEnumerable GetCharReplacements(this RequestHandlerSettings requestHandlerSettings) { - /// - /// Get concatenated user and default character replacements - /// taking into account - /// - public static IEnumerable GetCharReplacements(this RequestHandlerSettings requestHandlerSettings) + if (requestHandlerSettings.EnableDefaultCharReplacements is false) { - if (requestHandlerSettings.EnableDefaultCharReplacements is false) - { - return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty(); - } - - if (requestHandlerSettings.UserDefinedCharCollection == null || requestHandlerSettings.UserDefinedCharCollection.Any() is false) - { - return RequestHandlerSettings.DefaultCharCollection; - } - - return MergeUnique(requestHandlerSettings.UserDefinedCharCollection, RequestHandlerSettings.DefaultCharCollection); + return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty(); } - private static IEnumerable GetReplacements(IConfiguration configuration, string key) + if (requestHandlerSettings.UserDefinedCharCollection == null || + requestHandlerSettings.UserDefinedCharCollection.Any() is false) { - var replacements = new List(); - IEnumerable config = configuration.GetSection(key).GetChildren(); - - foreach (IConfigurationSection section in config) - { - var @char = section.GetValue(nameof(CharItem.Char)); - var replacement = section.GetValue(nameof(CharItem.Replacement)); - replacements.Add(new CharItem { Char = @char, Replacement = replacement }); - } - - return replacements; + return RequestHandlerSettings.DefaultCharCollection; } - /// - /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in alternativeReplacements - /// - private static IEnumerable MergeUnique( - IEnumerable priorityReplacements, - IEnumerable alternativeReplacements) - { - var priorityReplacementsList = priorityReplacements.ToList(); - var alternativeReplacementsList = alternativeReplacements.ToList(); + return MergeUnique( + requestHandlerSettings.UserDefinedCharCollection, + RequestHandlerSettings.DefaultCharCollection); + } - foreach (CharItem alternativeReplacement in alternativeReplacementsList) + /// + /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in + /// alternativeReplacements + /// + private static IEnumerable MergeUnique( + IEnumerable priorityReplacements, + IEnumerable alternativeReplacements) + { + var priorityReplacementsList = priorityReplacements.ToList(); + var alternativeReplacementsList = alternativeReplacements.ToList(); + + foreach (CharItem alternativeReplacement in alternativeReplacementsList) + { + foreach (CharItem priorityReplacement in priorityReplacementsList) { - foreach (CharItem priorityReplacement in priorityReplacementsList) + if (priorityReplacement.Char == alternativeReplacement.Char) { - if (priorityReplacement.Char == alternativeReplacement.Char) - { - alternativeReplacement.Replacement = priorityReplacement.Replacement; - } + alternativeReplacement.Replacement = priorityReplacement.Replacement; } } - - return priorityReplacementsList.Union( - alternativeReplacementsList, - new CharacterReplacementEqualityComparer()); } + + return priorityReplacementsList.Union( + alternativeReplacementsList, + new CharacterReplacementEqualityComparer()); } } diff --git a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs index 72930b89f8..219b73c39f 100644 --- a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs +++ b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs @@ -1,33 +1,34 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class RuntimeStateExtensions { - public static class RuntimeStateExtensions - { - /// - /// Returns true if the installer is enabled based on the current runtime state - /// - /// - /// - public static bool EnableInstaller(this IRuntimeState state) - => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade; - // TODO: If we want to enable the installer for package migrations, but IMO i think we should do migrations in the back office - // if they are not unattended. - //=> state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade || state.Level == RuntimeLevel.PackageMigrations; + /// + /// Returns true if the installer is enabled based on the current runtime state + /// + /// + /// + public static bool EnableInstaller(this IRuntimeState state) + => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade; - /// - /// Returns true if Umbraco is greater than - /// - public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + // TODO: If we want to enable the installer for package migrations, but IMO i think we should do migrations in the back office + // if they are not unattended. + // => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade || state.Level == RuntimeLevel.PackageMigrations; - /// - /// Returns true if the runtime state indicates that unattended boot logic should execute - /// - /// - /// - public static bool RunUnattendedBootLogic(this IRuntimeState state) - => (state.Reason == RuntimeLevelReason.UpgradeMigrations || state.Reason == RuntimeLevelReason.UpgradePackageMigrations) - && state.Level == RuntimeLevel.Run; - } + /// + /// Returns true if Umbraco is greater than + /// + public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + + /// + /// Returns true if the runtime state indicates that unattended boot logic should execute + /// + /// + /// + public static bool RunUnattendedBootLogic(this IRuntimeState state) + => (state.Reason == RuntimeLevelReason.UpgradeMigrations || + state.Reason == RuntimeLevelReason.UpgradePackageMigrations) + && state.Level == RuntimeLevel.Run; } diff --git a/src/Umbraco.Core/Extensions/SemVersionExtensions.cs b/src/Umbraco.Core/Extensions/SemVersionExtensions.cs index e8b2a2534b..afdd49612e 100644 --- a/src/Umbraco.Core/Extensions/SemVersionExtensions.cs +++ b/src/Umbraco.Core/Extensions/SemVersionExtensions.cs @@ -1,22 +1,19 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Semver; -namespace Umbraco.Extensions -{ - public static class SemVersionExtensions - { - public static string ToSemanticString(this SemVersion semVersion) - { - return semVersion.ToString().Replace("--", "-").Replace("-+", "+"); - } +namespace Umbraco.Extensions; - public static string ToSemanticStringWithoutBuild(this SemVersion semVersion) - { - var version = semVersion.ToSemanticString(); - var indexOfBuild = version.IndexOf('+'); - return indexOfBuild >= 0 ? version.Substring(0, indexOfBuild) : version; - } +public static class SemVersionExtensions +{ + public static string ToSemanticString(this SemVersion semVersion) => + semVersion.ToString().Replace("--", "-").Replace("-+", "+"); + + public static string ToSemanticStringWithoutBuild(this SemVersion semVersion) + { + var version = semVersion.ToSemanticString(); + var indexOfBuild = version.IndexOf('+'); + return indexOfBuild >= 0 ? version[..indexOfBuild] : version; } } diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs index c41bc290ff..694b4d05e6 100644 --- a/src/Umbraco.Core/Extensions/StringExtensions.cs +++ b/src/Umbraco.Core/Extensions/StringExtensions.cs @@ -1,1465 +1,1563 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; -using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// String extension methods +/// +public static class StringExtensions { - /// - /// String extension methods - /// - public static class StringExtensions + internal static readonly Lazy Whitespace = new(() => new Regex(@"\s+", RegexOptions.Compiled)); + + private const char DefaultEscapedStringEscapeChar = '\\'; + private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); + private static readonly char[] ToCSharpEscapeChars; + internal static readonly string[] JsonEmpties = { "[]", "{}" }; + + /// + /// The namespace for URLs (from RFC 4122, Appendix C). + /// See RFC 4122 + /// + internal static readonly Guid UrlNamespace = new("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + + private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); + + // From: http://stackoverflow.com/a/961504/5018 + // filters control characters but allows only properly-formed surrogate sequences + private static readonly Lazy InvalidXmlChars = new(() => + new Regex( + @"(? e[0]) + 1]; + foreach (var escape in escapes) { - var escapes = new[] { "\aa", "\bb", "\ff", "\nn", "\rr", "\tt", "\vv", "\"\"", "\\\\", "??", "\00" }; - ToCSharpEscapeChars = new char[escapes.Max(e => e[0]) + 1]; - foreach (var escape in escapes) - ToCSharpEscapeChars[escape[0]] = escape[1]; + ToCSharpEscapeChars[escape[0]] = escape[1]; } + } - /// - /// Convert a path to node ids in the order from right to left (deepest to shallowest) - /// - /// - /// - public static int[] GetIdsFromPathReversed(this string path) + /// + /// Convert a path to node ids in the order from right to left (deepest to shallowest) + /// + /// + /// + public static int[] GetIdsFromPathReversed(this string path) + { + var nodeIds = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var output) + ? Attempt.Succeed(output) + : Attempt.Fail()) + .Where(x => x.Success) + .Select(x => x.Result) + .Reverse() + .ToArray(); + return nodeIds; + } + + /// + /// Removes new lines and tabs + /// + /// + /// + public static string StripWhitespace(this string txt) => Regex.Replace(txt, @"\s", string.Empty); + + public static string StripFileExtension(this string fileName) + { + // filenames cannot contain line breaks + if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n")) { - var nodeIds = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var output) ? Attempt.Succeed(output) : Attempt.Fail()) - .Where(x => x.Success) - .Select(x=>x.Result) - .Reverse() - .ToArray(); - return nodeIds; - } - - /// - /// Removes new lines and tabs - /// - /// - /// - public static string StripWhitespace(this string txt) - { - return Regex.Replace(txt, @"\s", string.Empty); - } - - public static string StripFileExtension(this string fileName) - { - //filenames cannot contain line breaks - if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n")) return fileName; - - var lastIndex = fileName.LastIndexOf('.'); - if (lastIndex > 0) - { - var ext = fileName.Substring(lastIndex); - //file extensions cannot contain whitespace - if (ext.Contains(" ")) return fileName; - - return string.Format("{0}", fileName.Substring(0, fileName.IndexOf(ext, StringComparison.Ordinal))); - } - return fileName; - - } - /// - /// Determines the extension of the path or URL - /// - /// - /// Extension of the file - public static string GetFileExtension(this string file) + var lastIndex = fileName.LastIndexOf('.'); + if (lastIndex > 0) { - //Find any characters between the last . and the start of a query string or the end of the string - const string pattern = @"(?\.[^\.\?]+)(\?.*|$)"; - var match = Regex.Match(file, pattern); - return match.Success - ? match.Groups["extension"].Value - : string.Empty; - } + var ext = fileName.Substring(lastIndex); - /// - /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing - /// a try/catch when deserializing when it is not json. - /// - /// - /// - public static bool DetectIsJson(this string input) - { - if (input.IsNullOrWhiteSpace()) return false; - input = input.Trim(); - return (input.StartsWith("{") && input.EndsWith("}")) - || (input.StartsWith("[") && input.EndsWith("]")); - } - - internal static readonly Lazy Whitespace = new Lazy(() => new Regex(@"\s+", RegexOptions.Compiled)); - internal static readonly string[] JsonEmpties = { "[]", "{}" }; - public static bool DetectIsEmptyJson(this string input) - { - return JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); - } - - public static string ReplaceNonAlphanumericChars(this string input, string replacement) - { - //any character that is not alphanumeric, convert to a hyphen - var mName = input; - foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) + // file extensions cannot contain whitespace + if (ext.Contains(" ")) { - mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement); - } - return mName; - } - - public static string ReplaceNonAlphanumericChars(this string input, char replacement) - { - var inputArray = input.ToCharArray(); - var outputArray = new char[input.Length]; - for (var i = 0; i < inputArray.Length; i++) - outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; - return new string(outputArray); - } - private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); - - /// - /// Cleans string to aid in preventing xss attacks. - /// - /// - /// - /// - public static string CleanForXss(this string input, params char[] ignoreFromClean) - { - //remove any HTML - input = input.StripHtml(); - //strip out any potential chars involved with XSS - return input.ExceptChars(new HashSet(CleanForXssChars.Except(ignoreFromClean))); - } - - public static string ExceptChars(this string str, HashSet toExclude) - { - var sb = new StringBuilder(str.Length); - foreach (var c in str.Where(c => toExclude.Contains(c) == false)) - { - sb.Append(c); - } - return sb.ToString(); - } - - /// - /// Returns a stream from a string - /// - /// - /// - internal static Stream GenerateStreamFromString(this string s) - { - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(s); - writer.Flush(); - stream.Position = 0; - return stream; - } - - /// - /// This will append the query string to the URL - /// - /// - /// - /// - /// - /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are - /// delimited properly with '&' - /// - public static string AppendQueryStringToUrl(this string url, params string[] queryStrings) - { - //remove any prefixed '&' or '?' - for (var i = 0; i < queryStrings.Length; i++) - { - queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand).TrimEnd(Constants.CharArrays.Ampersand); + return fileName; } - var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); - - if (url.Contains("?")) - { - return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); - } - return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); + return string.Format("{0}", fileName.Substring(0, fileName.IndexOf(ext, StringComparison.Ordinal))); } + return fileName; + } - //this is from SqlMetal and just makes it a bit of fun to allow pluralization - public static string MakePluralName(this string name) - { - if ((name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || name.EndsWith("ch", StringComparison.OrdinalIgnoreCase)) || (name.EndsWith("s", StringComparison.OrdinalIgnoreCase) || name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))) - { - name = name + "es"; - return name; - } - if ((name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && (name.Length > 1)) && !IsVowel(name[name.Length - 2])) - { - name = name.Remove(name.Length - 1, 1); - name = name + "ies"; - return name; - } - if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) - { - name = name + "s"; - } - return name; - } + /// + /// Determines the extension of the path or URL + /// + /// + /// Extension of the file + public static string GetFileExtension(this string file) + { + // Find any characters between the last . and the start of a query string or the end of the string + const string pattern = @"(?\.[^\.\?]+)(\?.*|$)"; + Match match = Regex.Match(file, pattern); + return match.Success + ? match.Groups["extension"].Value + : string.Empty; + } - public static bool IsVowel(this char c) + /// + /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing + /// a try/catch when deserializing when it is not json. + /// + /// + /// + public static bool DetectIsJson(this string input) + { + if (input.IsNullOrWhiteSpace()) { - switch (c) - { - case 'O': - case 'U': - case 'Y': - case 'A': - case 'E': - case 'I': - case 'o': - case 'u': - case 'y': - case 'a': - case 'e': - case 'i': - return true; - } return false; } - /// - /// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts char or char[]. - /// - /// The value. - /// For removing. - /// - public static string Trim(this string value, string forRemoving) + input = input.Trim(); + return (input.StartsWith("{") && input.EndsWith("}")) + || (input.StartsWith("[") && input.EndsWith("]")); + } + + public static bool DetectIsEmptyJson(this string input) => + JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); + + public static string ReplaceNonAlphanumericChars(this string input, string replacement) + { + // any character that is not alphanumeric, convert to a hyphen + var mName = input; + foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) { - if (string.IsNullOrEmpty(value)) return value; - return value.TrimEnd(forRemoving).TrimStart(forRemoving); + mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement); } - public static string EncodeJsString(this string s) + return mName; + } + + public static string ReplaceNonAlphanumericChars(this string input, char replacement) + { + var inputArray = input.ToCharArray(); + var outputArray = new char[input.Length]; + for (var i = 0; i < inputArray.Length; i++) { - var sb = new StringBuilder(); - foreach (var c in s) - { - switch (c) - { - case '\"': - sb.Append("\\\""); - break; - case '\\': - sb.Append("\\\\"); - break; - case '\b': - sb.Append("\\b"); - break; - case '\f': - sb.Append("\\f"); - break; - case '\n': - sb.Append("\\n"); - break; - case '\r': - sb.Append("\\r"); - break; - case '\t': - sb.Append("\\t"); - break; - default: - int i = (int)c; - if (i < 32 || i > 127) - { - sb.AppendFormat("\\u{0:X04}", i); - } - else - { - sb.Append(c); - } - break; - } - } - return sb.ToString(); + outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; } - public static string TrimEnd(this string value, string forRemoving) - { - if (string.IsNullOrEmpty(value)) return value; - if (string.IsNullOrEmpty(forRemoving)) return value; + return new string(outputArray); + } - while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) - { - value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase)); - } - return value; + /// + /// Cleans string to aid in preventing xss attacks. + /// + /// + /// + /// + public static string CleanForXss(this string input, params char[] ignoreFromClean) + { + // remove any HTML + input = input.StripHtml(); + + // strip out any potential chars involved with XSS + return input.ExceptChars(new HashSet(CleanForXssChars.Except(ignoreFromClean))); + } + + public static string ExceptChars(this string str, HashSet toExclude) + { + var sb = new StringBuilder(str.Length); + foreach (var c in str.Where(c => toExclude.Contains(c) == false)) + { + sb.Append(c); } - public static string TrimStart(this string value, string forRemoving) - { - if (string.IsNullOrEmpty(value)) return value; - if (string.IsNullOrEmpty(forRemoving)) return value; + return sb.ToString(); + } - while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) - { - value = value.Substring(forRemoving.Length); - } - return value; + /// + /// This will append the query string to the URL + /// + /// + /// + /// + /// + /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are + /// delimited properly with '&' + /// + public static string AppendQueryStringToUrl(this string url, params string[] queryStrings) + { + // remove any prefixed '&' or '?' + for (var i = 0; i < queryStrings.Length; i++) + { + queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand) + .TrimEnd(Constants.CharArrays.Ampersand); } - public static string EnsureStartsWith(this string input, string toStartWith) + var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); + + if (url.Contains("?")) { - if (input.StartsWith(toStartWith)) return input; - return toStartWith + input.TrimStart(toStartWith); + return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); } - public static string EnsureStartsWith(this string input, char value) + return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); + } + + /// + /// Returns a stream from a string + /// + /// + /// + internal static Stream GenerateStreamFromString(this string s) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(s); + writer.Flush(); + stream.Position = 0; + return stream; + } + + // this is from SqlMetal and just makes it a bit of fun to allow pluralization + public static string MakePluralName(this string name) + { + if (name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || + name.EndsWith("ch", StringComparison.OrdinalIgnoreCase) || + name.EndsWith("s", StringComparison.OrdinalIgnoreCase) || + name.EndsWith("sh", StringComparison.OrdinalIgnoreCase)) { - return input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input; + name += "es"; + return name; } - public static string EnsureEndsWith(this string input, char value) + if (name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && name.Length > 1 && + !IsVowel(name[^2])) { - return input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value; + name = name.Remove(name.Length - 1, 1); + name += "ies"; + return name; } - public static string EnsureEndsWith(this string input, string toEndWith) + if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) { - return input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith; + name += "s"; } - public static bool IsLowerCase(this char ch) - { - return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); - } + return name; + } - public static bool IsUpperCase(this char ch) + public static bool IsVowel(this char c) + { + switch (c) { - return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(); - } - - /// Indicates whether a specified string is null, empty, or - /// consists only of white-space characters. - /// The value to check. - /// Returns if the value is null, - /// empty, or consists only of white-space characters, otherwise - /// returns . - public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value); - - public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) - { - return str.IsNullOrWhiteSpace() ? defaultValue : str; - } - - /// The to delimited list. - /// The list. - /// The delimiter. - /// the list - [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")] - public static IList ToDelimitedList(this string list, string delimiter = ",") - { - var delimiters = new[] { delimiter }; - return !list.IsNullOrWhiteSpace() - ? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) - .Select(i => i.Trim()) - .ToList() - : new List(); - } - - /// enum try parse. - /// The str type. - /// The ignore case. - /// The result. - /// The type - /// The enum try parse. - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] - [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] - public static bool EnumTryParse(this string strType, bool ignoreCase, out T? result) - { - try - { - result = (T)Enum.Parse(typeof(T), strType, ignoreCase); + case 'O': + case 'U': + case 'Y': + case 'A': + case 'E': + case 'I': + case 'o': + case 'u': + case 'y': + case 'a': + case 'e': + case 'i': return true; - } - catch + } + + return false; + } + + /// + /// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts + /// char or char[]. + /// + /// The value. + /// For removing. + /// + public static string Trim(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + return value.TrimEnd(forRemoving).TrimStart(forRemoving); + } + + public static string EncodeJsString(this string s) + { + var sb = new StringBuilder(); + foreach (var c in s) + { + switch (c) { - result = default(T); - return false; + case '\"': + sb.Append("\\\""); + break; + case '\\': + sb.Append("\\\\"); + break; + case '\b': + sb.Append("\\b"); + break; + case '\f': + sb.Append("\\f"); + break; + case '\n': + sb.Append("\\n"); + break; + case '\r': + sb.Append("\\r"); + break; + case '\t': + sb.Append("\\t"); + break; + default: + int i = c; + if (i < 32 || i > 127) + { + sb.AppendFormat("\\u{0:X04}", i); + } + else + { + sb.Append(c); + } + + break; } } - /// - /// Parse string to Enum - /// - /// The enum type - /// The string to parse - /// The ignore case - /// The parsed enum - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] - [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] - public static T EnumParse(this string strType, bool ignoreCase) + return sb.ToString(); + } + + public static string TrimEnd(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) { - return (T)Enum.Parse(typeof(T), strType, ignoreCase); + return value; } - /// - /// Strips all HTML from a string. - /// - /// The text. - /// Returns the string without any HTML tags. - public static string StripHtml(this string text) + if (string.IsNullOrEmpty(forRemoving)) { - const string pattern = @"<(.|\n)*?>"; - return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); + return value; } - /// - /// Encodes as GUID. - /// - /// The input. - /// - public static Guid EncodeAsGuid(this string input) + while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) { - if (string.IsNullOrWhiteSpace(input)) throw new ArgumentNullException("input"); - - var convertToHex = input.ConvertToHex(); - var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32; - var hex = convertToHex.Substring(0, hexLength).PadLeft(32, '0'); - var output = Guid.Empty; - return Guid.TryParse(hex, out output) ? output : Guid.Empty; + value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase)); } - /// - /// Converts to hex. - /// - /// The input. - /// - public static string ConvertToHex(this string input) - { - if (string.IsNullOrEmpty(input)) return string.Empty; + return value; + } - var sb = new StringBuilder(input.Length); - foreach (var c in input) + public static string TrimStart(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + if (string.IsNullOrEmpty(forRemoving)) + { + return value; + } + + while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) + { + value = value.Substring(forRemoving.Length); + } + + return value; + } + + public static string EnsureStartsWith(this string input, string toStartWith) + { + if (input.StartsWith(toStartWith)) + { + return input; + } + + return toStartWith + input.TrimStart(toStartWith); + } + + public static string EnsureStartsWith(this string input, char value) => + input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input; + + public static string EnsureEndsWith(this string input, char value) => + input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value; + + public static string EnsureEndsWith(this string input, string toEndWith) => + input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith; + + public static bool IsLowerCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) == + ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); + + public static bool IsUpperCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) == + ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(); + + /// + /// Indicates whether a specified string is null, empty, or + /// consists only of white-space characters. + /// + /// The value to check. + /// + /// Returns if the value is null, + /// empty, or consists only of white-space characters, otherwise + /// returns . + /// + public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value); + + public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) => + str.IsNullOrWhiteSpace() ? defaultValue : str; + + /// The to delimited list. + /// The list. + /// The delimiter. + /// the list + [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")] + public static IList ToDelimitedList(this string list, string delimiter = ",") + { + var delimiters = new[] { delimiter }; + return !list.IsNullOrWhiteSpace() + ? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) + .Select(i => i.Trim()) + .ToList() + : new List(); + } + + /// enum try parse. + /// The str type. + /// The ignore case. + /// The result. + /// The type + /// The enum try parse. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] + public static bool EnumTryParse(this string strType, bool ignoreCase, out T? result) + { + try + { + result = (T)Enum.Parse(typeof(T), strType, ignoreCase); + return true; + } + catch + { + result = default; + return false; + } + } + + /// + /// Parse string to Enum + /// + /// The enum type + /// The string to parse + /// The ignore case + /// The parsed enum + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] + public static T EnumParse(this string strType, bool ignoreCase) => (T)Enum.Parse(typeof(T), strType, ignoreCase); + + /// + /// Strips all HTML from a string. + /// + /// The text. + /// Returns the string without any HTML tags. + public static string StripHtml(this string text) + { + const string pattern = @"<(.|\n)*?>"; + return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); + } + + /// + /// Encodes as GUID. + /// + /// The input. + /// + public static Guid EncodeAsGuid(this string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + throw new ArgumentNullException("input"); + } + + var convertToHex = input.ConvertToHex(); + var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32; + var hex = convertToHex.Substring(0, hexLength).PadLeft(32, '0'); + Guid output = Guid.Empty; + return Guid.TryParse(hex, out output) ? output : Guid.Empty; + } + + /// + /// Converts to hex. + /// + /// The input. + /// + public static string ConvertToHex(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + var sb = new StringBuilder(input.Length); + foreach (var c in input) + { + sb.AppendFormat("{0:x2}", Convert.ToUInt32(c)); + } + + return sb.ToString(); + } + + public static string DecodeFromHex(this string hexValue) + { + var strValue = string.Empty; + while (hexValue.Length > 0) + { + strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); + hexValue = hexValue.Substring(2, hexValue.Length - 2); + } + + return strValue; + } + + /// + /// Encodes a string to a safe URL base64 string + /// + /// + /// + public static string ToUrlBase64(this string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + // return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ","); + var bytes = Encoding.UTF8.GetBytes(input); + return UrlTokenEncode(bytes); + } + + /// + /// Decodes a URL safe base64 string back + /// + /// + /// + public static string? FromUrlBase64(this string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + // if (input.IsInvalidBase64()) return null; + try + { + // var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "=")); + var decodedBytes = UrlTokenDecode(input); + return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null; + } + catch (FormatException) + { + return null; + } + } + + /// + /// formats the string with invariant culture + /// + /// The format. + /// The args. + /// + public static string InvariantFormat(this string? format, params object?[] args) => + string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args); + + /// + /// Converts an integer to an invariant formatted string + /// + /// + /// + public static string ToInvariantString(this int str) => str.ToString(CultureInfo.InvariantCulture); + + public static string ToInvariantString(this long str) => str.ToString(CultureInfo.InvariantCulture); + + /// + /// Compares 2 strings with invariant culture and case ignored + /// + /// The compare. + /// The compare to. + /// + public static bool InvariantEquals(this string? compare, string? compareTo) => + string.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase); + + public static bool InvariantStartsWith(this string compare, string compareTo) => + compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + + public static bool InvariantEndsWith(this string compare, string compareTo) => + compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + + public static bool InvariantContains(this string compare, string compareTo) => + compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; + + public static bool InvariantContains(this IEnumerable compare, string compareTo) => + compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); + + public static int InvariantIndexOf(this string s, string value) => + s.IndexOf(value, StringComparison.OrdinalIgnoreCase); + + public static int InvariantLastIndexOf(this string s, string value) => + s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); + + /// + /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method + /// + /// + /// + /// + public static T? ParseInto(this string val) => (T?)val.ParseInto(typeof(T)); + + /// + /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method + /// + /// + /// + /// + public static object? ParseInto(this string val, Type type) + { + if (string.IsNullOrEmpty(val) == false) + { + TypeConverter tc = TypeDescriptor.GetConverter(type); + return tc.ConvertFrom(val); + } + + return val; + } + + /// + /// Generates a hash of a string based on the FIPS compliance setting. + /// + /// Refers to itself + /// The hashed string + public static string GenerateHash(this string str) => str.ToSHA1(); + + /// + /// Generate a hash of a string based on the specified hash algorithm. + /// + /// The hash algorithm implementation to use. + /// The to hash. + /// + /// The hashed string. + /// + public static string GenerateHash(this string str) + where T : HashAlgorithm => str.GenerateHash(typeof(T).FullName); + + /// + /// Converts the string to SHA1 + /// + /// refers to itself + /// The SHA1 hashed string + public static string ToSHA1(this string stringToConvert) => stringToConvert.GenerateHash("SHA1"); + + /// + /// Decodes a string that was encoded with UrlTokenEncode + /// + /// + /// + public static byte[] UrlTokenDecode(this string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + if (input.Length == 0) + { + return Array.Empty(); + } + + // calc array size - must be groups of 4 + var arrayLength = input.Length; + var remain = arrayLength % 4; + if (remain != 0) + { + arrayLength += 4 - remain; + } + + var inArray = new char[arrayLength]; + for (var i = 0; i < input.Length; i++) + { + var ch = input[i]; + switch (ch) { - sb.AppendFormat("{0:x2}", Convert.ToUInt32(c)); + case '-': // restore '-' as '+' + inArray[i] = '+'; + break; + + case '_': // restore '_' as '/' + inArray[i] = '/'; + break; + + default: // keep char unchanged + inArray[i] = ch; + break; } - return sb.ToString(); } - public static string DecodeFromHex(this string hexValue) + // pad with '=' + for (var j = input.Length; j < inArray.Length; j++) { - var strValue = ""; - while (hexValue.Length > 0) + inArray[j] = '='; + } + + return Convert.FromBase64CharArray(inArray, 0, inArray.Length); + } + + /// + /// Generate a hash of a string based on the hashType passed in + /// + /// Refers to itself + /// + /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a + /// list of possible values. + /// + /// The hashed string + private static string GenerateHash(this string str, string? hashType) + { + HashAlgorithm? hasher = null; + + // create an instance of the correct hashing provider based on the type passed in + if (hashType is not null) + { + hasher = HashAlgorithm.Create(hashType); + } + + if (hasher == null) + { + throw new InvalidOperationException("No hashing type found by name " + hashType); + } + + using (hasher) + { + // convert our string into byte array + var byteArray = Encoding.UTF8.GetBytes(str); + + // get the hashed values created by our selected provider + var hashedByteArray = hasher.ComputeHash(byteArray); + + // create a StringBuilder object + var stringBuilder = new StringBuilder(); + + // loop to each byte + foreach (var b in hashedByteArray) { - strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); - hexValue = hexValue.Substring(2, hexValue.Length - 2); + // append it to our StringBuilder + stringBuilder.Append(b.ToString("x2")); } - return strValue; + + // return the hashed value + return stringBuilder.ToString(); + } + } + + /// + /// Encodes a string so that it is 'safe' for URLs, files, etc.. + /// + /// + /// + public static string UrlTokenEncode(this byte[] input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); } - /// - /// Encodes a string to a safe URL base64 string - /// - /// - /// - public static string ToUrlBase64(this string input) + if (input.Length == 0) { - if (input == null) throw new ArgumentNullException(nameof(input)); - - if (string.IsNullOrEmpty(input)) - return string.Empty; - - //return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ","); - var bytes = Encoding.UTF8.GetBytes(input); - return UrlTokenEncode(bytes); + return string.Empty; } - /// - /// Decodes a URL safe base64 string back - /// - /// - /// - public static string? FromUrlBase64(this string input) + // base-64 digits are A-Z, a-z, 0-9, + and / + // the = char is used for trailing padding + var str = Convert.ToBase64String(input); + + var pos = str.IndexOf('='); + if (pos < 0) { - if (input == null) throw new ArgumentNullException(nameof(input)); + pos = str.Length; + } - //if (input.IsInvalidBase64()) return null; - - try + // replace chars that would cause problems in URLs + var chArray = new char[pos]; + for (var i = 0; i < pos; i++) + { + var ch = str[i]; + switch (ch) { - //var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "=")); - var decodedBytes = UrlTokenDecode(input); - return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null; - } - catch (FormatException) - { - return null; + case '+': // replace '+' with '-' + chArray[i] = '-'; + break; + + case '/': // replace '/' with '_' + chArray[i] = '_'; + break; + + default: // keep char unchanged + chArray[i] = ch; + break; } } - /// - /// formats the string with invariant culture - /// - /// The format. - /// The args. - /// - public static string InvariantFormat(this string? format, params object?[] args) + return new string(chArray); + } + + /// + /// Ensures that the folder path ends with a DirectorySeparatorChar + /// + /// + /// + public static string NormaliseDirectoryPath(this string currentFolder) + { + currentFolder = currentFolder + .IfNull(x => string.Empty) + .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + return currentFolder; + } + + /// + /// Truncates the specified text string. + /// + /// The text. + /// Length of the max. + /// The suffix. + /// + public static string Truncate(this string text, int maxLength, string suffix = "...") + { + // replaces the truncated string to a ... + var truncatedString = text; + + if (maxLength <= 0) { - return string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args); - } - - /// - /// Converts an integer to an invariant formatted string - /// - /// - /// - public static string ToInvariantString(this int str) - { - return str.ToString(CultureInfo.InvariantCulture); - } - - public static string ToInvariantString(this long str) - { - return str.ToString(CultureInfo.InvariantCulture); - } - - /// - /// Compares 2 strings with invariant culture and case ignored - /// - /// The compare. - /// The compare to. - /// - public static bool InvariantEquals(this string? compare, string? compareTo) - { - return String.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase); - } - - public static bool InvariantStartsWith(this string compare, string compareTo) - { - return compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); - } - - public static bool InvariantEndsWith(this string compare, string compareTo) - { - return compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); - } - - public static bool InvariantContains(this string compare, string compareTo) - { - return compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; - } - - public static bool InvariantContains(this IEnumerable compare, string compareTo) - { - return compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); - } - - public static int InvariantIndexOf(this string s, string value) - { - return s.IndexOf(value, StringComparison.OrdinalIgnoreCase); - } - - public static int InvariantLastIndexOf(this string s, string value) - { - return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); - } - - - /// - /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method - /// - /// - /// - /// - public static T? ParseInto(this string val) - { - return (T?)val.ParseInto(typeof(T)); - } - - /// - /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method - /// - /// - /// - /// - public static object? ParseInto(this string val, Type type) - { - if (string.IsNullOrEmpty(val) == false) - { - TypeConverter tc = TypeDescriptor.GetConverter(type); - return tc.ConvertFrom(val); - } - return val; - } - - /// - /// Generates a hash of a string based on the FIPS compliance setting. - /// - /// Refers to itself - /// The hashed string - public static string GenerateHash(this string str) => str.ToSHA1(); - - /// - /// Generate a hash of a string based on the specified hash algorithm. - /// - /// The hash algorithm implementation to use. - /// The to hash. - /// - /// The hashed string. - /// - public static string GenerateHash(this string str) - where T : HashAlgorithm => str.GenerateHash(typeof(T).FullName); - - /// - /// Converts the string to SHA1 - /// - /// refers to itself - /// The SHA1 hashed string - public static string ToSHA1(this string stringToConvert) => stringToConvert.GenerateHash("SHA1"); - - /// Generate a hash of a string based on the hashType passed in - /// - /// Refers to itself - /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a list of possible values. - /// The hashed string - private static string GenerateHash(this string str, string? hashType) - { - HashAlgorithm? hasher = null; - //create an instance of the correct hashing provider based on the type passed in - if (hashType is not null) - { - hasher = HashAlgorithm.Create(hashType); - } - - if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType); - using (hasher) - { - //convert our string into byte array - var byteArray = Encoding.UTF8.GetBytes(str); - - //get the hashed values created by our selected provider - var hashedByteArray = hasher.ComputeHash(byteArray); - - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each byte - foreach (var b in hashedByteArray) - { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2")); - } - - //return the hashed value - return stringBuilder.ToString(); - } - } - - /// - /// Decodes a string that was encoded with UrlTokenEncode - /// - /// - /// - public static byte[] UrlTokenDecode(this string input) - { - if (input == null) - throw new ArgumentNullException(nameof(input)); - - if (input.Length == 0) - return Array.Empty(); - - // calc array size - must be groups of 4 - var arrayLength = input.Length; - var remain = arrayLength % 4; - if (remain != 0) arrayLength += 4 - remain; - - var inArray = new char[arrayLength]; - for (var i = 0; i < input.Length; i++) - { - var ch = input[i]; - switch (ch) - { - case '-': // restore '-' as '+' - inArray[i] = '+'; - break; - - case '_': // restore '_' as '/' - inArray[i] = '/'; - break; - - default: // keep char unchanged - inArray[i] = ch; - break; - } - } - - // pad with '=' - for (var j = input.Length; j < inArray.Length; j++) - inArray[j] = '='; - - return Convert.FromBase64CharArray(inArray, 0, inArray.Length); - } - - /// - /// Encodes a string so that it is 'safe' for URLs, files, etc.. - /// - /// - /// - public static string UrlTokenEncode(this byte[] input) - { - if (input == null) - throw new ArgumentNullException(nameof(input)); - - if (input.Length == 0) - return string.Empty; - - // base-64 digits are A-Z, a-z, 0-9, + and / - // the = char is used for trailing padding - - var str = Convert.ToBase64String(input); - - var pos = str.IndexOf('='); - if (pos < 0) pos = str.Length; - - // replace chars that would cause problems in URLs - var chArray = new char[pos]; - for (var i = 0; i < pos; i++) - { - var ch = str[i]; - switch (ch) - { - case '+': // replace '+' with '-' - chArray[i] = '-'; - break; - - case '/': // replace '/' with '_' - chArray[i] = '_'; - break; - - default: // keep char unchanged - chArray[i] = ch; - break; - } - } - - return new string(chArray); - } - - /// - /// Ensures that the folder path ends with a DirectorySeparatorChar - /// - /// - /// - public static string NormaliseDirectoryPath(this string currentFolder) - { - currentFolder = currentFolder - .IfNull(x => String.Empty) - .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - return currentFolder; - } - - /// - /// Truncates the specified text string. - /// - /// The text. - /// Length of the max. - /// The suffix. - /// - public static string Truncate(this string text, int maxLength, string suffix = "...") - { - // replaces the truncated string to a ... - var truncatedString = text; - - if (maxLength <= 0) return truncatedString; - var strLength = maxLength - suffix.Length; - - if (strLength <= 0) return truncatedString; - - if (text == null || text.Length <= maxLength) return truncatedString; - - truncatedString = text.Substring(0, strLength); - truncatedString = truncatedString.TrimEnd(); - truncatedString += suffix; - return truncatedString; } - /// - /// Strips carrage returns and line feeds from the specified text. - /// - /// The input. - /// - public static string StripNewLines(this string input) + var strLength = maxLength - suffix.Length; + + if (strLength <= 0) { - return input.Replace("\r", "").Replace("\n", ""); + return truncatedString; } - /// - /// Converts to single line by replacing line breaks with spaces. - /// - public static string ToSingleLine(this string text) + if (text == null || text.Length <= maxLength) + { + return truncatedString; + } + + truncatedString = text.Substring(0, strLength); + truncatedString = truncatedString.TrimEnd(); + truncatedString += suffix; + + return truncatedString; + } + + /// + /// Strips carrage returns and line feeds from the specified text. + /// + /// The input. + /// + public static string StripNewLines(this string input) => input.Replace("\r", string.Empty).Replace("\n", string.Empty); + + /// + /// Converts to single line by replacing line breaks with spaces. + /// + public static string ToSingleLine(this string text) + { + if (string.IsNullOrEmpty(text)) { - if (string.IsNullOrEmpty(text)) return text; - text = text.Replace("\r\n", " "); // remove CRLF - text = text.Replace("\r", " "); // remove CR - text = text.Replace("\n", " "); // remove LF return text; } - public static string OrIfNullOrWhiteSpace(this string input, string alternative) + text = text.Replace("\r\n", " "); // remove CRLF + text = text.Replace("\r", " "); // remove CR + text = text.Replace("\n", " "); // remove LF + return text; + } + + public static string OrIfNullOrWhiteSpace(this string input, string alternative) => + !string.IsNullOrWhiteSpace(input) + ? input + : alternative; + + /// + /// Returns a copy of the string with the first character converted to uppercase. + /// + /// The string. + /// The converted string. + public static string ToFirstUpper(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpper() + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to lowercase. + /// + /// The string. + /// The converted string. + public static string ToFirstLower(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLower() + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the + /// specified culture. + /// + /// The string. + /// The culture. + /// The converted string. + public static string ToFirstUpper(this string input, CultureInfo culture) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpper(culture) + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the + /// specified culture. + /// + /// The string. + /// The culture. + /// The converted string. + public static string ToFirstLower(this string input, CultureInfo culture) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLower(culture) + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the + /// invariant culture. + /// + /// The string. + /// The converted string. + public static string ToFirstUpperInvariant(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpperInvariant() + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the + /// invariant culture. + /// + /// The string. + /// The converted string. + public static string ToFirstLowerInvariant(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLowerInvariant() + input.Substring(1); + + /// + /// Returns a new string in which all occurrences of specified strings are replaced by other specified strings. + /// + /// The string to filter. + /// The replacements definition. + /// The filtered string. + public static string ReplaceMany(this string text, IDictionary replacements) + { + if (text == null) { - return !string.IsNullOrWhiteSpace(input) - ? input - : alternative; + throw new ArgumentNullException(nameof(text)); } - /// - /// Returns a copy of the string with the first character converted to uppercase. - /// - /// The string. - /// The converted string. - public static string ToFirstUpper(this string input) + if (replacements == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpper() + input.Substring(1); + throw new ArgumentNullException(nameof(replacements)); } - /// - /// Returns a copy of the string with the first character converted to lowercase. - /// - /// The string. - /// The converted string. - public static string ToFirstLower(this string input) + foreach (KeyValuePair item in replacements) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLower() + input.Substring(1); + text = text.Replace(item.Key, item.Value); } - /// - /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the specified culture. - /// - /// The string. - /// The culture. - /// The converted string. - public static string ToFirstUpper(this string input, CultureInfo culture) + return text; + } + + /// + /// Returns a new string in which all occurrences of specified characters are replaced by a specified character. + /// + /// The string to filter. + /// The characters to replace. + /// The replacement character. + /// The filtered string. + public static string ReplaceMany(this string text, char[] chars, char replacement) + { + if (text == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpper(culture) + input.Substring(1); + throw new ArgumentNullException(nameof(text)); } - /// - /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the specified culture. - /// - /// The string. - /// The culture. - /// The converted string. - public static string ToFirstLower(this string input, CultureInfo culture) + if (chars == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLower(culture) + input.Substring(1); + throw new ArgumentNullException(nameof(chars)); } - /// - /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the invariant culture. - /// - /// The string. - /// The converted string. - public static string ToFirstUpperInvariant(this string input) + for (var i = 0; i < chars.Length; i++) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpperInvariant() + input.Substring(1); + text = text.Replace(chars[i], replacement); } - /// - /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the invariant culture. - /// - /// The string. - /// The converted string. - public static string ToFirstLowerInvariant(this string input) + return text; + } + + /// + /// Returns a new string in which only the first occurrence of a specified string is replaced by a specified + /// replacement string. + /// + /// The string to filter. + /// The string to replace. + /// The replacement string. + /// The filtered string. + public static string ReplaceFirst(this string text, string search, string replace) + { + if (text == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLowerInvariant() + input.Substring(1); + throw new ArgumentNullException(nameof(text)); } - /// - /// Returns a new string in which all occurrences of specified strings are replaced by other specified strings. - /// - /// The string to filter. - /// The replacements definition. - /// The filtered string. - public static string ReplaceMany(this string text, IDictionary replacements) + var pos = text.IndexOf(search, StringComparison.InvariantCulture); + + if (pos < 0) { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (replacements == null) throw new ArgumentNullException(nameof(replacements)); - - - foreach (KeyValuePair item in replacements) - text = text.Replace(item.Key, item.Value); - return text; } - /// - /// Returns a new string in which all occurrences of specified characters are replaced by a specified character. - /// - /// The string to filter. - /// The characters to replace. - /// The replacement character. - /// The filtered string. - public static string ReplaceMany(this string text, char[] chars, char replacement) + return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + } + + /// + /// An extension method that returns a new string in which all occurrences of a + /// specified string in the current instance are replaced with another specified string. + /// StringComparison specifies the type of search to use for the specified string. + /// + /// Current instance of the string + /// Specified string to replace + /// Specified string to inject + /// String Comparison object to specify search type + /// Updated string + public static string Replace(this string source, string oldString, string newString, StringComparison stringComparison) + { + // This initialization ensures the first check starts at index zero of the source. On successive checks for + // a match, the source is skipped to immediately after the last replaced occurrence for efficiency + // and to avoid infinite loops when oldString and newString compare equal. + var index = -1 * newString.Length; + + // Determine if there are any matches left in source, starting from just after the result of replacing the last match. + while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0) { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (chars == null) throw new ArgumentNullException(nameof(chars)); + // Remove the old text. + source = source.Remove(index, oldString.Length); - - for (int i = 0; i < chars.Length; i++) - text = text.Replace(chars[i], replacement); - - return text; + // Add the replacement text. + source = source.Insert(index, newString); } - /// - /// Returns a new string in which only the first occurrence of a specified string is replaced by a specified replacement string. - /// - /// The string to filter. - /// The string to replace. - /// The replacement string. - /// The filtered string. - public static string ReplaceFirst(this string text, string search, string replace) + return source; + } + + /// + /// Converts a literal string into a C# expression. + /// + /// Current instance of the string. + /// The string in a C# format. + public static string ToCSharpString(this string s) + { + if (s == null) { - if (text == null) throw new ArgumentNullException(nameof(text)); - - var pos = text.IndexOf(search, StringComparison.InvariantCulture); - - if (pos < 0) - return text; - - return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + return ""; } - - - /// - /// An extension method that returns a new string in which all occurrences of a - /// specified string in the current instance are replaced with another specified string. - /// StringComparison specifies the type of search to use for the specified string. - /// - /// Current instance of the string - /// Specified string to replace - /// Specified string to inject - /// String Comparison object to specify search type - /// Updated string - public static string Replace(this string source, string oldString, string newString, StringComparison stringComparison) + // http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal + var sb = new StringBuilder(s.Length + 2); + for (var rp = 0; rp < s.Length; rp++) { - // This initialization ensures the first check starts at index zero of the source. On successive checks for - // a match, the source is skipped to immediately after the last replaced occurrence for efficiency - // and to avoid infinite loops when oldString and newString compare equal. - int index = -1 * newString.Length; - - // Determine if there are any matches left in source, starting from just after the result of replacing the last match. - while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0) + var c = s[rp]; + if (c < ToCSharpEscapeChars.Length && ToCSharpEscapeChars[c] != '\0') { - // Remove the old text. - source = source.Remove(index, oldString.Length); - - // Add the replacement text. - source = source.Insert(index, newString); + sb.Append('\\').Append(ToCSharpEscapeChars[c]); } - - return source; - } - - /// - /// Converts a literal string into a C# expression. - /// - /// Current instance of the string. - /// The string in a C# format. - public static string ToCSharpString(this string s) - { - if (s == null) return ""; - - // http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal - - var sb = new StringBuilder(s.Length + 2); - for (var rp = 0; rp < s.Length; rp++) + else if (c <= '~' && c >= ' ') { - var c = s[rp]; - if (c < ToCSharpEscapeChars.Length && '\0' != ToCSharpEscapeChars[c]) - sb.Append('\\').Append(ToCSharpEscapeChars[c]); - else if ('~' >= c && c >= ' ') - sb.Append(c); - else - sb.Append(@"\x") - .Append(ToCSharpHexDigitLower[c >> 12 & 0x0F]) - .Append(ToCSharpHexDigitLower[c >> 8 & 0x0F]) - .Append(ToCSharpHexDigitLower[c >> 4 & 0x0F]) - .Append(ToCSharpHexDigitLower[c & 0x0F]); + sb.Append(c); } - - return sb.ToString(); - - // requires full trust - /* - using (var writer = new StringWriter()) - using (var provider = CodeDomProvider.CreateProvider("CSharp")) + else { - provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null); - return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); + sb.Append(@"\x") + .Append(ToCSharpHexDigitLower[(c >> 12) & 0x0F]) + .Append(ToCSharpHexDigitLower[(c >> 8) & 0x0F]) + .Append(ToCSharpHexDigitLower[(c >> 4) & 0x0F]) + .Append(ToCSharpHexDigitLower[c & 0x0F]); } - */ } - public static string EscapeRegexSpecialCharacters(this string text) + return sb.ToString(); + + // requires full trust + /* + using (var writer = new StringWriter()) + using (var provider = CodeDomProvider.CreateProvider("CSharp")) { - var regexSpecialCharacters = new Dictionary - { - {".", @"\."}, - {"(", @"\("}, - {")", @"\)"}, - {"]", @"\]"}, - {"[", @"\["}, - {"{", @"\{"}, - {"}", @"\}"}, - {"?", @"\?"}, - {"!", @"\!"}, - {"$", @"\$"}, - {"^", @"\^"}, - {"+", @"\+"}, - {"*", @"\*"}, - {"|", @"\|"}, - {"<", @"\<"}, - {">", @"\>"} - }; - return ReplaceMany(text, regexSpecialCharacters); + provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null); + return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); + } + */ + } + + public static string EscapeRegexSpecialCharacters(this string text) + { + var regexSpecialCharacters = new Dictionary + { + { ".", @"\." }, + { "(", @"\(" }, + { ")", @"\)" }, + { "]", @"\]" }, + { "[", @"\[" }, + { "{", @"\{" }, + { "}", @"\}" }, + { "?", @"\?" }, + { "!", @"\!" }, + { "$", @"\$" }, + { "^", @"\^" }, + { "+", @"\+" }, + { "*", @"\*" }, + { "|", @"\|" }, + { "<", @"\<" }, + { ">", @"\>" }, + }; + return ReplaceMany(text, regexSpecialCharacters); + } + + /// + /// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns + /// true if it does or false if it doesn't + /// + /// The string to check + /// The collection of strings to check are contained within the first string + /// + /// The type of comparison to perform - defaults to + /// + /// True if any of the needles are contained with haystack; otherwise returns false + /// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313 + public static bool ContainsAny(this string haystack, IEnumerable needles, StringComparison comparison = StringComparison.CurrentCulture) + { + if (haystack == null) + { + throw new ArgumentNullException("haystack"); } - /// - /// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns true if it does or false if it doesn't - /// - /// The string to check - /// The collection of strings to check are contained within the first string - /// The type of comparison to perform - defaults to - /// True if any of the needles are contained with haystack; otherwise returns false - /// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313 - public static bool ContainsAny(this string haystack, IEnumerable needles, StringComparison comparison = StringComparison.CurrentCulture) + if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any()) { - if (haystack == null) - throw new ArgumentNullException("haystack"); + return false; + } - if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any()) + return needles.Any(value => haystack.IndexOf(value, comparison) >= 0); + } + + public static bool CsvContains(this string csv, string value) + { + if (string.IsNullOrEmpty(csv)) + { + return false; + } + + var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return idCheckList.Contains(value); + } + + /// + /// Converts a file name to a friendly name for a content item + /// + /// + /// + public static string ToFriendlyName(this string fileName) + { + // strip the file extension + fileName = fileName.StripFileExtension(); + + // underscores and dashes to spaces + fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' '); + + // any other conversions ? + + // Pascalcase (to be done last) + fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); + + // Replace multiple consecutive spaces with a single space + fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries)); + + return fileName; + } + + /// + /// An extension method that returns a new string in which all occurrences of an + /// unicode characters that are invalid in XML files are replaced with an empty string. + /// + /// Current instance of the string + /// Updated string + /// + /// removes any unusual unicode characters that can't be encoded into XML + /// + public static string ToValidXmlString(this string text) => + string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, string.Empty); + + /// + /// Converts a string to a Guid - WARNING, depending on the string, this may not be unique + /// + /// + /// + public static Guid ToGuid(this string text) => + CreateGuidFromHash( + UrlNamespace, + text, + CryptoConfig.AllowOnlyFipsAlgorithms ? 5 // SHA1 + : 3); // MD5 + + /// + /// Turns an null-or-whitespace string into a null string. + /// + public static string? NullOrWhiteSpaceAsNull(this string text) + => string.IsNullOrWhiteSpace(text) ? null : text; + + /// + /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. + /// See + /// GuidUtility.cs + /// for original implementation. + /// + /// The ID of the namespace. + /// The name (within that namespace). + /// + /// The version number of the UUID to create; this value must be either + /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). + /// + /// A UUID derived from the namespace and name. + /// + /// See + /// Generating a deterministic GUID + /// . + /// + internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (version != 3 && version != 5) + { + throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5."); + } + + // convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3) + // ASSUME: UTF-8 encoding is always appropriate + var nameBytes = Encoding.UTF8.GetBytes(name); + + // convert the namespace UUID to network order (step 3) + var namespaceBytes = namespaceId.ToByteArray(); + SwapByteOrder(namespaceBytes); + + // comput the hash of the name space ID concatenated with the name (step 4) + byte[] hash; + using (HashAlgorithm algorithm = version == 3 ? MD5.Create() : SHA1.Create()) + { + algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); + algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); + hash = algorithm.Hash!; + } + + // most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12) + var newGuid = new byte[16]; + Array.Copy(hash, 0, newGuid, 0, 16); + + // set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8) + newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4)); + + // set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10) + newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80); + + // convert the resulting UUID to local byte order (step 13) + SwapByteOrder(newGuid); + return new Guid(newGuid); + } + + // Converts a GUID (expressed as a byte array) to/from network order (MSB-first). + internal static void SwapByteOrder(byte[] guid) + { + SwapBytes(guid, 0, 3); + SwapBytes(guid, 1, 2); + SwapBytes(guid, 4, 5); + SwapBytes(guid, 6, 7); + } + + private static void SwapBytes(byte[] guid, int left, int right) + { + var temp = guid[left]; + guid[left] = guid[right]; + guid[right] = temp; + } + + /// + /// Checks if a given path is a full path including drive letter + /// + /// + /// + // From: http://stackoverflow.com/a/35046453/5018 + public static bool IsFullPath(this string path) => + string.IsNullOrWhiteSpace(path) == false + && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 + && Path.IsPathRooted(path) + && Path.GetPathRoot(path)?.Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; + + // FORMAT STRINGS + + /// + /// Cleans a string to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// The short string helper. + /// The safe alias. + public static string ToSafeAlias(this string alias, IShortStringHelper? shortStringHelper) => + shortStringHelper?.CleanStringForSafeAlias(alias) ?? string.Empty; + + /// + /// Cleans a string to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// A value indicating that we want to camel-case the alias. + /// The short string helper. + /// The safe alias. + public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, bool camel) + { + var a = shortStringHelper.CleanStringForSafeAlias(alias); + if (string.IsNullOrWhiteSpace(a) || camel == false) + { + return a; + } + + return char.ToLowerInvariant(a[0]) + a.Substring(1); + } + + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// The culture. + /// The short string helper. + /// The safe alias. + public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, string culture) => + shortStringHelper.CleanStringForSafeAlias(alias, culture); + + // the new methods to get a url segment + + /// + /// Cleans a string to produce a string that can safely be used in an url segment. + /// + /// The text to filter. + /// The short string helper. + /// The safe url segment. + public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(text)); + } + + return shortStringHelper.CleanStringForUrlSegment(text); + } + + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url + /// segment. + /// + /// The text to filter. + /// The short string helper. + /// The culture. + /// The safe url segment. + public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper, string? culture) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(text)); + } + + return shortStringHelper.CleanStringForUrlSegment(text, culture); + } + + /// + /// Cleans a string. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The clean string. + /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType) => shortStringHelper.CleanString(text, stringType); + + /// + /// Cleans a string, using a specified separator. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The clean string. + /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator) => shortStringHelper.CleanString(text, stringType, separator); + + /// + /// Cleans a string in the context of a specified culture. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The culture. + /// The clean string. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, string culture) => shortStringHelper.CleanString(text, stringType, culture); + + /// + /// Cleans a string in the context of a specified culture, using a specified separator. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The culture. + /// The clean string. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator, string culture) => + shortStringHelper.CleanString(text, stringType, separator, culture); + + // note: LegacyCurrent.ShortStringHelper will produce 100% backward-compatible output for SplitPascalCasing. + // other helpers may not. DefaultCurrent.ShortStringHelper produces better, but non-compatible, results. + + /// + /// Splits a Pascal cased string into a phrase separated by spaces. + /// + /// The text to split. + /// + /// The split text. + public static string SplitPascalCasing(this string phrase, IShortStringHelper shortStringHelper) => + shortStringHelper.SplitPascalCasing(phrase, ' '); + + /// + /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a + /// filename, + /// both internally (on disk) and externally (as a url). + /// + /// The text to filter. + /// + /// The safe filename. + public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper) => + shortStringHelper.CleanStringForSafeFileName(text); + + // NOTE: Not sure what this actually does but is used a few places, need to figure it out and then move to StringExtensions and obsolete. + // it basically is yet another version of SplitPascalCasing + // plugging string extensions here to be 99% compatible + // the only diff. is with numbers, Number6Is was "Number6 Is", and the new string helper does it too, + // but the legacy one does "Number6Is"... assuming it is not a big deal. + internal static string SpaceCamelCasing(this string phrase, IShortStringHelper shortStringHelper) => + phrase.Length < 2 ? phrase : phrase.SplitPascalCasing(shortStringHelper).ToFirstUpperInvariant(); + + /// + /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a + /// filename, + /// both internally (on disk) and externally (as a url). + /// + /// The text to filter. + /// + /// The culture. + /// The safe filename. + public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper, string culture) => + shortStringHelper.CleanStringForSafeFileName(text, culture); + + /// + /// Splits a string with an escape character that allows for the split character to exist in a string + /// + /// The string to split + /// The character to split on + /// The character which can be used to escape the character to split on + /// The string split into substrings delimited by the split character + public static IEnumerable EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar) + { + if (value == null) + { + yield break; + } + + var sb = new StringBuilder(value.Length); + var escaped = false; + + foreach (var chr in value.ToCharArray()) + { + if (escaped) { - return false; + escaped = false; + sb.Append(chr); } - - return needles.Any(value => haystack.IndexOf(value, comparison) >= 0); - } - - public static bool CsvContains(this string csv, string value) - { - if (string.IsNullOrEmpty(csv)) + else if (chr == splitChar) { - return false; + yield return sb.ToString(); + sb.Clear(); } - var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - return idCheckList.Contains(value); - } - - /// - /// Converts a file name to a friendly name for a content item - /// - /// - /// - public static string ToFriendlyName(this string fileName) - { - // strip the file extension - fileName = fileName.StripFileExtension(); - - // underscores and dashes to spaces - fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' '); - - // any other conversions ? - - // Pascalcase (to be done last) - fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); - - // Replace multiple consecutive spaces with a single space - fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries)); - - return fileName; - } - - // From: http://stackoverflow.com/a/961504/5018 - // filters control characters but allows only properly-formed surrogate sequences - private static readonly Lazy InvalidXmlChars = new Lazy(() => - new Regex( - @"(? - /// An extension method that returns a new string in which all occurrences of an - /// unicode characters that are invalid in XML files are replaced with an empty string. - /// - /// Current instance of the string - /// Updated string - /// - /// - /// removes any unusual unicode characters that can't be encoded into XML - /// - public static string ToValidXmlString(this string text) - { - return string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, ""); - } - - /// - /// Converts a string to a Guid - WARNING, depending on the string, this may not be unique - /// - /// - /// - public static Guid ToGuid(this string text) - { - return CreateGuidFromHash(UrlNamespace, - text, - CryptoConfig.AllowOnlyFipsAlgorithms - ? 5 // SHA1 - : 3); // MD5 - } - - /// - /// The namespace for URLs (from RFC 4122, Appendix C). - /// - /// See RFC 4122 - /// - internal static readonly Guid UrlNamespace = new Guid("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); - - /// - /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. - /// - /// See GuidUtility.cs for original implementation. - /// - /// The ID of the namespace. - /// The name (within that namespace). - /// The version number of the UUID to create; this value must be either - /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). - /// A UUID derived from the namespace and name. - /// See Generating a deterministic GUID. - internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version) - { - if (name == null) - throw new ArgumentNullException("name"); - if (version != 3 && version != 5) - throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5."); - - // convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3) - // ASSUME: UTF-8 encoding is always appropriate - byte[] nameBytes = Encoding.UTF8.GetBytes(name); - - // convert the namespace UUID to network order (step 3) - byte[] namespaceBytes = namespaceId.ToByteArray(); - SwapByteOrder(namespaceBytes); - - // comput the hash of the name space ID concatenated with the name (step 4) - byte[] hash; - using (HashAlgorithm algorithm = version == 3 ? (HashAlgorithm)MD5.Create() : SHA1.Create()) + else if (chr == escapeChar) { - algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); - algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); - hash = algorithm.Hash!; + escaped = true; } - - // most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12) - byte[] newGuid = new byte[16]; - Array.Copy(hash, 0, newGuid, 0, 16); - - // set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8) - newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4)); - - // set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10) - newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80); - - // convert the resulting UUID to local byte order (step 13) - SwapByteOrder(newGuid); - return new Guid(newGuid); - } - - // Converts a GUID (expressed as a byte array) to/from network order (MSB-first). - internal static void SwapByteOrder(byte[] guid) - { - SwapBytes(guid, 0, 3); - SwapBytes(guid, 1, 2); - SwapBytes(guid, 4, 5); - SwapBytes(guid, 6, 7); - } - - private static void SwapBytes(byte[] guid, int left, int right) - { - byte temp = guid[left]; - guid[left] = guid[right]; - guid[right] = temp; - } - - /// - /// Turns an null-or-whitespace string into a null string. - /// - public static string? NullOrWhiteSpaceAsNull(this string text) - => string.IsNullOrWhiteSpace(text) ? null : text; - - - /// - /// Checks if a given path is a full path including drive letter - /// - /// - /// - // From: http://stackoverflow.com/a/35046453/5018 - public static bool IsFullPath(this string path) - { - return string.IsNullOrWhiteSpace(path) == false - && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 - && Path.IsPathRooted(path) - && Path.GetPathRoot(path)?.Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; - } - - // FORMAT STRINGS - - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The short string helper. - /// The safe alias. - public static string ToSafeAlias(this string alias, IShortStringHelper? shortStringHelper) - { - return shortStringHelper?.CleanStringForSafeAlias(alias) ?? string.Empty; - } - - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// A value indicating that we want to camel-case the alias. - /// The short string helper. - /// The safe alias. - public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, bool camel) - { - var a = shortStringHelper.CleanStringForSafeAlias(alias); - if (string.IsNullOrWhiteSpace(a) || camel == false) return a; - return char.ToLowerInvariant(a[0]) + a.Substring(1); - } - - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The culture. - /// The short string helper. - /// The safe alias. - public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, string culture) - { - return shortStringHelper.CleanStringForSafeAlias(alias, culture); - } - - - // the new methods to get a url segment - - /// - /// Cleans a string to produce a string that can safely be used in an url segment. - /// - /// The text to filter. - /// The short string helper. - /// The safe url segment. - public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); - - return shortStringHelper.CleanStringForUrlSegment(text); - } - - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url segment. - /// - /// The text to filter. - /// The short string helper. - /// The culture. - /// The safe url segment. - public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper, string? culture) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); - - return shortStringHelper.CleanStringForUrlSegment(text, culture); - } - - - /// - /// Cleans a string. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The clean string. - /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType) - { - return shortStringHelper.CleanString(text, stringType); - } - - /// - /// Cleans a string, using a specified separator. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The clean string. - /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator) - { - return shortStringHelper.CleanString(text, stringType, separator); - } - - /// - /// Cleans a string in the context of a specified culture. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The culture. - /// The clean string. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, string culture) - { - return shortStringHelper.CleanString(text, stringType, culture); - } - - /// - /// Cleans a string in the context of a specified culture, using a specified separator. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The culture. - /// The clean string. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator, string culture) - { - return shortStringHelper.CleanString(text, stringType, separator, culture); - } - - // note: LegacyCurrent.ShortStringHelper will produce 100% backward-compatible output for SplitPascalCasing. - // other helpers may not. DefaultCurrent.ShortStringHelper produces better, but non-compatible, results. - - /// - /// Splits a Pascal cased string into a phrase separated by spaces. - /// - /// The text to split. - /// - /// The split text. - public static string SplitPascalCasing(this string phrase, IShortStringHelper shortStringHelper) - { - return shortStringHelper.SplitPascalCasing(phrase, ' '); - } - - //NOTE: Not sure what this actually does but is used a few places, need to figure it out and then move to StringExtensions and obsolete. - // it basically is yet another version of SplitPascalCasing - // plugging string extensions here to be 99% compatible - // the only diff. is with numbers, Number6Is was "Number6 Is", and the new string helper does it too, - // but the legacy one does "Number6Is"... assuming it is not a big deal. - internal static string SpaceCamelCasing(this string phrase, IShortStringHelper shortStringHelper) - { - return phrase.Length < 2 ? phrase : phrase.SplitPascalCasing(shortStringHelper).ToFirstUpperInvariant(); - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). - /// - /// The text to filter. - /// - /// The safe filename. - public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper) - { - return shortStringHelper.CleanStringForSafeFileName(text); - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). - /// - /// The text to filter. - /// - /// The culture. - /// The safe filename. - public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper, string culture) - { - return shortStringHelper.CleanStringForSafeFileName(text, culture); - } - - /// - /// Splits a string with an escape character that allows for the split character to exist in a string - /// - /// The string to split - /// The character to split on - /// The character which can be used to escape the character to split on - /// The string split into substrings delimited by the split character - public static IEnumerable EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar) - { - if (value == null) yield break; - - var sb = new StringBuilder(value.Length); - var escaped = false; - - foreach (var chr in value.ToCharArray()) + else { - if (escaped) - { - escaped = false; - sb.Append(chr); - } - else if (chr == splitChar) - { - yield return sb.ToString(); - sb.Clear(); - } - else if (chr == escapeChar) - { - escaped = true; - } - else - { - sb.Append(chr); - } + sb.Append(chr); } - - yield return sb.ToString(); } + + yield return sb.ToString(); } } diff --git a/src/Umbraco.Core/Extensions/ThreadExtensions.cs b/src/Umbraco.Core/Extensions/ThreadExtensions.cs index 1c585a2de8..b1e5515b88 100644 --- a/src/Umbraco.Core/Extensions/ThreadExtensions.cs +++ b/src/Umbraco.Core/Extensions/ThreadExtensions.cs @@ -1,54 +1,54 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Globalization; -using System.Threading; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ThreadExtensions { - public static class ThreadExtensions + public static void SanitizeThreadCulture(this Thread thread) { - public static void SanitizeThreadCulture(this Thread thread) + // get the current culture + CultureInfo currentCulture = CultureInfo.CurrentCulture; + + // at the top of any culture should be the invariant culture - find it + // doing an .Equals comparison ensure that we *will* find it and not loop + // endlessly + CultureInfo invariantCulture = currentCulture; + while (invariantCulture.Equals(CultureInfo.InvariantCulture) == false) { - // get the current culture - var currentCulture = CultureInfo.CurrentCulture; - - // at the top of any culture should be the invariant culture - find it - // doing an .Equals comparison ensure that we *will* find it and not loop - // endlessly - var invariantCulture = currentCulture; - while (invariantCulture.Equals(CultureInfo.InvariantCulture) == false) - invariantCulture = invariantCulture.Parent; - - // now that invariant culture should be the same object as CultureInfo.InvariantCulture - // yet for some reasons, sometimes it is not - and this breaks anything that loops on - // culture.Parent until a reference equality to CultureInfo.InvariantCulture. See, for - // example, the following code in PerformanceCounterLib.IsCustomCategory: - // - // CultureInfo culture = CultureInfo.CurrentCulture; - // while (culture != CultureInfo.InvariantCulture) - // { - // library = GetPerformanceCounterLib(machine, culture); - // if (library.IsCustomCategory(category)) - // return true; - // culture = culture.Parent; - // } - // - // The reference comparisons never succeeds, hence the loop never ends, and the - // application hangs. - // - // granted, that comparison should probably be a .Equals comparison, but who knows - // how many times the framework assumes that it can do a reference comparison? So, - // better fix the cultures. - - if (ReferenceEquals(invariantCulture, CultureInfo.InvariantCulture)) - return; - - // if we do not have equality, fix cultures by replacing them with a culture with - // the same name, but obtained here and now, with a proper invariant top culture - - thread.CurrentCulture = CultureInfo.GetCultureInfo(thread.CurrentCulture.Name); - thread.CurrentUICulture = CultureInfo.GetCultureInfo(thread.CurrentUICulture.Name); + invariantCulture = invariantCulture.Parent; } + + // now that invariant culture should be the same object as CultureInfo.InvariantCulture + // yet for some reasons, sometimes it is not - and this breaks anything that loops on + // culture.Parent until a reference equality to CultureInfo.InvariantCulture. See, for + // example, the following code in PerformanceCounterLib.IsCustomCategory: + // + // CultureInfo culture = CultureInfo.CurrentCulture; + // while (culture != CultureInfo.InvariantCulture) + // { + // library = GetPerformanceCounterLib(machine, culture); + // if (library.IsCustomCategory(category)) + // return true; + // culture = culture.Parent; + // } + // + // The reference comparisons never succeeds, hence the loop never ends, and the + // application hangs. + // + // granted, that comparison should probably be a .Equals comparison, but who knows + // how many times the framework assumes that it can do a reference comparison? So, + // better fix the cultures. + if (ReferenceEquals(invariantCulture, CultureInfo.InvariantCulture)) + { + return; + } + + // if we do not have equality, fix cultures by replacing them with a culture with + // the same name, but obtained here and now, with a proper invariant top culture + thread.CurrentCulture = CultureInfo.GetCultureInfo(thread.CurrentCulture.Name); + thread.CurrentUICulture = CultureInfo.GetCultureInfo(thread.CurrentUICulture.Name); } } diff --git a/src/Umbraco.Core/Extensions/TypeExtensions.cs b/src/Umbraco.Core/Extensions/TypeExtensions.cs index bb43c2b5d9..e3da8d9ee1 100644 --- a/src/Umbraco.Core/Extensions/TypeExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeExtensions.cs @@ -1,492 +1,515 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TypeExtensions { - public static class TypeExtensions + public static object? GetDefaultValue(this Type t) => + t.IsValueType + ? Activator.CreateInstance(t) + : null; + + /// + /// Checks if the type is an anonymous type + /// + /// + /// + /// + /// reference: http://jclaes.blogspot.com/2011/05/checking-for-anonymous-types.html + /// + public static bool IsAnonymousType(this Type type) { - public static object? GetDefaultValue(this Type t) + if (type == null) { - return t.IsValueType - ? Activator.CreateInstance(t) - : null; + throw new ArgumentNullException("type"); } - internal static MethodInfo? GetGenericMethod(this Type type, string name, params Type[] parameterTypes) - { - var methods = type.GetMethods().Where(method => method.Name == name); + return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false) + && type.IsGenericType && type.Name.Contains("AnonymousType") + && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$")) + && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic; + } - foreach (var method in methods) + public static IEnumerable GetBaseTypes(this Type? type, bool andSelf) + { + if (andSelf) + { + yield return type; + } + + while ((type = type?.BaseType) != null) + { + yield return type; + } + } + + internal static MethodInfo? GetGenericMethod(this Type type, string name, params Type[] parameterTypes) + { + IEnumerable methods = type.GetMethods().Where(method => method.Name == name); + + foreach (MethodInfo method in methods) + { + if (method.HasParameters(parameterTypes)) { - if (method.HasParameters(parameterTypes)) - return method; + return method; + } + } + + return null; + } + + /// + /// Determines whether the specified type is enumerable. + /// + /// The type. + /// + internal static bool HasParameters(this MethodInfo method, params Type[] parameterTypes) + { + Type[] methodParameters = method.GetParameters().Select(parameter => parameter.ParameterType).ToArray(); + + if (methodParameters.Length != parameterTypes.Length) + { + return false; + } + + for (var i = 0; i < methodParameters.Length; i++) + { + if (methodParameters[i].ToString() != parameterTypes[i].ToString()) + { + return false; + } + } + + return true; + } + + public static IEnumerable AllMethods(this Type target) + { + // var allTypes = target.AllInterfaces().ToList(); + var allTypes = target.GetInterfaces().ToList(); // GetInterfaces is ok here + allTypes.Add(target); + + return allTypes.SelectMany(t => t.GetMethods()); + } + + /// + /// true if the specified type is enumerable; otherwise, false. + /// + public static bool IsEnumerable(this Type type) + { + if (type.IsGenericType) + { + if (type.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable))) + { + return true; + } + } + else + { + if (type.GetInterfaces().Contains(typeof(IEnumerable))) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether [is of generic type] [the specified type]. + /// + /// The type. + /// Type of the generic. + /// + /// true if [is of generic type] [the specified type]; otherwise, false. + /// + public static bool IsOfGenericType(this Type type, Type genericType) + { + return type.TryGetGenericArguments(genericType, out Type[]? args); + } + + /// + /// Will find the generic type of the 'type' parameter passed in that is equal to the 'genericType' parameter passed in + /// + /// + /// + /// + /// + public static bool TryGetGenericArguments(this Type type, Type genericType, out Type[]? genericArgType) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + + if (genericType == null) + { + throw new ArgumentNullException("genericType"); + } + + if (genericType.IsGenericType == false) + { + throw new ArgumentException("genericType must be a generic type"); + } + + Func checkGenericType = (@int, t) => + { + if (@int.IsGenericType) + { + Type def = @int.GetGenericTypeDefinition(); + if (def == t) + { + return @int.GetGenericArguments(); + } } return null; - } + }; - /// - /// Checks if the type is an anonymous type - /// - /// - /// - /// - /// reference: http://jclaes.blogspot.com/2011/05/checking-for-anonymous-types.html - /// - public static bool IsAnonymousType(this Type type) + // first, check if the type passed in is already the generic type + genericArgType = checkGenericType(type, genericType); + if (genericArgType != null) { - if (type == null) throw new ArgumentNullException("type"); - - - return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false) - && type.IsGenericType && type.Name.Contains("AnonymousType") - && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$")) - && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic; - } - - - - /// - /// Determines whether the specified type is enumerable. - /// - /// The type. - /// - internal static bool HasParameters(this MethodInfo method, params Type[] parameterTypes) - { - var methodParameters = method.GetParameters().Select(parameter => parameter.ParameterType).ToArray(); - - if (methodParameters.Length != parameterTypes.Length) - return false; - - for (int i = 0; i < methodParameters.Length; i++) - if (methodParameters[i].ToString() != parameterTypes[i].ToString()) - return false; - return true; } - public static IEnumerable GetBaseTypes(this Type? type, bool andSelf) + // if we're looking for interfaces, enumerate them: + if (genericType.IsInterface) { - if (andSelf) - yield return type; - - while ((type = type?.BaseType) != null) - yield return type; - } - - public static IEnumerable AllMethods(this Type target) - { - //var allTypes = target.AllInterfaces().ToList(); - var allTypes = target.GetInterfaces().ToList(); // GetInterfaces is ok here - allTypes.Add(target); - - return allTypes.SelectMany(t => t.GetMethods()); - } - - /// - /// true if the specified type is enumerable; otherwise, false. - /// - public static bool IsEnumerable(this Type type) - { - if (type.IsGenericType) + foreach (Type @interface in type.GetInterfaces()) { - if (type.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable))) + genericArgType = checkGenericType(@interface, genericType); + if (genericArgType != null) + { return true; + } } - else + } + else + { + // loop back into the base types as long as they are generic + while (type.BaseType != null && type.BaseType != typeof(object)) { - if (type.GetInterfaces().Contains(typeof(IEnumerable))) + genericArgType = checkGenericType(type.BaseType, genericType); + if (genericArgType != null) + { return true; + } + + type = type.BaseType; } - return false; } - /// - /// Determines whether [is of generic type] [the specified type]. - /// - /// The type. - /// Type of the generic. - /// - /// true if [is of generic type] [the specified type]; otherwise, false. - /// - public static bool IsOfGenericType(this Type type, Type genericType) - { - Type[]? args; - return type.TryGetGenericArguments(genericType, out args); - } + return false; + } - /// - /// Will find the generic type of the 'type' parameter passed in that is equal to the 'genericType' parameter passed in - /// - /// - /// - /// - /// - public static bool TryGetGenericArguments(this Type type, Type genericType, out Type[]? genericArgType) + /// + /// Gets all properties in a flat hierarchy + /// + /// Includes both Public and Non-Public properties + /// + /// + public static PropertyInfo[] GetAllProperties(this Type type) + { + if (type.IsInterface) { - if (type == null) - { - throw new ArgumentNullException("type"); - } - if (genericType == null) - { - throw new ArgumentNullException("genericType"); - } - if (genericType.IsGenericType == false) - { - throw new ArgumentException("genericType must be a generic type"); - } + var propertyInfos = new List(); - Func checkGenericType = (@int, t) => + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + while (queue.Count > 0) { - if (@int.IsGenericType) + Type subType = queue.Dequeue(); + foreach (Type subInterface in subType.GetInterfaces()) { - var def = @int.GetGenericTypeDefinition(); - if (def == t) + if (considered.Contains(subInterface)) { - return @int.GetGenericArguments(); - } - } - return null; - }; - - //first, check if the type passed in is already the generic type - genericArgType = checkGenericType(type, genericType); - if (genericArgType != null) - return true; - - //if we're looking for interfaces, enumerate them: - if (genericType.IsInterface) - { - foreach (Type @interface in type.GetInterfaces()) - { - genericArgType = checkGenericType(@interface, genericType); - if (genericArgType != null) - return true; - } - } - else - { - //loop back into the base types as long as they are generic - while (type.BaseType != null && type.BaseType != typeof(object)) - { - genericArgType = checkGenericType(type.BaseType, genericType); - if (genericArgType != null) - return true; - type = type.BaseType; - } - - } - - return false; - - } - - /// - /// Gets all properties in a flat hierarchy - /// - /// Includes both Public and Non-Public properties - /// - /// - public static PropertyInfo[] GetAllProperties(this Type type) - { - if (type.IsInterface) - { - var propertyInfos = new List(); - - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) - { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetInterfaces()) - { - if (considered.Contains(subInterface)) continue; - - considered.Add(subInterface); - queue.Enqueue(subInterface); + continue; } - var typeProperties = subType.GetProperties( - BindingFlags.FlattenHierarchy - | BindingFlags.Public - | BindingFlags.NonPublic - | BindingFlags.Instance); - - var newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); + considered.Add(subInterface); + queue.Enqueue(subInterface); } - return propertyInfos.ToArray(); + PropertyInfo[] typeProperties = subType.GetProperties( + BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance); + + IEnumerable newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); } - return type.GetProperties(BindingFlags.FlattenHierarchy - | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + return propertyInfos.ToArray(); } - /// - /// Returns all public properties including inherited properties even for interfaces - /// - /// - /// - /// - /// taken from http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy - /// - public static PropertyInfo[] GetPublicProperties(this Type type) + return type.GetProperties(BindingFlags.FlattenHierarchy + | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } + + /// + /// Returns all public properties including inherited properties even for interfaces + /// + /// + /// + /// + /// taken from + /// http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy + /// + public static PropertyInfo[] GetPublicProperties(this Type type) + { + if (type.IsInterface) { - if (type.IsInterface) + var propertyInfos = new List(); + + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + while (queue.Count > 0) { - var propertyInfos = new List(); - - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) + Type subType = queue.Dequeue(); + foreach (Type subInterface in subType.GetInterfaces()) { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetInterfaces()) + if (considered.Contains(subInterface)) { - if (considered.Contains(subInterface)) continue; - - considered.Add(subInterface); - queue.Enqueue(subInterface); + continue; } - var typeProperties = subType.GetProperties( - BindingFlags.FlattenHierarchy - | BindingFlags.Public - | BindingFlags.Instance); - - var newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); + considered.Add(subInterface); + queue.Enqueue(subInterface); } - return propertyInfos.ToArray(); + PropertyInfo[] typeProperties = subType.GetProperties( + BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.Instance); + + IEnumerable newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); } - return type.GetProperties(BindingFlags.FlattenHierarchy - | BindingFlags.Public | BindingFlags.Instance); + return propertyInfos.ToArray(); } - /// - /// Determines whether the specified actual type is type. - /// - /// - /// The actual type. - /// - /// true if the specified actual type is type; otherwise, false. - /// - public static bool IsType(this Type actualType) + return type.GetProperties(BindingFlags.FlattenHierarchy + | BindingFlags.Public | BindingFlags.Instance); + } + + /// + /// Determines whether the specified actual type is type. + /// + /// + /// The actual type. + /// + /// true if the specified actual type is type; otherwise, false. + /// + public static bool IsType(this Type actualType) => TypeHelper.IsTypeAssignableFrom(actualType); + + public static bool Inherits(this Type type) => typeof(TBase).IsAssignableFrom(type); + + public static bool Inherits(this Type type, Type tbase) => tbase.IsAssignableFrom(type); + + public static bool Implements(this Type type) => typeof(TInterface).IsAssignableFrom(type); + + public static TAttribute? FirstAttribute(this Type type) => type.FirstAttribute(true); + + public static TAttribute? FirstAttribute(this Type type, bool inherit) + { + var attrs = type.GetCustomAttributes(typeof(TAttribute), inherit); + return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); + } + + public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo) => + propertyInfo.FirstAttribute(true); + + public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo, bool inherit) + { + var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); + return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); + } + + public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo) => + propertyInfo.MultipleAttribute(true); + + public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo, bool inherit) + { + var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); + return attrs.Length > 0 ? attrs.ToList().ConvertAll(input => (TAttribute)input) : null; + } + + /// + /// Returns the full type name with the assembly but without all of the assembly specific version information. + /// + /// + /// + /// + /// This method is like an 'in between' of Type.FullName and Type.AssemblyQualifiedName which returns the type and the + /// assembly separated + /// by a comma. + /// + /// + /// The output of this class would be: + /// Umbraco.Core.TypeExtensions, Umbraco.Core + /// + public static string GetFullNameWithAssembly(this Type type) + { + AssemblyName assemblyName = type.Assembly.GetName(); + + return string.Concat(type.FullName, ", ", assemblyName.FullName.StartsWith("App_Code.") ? "App_Code" : assemblyName.Name); + } + + /// + /// Determines whether an instance of a specified type can be assigned to the current type instance. + /// + /// The current type. + /// The type to compare with the current type. + /// A value indicating whether an instance of the specified type can be assigned to the current type instance. + /// + /// This extended version supports the current type being a generic type definition, and will + /// consider that eg List{int} is "assignable to" IList{}. + /// + public static bool IsAssignableFromGtd(this Type type, Type c) + { + // type *can* be a generic type definition + // c is a real type, cannot be a generic type definition + if (type.IsGenericTypeDefinition == false) { - return TypeHelper.IsTypeAssignableFrom(actualType); + return type.IsAssignableFrom(c); } - public static bool Inherits(this Type type) + if (c.IsInterface == false) { - return typeof(TBase).IsAssignableFrom(type); - } - - public static bool Inherits(this Type type, Type tbase) - { - return tbase.IsAssignableFrom(type); - } - - public static bool Implements(this Type type) - { - return typeof(TInterface).IsAssignableFrom(type); - } - - public static TAttribute? FirstAttribute(this Type type) - { - return type.FirstAttribute(true); - } - - public static TAttribute? FirstAttribute(this Type type, bool inherit) - { - var attrs = type.GetCustomAttributes(typeof(TAttribute), inherit); - return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); - } - - public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo) - { - return propertyInfo.FirstAttribute(true); - } - - public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo, bool inherit) - { - var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); - return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); - } - - public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo) - { - return propertyInfo.MultipleAttribute(true); - } - - public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo, bool inherit) - { - var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); - return (attrs.Length > 0 ? attrs.ToList().ConvertAll(input => (TAttribute)input) : null); - } - - /// - /// Returns the full type name with the assembly but without all of the assembly specific version information. - /// - /// - /// - /// - /// This method is like an 'in between' of Type.FullName and Type.AssemblyQualifiedName which returns the type and the assembly separated - /// by a comma. - /// - /// - /// The output of this class would be: - /// - /// Umbraco.Core.TypeExtensions, Umbraco.Core - /// - public static string GetFullNameWithAssembly(this Type type) - { - var assemblyName = type.Assembly.GetName(); - - return string.Concat(type.FullName, ", ", - assemblyName.FullName.StartsWith("App_Code.") ? "App_Code" : assemblyName.Name); - } - - /// - /// Determines whether an instance of a specified type can be assigned to the current type instance. - /// - /// The current type. - /// The type to compare with the current type. - /// A value indicating whether an instance of the specified type can be assigned to the current type instance. - /// This extended version supports the current type being a generic type definition, and will - /// consider that eg List{int} is "assignable to" IList{}. - public static bool IsAssignableFromGtd(this Type type, Type c) - { - // type *can* be a generic type definition - // c is a real type, cannot be a generic type definition - - if (type.IsGenericTypeDefinition == false) - return type.IsAssignableFrom(c); - - if (c.IsInterface == false) + Type? t = c; + while (t != typeof(object)) { - var t = c; - while (t != typeof(object)) + if (t is not null && t.IsGenericType && t.GetGenericTypeDefinition() == type) { - if (t is not null && t.IsGenericType && t.GetGenericTypeDefinition() == type) return true; - t = t?.BaseType; + return true; } - } - return c.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == type); + t = t?.BaseType; + } } - /// - /// If the given is an array or some other collection - /// comprised of 0 or more instances of a "subtype", get that type - /// - /// the source type - /// - public static Type? GetEnumeratedType(this Type type) + return c.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == type); + } + + /// + /// If the given is an array or some other collection + /// comprised of 0 or more instances of a "subtype", get that type + /// + /// the source type + /// + public static Type? GetEnumeratedType(this Type type) + { + if (typeof(IEnumerable).IsAssignableFrom(type) == false) { - if (typeof(IEnumerable).IsAssignableFrom(type) == false) - return null; - - // provided by Array - var elType = type.GetElementType(); - if (null != elType) return elType; - - // otherwise provided by collection - var elTypes = type.GetGenericArguments(); - if (elTypes.Length > 0) return elTypes[0]; - - // otherwise is not an 'enumerated' type return null; } - public static T? GetCustomAttribute(this Type type, bool inherit) - where T : Attribute + // provided by Array + Type? elType = type.GetElementType(); + if (elType != null) { - return type.GetCustomAttributes(inherit).SingleOrDefault(); + return elType; } - public static IEnumerable GetCustomAttributes(this Type type, bool inherited) - where T : Attribute + // otherwise provided by collection + Type[] elTypes = type.GetGenericArguments(); + if (elTypes.Length > 0) { - if (type == null) return Enumerable.Empty(); - return type.GetCustomAttributes(typeof(T), inherited).OfType(); + return elTypes[0]; } - public static bool HasCustomAttribute(this Type type, bool inherit) - where T : Attribute + // otherwise is not an 'enumerated' type + return null; + } + + public static T? GetCustomAttribute(this Type type, bool inherit) + where T : Attribute => + type.GetCustomAttributes(inherit).SingleOrDefault(); + + public static IEnumerable GetCustomAttributes(this Type? type, bool inherited) + where T : Attribute + { + if (type == null) { - return type.GetCustomAttribute(inherit) != null; + return Enumerable.Empty(); } - /// - /// Tries to return a value based on a property name for an object but ignores case sensitivity - /// - /// - /// - /// - /// - /// - /// - /// Currently this will only work for ProperCase and camelCase properties, see the TODO below to enable complete case insensitivity - /// - internal static Attempt GetMemberIgnoreCase(this Type type, IShortStringHelper shortStringHelper, object target, string memberName) - { - Func> getMember = - memberAlias => - { - try - { - return Attempt.Succeed( - type.InvokeMember(memberAlias, - System.Reflection.BindingFlags.GetProperty | - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public, - null, - target, - null)); - } - catch (MissingMethodException ex) - { - return Attempt.Fail(ex); - } - }; + return type.GetCustomAttributes(typeof(T), inherited).OfType(); + } - //try with the current casing - var attempt = getMember(memberName); - if (attempt.Success == false) + public static bool HasCustomAttribute(this Type type, bool inherit) + where T : Attribute => + type.GetCustomAttribute(inherit) != null; + + /// + /// Tries to return a value based on a property name for an object but ignores case sensitivity + /// + /// + /// + /// + /// + /// + /// + /// Currently this will only work for ProperCase and camelCase properties, see the TODO below to enable complete case + /// insensitivity + /// + internal static Attempt GetMemberIgnoreCase(this Type type, IShortStringHelper shortStringHelper, object target, string memberName) + { + Func> getMember = + memberAlias => { - //if we cannot get with the current alias, try changing it's case - attempt = memberName[0].IsUpperCase() - ? getMember(memberName.ToCleanString(shortStringHelper, CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.CamelCase)) - : getMember(memberName.ToCleanString(shortStringHelper, CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.PascalCase)); + try + { + return Attempt.Succeed( + type.InvokeMember( + memberAlias, + BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public, + null, + target, + null)); + } + catch (MissingMethodException ex) + { + return Attempt.Fail(ex); + } + }; - // TODO: If this still fails then we should get a list of properties from the object and then compare - doing the above without listing - // all properties will surely be faster than using reflection to get ALL properties first and then query against them. - } + // try with the current casing + Attempt attempt = getMember(memberName); + if (attempt.Success == false) + { + // if we cannot get with the current alias, try changing it's case + attempt = memberName[0].IsUpperCase() + ? getMember(memberName.ToCleanString( + shortStringHelper, + CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.CamelCase)) + : getMember(memberName.ToCleanString( + shortStringHelper, + CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.PascalCase)); - return attempt; + // TODO: If this still fails then we should get a list of properties from the object and then compare - doing the above without listing + // all properties will surely be faster than using reflection to get ALL properties first and then query against them. } + return attempt; } } diff --git a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs index 8928d221c5..1ea73af009 100644 --- a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs @@ -1,33 +1,29 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TypeLoaderExtensions { - public static class TypeLoaderExtensions - { - /// - /// Gets all types implementing . - /// - public static IEnumerable GetDataEditors(this TypeLoader mgr) => mgr.GetTypes(); + /// + /// Gets all types implementing . + /// + public static IEnumerable GetDataEditors(this TypeLoader mgr) => mgr.GetTypes(); - /// - /// Gets all types implementing ICacheRefresher. - /// - public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes(); + /// + /// Gets all types implementing ICacheRefresher. + /// + public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes(); - /// - /// Gets all types implementing - /// - /// - /// - public static IEnumerable GetActions(this TypeLoader mgr) => mgr.GetTypes(); - } + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetActions(this TypeLoader mgr) => mgr.GetTypes(); } diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index 70dd11ff33..1ad94cbdc3 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -1,325 +1,482 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods that return udis for Umbraco entities. +/// +public static class UdiGetterExtensions { /// - /// Provides extension methods that return udis for Umbraco entities. + /// Gets the entity identifier of the entity. /// - public static class UdiGetterExtensions + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this ITemplate entity) { - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this ITemplate entity) + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContentType entity) + return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentType entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DocumentType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMediaType entity) + return new GuidUdi(Constants.UdiEntityType.DocumentType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMediaType entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.MediaType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMemberType entity) + return new GuidUdi(Constants.UdiEntityType.MediaType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMemberType entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMemberGroup entity) + return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMemberGroup entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContentTypeComposition entity) - { - if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); + } - string type; - if (entity is IContentType) type = Constants.UdiEntityType.DocumentType; - else if (entity is IMediaType) type = Constants.UdiEntityType.MediaType; - else if (entity is IMemberType) type = Constants.UdiEntityType.MemberType; - else throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)); - return new GuidUdi(type, entity.Key).EnsureClosed(); + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentTypeComposition entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IDataType entity) + string type; + if (entity is IContentType) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed(); + type = Constants.UdiEntityType.DocumentType; + } + else if (entity is IMediaType) + { + type = Constants.UdiEntityType.MediaType; + } + else if (entity is IMemberType) + { + type = Constants.UdiEntityType.MemberType; + } + else + { + throw new NotSupportedException(string.Format( + "Composition type {0} is not supported.", + entity.GetType().FullName)); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this EntityContainer entity) - { - if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(type, entity.Key).EnsureClosed(); + } - string entityType; - if (entity.ContainedObjectType == Constants.ObjectTypes.DataType) - entityType = Constants.UdiEntityType.DataTypeContainer; - else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType) - entityType = Constants.UdiEntityType.DocumentTypeContainer; - else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType) - entityType = Constants.UdiEntityType.MediaTypeContainer; - else - throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType)); - return new GuidUdi(entityType, entity.Key).EnsureClosed(); + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IDataType entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMedia entity) + return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this EntityContainer entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContent entity) + string entityType; + if (entity.ContainedObjectType == Constants.ObjectTypes.DataType) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document, entity.Key).EnsureClosed(); + entityType = Constants.UdiEntityType.DataTypeContainer; + } + else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType) + { + entityType = Constants.UdiEntityType.DocumentTypeContainer; + } + else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType) + { + entityType = Constants.UdiEntityType.MediaTypeContainer; + } + else + { + throw new NotSupportedException(string.Format( + "Contained object type {0} is not supported.", + entity.ContainedObjectType)); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMember entity) + return new GuidUdi(entityType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMedia entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this Stylesheet entity) + return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContent entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Stylesheet, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this Script entity) + return new GuidUdi( + entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document, + entity.Key) + .EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMember entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IDictionaryItem entity) + return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this Stylesheet entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DictionaryItem, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMacro entity) + return new StringUdi( + Constants.UdiEntityType.Stylesheet, + entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this Script entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Macro, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this IPartialView entity) + return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)) + .EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IDictionaryItem entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - - // we should throw on Unknown but for the time being, assume it means PartialView - var entityType = entity.ViewType == PartialViewType.PartialViewMacro - ? Constants.UdiEntityType.PartialViewMacro - : Constants.UdiEntityType.PartialView; - - return new StringUdi(entityType, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContentBase entity) - { - if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.UdiEntityType.DictionaryItem, entity.Key).EnsureClosed(); + } - string type; - if (entity is IContent) type = Constants.UdiEntityType.Document; - else if (entity is IMedia) type = Constants.UdiEntityType.Media; - else if (entity is IMember) type = Constants.UdiEntityType.Member; - else throw new NotSupportedException(string.Format("ContentBase type {0} is not supported.", entity.GetType().FullName)); - return new GuidUdi(type, entity.Key).EnsureClosed(); + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMacro entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IRelationType entity) + return new GuidUdi(Constants.UdiEntityType.Macro, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this IPartialView entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this ILanguage entity) + // we should throw on Unknown but for the time being, assume it means PartialView + var entityType = entity.ViewType == PartialViewType.PartialViewMacro + ? Constants.UdiEntityType.PartialViewMacro + : Constants.UdiEntityType.PartialView; + + return new StringUdi(entityType, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentBase entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static Udi GetUdi(this IEntity entity) + string type; + if (entity is IContent) { - if (entity == null) throw new ArgumentNullException("entity"); - - // entity could eg be anything implementing IThing - // so we have to go through casts here - - var template = entity as ITemplate; - if (template != null) return template.GetUdi(); - - var contentType = entity as IContentType; - if (contentType != null) return contentType.GetUdi(); - - var mediaType = entity as IMediaType; - if (mediaType != null) return mediaType.GetUdi(); - - var memberType = entity as IMemberType; - if (memberType != null) return memberType.GetUdi(); - - var memberGroup = entity as IMemberGroup; - if (memberGroup != null) return memberGroup.GetUdi(); - - var contentTypeComposition = entity as IContentTypeComposition; - if (contentTypeComposition != null) return contentTypeComposition.GetUdi(); - - var dataTypeComposition = entity as IDataType; - if (dataTypeComposition != null) return dataTypeComposition.GetUdi(); - - var container = entity as EntityContainer; - if (container != null) return container.GetUdi(); - - var media = entity as IMedia; - if (media != null) return media.GetUdi(); - - var content = entity as IContent; - if (content != null) return content.GetUdi(); - - var member = entity as IMember; - if (member != null) return member.GetUdi(); - - var stylesheet = entity as Stylesheet; - if (stylesheet != null) return stylesheet.GetUdi(); - - var script = entity as Script; - if (script != null) return script.GetUdi(); - - var dictionaryItem = entity as IDictionaryItem; - if (dictionaryItem != null) return dictionaryItem.GetUdi(); - - var macro = entity as IMacro; - if (macro != null) return macro.GetUdi(); - - var partialView = entity as IPartialView; - if (partialView != null) return partialView.GetUdi(); - - var contentBase = entity as IContentBase; - if (contentBase != null) return contentBase.GetUdi(); - - var relationType = entity as IRelationType; - if (relationType != null) return relationType.GetUdi(); - - var language = entity as ILanguage; - if (language != null) return language.GetUdi(); - - throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); + type = Constants.UdiEntityType.Document; } + else if (entity is IMedia) + { + type = Constants.UdiEntityType.Media; + } + else if (entity is IMember) + { + type = Constants.UdiEntityType.Member; + } + else + { + throw new NotSupportedException(string.Format( + "ContentBase type {0} is not supported.", + entity.GetType().FullName)); + } + + return new GuidUdi(type, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IRelationType entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this ILanguage entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static Udi GetUdi(this IEntity entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + // entity could eg be anything implementing IThing + // so we have to go through casts here + if (entity is ITemplate template) + { + return template.GetUdi(); + } + + if (entity is IContentType contentType) + { + return contentType.GetUdi(); + } + + if (entity is IMediaType mediaType) + { + return mediaType.GetUdi(); + } + + if (entity is IMemberType memberType) + { + return memberType.GetUdi(); + } + + if (entity is IMemberGroup memberGroup) + { + return memberGroup.GetUdi(); + } + + if (entity is IContentTypeComposition contentTypeComposition) + { + return contentTypeComposition.GetUdi(); + } + + if (entity is IDataType dataTypeComposition) + { + return dataTypeComposition.GetUdi(); + } + + if (entity is EntityContainer container) + { + return container.GetUdi(); + } + + if (entity is IMedia media) + { + return media.GetUdi(); + } + + if (entity is IContent content) + { + return content.GetUdi(); + } + + if (entity is IMember member) + { + return member.GetUdi(); + } + + if (entity is Stylesheet stylesheet) + { + return stylesheet.GetUdi(); + } + + if (entity is Script script) + { + return script.GetUdi(); + } + + if (entity is IDictionaryItem dictionaryItem) + { + return dictionaryItem.GetUdi(); + } + + if (entity is IMacro macro) + { + return macro.GetUdi(); + } + + if (entity is IPartialView partialView) + { + return partialView.GetUdi(); + } + + if (entity is IContentBase contentBase) + { + return contentBase.GetUdi(); + } + + if (entity is IRelationType relationType) + { + return relationType.GetUdi(); + } + + if (entity is ILanguage language) + { + return language.GetUdi(); + } + + throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); } } diff --git a/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs index 5b4e3a92d9..53e86109c3 100644 --- a/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs @@ -1,104 +1,103 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Extensions +namespace Umbraco.Cms.Core.Extensions; + +public static class UmbracoBuilderExtensions { - public static class UmbracoBuilderExtensions + /// + /// Registers all within an assembly + /// + /// + /// + /// + /// Type contained within the targeted assembly + /// + public static IUmbracoBuilder AddNotificationsFromAssembly(this IUmbracoBuilder self) { - /// - /// Registers all within an assembly - /// - /// - /// Type contained within the targeted assembly - /// - public static IUmbracoBuilder AddNotificationsFromAssembly(this IUmbracoBuilder self) - { - AddNotificationHandlers(self); - AddAsyncNotificationHandlers(self); + AddNotificationHandlers(self); + AddAsyncNotificationHandlers(self); - return self; - } + return self; + } - private static void AddNotificationHandlers(IUmbracoBuilder self) + private static void AddNotificationHandlers(IUmbracoBuilder self) + { + List notificationHandlers = GetNotificationHandlers(); + foreach (Type notificationHandler in notificationHandlers) { - var notificationHandlers = GetNotificationHandlers(); - foreach (var notificationHandler in notificationHandlers) + List handlerImplementations = GetNotificationHandlerImplementations(notificationHandler); + foreach (Type implementation in handlerImplementations) { - var handlerImplementations = GetNotificationHandlerImplementations(notificationHandler); - foreach (var implementation in handlerImplementations) - { - RegisterNotificationHandler(self, implementation, notificationHandler); - } + RegisterNotificationHandler(self, implementation, notificationHandler); } } + } - private static List GetNotificationHandlers() => - typeof(T).Assembly.GetTypes() - .Where(x => x.IsAssignableToGenericType(typeof(INotificationHandler<>))) - .ToList(); + private static List GetNotificationHandlers() => + typeof(T).Assembly.GetTypes() + .Where(x => x.IsAssignableToGenericType(typeof(INotificationHandler<>))) + .ToList(); - private static List GetNotificationHandlerImplementations(Type handlerType) => - handlerType - .GetInterfaces() - .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationHandler<>)) - .ToList(); + private static List GetNotificationHandlerImplementations(Type handlerType) => + handlerType + .GetInterfaces() + .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationHandler<>)) + .ToList(); - private static void AddAsyncNotificationHandlers(IUmbracoBuilder self) + private static void AddAsyncNotificationHandlers(IUmbracoBuilder self) + { + List notificationHandlers = GetAsyncNotificationHandlers(); + foreach (Type notificationHandler in notificationHandlers) { - var notificationHandlers = GetAsyncNotificationHandlers(); - foreach (var notificationHandler in notificationHandlers) + List handlerImplementations = GetAsyncNotificationHandlerImplementations(notificationHandler); + foreach (Type handler in handlerImplementations) { - var handlerImplementations = GetAsyncNotificationHandlerImplementations(notificationHandler); - foreach (var handler in handlerImplementations) - { - RegisterNotificationHandler(self, handler, notificationHandler); - } + RegisterNotificationHandler(self, handler, notificationHandler); } } + } - private static List GetAsyncNotificationHandlers() => - typeof(T).Assembly.GetTypes() - .Where(x => x.IsAssignableToGenericType(typeof(INotificationAsyncHandler<>))) - .ToList(); + private static List GetAsyncNotificationHandlers() => + typeof(T).Assembly.GetTypes() + .Where(x => x.IsAssignableToGenericType(typeof(INotificationAsyncHandler<>))) + .ToList(); - private static List GetAsyncNotificationHandlerImplementations(Type handlerType) => - handlerType - .GetInterfaces() - .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationAsyncHandler<>)) - .ToList(); + private static List GetAsyncNotificationHandlerImplementations(Type handlerType) => + handlerType + .GetInterfaces() + .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationAsyncHandler<>)) + .ToList(); - private static void RegisterNotificationHandler(IUmbracoBuilder self, Type notificationHandlerType, Type implementingHandlerType) + private static void RegisterNotificationHandler(IUmbracoBuilder self, Type notificationHandlerType, Type implementingHandlerType) + { + var descriptor = + new UniqueServiceDescriptor(notificationHandlerType, implementingHandlerType, ServiceLifetime.Transient); + if (!self.Services.Contains(descriptor)) { - var descriptor = new UniqueServiceDescriptor(notificationHandlerType, implementingHandlerType, ServiceLifetime.Transient); - if (!self.Services.Contains(descriptor)) - { - self.Services.Add(descriptor); - } + self.Services.Add(descriptor); } + } - private static bool IsAssignableToGenericType(this Type givenType, Type genericType) + private static bool IsAssignableToGenericType(this Type givenType, Type genericType) + { + Type[] interfaceTypes = givenType.GetInterfaces(); + + foreach (Type it in interfaceTypes) { - var interfaceTypes = givenType.GetInterfaces(); - - foreach (var it in interfaceTypes) - { - if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) - { - return true; - } - } - - if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) { return true; } - - var baseType = givenType.BaseType; - return baseType != null && IsAssignableToGenericType(baseType, genericType); } + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + { + return true; + } + + Type? baseType = givenType.BaseType; + return baseType != null && IsAssignableToGenericType(baseType, genericType); } } diff --git a/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs b/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs index 794c206db8..b0256ad9e6 100644 --- a/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs +++ b/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs @@ -1,21 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoContextAccessorExtensions { - public static class UmbracoContextAccessorExtensions + public static IUmbracoContext GetRequiredUmbracoContext(this IUmbracoContextAccessor umbracoContextAccessor) { - public static IUmbracoContext GetRequiredUmbracoContext(this IUmbracoContextAccessor umbracoContextAccessor) + if (umbracoContextAccessor == null) { - if (umbracoContextAccessor == null) throw new ArgumentNullException(nameof(umbracoContextAccessor)); - if(!umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - throw new InvalidOperationException("Wasn't able to get an UmbracoContext"); - } - return umbracoContext!; + throw new ArgumentNullException(nameof(umbracoContextAccessor)); } + + if (!umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + throw new InvalidOperationException("Wasn't able to get an UmbracoContext"); + } + + return umbracoContext; } } diff --git a/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs b/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs index 7d0e31f285..e5ec62530d 100644 --- a/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs +++ b/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs @@ -3,13 +3,13 @@ using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoContextExtensions { - public static class UmbracoContextExtensions - { - /// - /// Boolean value indicating whether the current request is a front-end umbraco request - /// - public static bool IsFrontEndUmbracoRequest(this IUmbracoContext umbracoContext) => umbracoContext.PublishedRequest != null; - } + /// + /// Boolean value indicating whether the current request is a front-end umbraco request + /// + public static bool IsFrontEndUmbracoRequest(this IUmbracoContext umbracoContext) => + umbracoContext.PublishedRequest != null; } diff --git a/src/Umbraco.Core/Extensions/UriExtensions.cs b/src/Umbraco.Core/Extensions/UriExtensions.cs index 52adbc6b67..60ef7b6a7e 100644 --- a/src/Umbraco.Core/Extensions/UriExtensions.cs +++ b/src/Umbraco.Core/Extensions/UriExtensions.cs @@ -1,192 +1,206 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +using System.Net; +using System.Web; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class UriExtensions { /// - /// Provides extension methods to . + /// Rewrites the path of uri. /// - public static class UriExtensions + /// The uri. + /// The new path, which must begin with a slash. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path) { - /// - /// Rewrites the path of uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path) + if (path.StartsWith("/") == false) { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) - : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); + throw new ArgumentException("Path must start with a slash.", "path"); } - /// - /// Rewrites the path and query of a uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The new query, which must be empty or begin with a question mark. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path, string query) - { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - if (query.Length > 0 && query.StartsWith("?") == false) - throw new ArgumentException("Query must start with a question mark.", "query"); - if (query == "?") - query = ""; - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) - : new Uri(path + query, UriKind.Relative); - } - - /// - /// Gets the absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePath(this Uri uri) - { - if (uri.IsAbsoluteUri) - { - return uri.AbsolutePath; - } - - // cannot get .AbsolutePath on relative uri (InvalidOperation) - var s = uri.OriginalString; - - // TODO: Shouldn't this just use Uri.GetLeftPart? - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var pos = posq > 0 ? posq : (posf > 0 ? posf : 0); - var path = pos > 0 ? s.Substring(0, pos) : s; - return path; - } - - /// - /// Gets the decoded, absolute path of the uri. - /// - /// The uri. - /// The absolute path of the uri. - /// Only for absolute uris. - public static string GetAbsolutePathDecoded(this Uri uri) - { - return System.Web.HttpUtility.UrlDecode(uri.AbsolutePath); - } - - /// - /// Gets the decoded, absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePathDecoded(this Uri uri) - { - return System.Net.WebUtility.UrlDecode(uri.GetSafeAbsolutePath()); - } - - /// - /// Rewrites the path of the uri so it ends with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri EndPathWithSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); - return uri; - } - - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(path + "/" + uri.Query, UriKind.Relative); - - return uri; - } - - /// - /// Rewrites the path of the uri so it does not end with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri TrimPathEndSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/") - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query); - } - else - { - if (path != "/") - uri = new Uri(path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query, UriKind.Relative); - } - return uri; - } - - /// - /// Transforms a relative uri into an absolute uri. - /// - /// The relative uri. - /// The base absolute uri. - /// The absolute uri. - public static Uri MakeAbsolute(this Uri uri, Uri baseUri) - { - if (uri.IsAbsoluteUri) - throw new ArgumentException("Uri is already absolute.", "uri"); - - return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); - } - - static string? GetSafeQuery(this Uri uri) - { - if (uri.IsAbsoluteUri) - return uri.Query; - - // cannot get .Query on relative uri (InvalidOperation) - var s = uri.OriginalString; - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); - - return query; - } - - /// - /// Removes the port from the uri. - /// - /// The uri. - /// The same uri, without its port. - public static Uri WithoutPort(this Uri uri) - { - return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); - } - - /// - /// Replaces the host of a uri. - /// - /// The uri. - /// A replacement host. - /// The same uri, with its host replaced. - public static Uri ReplaceHost(this Uri uri, string host) - { - return new UriBuilder(uri) { Host = host }.Uri; - } + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) + : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); } + + /// + /// Rewrites the path and query of a uri. + /// + /// The uri. + /// The new path, which must begin with a slash. + /// The new query, which must be empty or begin with a question mark. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path, string query) + { + if (path.StartsWith("/") == false) + { + throw new ArgumentException("Path must start with a slash.", "path"); + } + + if (query.Length > 0 && query.StartsWith("?") == false) + { + throw new ArgumentException("Query must start with a question mark.", "query"); + } + + if (query == "?") + { + query = string.Empty; + } + + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) + : new Uri(path + query, UriKind.Relative); + } + + /// + /// Gets the absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePath(this Uri uri) + { + if (uri.IsAbsoluteUri) + { + return uri.AbsolutePath; + } + + // cannot get .AbsolutePath on relative uri (InvalidOperation) + var s = uri.OriginalString; + + // TODO: Shouldn't this just use Uri.GetLeftPart? + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var pos = posq > 0 ? posq : posf > 0 ? posf : 0; + var path = pos > 0 ? s.Substring(0, pos) : s; + return path; + } + + /// + /// Gets the decoded, absolute path of the uri. + /// + /// The uri. + /// The absolute path of the uri. + /// Only for absolute uris. + public static string GetAbsolutePathDecoded(this Uri uri) => HttpUtility.UrlDecode(uri.AbsolutePath); + + /// + /// Gets the decoded, absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePathDecoded(this Uri uri) => WebUtility.UrlDecode(uri.GetSafeAbsolutePath()); + + /// + /// Rewrites the path of the uri so it ends with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri EndPathWithSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/" && path.EndsWith("/") == false) + { + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); + } + + return uri; + } + + if (path != "/" && path.EndsWith("/") == false) + { + uri = new Uri(path + "/" + uri.Query, UriKind.Relative); + } + + return uri; + } + + /// + /// Rewrites the path of the uri so it does not end with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri TrimPathEndSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/") + { + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd(Constants.CharArrays.ForwardSlash) + + uri.Query); + } + } + else + { + if (path != "/") + { + uri = new Uri(path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query, UriKind.Relative); + } + } + + return uri; + } + + /// + /// Transforms a relative uri into an absolute uri. + /// + /// The relative uri. + /// The base absolute uri. + /// The absolute uri. + public static Uri MakeAbsolute(this Uri uri, Uri baseUri) + { + if (uri.IsAbsoluteUri) + { + throw new ArgumentException("Uri is already absolute.", "uri"); + } + + return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); + } + + /// + /// Removes the port from the uri. + /// + /// The uri. + /// The same uri, without its port. + public static Uri WithoutPort(this Uri uri) => + new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); + + private static string? GetSafeQuery(this Uri uri) + { + if (uri.IsAbsoluteUri) + { + return uri.Query; + } + + // cannot get .Query on relative uri (InvalidOperation) + var s = uri.OriginalString; + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); + + return query; + } + + /// + /// Replaces the host of a uri. + /// + /// The uri. + /// A replacement host. + /// The same uri, with its host replaced. + public static Uri ReplaceHost(this Uri uri, string host) => new UriBuilder(uri) { Host = host }.Uri; } diff --git a/src/Umbraco.Core/Extensions/VersionExtensions.cs b/src/Umbraco.Core/Extensions/VersionExtensions.cs index 24326ef327..4e9309da45 100644 --- a/src/Umbraco.Core/Extensions/VersionExtensions.cs +++ b/src/Umbraco.Core/Extensions/VersionExtensions.cs @@ -1,87 +1,91 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class VersionExtensions { - public static class VersionExtensions + public static Version GetVersion(this SemVersion semVersion, int maxParts = 4) { - public static Version GetVersion(this SemVersion semVersion, int maxParts = 4) + int.TryParse(semVersion.Build, NumberStyles.Integer, CultureInfo.InvariantCulture, out int build); + + if (maxParts >= 4) { - int build = 0; - int.TryParse(semVersion.Build, NumberStyles.Integer, CultureInfo.InvariantCulture, out build); - - if (maxParts >= 4) - { - return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch, build); - } - if (maxParts == 3) - { - return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch); - } - - return new Version(semVersion.Major, semVersion.Minor); + return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch, build); } - public static Version SubtractRevision(this Version version) + if (maxParts == 3) { - var parts = new List(new[] {version.Major, version.Minor, version.Build, version.Revision}); + return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch); + } - //remove all prefixed zero parts - while (parts[0] <= 0) + return new Version(semVersion.Major, semVersion.Minor); + } + + public static Version SubtractRevision(this Version version) + { + var parts = new List(new[] { version.Major, version.Minor, version.Build, version.Revision }); + + // remove all prefixed zero parts + while (parts[0] <= 0) + { + parts.RemoveAt(0); + if (parts.Count == 0) { - parts.RemoveAt(0); - if (parts.Count == 0) break; + break; } + } - for (int index = 0; index < parts.Count; index++) + for (var index = 0; index < parts.Count; index++) + { + var part = parts[index]; + if (part <= 0) { - var part = parts[index]; - if (part <= 0) - { - parts.RemoveAt(index); - index++; - } - else - { - //break when there isn't a zero part - break; - } + parts.RemoveAt(index); + index++; } - - if (parts.Count == 0) throw new InvalidOperationException("Cannot subtract a revision from a zero version"); - - var lastNonZero = parts.FindLastIndex(i => i > 0); - - //subtract 1 from the last non-zero - parts[lastNonZero] = parts[lastNonZero] - 1; - - //the last non zero is actually the revision so we can just return - if (lastNonZero == (parts.Count -1)) + else { - return FromList(parts); + // break when there isn't a zero part + break; } + } - //the last non zero isn't the revision so the remaining zero's need to be replaced with int.max - for (var i = lastNonZero + 1; i < parts.Count; i++) - { - parts[i] = int.MaxValue; - } + if (parts.Count == 0) + { + throw new InvalidOperationException("Cannot subtract a revision from a zero version"); + } + var lastNonZero = parts.FindLastIndex(i => i > 0); + + // subtract 1 from the last non-zero + parts[lastNonZero] = parts[lastNonZero] - 1; + + // the last non zero is actually the revision so we can just return + if (lastNonZero == parts.Count - 1) + { return FromList(parts); } - private static Version FromList(IList parts) + // the last non zero isn't the revision so the remaining zero's need to be replaced with int.max + for (var i = lastNonZero + 1; i < parts.Count; i++) { - while (parts.Count < 4) - { - parts.Insert(0, 0); - } - return new Version(parts[0], parts[1], parts[2], parts[3]); + parts[i] = int.MaxValue; } + + return FromList(parts); + } + + private static Version FromList(IList parts) + { + while (parts.Count < 4) + { + parts.Insert(0, 0); + } + + return new Version(parts[0], parts[1], parts[2], parts[3]); } } diff --git a/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs b/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs index 5cb7639497..b0058dd798 100644 --- a/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs +++ b/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs @@ -1,48 +1,43 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +public static class WaitHandleExtensions { - public static class WaitHandleExtensions + // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 + // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html + // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... + // version below should be OK + public static Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout = Timeout.Infinite) { - // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 - // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html - // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... - // version below should be OK - - public static Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout = Timeout.Infinite) + var tcs = new TaskCompletionSource(); + var callbackHandleInitLock = new object(); + lock (callbackHandleInitLock) { - var tcs = new TaskCompletionSource(); - var callbackHandleInitLock = new object(); - lock (callbackHandleInitLock) - { - RegisteredWaitHandle? callbackHandle = null; - // ReSharper disable once RedundantAssignment - callbackHandle = ThreadPool.RegisterWaitForSingleObject( - handle, - (state, timedOut) => + RegisteredWaitHandle? callbackHandle = null; + + // ReSharper disable once RedundantAssignment + callbackHandle = ThreadPool.RegisterWaitForSingleObject( + handle, + (state, timedOut) => + { + // TODO: We aren't checking if this is timed out + tcs.SetResult(null); + + // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. + lock (callbackHandleInitLock) { - //TODO: We aren't checking if this is timed out - - tcs.SetResult(null); - - // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. - lock (callbackHandleInitLock) - { - // ReSharper disable once PossibleNullReferenceException - // ReSharper disable once AccessToModifiedClosure - callbackHandle?.Unregister(null); - } - }, - /*state:*/ null, - /*millisecondsTimeOutInterval:*/ millisecondsTimeout, - /*executeOnlyOnce:*/ true); - } - - return tcs.Task; + // ReSharper disable once PossibleNullReferenceException + // ReSharper disable once AccessToModifiedClosure + callbackHandle?.Unregister(null); + } + }, + /*state:*/ null, + /*millisecondsTimeOutInterval:*/ millisecondsTimeout, + /*executeOnlyOnce:*/ true); } + + return tcs.Task; } } diff --git a/src/Umbraco.Core/Extensions/XmlExtensions.cs b/src/Umbraco.Core/Extensions/XmlExtensions.cs index 141f4a0c19..34e2b7b2aa 100644 --- a/src/Umbraco.Core/Extensions/XmlExtensions.cs +++ b/src/Umbraco.Core/Extensions/XmlExtensions.cs @@ -1,9 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Xml; using System.Xml.Linq; @@ -11,337 +8,402 @@ using System.Xml.XPath; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for xml objects +/// +public static class XmlExtensions { + public static bool HasAttribute(this XmlAttributeCollection attributes, string attributeName) => + attributes.Cast().Any(x => x.Name == attributeName); + /// - /// Extension methods for xml objects + /// Selects a list of XmlNode matching an XPath expression. /// - public static class XmlExtensions + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList? SelectNodes(this XmlNode source, string expression, IEnumerable? variables) { - public static bool HasAttribute(this XmlAttributeCollection attributes, string attributeName) + XPathVariable[]? av = variables?.ToArray(); + return SelectNodes(source, expression, av); + } + + /// + /// Selects a list of XmlNode matching an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList? SelectNodes(this XmlNode source, XPathExpression expression, IEnumerable? variables) + { + XPathVariable[]? av = variables?.ToArray(); + return SelectNodes(source, expression, av); + } + + /// + /// Selects a list of XmlNode matching an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList? SelectNodes(this XmlNode source, string? expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) { - return attributes.Cast().Any(x => x.Name == attributeName); + return source.SelectNodes(expression ?? string.Empty); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList? SelectNodes(this XmlNode source, string expression, IEnumerable? variables) + XPathNodeIterator? iterator = source.CreateNavigator()?.Select(expression ?? string.Empty, variables); + return XmlNodeListFactory.CreateNodeList(iterator); + } + + /// + /// Selects a list of XmlNode matching an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList SelectNodes(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) { - var av = variables == null ? null : variables.ToArray(); - return SelectNodes(source, expression, av); + return source.SelectNodes(expression); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList? SelectNodes(this XmlNode source, XPathExpression expression, IEnumerable? variables) + XPathNodeIterator? iterator = source.CreateNavigator()?.Select(expression, variables); + return XmlNodeListFactory.CreateNodeList(iterator); + } + + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, string expression, IEnumerable? variables) + { + XPathVariable[]? av = variables?.ToArray(); + return SelectSingleNode(source, expression, av); + } + + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, IEnumerable? variables) + { + XPathVariable[]? av = variables?.ToArray(); + return SelectSingleNode(source, expression, av); + } + + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, string expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) { - var av = variables == null ? null : variables.ToArray(); - return SelectNodes(source, expression, av); + return source.SelectSingleNode(expression); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList? SelectNodes(this XmlNode source, string? expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectNodes(expression ?? ""); + return SelectNodes(source, expression, variables)?.Cast().FirstOrDefault(); + } - var iterator = source.CreateNavigator()?.Select(expression ?? "", variables); - return XmlNodeListFactory.CreateNodeList(iterator); + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) + { + return source.SelectSingleNode(expression); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList SelectNodes(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectNodes(expression); + return SelectNodes(source, expression, variables).Cast().FirstOrDefault(); + } - var iterator = source.CreateNavigator()?.Select(expression, variables); - return XmlNodeListFactory.CreateNodeList(iterator); + /// + /// Converts from an XDocument to an XmlDocument + /// + /// + /// + public static XmlDocument ToXmlDocument(this XDocument xDocument) + { + var xmlDocument = new XmlDocument(); + using (XmlReader xmlReader = xDocument.CreateReader()) + { + xmlDocument.Load(xmlReader); } - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, string expression, IEnumerable? variables) + return xmlDocument; + } + + /// + /// Converts from an XmlDocument to an XDocument + /// + /// + /// + public static XDocument ToXDocument(this XmlDocument xmlDocument) + { + using (var nodeReader = new XmlNodeReader(xmlDocument)) { - var av = variables == null ? null : variables.ToArray(); - return SelectSingleNode(source, expression, av); - } - - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, IEnumerable? variables) - { - var av = variables == null ? null : variables.ToArray(); - return SelectSingleNode(source, expression, av); - } - - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, string expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectSingleNode(expression); - - return SelectNodes(source, expression, variables)?.Cast().FirstOrDefault(); - } - - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectSingleNode(expression); - - return SelectNodes(source, expression, variables).Cast().FirstOrDefault(); - } - - /// - /// Converts from an XDocument to an XmlDocument - /// - /// - /// - public static XmlDocument ToXmlDocument(this XDocument xDocument) - { - var xmlDocument = new XmlDocument(); - using (var xmlReader = xDocument.CreateReader()) - { - xmlDocument.Load(xmlReader); - } - return xmlDocument; - } - - /// - /// Converts from an XmlDocument to an XDocument - /// - /// - /// - public static XDocument ToXDocument(this XmlDocument xmlDocument) - { - using (var nodeReader = new XmlNodeReader(xmlDocument)) - { - nodeReader.MoveToContent(); - return XDocument.Load(nodeReader); - } - } - - ///// - ///// Converts from an XElement to an XmlElement - ///// - ///// - ///// - public static XmlNode? ToXmlElement(this XContainer xElement) - { - var xmlDocument = new XmlDocument(); - using (var xmlReader = xElement.CreateReader()) - { - xmlDocument.Load(xmlReader); - } - return xmlDocument.DocumentElement; - } - - /// - /// Converts from an XmlElement to an XElement - /// - /// - /// - public static XElement ToXElement(this XmlNode xmlElement) - { - using (var nodeReader = new XmlNodeReader(xmlElement)) - { - nodeReader.MoveToContent(); - return XElement.Load(nodeReader); - } - } - - public static T? RequiredAttributeValue(this XElement xml, string attributeName) - { - if (xml == null) - { - throw new ArgumentNullException(nameof(xml)); - } - - if (xml.HasAttributes == false) - { - throw new InvalidOperationException($"{attributeName} not found in xml"); - } - - XAttribute? attribute = xml.Attribute(attributeName); - if (attribute is null) - { - throw new InvalidOperationException($"{attributeName} not found in xml"); - } - - Attempt result = attribute.Value.TryConvertTo(); - if (result.Success) - { - return result.Result; - } - - throw new InvalidOperationException($"{attribute.Value} attribute value cannot be converted to {typeof(T)}"); - } - - public static T? AttributeValue(this XElement xml, string attributeName) - { - if (xml == null) throw new ArgumentNullException("xml"); - if (xml.HasAttributes == false) return default(T); - - if (xml.Attribute(attributeName) == null) - return default(T); - - var val = xml.Attribute(attributeName)?.Value; - var result = val.TryConvertTo(); - if (result.Success) - return result.Result; - - return default(T); - } - - public static T? AttributeValue(this XmlNode xml, string attributeName) - { - if (xml == null) throw new ArgumentNullException("xml"); - if (xml.Attributes == null) return default(T); - - if (xml.Attributes[attributeName] == null) - return default(T); - - var val = xml.Attributes[attributeName]?.Value; - var result = val.TryConvertTo(); - if (result.Success) - return result.Result; - - return default(T); - } - - public static XElement? GetXElement(this XmlNode node) - { - XDocument xDoc = new XDocument(); - using (XmlWriter xmlWriter = xDoc.CreateWriter()) - node.WriteTo(xmlWriter); - return xDoc.Root; - } - - public static XmlNode? GetXmlNode(this XContainer element) - { - using (var xmlReader = element.CreateReader()) - { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(xmlReader); - return xmlDoc.DocumentElement; - } - } - - public static XmlNode? GetXmlNode(this XContainer element, XmlDocument xmlDoc) - { - var node = element.GetXmlNode(); - if (node is not null) - { - return xmlDoc.ImportNode(node, true); - } - - return null; - } - - // this exists because - // new XElement("root", "a\nb").Value is "a\nb" but - // .ToString(SaveOptions.*) is "a\r\nb" and cannot figure out how to get rid of "\r" - // and when saving data we want nothing to change - // this method will produce a string that respects the \r and \n in the data value - public static string ToDataString(this XElement xml) - { - var settings = new XmlWriterSettings - { - OmitXmlDeclaration = true, - NewLineHandling = NewLineHandling.None, - Indent = false - }; - var output = new StringBuilder(); - using (var writer = XmlWriter.Create(output, settings)) - { - xml.WriteTo(writer); - } - return output.ToString(); + nodeReader.MoveToContent(); + return XDocument.Load(nodeReader); } } + + ///// + ///// Converts from an XElement to an XmlElement + ///// + ///// + ///// + public static XmlNode? ToXmlElement(this XContainer xElement) + { + var xmlDocument = new XmlDocument(); + using (XmlReader xmlReader = xElement.CreateReader()) + { + xmlDocument.Load(xmlReader); + } + + return xmlDocument.DocumentElement; + } + + /// + /// Converts from an XmlElement to an XElement + /// + /// + /// + public static XElement ToXElement(this XmlNode xmlElement) + { + using (var nodeReader = new XmlNodeReader(xmlElement)) + { + nodeReader.MoveToContent(); + return XElement.Load(nodeReader); + } + } + + public static T? RequiredAttributeValue(this XElement xml, string attributeName) + { + if (xml == null) + { + throw new ArgumentNullException(nameof(xml)); + } + + if (xml.HasAttributes == false) + { + throw new InvalidOperationException($"{attributeName} not found in xml"); + } + + XAttribute? attribute = xml.Attribute(attributeName); + if (attribute is null) + { + throw new InvalidOperationException($"{attributeName} not found in xml"); + } + + Attempt result = attribute.Value.TryConvertTo(); + if (result.Success) + { + return result.Result; + } + + throw new InvalidOperationException($"{attribute.Value} attribute value cannot be converted to {typeof(T)}"); + } + + public static T? AttributeValue(this XElement xml, string attributeName) + { + if (xml == null) + { + throw new ArgumentNullException("xml"); + } + + if (xml.HasAttributes == false) + { + return default; + } + + if (xml.Attribute(attributeName) == null) + { + return default; + } + + var val = xml.Attribute(attributeName)?.Value; + Attempt result = val.TryConvertTo(); + if (result.Success) + { + return result.Result; + } + + return default; + } + + public static T? AttributeValue(this XmlNode xml, string attributeName) + { + if (xml == null) + { + throw new ArgumentNullException("xml"); + } + + if (xml.Attributes == null) + { + return default; + } + + if (xml.Attributes[attributeName] == null) + { + return default; + } + + var val = xml.Attributes[attributeName]?.Value; + Attempt result = val.TryConvertTo(); + if (result.Success) + { + return result.Result; + } + + return default; + } + + public static XElement? GetXElement(this XmlNode node) + { + var xDoc = new XDocument(); + using (XmlWriter xmlWriter = xDoc.CreateWriter()) + { + node.WriteTo(xmlWriter); + } + + return xDoc.Root; + } + + public static XmlNode? GetXmlNode(this XContainer element) + { + using (XmlReader xmlReader = element.CreateReader()) + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(xmlReader); + return xmlDoc.DocumentElement; + } + } + + public static XmlNode? GetXmlNode(this XContainer element, XmlDocument xmlDoc) + { + XmlNode? node = element.GetXmlNode(); + if (node is not null) + { + return xmlDoc.ImportNode(node, true); + } + + return null; + } + + // this exists because + // new XElement("root", "a\nb").Value is "a\nb" but + // .ToString(SaveOptions.*) is "a\r\nb" and cannot figure out how to get rid of "\r" + // and when saving data we want nothing to change + // this method will produce a string that respects the \r and \n in the data value + public static string ToDataString(this XElement xml) + { + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + NewLineHandling = NewLineHandling.None, + Indent = false, + }; + var output = new StringBuilder(); + using (var writer = XmlWriter.Create(output, settings)) + { + xml.WriteTo(writer); + } + + return output.ToString(); + } } diff --git a/src/Umbraco.Core/Features/DisabledFeatures.cs b/src/Umbraco.Core/Features/DisabledFeatures.cs index e572818baf..e7f9eeb83d 100644 --- a/src/Umbraco.Core/Features/DisabledFeatures.cs +++ b/src/Umbraco.Core/Features/DisabledFeatures.cs @@ -1,34 +1,29 @@ using Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Features +namespace Umbraco.Cms.Core.Features; + +/// +/// Represents disabled features. +/// +public class DisabledFeatures { /// - /// Represents disabled features. + /// Initializes a new instance of the class. /// - public class DisabledFeatures - { - /// - /// Initializes a new instance of the class. - /// - public DisabledFeatures() - { - Controllers = new TypeList(); - } + public DisabledFeatures() => Controllers = new TypeList(); - /// - /// Gets the disabled controllers. - /// - public TypeList Controllers { get; } + /// + /// Gets the disabled controllers. + /// + public TypeList Controllers { get; } - /// - /// Disables the device preview feature of previewing. - /// - public bool DisableDevicePreview { get; set; } + /// + /// Disables the device preview feature of previewing. + /// + public bool DisableDevicePreview { get; set; } - /// - /// If true, all references to templates will be removed in the back office and routing - /// - public bool DisableTemplates { get; set; } - - } + /// + /// If true, all references to templates will be removed in the back office and routing + /// + public bool DisableTemplates { get; set; } } diff --git a/src/Umbraco.Core/Features/EnabledFeatures.cs b/src/Umbraco.Core/Features/EnabledFeatures.cs index 5fb7a581dc..aee19e2f14 100644 --- a/src/Umbraco.Core/Features/EnabledFeatures.cs +++ b/src/Umbraco.Core/Features/EnabledFeatures.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core.Features +namespace Umbraco.Cms.Core.Features; + +/// +/// Represents enabled features. +/// +public class EnabledFeatures { /// - /// Represents enabled features. + /// This allows us to inject a razor view into the Umbraco preview view to extend it /// - public class EnabledFeatures - { - /// - /// This allows us to inject a razor view into the Umbraco preview view to extend it - /// - /// - /// This is set to a virtual path of a razor view file - /// - public string? PreviewExtendedView { get; set; } - } + /// + /// This is set to a virtual path of a razor view file + /// + public string? PreviewExtendedView { get; set; } } diff --git a/src/Umbraco.Core/Features/IUmbracoFeature.cs b/src/Umbraco.Core/Features/IUmbracoFeature.cs index efb5337a00..8beaeef321 100644 --- a/src/Umbraco.Core/Features/IUmbracoFeature.cs +++ b/src/Umbraco.Core/Features/IUmbracoFeature.cs @@ -1,10 +1,8 @@ -namespace Umbraco.Cms.Core.Features -{ - /// - /// This is a marker interface to allow controllers to be disabled if also marked with FeatureAuthorizeAttribute. - /// - public interface IUmbracoFeature - { +namespace Umbraco.Cms.Core.Features; - } +/// +/// This is a marker interface to allow controllers to be disabled if also marked with FeatureAuthorizeAttribute. +/// +public interface IUmbracoFeature +{ } diff --git a/src/Umbraco.Core/Features/UmbracoFeatures.cs b/src/Umbraco.Core/Features/UmbracoFeatures.cs index 5b6bfd7bfb..0f971d8ba1 100644 --- a/src/Umbraco.Core/Features/UmbracoFeatures.cs +++ b/src/Umbraco.Core/Features/UmbracoFeatures.cs @@ -1,40 +1,39 @@ -using System; +namespace Umbraco.Cms.Core.Features; -namespace Umbraco.Cms.Core.Features +/// +/// Represents the Umbraco features. +/// +public class UmbracoFeatures { /// - /// Represents the Umbraco features. + /// Initializes a new instance of the class. /// - public class UmbracoFeatures + public UmbracoFeatures() { - /// - /// Initializes a new instance of the class. - /// - public UmbracoFeatures() + Disabled = new DisabledFeatures(); + Enabled = new EnabledFeatures(); + } + + /// + /// Gets the disabled features. + /// + public DisabledFeatures Disabled { get; } + + /// + /// Gets the enabled features. + /// + public EnabledFeatures Enabled { get; } + + /// + /// Determines whether a controller is enabled. + /// + public bool IsControllerEnabled(Type? feature) + { + if (typeof(IUmbracoFeature).IsAssignableFrom(feature)) { - Disabled = new DisabledFeatures(); - Enabled = new EnabledFeatures(); + return Disabled.Controllers.Contains(feature) == false; } - /// - /// Gets the disabled features. - /// - public DisabledFeatures Disabled { get; } - - /// - /// Gets the enabled features. - /// - public EnabledFeatures Enabled { get; } - - /// - /// Determines whether a controller is enabled. - /// - public bool IsControllerEnabled(Type? feature) - { - if (typeof(IUmbracoFeature).IsAssignableFrom(feature)) - return Disabled.Controllers.Contains(feature) == false; - - throw new NotSupportedException("Not a supported feature type."); - } + throw new NotSupportedException("Not a supported feature type."); } } diff --git a/src/Umbraco.Core/FireAndForgetRunner.cs b/src/Umbraco.Core/FireAndForgetRunner.cs new file mode 100644 index 0000000000..8c466bd439 --- /dev/null +++ b/src/Umbraco.Core/FireAndForgetRunner.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Logging; + +namespace Umbraco.Cms.Core; + + +public class FireAndForgetRunner : IFireAndForgetRunner +{ + private readonly ILogger _logger; + + public FireAndForgetRunner(ILogger logger) => _logger = logger; + + public void RunFireAndForget(Func task) => ExecuteBackgroundTask(task); + + private Task ExecuteBackgroundTask(Func fn) + { + // it is also possible to use UnsafeQueueUserWorkItem which does not flow the execution context, + // however that seems more difficult to use for async operations. + + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + // NOTE: ConfigureAwait(false) is irrelevant here, it is not needed because this is not being + // awaited. ConfigureAwait(false) is only relevant when awaiting to prevent the SynchronizationContext + // (very different from the ExecutionContext!) from running the continuation on the calling thread. + return Task.Run(LoggingWrapper(fn)); + } + } + + private Func LoggingWrapper(Func fn) => + async () => + { + try + { + await fn(); + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown in a background thread"); + } + }; +} diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs index 53c495ba87..e8280bceb6 100644 --- a/src/Umbraco.Core/GuidUdi.cs +++ b/src/Umbraco.Core/GuidUdi.cs @@ -1,66 +1,60 @@ -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a guid-based entity identifier. +/// +[TypeConverter(typeof(UdiTypeConverter))] +public class GuidUdi : Udi { /// - /// Represents a guid-based entity identifier. + /// Initializes a new instance of the GuidUdi class with an entity type and a guid. /// - [TypeConverter(typeof(UdiTypeConverter))] - public class GuidUdi : Udi + /// The entity type part of the udi. + /// The guid part of the udi. + public GuidUdi(string entityType, Guid guid) + : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) => + Guid = guid; + + /// + /// Initializes a new instance of the GuidUdi class with an uri value. + /// + /// The uri value of the udi. + public GuidUdi(Uri uriValue) + : base(uriValue) { - /// - /// The guid part of the identifier. - /// - public Guid Guid { get; private set; } - - /// - /// Initializes a new instance of the GuidUdi class with an entity type and a guid. - /// - /// The entity type part of the udi. - /// The guid part of the udi. - public GuidUdi(string entityType, Guid guid) - : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) + if (Guid.TryParse(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash), out Guid guid) == false) { - Guid = guid; + throw new FormatException("URI \"" + uriValue + "\" is not a GUID entity ID."); } - /// - /// Initializes a new instance of the GuidUdi class with an uri value. - /// - /// The uri value of the udi. - public GuidUdi(Uri uriValue) - : base(uriValue) - { - Guid guid; - if (Guid.TryParse(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash), out guid) == false) - throw new FormatException("URI \"" + uriValue + "\" is not a GUID entity ID."); + Guid = guid; + } - Guid = guid; + /// + /// The guid part of the identifier. + /// + public Guid Guid { get; } + + /// + public override bool IsRoot => Guid == Guid.Empty; + + public override bool Equals(object? obj) + { + if (obj is not GuidUdi other) + { + return false; } - public override bool Equals(object? obj) - { - var other = obj as GuidUdi; - if (other is null) return false; - return EntityType == other.EntityType && Guid == other.Guid; - } + return EntityType == other.EntityType && Guid == other.Guid; + } - public override int GetHashCode() - { - return base.GetHashCode(); - } + public override int GetHashCode() => base.GetHashCode(); - /// - public override bool IsRoot - { - get { return Guid == Guid.Empty; } - } - - public GuidUdi EnsureClosed() - { - EnsureNotRoot(); - return this; - } + public GuidUdi EnsureClosed() + { + EnsureNotRoot(); + return this; } } diff --git a/src/Umbraco.Core/GuidUtils.cs b/src/Umbraco.Core/GuidUtils.cs index e6ccd6b27f..290f36cdcf 100644 --- a/src/Umbraco.Core/GuidUtils.cs +++ b/src/Umbraco.Core/GuidUtils.cs @@ -1,112 +1,154 @@ -using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Utility methods for the struct. +/// +public static class GuidUtils { - /// - /// Utility methods for the struct. - /// - public static class GuidUtils + private static readonly char[] Base32Table = { - /// - /// Combines two guid instances utilizing an exclusive disjunction. - /// The resultant guid is not guaranteed to be unique since the number of unique bits is halved. - /// - /// The first guid. - /// The seconds guid. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid Combine(Guid a, Guid b) + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', + 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + }; + + /// + /// Combines two guid instances utilizing an exclusive disjunction. + /// The resultant guid is not guaranteed to be unique since the number of unique bits is halved. + /// + /// The first guid. + /// The seconds guid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Combine(Guid a, Guid b) + { + var ad = new DecomposedGuid(a); + var bd = new DecomposedGuid(b); + + ad.Hi ^= bd.Hi; + ad.Lo ^= bd.Lo; + + return ad.Value; + } + + /// + /// Converts a Guid into a base-32 string. + /// + /// A Guid. + /// The string length. + /// A base-32 encoded string. + /// + /// + /// A base-32 string representation of a Guid is the shortest, efficient, representation + /// that is case insensitive (base-64 is case sensitive). + /// + /// Length must be 1-26, anything else becomes 26. + /// + public static string ToBase32String(Guid guid, int length = 26) + { + if (length <= 0 || length > 26) { - var ad = new DecomposedGuid(a); - var bd = new DecomposedGuid(b); - - ad.Hi ^= bd.Hi; - ad.Lo ^= bd.Lo; - - return ad.Value; + length = 26; } - /// - /// A decomposed guid. Allows access to the high and low bits without unsafe code. - /// - [StructLayout(LayoutKind.Explicit)] - private struct DecomposedGuid + var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes + + // this could be optimized by making it unsafe, + // and fixing the table + bytes + chars (see Convert.ToBase64CharArray) + + // each block of 5 bytes = 5*8 = 40 bits + // becomes 40 bits = 8*5 = 8 byte-32 chars + // a Guid is 3 blocks + 8 bits + + // so it turns into a 3*8+2 = 26 chars string + var chars = new char[length]; + + var i = 0; + var j = 0; + + while (i < 15) { - [FieldOffset(00)] public Guid Value; - [FieldOffset(00)] public long Hi; - [FieldOffset(08)] public long Lo; - - public DecomposedGuid(Guid value) : this() => this.Value = value; - } - - private static readonly char[] Base32Table = - { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', - 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5' - }; - - /// - /// Converts a Guid into a base-32 string. - /// - /// A Guid. - /// The string length. - /// A base-32 encoded string. - /// - /// A base-32 string representation of a Guid is the shortest, efficient, representation - /// that is case insensitive (base-64 is case sensitive). - /// Length must be 1-26, anything else becomes 26. - /// - public static string ToBase32String(Guid guid, int length = 26) - { - - if (length <= 0 || length > 26) - length = 26; - - var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes - - // this could be optimized by making it unsafe, - // and fixing the table + bytes + chars (see Convert.ToBase64CharArray) - - // each block of 5 bytes = 5*8 = 40 bits - // becomes 40 bits = 8*5 = 8 byte-32 chars - // a Guid is 3 blocks + 8 bits - - // so it turns into a 3*8+2 = 26 chars string - var chars = new char[length]; - - var i = 0; - var j = 0; - - while (i < 15) + if (j == length) { - if (j == length) break; - chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; - if (j == length) break; - chars[j++] = Base32Table[((bytes[i] & 0b0000_0111) << 2) | ((bytes[i + 1] & 0b1100_0000) >> 6)]; - if (j == length) break; - chars[j++] = Base32Table[(bytes[i + 1] & 0b0011_1110) >> 1]; - if (j == length) break; - chars[j++] = Base32Table[(bytes[i + 1] & 0b0000_0001) | ((bytes[i + 2] & 0b1111_0000) >> 4)]; - if (j == length) break; - chars[j++] = Base32Table[((bytes[i + 2] & 0b0000_1111) << 1) | ((bytes[i + 3] & 0b1000_0000) >> 7)]; - if (j == length) break; - chars[j++] = Base32Table[(bytes[i + 3] & 0b0111_1100) >> 2]; - if (j == length) break; - chars[j++] = Base32Table[((bytes[i + 3] & 0b0000_0011) << 3) | ((bytes[i + 4] & 0b1110_0000) >> 5)]; - if (j == length) break; - chars[j++] = Base32Table[bytes[i + 4] & 0b0001_1111]; - - i += 5; + break; } - if (j < length) - chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; - if (j < length) - chars[j] = Base32Table[(bytes[i] & 0b0000_0111) << 2]; + chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; + if (j == length) + { + break; + } - return new string(chars); + chars[j++] = Base32Table[((bytes[i] & 0b0000_0111) << 2) | ((bytes[i + 1] & 0b1100_0000) >> 6)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[(bytes[i + 1] & 0b0011_1110) >> 1]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[(bytes[i + 1] & 0b0000_0001) | ((bytes[i + 2] & 0b1111_0000) >> 4)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[((bytes[i + 2] & 0b0000_1111) << 1) | ((bytes[i + 3] & 0b1000_0000) >> 7)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[(bytes[i + 3] & 0b0111_1100) >> 2]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[((bytes[i + 3] & 0b0000_0011) << 3) | ((bytes[i + 4] & 0b1110_0000) >> 5)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[bytes[i + 4] & 0b0001_1111]; + + i += 5; } + + if (j < length) + { + chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; + } + + if (j < length) + { + chars[j] = Base32Table[(bytes[i] & 0b0000_0111) << 2]; + } + + return new string(chars); + } + + /// + /// A decomposed guid. Allows access to the high and low bits without unsafe code. + /// + [StructLayout(LayoutKind.Explicit)] + private struct DecomposedGuid + { + [FieldOffset(00)] + public readonly Guid Value; + [FieldOffset(00)] + public long Hi; + [FieldOffset(08)] + public long Lo; + + public DecomposedGuid(Guid value) + : this() => Value = value; } } diff --git a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs index f15edfa1be..28fbea027b 100644 --- a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs +++ b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs @@ -1,10 +1,9 @@ -using System; -using System.Linq; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Notifications; @@ -12,228 +11,295 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Handlers +namespace Umbraco.Cms.Core.Handlers; + +public sealed class AuditNotificationsHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - public sealed class AuditNotificationsHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly IAuditService _auditService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IEntityService _entityService; + private readonly GlobalSettings _globalSettings; + private readonly IIpResolver _ipResolver; + private readonly IMemberService _memberService; + private readonly IUserService _userService; + + public AuditNotificationsHandler( + IAuditService auditService, + IUserService userService, + IEntityService entityService, + IIpResolver ipResolver, + IOptionsMonitor globalSettings, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMemberService memberService) { - private readonly IAuditService _auditService; - private readonly IUserService _userService; - private readonly IEntityService _entityService; - private readonly IIpResolver _ipResolver; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly GlobalSettings _globalSettings; - private readonly IMemberService _memberService; + _auditService = auditService; + _userService = userService; + _entityService = entityService; + _ipResolver = ipResolver; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _memberService = memberService; + _globalSettings = globalSettings.CurrentValue; + } - public AuditNotificationsHandler( - IAuditService auditService, - IUserService userService, - IEntityService entityService, - IIpResolver ipResolver, - IOptionsMonitor globalSettings, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IMemberService memberService) + private IUser CurrentPerformingUser + { + get { - _auditService = auditService; - _userService = userService; - _entityService = entityService; - _ipResolver = ipResolver; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _memberService = memberService; - _globalSettings = globalSettings.CurrentValue; + IUser? identity = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + IUser? user = identity == null ? null : _userService.GetUserById(Convert.ToInt32(identity.Id)); + return user ?? UnknownUser(_globalSettings); } + } - private IUser CurrentPerformingUser + private string PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); + + public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) + { + Id = Constants.Security.UnknownUserId, + Name = Constants.Security.UnknownUserName, + Email = string.Empty, + }; + + public void Handle(AssignedMemberRolesNotification notification) + { + IUser performingUser = CurrentPerformingUser; + var roles = string.Join(", ", notification.Roles); + var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in notification.MemberIds) { - get - { - var identity = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; - var user = identity == null ? null : _userService.GetUserById(Convert.ToInt32(identity.Id)); - return user ?? UnknownUser(_globalSettings); - } - } - - public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Constants.Security.UnknownUserId, Name = Constants.Security.UnknownUserName, Email = "" }; - - private string PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); - - private string FormatEmail(IMember? member) => member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>"; - - private string FormatEmail(IUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; - - public void Handle(MemberSavedNotification notification) - { - var performingUser = CurrentPerformingUser; - var members = notification.SavedEntities; - foreach (var member in members) - { - var dp = string.Join(", ", ((Member)member).GetWereDirtyProperties()); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); - } - } - - public void Handle(MemberDeletedNotification notification) - { - var performingUser = CurrentPerformingUser; - var members = notification.DeletedEntities; - foreach (var member in members) - { - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/delete", $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); - } - } - - public void Handle(AssignedMemberRolesNotification notification) - { - var performingUser = CurrentPerformingUser; - var roles = string.Join(", ", notification.Roles); - var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); - foreach (var id in notification.MemberIds) - { - members.TryGetValue(id, out var member); - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", - "umbraco/member/roles/assigned", $"roles modified, assigned {roles}"); - } - } - - public void Handle(RemovedMemberRolesNotification notification) - { - var performingUser = CurrentPerformingUser; - var roles = string.Join(", ", notification.Roles); - var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); - foreach (var id in notification.MemberIds) - { - members.TryGetValue(id, out var member); - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", - "umbraco/member/roles/removed", $"roles modified, removed {roles}"); - } - } - - public void Handle(ExportedMemberNotification notification) - { - var performingUser = CurrentPerformingUser; - var member = notification.Member; - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + members.TryGetValue(id, out IMember? member); + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/exported", "exported member data"); + -1, + $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", + "umbraco/member/roles/assigned", + $"roles modified, assigned {roles}"); } + } - public void Handle(UserSavedNotification notification) + public void Handle(AssignedUserGroupPermissionsNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable perms = notification.EntityPermissions; + foreach (EntityPermission perm in perms) { - var performingUser = CurrentPerformingUser; - var affectedUsers = notification.SavedEntities; - foreach (var affectedUser in affectedUsers) + IUserGroup? group = _userService.GetUserGroupById(perm.UserGroupId); + var assigned = string.Join(", ", perm.AssignedPermissions ?? Array.Empty()); + IEntitySlim? entity = _entityService.Get(perm.EntityId); + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"User Group {group?.Id} \"{group?.Name}\" ({group?.Alias})", + "umbraco/user-group/permissions-change", + $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity?.Name}\""); + } + } + + public void Handle(ExportedMemberNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IMember member = notification.Member; + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/exported", + "exported member data"); + } + + public void Handle(MemberDeletedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable members = notification.DeletedEntities; + foreach (IMember member in members) + { + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/delete", + $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); + } + } + + public void Handle(MemberSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable members = notification.SavedEntities; + foreach (IMember member in members) + { + var dp = string.Join(", ", ((Member)member).GetWereDirtyProperties()); + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/save", + $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); + } + } + + public void Handle(RemovedMemberRolesNotification notification) + { + IUser performingUser = CurrentPerformingUser; + var roles = string.Join(", ", notification.Roles); + var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in notification.MemberIds) + { + members.TryGetValue(id, out IMember? member); + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", + "umbraco/member/roles/removed", + $"roles modified, removed {roles}"); + } + } + + public void Handle(UserDeletedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable affectedUsers = notification.DeletedEntities; + foreach (IUser affectedUser in affectedUsers) + { + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + affectedUser.Id, + $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/delete", + "delete user"); + } + } + + public void Handle(UserGroupWithUsersSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + foreach (UserGroupWithUsers groupWithUser in notification.SavedEntities) + { + IUserGroup group = groupWithUser.UserGroup; + + var dp = string.Join(", ", ((UserGroup)group).GetWereDirtyProperties()); + var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections") + ? string.Join(", ", group.AllowedSections) + : null; + var perms = ((UserGroup)group).WasPropertyDirty("Permissions") && group.Permissions is not null + ? string.Join(", ", group.Permissions) + : null; + + var sb = new StringBuilder(); + sb.Append($"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)};"); + if (sections != null) { - var groups = affectedUser.WasPropertyDirty("Groups") - ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) - : null; - - var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", - "umbraco/user/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? "" : "; groups assigned: " + groups)}"); + sb.Append($", assigned sections: {sections}"); } - } - public void Handle(UserDeletedNotification notification) - { - var performingUser = CurrentPerformingUser; - var affectedUsers = notification.DeletedEntities; - foreach (var affectedUser in affectedUsers) - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", - "umbraco/user/delete", "delete user"); - } - - public void Handle(UserGroupWithUsersSavedNotification notification) - { - var performingUser = CurrentPerformingUser; - foreach (var groupWithUser in notification.SavedEntities) + if (perms != null) { - var group = groupWithUser.UserGroup; - - var dp = string.Join(", ", ((UserGroup)group).GetWereDirtyProperties()); - var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections") - ? string.Join(", ", group.AllowedSections) - : null; - var perms = ((UserGroup)group).WasPropertyDirty("Permissions") && group.Permissions is not null - ? string.Join(", ", group.Permissions) - : null; - - var sb = new StringBuilder(); - sb.Append($"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)};"); if (sections != null) - sb.Append($", assigned sections: {sections}"); - if (perms != null) { - if (sections != null) - sb.Append(", "); - sb.Append($"default perms: {perms}"); + sb.Append(", "); } - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", - "umbraco/user-group/save", $"{sb}"); - - // now audit the users that have changed - - foreach (var user in groupWithUser.RemovedUsers) - { - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", - "umbraco/user-group/save", $"Removed user \"{user.Name}\" {FormatEmail(user)} from group {group.Id} \"{group.Name}\" ({group.Alias})"); - } - - foreach (var user in groupWithUser.AddedUsers) - { - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", - "umbraco/user-group/save", $"Added user \"{user.Name}\" {FormatEmail(user)} to group {group.Id} \"{group.Name}\" ({group.Alias})"); - } + sb.Append($"default perms: {perms}"); } - } - public void Handle(AssignedUserGroupPermissionsNotification notification) - { - var performingUser = CurrentPerformingUser; - var perms = notification.EntityPermissions; - foreach (EntityPermission perm in perms) + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", + "umbraco/user-group/save", + $"{sb}"); + + // now audit the users that have changed + foreach (IUser user in groupWithUser.RemovedUsers) { - var group = _userService.GetUserGroupById(perm.UserGroupId); - var assigned = string.Join(", ", perm.AssignedPermissions ?? Array.Empty()); - var entity = _entityService.Get(perm.EntityId); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, DateTime.UtcNow, - -1, $"User Group {group?.Id} \"{group?.Name}\" ({group?.Alias})", - "umbraco/user-group/permissions-change", $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity?.Name}\""); + user.Id, + $"User \"{user.Name}\" {FormatEmail(user)}", + "umbraco/user-group/save", + $"Removed user \"{user.Name}\" {FormatEmail(user)} from group {group.Id} \"{group.Name}\" ({group.Alias})"); + } + + foreach (IUser user in groupWithUser.AddedUsers) + { + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + user.Id, + $"User \"{user.Name}\" {FormatEmail(user)}", + "umbraco/user-group/save", + $"Added user \"{user.Name}\" {FormatEmail(user)} to group {group.Id} \"{group.Name}\" ({group.Alias})"); } } } + + public void Handle(UserSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable affectedUsers = notification.SavedEntities; + foreach (IUser affectedUser in affectedUsers) + { + var groups = affectedUser.WasPropertyDirty("Groups") + ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) + : null; + + var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + affectedUser.Id, + $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/save", + $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? string.Empty : "; groups assigned: " + groups)}"); + } + } + + private string FormatEmail(IMember? member) => + member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{member.Email}>"; + + private string FormatEmail(IUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{user.Email}>"; } diff --git a/src/Umbraco.Core/Handlers/PublicAccessHandler.cs b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs index 466e09e3f1..d441509a85 100644 --- a/src/Umbraco.Core/Handlers/PublicAccessHandler.cs +++ b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs @@ -1,38 +1,37 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Handlers +namespace Umbraco.Cms.Core.Handlers; + +public sealed class PublicAccessHandler : + INotificationHandler, + INotificationHandler { - public sealed class PublicAccessHandler : - INotificationHandler, - INotificationHandler + private readonly IPublicAccessService _publicAccessService; + + public PublicAccessHandler(IPublicAccessService publicAccessService) => + _publicAccessService = publicAccessService ?? throw new ArgumentNullException(nameof(publicAccessService)); + + public void Handle(MemberGroupDeletedNotification notification) => Handle(notification.DeletedEntities); + + public void Handle(MemberGroupSavedNotification notification) => Handle(notification.SavedEntities); + + private void Handle(IEnumerable affectedEntities) { - private readonly IPublicAccessService _publicAccessService; - - public PublicAccessHandler(IPublicAccessService publicAccessService) => - _publicAccessService = publicAccessService ?? throw new ArgumentNullException(nameof(publicAccessService)); - - public void Handle(MemberGroupSavedNotification notification) => Handle(notification.SavedEntities); - - public void Handle(MemberGroupDeletedNotification notification) => Handle(notification.DeletedEntities); - - private void Handle(IEnumerable affectedEntities) + foreach (IMemberGroup grp in affectedEntities) { - foreach (var grp in affectedEntities) + // check if the name has changed + if ((grp.AdditionalData?.ContainsKey("previousName") ?? false) + && grp.AdditionalData["previousName"] != null + && grp.AdditionalData["previousName"]?.ToString().IsNullOrWhiteSpace() == false + && grp.AdditionalData["previousName"]?.ToString() != grp.Name) { - //check if the name has changed - if ((grp.AdditionalData?.ContainsKey("previousName") ?? false) - && grp.AdditionalData["previousName"] != null - && grp.AdditionalData["previousName"]?.ToString().IsNullOrWhiteSpace() == false - && grp.AdditionalData["previousName"]?.ToString() != grp.Name) - { - _publicAccessService.RenameMemberGroupRoleRules(grp.AdditionalData["previousName"]?.ToString(), grp.Name); - } + _publicAccessService.RenameMemberGroupRoleRules( + grp.AdditionalData["previousName"]?.ToString(), + grp.Name); } } } diff --git a/src/Umbraco.Core/HashCodeCombiner.cs b/src/Umbraco.Core/HashCodeCombiner.cs index d8c1ac2a07..3506d335b8 100644 --- a/src/Umbraco.Core/HashCodeCombiner.cs +++ b/src/Umbraco.Core/HashCodeCombiner.cs @@ -1,100 +1,83 @@ -using System; using System.Globalization; -using System.IO; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Used to create a .NET HashCode from multiple objects. +/// +/// +/// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things +/// which we've not included here as we just need a quick easy class for this in order to create a unique +/// hash of directories/files to see if they have changed. +/// NOTE: It's probably best to not relying on the hashing result across AppDomains! If you need a constant/reliable +/// hash value +/// between AppDomains use SHA1. This is perfect for hashing things in a very fast way for a single AppDomain. +/// +public class HashCodeCombiner { - /// - /// Used to create a .NET HashCode from multiple objects. - /// - /// - /// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things - /// which we've not included here as we just need a quick easy class for this in order to create a unique - /// hash of directories/files to see if they have changed. - /// - /// NOTE: It's probably best to not relying on the hashing result across AppDomains! If you need a constant/reliable hash value - /// between AppDomains use SHA1. This is perfect for hashing things in a very fast way for a single AppDomain. - /// - public class HashCodeCombiner + private long _combinedHash = 5381L; + + public void AddInt(int i) => _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + + public void AddObject(object o) => AddInt(o.GetHashCode()); + + public void AddDateTime(DateTime d) => AddInt(d.GetHashCode()); + + public void AddString(string s) { - private long _combinedHash = 5381L; - - public void AddInt(int i) + if (s != null) { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + AddInt(StringComparer.InvariantCulture.GetHashCode(s)); } - - public void AddObject(object o) - { - AddInt(o.GetHashCode()); - } - - public void AddDateTime(DateTime d) - { - AddInt(d.GetHashCode()); - } - - public void AddString(string s) - { - if (s != null) - AddInt((StringComparer.InvariantCulture).GetHashCode(s)); - } - - public void AddCaseInsensitiveString(string s) - { - if (s != null) - AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); - } - - public void AddFileSystemItem(FileSystemInfo f) - { - //if it doesn't exist, don't proceed. - if (!f.Exists) - return; - - AddCaseInsensitiveString(f.FullName); - AddDateTime(f.CreationTimeUtc); - AddDateTime(f.LastWriteTimeUtc); - - //check if it is a file or folder - var fileInfo = f as FileInfo; - if (fileInfo != null) - { - AddInt(fileInfo.Length.GetHashCode()); - } - - var dirInfo = f as DirectoryInfo; - if (dirInfo != null) - { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } - } - } - - public void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } - - public void AddFolder(DirectoryInfo d) - { - AddFileSystemItem(d); - } - - /// - /// Returns the hex code of the combined hash code - /// - /// - public string GetCombinedHashCode() - { - return _combinedHash.ToString("x", CultureInfo.InvariantCulture); - } - } + + public void AddCaseInsensitiveString(string s) + { + if (s != null) + { + AddInt(StringComparer.InvariantCultureIgnoreCase.GetHashCode(s)); + } + } + + public void AddFileSystemItem(FileSystemInfo f) + { + // if it doesn't exist, don't proceed. + if (!f.Exists) + { + return; + } + + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); + + // check if it is a file or folder + if (f is FileInfo fileInfo) + { + AddInt(fileInfo.Length.GetHashCode()); + } + + if (f is DirectoryInfo dirInfo) + { + foreach (FileInfo d in dirInfo.GetFiles()) + { + AddFile(d); + } + + foreach (DirectoryInfo s in dirInfo.GetDirectories()) + { + AddFolder(s); + } + } + } + + public void AddFile(FileInfo f) => AddFileSystemItem(f); + + public void AddFolder(DirectoryInfo d) => AddFileSystemItem(d); + + /// + /// Returns the hex code of the combined hash code + /// + /// + public string GetCombinedHashCode() => _combinedHash.ToString("x", CultureInfo.InvariantCulture); } diff --git a/src/Umbraco.Core/HashCodeHelper.cs b/src/Umbraco.Core/HashCodeHelper.cs index 6d98ec57b8..ecf209c532 100644 --- a/src/Umbraco.Core/HashCodeHelper.cs +++ b/src/Umbraco.Core/HashCodeHelper.cs @@ -1,104 +1,115 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Borrowed from http://stackoverflow.com/a/2575444/694494 +/// +public static class HashCodeHelper { - /// - /// Borrowed from http://stackoverflow.com/a/2575444/694494 - /// - public static class HashCodeHelper + public static int GetHashCode(T1 arg1, T2 arg2) { - public static int GetHashCode(T1 arg1, T2 arg2) + unchecked { - unchecked - { - return 31 * arg1!.GetHashCode() + arg2!.GetHashCode(); - } + return (31 * arg1!.GetHashCode()) + arg2!.GetHashCode(); } + } - public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3) + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3) + { + unchecked { - unchecked - { - int hash = arg1!.GetHashCode(); - hash = 31 * hash + arg2!.GetHashCode(); - return 31 * hash + arg3!.GetHashCode(); - } + var hash = arg1!.GetHashCode(); + hash = (31 * hash) + arg2!.GetHashCode(); + return (31 * hash) + arg3!.GetHashCode(); } + } - public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3, - T4 arg4) + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + unchecked { - unchecked - { - int hash = arg1!.GetHashCode(); - hash = 31 * hash + arg2!.GetHashCode(); - hash = 31 * hash + arg3!.GetHashCode(); - return 31 * hash + arg4!.GetHashCode(); - } + var hash = arg1!.GetHashCode(); + hash = (31 * hash) + arg2!.GetHashCode(); + hash = (31 * hash) + arg3!.GetHashCode(); + return (31 * hash) + arg4!.GetHashCode(); } + } - public static int GetHashCode(T[] list) + public static int GetHashCode(T[] list) + { + unchecked { - unchecked + var hash = 0; + foreach (T item in list) { - int hash = 0; - foreach (var item in list) + if (item == null) { - if (item == null) continue; - hash = 31 * hash + item.GetHashCode(); + continue; } - return hash; - } - } - public static int GetHashCode(IEnumerable list) + hash = (31 * hash) + item.GetHashCode(); + } + + return hash; + } + } + + public static int GetHashCode(IEnumerable list) + { + unchecked { - unchecked + var hash = 0; + foreach (T item in list) { - int hash = 0; - foreach (var item in list) + if (item == null) { - if (item == null) continue; - hash = 31 * hash + item.GetHashCode(); + continue; } - return hash; - } - } - /// - /// Gets a hashcode for a collection for that the order of items - /// does not matter. - /// So {1, 2, 3} and {3, 2, 1} will get same hash code. - /// - public static int GetHashCodeForOrderNoMatterCollection( - IEnumerable list) + hash = (31 * hash) + item.GetHashCode(); + } + + return hash; + } + } + + /// + /// Gets a hashcode for a collection for that the order of items + /// does not matter. + /// So {1, 2, 3} and {3, 2, 1} will get same hash code. + /// + public static int GetHashCodeForOrderNoMatterCollection( + IEnumerable list) + { + unchecked { - unchecked + var hash = 0; + var count = 0; + foreach (T item in list) { - int hash = 0; - int count = 0; - foreach (var item in list) + if (item == null) { - if (item == null) continue; - hash += item.GetHashCode(); - count++; + continue; } - return 31 * hash + count.GetHashCode(); - } - } - /// - /// Alternative way to get a hashcode is to use a fluent - /// interface like this:
- /// return 0.CombineHashCode(field1).CombineHashCode(field2). - /// CombineHashCode(field3); - ///
- public static int CombineHashCode(this int hashCode, T arg) - { - unchecked - { - return 31 * hashCode + arg!.GetHashCode(); + hash += item.GetHashCode(); + count++; } + + return (31 * hash) + count.GetHashCode(); + } + } + + /// + /// Alternative way to get a hashcode is to use a fluent + /// interface like this:
+ /// return 0.CombineHashCode(field1).CombineHashCode(field2). + /// CombineHashCode(field3); + ///
+ public static int CombineHashCode(this int hashCode, T arg) + { + unchecked + { + return (31 * hashCode) + arg!.GetHashCode(); } } } diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 944e0bdf49..cad3d4b6b8 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -1,151 +1,137 @@ -using System; -using System.IO; using System.Security.Cryptography; using System.Text; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Used to generate a string hash using crypto libraries over multiple objects +/// +/// +/// This should be used to generate a reliable hash that survives AppDomain restarts. +/// This will use the crypto libs to generate the hash and will try to ensure that +/// strings, etc... are not re-allocated so it's not consuming much memory. +/// +public class HashGenerator : DisposableObjectSlim { - /// - /// Used to generate a string hash using crypto libraries over multiple objects - /// - /// - /// This should be used to generate a reliable hash that survives AppDomain restarts. - /// This will use the crypto libs to generate the hash and will try to ensure that - /// strings, etc... are not re-allocated so it's not consuming much memory. - /// - public class HashGenerator : DisposableObjectSlim + private readonly MemoryStream _ms = new(); + private StreamWriter _writer; + + public HashGenerator() => _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, true); + + public void AddInt(int i) => _writer.Write(i); + + public void AddLong(long i) => _writer.Write(i); + + public void AddObject(object o) => _writer.Write(o); + + public void AddDateTime(DateTime d) => _writer.Write(d.Ticks); + + public void AddString(string s) { - public HashGenerator() + if (s != null) { - _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, leaveOpen: true); - } - - private readonly MemoryStream _ms = new MemoryStream(); - private StreamWriter _writer; - - public void AddInt(int i) - { - _writer.Write(i); - } - - public void AddLong(long i) - { - _writer.Write(i); - } - - public void AddObject(object o) - { - _writer.Write(o); - } - - public void AddDateTime(DateTime d) - { - _writer.Write(d.Ticks); - } - - public void AddString(string s) - { - if (s != null) - _writer.Write(s); - } - - public void AddCaseInsensitiveString(string s) - { - //I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new - //byte array that we can use to write to the output, however this also allocates new objects so i really don't think the performance - //would be much different. In any case, I'll leave this here for reference. We could write the bytes out based on the sort key, - //this is how we could deal with case insensitivity without allocating another string - //for reference see: https://stackoverflow.com/a/10452967/694494 - //we could go a step further and s.Normalize() but we're not really dealing with crazy unicode with this class so far. - - if (s != null) - _writer.Write(s.ToUpperInvariant()); - } - - public void AddFileSystemItem(FileSystemInfo f) - { - //if it doesn't exist, don't proceed. - if (f.Exists == false) - return; - - AddCaseInsensitiveString(f.FullName); - AddDateTime(f.CreationTimeUtc); - AddDateTime(f.LastWriteTimeUtc); - - //check if it is a file or folder - if (f is FileInfo fileInfo) - { - AddLong(fileInfo.Length); - } - - if (f is DirectoryInfo dirInfo) - { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } - } - } - - public void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } - - public void AddFolder(DirectoryInfo d) - { - AddFileSystemItem(d); - } - - /// - /// Returns the generated hash output of all added objects - /// - /// - public string GenerateHash() - { - //flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called. - - _writer.Flush(); - _writer.Close(); - _writer.Dispose(); - _writer = new StreamWriter(_ms, Encoding.UTF8, 1024, leaveOpen: true); - - var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"; - - //create an instance of the correct hashing provider based on the type passed in - var hasher = HashAlgorithm.Create(hashType); - if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType); - using (hasher) - { - var buffer = _ms.GetBuffer(); - //get the hashed values created by our selected provider - var hashedByteArray = hasher.ComputeHash(buffer); - - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each byte - foreach (var b in hashedByteArray) - { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2")); - } - - //return the hashed value - return stringBuilder.ToString(); - } - } - - protected override void DisposeResources() - { - _writer.Close(); - _writer.Dispose(); - _ms.Close(); - _ms.Dispose(); + _writer.Write(s); } } + + public void AddCaseInsensitiveString(string s) + { + // I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new + // byte array that we can use to write to the output, however this also allocates new objects so i really don't think the performance + // would be much different. In any case, I'll leave this here for reference. We could write the bytes out based on the sort key, + // this is how we could deal with case insensitivity without allocating another string + // for reference see: https://stackoverflow.com/a/10452967/694494 + // we could go a step further and s.Normalize() but we're not really dealing with crazy unicode with this class so far. + if (s != null) + { + _writer.Write(s.ToUpperInvariant()); + } + } + + public void AddFileSystemItem(FileSystemInfo f) + { + // if it doesn't exist, don't proceed. + if (f.Exists == false) + { + return; + } + + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); + + // check if it is a file or folder + if (f is FileInfo fileInfo) + { + AddLong(fileInfo.Length); + } + + if (f is DirectoryInfo dirInfo) + { + foreach (FileInfo d in dirInfo.GetFiles()) + { + AddFile(d); + } + + foreach (DirectoryInfo s in dirInfo.GetDirectories()) + { + AddFolder(s); + } + } + } + + public void AddFile(FileInfo f) => AddFileSystemItem(f); + + public void AddFolder(DirectoryInfo d) => AddFileSystemItem(d); + + /// + /// Returns the generated hash output of all added objects + /// + /// + public string GenerateHash() + { + // flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called. + _writer.Flush(); + _writer.Close(); + _writer.Dispose(); + _writer = new StreamWriter(_ms, Encoding.UTF8, 1024, true); + + var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"; + + // create an instance of the correct hashing provider based on the type passed in + var hasher = HashAlgorithm.Create(hashType); + if (hasher == null) + { + throw new InvalidOperationException("No hashing type found by name " + hashType); + } + + using (hasher) + { + var buffer = _ms.GetBuffer(); + + // get the hashed values created by our selected provider + var hashedByteArray = hasher.ComputeHash(buffer); + + // create a StringBuilder object + var stringBuilder = new StringBuilder(); + + // loop to each byte + foreach (var b in hashedByteArray) + { + // append it to our StringBuilder + stringBuilder.Append(b.ToString("x2")); + } + + // return the hashed value + return stringBuilder.ToString(); + } + } + + protected override void DisposeResources() + { + _writer.Close(); + _writer.Dispose(); + _ms.Close(); + _ms.Dispose(); + } } diff --git a/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs b/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs index 93cdea7c0b..42420b8954 100644 --- a/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs +++ b/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class AcceptableConfiguration { - public class AcceptableConfiguration - { - public string? Value { get; set; } - public bool IsRecommended { get; set; } - } + public string? Value { get; set; } + + public bool IsRecommended { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs index 7123255b0d..4dddf34270 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs @@ -1,101 +1,93 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks +namespace Umbraco.Cms.Core.HealthChecks.Checks; + +/// +/// Provides a base class for health checks of configuration values. +/// +public abstract class AbstractSettingsCheck : HealthCheck { /// - /// Provides a base class for health checks of configuration values. + /// Initializes a new instance of the class. /// - public abstract class AbstractSettingsCheck : HealthCheck + protected AbstractSettingsCheck(ILocalizedTextService textService) => LocalizedTextService = textService; + + /// + /// Gets key within the JSON to check, in the colon-delimited format + /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1 + /// + public abstract string ItemPath { get; } + + /// + /// Gets the localized text service. + /// + protected ILocalizedTextService LocalizedTextService { get; } + + /// + /// Gets a link to an external resource with more information. + /// + public abstract string ReadMoreLink { get; } + + /// + /// Gets the values to compare against. + /// + public abstract IEnumerable Values { get; } + + /// + /// Gets the current value of the config setting + /// + public abstract string CurrentValue { get; } + + /// + /// Gets the comparison type for checking the value. + /// + public abstract ValueComparisonType ValueComparisonType { get; } + + /// + /// Gets the message for when the check has succeeded. + /// + public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); + + /// + /// Gets the message for when the check has failed. + /// + public virtual string CheckErrorMessage => + ValueComparisonType == ValueComparisonType.ShouldEqual + ? LocalizedTextService.Localize( + "healthcheck", "checkErrorMessageDifferentExpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }) + : LocalizedTextService.Localize( + "healthcheck", "checkErrorMessageUnexpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); + + /// + public override Task> GetStatus() { - /// - /// Initializes a new instance of the class. - /// - protected AbstractSettingsCheck(ILocalizedTextService textService) => LocalizedTextService = textService; + // update the successMessage with the CurrentValue + var successMessage = string.Format(CheckSuccessMessage, ItemPath, Values, CurrentValue); + var valueFound = Values.Any(value => + string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase)); - /// - /// Gets the localized text service. - /// - protected ILocalizedTextService LocalizedTextService { get; } - - /// - /// Gets key within the JSON to check, in the colon-delimited format - /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1 - /// - public abstract string ItemPath { get; } - - /// - /// Gets a link to an external resource with more information. - /// - public abstract string ReadMoreLink { get; } - - /// - /// Gets the values to compare against. - /// - public abstract IEnumerable Values { get; } - - /// - /// Gets the current value of the config setting - /// - public abstract string CurrentValue { get; } - - /// - /// Gets the comparison type for checking the value. - /// - public abstract ValueComparisonType ValueComparisonType { get; } - - /// - /// Gets the message for when the check has succeeded. - /// - public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); - - /// - /// Gets the message for when the check has failed. - /// - public virtual string CheckErrorMessage => - ValueComparisonType == ValueComparisonType.ShouldEqual - ? LocalizedTextService.Localize( - "healthcheck", "checkErrorMessageDifferentExpectedValue", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }) - : LocalizedTextService.Localize( - "healthcheck", "checkErrorMessageUnexpectedValue", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); - - /// - public override Task> GetStatus() + if ((ValueComparisonType == ValueComparisonType.ShouldEqual && valueFound) + || (ValueComparisonType == ValueComparisonType.ShouldNotEqual && valueFound == false)) { - // update the successMessage with the CurrentValue - var successMessage = string.Format(CheckSuccessMessage, ItemPath, Values, CurrentValue); - bool valueFound = Values.Any(value => string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase)); - - if ((ValueComparisonType == ValueComparisonType.ShouldEqual && valueFound) - || (ValueComparisonType == ValueComparisonType.ShouldNotEqual && valueFound == false)) - { - return Task.FromResult(new HealthCheckStatus(successMessage) - { - ResultType = StatusResultType.Success, - }.Yield()); - } - - string resultMessage = string.Format(CheckErrorMessage, ItemPath, Values, CurrentValue); - var healthCheckStatus = new HealthCheckStatus(resultMessage) - { - ResultType = StatusResultType.Error, - ReadMoreLink = ReadMoreLink - }; - - return Task.FromResult(healthCheckStatus.Yield()); + return Task.FromResult( + new HealthCheckStatus(successMessage) { ResultType = StatusResultType.Success }.Yield()); } - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new NotSupportedException("Configuration cannot be automatically fixed."); + var resultMessage = string.Format(CheckErrorMessage, ItemPath, Values, CurrentValue); + var healthCheckStatus = new HealthCheckStatus(resultMessage) + { + ResultType = StatusResultType.Error, + ReadMoreLink = ReadMoreLink, + }; + + return Task.FromResult(healthCheckStatus.Yield()); } + + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new NotSupportedException("Configuration cannot be automatically fixed."); } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs index 2ded5a0659..a212a69a3e 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs @@ -1,92 +1,79 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration +namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration; + +/// +/// Health check for the recommended production configuration for Macro Errors. +/// +[HealthCheck( + "D0F7599E-9B2A-4D9E-9883-81C7EDC5616F", + "Macro errors", + Description = "Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.", + Group = "Configuration")] +public class MacroErrorsCheck : AbstractSettingsCheck { + private readonly IOptionsMonitor _contentSettings; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the recommended production configuration for Macro Errors. + /// Initializes a new instance of the class. /// - [HealthCheck( - "D0F7599E-9B2A-4D9E-9883-81C7EDC5616F", - "Macro errors", - Description = "Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.", - Group = "Configuration")] - public class MacroErrorsCheck : AbstractSettingsCheck + public MacroErrorsCheck( + ILocalizedTextService textService, + IOptionsMonitor contentSettings) + : base(textService) { - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _contentSettings; - - /// - /// Initializes a new instance of the class. - /// - public MacroErrorsCheck( - ILocalizedTextService textService, - IOptionsMonitor contentSettings) - : base(textService) - { - _textService = textService; - _contentSettings = contentSettings; - } - - /// - public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.MacroErrorsCheck; - - /// - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - /// - public override string ItemPath => Constants.Configuration.ConfigContentMacroErrors; - - /// - /// Gets the values to compare against. - /// - public override IEnumerable Values - { - get - { - var values = new List - { - new AcceptableConfiguration - { - IsRecommended = true, - Value = MacroErrorBehaviour.Inline.ToString() - }, - new AcceptableConfiguration - { - IsRecommended = false, - Value = MacroErrorBehaviour.Silent.ToString() - } - }; - - return values; - } - } - - /// - public override string CurrentValue => _contentSettings.CurrentValue.MacroErrors.ToString(); - - /// - /// Gets the message for when the check has succeeded. - /// - public override string CheckSuccessMessage => - _textService.Localize( - "healthcheck","macroErrorModeCheckSuccessMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); - - /// - /// Gets the message for when the check has failed. - /// - public override string CheckErrorMessage => - _textService.Localize( - "healthcheck","macroErrorModeCheckErrorMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); + _textService = textService; + _contentSettings = contentSettings; } + + /// + public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.MacroErrorsCheck; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + + /// + public override string ItemPath => Constants.Configuration.ConfigContentMacroErrors; + + /// + /// Gets the values to compare against. + /// + public override IEnumerable Values + { + get + { + var values = new List + { + new() { IsRecommended = true, Value = MacroErrorBehaviour.Inline.ToString() }, + new() { IsRecommended = false, Value = MacroErrorBehaviour.Silent.ToString() }, + }; + + return values; + } + } + + /// + public override string CurrentValue => _contentSettings.CurrentValue.MacroErrors.ToString(); + + /// + /// Gets the message for when the check has succeeded. + /// + public override string CheckSuccessMessage => + _textService.Localize( + "healthcheck", "macroErrorModeCheckSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); + + /// + /// Gets the message for when the check has failed. + /// + public override string CheckErrorMessage => + _textService.Localize( + "healthcheck", "macroErrorModeCheckErrorMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs index 9cb5639205..9629aa8917 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs @@ -1,62 +1,61 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration +namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration; + +/// +/// Health check for the recommended production configuration for Notification Email. +/// +[HealthCheck( + "3E2F7B14-4B41-452B-9A30-E67FBC8E1206", + "Notification Email Settings", + Description = "If notifications are used, the 'from' email address should be specified and changed from the default value.", + Group = "Configuration")] +public class NotificationEmailCheck : AbstractSettingsCheck { + private const string DefaultFromEmail = "your@email.here"; + private readonly IOptionsMonitor _contentSettings; /// - /// Health check for the recommended production configuration for Notification Email. + /// Initializes a new instance of the class. /// - [HealthCheck( - "3E2F7B14-4B41-452B-9A30-E67FBC8E1206", - "Notification Email Settings", - Description = "If notifications are used, the 'from' email address should be specified and changed from the default value.", - Group = "Configuration")] - public class NotificationEmailCheck : AbstractSettingsCheck + public NotificationEmailCheck( + ILocalizedTextService textService, + IOptionsMonitor contentSettings) + : base(textService) => + _contentSettings = contentSettings; + + /// + public override string ItemPath => Constants.Configuration.ConfigContentNotificationsEmail; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldNotEqual; + + /// + public override IEnumerable Values => new List { - private readonly IOptionsMonitor _contentSettings; - private const string DefaultFromEmail = "your@email.here"; + new() { IsRecommended = false, Value = DefaultFromEmail }, new() { IsRecommended = false, Value = string.Empty }, + }; - /// - /// Initializes a new instance of the class. - /// - public NotificationEmailCheck( - ILocalizedTextService textService, - IOptionsMonitor contentSettings) - : base(textService) => - _contentSettings = contentSettings; + /// + public override string CurrentValue => _contentSettings.CurrentValue.Notifications.Email ?? string.Empty; - /// - public override string ItemPath => Constants.Configuration.ConfigContentNotificationsEmail; + /// + public override string CheckSuccessMessage => + LocalizedTextService.Localize("healthcheck", "notificationEmailsCheckSuccessMessage", new[] { CurrentValue ?? "<null>" }); - /// - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldNotEqual; + /// + public override string CheckErrorMessage => LocalizedTextService.Localize( + "healthcheck", + "notificationEmailsCheckErrorMessage", + new[] { DefaultFromEmail }); - /// - public override IEnumerable Values => new List - { - new AcceptableConfiguration { IsRecommended = false, Value = DefaultFromEmail }, - new AcceptableConfiguration { IsRecommended = false, Value = string.Empty } - }; - - /// - public override string CurrentValue => _contentSettings.CurrentValue.Notifications.Email ?? string.Empty; - - /// - public override string CheckSuccessMessage => - LocalizedTextService.Localize("healthcheck","notificationEmailsCheckSuccessMessage", - new[] { CurrentValue ?? "<null>" }); - - /// - public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); - - /// - public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.NotificationEmailCheck; - } + /// + public override string ReadMoreLink => + Constants.HealthChecks.DocumentationLinks.Configuration.NotificationEmailCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs index dda7fb2e6e..4c3936f6cb 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs @@ -1,138 +1,127 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Data +namespace Umbraco.Cms.Core.HealthChecks.Checks.Data; + +/// +/// Health check for the integrity of the data in the database. +/// +[HealthCheck( + "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", + "Database data integrity check", + Description = "Checks for various data integrity issues in the Umbraco database.", + Group = "Data Integrity")] +public class DatabaseIntegrityCheck : HealthCheck { + private const string SSsFixMediaPaths = "fixMediaPaths"; + private const string SFixContentPaths = "fixContentPaths"; + private const string SFixMediaPathsTitle = "Fix media paths"; + private const string SFixContentPathsTitle = "Fix content paths"; + private readonly IContentService _contentService; + private readonly IMediaService _mediaService; + /// - /// Health check for the integrity of the data in the database. + /// Initializes a new instance of the class. /// - [HealthCheck( - "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", - "Database data integrity check", - Description = "Checks for various data integrity issues in the Umbraco database.", - Group = "Data Integrity")] - public class DatabaseIntegrityCheck : HealthCheck + public DatabaseIntegrityCheck( + IContentService contentService, + IMediaService mediaService) { - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; - private const string SSsFixMediaPaths = "fixMediaPaths"; - private const string SFixContentPaths = "fixContentPaths"; - private const string SFixMediaPathsTitle = "Fix media paths"; - private const string SFixContentPathsTitle = "Fix content paths"; + _contentService = contentService; + _mediaService = mediaService; + } - /// - /// Initializes a new instance of the class. - /// - public DatabaseIntegrityCheck( - IContentService contentService, - IMediaService mediaService) + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult((IEnumerable)new[] { CheckDocuments(false), CheckMedia(false) }); + + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) { - _contentService = contentService; - _mediaService = mediaService; - } - - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult((IEnumerable)new[] - { - CheckDocuments(false), - CheckMedia(false) - }); - - private HealthCheckStatus CheckMedia(bool fix) => - CheckPaths( - SSsFixMediaPaths, - SFixMediaPathsTitle, - Constants.UdiEntityType.Media, - fix, - () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); - - private HealthCheckStatus CheckDocuments(bool fix) => - CheckPaths( - SFixContentPaths, - SFixContentPathsTitle, - Constants.UdiEntityType.Document, - fix, - () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); - - private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) - { - ContentDataIntegrityReport report = doCheck(); - - var actions = new List(); - if (!report.Ok) - { - actions.Add(new HealthCheckAction(actionAlias, Id) - { - Name = actionName - }); - } - - return new HealthCheckStatus(GetReport(report, entityType, detailedReport)) - { - ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, - Actions = actions - }; - } - - private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) - { - var sb = new StringBuilder(); - - if (report.Ok) - { - sb.AppendLine($"

All {entityType} paths are valid

"); - - if (!detailed) - { - return sb.ToString(); - } - } - else - { - sb.AppendLine($"

{report.DetectedIssues.Count} invalid {entityType} paths detected.

"); - } - - if (detailed && report.DetectedIssues.Count > 0) - { - sb.AppendLine("
    "); - foreach (IGrouping> issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) - { - var countByGroup = issueGroup.Count(); - var fixedByGroup = issueGroup.Count(x => x.Value.Fixed); - sb.AppendLine("
  • "); - sb.AppendLine($"{countByGroup} issues of type {issueGroup.Key} ... {fixedByGroup} fixed"); - sb.AppendLine("
  • "); - } - - sb.AppendLine("
"); - } - - return sb.ToString(); - } - - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - { - switch (action.Alias) - { - case SFixContentPaths: - return CheckDocuments(true); - case SSsFixMediaPaths: - return CheckMedia(true); - default: - throw new InvalidOperationException("Action not supported"); - } + case SFixContentPaths: + return CheckDocuments(true); + case SSsFixMediaPaths: + return CheckMedia(true); + default: + throw new InvalidOperationException("Action not supported"); } } + + private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) + { + var sb = new StringBuilder(); + + if (report.Ok) + { + sb.AppendLine($"

All {entityType} paths are valid

"); + + if (!detailed) + { + return sb.ToString(); + } + } + else + { + sb.AppendLine($"

{report.DetectedIssues.Count} invalid {entityType} paths detected.

"); + } + + if (detailed && report.DetectedIssues.Count > 0) + { + sb.AppendLine("
    "); + foreach (IGrouping> + issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) + { + var countByGroup = issueGroup.Count(); + var fixedByGroup = issueGroup.Count(x => x.Value.Fixed); + sb.AppendLine("
  • "); + sb.AppendLine($"{countByGroup} issues of type {issueGroup.Key} ... {fixedByGroup} fixed"); + sb.AppendLine("
  • "); + } + + sb.AppendLine("
"); + } + + return sb.ToString(); + } + + private HealthCheckStatus CheckMedia(bool fix) => + CheckPaths( + SSsFixMediaPaths, + SFixMediaPathsTitle, + Constants.UdiEntityType.Media, + fix, + () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); + + private HealthCheckStatus CheckDocuments(bool fix) => + CheckPaths( + SFixContentPaths, + SFixContentPathsTitle, + Constants.UdiEntityType.Document, + fix, + () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); + + private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) + { + ContentDataIntegrityReport report = doCheck(); + + var actions = new List(); + if (!report.Ok) + { + actions.Add(new HealthCheckAction(actionAlias, Id) { Name = actionName }); + } + + return new HealthCheckStatus(GetReport(report, entityType, detailedReport)) + { + ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, + Actions = actions, + }; + } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs index d28c3ca8f5..ee4d9fe788 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs @@ -1,59 +1,56 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment +namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment; + +/// +/// Health check for the configuration of debug-flag. +/// +[HealthCheck( + "61214FF3-FC57-4B31-B5CF-1D095C977D6D", + "Debug Compilation Mode", + Description = "Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.", + Group = "Live Environment")] +public class CompilationDebugCheck : AbstractSettingsCheck { + private readonly IOptionsMonitor _hostingSettings; + /// - /// Health check for the configuration of debug-flag. + /// Initializes a new instance of the class. /// - [HealthCheck( - "61214FF3-FC57-4B31-B5CF-1D095C977D6D", - "Debug Compilation Mode", - Description = "Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.", - Group = "Live Environment")] - public class CompilationDebugCheck : AbstractSettingsCheck + public CompilationDebugCheck(ILocalizedTextService textService, IOptionsMonitor hostingSettings) + : base(textService) => + _hostingSettings = hostingSettings; + + /// + public override string ItemPath => Constants.Configuration.ConfigHostingDebug; + + /// + public override string ReadMoreLink => + Constants.HealthChecks.DocumentationLinks.LiveEnvironment.CompilationDebugCheck; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + + /// + public override IEnumerable Values => new List { - private readonly IOptionsMonitor _hostingSettings; + new() { IsRecommended = true, Value = bool.FalseString.ToLower() }, + }; - /// - /// Initializes a new instance of the class. - /// - public CompilationDebugCheck(ILocalizedTextService textService, IOptionsMonitor hostingSettings) - : base(textService) => - _hostingSettings = hostingSettings; + /// + public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString(); - /// - public override string ItemPath => Constants.Configuration.ConfigHostingDebug; + /// + public override string CheckSuccessMessage => + LocalizedTextService.Localize("healthcheck", "compilationDebugCheckSuccessMessage"); - /// - public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.LiveEnvironment.CompilationDebugCheck; - - /// - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - /// - public override IEnumerable Values => new List - { - new AcceptableConfiguration - { - IsRecommended = true, - Value = bool.FalseString.ToLower() - } - }; - - /// - public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString(); - - /// - public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckSuccessMessage"); - - /// - public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckErrorMessage"); - } + /// + public override string CheckErrorMessage => + LocalizedTextService.Localize("healthcheck", "compilationDebugCheckErrorMessage"); } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs index d10dc8fedd..13a45c169c 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs @@ -1,102 +1,100 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Permissions +namespace Umbraco.Cms.Core.HealthChecks.Checks.Permissions; + +/// +/// Health check for the folder and file permissions. +/// +[HealthCheck( + "53DBA282-4A79-4B67-B958-B29EC40FCC23", + "Folder & File Permissions", + Description = "Checks that the web server folder and file permissions are set correctly for Umbraco to run.", + Group = "Permissions")] +public class FolderAndFilePermissionsCheck : HealthCheck { + private readonly IFilePermissionHelper _filePermissionHelper; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the folder and file permissions. + /// Initializes a new instance of the class. /// - [HealthCheck( - "53DBA282-4A79-4B67-B958-B29EC40FCC23", - "Folder & File Permissions", - Description = "Checks that the web server folder and file permissions are set correctly for Umbraco to run.", - Group = "Permissions")] - public class FolderAndFilePermissionsCheck : HealthCheck + public FolderAndFilePermissionsCheck( + ILocalizedTextService textService, + IFilePermissionHelper filePermissionHelper) { - private readonly ILocalizedTextService _textService; - private readonly IFilePermissionHelper _filePermissionHelper; + _textService = textService; + _filePermissionHelper = filePermissionHelper; + } - /// - /// Initializes a new instance of the class. - /// - public FolderAndFilePermissionsCheck( - ILocalizedTextService textService, - IFilePermissionHelper filePermissionHelper) + /// + /// Get the status for this health check + /// + public override Task> GetStatus() + { + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> errors); + + return Task.FromResult(errors.Select(x => new HealthCheckStatus(GetMessage(x)) { - _textService = textService; - _filePermissionHelper = filePermissionHelper; + ResultType = x.Value.Any() ? StatusResultType.Error : StatusResultType.Success, + ReadMoreLink = GetReadMoreLink(x), + Description = GetErrorDescription(x), + })); + } + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => + throw new InvalidOperationException("FolderAndFilePermissionsCheck has no executable actions"); + + private string? GetErrorDescription(KeyValuePair> status) + { + if (!status.Value.Any()) + { + return null; } - /// - /// Get the status for this health check - /// - public override Task> GetStatus() - { - _filePermissionHelper.RunFilePermissionTestSuite(out Dictionary> errors); + var sb = new StringBuilder("The following failed:"); - return Task.FromResult(errors.Select(x => new HealthCheckStatus(GetMessage(x)) - { - ResultType = x.Value.Any() ? StatusResultType.Error : StatusResultType.Success, - ReadMoreLink = GetReadMoreLink(x), - Description = GetErrorDescription(x) - })); + sb.AppendLine("
    "); + foreach (var error in status.Value) + { + sb.Append("
  • " + error + "
  • "); } - private string? GetErrorDescription(KeyValuePair> status) + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GetMessage(KeyValuePair> status) + => _textService.Localize("permissions", status.Key); + + private string? GetReadMoreLink(KeyValuePair> status) + { + if (!status.Value.Any()) { - if (!status.Value.Any()) - { + return null; + } + + switch (status.Key) + { + case FilePermissionTest.FileWriting: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWriting; + case FilePermissionTest.FolderCreation: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FolderCreation; + case FilePermissionTest.FileWritingForPackages: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWritingForPackages; + case FilePermissionTest.MediaFolderCreation: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.MediaFolderCreation; + default: return null; - } - - var sb = new StringBuilder("The following failed:"); - - sb.AppendLine("
    "); - foreach (var error in status.Value) - { - sb.Append("
  • " + error + "
  • "); - } - - sb.AppendLine("
"); - return sb.ToString(); } - - private string GetMessage(KeyValuePair> status) - => _textService.Localize("permissions", status.Key); - - private string? GetReadMoreLink(KeyValuePair> status) - { - if (!status.Value.Any()) - { - return null; - } - - switch (status.Key) - { - case FilePermissionTest.FileWriting: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWriting; - case FilePermissionTest.FolderCreation: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FolderCreation; - case FilePermissionTest.FileWritingForPackages: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWritingForPackages; - case FilePermissionTest.MediaFolderCreation: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.MediaFolderCreation; - default: return null; - } - } - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("FolderAndFilePermissionsCheck has no executable actions"); } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs b/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs index d99f05d738..041ace503f 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs @@ -1,12 +1,11 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.HealthChecks.Checks +namespace Umbraco.Cms.Core.HealthChecks.Checks; + +public enum ProvidedValueValidation { - public enum ProvidedValueValidation - { - None = 1, - Email = 2, - Regex = 3 - } + None = 1, + Email = 2, + Regex = 3, } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs index d107e85385..9a0ecf57ae 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs @@ -1,136 +1,131 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Provides a base class for health checks of http header values. +/// +public abstract class BaseHttpHeaderCheck : HealthCheck { + private static HttpClient? httpClient; + private readonly string _header; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly string _localizedTextPrefix; + private readonly bool _metaTagOptionAvailable; + /// - /// Provides a base class for health checks of http header values. + /// Initializes a new instance of the class. /// - public abstract class BaseHttpHeaderCheck : HealthCheck + protected BaseHttpHeaderCheck( + IHostingEnvironment hostingEnvironment, + ILocalizedTextService textService, + string header, + string localizedTextPrefix, + bool metaTagOptionAvailable) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILocalizedTextService _textService; - private readonly string _header; - private readonly string _localizedTextPrefix; - private readonly bool _metaTagOptionAvailable; - private static HttpClient? s_httpClient; + LocalizedTextService = textService ?? throw new ArgumentNullException(nameof(textService)); + _hostingEnvironment = hostingEnvironment; + _header = header; + _localizedTextPrefix = localizedTextPrefix; + _metaTagOptionAvailable = metaTagOptionAvailable; + } - /// - /// Initializes a new instance of the class. - /// - protected BaseHttpHeaderCheck( - IHostingEnvironment hostingEnvironment, - ILocalizedTextService textService, - string header, - string localizedTextPrefix, - bool metaTagOptionAvailable) + [Obsolete("Save ILocalizedTextService in a field on the super class instead of using this")] + protected ILocalizedTextService LocalizedTextService { get; } + + /// + /// Gets a link to an external read more page. + /// + protected abstract string ReadMoreLink { get; } + + private static HttpClient HttpClient => httpClient ??= new HttpClient(); + + /// + /// Get the status for this health check + /// + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeader()); + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException( + "HTTP Header action requested is either not executable or does not exist"); + + /// + /// The actual health check method. + /// + protected async Task CheckForHeader() + { + string message; + var success = false; + + // Access the site home page and check for the click-jack protection header or meta tag + var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); + + try { - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _hostingEnvironment = hostingEnvironment; - _header = header; - _localizedTextPrefix = localizedTextPrefix; - _metaTagOptionAvailable = metaTagOptionAvailable; - } + using HttpResponseMessage response = await HttpClient.GetAsync(url); - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(); + // Check first for header + success = HasMatchingHeader(response.Headers.Select(x => x.Key)); - /// - /// Gets a link to an external read more page. - /// - protected abstract string ReadMoreLink { get; } - - /// - /// Get the status for this health check - /// - public override async Task> GetStatus() => - await Task.WhenAll(CheckForHeader()); - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist"); - - /// - /// The actual health check method. - /// - protected async Task CheckForHeader() - { - string message; - var success = false; - - // Access the site home page and check for the click-jack protection header or meta tag - var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); - - try + // If not found, and available, check for meta-tag + if (success == false && _metaTagOptionAvailable) { - using HttpResponseMessage response = await HttpClient.GetAsync(url); - - // Check first for header - success = HasMatchingHeader(response.Headers.Select(x => x.Key)); - - // If not found, and available, check for meta-tag - if (success == false && _metaTagOptionAvailable) - { - success = await DoMetaTagsContainKeyForHeader(response); - } - - message = success - ? _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") - : _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); - } - catch (Exception ex) - { - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url?.ToString(), ex.Message }); + success = await DoMetaTagsContainKeyForHeader(response); } - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : ReadMoreLink - }; + message = success + ? LocalizedTextService.Localize("healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") + : LocalizedTextService.Localize("healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); + } + catch (Exception ex) + { + message = LocalizedTextService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url, ex.Message }); } - private bool HasMatchingHeader(IEnumerable headerKeys) - => headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); - - private async Task DoMetaTagsContainKeyForHeader(HttpResponseMessage response) - { - using (Stream stream = await response.Content.ReadAsStreamAsync()) + return + new HealthCheckStatus(message) { - if (stream == null) - { - return false; - } + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success ? null : ReadMoreLink, + }; + } - using (var reader = new StreamReader(stream)) - { - var html = reader.ReadToEnd(); - Dictionary metaTags = ParseMetaTags(html); - return HasMatchingHeader(metaTags.Keys); - } - } - } + private static Dictionary ParseMetaTags(string html) + { + var regex = new Regex(" ParseMetaTags(string html) + return regex.Matches(html) + .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); + } + + private bool HasMatchingHeader(IEnumerable headerKeys) + => headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); + + private async Task DoMetaTagsContainKeyForHeader(HttpResponseMessage response) + { + using (Stream stream = await response.Content.ReadAsStreamAsync()) { - var regex = new Regex("() - .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); + using (var reader = new StreamReader(stream)) + { + var html = reader.ReadToEnd(); + Dictionary metaTags = ParseMetaTags(html); + return HasMatchingHeader(metaTags.Keys); + } } } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs index 8586989f32..2d15e49e6a 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs @@ -4,27 +4,26 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the X-Frame-Options header. +/// +[HealthCheck( + "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0", + "Click-Jacking Protection", + Description = "Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.", + Group = "Security")] +public class ClickJackingCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the X-Frame-Options header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0", - "Click-Jacking Protection", - Description = "Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.", - Group = "Security")] - public class ClickJackingCheck : BaseHttpHeaderCheck + public ClickJackingCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-Frame-Options", "clickJacking", true) { - /// - /// Initializes a new instance of the class. - /// - public ClickJackingCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "X-Frame-Options", "clickJacking", true) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.ClickJackingCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.ClickJackingCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs index 99729286c5..e211d7c257 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs @@ -1,94 +1,93 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding unnecessary headers. +/// +[HealthCheck( + "92ABBAA2-0586-4089-8AE2-9A843439D577", + "Excessive Headers", + Description = "Checks to see if your site is revealing information in its headers that gives away unnecessary details about the technology used to build and host it.", + Group = "Security")] +public class ExcessiveHeadersCheck : HealthCheck { + private static HttpClient? httpClient; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the recommended production setup regarding unnecessary headers. + /// Initializes a new instance of the class. /// - [HealthCheck( - "92ABBAA2-0586-4089-8AE2-9A843439D577", - "Excessive Headers", - Description = "Checks to see if your site is revealing information in its headers that gives away unnecessary details about the technology used to build and host it.", - Group = "Security")] - public class ExcessiveHeadersCheck : HealthCheck + public ExcessiveHeadersCheck(ILocalizedTextService textService, IHostingEnvironment hostingEnvironment) { - private readonly ILocalizedTextService _textService; - private readonly IHostingEnvironment _hostingEnvironment; - private static HttpClient? s_httpClient; + _textService = textService; + _hostingEnvironment = hostingEnvironment; + } - /// - /// Initializes a new instance of the class. - /// - public ExcessiveHeadersCheck(ILocalizedTextService textService, IHostingEnvironment hostingEnvironment) + private static HttpClient HttpClient => httpClient ??= new HttpClient(); + + /// + /// Get the status for this health check + /// + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeaders()); + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException("ExcessiveHeadersCheck has no executable actions"); + + private async Task CheckForHeaders() + { + string message; + var success = false; + var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); + + // Access the site home page and check for the headers + var request = new HttpRequestMessage(HttpMethod.Head, url); + try { - _textService = textService; - _hostingEnvironment = hostingEnvironment; - } + using HttpResponseMessage response = await HttpClient.SendAsync(request); - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(); + IEnumerable allHeaders = response.Headers.Select(x => x.Key); + var headersToCheckFor = + new List { "Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version" }; - /// - /// Get the status for this health check - /// - public override async Task> GetStatus() => - await Task.WhenAll(CheckForHeaders()); - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("ExcessiveHeadersCheck has no executable actions"); - - private async Task CheckForHeaders() - { - string message; - var success = false; - var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); - - // Access the site home page and check for the headers - var request = new HttpRequestMessage(HttpMethod.Head, url); - try + // Ignore if server header is present and it's set to cloudflare + if (allHeaders.InvariantContains("Server") && + response.Headers.TryGetValues("Server", out IEnumerable? serverHeaders) && + (serverHeaders.FirstOrDefault()?.InvariantEquals("cloudflare") ?? false)) { - using HttpResponseMessage response = await HttpClient.SendAsync(request); - - IEnumerable allHeaders = response.Headers.Select(x => x.Key); - var headersToCheckFor = new List {"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version" }; - - // Ignore if server header is present and it's set to cloudflare - if (allHeaders.InvariantContains("Server") && response.Headers.TryGetValues("Server", out var serverHeaders) && (serverHeaders.FirstOrDefault()?.InvariantEquals("cloudflare") ?? false)) - { - headersToCheckFor.Remove("Server"); - } - - var headersFound = allHeaders - .Intersect(headersToCheckFor) - .ToArray(); - success = headersFound.Any() == false; - message = success - ? _textService.Localize("healthcheck","excessiveHeadersNotFound") - : _textService.Localize("healthcheck","excessiveHeadersFound", new[] { string.Join(", ", headersFound) }); - } - catch (Exception ex) - { - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url?.ToString(), ex.Message }); + headersToCheckFor.Remove("Server"); } - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Warning, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.ExcessiveHeadersCheck, - }; + var headersFound = allHeaders + .Intersect(headersToCheckFor) + .ToArray(); + success = headersFound.Any() == false; + message = success + ? _textService.Localize("healthcheck", "excessiveHeadersNotFound") + : _textService.Localize("healthcheck", "excessiveHeadersFound", new[] { string.Join(", ", headersFound) }); } - } + catch (Exception ex) + { + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url, ex.Message }); + } + + return + new HealthCheckStatus(message) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Warning, + ReadMoreLink = success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.ExcessiveHeadersCheck, + }; + } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs index 7902f4e3f8..229999472e 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs @@ -4,34 +4,33 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the Strict-Transport-Security header. +/// +[HealthCheck( + "E2048C48-21C5-4BE1-A80B-8062162DF124", + "Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))", + Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS).", + Group = "Security")] +public class HstsCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the Strict-Transport-Security header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "E2048C48-21C5-4BE1-A80B-8062162DF124", - "Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))", - Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS).", - Group = "Security")] - public class HstsCheck : BaseHttpHeaderCheck + /// + /// The check is mostly based on the instructions in the OWASP CheatSheet + /// (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.md) + /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) + /// If you want do to it perfectly, you have to submit it https://hstspreload.org/, + /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. + /// + public HstsCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "Strict-Transport-Security", "hSTS", true) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The check is mostly based on the instructions in the OWASP CheatSheet - /// (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.md) - /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) - /// If you want do to it perfectly, you have to submit it https://hstspreload.org/, - /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. - /// - public HstsCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "Strict-Transport-Security", "hSTS", true) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.HstsCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.HstsCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs index 0b58ca4b40..dbff50c480 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs @@ -1,193 +1,196 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health checks for the recommended production setup regarding HTTPS. +/// +[HealthCheck( + "EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7", + "HTTPS Configuration", + Description = "Checks if your site is configured to work over HTTPS and if the Umbraco related configuration for that is correct.", + Group = "Security")] +public class HttpsCheck : HealthCheck { + private const int NumberOfDaysForExpiryWarning = 14; + private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry"; + + private static HttpClient? _httpClient; + private readonly IOptionsMonitor _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + + private readonly ILocalizedTextService _textService; + /// - /// Health checks for the recommended production setup regarding HTTPS. + /// Initializes a new instance of the class. /// - [HealthCheck( - "EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7", - "HTTPS Configuration", - Description = "Checks if your site is configured to work over HTTPS and if the Umbraco related configuration for that is correct.", - Group = "Security")] - public class HttpsCheck : HealthCheck + /// The text service. + /// The global settings. + /// The hosting environment. + public HttpsCheck( + ILocalizedTextService textService, + IOptionsMonitor globalSettings, + IHostingEnvironment hostingEnvironment) { - private const int NumberOfDaysForExpiryWarning = 14; - private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry"; + _textService = textService; + _globalSettings = globalSettings; + _hostingEnvironment = hostingEnvironment; + } - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; + private static HttpClient _httpClientEnsureInitialized => _httpClient ??= new HttpClient(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation, + }); - private static HttpClient? s_httpClient; + /// + public override async Task> GetStatus() => + await Task.WhenAll( + CheckIfCurrentSchemeIsHttps(), + CheckHttpsConfigurationSetting(), + CheckForValidCertificate()); - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(new HttpClientHandler() + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException( + "HttpsCheck action requested is either not executable or does not exist"); + + private static bool ServerCertificateCustomValidation( + HttpRequestMessage requestMessage, + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslErrors) + { + if (certificate is not null) { - ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation - }); - - /// - /// Initializes a new instance of the class. - /// - /// The text service. - /// The global settings. - /// The hosting environment. - public HttpsCheck( - ILocalizedTextService textService, - IOptionsMonitor globalSettings, - IHostingEnvironment hostingEnvironment) - { - _textService = textService; - _globalSettings = globalSettings; - _hostingEnvironment = hostingEnvironment; + requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = + (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); } - /// - public override async Task> GetStatus() => - await Task.WhenAll( - CheckIfCurrentSchemeIsHttps(), - CheckHttpsConfigurationSetting(), - CheckForValidCertificate()); + return sslErrors == SslPolicyErrors.None; + } - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist"); + private async Task CheckForValidCertificate() + { + string message; + StatusResultType result; - private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2? certificate, X509Chain? chain, SslPolicyErrors sslErrors) + // Attempt to access the site over HTTPS to see if it HTTPS is supported and a valid certificate has been configured + var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl) { Scheme = Uri.UriSchemeHttps }; + Uri url = urlBuilder.Uri; + + var request = new HttpRequestMessage(HttpMethod.Head, url); + + try { - if (certificate is not null) + using HttpResponseMessage response = await _httpClientEnsureInitialized.SendAsync(request); + + if (response.StatusCode == HttpStatusCode.OK) { - requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); - } - - return sslErrors == SslPolicyErrors.None; - } - - private async Task CheckForValidCertificate() - { - string message; - StatusResultType result; - - // Attempt to access the site over HTTPS to see if it HTTPS is supported and a valid certificate has been configured - var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl) - { - Scheme = Uri.UriSchemeHttps - }; - var url = urlBuilder.Uri; - - var request = new HttpRequestMessage(HttpMethod.Head, url); - - try - { - using HttpResponseMessage response = await HttpClient.SendAsync(request); - - if (response.StatusCode == HttpStatusCode.OK) + // Got a valid response, check now if the certificate is expiring within the specified amount of days + int? daysToExpiry = 0; + if (request.Properties.TryGetValue( + HttpPropertyKeyCertificateDaysToExpiry, + out var certificateDaysToExpiry)) { - // Got a valid response, check now if the certificate is expiring within the specified amount of days - int? daysToExpiry = 0; - if (request.Properties.TryGetValue(HttpPropertyKeyCertificateDaysToExpiry, out var certificateDaysToExpiry)) - { - daysToExpiry = (int?)certificateDaysToExpiry; - } - - if (daysToExpiry <= 0) - { - result = StatusResultType.Error; - message = _textService.Localize("healthcheck","httpsCheckExpiredCertificate"); - } - else if (daysToExpiry < NumberOfDaysForExpiryWarning) - { - result = StatusResultType.Warning; - message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); - } - else - { - result = StatusResultType.Success; - message = _textService.Localize("healthcheck","httpsCheckValidCertificate"); - } + daysToExpiry = (int?)certificateDaysToExpiry; } - else + + if (daysToExpiry <= 0) { result = StatusResultType.Error; - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase }); + message = _textService.Localize("healthcheck", "httpsCheckExpiredCertificate"); } - } - catch (Exception ex) - { - if (ex is WebException exception) + else if (daysToExpiry < NumberOfDaysForExpiryWarning) { - message = exception.Status == WebExceptionStatus.TrustFailure - ? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message }) - : _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message }); + result = StatusResultType.Warning; + message = _textService.Localize("healthcheck", "httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); } else { - message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message }); + result = StatusResultType.Success; + message = _textService.Localize("healthcheck", "httpsCheckValidCertificate"); } - - result = StatusResultType.Error; - } - - return new HealthCheckStatus(message) - { - ResultType = result, - ReadMoreLink = result == StatusResultType.Success - ? null - : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps - }; - } - - private Task CheckIfCurrentSchemeIsHttps() - { - Uri uri = _hostingEnvironment.ApplicationMainUrl; - var success = uri.Scheme == Uri.UriSchemeHttps; - - return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck","httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps - }); - } - - private Task CheckHttpsConfigurationSetting() - { - bool httpsSettingEnabled = _globalSettings.CurrentValue.UseHttps; - Uri uri = _hostingEnvironment.ApplicationMainUrl; - - string resultMessage; - StatusResultType resultType; - if (uri.Scheme != Uri.UriSchemeHttps) - { - resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible"); - resultType = StatusResultType.Info; } else { - resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); - resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; + result = StatusResultType.Error; + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase }); + } + } + catch (Exception ex) + { + if (ex is WebException exception) + { + message = exception.Status == WebExceptionStatus.TrustFailure + ? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message }) + : _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message }); + } + else + { + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message }); } - return Task.FromResult(new HealthCheckStatus(resultMessage) - { - ResultType = resultType, - ReadMoreLink = resultType == StatusResultType.Success - ? null - : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckHttpsConfigurationSetting - }); + result = StatusResultType.Error; } + + return new HealthCheckStatus(message) + { + ResultType = result, + ReadMoreLink = result == StatusResultType.Success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps, + }; + } + + private Task CheckIfCurrentSchemeIsHttps() + { + Uri uri = _hostingEnvironment.ApplicationMainUrl; + var success = uri.Scheme == Uri.UriSchemeHttps; + + return Task.FromResult( + new HealthCheckStatus(_textService.Localize("healthcheck", "httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps, + }); + } + + private Task CheckHttpsConfigurationSetting() + { + var httpsSettingEnabled = _globalSettings.CurrentValue.UseHttps; + Uri uri = _hostingEnvironment.ApplicationMainUrl; + + string resultMessage; + StatusResultType resultType; + if (uri.Scheme != Uri.UriSchemeHttps) + { + resultMessage = _textService.Localize("healthcheck", "httpsCheckConfigurationRectifyNotPossible"); + resultType = StatusResultType.Info; + } + else + { + resultMessage = _textService.Localize("healthcheck", "httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); + resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; + } + + return Task.FromResult(new HealthCheckStatus(resultMessage) + { + ResultType = resultType, + ReadMoreLink = resultType == StatusResultType.Success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckHttpsConfigurationSetting, + }); } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs index 78ee2c0e12..b36201d5aa 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs @@ -4,27 +4,26 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the X-Content-Type-Options header. +/// +[HealthCheck( + "1CF27DB3-EFC0-41D7-A1BB-EA912064E071", + "Content/MIME Sniffing Protection", + Description = "Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.", + Group = "Security")] +public class NoSniffCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the X-Content-Type-Options header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "1CF27DB3-EFC0-41D7-A1BB-EA912064E071", - "Content/MIME Sniffing Protection", - Description = "Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.", - Group = "Security")] - public class NoSniffCheck : BaseHttpHeaderCheck + public NoSniffCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-Content-Type-Options", "noSniff", false) { - /// - /// Initializes a new instance of the class. - /// - public NoSniffCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "X-Content-Type-Options", "noSniff", false) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.NoSniffCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.NoSniffCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs index 44b10ba0e3..55406b9c0a 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs @@ -1,68 +1,69 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +[HealthCheck( + "6708CA45-E96E-40B8-A40A-0607C1CA7F28", + "Application URL Configuration", + Description = "Checks if the Umbraco application URL is configured for your site.", + Group = "Security")] +public class UmbracoApplicationUrlCheck : HealthCheck { - [HealthCheck( - "6708CA45-E96E-40B8-A40A-0607C1CA7F28", - "Application URL Configuration", - Description = "Checks if the Umbraco application URL is configured for your site.", - Group = "Security")] - public class UmbracoApplicationUrlCheck : HealthCheck + private readonly ILocalizedTextService _textService; + private readonly IOptionsMonitor _webRoutingSettings; + + public UmbracoApplicationUrlCheck( + ILocalizedTextService textService, + IOptionsMonitor webRoutingSettings) { - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _webRoutingSettings; + _textService = textService; + _webRoutingSettings = webRoutingSettings; + } - public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IOptionsMonitor webRoutingSettings) + /// + /// Executes the action and returns its status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => + throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); + + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckUmbracoApplicationUrl().Yield()); + + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; + + string resultMessage; + StatusResultType resultType; + var success = false; + + if (url.IsNullOrWhiteSpace()) { - _textService = textService; - _webRoutingSettings = webRoutingSettings; + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); + resultType = StatusResultType.Warning; + } + else + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); + resultType = StatusResultType.Success; + success = true; } - /// - /// Executes the action and returns its status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); - - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult(CheckUmbracoApplicationUrl().Yield()); - - private HealthCheckStatus CheckUmbracoApplicationUrl() + return new HealthCheckStatus(resultMessage) { - var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; - - string resultMessage; - StatusResultType resultType; - var success = false; - - if (url.IsNullOrWhiteSpace()) - { - resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); - resultType = StatusResultType.Warning; - } - else - { - resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); - resultType = StatusResultType.Success; - success = true; - } - - return new HealthCheckStatus(resultMessage) - { - ResultType = resultType, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck - }; - } + ResultType = resultType, + ReadMoreLink = success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck, + }; } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs index 570ca8002d..ca988fe45a 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs @@ -4,34 +4,33 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the X-XSS-Protection header. +/// +[HealthCheck( + "F4D2B02E-28C5-4999-8463-05759FA15C3A", + "Cross-site scripting Protection (X-XSS-Protection header)", + Description = "This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.", + Group = "Security")] +public class XssProtectionCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the X-XSS-Protection header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "F4D2B02E-28C5-4999-8463-05759FA15C3A", - "Cross-site scripting Protection (X-XSS-Protection header)", - Description = "This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.", - Group = "Security")] - public class XssProtectionCheck : BaseHttpHeaderCheck + /// + /// The check is mostly based on the instructions in the OWASP CheatSheet + /// (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) + /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) + /// If you want do to it perfectly, you have to submit it https://hstspreload.appspot.com/, + /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. + /// + public XssProtectionCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-XSS-Protection", "xssProtection", true) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The check is mostly based on the instructions in the OWASP CheatSheet - /// (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) - /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) - /// If you want do to it perfectly, you have to submit it https://hstspreload.appspot.com/, - /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. - /// - public XssProtectionCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "X-XSS-Protection", "xssProtection", true) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.XssProtectionCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.XssProtectionCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs index 302a5829f6..6119f4c715 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs @@ -1,112 +1,106 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.IO; using System.Net.Sockets; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Services +namespace Umbraco.Cms.Core.HealthChecks.Checks.Services; + +/// +/// Health check for the recommended setup regarding SMTP. +/// +[HealthCheck( + "1B5D221B-CE99-4193-97CB-5F3261EC73DF", + "SMTP Settings", + Description = "Checks that valid settings for sending emails are in place.", + Group = "Services")] +public class SmtpCheck : HealthCheck { + private readonly IOptionsMonitor _globalSettings; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the recommended setup regarding SMTP. + /// Initializes a new instance of the class. /// - [HealthCheck( - "1B5D221B-CE99-4193-97CB-5F3261EC73DF", - "SMTP Settings", - Description = "Checks that valid settings for sending emails are in place.", - Group = "Services")] - public class SmtpCheck : HealthCheck + public SmtpCheck(ILocalizedTextService textService, IOptionsMonitor globalSettings) { - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _globalSettings; + _textService = textService; + _globalSettings = globalSettings; + } - /// - /// Initializes a new instance of the class. - /// - public SmtpCheck(ILocalizedTextService textService, IOptionsMonitor globalSettings) + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckSmtpSettings().Yield()); + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException("SmtpCheck has no executable actions"); + + private static bool CanMakeSmtpConnection(string host, int port) + { + try { - _textService = textService; - _globalSettings = globalSettings; - } - - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult(CheckSmtpSettings().Yield()); - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("SmtpCheck has no executable actions"); - - private HealthCheckStatus CheckSmtpSettings() - { - var success = false; - - SmtpSettings? smtpSettings = _globalSettings.CurrentValue.Smtp; - - string message; - if (smtpSettings == null) + using (var client = new TcpClient()) { - message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound"); - } - else - { - if (string.IsNullOrEmpty(smtpSettings.Host)) + client.Connect(host, port); + using (NetworkStream stream = client.GetStream()) { - message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); - } - else - { - success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port); - message = success - ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess") - : _textService.Localize( - "healthcheck", "smtpMailSettingsConnectionFail", - new[] { smtpSettings.Host, smtpSettings.Port.ToString() }); - } - } - - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.SmtpCheck - }; - } - - private static bool CanMakeSmtpConnection(string host, int port) - { - try - { - using (var client = new TcpClient()) - { - client.Connect(host, port); - using (NetworkStream stream = client.GetStream()) + using (var writer = new StreamWriter(stream)) + using (var reader = new StreamReader(stream)) { - using (var writer = new StreamWriter(stream)) - using (var reader = new StreamReader(stream)) - { - writer.WriteLine("EHLO " + host); - writer.Flush(); - reader.ReadLine(); - return true; - } + writer.WriteLine("EHLO " + host); + writer.Flush(); + reader.ReadLine(); + return true; } } } - catch - { - return false; - } + } + catch + { + return false; } } + + private HealthCheckStatus CheckSmtpSettings() + { + var success = false; + + SmtpSettings? smtpSettings = _globalSettings.CurrentValue.Smtp; + + string message; + if (smtpSettings == null) + { + message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound"); + } + else + { + if (string.IsNullOrEmpty(smtpSettings.Host)) + { + message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); + } + else + { + success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port); + message = success + ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess") + : _textService.Localize( + "healthcheck", "smtpMailSettingsConnectionFail", new[] { smtpSettings.Host, smtpSettings.Port.ToString() }); + } + } + + return + new HealthCheckStatus(message) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.SmtpCheck, + }; + } } diff --git a/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs b/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs index a5d3ae82da..564bcc59a5 100644 --- a/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs +++ b/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class ConfigurationServiceResult { - public class ConfigurationServiceResult - { - public bool Success { get; set; } - public string? Result { get; set; } - } + public bool Success { get; set; } + + public string? Result { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheck.cs b/src/Umbraco.Core/HealthChecks/HealthCheck.cs index 59d6f912fa..06a1bd27f3 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheck.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheck.cs @@ -1,60 +1,57 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -using System.Threading.Tasks; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks -{ - /// - /// Provides a base class for health checks, filling in the healthcheck metadata on construction - /// - [DataContract(Name = "healthCheck", Namespace = "")] - public abstract class HealthCheck : IDiscoverable - { - protected HealthCheck() - { - var thisType = GetType(); - var meta = thisType.GetCustomAttribute(false); - if (meta == null) - { - throw new InvalidOperationException($"The health check {thisType} requires a {typeof(HealthCheckAttribute)}"); - } +namespace Umbraco.Cms.Core.HealthChecks; - Name = meta.Name; - Description = meta.Description; - Group = meta.Group; - Id = meta.Id; +/// +/// Provides a base class for health checks, filling in the healthcheck metadata on construction +/// +[DataContract(Name = "healthCheck", Namespace = "")] +public abstract class HealthCheck : IDiscoverable +{ + protected HealthCheck() + { + Type thisType = GetType(); + HealthCheckAttribute? meta = thisType.GetCustomAttribute(false); + if (meta == null) + { + throw new InvalidOperationException( + $"The health check {thisType} requires a {typeof(HealthCheckAttribute)}"); } - [DataMember(Name = "id")] - public Guid Id { get; private set; } - - [DataMember(Name = "name")] - public string Name { get; private set; } - - [DataMember(Name = "description")] - public string? Description { get; private set; } - - [DataMember(Name = "group")] - public string? Group { get; private set; } - - /// - /// Get the status for this health check - /// - /// - /// - /// If there are possible actions to take to rectify this check, this method must be overridden by a sub class - /// in order to explicitly provide those actions. - /// - public abstract Task> GetStatus(); - - /// - /// Executes the action and returns it's status - /// - /// - /// - public abstract HealthCheckStatus ExecuteAction(HealthCheckAction action); + Name = meta.Name; + Description = meta.Description; + Group = meta.Group; + Id = meta.Id; } + + [DataMember(Name = "id")] + public Guid Id { get; private set; } + + [DataMember(Name = "name")] + public string Name { get; private set; } + + [DataMember(Name = "description")] + public string? Description { get; private set; } + + [DataMember(Name = "group")] + public string? Group { get; private set; } + + /// + /// Get the status for this health check + /// + /// + /// + /// If there are possible actions to take to rectify this check, this method must be overridden by a sub class + /// in order to explicitly provide those actions. + /// + public abstract Task> GetStatus(); + + /// + /// Executes the action and returns it's status + /// + /// + /// + public abstract HealthCheckStatus ExecuteAction(HealthCheckAction action); } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs b/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs index 06bc05f44a..7593a54cc2 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs @@ -1,89 +1,89 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +[DataContract(Name = "healthCheckAction", Namespace = "")] +public class HealthCheckAction { - [DataContract(Name = "healthCheckAction", Namespace = "")] - public class HealthCheckAction + /// + /// The name of the action - this is used to name the fix button + /// + [DataMember(Name = "name")] + private string? _name; + + /// + /// Empty ctor used for serialization + /// + public HealthCheckAction() { - /// - /// Empty ctor used for serialization - /// - public HealthCheckAction() { } - - /// - /// Default ctor - /// - /// - /// - public HealthCheckAction(string alias, Guid healthCheckId) - { - Alias = alias; - HealthCheckId = healthCheckId; - } - - /// - /// The alias of the action - this is used by the Health Check instance to execute the action - /// - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - /// - /// The Id of the Health Check instance - /// - /// - /// This is used to find the Health Check instance to execute this action - /// - [DataMember(Name = "healthCheckId")] - public Guid? HealthCheckId { get; set; } - - /// - /// This could be used if the status has a custom view that specifies some parameters to be sent to the server - /// when an action needs to be executed - /// - [DataMember(Name = "actionParameters")] - public Dictionary? ActionParameters { get; set; } - - /// - /// The name of the action - this is used to name the fix button - /// - [DataMember(Name = "name")] - private string? _name; - public string? Name - { - get { return _name; } - set { _name = value; } - } - - /// - /// The description of the action - this is used to give a description before executing the action - /// - [DataMember(Name = "description")] - public string? Description { get; set; } - - /// - /// Indicates if a value is required to rectify the issue - /// - [DataMember(Name = "valueRequired")] - public bool ValueRequired { get; set; } - - /// - /// Indicates if a value required, how it is validated - /// - [DataMember(Name = "providedValueValidation")] - public string? ProvidedValueValidation { get; set; } - - /// - /// Indicates if a value required, and is validated by a regex, what the regex to use is - /// - [DataMember(Name = "providedValueValidationRegex")] - public string? ProvidedValueValidationRegex { get; set; } - - /// - /// Provides a value to rectify the issue - /// - [DataMember(Name = "providedValue")] - public string? ProvidedValue { get; set; } } + + /// + /// Default ctor + /// + /// + /// + public HealthCheckAction(string alias, Guid healthCheckId) + { + Alias = alias; + HealthCheckId = healthCheckId; + } + + /// + /// The alias of the action - this is used by the Health Check instance to execute the action + /// + [DataMember(Name = "alias")] + public string? Alias { get; set; } + + /// + /// The Id of the Health Check instance + /// + /// + /// This is used to find the Health Check instance to execute this action + /// + [DataMember(Name = "healthCheckId")] + public Guid? HealthCheckId { get; set; } + + /// + /// This could be used if the status has a custom view that specifies some parameters to be sent to the server + /// when an action needs to be executed + /// + [DataMember(Name = "actionParameters")] + public Dictionary? ActionParameters { get; set; } + + public string? Name + { + get => _name; + set => _name = value; + } + + /// + /// The description of the action - this is used to give a description before executing the action + /// + [DataMember(Name = "description")] + public string? Description { get; set; } + + /// + /// Indicates if a value is required to rectify the issue + /// + [DataMember(Name = "valueRequired")] + public bool ValueRequired { get; set; } + + /// + /// Indicates if a value required, how it is validated + /// + [DataMember(Name = "providedValueValidation")] + public string? ProvidedValueValidation { get; set; } + + /// + /// Indicates if a value required, and is validated by a regex, what the regex to use is + /// + [DataMember(Name = "providedValueValidationRegex")] + public string? ProvidedValueValidationRegex { get; set; } + + /// + /// Provides a value to rectify the issue + /// + [DataMember(Name = "providedValue")] + public string? ProvidedValue { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs b/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs index 0fa6647971..718a689caf 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs @@ -1,26 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.HealthChecks; -namespace Umbraco.Cms.Core.HealthChecks +/// +/// Metadata attribute for Health checks +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class HealthCheckAttribute : Attribute { - /// - /// Metadata attribute for Health checks - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public sealed class HealthCheckAttribute : Attribute + public HealthCheckAttribute(string id, string name) { - public HealthCheckAttribute(string id, string name) - { - Id = new Guid(id); - Name = name; - } - - public string Name { get; private set; } - public string? Description { get; set; } - - public string? Group { get; set; } - - public Guid Id { get; private set; } - - // TODO: Do we need more metadata? + Id = new Guid(id); + Name = name; } + + public string Name { get; } + + public string? Description { get; set; } + + public string? Group { get; set; } + + public Guid Id { get; } + + // TODO: Do we need more metadata? } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs index bcbee9036b..c2c47c1948 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckCollection : BuilderCollectionBase { - public class HealthCheckCollection : BuilderCollectionBase + public HealthCheckCollection(Func> items) + : base(items) { - public HealthCheckCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs b/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs index aee97647d9..ae67c192f5 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.HealthChecks -{ - [DataContract(Name = "healthCheckGroup", Namespace = "")] - public class HealthCheckGroup - { - [DataMember(Name = "name")] - public string? Name { get; set; } +namespace Umbraco.Cms.Core.HealthChecks; - [DataMember(Name = "checks")] - public List? Checks { get; set; } - } +[DataContract(Name = "healthCheckGroup", Namespace = "")] +public class HealthCheckGroup +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "checks")] + public List? Checks { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs index 6dd6df4b8b..128e6dabbe 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs @@ -1,18 +1,12 @@ -using System; +namespace Umbraco.Cms.Core.HealthChecks; -namespace Umbraco.Cms.Core.HealthChecks +/// +/// Metadata attribute for health check notification methods +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class HealthCheckNotificationMethodAttribute : Attribute { - /// - /// Metadata attribute for health check notification methods - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public sealed class HealthCheckNotificationMethodAttribute : Attribute - { - public HealthCheckNotificationMethodAttribute(string alias) - { - Alias = alias; - } + public HealthCheckNotificationMethodAttribute(string alias) => Alias = alias; - public string Alias { get; } - } + public string Alias { get; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs index af964857d8..1d681690db 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs @@ -1,14 +1,12 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckNotificationMethodCollection : BuilderCollectionBase { - public class HealthCheckNotificationMethodCollection : BuilderCollectionBase + public HealthCheckNotificationMethodCollection(Func> items) + : base(items) { - public HealthCheckNotificationMethodCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs index 48f2629e2a..375ddc7e2e 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs @@ -1,10 +1,11 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckNotificationMethodCollectionBuilder : LazyCollectionBuilderBase< + HealthCheckNotificationMethodCollectionBuilder, HealthCheckNotificationMethodCollection, + IHealthCheckNotificationMethod> { - public class HealthCheckNotificationMethodCollectionBuilder : LazyCollectionBuilderBase - { - protected override HealthCheckNotificationMethodCollectionBuilder This => this; - } + protected override HealthCheckNotificationMethodCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs index cba8ab5c0f..1e7ea90532 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs @@ -1,9 +1,7 @@ -namespace Umbraco.Cms.Core.HealthChecks -{ - public enum HealthCheckNotificationVerbosity - { +namespace Umbraco.Cms.Core.HealthChecks; - Summary, - Detailed - } +public enum HealthCheckNotificationVerbosity +{ + Summary, + Detailed, } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs index bde90627c7..afeb8ba9fa 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs @@ -1,166 +1,168 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckResults { - public class HealthCheckResults + public readonly bool AllChecksSuccessful; + + private HealthCheckResults(Dictionary> results, bool allChecksSuccessful) { - private readonly Dictionary> _results; - public readonly bool AllChecksSuccessful; + ResultsAsDictionary = results; + AllChecksSuccessful = allChecksSuccessful; + } - private static ILogger Logger => StaticApplicationLogging.Logger; // TODO: inject + internal Dictionary> ResultsAsDictionary { get; } - private HealthCheckResults(Dictionary> results, bool allChecksSuccessful) - { - _results = results; - AllChecksSuccessful = allChecksSuccessful; - } + private static ILogger Logger => StaticApplicationLogging.Logger; // TODO: inject - public static async Task Create(IEnumerable checks) - { - var results = await checks.ToDictionaryAsync( - t => t.Name, - async t => { - try - { - return await t.GetStatus(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error running scheduled health check: {HealthCheckName}", t.Name); - var message = $"Health check failed with exception: {ex.Message}. See logs for details."; - return new List - { - new HealthCheckStatus(message) - { - ResultType = StatusResultType.Error - } - }; - } - }); - - // find out if all checks pass or not - var allChecksSuccessful = true; - foreach (var result in results) + public static async Task Create(IEnumerable checks) + { + Dictionary> results = await checks.ToDictionaryAsync( + t => t.Name, + async t => { - var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success || x.ResultType == StatusResultType.Info || x.ResultType == StatusResultType.Warning); - if (checkIsSuccess == false) + try { - allChecksSuccessful = false; - break; + return await t.GetStatus(); } - } + catch (Exception ex) + { + Logger.LogError(ex, "Error running scheduled health check: {HealthCheckName}", t.Name); + var message = $"Health check failed with exception: {ex.Message}. See logs for details."; + return new List { new(message) { ResultType = StatusResultType.Error } }; + } + }); - return new HealthCheckResults(results, allChecksSuccessful); - } - - public void LogResults() + // find out if all checks pass or not + var allChecksSuccessful = true; + foreach (KeyValuePair> result in results) { - Logger.LogInformation("Scheduled health check results:"); - foreach (var result in _results) + var checkIsSuccess = result.Value.All(x => + x.ResultType == StatusResultType.Success || x.ResultType == StatusResultType.Info || + x.ResultType == StatusResultType.Warning); + if (checkIsSuccess == false) { - var checkName = result.Key; - var checkResults = result.Value; - var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); - if (checkIsSuccess) - { - Logger.LogInformation("Checks for '{HealthCheckName}' all completed successfully.", checkName); - } - else - { - Logger.LogWarning("Checks for '{HealthCheckName}' completed with errors.", checkName); - } - - foreach (var checkResult in checkResults) - { - Logger.LogInformation("Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", checkName, checkResult.ResultType, checkResult.Message); - } + allChecksSuccessful = false; + break; } } - public string ResultsAsMarkDown(HealthCheckNotificationVerbosity verbosity) + return new HealthCheckResults(results, allChecksSuccessful); + } + + public void LogResults() + { + Logger.LogInformation("Scheduled health check results:"); + foreach (KeyValuePair> result in ResultsAsDictionary) { - var newItem = "- "; - - var sb = new StringBuilder(); - - foreach (var result in _results) + var checkName = result.Key; + IEnumerable checkResults = result.Value; + var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); + if (checkIsSuccess) { - var checkName = result.Key; - var checkResults = result.Value; - var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); - - // add a new line if not the first check - if (result.Equals(_results.First()) == false) - { - sb.Append(Environment.NewLine); - } - - if (checkIsSuccess) - { - sb.AppendFormat("{0}Checks for '{1}' all completed successfully.{2}", newItem, checkName, Environment.NewLine); - } - else - { - sb.AppendFormat("{0}Checks for '{1}' completed with errors.{2}", newItem, checkName, Environment.NewLine); - } - - foreach (var checkResult in checkResults) - { - sb.AppendFormat("\t{0}Result: '{1}'", newItem, checkResult.ResultType); - - // With summary logging, only record details of warnings or errors - if (checkResult.ResultType != StatusResultType.Success || verbosity == HealthCheckNotificationVerbosity.Detailed) - { - sb.AppendFormat(", Message: '{0}'", SimpleHtmlToMarkDown(checkResult.Message)); - } - - sb.AppendLine(Environment.NewLine); - } + Logger.LogInformation("Checks for '{HealthCheckName}' all completed successfully.", checkName); + } + else + { + Logger.LogWarning("Checks for '{HealthCheckName}' completed with errors.", checkName); } - return sb.ToString(); - } - - - internal Dictionary> ResultsAsDictionary => _results; - - private string SimpleHtmlToMarkDown(string html) - { - return html.Replace("", "**") - .Replace("", "**") - .Replace("", "*") - .Replace("", "*"); - } - - public Dictionary>? GetResultsForStatus(StatusResultType resultType) - { - switch (resultType) + foreach (HealthCheckStatus checkResult in checkResults) { - case StatusResultType.Success: - // a check is considered a success status if all checks are successful or info - var successResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Success) && x.Value.All(y => y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info)); - return successResults.ToDictionary(x => x.Key, x => x.Value); - case StatusResultType.Warning: - // a check is considered warn status if one check is warn and all others are success or info - var warnResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Warning) && x.Value.All(y => y.ResultType == StatusResultType.Warning || y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info)); - return warnResults.ToDictionary(x => x.Key, x => x.Value); - case StatusResultType.Error: - // a check is considered error status if any check is error - var errorResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Error)); - return errorResults.ToDictionary(x => x.Key, x => x.Value); - case StatusResultType.Info: - // a check is considered info status if all checks are info - var infoResults = _results.Where(x => x.Value.All(y => y.ResultType == StatusResultType.Info)); - return infoResults.ToDictionary(x => x.Key, x => x.Value); + Logger.LogInformation( + "Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", + checkName, + checkResult.ResultType, + checkResult.Message); } - - return null; } } + + public string ResultsAsMarkDown(HealthCheckNotificationVerbosity verbosity) + { + var newItem = "- "; + + var sb = new StringBuilder(); + + foreach (KeyValuePair> result in ResultsAsDictionary) + { + var checkName = result.Key; + IEnumerable checkResults = result.Value; + var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); + + // add a new line if not the first check + if (result.Equals(ResultsAsDictionary.First()) == false) + { + sb.Append(Environment.NewLine); + } + + if (checkIsSuccess) + { + sb.AppendFormat("{0}Checks for '{1}' all completed successfully.{2}", newItem, checkName, Environment.NewLine); + } + else + { + sb.AppendFormat("{0}Checks for '{1}' completed with errors.{2}", newItem, checkName, Environment.NewLine); + } + + foreach (HealthCheckStatus checkResult in checkResults) + { + sb.AppendFormat("\t{0}Result: '{1}'", newItem, checkResult.ResultType); + + // With summary logging, only record details of warnings or errors + if (checkResult.ResultType != StatusResultType.Success || + verbosity == HealthCheckNotificationVerbosity.Detailed) + { + sb.AppendFormat(", Message: '{0}'", SimpleHtmlToMarkDown(checkResult.Message)); + } + + sb.AppendLine(Environment.NewLine); + } + } + + return sb.ToString(); + } + + public Dictionary>? GetResultsForStatus(StatusResultType resultType) + { + switch (resultType) + { + case StatusResultType.Success: + // a check is considered a success status if all checks are successful or info + IEnumerable>> successResults = + ResultsAsDictionary.Where(x => + x.Value.Any(y => y.ResultType == StatusResultType.Success) && x.Value.All(y => + y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info)); + return successResults.ToDictionary(x => x.Key, x => x.Value); + case StatusResultType.Warning: + // a check is considered warn status if one check is warn and all others are success or info + IEnumerable>> warnResults = + ResultsAsDictionary.Where(x => + x.Value.Any(y => y.ResultType == StatusResultType.Warning) && x.Value.All(y => + y.ResultType == StatusResultType.Warning || y.ResultType == StatusResultType.Success || + y.ResultType == StatusResultType.Info)); + return warnResults.ToDictionary(x => x.Key, x => x.Value); + case StatusResultType.Error: + // a check is considered error status if any check is error + IEnumerable>> errorResults = + ResultsAsDictionary.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Error)); + return errorResults.ToDictionary(x => x.Key, x => x.Value); + case StatusResultType.Info: + // a check is considered info status if all checks are info + IEnumerable>> infoResults = + ResultsAsDictionary.Where(x => x.Value.All(y => y.ResultType == StatusResultType.Info)); + return infoResults.ToDictionary(x => x.Key, x => x.Value); + } + + return null; + } + + private string SimpleHtmlToMarkDown(string html) => + html.Replace("", "**") + .Replace("", "**") + .Replace("", "*") + .Replace("", "*"); } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs b/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs index 49428fe899..7f04e51541 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs @@ -1,58 +1,55 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +/// +/// The status returned for a health check when it performs it check +/// TODO: This model will be used in the WebApi result so needs attributes for JSON usage +/// +[DataContract(Name = "healthCheckStatus", Namespace = "")] +public class HealthCheckStatus { - /// - /// The status returned for a health check when it performs it check - /// TODO: This model will be used in the WebApi result so needs attributes for JSON usage - /// - [DataContract(Name = "healthCheckStatus", Namespace = "")] - public class HealthCheckStatus + public HealthCheckStatus(string message) { - public HealthCheckStatus(string message) - { - Message = message; - Actions = Enumerable.Empty(); - } - - /// - /// The status message - /// - [DataMember(Name = "message")] - public string Message { get; private set; } - - /// - /// The status description if one is necessary - /// - [DataMember(Name = "description")] - public string? Description { get; set; } - - /// - /// This is optional but would allow a developer to specify a path to an angular HTML view - /// in order to either show more advanced information and/or to provide input for the admin - /// to configure how an action is executed - /// - [DataMember(Name = "view")] - public string? View { get; set; } - - /// - /// The status type - /// - [DataMember(Name = "resultType")] - public StatusResultType ResultType { get; set; } - - /// - /// The potential actions to take (in any) - /// - [DataMember(Name = "actions")] - public IEnumerable Actions { get; set; } - - /// - /// This is optional but would allow a developer to specify a link that is shown as a "read more" button. - /// - [DataMember(Name = "readMoreLink")] - public string? ReadMoreLink { get; set; } + Message = message; + Actions = Enumerable.Empty(); } + + /// + /// The status message + /// + [DataMember(Name = "message")] + public string Message { get; private set; } + + /// + /// The status description if one is necessary + /// + [DataMember(Name = "description")] + public string? Description { get; set; } + + /// + /// This is optional but would allow a developer to specify a path to an angular HTML view + /// in order to either show more advanced information and/or to provide input for the admin + /// to configure how an action is executed + /// + [DataMember(Name = "view")] + public string? View { get; set; } + + /// + /// The status type + /// + [DataMember(Name = "resultType")] + public StatusResultType ResultType { get; set; } + + /// + /// The potential actions to take (in any) + /// + [DataMember(Name = "actions")] + public IEnumerable Actions { get; set; } + + /// + /// This is optional but would allow a developer to specify a link that is shown as a "read more" button. + /// + [DataMember(Name = "readMoreLink")] + public string? ReadMoreLink { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs b/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs index 495fc42cf1..1c026248c8 100644 --- a/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs +++ b/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs @@ -1,14 +1,15 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.HealthChecks -{ - public class HealthCheckCollectionBuilder : LazyCollectionBuilderBase - { - protected override HealthCheckCollectionBuilder This => this; +namespace Umbraco.Cms.Core.HealthChecks; - // note: in v7 they were per-request, not sure why? - // the collection is injected into the controller & there's only 1 controller per request anyways - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; // transient! - } +public class + HealthCheckCollectionBuilder : LazyCollectionBuilderBase +{ + protected override HealthCheckCollectionBuilder This => this; + + // note: in v7 they were per-request, not sure why? + // the collection is injected into the controller & there's only 1 controller per request anyways + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; // transient! } diff --git a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs index 94867d8882..022531c1ec 100644 --- a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; @@ -8,89 +6,91 @@ using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods +namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods; + +[HealthCheckNotificationMethod("email")] +public class EmailNotificationMethod : NotificationMethodBase { - [HealthCheckNotificationMethod("email")] - public class EmailNotificationMethod : NotificationMethodBase + private readonly IEmailSender? _emailSender; + private readonly IHostingEnvironment? _hostingEnvironment; + private readonly IMarkdownToHtmlConverter? _markdownToHtmlConverter; + private readonly ILocalizedTextService? _textService; + private ContentSettings? _contentSettings; + + public EmailNotificationMethod( + ILocalizedTextService textService, + IHostingEnvironment hostingEnvironment, + IEmailSender emailSender, + IOptionsMonitor healthChecksSettings, + IOptionsMonitor contentSettings, + IMarkdownToHtmlConverter markdownToHtmlConverter) + : base(healthChecksSettings) { - private readonly ILocalizedTextService? _textService; - private readonly IHostingEnvironment? _hostingEnvironment; - private readonly IEmailSender? _emailSender; - private readonly IMarkdownToHtmlConverter? _markdownToHtmlConverter; - private ContentSettings? _contentSettings; - - public EmailNotificationMethod( - ILocalizedTextService textService, - IHostingEnvironment hostingEnvironment, - IEmailSender emailSender, - IOptionsMonitor healthChecksSettings, - IOptionsMonitor contentSettings, - IMarkdownToHtmlConverter markdownToHtmlConverter) - : base(healthChecksSettings) + var recipientEmail = Settings?["RecipientEmail"]; + if (string.IsNullOrWhiteSpace(recipientEmail)) { - var recipientEmail = Settings?["RecipientEmail"]; - if (string.IsNullOrWhiteSpace(recipientEmail)) - { - Enabled = false; - return; - } - - RecipientEmail = recipientEmail; - - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _hostingEnvironment = hostingEnvironment; - _emailSender = emailSender; - _markdownToHtmlConverter = markdownToHtmlConverter; - _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); - - contentSettings.OnChange(x => _contentSettings = x); + Enabled = false; + return; } - public string? RecipientEmail { get; } + RecipientEmail = recipientEmail; - public override async Task SendAsync(HealthCheckResults results) + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _hostingEnvironment = hostingEnvironment; + _emailSender = emailSender; + _markdownToHtmlConverter = markdownToHtmlConverter; + _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); + + contentSettings.OnChange(x => _contentSettings = x); + } + + public string? RecipientEmail { get; } + + public override async Task SendAsync(HealthCheckResults results) + { + if (ShouldSend(results) == false) { - if (ShouldSend(results) == false) - { - return; - } + return; + } - if (string.IsNullOrEmpty(RecipientEmail)) - { - return; - } + if (string.IsNullOrEmpty(RecipientEmail)) + { + return; + } - var message = _textService?.Localize("healthcheck","scheduledHealthCheckEmailBody", new[] + var message = _textService?.Localize( + "healthcheck", + "scheduledHealthCheckEmailBody", + new[] { - DateTime.Now.ToShortDateString(), - DateTime.Now.ToShortTimeString(), - _markdownToHtmlConverter?.ToHtml(results, Verbosity) + DateTime.Now.ToShortDateString(), DateTime.Now.ToShortTimeString(), + _markdownToHtmlConverter?.ToHtml(results, Verbosity), }); - // Include the umbraco Application URL host in the message subject so that - // you can identify the site that these results are for. - var host = _hostingEnvironment?.ApplicationMainUrl?.ToString(); + // Include the umbraco Application URL host in the message subject so that + // you can identify the site that these results are for. + var host = _hostingEnvironment?.ApplicationMainUrl?.ToString(); - var subject = _textService?.Localize("healthcheck","scheduledHealthCheckEmailSubject", new[] { host }); + var subject = _textService?.Localize("healthcheck", "scheduledHealthCheckEmailSubject", new[] { host }); - - var mailMessage = CreateMailMessage(subject, message); - Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck); - if (task is not null) - { - await task; - } - } - - private EmailMessage CreateMailMessage(string? subject, string? message) + EmailMessage mailMessage = CreateMailMessage(subject, message); + Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck); + if (task is not null) { - var to = _contentSettings?.Notifications.Email; - - if (string.IsNullOrWhiteSpace(subject)) - subject = "Umbraco Health Check Status"; - - var isBodyHtml = message.IsNullOrWhiteSpace() == false && message!.Contains("<") && message.Contains(" healthCheckSettings) { - protected NotificationMethodBase(IOptionsMonitor healthCheckSettings) + Type type = GetType(); + HealthCheckNotificationMethodAttribute? attribute = type.GetCustomAttribute(); + if (attribute == null) { - var type = GetType(); - var attribute = type.GetCustomAttribute(); - if (attribute == null) - { - Enabled = false; - return; - } - - var notificationMethods = healthCheckSettings.CurrentValue.Notification.NotificationMethods; - if (!notificationMethods.TryGetValue(attribute.Alias, out var notificationMethod)) - { - Enabled = false; - return; - } - - Enabled = notificationMethod.Enabled; - FailureOnly = notificationMethod.FailureOnly; - Verbosity = notificationMethod.Verbosity; - Settings = notificationMethod.Settings; + Enabled = false; + return; } - public bool Enabled { get; protected set; } - - public bool FailureOnly { get; protected set; } - - public HealthCheckNotificationVerbosity Verbosity { get; protected set; } - - public IDictionary? Settings { get; } - - protected bool ShouldSend(HealthCheckResults results) + IDictionary notificationMethods = + healthCheckSettings.CurrentValue.Notification.NotificationMethods; + if (!notificationMethods.TryGetValue( + attribute.Alias, out HealthChecksNotificationMethodSettings? notificationMethod)) { - return Enabled && (!FailureOnly || !results.AllChecksSuccessful); + Enabled = false; + return; } - public abstract Task SendAsync(HealthCheckResults results); + Enabled = notificationMethod.Enabled; + FailureOnly = notificationMethod.FailureOnly; + Verbosity = notificationMethod.Verbosity; + Settings = notificationMethod.Settings; } + + public bool FailureOnly { get; protected set; } + + public HealthCheckNotificationVerbosity Verbosity { get; protected set; } + + public IDictionary? Settings { get; } + + public bool Enabled { get; protected set; } + + public abstract Task SendAsync(HealthCheckResults results); + + protected bool ShouldSend(HealthCheckResults results) => Enabled && (!FailureOnly || !results.AllChecksSuccessful); } diff --git a/src/Umbraco.Core/HealthChecks/StatusResultType.cs b/src/Umbraco.Core/HealthChecks/StatusResultType.cs index b06322a267..0516fc3544 100644 --- a/src/Umbraco.Core/HealthChecks/StatusResultType.cs +++ b/src/Umbraco.Core/HealthChecks/StatusResultType.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public enum StatusResultType { - public enum StatusResultType - { - Success, - Warning, - Error, - Info - } + Success, + Warning, + Error, + Info, } diff --git a/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs b/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs index 254a53c6fb..9269f905f4 100644 --- a/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs +++ b/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public enum ValueComparisonType { - public enum ValueComparisonType - { - ShouldEqual, - ShouldNotEqual, - } + ShouldEqual, + ShouldNotEqual, } diff --git a/src/Umbraco.Core/HexEncoder.cs b/src/Umbraco.Core/HexEncoder.cs index ce4df997ab..b95376646b 100644 --- a/src/Umbraco.Core/HexEncoder.cs +++ b/src/Umbraco.Core/HexEncoder.cs @@ -1,84 +1,85 @@ -using System.Linq; using System.Runtime.CompilerServices; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides methods for encoding byte arrays into hexadecimal strings. +/// +public static class HexEncoder { - /// - /// Provides methods for encoding byte arrays into hexadecimal strings. - /// - public static class HexEncoder + // LUT's that provide the hexadecimal representation of each possible byte value. + private static readonly char[] HexLutBase = { - // LUT's that provide the hexadecimal representation of each possible byte value. - private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + }; - // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16 - private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray(); + // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16 + private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray(); - // The base LUT repeated 16x. - private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray(); + // The base LUT repeated 16x. + private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray(); - /// - /// Converts a to a hexadecimal formatted padded to 2 digits. - /// - /// The bytes. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Encode(byte[] bytes) + /// + /// Converts a to a hexadecimal formatted padded to 2 digits. + /// + /// The bytes. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes) + { + var length = bytes.Length; + var chars = new char[length * 2]; + + var index = 0; + for (var i = 0; i < length; i++) { - var length = bytes.Length; - var chars = new char[length * 2]; - - var index = 0; - for (var i = 0; i < length; i++) - { - var byteIndex = bytes[i]; - chars[index++] = HexLutHi[byteIndex]; - chars[index++] = HexLutLo[byteIndex]; - } - - return new string(chars, 0, chars.Length); + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; } - /// - /// Converts a to a hexadecimal formatted padded to 2 digits - /// and split into blocks with the given char separator. - /// - /// The bytes. - /// The separator. - /// The block size. - /// The block count. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount) + return new string(chars, 0, chars.Length); + } + + /// + /// Converts a to a hexadecimal formatted padded to 2 digits + /// and split into blocks with the given char separator. + /// + /// The bytes. + /// The separator. + /// The block size. + /// The block count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount) + { + var length = bytes.Length; + var chars = new char[(length * 2) + blockCount]; + var count = 0; + var size = 0; + var index = 0; + + for (var i = 0; i < length; i++) { - var length = bytes.Length; - var chars = new char[(length * 2) + blockCount]; - var count = 0; - var size = 0; - var index = 0; + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; - for (var i = 0; i < length; i++) + if (count == blockCount) { - var byteIndex = bytes[i]; - chars[index++] = HexLutHi[byteIndex]; - chars[index++] = HexLutLo[byteIndex]; - - if (count == blockCount) - { - continue; - } - - if (++size < blockSize) - { - continue; - } - - chars[index++] = separator; - size = 0; - count++; + continue; } - return new string(chars, 0, chars.Length); + if (++size < blockSize) + { + continue; + } + + chars[index++] = separator; + size = 0; + count++; } + + return new string(chars, 0, chars.Length); } } diff --git a/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs b/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs index 2d1336ab90..84b275714b 100644 --- a/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs +++ b/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Hosting +namespace Umbraco.Cms.Core.Hosting; + +public interface IApplicationShutdownRegistry { - public interface IApplicationShutdownRegistry - { - void RegisterObject(IRegisteredObject registeredObject); - void UnregisterObject(IRegisteredObject registeredObject); - } + void RegisterObject(IRegisteredObject registeredObject); + + void UnregisterObject(IRegisteredObject registeredObject); } diff --git a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs index c2c7cfe792..b8960048f6 100644 --- a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs +++ b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs @@ -1,101 +1,108 @@ -using System; +namespace Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Hosting +public interface IHostingEnvironment { - public interface IHostingEnvironment - { - string SiteName { get; } + string SiteName { get; } - /// - /// The unique application ID for this Umbraco website. - /// - /// - /// - /// The returned value will be the same consistent value for an Umbraco website on a specific server and will the same - /// between restarts of that Umbraco website/application on that specific server. - /// - /// - /// The value of this does not distinguish between unique workers/servers for this Umbraco application. - /// Usage of this must take into account that the same may be returned for the same - /// Umbraco website hosted on different servers.
- /// Similarly the usage of this must take into account that a different - /// may be returned for the same Umbraco website hosted on different servers. - ///
- /// - /// This returns a hash of the value of IApplicationDiscriminator.Discriminator (which is most likely just the value of unless an alternative implementation of IApplicationDiscriminator has been registered).
- /// However during ConfigureServices a temporary instance of IHostingEnvironment is constructed which guarantees that this will be the hash of , so the value may differ depend on when the property is used. - ///
- /// - /// If you require this value during ConfigureServices it is probably a code smell. - /// - ///
- [Obsolete("Please use IApplicationDiscriminator.Discriminator instead.")] - string ApplicationId { get; } + /// + /// The unique application ID for this Umbraco website. + /// + /// + /// + /// The returned value will be the same consistent value for an Umbraco website on a specific server and will the + /// same + /// between restarts of that Umbraco website/application on that specific server. + /// + /// + /// The value of this does not distinguish between unique workers/servers for this Umbraco application. + /// Usage of this must take into account that the same may be returned for the same + /// Umbraco website hosted on different servers.
+ /// Similarly the usage of this must take into account that a different + /// may be returned for the same Umbraco website hosted on different servers. + ///
+ /// + /// This returns a hash of the value of IApplicationDiscriminator.Discriminator (which is most likely just the + /// value of unless an alternative + /// implementation of IApplicationDiscriminator has been registered).
+ /// However during ConfigureServices a temporary instance of IHostingEnvironment is constructed which guarantees + /// that this will be the hash of , so + /// the value may differ depend on when the property is used. + ///
+ /// + /// If you require this value during ConfigureServices it is probably a code smell. + /// + ///
+ [Obsolete("Please use IApplicationDiscriminator.Discriminator instead.")] + string ApplicationId { get; } - /// - /// Will return the physical path to the root of the application - /// - string ApplicationPhysicalPath { get; } + /// + /// Will return the physical path to the root of the application + /// + string ApplicationPhysicalPath { get; } - string LocalTempPath { get; } + string LocalTempPath { get; } - /// - /// The web application's hosted path - /// - /// - /// In most cases this will return "/" but if the site is hosted in a virtual directory then this will return the virtual directory's path such as "/mysite". - /// This value must begin with a "/" and cannot end with "/". - /// - string ApplicationVirtualPath { get; } + /// + /// The web application's hosted path + /// + /// + /// In most cases this will return "/" but if the site is hosted in a virtual directory then this will return the + /// virtual directory's path such as "/mysite". + /// This value must begin with a "/" and cannot end with "/". + /// + string ApplicationVirtualPath { get; } - bool IsDebugMode { get; } + bool IsDebugMode { get; } - /// - /// Gets a value indicating whether Umbraco is hosted. - /// - bool IsHosted { get; } + /// + /// Gets a value indicating whether Umbraco is hosted. + /// + bool IsHosted { get; } - /// - /// Gets the main application url. - /// - Uri ApplicationMainUrl { get; } + /// + /// Gets the main application url. + /// + Uri ApplicationMainUrl { get; } - /// - /// Maps a virtual path to a physical path to the application's web root - /// - /// - /// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however - /// in netcore the web root is /www therefore this will Map to a physical path within www. - /// - [Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")] - string MapPathWebRoot(string path); + /// + /// Maps a virtual path to a physical path to the application's web root + /// + /// + /// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the + /// content root are the same, however + /// in netcore the web root is /www therefore this will Map to a physical path within www. + /// + [Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")] + string MapPathWebRoot(string path); - /// - /// Maps a virtual path to a physical path to the application's root (not always equal to the web root) - /// - /// - /// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however - /// in netcore the web root is /www therefore this will Map to a physical path within www. - /// - [Obsolete("Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")] - string MapPathContentRoot(string path); + /// + /// Maps a virtual path to a physical path to the application's root (not always equal to the web root) + /// + /// + /// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the + /// content root are the same, however + /// in netcore the web root is /www therefore this will Map to a physical path within www. + /// + [Obsolete( + "Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")] + string MapPathContentRoot(string path); - /// - /// Converts a virtual path to an absolute URL path based on the application's web root - /// - /// The virtual path. Must start with either ~/ or / else an exception is thrown. - /// - /// This maps the virtual path syntax to the web root. For example when hosting in a virtual directory called "site" and the value "~/pages/test" is passed in, it will - /// map to "/site/pages/test" where "/site" is the value of . - /// - /// - /// If virtualPath does not start with ~/ or / - /// - string ToAbsolute(string virtualPath); + /// + /// Converts a virtual path to an absolute URL path based on the application's web root + /// + /// The virtual path. Must start with either ~/ or / else an exception is thrown. + /// + /// This maps the virtual path syntax to the web root. For example when hosting in a virtual directory called "site" + /// and the value "~/pages/test" is passed in, it will + /// map to "/site/pages/test" where "/site" is the value of . + /// + /// + /// If virtualPath does not start with ~/ or / + /// + string ToAbsolute(string virtualPath); - /// - /// Ensures that the application know its main Url. - /// - void EnsureApplicationMainUrl(Uri? currentApplicationUrl); - } + /// + /// Ensures that the application know its main Url. + /// + void EnsureApplicationMainUrl(Uri? currentApplicationUrl); } diff --git a/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs index f55040f96a..493c3ab4dc 100644 --- a/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Hosting -{ - public interface IUmbracoApplicationLifetime - { - /// - /// A value indicating whether the application is restarting after the current request. - /// - bool IsRestarting { get; } +namespace Umbraco.Cms.Core.Hosting; - /// - /// Terminates the current application. The application restarts the next time a request is received for it. - /// - void Restart(); - } +public interface IUmbracoApplicationLifetime +{ + /// + /// A value indicating whether the application is restarting after the current request. + /// + bool IsRestarting { get; } + + /// + /// Terminates the current application. The application restarts the next time a request is received for it. + /// + void Restart(); } diff --git a/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs b/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs index 15b08d1ac6..e821102f09 100644 --- a/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs +++ b/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs @@ -1,8 +1,12 @@ -namespace Umbraco.Cms.Core.Hosting +namespace Umbraco.Cms.Core.Hosting; + +internal class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry { - internal class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry + public void RegisterObject(IRegisteredObject registeredObject) + { + } + + public void UnregisterObject(IRegisteredObject registeredObject) { - public void RegisterObject(IRegisteredObject registeredObject) { } - public void UnregisterObject(IRegisteredObject registeredObject) { } } } diff --git a/src/Umbraco.Core/HybridAccessorBase.cs b/src/Umbraco.Core/HybridAccessorBase.cs index 3200f97d7d..fdee8e4ec5 100644 --- a/src/Umbraco.Core/HybridAccessorBase.cs +++ b/src/Umbraco.Core/HybridAccessorBase.cs @@ -1,78 +1,78 @@ -using System; -using System.Threading; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides a base class for hybrid accessors. +/// +/// The type of the accessed object. +/// +/// +/// Hybrid accessors store the accessed object in HttpContext if they can, +/// otherwise they rely on the logical call context, to maintain an ambient +/// object that flows with async. +/// +/// +public abstract class HybridAccessorBase + where T : class { - /// - /// Provides a base class for hybrid accessors. - /// - /// The type of the accessed object. - /// - /// Hybrid accessors store the accessed object in HttpContext if they can, - /// otherwise they rely on the logical call context, to maintain an ambient - /// object that flows with async. - /// - public abstract class HybridAccessorBase - where T : class + private static readonly AsyncLocal AmbientContext = new(); + + private readonly IRequestCache _requestCache; + private string? _itemKey; + + protected HybridAccessorBase(IRequestCache requestCache) + => _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + + protected string ItemKey => _itemKey ??= GetType().FullName!; + + protected T? Value { - private static readonly AsyncLocal s_ambientContext = new AsyncLocal(); - - private readonly IRequestCache _requestCache; - private string? _itemKey; - protected string ItemKey => _itemKey ??= GetType().FullName!; - - // read - // http://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html - // http://stackoverflow.com/questions/14176028/why-does-logicalcallcontext-not-work-with-async - // http://stackoverflow.com/questions/854976/will-values-in-my-threadstatic-variables-still-be-there-when-cycled-via-threadpo - // https://msdn.microsoft.com/en-us/library/dd642243.aspx?f=255&MSPPError=-2147217396 ThreadLocal - // http://stackoverflow.com/questions/29001266/cleaning-up-callcontext-in-tpl clearing call context - // - // anything that is ThreadStatic will stay with the thread and NOT flow in async threads - // the only thing that flows is the logical call context (safe in 4.5+) - - // no! - //[ThreadStatic] - //private static T _value; - - // yes! flows with async! - private T? NonContextValue + get { - get => s_ambientContext.Value ?? default; - set => s_ambientContext.Value = value; - } - - protected HybridAccessorBase(IRequestCache requestCache) - => _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - - protected T? Value - { - get + if (!_requestCache.IsAvailable) { - if (!_requestCache.IsAvailable) - { - return NonContextValue; - } - return (T?) _requestCache.Get(ItemKey); + return NonContextValue; } - set + return (T?)_requestCache.Get(ItemKey); + } + + set + { + if (!_requestCache.IsAvailable) { - if (!_requestCache.IsAvailable) - { - NonContextValue = value; - } - else if (value == null) - { - _requestCache.Remove(ItemKey); - } - else - { - _requestCache.Set(ItemKey, value); - } + NonContextValue = value; + } + else if (value == null) + { + _requestCache.Remove(ItemKey); + } + else + { + _requestCache.Set(ItemKey, value); } } } + + // read + // http://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html + // http://stackoverflow.com/questions/14176028/why-does-logicalcallcontext-not-work-with-async + // http://stackoverflow.com/questions/854976/will-values-in-my-threadstatic-variables-still-be-there-when-cycled-via-threadpo + // https://msdn.microsoft.com/en-us/library/dd642243.aspx?f=255&MSPPError=-2147217396 ThreadLocal + // http://stackoverflow.com/questions/29001266/cleaning-up-callcontext-in-tpl clearing call context + // + // anything that is ThreadStatic will stay with the thread and NOT flow in async threads + // the only thing that flows is the logical call context (safe in 4.5+) + + // no! + // [ThreadStatic] + // private static T _value; + + // yes! flows with async! + private T? NonContextValue + { + get => AmbientContext.Value ?? default; + set => AmbientContext.Value = value; + } } diff --git a/src/Umbraco.Core/HybridEventMessagesAccessor.cs b/src/Umbraco.Core/HybridEventMessagesAccessor.cs index 14fa0433ce..d129b9a117 100644 --- a/src/Umbraco.Core/HybridEventMessagesAccessor.cs +++ b/src/Umbraco.Core/HybridEventMessagesAccessor.cs @@ -1,19 +1,18 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core -{ - public class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor - { - public HybridEventMessagesAccessor(IRequestCache requestCache) - : base(requestCache) - { - } +namespace Umbraco.Cms.Core; - public EventMessages? EventMessages - { - get { return Value; } - set { Value = value; } - } +public class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor +{ + public HybridEventMessagesAccessor(IRequestCache requestCache) + : base(requestCache) + { + } + + public EventMessages? EventMessages + { + get => Value; + set => Value = value; } } diff --git a/src/Umbraco.Core/IBackOfficeInfo.cs b/src/Umbraco.Core/IBackOfficeInfo.cs index 66f5d97bd9..bc27eb7f16 100644 --- a/src/Umbraco.Core/IBackOfficeInfo.cs +++ b/src/Umbraco.Core/IBackOfficeInfo.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public interface IBackOfficeInfo { - public interface IBackOfficeInfo - { - /// - /// Gets the absolute url to the Umbraco Backoffice. This info can be used to build absolute urls for Backoffice to use in mails etc. - /// - string GetAbsoluteUrl { get; } - } + /// + /// Gets the absolute url to the Umbraco Backoffice. This info can be used to build absolute urls for Backoffice to use + /// in mails etc. + /// + string GetAbsoluteUrl { get; } } diff --git a/src/Umbraco.Core/ICompletable.cs b/src/Umbraco.Core/ICompletable.cs index 2061723575..b13000de22 100644 --- a/src/Umbraco.Core/ICompletable.cs +++ b/src/Umbraco.Core/ICompletable.cs @@ -1,9 +1,6 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public interface ICompletable : IDisposable { - public interface ICompletable : IDisposable - { - void Complete(); - } + void Complete(); } diff --git a/src/Umbraco.Core/IFireAndForgetRunner.cs b/src/Umbraco.Core/IFireAndForgetRunner.cs new file mode 100644 index 0000000000..b28a777990 --- /dev/null +++ b/src/Umbraco.Core/IFireAndForgetRunner.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core; + +public interface IFireAndForgetRunner +{ + void RunFireAndForget(Func task); +} diff --git a/src/Umbraco.Core/IO/CleanFolderResult.cs b/src/Umbraco.Core/IO/CleanFolderResult.cs index d2bed317a6..76d1767eab 100644 --- a/src/Umbraco.Core/IO/CleanFolderResult.cs +++ b/src/Umbraco.Core/IO/CleanFolderResult.cs @@ -1,49 +1,33 @@ -using System; -using System.Collections.Generic; -using System.IO; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +public class CleanFolderResult { - public class CleanFolderResult + private CleanFolderResult() { - private CleanFolderResult() + } + + public CleanFolderResultStatus Status { get; private set; } + + public IReadOnlyCollection? Errors { get; private set; } + + public static CleanFolderResult Success() => new CleanFolderResult { Status = CleanFolderResultStatus.Success }; + + public static CleanFolderResult FailedAsDoesNotExist() => + new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist }; + + public static CleanFolderResult FailedWithErrors(List errors) => + new CleanFolderResult { Status = CleanFolderResultStatus.FailedWithException, Errors = errors.AsReadOnly() }; + + public class Error + { + public Error(Exception exception, FileInfo erroringFile) { + Exception = exception; + ErroringFile = erroringFile; } - public CleanFolderResultStatus Status { get; private set; } + public Exception Exception { get; set; } - public IReadOnlyCollection? Errors { get; private set; } - - public static CleanFolderResult Success() - { - return new CleanFolderResult { Status = CleanFolderResultStatus.Success }; - } - - public static CleanFolderResult FailedAsDoesNotExist() - { - return new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist }; - } - - public static CleanFolderResult FailedWithErrors(List errors) - { - return new CleanFolderResult - { - Status = CleanFolderResultStatus.FailedWithException, - Errors = errors.AsReadOnly(), - }; - } - - public class Error - { - public Error(Exception exception, FileInfo erroringFile) - { - Exception = exception; - ErroringFile = erroringFile; - } - - public Exception Exception { get; set; } - - public FileInfo ErroringFile { get; set; } - } + public FileInfo ErroringFile { get; set; } } } diff --git a/src/Umbraco.Core/IO/CleanFolderResultStatus.cs b/src/Umbraco.Core/IO/CleanFolderResultStatus.cs index 3180677acb..73d32982aa 100644 --- a/src/Umbraco.Core/IO/CleanFolderResultStatus.cs +++ b/src/Umbraco.Core/IO/CleanFolderResultStatus.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public enum CleanFolderResultStatus { - public enum CleanFolderResultStatus - { - Success, - FailedAsDoesNotExist, - FailedWithException - } + Success, + FailedAsDoesNotExist, + FailedWithException, } diff --git a/src/Umbraco.Core/IO/DefaultViewContentProvider.cs b/src/Umbraco.Core/IO/DefaultViewContentProvider.cs index e78118da62..5e0c10d80d 100644 --- a/src/Umbraco.Core/IO/DefaultViewContentProvider.cs +++ b/src/Umbraco.Core/IO/DefaultViewContentProvider.cs @@ -1,62 +1,66 @@ using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class DefaultViewContentProvider : IDefaultViewContentProvider { - public class DefaultViewContentProvider : IDefaultViewContentProvider + public string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null) { - public string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null) + var content = new StringBuilder(); + + if (string.IsNullOrWhiteSpace(modelNamespaceAlias)) { - var content = new StringBuilder(); - - if (string.IsNullOrWhiteSpace(modelNamespaceAlias)) - modelNamespaceAlias = "ContentModels"; - - // either - // @inherits Umbraco.Web.Mvc.UmbracoViewPage - // @inherits Umbraco.Web.Mvc.UmbracoViewPage - content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;"); - content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"); - if (modelClassName.IsNullOrWhiteSpace() == false) - { - content.Append("<"); - if (modelNamespace.IsNullOrWhiteSpace() == false) - { - content.Append(modelNamespaceAlias); - content.Append("."); - } - content.Append(modelClassName); - content.Append(">"); - } - content.Append("\r\n"); - - // if required, add - // @using ContentModels = ModelNamespace; - if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false) - { - content.Append("@using "); - content.Append(modelNamespaceAlias); - content.Append(" = "); - content.Append(modelNamespace); - content.Append(";\r\n"); - } - - // either - // Layout = null; - // Layout = "layoutPage.cshtml"; - content.Append("@{\r\n\tLayout = "); - if (layoutPageAlias.IsNullOrWhiteSpace()) - { - content.Append("null"); - } - else - { - content.Append("\""); - content.Append(layoutPageAlias); - content.Append(".cshtml\""); - } - content.Append(";\r\n}"); - return content.ToString(); + modelNamespaceAlias = "ContentModels"; } + + // either + // @inherits Umbraco.Web.Mvc.UmbracoViewPage + // @inherits Umbraco.Web.Mvc.UmbracoViewPage + content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;"); + content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"); + if (modelClassName.IsNullOrWhiteSpace() == false) + { + content.Append("<"); + if (modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append(modelNamespaceAlias); + content.Append("."); + } + + content.Append(modelClassName); + content.Append(">"); + } + + content.Append("\r\n"); + + // if required, add + // @using ContentModels = ModelNamespace; + if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append("@using "); + content.Append(modelNamespaceAlias); + content.Append(" = "); + content.Append(modelNamespace); + content.Append(";\r\n"); + } + + // either + // Layout = null; + // Layout = "layoutPage.cshtml"; + content.Append("@{\r\n\tLayout = "); + if (layoutPageAlias.IsNullOrWhiteSpace()) + { + content.Append("null"); + } + else + { + content.Append("\""); + content.Append(layoutPageAlias); + content.Append(".cshtml\""); + } + + content.Append(";\r\n}"); + return content.ToString(); } } diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 16ac1b0041..44bc1ac2ad 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -1,112 +1,109 @@ -using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Security.Cryptography; using System.Text; -using System.Threading; using Microsoft.Extensions.FileProviders; using Umbraco.Cms.Core.IO; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class FileSystemExtensions { - public static class FileSystemExtensions + public static string GetStreamHash(this Stream fileStream) { - public static string GetStreamHash(this Stream fileStream) + if (fileStream.CanSeek) { - if (fileStream.CanSeek) - { - fileStream.Seek(0, SeekOrigin.Begin); - } - - using HashAlgorithm alg = SHA1.Create(); - - // create a string output for the hash - var stringBuilder = new StringBuilder(); - var hashedByteArray = alg.ComputeHash(fileStream); - foreach (var b in hashedByteArray) - { - stringBuilder.Append(b.ToString("x2")); - } - return stringBuilder.ToString(); + fileStream.Seek(0, SeekOrigin.Begin); } - /// - /// Attempts to open the file at filePath up to maxRetries times, - /// with a thread sleep time of sleepPerRetryInMilliseconds between retries. - /// - public static FileStream OpenReadWithRetry(this FileInfo file, int maxRetries = 5, int sleepPerRetryInMilliseconds = 50) - { - var retries = maxRetries; + using HashAlgorithm alg = SHA1.Create(); - while (retries > 0) + // create a string output for the hash + var stringBuilder = new StringBuilder(); + var hashedByteArray = alg.ComputeHash(fileStream); + foreach (var b in hashedByteArray) + { + stringBuilder.Append(b.ToString("x2")); + } + + return stringBuilder.ToString(); + } + + /// + /// Attempts to open the file at filePath up to maxRetries times, + /// with a thread sleep time of sleepPerRetryInMilliseconds between retries. + /// + public static FileStream OpenReadWithRetry(this FileInfo file, int maxRetries = 5, int sleepPerRetryInMilliseconds = 50) + { + var retries = maxRetries; + + while (retries > 0) + { + try { - try + return File.OpenRead(file.FullName); + } + catch (IOException) + { + retries--; + + if (retries == 0) { - return File.OpenRead(file.FullName); + throw; } - catch(IOException) - { - retries--; - if (retries == 0) - { - throw; - } - - Thread.Sleep(sleepPerRetryInMilliseconds); - } - } - - throw new ArgumentException("Retries must be greater than zero"); - } - - public static void CopyFile(this IFileSystem fs, string path, string newPath) - { - using (Stream stream = fs.OpenFile(path)) - { - fs.AddFile(newPath, stream); + Thread.Sleep(sleepPerRetryInMilliseconds); } } - public static string GetExtension(this IFileSystem fs, string path) - { - return Path.GetExtension(fs.GetFullPath(path)); - } + throw new ArgumentException("Retries must be greater than zero"); + } - public static string GetFileName(this IFileSystem fs, string path) + public static void CopyFile(this IFileSystem fs, string path, string newPath) + { + using (Stream stream = fs.OpenFile(path)) { - return Path.GetFileName(fs.GetFullPath(path)); - } - - // TODO: Currently this is the only way to do this - public static void CreateFolder(this IFileSystem fs, string folderPath) - { - var path = fs.GetRelativePath(folderPath); - var tempFile = Path.Combine(path, Guid.NewGuid().ToString("N") + ".tmp"); - using (var s = new MemoryStream()) - { - fs.AddFile(tempFile, s); - } - fs.DeleteFile(tempFile); - } - - /// - /// Creates a new from the file system. - /// - /// The file system. - /// When this method returns, contains an created from the file system. - /// - /// true if the was successfully created; otherwise, false. - /// - public static bool TryCreateFileProvider(this IFileSystem fileSystem, [MaybeNullWhen(false)] out IFileProvider fileProvider) - { - fileProvider = fileSystem switch - { - IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), - _ => null - }; - - return fileProvider != null; + fs.AddFile(newPath, stream); } } + + public static string GetExtension(this IFileSystem fs, string path) => Path.GetExtension(fs.GetFullPath(path)); + + public static string GetFileName(this IFileSystem fs, string path) => Path.GetFileName(fs.GetFullPath(path)); + + // TODO: Currently this is the only way to do this + public static void CreateFolder(this IFileSystem fs, string folderPath) + { + var path = fs.GetRelativePath(folderPath); + var tempFile = Path.Combine(path, Guid.NewGuid().ToString("N") + ".tmp"); + using (var s = new MemoryStream()) + { + fs.AddFile(tempFile, s); + } + + fs.DeleteFile(tempFile); + } + + /// + /// Creates a new from the file system. + /// + /// The file system. + /// + /// When this method returns, contains an created from the file + /// system. + /// + /// + /// true if the was successfully created; otherwise, false. + /// + public static bool TryCreateFileProvider( + this IFileSystem fileSystem, + [MaybeNullWhen(false)] out IFileProvider fileProvider) + { + fileProvider = fileSystem switch + { + IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), + _ => null, + }; + + return fileProvider != null; + } } diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 5a4c92d509..2a5fa685df 100644 --- a/src/Umbraco.Core/IO/FileSystems.cs +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -29,13 +26,13 @@ namespace Umbraco.Cms.Core.IO private ShadowWrapper? _mvcViewsFileSystem; // well-known file systems lazy initialization - private object _wkfsLock = new object(); + private object _wkfsLock = new(); private bool _wkfsInitialized; private object? _wkfsObject; // unused // shadow support - private readonly List _shadowWrappers = new List(); - private readonly object _shadowLocker = new object(); + private readonly List _shadowWrappers = new(); + private readonly object _shadowLocker = new(); private static string? _shadowCurrentId; // static - unique!! #region Constructor @@ -193,7 +190,7 @@ namespace Umbraco.Cms.Core.IO // to the VirtualPath we get with CodeFileDisplay from the frontend. try { - var rootPath = fileSystem.GetFullPath("/css/"); + fileSystem.GetFullPath("/css/"); } catch (UnauthorizedAccessException exception) { @@ -201,7 +198,8 @@ namespace Umbraco.Cms.Core.IO "Can't register the stylesheet filesystem, " + "this is most likely caused by using a PhysicalFileSystem with an incorrect " + "rootPath/rootUrl. RootPath must be \\wwwroot\\css" - + " and rootUrl must be /css", exception); + + " and rootUrl must be /css", + exception); } _stylesheetsFileSystem = CreateShadowWrapperInternal(fileSystem, "css"); @@ -213,7 +211,7 @@ namespace Umbraco.Cms.Core.IO // but it does not really matter what we return - here, null private object? CreateWellKnownFileSystems() { - var logger = _loggerFactory.CreateLogger(); + ILogger logger = _loggerFactory.CreateLogger(); //TODO this is fucked, why do PhysicalFileSystem has a root url? Mvc views cannot be accessed by url! var macroPartialFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials), _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.MacroPartials)); @@ -228,7 +226,10 @@ namespace Umbraco.Cms.Core.IO if (_stylesheetsFileSystem == null) { - var stylesheetsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, + var stylesheetsFileSystem = new PhysicalFileSystem( + _ioHelper, + _hostingEnvironment, + logger, _hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath)); diff --git a/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs b/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs index a2937f3f8e..3ca1fadbff 100644 --- a/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs +++ b/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs @@ -1,8 +1,6 @@ -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public interface IDefaultViewContentProvider { - public interface IDefaultViewContentProvider - { - string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, - string? modelNamespace = null, string? modelNamespaceAlias = null); - } + string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null); } diff --git a/src/Umbraco.Core/IO/IFileProviderFactory.cs b/src/Umbraco.Core/IO/IFileProviderFactory.cs index 981d5558fc..0e6cb0f0a8 100644 --- a/src/Umbraco.Core/IO/IFileProviderFactory.cs +++ b/src/Umbraco.Core/IO/IFileProviderFactory.cs @@ -1,18 +1,17 @@ using Microsoft.Extensions.FileProviders; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +/// +/// Factory for creating instances. +/// +public interface IFileProviderFactory { /// - /// Factory for creating instances. + /// Creates a new instance. /// - public interface IFileProviderFactory - { - /// - /// Creates a new instance. - /// - /// - /// The newly created instance (or null if not supported). - /// - IFileProvider? Create(); - } + /// + /// The newly created instance (or null if not supported). + /// + IFileProvider? Create(); } diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 54503b167b..da9dd0b9bb 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -1,178 +1,177 @@ -using System; -using System.Collections.Generic; -using System.IO; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +/// +/// Provides methods allowing the manipulation of files. +/// +public interface IFileSystem { /// - /// Provides methods allowing the manipulation of files. + /// Gets a value indicating whether the filesystem can add/copy + /// a file which is on a physical filesystem. /// - public interface IFileSystem - { - /// - /// Gets all directories matching the given path. - /// - /// The path to the directories. - /// - /// The representing the matched directories. - /// - IEnumerable GetDirectories(string path); + /// + /// In other words, whether the filesystem can copy/move a file + /// that is on local disk, in a fast and efficient way. + /// + bool CanAddPhysical { get; } - /// - /// Deletes the specified directory. - /// - /// The name of the directory to remove. - void DeleteDirectory(string path); + /// + /// Gets all directories matching the given path. + /// + /// The path to the directories. + /// + /// The representing the matched directories. + /// + IEnumerable GetDirectories(string path); - /// - /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. - /// - /// Azure blob storage has no real concept of directories so deletion is always recursive. - /// The name of the directory to remove. - /// Whether to remove directories, subdirectories, and files in path. - void DeleteDirectory(string path, bool recursive); + /// + /// Deletes the specified directory. + /// + /// The name of the directory to remove. + void DeleteDirectory(string path); - /// - /// Determines whether the specified directory exists. - /// - /// The directory to check. - /// - /// True if the directory exists and the user has permission to view it; otherwise false. - /// - bool DirectoryExists(string path); + /// + /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. + /// + /// Azure blob storage has no real concept of directories so deletion is always recursive. + /// The name of the directory to remove. + /// Whether to remove directories, subdirectories, and files in path. + void DeleteDirectory(string path, bool recursive); - /// - /// Adds a file to the file system. - /// - /// The path to the given file. - /// The containing the file contents. - void AddFile(string path, Stream stream); + /// + /// Determines whether the specified directory exists. + /// + /// The directory to check. + /// + /// True if the directory exists and the user has permission to view it; otherwise false. + /// + bool DirectoryExists(string path); - /// - /// Adds a file to the file system. - /// - /// The path to the given file. - /// The containing the file contents. - /// Whether to override the file if it already exists. - void AddFile(string path, Stream stream, bool overrideIfExists); + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. + void AddFile(string path, Stream stream); - /// - /// Gets all files matching the given path. - /// - /// The path to the files. - /// - /// The representing the matched files. - /// - IEnumerable GetFiles(string path); + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. + /// Whether to override the file if it already exists. + void AddFile(string path, Stream stream, bool overrideIfExists); - /// - /// Gets all files matching the given path and filter. - /// - /// The path to the files. - /// A filter that allows the querying of file extension. *.jpg - /// - /// The representing the matched files. - /// - IEnumerable GetFiles(string path, string filter); + /// + /// Gets all files matching the given path. + /// + /// The path to the files. + /// + /// The representing the matched files. + /// + IEnumerable GetFiles(string path); - /// - /// Gets a representing the file at the given path. - /// - /// The path to the file. - /// - /// . - /// - Stream OpenFile(string path); + /// + /// Gets all files matching the given path and filter. + /// + /// The path to the files. + /// A filter that allows the querying of file extension. + /// *.jpg + /// + /// + /// The representing the matched files. + /// + IEnumerable GetFiles(string path, string filter); - /// - /// Deletes the specified file. - /// - /// The name of the file to remove. - void DeleteFile(string path); + /// + /// Gets a representing the file at the given path. + /// + /// The path to the file. + /// + /// . + /// + Stream OpenFile(string path); - /// - /// Determines whether the specified file exists. - /// - /// The file to check. - /// - /// True if the file exists and the user has permission to view it; otherwise false. - /// - bool FileExists(string path); + /// + /// Deletes the specified file. + /// + /// The name of the file to remove. + void DeleteFile(string path); - /// - /// Returns the application relative path to the file. - /// - /// The full path or URL. - /// - /// The representing the relative path. - /// - string GetRelativePath(string fullPathOrUrl); + /// + /// Determines whether the specified file exists. + /// + /// The file to check. + /// + /// True if the file exists and the user has permission to view it; otherwise false. + /// + bool FileExists(string path); - /// - /// Gets the full qualified path to the file. - /// - /// The file to return the full path for. - /// - /// The representing the full path. - /// - string GetFullPath(string path); + /// + /// Returns the application relative path to the file. + /// + /// The full path or URL. + /// + /// The representing the relative path. + /// + string GetRelativePath(string fullPathOrUrl); - /// - /// Returns the application relative URL to the file. - /// - /// The path to return the URL for. - /// - /// representing the relative URL. - /// - string GetUrl(string? path); + /// + /// Gets the full qualified path to the file. + /// + /// The file to return the full path for. + /// + /// The representing the full path. + /// + string GetFullPath(string path); - /// - /// Gets the last modified date/time of the file, expressed as a UTC value. - /// - /// The path to the file. - /// - /// . - /// - DateTimeOffset GetLastModified(string path); + /// + /// Returns the application relative URL to the file. + /// + /// The path to return the URL for. + /// + /// representing the relative URL. + /// + string GetUrl(string? path); - /// - /// Gets the created date/time of the file, expressed as a UTC value. - /// - /// The path to the file. - /// - /// . - /// - DateTimeOffset GetCreated(string path); + /// + /// Gets the last modified date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// + DateTimeOffset GetLastModified(string path); - /// - /// Gets the size of a file. - /// - /// The path to the file. - /// The size (in bytes) of the file. - long GetSize(string path); + /// + /// Gets the created date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// + DateTimeOffset GetCreated(string path); - /// - /// Gets a value indicating whether the filesystem can add/copy - /// a file which is on a physical filesystem. - /// - /// In other words, whether the filesystem can copy/move a file - /// that is on local disk, in a fast and efficient way. - bool CanAddPhysical { get; } + /// + /// Gets the size of a file. + /// + /// The path to the file. + /// The size (in bytes) of the file. + long GetSize(string path); - /// - /// Adds a file which is on a physical filesystem. - /// - /// The path to the file. - /// The absolute physical path to the source file. - /// A value indicating what to do if the file already exists. - /// A value indicating whether to move (default) or copy. - void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false); + /// + /// Adds a file which is on a physical filesystem. + /// + /// The path to the file. + /// The absolute physical path to the source file. + /// A value indicating what to do if the file already exists. + /// A value indicating whether to move (default) or copy. + void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false); - // TODO: implement these - // - //void CreateDirectory(string path); - // - //// move or rename, directory or file - //void Move(string source, string target); - } + // TODO: implement these + // + // void CreateDirectory(string path); + // + //// move or rename, directory or file + // void Move(string source, string target); } diff --git a/src/Umbraco.Core/IO/IIOHelper.cs b/src/Umbraco.Core/IO/IIOHelper.cs index 5a814ab386..53376dd48b 100644 --- a/src/Umbraco.Core/IO/IIOHelper.cs +++ b/src/Umbraco.Core/IO/IIOHelper.cs @@ -1,73 +1,68 @@ -using System; -using System.Collections.Generic; -using System.IO; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +public interface IIOHelper { - public interface IIOHelper - { - string FindFile(string virtualPath); + string FindFile(string virtualPath); - [Obsolete("Use IHostingEnvironment.ToAbsolute instead")] - string ResolveUrl(string virtualPath); + [Obsolete("Use IHostingEnvironment.ToAbsolute instead")] + string ResolveUrl(string virtualPath); - /// - /// Maps a virtual path to a physical path in the content root folder (i.e. www) - /// - /// - /// - [Obsolete("Use IHostingEnvironment.MapPathContentRoot or IHostingEnvironment.MapPathWebRoot instead")] - string MapPath(string path); + /// + /// Maps a virtual path to a physical path in the content root folder (i.e. www) + /// + /// + /// + [Obsolete("Use IHostingEnvironment.MapPathContentRoot or IHostingEnvironment.MapPathWebRoot instead")] + string MapPath(string path); - /// - /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directory. - /// A value indicating whether the filepath is valid. - bool VerifyEditPath(string filePath, string validDir); + /// + /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directory. + /// A value indicating whether the filepath is valid. + bool VerifyEditPath(string filePath, string validDir); - /// - /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directories. - /// A value indicating whether the filepath is valid. - bool VerifyEditPath(string filePath, IEnumerable validDirs); + /// + /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directories. + /// A value indicating whether the filepath is valid. + bool VerifyEditPath(string filePath, IEnumerable validDirs); - /// - /// Verifies that the current filepath has one of several authorized extensions. - /// - /// The filepath to validate. - /// The valid extensions. - /// A value indicating whether the filepath is valid. - bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions); + /// + /// Verifies that the current filepath has one of several authorized extensions. + /// + /// The filepath to validate. + /// The valid extensions. + /// A value indicating whether the filepath is valid. + bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions); - bool PathStartsWith(string path, string root, params char[] separators); + bool PathStartsWith(string path, string root, params char[] separators); - void EnsurePathExists(string path); + void EnsurePathExists(string path); - /// - /// Get properly formatted relative path from an existing absolute or relative path - /// - /// - /// - string GetRelativePath(string path); + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + string GetRelativePath(string path); - /// - /// Retrieves array of temporary folders from the hosting environment. - /// - /// Array of instances. - DirectoryInfo[] GetTempFolders(); + /// + /// Retrieves array of temporary folders from the hosting environment. + /// + /// Array of instances. + DirectoryInfo[] GetTempFolders(); - /// - /// Cleans contents of a folder by deleting all files older that the provided age. - /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it can. - /// - /// Folder to clean. - /// Age of files within folder to delete. - /// Result of operation - CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age); - - } + /// + /// Cleans contents of a folder by deleting all files older that the provided age. + /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it + /// can. + /// + /// Folder to clean. + /// Age of files within folder to delete. + /// Result of operation + CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age); } diff --git a/src/Umbraco.Core/IO/IMediaPathScheme.cs b/src/Umbraco.Core/IO/IMediaPathScheme.cs index da9a06d1b1..70ed6c7a3b 100644 --- a/src/Umbraco.Core/IO/IMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/IMediaPathScheme.cs @@ -1,33 +1,29 @@ -using System; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +/// +/// Represents a media file path scheme. +/// +public interface IMediaPathScheme { /// - /// Represents a media file path scheme. + /// Gets a media file path. /// - public interface IMediaPathScheme - { - /// - /// Gets a media file path. - /// - /// The media filesystem. - /// The (content, media) item unique identifier. - /// The property type unique identifier. - /// The file name. - /// - /// The filesystem-relative complete file path. - string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename); + /// The media filesystem. + /// The (content, media) item unique identifier. + /// The property type unique identifier. + /// The file name. + /// The filesystem-relative complete file path. + string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename); - /// - /// Gets the directory that can be deleted when the file is deleted. - /// - /// The media filesystem. - /// The filesystem-relative path of the file. - /// The filesystem-relative path of the directory. - /// - /// The directory, and anything below it, will be deleted. - /// Can return null (or empty) when no directory should be deleted. - /// - string? GetDeleteDirectory(MediaFileManager fileSystem, string filepath); - } + /// + /// Gets the directory that can be deleted when the file is deleted. + /// + /// The media filesystem. + /// The filesystem-relative path of the file. + /// The filesystem-relative path of the directory. + /// + /// The directory, and anything below it, will be deleted. + /// Can return null (or empty) when no directory should be deleted. + /// + string? GetDeleteDirectory(MediaFileManager fileSystem, string filepath); } diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index d0f190868b..cffd2780da 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -1,232 +1,243 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Reflection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public abstract class IOHelper : IIOHelper { - public abstract class IOHelper : IIOHelper + private readonly IHostingEnvironment _hostingEnvironment; + + public IOHelper(IHostingEnvironment hostingEnvironment) => _hostingEnvironment = + hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + + // static compiled regex for faster performance + // private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + // helper to try and match the old path to a new virtual one + public string FindFile(string virtualPath) { - private readonly IHostingEnvironment _hostingEnvironment; + var retval = virtualPath; - public IOHelper(IHostingEnvironment hostingEnvironment) + if (virtualPath.StartsWith("~")) { - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath); } - // static compiled regex for faster performance - //private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - //helper to try and match the old path to a new virtual one - public string FindFile(string virtualPath) + if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath)) { - string retval = virtualPath; - - if (virtualPath.StartsWith("~")) - retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath); - - if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath)) - retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash); - - return retval; + retval = _hostingEnvironment.ApplicationVirtualPath + "/" + + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash); } - // TODO: This is the same as IHostingEnvironment.ToAbsolute - marked as obsolete in IIOHelper for now - public string ResolveUrl(string virtualPath) - { - if (string.IsNullOrWhiteSpace(virtualPath)) return virtualPath; - return _hostingEnvironment.ToAbsolute(virtualPath); + return retval; + } + // TODO: This is the same as IHostingEnvironment.ToAbsolute - marked as obsolete in IIOHelper for now + public string ResolveUrl(string virtualPath) + { + if (string.IsNullOrWhiteSpace(virtualPath)) + { + return virtualPath; } - public string MapPath(string path) - { - if (path == null) throw new ArgumentNullException(nameof(path)); + return _hostingEnvironment.ToAbsolute(virtualPath); + } - // Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1 - if (IsPathFullyQualified(path)) + public string MapPath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1 + if (IsPathFullyQualified(path)) + { + return path; + } + + if (_hostingEnvironment.IsHosted) + { + var result = !string.IsNullOrEmpty(path) && + (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath)) + ? _hostingEnvironment.MapPathWebRoot(path) + : _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash)); + + if (result != null) { - return path; + return result; + } + } + + var dirSepChar = Path.DirectorySeparatorChar; + var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); + var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', dirSepChar); + var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath; + + return retval; + } + + /// + /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directory. + /// A value indicating whether the filepath is valid. + public bool VerifyEditPath(string filePath, string validDir) => VerifyEditPath(filePath, new[] { validDir }); + + /// + /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directories. + /// A value indicating whether the filepath is valid. + public bool VerifyEditPath(string filePath, IEnumerable validDirs) + { + // this is called from ScriptRepository, PartialViewRepository, etc. + // filePath is the fullPath (rooted, filesystem path, can be trusted) + // validDirs are virtual paths (eg ~/Views) + // + // except that for templates, filePath actually is a virtual path + + // TODO: what's below is dirty, there are too many ways to get the root dir, etc. + // not going to fix everything today + var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath); + if (!PathStartsWith(filePath, mappedRoot)) + { + // TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot + filePath = _hostingEnvironment.MapPathWebRoot(filePath); + } + + // yes we can (see above) + //// don't trust what we get, it may contain relative segments + // filePath = Path.GetFullPath(filePath); + foreach (var dir in validDirs) + { + var validDir = dir; + if (!PathStartsWith(validDir, mappedRoot)) + { + validDir = _hostingEnvironment.MapPathWebRoot(validDir); } - if (_hostingEnvironment.IsHosted) + if (PathStartsWith(filePath, validDir)) { - var result = (!string.IsNullOrEmpty(path) && (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath))) - ? _hostingEnvironment.MapPathWebRoot(path) - : _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash)); - - if (result != null) return result; + return true; } - - var dirSepChar = Path.DirectorySeparatorChar; - var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); - var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', dirSepChar); - var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath; - - return retval; } - /// - /// Returns true if the path has a root, and is considered fully qualified for the OS it is on - /// See https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 for the .NET Standard 2.1 version of this - /// - /// The path to check - /// True if the path is fully qualified, false otherwise - public abstract bool IsPathFullyQualified(string path); + return false; + } + /// + /// Verifies that the current filepath has one of several authorized extensions. + /// + /// The filepath to validate. + /// The valid extensions. + /// A value indicating whether the filepath is valid. + public bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions) + { + var ext = Path.GetExtension(filePath); + return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period)); + } - /// - /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directory. - /// A value indicating whether the filepath is valid. - public bool VerifyEditPath(string filePath, string validDir) + public abstract bool PathStartsWith(string path, string root, params char[] separators); + + public void EnsurePathExists(string path) + { + var absolutePath = MapPath(path); + if (Directory.Exists(absolutePath) == false) { - return VerifyEditPath(filePath, new[] { validDir }); - } - - /// - /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directories. - /// A value indicating whether the filepath is valid. - public bool VerifyEditPath(string filePath, IEnumerable validDirs) - { - // this is called from ScriptRepository, PartialViewRepository, etc. - // filePath is the fullPath (rooted, filesystem path, can be trusted) - // validDirs are virtual paths (eg ~/Views) - // - // except that for templates, filePath actually is a virtual path - - // TODO: what's below is dirty, there are too many ways to get the root dir, etc. - // not going to fix everything today - - var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath); - if (!PathStartsWith(filePath, mappedRoot)) - { - // TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot - filePath = _hostingEnvironment.MapPathWebRoot(filePath); - } - - // yes we can (see above) - //// don't trust what we get, it may contain relative segments - //filePath = Path.GetFullPath(filePath); - - foreach (var dir in validDirs) - { - var validDir = dir; - if (!PathStartsWith(validDir, mappedRoot)) - validDir = _hostingEnvironment.MapPathWebRoot(validDir); - - if (PathStartsWith(filePath, validDir)) - return true; - } - - return false; - } - - /// - /// Verifies that the current filepath has one of several authorized extensions. - /// - /// The filepath to validate. - /// The valid extensions. - /// A value indicating whether the filepath is valid. - public bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions) - { - var ext = Path.GetExtension(filePath); - return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period)); - } - - public abstract bool PathStartsWith(string path, string root, params char[] separators); - - public void EnsurePathExists(string path) - { - var absolutePath = MapPath(path); - if (Directory.Exists(absolutePath) == false) - Directory.CreateDirectory(absolutePath); - } - - /// - /// Get properly formatted relative path from an existing absolute or relative path - /// - /// - /// - public string GetRelativePath(string path) - { - if (path.IsFullPath()) - { - var rootDirectory = MapPath("~"); - var relativePath = PathStartsWith(path, rootDirectory) ? path.Substring(rootDirectory.Length) : path; - path = relativePath; - } - - return PathUtility.EnsurePathIsApplicationRootPrefixed(path); - } - - /// - /// Retrieves array of temporary folders from the hosting environment. - /// - /// Array of instances. - public DirectoryInfo[] GetTempFolders() - { - var tempFolderPaths = new[] - { - _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads) - }; - - foreach (var tempFolderPath in tempFolderPaths) - { - // Ensure it exists - Directory.CreateDirectory(tempFolderPath); - } - - return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray(); - } - - /// - /// Cleans contents of a folder by deleting all files older that the provided age. - /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it can. - /// - /// Folder to clean. - /// Age of files within folder to delete. - /// Result of operation. - public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age) - { - folder.Refresh(); // In case it's changed during runtime. - - if (!folder.Exists) - { - return CleanFolderResult.FailedAsDoesNotExist(); - } - - var files = folder.GetFiles("*.*", SearchOption.AllDirectories); - var errors = new List(); - foreach (var file in files) - { - if (DateTime.UtcNow - file.LastWriteTimeUtc > age) - { - try - { - file.IsReadOnly = false; - file.Delete(); - } - catch (Exception ex) - { - errors.Add(new CleanFolderResult.Error(ex, file)); - } - } - } - - return errors.Any() - ? CleanFolderResult.FailedWithErrors(errors) - : CleanFolderResult.Success(); + Directory.CreateDirectory(absolutePath); } } + + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + public string GetRelativePath(string path) + { + if (path.IsFullPath()) + { + var rootDirectory = MapPath("~"); + var relativePath = PathStartsWith(path, rootDirectory) ? path[rootDirectory.Length..] : path; + path = relativePath; + } + + return PathUtility.EnsurePathIsApplicationRootPrefixed(path); + } + + /// + /// Retrieves array of temporary folders from the hosting environment. + /// + /// Array of instances. + public DirectoryInfo[] GetTempFolders() + { + var tempFolderPaths = new[] + { + _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads), + }; + + foreach (var tempFolderPath in tempFolderPaths) + { + // Ensure it exists + Directory.CreateDirectory(tempFolderPath); + } + + return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray(); + } + + /// + /// Cleans contents of a folder by deleting all files older that the provided age. + /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it + /// can. + /// + /// Folder to clean. + /// Age of files within folder to delete. + /// Result of operation. + public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age) + { + folder.Refresh(); // In case it's changed during runtime. + + if (!folder.Exists) + { + return CleanFolderResult.FailedAsDoesNotExist(); + } + + FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); + var errors = new List(); + foreach (FileInfo file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > age) + { + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) + { + errors.Add(new CleanFolderResult.Error(ex, file)); + } + } + } + + return errors.Any() + ? CleanFolderResult.FailedWithErrors(errors) + : CleanFolderResult.Success(); + } + + /// + /// Returns true if the path has a root, and is considered fully qualified for the OS it is on + /// See + /// https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 + /// for the .NET Standard 2.1 version of this + /// + /// The path to check + /// True if the path is fully qualified, false otherwise + public abstract bool IsPathFullyQualified(string path); } diff --git a/src/Umbraco.Core/IO/IOHelperExtensions.cs b/src/Umbraco.Core/IO/IOHelperExtensions.cs index 1625c239ff..7ae90e7f8e 100644 --- a/src/Umbraco.Core/IO/IOHelperExtensions.cs +++ b/src/Umbraco.Core/IO/IOHelperExtensions.cs @@ -1,55 +1,54 @@ -using System; -using System.IO; using Umbraco.Cms.Core.IO; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class IOHelperExtensions { - public static class IOHelperExtensions + /// + /// Will resolve a virtual path URL to an absolute path, else if it is not a virtual path (i.e. starts with ~/) then + /// it will just return the path as-is (relative). + /// + /// + /// + /// + public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path) { - /// - /// Will resolve a virtual path URL to an absolute path, else if it is not a virtual path (i.e. starts with ~/) then - /// it will just return the path as-is (relative). - /// - /// - /// - /// - public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path) + if (string.IsNullOrWhiteSpace(path)) { - if (string.IsNullOrWhiteSpace(path)) return path; - return path.StartsWith("~/") ? ioHelper.ResolveUrl(path) : path; + return path; } - /// - /// Tries to create a directory. - /// - /// The IOHelper. - /// the directory path. - /// true if the directory was created, false otherwise. - public static bool TryCreateDirectory(this IIOHelper ioHelper, string dir) - { - try - { - var dirPath = ioHelper.MapPath(dir); - - if (Directory.Exists(dirPath) == false) - Directory.CreateDirectory(dirPath); - - var filePath = dirPath + "/" + CreateRandomFileName(ioHelper) + ".tmp"; - File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); - File.Delete(filePath); - return true; - } - catch - { - return false; - } - } - - public static string CreateRandomFileName(this IIOHelper ioHelper) - { - return "umbraco-test." + Guid.NewGuid().ToString("N").Substring(0, 8); - } - - + return path.StartsWith("~/") ? ioHelper.ResolveUrl(path) : path; } + + /// + /// Tries to create a directory. + /// + /// The IOHelper. + /// the directory path. + /// true if the directory was created, false otherwise. + public static bool TryCreateDirectory(this IIOHelper ioHelper, string dir) + { + try + { + var dirPath = ioHelper.MapPath(dir); + + if (Directory.Exists(dirPath) == false) + { + Directory.CreateDirectory(dirPath); + } + + var filePath = dirPath + "/" + CreateRandomFileName(ioHelper) + ".tmp"; + File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); + File.Delete(filePath); + return true; + } + catch + { + return false; + } + } + + public static string CreateRandomFileName(this IIOHelper ioHelper) => + "umbraco-test." + Guid.NewGuid().ToString("N").Substring(0, 8); } diff --git a/src/Umbraco.Core/IO/IOHelperLinux.cs b/src/Umbraco.Core/IO/IOHelperLinux.cs index 116a7200b3..7d936895a1 100644 --- a/src/Umbraco.Core/IO/IOHelperLinux.cs +++ b/src/Umbraco.Core/IO/IOHelperLinux.cs @@ -1,28 +1,40 @@ -using System; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class IOHelperLinux : IOHelper { - public class IOHelperLinux : IOHelper + public IOHelperLinux(IHostingEnvironment hostingEnvironment) + : base(hostingEnvironment) { - public IOHelperLinux(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + } + + public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); + + public override bool PathStartsWith(string path, string root, params char[] separators) + { + // either it is identical to root, + // or it is root + separator + anything + if (separators == null || separators.Length == 0) { + separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; } - public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); - - public override bool PathStartsWith(string path, string root, params char[] separators) + if (!path.StartsWith(root, StringComparison.Ordinal)) { - // either it is identical to root, - // or it is root + separator + anything - - if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - if (!path.StartsWith(root, StringComparison.Ordinal)) return false; - if (path.Length == root.Length) return true; - if (path.Length < root.Length) return false; - return separators.Contains(path[root.Length]); + return false; } + + if (path.Length == root.Length) + { + return true; + } + + if (path.Length < root.Length) + { + return false; + } + + return separators.Contains(path[root.Length]); } } diff --git a/src/Umbraco.Core/IO/IOHelperOSX.cs b/src/Umbraco.Core/IO/IOHelperOSX.cs index 53b9cb4dc0..8b8ed20939 100644 --- a/src/Umbraco.Core/IO/IOHelperOSX.cs +++ b/src/Umbraco.Core/IO/IOHelperOSX.cs @@ -1,28 +1,40 @@ -using System; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class IOHelperOSX : IOHelper { - public class IOHelperOSX : IOHelper + public IOHelperOSX(IHostingEnvironment hostingEnvironment) + : base(hostingEnvironment) { - public IOHelperOSX(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + } + + public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); + + public override bool PathStartsWith(string path, string root, params char[] separators) + { + // either it is identical to root, + // or it is root + separator + anything + if (separators == null || separators.Length == 0) { + separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; } - public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); - - public override bool PathStartsWith(string path, string root, params char[] separators) + if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) { - // either it is identical to root, - // or it is root + separator + anything - - if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false; - if (path.Length == root.Length) return true; - if (path.Length < root.Length) return false; - return separators.Contains(path[root.Length]); + return false; } + + if (path.Length == root.Length) + { + return true; + } + + if (path.Length < root.Length) + { + return false; + } + + return separators.Contains(path[root.Length]); } } diff --git a/src/Umbraco.Core/IO/IOHelperWindows.cs b/src/Umbraco.Core/IO/IOHelperWindows.cs index cb60f164dc..9dfec76f36 100644 --- a/src/Umbraco.Core/IO/IOHelperWindows.cs +++ b/src/Umbraco.Core/IO/IOHelperWindows.cs @@ -1,54 +1,67 @@ -using System; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class IOHelperWindows : IOHelper { - public class IOHelperWindows : IOHelper + public IOHelperWindows(IHostingEnvironment hostingEnvironment) + : base(hostingEnvironment) { - public IOHelperWindows(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + } + + public override bool IsPathFullyQualified(string path) + { + // TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1 + if (path.Length < 2) { + // It isn't fixed, it must be relative. There is no way to specify a fixed + // path with one character (or less). + return false; } - public override bool IsPathFullyQualified(string path) + if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar) { - // TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1 - - if (path.Length < 2) - { - // It isn't fixed, it must be relative. There is no way to specify a fixed - // path with one character (or less). - return false; - } - - if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar) - { - // There is no valid way to specify a relative path with two initial slashes or - // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ - return path[1] == '?' || path[1] == Path.DirectorySeparatorChar || path[1] == Path.AltDirectorySeparatorChar; - } - - // The only way to specify a fixed path that doesn't begin with two slashes - // is the drive, colon, slash format- i.e. C:\ - return (path.Length >= 3) - && (path[1] == Path.VolumeSeparatorChar) - && (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar) - // To match old behavior we'll check the drive character for validity as the path is technically - // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. - && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')); + // There is no valid way to specify a relative path with two initial slashes or + // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ + return path[1] == '?' || path[1] == Path.DirectorySeparatorChar || + path[1] == Path.AltDirectorySeparatorChar; } - public override bool PathStartsWith(string path, string root, params char[] separators) - { - // either it is identical to root, - // or it is root + separator + anything + // The only way to specify a fixed path that doesn't begin with two slashes + // is the drive, colon, slash format- i.e. C:\ + return path.Length >= 3 + && path[1] == Path.VolumeSeparatorChar + && (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar) - if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false; - if (path.Length == root.Length) return true; - if (path.Length < root.Length) return false; - return separators.Contains(path[root.Length]); + // To match old behavior we'll check the drive character for validity as the path is technically + // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. + && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')); + } + + public override bool PathStartsWith(string path, string root, params char[] separators) + { + // either it is identical to root, + // or it is root + separator + anything + if (separators == null || separators.Length == 0) + { + separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; } + + if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (path.Length == root.Length) + { + return true; + } + + if (path.Length < root.Length) + { + return false; + } + + return separators.Contains(path[root.Length]); } } diff --git a/src/Umbraco.Core/IO/IViewHelper.cs b/src/Umbraco.Core/IO/IViewHelper.cs index ae6f8698a4..f84a1ba256 100644 --- a/src/Umbraco.Core/IO/IViewHelper.cs +++ b/src/Umbraco.Core/IO/IViewHelper.cs @@ -1,13 +1,16 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public interface IViewHelper { - public interface IViewHelper - { - bool ViewExists(ITemplate t); - string GetFileContents(ITemplate t); - string CreateView(ITemplate t, bool overWrite = false); - string? UpdateViewFile(ITemplate t, string? currentAlias = null); - string ViewPath(string alias); - } + bool ViewExists(ITemplate t); + + string GetFileContents(ITemplate t); + + string CreateView(ITemplate t, bool overWrite = false); + + string? UpdateViewFile(ITemplate t, string? currentAlias = null); + + string ViewPath(string alias); } diff --git a/src/Umbraco.Core/IO/MediaFileManager.cs b/src/Umbraco.Core/IO/MediaFileManager.cs index d5c421721e..c222c01744 100644 --- a/src/Umbraco.Core/IO/MediaFileManager.cs +++ b/src/Umbraco.Core/IO/MediaFileManager.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,237 +7,246 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public sealed class MediaFileManager { - public sealed class MediaFileManager + private readonly ILogger _logger; + private readonly IMediaPathScheme _mediaPathScheme; + private readonly IServiceProvider _serviceProvider; + private readonly IShortStringHelper _shortStringHelper; + private MediaUrlGeneratorCollection? _mediaUrlGenerators; + + public MediaFileManager( + IFileSystem fileSystem, + IMediaPathScheme mediaPathScheme, + ILogger logger, + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider) { - private readonly IMediaPathScheme _mediaPathScheme; - private readonly ILogger _logger; - private readonly IShortStringHelper _shortStringHelper; - private readonly IServiceProvider _serviceProvider; - private MediaUrlGeneratorCollection? _mediaUrlGenerators; - - public MediaFileManager( - IFileSystem fileSystem, - IMediaPathScheme mediaPathScheme, - ILogger logger, - IShortStringHelper shortStringHelper, - IServiceProvider serviceProvider) - { - _mediaPathScheme = mediaPathScheme; - _logger = logger; - _shortStringHelper = shortStringHelper; - _serviceProvider = serviceProvider; - FileSystem = fileSystem; - } - - [Obsolete("Use the ctr that doesn't include unused parameters.")] - public MediaFileManager( - IFileSystem fileSystem, - IMediaPathScheme mediaPathScheme, - ILogger logger, - IShortStringHelper shortStringHelper, - IServiceProvider serviceProvider, - IOptions contentSettings) - : this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider) - { } - - /// - /// Gets the media filesystem. - /// - public IFileSystem FileSystem { get; } - - /// - /// Delete media files. - /// - /// Files to delete (filesystem-relative paths). - public void DeleteMediaFiles(IEnumerable files) - { - files = files.Distinct(); - - // kinda try to keep things under control - var options = new ParallelOptions { MaxDegreeOfParallelism = 20 }; - - Parallel.ForEach(files, options, file => - { - try - { - if (file.IsNullOrWhiteSpace()) - { - return; - } - - if (FileSystem.FileExists(file) == false) - { - return; - } - - FileSystem.DeleteFile(file); - - var directory = _mediaPathScheme.GetDeleteDirectory(this, file); - if (!directory.IsNullOrWhiteSpace()) - { - FileSystem.DeleteDirectory(directory!, true); - } - } - catch (Exception e) - { - _logger.LogError(e, "Failed to delete media file '{File}'.", file); - } - }); - } - - #region Media Path - - /// - /// Gets the file path of a media file. - /// - /// The file name. - /// The unique identifier of the content/media owning the file. - /// The unique identifier of the property type owning the file. - /// The filesystem-relative path to the media file. - /// With the old media path scheme, this CREATES a new media path each time it is invoked. - public string GetMediaPath(string? filename, Guid cuid, Guid puid) - { - filename = Path.GetFileName(filename); - if (filename == null) - { - throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); - } - - filename = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant()); - - return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); - } - - #endregion - - #region Associated Media Files - - /// - /// Returns a stream (file) for a content item (or a null stream if there is no file). - /// - /// - /// The file path if a file was found - /// - /// - /// - public Stream GetFile( - IContentBase content, - out string? mediaFilePath, - string propertyTypeAlias = Constants.Conventions.Media.File, - string? culture = null, - string? segment = null) - { - // TODO: If collections were lazy we could just inject them - if (_mediaUrlGenerators == null) - { - _mediaUrlGenerators = _serviceProvider.GetRequiredService(); - } - - if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment)) - { - return Stream.Null; - } - - return FileSystem.OpenFile(mediaFilePath!); - } - - /// - /// Stores a media file associated to a property of a content item. - /// - /// The content item owning the media file. - /// The property type owning the media file. - /// The media file name. - /// A stream containing the media bytes. - /// An optional filesystem-relative filepath to the previous media file. - /// The filesystem-relative filepath to the media file. - /// - /// The file is considered "owned" by the content/propertyType. - /// If an is provided then that file (and associated thumbnails if any) is deleted - /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file. - /// - public string StoreFile(IContentBase content, IPropertyType? propertyType, string filename, Stream filestream, string? oldpath) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } - - if (filename == null) - { - throw new ArgumentNullException(nameof(filename)); - } - - if (string.IsNullOrWhiteSpace(filename)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(filename)); - } - - if (filestream == null) - { - throw new ArgumentNullException(nameof(filestream)); - } - - // clear the old file, if any - if (string.IsNullOrWhiteSpace(oldpath) == false) - { - FileSystem.DeleteFile(oldpath!); - } - - // get the filepath, store the data - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - FileSystem.AddFile(filepath, filestream); - return filepath; - } - - /// - /// Copies a media file as a new media file, associated to a property of a content item. - /// - /// The content item owning the copy of the media file. - /// The property type owning the copy of the media file. - /// The filesystem-relative path to the source media file. - /// The filesystem-relative path to the copy of the media file. - public string? CopyFile(IContentBase content, IPropertyType propertyType, string sourcepath) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } - - if (sourcepath == null) - { - throw new ArgumentNullException(nameof(sourcepath)); - } - - if (string.IsNullOrWhiteSpace(sourcepath)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(sourcepath)); - } - - // ensure we have a file to copy - if (FileSystem.FileExists(sourcepath) == false) - { - return null; - } - - // get the filepath - var filename = Path.GetFileName(sourcepath); - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - FileSystem.CopyFile(sourcepath, filepath); - return filepath; - } - - #endregion + _mediaPathScheme = mediaPathScheme; + _logger = logger; + _shortStringHelper = shortStringHelper; + _serviceProvider = serviceProvider; + FileSystem = fileSystem; } + + [Obsolete("Use the ctr that doesn't include unused parameters.")] + public MediaFileManager( + IFileSystem fileSystem, + IMediaPathScheme mediaPathScheme, + ILogger logger, + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider, + IOptions contentSettings) + : this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider) + { + } + + /// + /// Gets the media filesystem. + /// + public IFileSystem FileSystem { get; } + + /// + /// Delete media files. + /// + /// Files to delete (filesystem-relative paths). + public void DeleteMediaFiles(IEnumerable files) + { + files = files.Distinct(); + + // kinda try to keep things under control + var options = new ParallelOptions { MaxDegreeOfParallelism = 20 }; + + Parallel.ForEach(files, options, file => + { + try + { + if (file.IsNullOrWhiteSpace()) + { + return; + } + + if (FileSystem.FileExists(file) == false) + { + return; + } + + FileSystem.DeleteFile(file); + + var directory = _mediaPathScheme.GetDeleteDirectory(this, file); + if (!directory.IsNullOrWhiteSpace()) + { + FileSystem.DeleteDirectory(directory!, true); + } + } + catch (Exception e) + { + _logger.LogError(e, "Failed to delete media file '{File}'.", file); + } + }); + } + + #region Media Path + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// With the old media path scheme, this CREATES a new media path each time it is invoked. + public string GetMediaPath(string? filename, Guid cuid, Guid puid) + { + filename = Path.GetFileName(filename); + if (filename == null) + { + throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); + } + + filename = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant()); + + return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); + } + + #endregion + + #region Associated Media Files + + /// + /// Returns a stream (file) for a content item (or a null stream if there is no file). + /// + /// + /// The file path if a file was found + /// + /// + /// + /// + /// + public Stream GetFile( + IContentBase content, + out string? mediaFilePath, + string propertyTypeAlias = Constants.Conventions.Media.File, + string? culture = null, + string? segment = null) + { + // TODO: If collections were lazy we could just inject them + if (_mediaUrlGenerators == null) + { + _mediaUrlGenerators = _serviceProvider.GetRequiredService(); + } + + if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment)) + { + return Stream.Null; + } + + return FileSystem.OpenFile(mediaFilePath!); + } + + /// + /// Stores a media file associated to a property of a content item. + /// + /// The content item owning the media file. + /// The property type owning the media file. + /// The media file name. + /// A stream containing the media bytes. + /// An optional filesystem-relative filepath to the previous media file. + /// The filesystem-relative filepath to the media file. + /// + /// The file is considered "owned" by the content/propertyType. + /// + /// If an is provided then that file (and associated thumbnails if any) is deleted + /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new + /// file. + /// + /// + public string StoreFile(IContentBase content, IPropertyType? propertyType, string filename, Stream filestream, string? oldpath) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (propertyType == null) + { + throw new ArgumentNullException(nameof(propertyType)); + } + + if (filename == null) + { + throw new ArgumentNullException(nameof(filename)); + } + + if (string.IsNullOrWhiteSpace(filename)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(filename)); + } + + if (filestream == null) + { + throw new ArgumentNullException(nameof(filestream)); + } + + // clear the old file, if any + if (string.IsNullOrWhiteSpace(oldpath) == false) + { + FileSystem.DeleteFile(oldpath); + } + + // get the filepath, store the data + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + FileSystem.AddFile(filepath, filestream); + return filepath; + } + + /// + /// Copies a media file as a new media file, associated to a property of a content item. + /// + /// The content item owning the copy of the media file. + /// The property type owning the copy of the media file. + /// The filesystem-relative path to the source media file. + /// The filesystem-relative path to the copy of the media file. + public string? CopyFile(IContentBase content, IPropertyType propertyType, string sourcepath) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (propertyType == null) + { + throw new ArgumentNullException(nameof(propertyType)); + } + + if (sourcepath == null) + { + throw new ArgumentNullException(nameof(sourcepath)); + } + + if (string.IsNullOrWhiteSpace(sourcepath)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(sourcepath)); + } + + // ensure we have a file to copy + if (FileSystem.FileExists(sourcepath) == false) + { + return null; + } + + // get the filepath + var filename = Path.GetFileName(sourcepath); + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + FileSystem.CopyFile(sourcepath, filepath); + return filepath; + } + + #endregion } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs index 5adc81276b..b73d29df60 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs @@ -1,28 +1,25 @@ -using System; -using System.IO; +namespace Umbraco.Cms.Core.IO.MediaPathSchemes; -namespace Umbraco.Cms.Core.IO.MediaPathSchemes +/// +/// Implements a combined-guids media path scheme. +/// +/// +/// Path is "{combinedGuid}/{filename}" where combinedGuid is a combination of itemGuid and propertyGuid. +/// +public class CombinedGuidsMediaPathScheme : IMediaPathScheme { - /// - /// Implements a combined-guids media path scheme. - /// - /// - /// Path is "{combinedGuid}/{filename}" where combinedGuid is a combination of itemGuid and propertyGuid. - /// - public class CombinedGuidsMediaPathScheme : IMediaPathScheme + /// + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) { - /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) - { - // assumes that cuid and puid keys can be trusted - and that a single property type - // for a single content cannot store two different files with the same name - - var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); - var directory = HexEncoder.Encode(combinedGuid.ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... - return Path.Combine(directory, filename).Replace('\\', '/'); - } - - /// - public string GetDeleteDirectory(MediaFileManager fileSystem, string filepath) => Path.GetDirectoryName(filepath)!; + // assumes that cuid and puid keys can be trusted - and that a single property type + // for a single content cannot store two different files with the same name + Guid combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); + var directory = + HexEncoder.Encode( + combinedGuid.ToByteArray() /*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + return Path.Combine(directory, filename).Replace('\\', '/'); } + + /// + public string GetDeleteDirectory(MediaFileManager fileSystem, string filepath) => Path.GetDirectoryName(filepath)!; } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs index 1ee821e3ed..a533a62c92 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs @@ -1,26 +1,17 @@ -using System; -using System.IO; +namespace Umbraco.Cms.Core.IO.MediaPathSchemes; -namespace Umbraco.Cms.Core.IO.MediaPathSchemes +/// +/// Implements a two-guids media path scheme. +/// +/// +/// Path is "{itemGuid}/{propertyGuid}/{filename}". +/// +public class TwoGuidsMediaPathScheme : IMediaPathScheme { - /// - /// Implements a two-guids media path scheme. - /// - /// - /// Path is "{itemGuid}/{propertyGuid}/{filename}". - /// - public class TwoGuidsMediaPathScheme : IMediaPathScheme - { - /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) - { - return Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/'); - } + /// + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) => + Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/'); - /// - public string GetDeleteDirectory(MediaFileManager fileManager, string filepath) - { - return Path.GetDirectoryName(filepath)!; - } - } + /// + public string GetDeleteDirectory(MediaFileManager fileManager, string filepath) => Path.GetDirectoryName(filepath)!; } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs index a3fe36bde9..7b7061506d 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs @@ -1,37 +1,37 @@ -using System; -using System.IO; +namespace Umbraco.Cms.Core.IO.MediaPathSchemes; -namespace Umbraco.Cms.Core.IO.MediaPathSchemes +/// +/// Implements a unique directory media path scheme. +/// +/// +/// This scheme provides deterministic short paths, with potential collisions. +/// +public class UniqueMediaPathScheme : IMediaPathScheme { - /// - /// Implements a unique directory media path scheme. - /// - /// - /// This scheme provides deterministic short paths, with potential collisions. - /// - public class UniqueMediaPathScheme : IMediaPathScheme + private const int DirectoryLength = 8; + + /// + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) { - private const int DirectoryLength = 8; + Guid combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); + var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); - /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) - { - var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); - var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); - - return Path.Combine(directory, filename).Replace('\\', '/'); - } - - /// - /// - /// Returning null so that does *not* - /// delete any directory. This is because the above shortening of the Guid to 8 chars - /// means we're increasing the risk of collision, and we don't want to delete files - /// belonging to other media items. - /// And, at the moment, we cannot delete directory "only if it is empty" because of - /// race conditions. We'd need to implement locks in for - /// this. - /// - public string? GetDeleteDirectory(MediaFileManager fileManager, string filepath) => null; + return Path.Combine(directory, filename).Replace('\\', '/'); } + + /// + /// + /// + /// Returning null so that does *not* + /// delete any directory. This is because the above shortening of the Guid to 8 chars + /// means we're increasing the risk of collision, and we don't want to delete files + /// belonging to other media items. + /// + /// + /// And, at the moment, we cannot delete directory "only if it is empty" because of + /// race conditions. We'd need to implement locks in for + /// this. + /// + /// + public string? GetDeleteDirectory(MediaFileManager fileManager, string filepath) => null; } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 30d1893792..ede481b833 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; @@ -36,11 +31,30 @@ namespace Umbraco.Cms.Core.IO _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - if (rootPath == null) throw new ArgumentNullException(nameof(rootPath)); - if (string.IsNullOrEmpty(rootPath)) throw new ArgumentException("Value can't be empty.", nameof(rootPath)); - if (rootUrl == null) throw new ArgumentNullException(nameof(rootUrl)); - if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentException("Value can't be empty.", nameof(rootUrl)); - if (rootPath.StartsWith("~/")) throw new ArgumentException("Value can't be a virtual path and start with '~/'.", nameof(rootPath)); + if (rootPath == null) + { + throw new ArgumentNullException(nameof(rootPath)); + } + + if (string.IsNullOrEmpty(rootPath)) + { + throw new ArgumentException("Value can't be empty.", nameof(rootPath)); + } + + if (rootUrl == null) + { + throw new ArgumentNullException(nameof(rootUrl)); + } + + if (string.IsNullOrEmpty(rootUrl)) + { + throw new ArgumentException("Value can't be empty.", nameof(rootUrl)); + } + + if (rootPath.StartsWith("~/")) + { + throw new ArgumentException("Value can't be a virtual path and start with '~/'.", nameof(rootPath)); + } // rootPath should be... rooted, as in, it's a root path! if (Path.IsPathRooted(rootPath) == false) @@ -71,7 +85,9 @@ namespace Umbraco.Cms.Core.IO try { if (Directory.Exists(fullPath)) + { return Directory.EnumerateDirectories(fullPath).Select(GetRelativePath); + } } catch (UnauthorizedAccessException ex) { @@ -103,7 +119,9 @@ namespace Umbraco.Cms.Core.IO { var fullPath = GetFullPath(path); if (Directory.Exists(fullPath) == false) + { return; + } try { @@ -154,7 +172,11 @@ namespace Umbraco.Cms.Core.IO } var directory = Path.GetDirectoryName(fullPath); - if (directory == null) throw new InvalidOperationException("Could not get directory."); + if (directory == null) + { + throw new InvalidOperationException("Could not get directory."); + } + Directory.CreateDirectory(directory); // ensure it exists if (stream.CanSeek) @@ -191,7 +213,9 @@ namespace Umbraco.Cms.Core.IO try { if (Directory.Exists(fullPath)) + { return Directory.EnumerateFiles(fullPath, filter).Select(GetRelativePath); + } } catch (UnauthorizedAccessException ex) { @@ -224,7 +248,9 @@ namespace Umbraco.Cms.Core.IO { var fullPath = GetFullPath(path); if (File.Exists(fullPath) == false) + { return; + } try { @@ -265,12 +291,16 @@ namespace Umbraco.Cms.Core.IO // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" // or on unix systems "/var/wwwroot/test/Meia/1234/img.jpg" if (_ioHelper.PathStartsWith(path, _rootPathFwd, '/')) + { return path.Substring(_rootPathFwd.Length).TrimStart(Constants.CharArrays.ForwardSlash); + } // if it starts with the root URL, strip it and trim the starting slash to make it relative // eg "/Media/1234/img.jpg" => "1234/img.jpg" if (_ioHelper.PathStartsWith(path, _rootUrl, '/')) + { return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash); + } // unchanged - what else? return path.TrimStart(Constants.CharArrays.ForwardSlash); @@ -296,11 +326,15 @@ namespace Umbraco.Cms.Core.IO // we assume it's not a FS relative path and we try to convert it... but it // really makes little sense? if (path.StartsWith(Path.DirectorySeparatorChar.ToString())) + { path = GetRelativePath(path); + } // if not already rooted, combine with the root path if (_ioHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar) == false) + { path = Path.Combine(_rootPath, path); + } // sanitize - GetFullPath will take care of any relative // segments in path, eg '../../foo.tmp' - it may throw a SecurityException @@ -315,7 +349,10 @@ namespace Umbraco.Cms.Core.IO // this says that 4.7.2 supports long paths - but Windows does not // https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2 if (path.Length > 260) + { throw new PathTooLongException($"Path {path} is too long."); + } + return path; } @@ -384,18 +421,29 @@ namespace Umbraco.Cms.Core.IO if (File.Exists(fullPath)) { if (overrideIfExists == false) + { throw new InvalidOperationException($"A file at path '{path}' already exists"); + } + WithRetry(() => File.Delete(fullPath)); } var directory = Path.GetDirectoryName(fullPath); - if (directory == null) throw new InvalidOperationException("Could not get directory."); + if (directory == null) + { + throw new InvalidOperationException("Could not get directory."); + } + Directory.CreateDirectory(directory); // ensure it exists if (copy) + { WithRetry(() => File.Copy(physicalPath, fullPath)); + } else + { WithRetry(() => File.Move(physicalPath, fullPath)); + } } #region Helper Methods @@ -442,11 +490,17 @@ namespace Umbraco.Cms.Core.IO // if it's not *exactly* IOException then it could be // some inherited exception such as FileNotFoundException, // and then we don't want to retry - if (e.GetType() != typeof(IOException)) throw; + if (e.GetType() != typeof(IOException)) + { + throw; + } // if we have tried enough, throw, else swallow // the exception and retry after a pause - if (i == count) throw; + if (i == count) + { + throw; + } } Thread.Sleep(pausems); diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index bd3f9d770d..95517f8054 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -1,386 +1,473 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +internal class ShadowFileSystem : IFileSystem { - internal class ShadowFileSystem : IFileSystem + private readonly IFileSystem _sfs; + + private Dictionary? _nodes; + + public ShadowFileSystem(IFileSystem fs, IFileSystem sfs) { - private readonly IFileSystem _fs; - private readonly IFileSystem _sfs; + Inner = fs; + _sfs = sfs; + } - public ShadowFileSystem(IFileSystem fs, IFileSystem sfs) + public IFileSystem Inner { get; } + + public bool CanAddPhysical => true; + + private Dictionary Nodes => _nodes ??= new Dictionary(); + + public IEnumerable GetDirectories(string path) + { + var normPath = NormPath(path); + KeyValuePair[] shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + IEnumerable directories = Inner.GetDirectories(path); + return directories + .Except(shadows + .Where(kvp => (kvp.Value.IsDir && kvp.Value.IsDelete) || (kvp.Value.IsFile && kvp.Value.IsExist)) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsDir && kvp.Value.IsExist).Select(kvp => kvp.Key)) + .Distinct(); + } + + public void DeleteDirectory(string path) => DeleteDirectory(path, false); + + public void DeleteDirectory(string path, bool recursive) + { + if (DirectoryExists(path) == false) { - _fs = fs; - _sfs = sfs; + return; } - public IFileSystem Inner => _fs; - - public void Complete() + var normPath = NormPath(path); + if (recursive) { - if (_nodes == null) return; - var exceptions = new List(); - foreach (var kvp in _nodes) + Nodes[normPath] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); + foreach (KeyValuePair kvp in remove) { - if (kvp.Value.IsExist) - { - if (kvp.Value.IsFile) - { - try - { - if (_fs.CanAddPhysical) - { - _fs.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move - } - else - { - using (Stream stream = _sfs.OpenFile(kvp.Key)) - { - _fs.AddFile(kvp.Key, stream, true); - } - } - } - catch (Exception e) - { - exceptions.Add(new Exception("Could not save file \"" + kvp.Key + "\".", e)); - } - } - } - else - { - try - { - if (kvp.Value.IsDir) - _fs.DeleteDirectory(kvp.Key, true); - else - _fs.DeleteFile(kvp.Key); - } - catch (Exception e) - { - exceptions.Add(new Exception("Could not delete " + (kvp.Value.IsDir ? "directory": "file") + " \"" + kvp.Key + "\".", e)); - } - } - } - _nodes.Clear(); - - if (exceptions.Count == 0) return; - throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); - } - - private Dictionary? _nodes; - - private Dictionary Nodes => _nodes ?? (_nodes = new Dictionary()); - - private class ShadowNode - { - public ShadowNode(bool isDelete, bool isdir) - { - IsDelete = isDelete; - IsDir = isdir; + Nodes.Remove(kvp.Key); } - public bool IsDelete { get; } - public bool IsDir { get; } - - public bool IsExist => IsDelete == false; - public bool IsFile => IsDir == false; + Delete(path, true); } - - private static string NormPath(string path) + else { - return path.ToLowerInvariant().Replace("\\", "/"); - } - - // values can be "" (root), "foo", "foo/bar"... - private static bool IsChild(string path, string input) - { - if (input.StartsWith(path) == false || input.Length < path.Length + 2) - return false; - if (path.Length > 0 && input[path.Length] != '/') return false; - var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase); - return pos < 0; - } - - private static bool IsDescendant(string path, string input) - { - if (input.StartsWith(path) == false || input.Length < path.Length + 2) - return false; - return path.Length == 0 || input[path.Length] == '/'; - } - - public IEnumerable GetDirectories(string path) - { - var normPath = NormPath(path); - var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); - var directories = _fs.GetDirectories(path); - return directories - .Except(shadows.Where(kvp => (kvp.Value.IsDir && kvp.Value.IsDelete) || (kvp.Value.IsFile && kvp.Value.IsExist)) - .Select(kvp => kvp.Key)) - .Union(shadows.Where(kvp => kvp.Value.IsDir && kvp.Value.IsExist).Select(kvp => kvp.Key)) - .Distinct(); - } - - public void DeleteDirectory(string path) - { - DeleteDirectory(path, false); - } - - public void DeleteDirectory(string path, bool recursive) - { - if (DirectoryExists(path) == false) return; - var normPath = NormPath(path); - if (recursive) + // actual content + if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content + || Inner.GetDirectories(path).Any() || Inner.GetFiles(path).Any()) { - Nodes[normPath] = new ShadowNode(true, true); - var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); - foreach (var kvp in remove) Nodes.Remove(kvp.Key); - Delete(path, true); + throw new InvalidOperationException("Directory is not empty."); + } + + Nodes[path] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList(); + foreach (KeyValuePair kvp in remove) + { + Nodes.Remove(kvp.Key); + } + + Delete(path, false); + } + } + + public bool DirectoryExists(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsDir && sf.IsExist; + } + + return Inner.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) => AddFile(path, stream, true); + + public void AddFile(string path, Stream stream, bool overrideIfExists) + { + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out ShadowNode? sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + { + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + } + + var parts = normPath.Split(Constants.CharArrays.ForwardSlash); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + if (Nodes.TryGetValue(dirPath, out ShadowNode? sd)) + { + if (sd.IsFile) + { + throw new InvalidOperationException("Invalid path."); + } + + if (sd.IsDelete) + { + Nodes[dirPath] = new ShadowNode(false, true); + } } else { - if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content - || _fs.GetDirectories(path).Any() || _fs.GetFiles(path).Any()) // actual content - throw new InvalidOperationException("Directory is not empty."); - Nodes[path] = new ShadowNode(true, true); - var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList(); - foreach (var kvp in remove) Nodes.Remove(kvp.Key); - Delete(path, false); - } - } - - private void Delete(string path, bool recurse) - { - foreach (var file in _fs.GetFiles(path)) - { - Nodes[NormPath(file)] = new ShadowNode(true, false); - } - foreach (var dir in _fs.GetDirectories(path)) - { - Nodes[NormPath(dir)] = new ShadowNode(true, true); - if (recurse) Delete(dir, true); - } - } - - public bool DirectoryExists(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsDir && sf.IsExist; - return _fs.DirectoryExists(path); - } - - public void AddFile(string path, Stream stream) - { - AddFile(path, stream, true); - } - - public void AddFile(string path, Stream stream, bool overrideIfExists) - { - ShadowNode? sf; - var normPath = NormPath(path); - if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) - throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - - var parts = normPath.Split(Constants.CharArrays.ForwardSlash); - for (var i = 0; i < parts.Length - 1; i++) - { - var dirPath = string.Join("/", parts.Take(i + 1)); - ShadowNode? sd; - if (Nodes.TryGetValue(dirPath, out sd)) + if (Inner.DirectoryExists(dirPath)) { - if (sd.IsFile) throw new InvalidOperationException("Invalid path."); - if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + continue; } - else + + if (Inner.FileExists(dirPath)) + { + throw new InvalidOperationException("Invalid path."); + } + + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, stream, overrideIfExists); + Nodes[normPath] = new ShadowNode(false, false); + } + + public IEnumerable GetFiles(string path) => GetFiles(path, null); + + public IEnumerable GetFiles(string path, string? filter) + { + var normPath = NormPath(path); + KeyValuePair[] shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + IEnumerable files = filter != null ? Inner.GetFiles(path, filter) : Inner.GetFiles(path); + WildcardExpression? wildcard = filter == null ? null : new WildcardExpression(filter); + return files + .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) + .Select(kvp => kvp.Key)) + .Union(shadows + .Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && (wildcard == null || wildcard.IsMatch(kvp.Key))) + .Select(kvp => kvp.Key)) + .Distinct(); + } + + public Stream OpenFile(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path); + } + + return Inner.OpenFile(path); + } + + public void DeleteFile(string path) + { + if (FileExists(path) == false) + { + return; + } + + Nodes[NormPath(path)] = new ShadowNode(true, false); + } + + public bool FileExists(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsFile && sf.IsExist; + } + + return Inner.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) => Inner.GetRelativePath(fullPathOrUrl); + + public string GetFullPath(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path); + } + + return Inner.GetFullPath(path); + } + + public string GetUrl(string? path) => Inner.GetUrl(path); + + public DateTimeOffset GetLastModified(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) + { + return Inner.GetLastModified(path); + } + + if (sf.IsDelete) + { + throw new InvalidOperationException("Invalid path."); + } + + return _sfs.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) + { + return Inner.GetCreated(path); + } + + if (sf.IsDelete) + { + throw new InvalidOperationException("Invalid path."); + } + + return _sfs.GetCreated(path); + } + + public long GetSize(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) + { + return Inner.GetSize(path); + } + + if (sf.IsDelete || sf.IsDir) + { + throw new InvalidOperationException("Invalid path."); + } + + return _sfs.GetSize(path); + } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out ShadowNode? sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + { + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + } + + var parts = normPath.Split(Constants.CharArrays.ForwardSlash); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + if (Nodes.TryGetValue(dirPath, out ShadowNode? sd)) + { + if (sd.IsFile) + { + throw new InvalidOperationException("Invalid path."); + } + + if (sd.IsDelete) { - if (_fs.DirectoryExists(dirPath)) continue; - if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); Nodes[dirPath] = new ShadowNode(false, true); } } - - _sfs.AddFile(path, stream, overrideIfExists); - Nodes[normPath] = new ShadowNode(false, false); - } - - public IEnumerable GetFiles(string path) - { - return GetFiles(path, null); - } - - public IEnumerable GetFiles(string path, string? filter) - { - var normPath = NormPath(path); - var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); - var files = filter != null ? _fs.GetFiles(path, filter) : _fs.GetFiles(path); - var wildcard = filter == null ? null : new WildcardExpression(filter); - return files - .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) - .Select(kvp => kvp.Key)) - .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && (wildcard == null || wildcard.IsMatch(kvp.Key))).Select(kvp => kvp.Key)) - .Distinct(); - } - - public Stream OpenFile(string path) - { - if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + else { - return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path); - } - - return _fs.OpenFile(path); - } - - public void DeleteFile(string path) - { - if (FileExists(path) == false) return; - Nodes[NormPath(path)] = new ShadowNode(true, false); - } - - public bool FileExists(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsFile && sf.IsExist; - return _fs.FileExists(path); - } - - public string GetRelativePath(string fullPathOrUrl) - { - return _fs.GetRelativePath(fullPathOrUrl); - } - - public string GetFullPath(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path); - return _fs.GetFullPath(path); - } - - public string GetUrl(string? path) - { - return _fs.GetUrl(path); - } - - public DateTimeOffset GetLastModified(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetLastModified(path); - if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); - return _sfs.GetLastModified(path); - } - - public DateTimeOffset GetCreated(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetCreated(path); - if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); - return _sfs.GetCreated(path); - } - - public long GetSize(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf) == false) - return _fs.GetSize(path); - - if (sf.IsDelete || sf.IsDir) throw new InvalidOperationException("Invalid path."); - return _sfs.GetSize(path); - } - - public bool CanAddPhysical { get { return true; } } - - public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) - { - ShadowNode? sf; - var normPath = NormPath(path); - if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) - throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - - var parts = normPath.Split(Constants.CharArrays.ForwardSlash); - for (var i = 0; i < parts.Length - 1; i++) - { - var dirPath = string.Join("/", parts.Take(i + 1)); - ShadowNode? sd; - if (Nodes.TryGetValue(dirPath, out sd)) + if (Inner.DirectoryExists(dirPath)) { - if (sd.IsFile) throw new InvalidOperationException("Invalid path."); - if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + continue; } - else + + if (Inner.FileExists(dirPath)) { - if (_fs.DirectoryExists(dirPath)) continue; - if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); - Nodes[dirPath] = new ShadowNode(false, true); + throw new InvalidOperationException("Invalid path."); + } + + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, physicalPath, overrideIfExists, copy); + Nodes[normPath] = new ShadowNode(false, false); + } + + public void Complete() + { + if (_nodes == null) + { + return; + } + + var exceptions = new List(); + foreach (KeyValuePair kvp in _nodes) + { + if (kvp.Value.IsExist) + { + if (kvp.Value.IsFile) + { + try + { + if (Inner.CanAddPhysical) + { + Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move + } + else + { + using (Stream stream = _sfs.OpenFile(kvp.Key)) + { + Inner.AddFile(kvp.Key, stream, true); + } + } + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not save file \"" + kvp.Key + "\".", e)); + } + } + } + else + { + try + { + if (kvp.Value.IsDir) + { + Inner.DeleteDirectory(kvp.Key, true); + } + else + { + Inner.DeleteFile(kvp.Key); + } + } + catch (Exception e) + { + exceptions.Add(new Exception( + "Could not delete " + (kvp.Value.IsDir ? "directory" : "file") + " \"" + kvp.Key + "\".", e)); } } - - _sfs.AddFile(path, physicalPath, overrideIfExists, copy); - Nodes[normPath] = new ShadowNode(false, false); } - // copied from System.Web.Util.Wildcard internal - internal class WildcardExpression + _nodes.Clear(); + + if (exceptions.Count == 0) { - private readonly string _pattern; - private readonly bool _caseInsensitive; - private Regex? _regex; + return; + } - private static Regex metaRegex = new Regex("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]"); - private static Regex questRegex = new Regex("\\?"); - private static Regex starRegex = new Regex("\\*"); - private static Regex commaRegex = new Regex(","); - private static Regex slashRegex = new Regex("(?=/)"); - private static Regex backslashRegex = new Regex("(?=[\\\\:])"); + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + } - public WildcardExpression(string pattern, bool caseInsensitive = true) + private static string NormPath(string path) => path.ToLowerInvariant().Replace("\\", "/"); + + // values can be "" (root), "foo", "foo/bar"... + private static bool IsChild(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + { + return false; + } + + if (path.Length > 0 && input[path.Length] != '/') + { + return false; + } + + var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase); + return pos < 0; + } + + private static bool IsDescendant(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + { + return false; + } + + return path.Length == 0 || input[path.Length] == '/'; + } + + private void Delete(string path, bool recurse) + { + foreach (var file in Inner.GetFiles(path)) + { + Nodes[NormPath(file)] = new ShadowNode(true, false); + } + + foreach (var dir in Inner.GetDirectories(path)) + { + Nodes[NormPath(dir)] = new ShadowNode(true, true); + if (recurse) { - _pattern = pattern; - _caseInsensitive = caseInsensitive; - } - - private void EnsureRegex(string pattern) - { - if (_regex != null) return; - - var options = RegexOptions.None; - - // match right-to-left (for speed) if the pattern starts with a * - - if (pattern.Length > 0 && pattern[0] == '*') - options = RegexOptions.RightToLeft | RegexOptions.Singleline; - else - options = RegexOptions.Singleline; - - // case insensitivity - - if (_caseInsensitive) - options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; - - // Remove regex metacharacters - - pattern = metaRegex.Replace(pattern, "\\$0"); - - // Replace wildcard metacharacters with regex codes - - pattern = questRegex.Replace(pattern, "."); - pattern = starRegex.Replace(pattern, ".*"); - pattern = commaRegex.Replace(pattern, "\\z|\\A"); - - // anchor the pattern at beginning and end, and return the regex - - _regex = new Regex("\\A" + pattern + "\\z", options); - } - - public bool IsMatch(string input) - { - EnsureRegex(_pattern); - return _regex?.IsMatch(input) ?? false; + Delete(dir, true); } } } + + // copied from System.Web.Util.Wildcard internal + internal class WildcardExpression + { + private static readonly Regex MetaRegex = new("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]"); + private static readonly Regex QuestRegex = new("\\?"); + private static readonly Regex StarRegex = new("\\*"); + private static readonly Regex CommaRegex = new(","); + private static readonly Regex SlashRegex = new("(?=/)"); + private static readonly Regex BackslashRegex = new("(?=[\\\\:])"); + private readonly bool _caseInsensitive; + private readonly string _pattern; + private Regex? _regex; + + public WildcardExpression(string pattern, bool caseInsensitive = true) + { + _pattern = pattern; + _caseInsensitive = caseInsensitive; + } + + public bool IsMatch(string input) + { + EnsureRegex(_pattern); + return _regex?.IsMatch(input) ?? false; + } + + private void EnsureRegex(string pattern) + { + if (_regex != null) + { + return; + } + + RegexOptions options = RegexOptions.None; + + // match right-to-left (for speed) if the pattern starts with a * + if (pattern.Length > 0 && pattern[0] == '*') + { + options = RegexOptions.RightToLeft | RegexOptions.Singleline; + } + else + { + options = RegexOptions.Singleline; + } + + // case insensitivity + if (_caseInsensitive) + { + options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + } + + // Remove regex metacharacters + pattern = MetaRegex.Replace(pattern, "\\$0"); + + // Replace wildcard metacharacters with regex codes + pattern = QuestRegex.Replace(pattern, "."); + pattern = StarRegex.Replace(pattern, ".*"); + pattern = CommaRegex.Replace(pattern, "\\z|\\A"); + + // anchor the pattern at beginning and end, and return the regex + _regex = new Regex("\\A" + pattern + "\\z", options); + } + } + + private class ShadowNode + { + public ShadowNode(bool isDelete, bool isdir) + { + IsDelete = isDelete; + IsDir = isdir; + } + + public bool IsDelete { get; } + + public bool IsDir { get; } + + public bool IsExist => IsDelete == false; + + public bool IsFile => IsDir == false; + } } diff --git a/src/Umbraco.Core/IO/ShadowFileSystems.cs b/src/Umbraco.Core/IO/ShadowFileSystems.cs index 413cc73d8a..3d69875dc4 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystems.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystems.cs @@ -1,34 +1,26 @@ -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +// shadow filesystems is definitively ... too convoluted +internal class ShadowFileSystems : ICompletable { - // shadow filesystems is definitively ... too convoluted + private readonly FileSystems _fileSystems; + private bool _completed; - internal class ShadowFileSystems : ICompletable + // invoked by the filesystems when shadowing + public ShadowFileSystems(FileSystems fileSystems, string id) { - private readonly FileSystems _fileSystems; - private bool _completed; + _fileSystems = fileSystems; + Id = id; - // invoked by the filesystems when shadowing - public ShadowFileSystems(FileSystems fileSystems, string id) - { - _fileSystems = fileSystems; - Id = id; - - _fileSystems.BeginShadow(id); - } - - // for tests - public string Id { get; } - - // invoked by the scope when exiting, if completed - public void Complete() - { - _completed = true; - } - - // invoked by the scope when exiting - public void Dispose() - { - _fileSystems.EndShadow(Id, _completed); - } + _fileSystems.BeginShadow(id); } + + // for tests + public string Id { get; } + + // invoked by the scope when exiting, if completed + public void Complete() => _completed = true; + + // invoked by the scope when exiting + public void Dispose() => _fileSystems.EndShadow(Id, _completed); } diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 67808e1fdb..2f2e0a991c 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -1,233 +1,186 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +internal class ShadowWrapper : IFileSystem, IFileProviderFactory { - internal class ShadowWrapper : IFileSystem, IFileProviderFactory + private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; + + private readonly Func? _isScoped; + private readonly ILoggerFactory _loggerFactory; + private readonly string _shadowPath; + private string? _shadowDir; + private ShadowFileSystem? _shadowFileSystem; + + public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func? isScoped = null) { - private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; + InnerFileSystem = innerFileSystem; + _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _loggerFactory = loggerFactory; + _shadowPath = shadowPath; + _isScoped = isScoped; + } - private readonly Func? _isScoped; - private readonly IFileSystem _innerFileSystem; - private readonly string _shadowPath; - private ShadowFileSystem? _shadowFileSystem; - private string? _shadowDir; - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILoggerFactory _loggerFactory; + public IFileSystem InnerFileSystem { get; } - public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func? isScoped = null) + public bool CanAddPhysical => FileSystem.CanAddPhysical; + + private IFileSystem FileSystem + { + get { - _innerFileSystem = innerFileSystem; - _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _loggerFactory = loggerFactory; - _shadowPath = shadowPath; - _isScoped = isScoped; - } - - public static string CreateShadowId(IHostingEnvironment hostingEnvironment) - { - const int retries = 50; // avoid infinite loop - const int idLength = 8; // 6 chars - - // shorten a Guid to idLength chars, and see whether it collides - // with an existing directory or not - if it does, try again, and - // we should end up with a unique identifier eventually - but just - // detect infinite loops (just in case) - - for (var i = 0; i < retries; i++) + if (_isScoped is not null && _shadowFileSystem is not null) { - var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength); + var isScoped = _isScoped!(); - var virt = ShadowFsPath + "/" + id; - var shadowDir = hostingEnvironment.MapPathContentRoot(virt); - if (Directory.Exists(shadowDir)) - continue; + // if the filesystem is created *after* shadowing starts, it won't be shadowing + // better not ignore that situation and raised a meaningful (?) exception + if (isScoped.HasValue && isScoped.Value && _shadowFileSystem == null) + { + throw new Exception("The filesystems are shadowing, but this filesystem is not."); + } - Directory.CreateDirectory(shadowDir); - return id; + return isScoped.HasValue && isScoped.Value + ? _shadowFileSystem + : InnerFileSystem; } - throw new Exception($"Could not get a shadow identifier (tried {retries} times)"); + return InnerFileSystem; + } + } + + /// + public IFileProvider? Create() => + InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null; + + public IEnumerable GetDirectories(string path) => FileSystem.GetDirectories(path); + + public void DeleteDirectory(string path) => FileSystem.DeleteDirectory(path); + + public void DeleteDirectory(string path, bool recursive) => FileSystem.DeleteDirectory(path, recursive); + + public bool DirectoryExists(string path) => FileSystem.DirectoryExists(path); + + public void AddFile(string path, Stream stream) => FileSystem.AddFile(path, stream); + + public void AddFile(string path, Stream stream, bool overrideExisting) => + FileSystem.AddFile(path, stream, overrideExisting); + + public IEnumerable GetFiles(string path) => FileSystem.GetFiles(path); + + public IEnumerable GetFiles(string path, string filter) => FileSystem.GetFiles(path, filter); + + public Stream OpenFile(string path) => FileSystem.OpenFile(path); + + public void DeleteFile(string path) => FileSystem.DeleteFile(path); + + public bool FileExists(string path) => FileSystem.FileExists(path); + + public string GetRelativePath(string fullPathOrUrl) => FileSystem.GetRelativePath(fullPathOrUrl); + + public string GetFullPath(string path) => FileSystem.GetFullPath(path); + + public string GetUrl(string? path) => FileSystem.GetUrl(path); + + public DateTimeOffset GetLastModified(string path) => FileSystem.GetLastModified(path); + + public DateTimeOffset GetCreated(string path) => FileSystem.GetCreated(path); + + public long GetSize(string path) => FileSystem.GetSize(path); + + public static string CreateShadowId(IHostingEnvironment hostingEnvironment) + { + const int retries = 50; // avoid infinite loop + const int idLength = 8; // 6 chars + + // shorten a Guid to idLength chars, and see whether it collides + // with an existing directory or not - if it does, try again, and + // we should end up with a unique identifier eventually - but just + // detect infinite loops (just in case) + for (var i = 0; i < retries; i++) + { + var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength); + + var virt = ShadowFsPath + "/" + id; + var shadowDir = hostingEnvironment.MapPathContentRoot(virt); + if (Directory.Exists(shadowDir)) + { + continue; + } + + Directory.CreateDirectory(shadowDir); + return id; } - internal void Shadow(string id) - { - // note: no thread-safety here, because ShadowFs is thread-safe due to the check - // on ShadowFileSystemsScope.None - and if None is false then we should be running - // in a single thread anyways + throw new Exception($"Could not get a shadow identifier (tried {retries} times)"); + } - var virt = Path.Combine(ShadowFsPath , id , _shadowPath); - _shadowDir = _hostingEnvironment.MapPathContentRoot(virt); - Directory.CreateDirectory(_shadowDir); - var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger(), _shadowDir, _hostingEnvironment.ToAbsolute(virt)); - _shadowFileSystem = new ShadowFileSystem(_innerFileSystem, tempfs); + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) => + FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); + + internal void Shadow(string id) + { + // note: no thread-safety here, because ShadowFs is thread-safe due to the check + // on ShadowFileSystemsScope.None - and if None is false then we should be running + // in a single thread anyways + var virt = Path.Combine(ShadowFsPath, id, _shadowPath); + _shadowDir = _hostingEnvironment.MapPathContentRoot(virt); + Directory.CreateDirectory(_shadowDir); + var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger(), _shadowDir, _hostingEnvironment.ToAbsolute(virt)); + _shadowFileSystem = new ShadowFileSystem(InnerFileSystem, tempfs); + } + + internal void UnShadow(bool complete) + { + ShadowFileSystem? shadowFileSystem = _shadowFileSystem; + var dir = _shadowDir; + _shadowFileSystem = null; + _shadowDir = null; + + try + { + // this may throw an AggregateException if some of the changes could not be applied + if (complete) + { + shadowFileSystem?.Complete(); + } } - - internal void UnShadow(bool complete) + finally { - var shadowFileSystem = _shadowFileSystem; - var dir = _shadowDir; - _shadowFileSystem = null; - _shadowDir = null; - + // in any case, cleanup try { - // this may throw an AggregateException if some of the changes could not be applied - if (complete) shadowFileSystem?.Complete(); - } - finally - { - // in any case, cleanup - try - { - Directory.Delete(dir!, true); + Directory.Delete(dir!, true); - // shadowPath make be path/to/dir, remove each - dir = dir!.Replace('/', Path.DirectorySeparatorChar); - var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length; - var pos = dir.LastIndexOf(Path.DirectorySeparatorChar); - while (pos > min) + // shadowPath make be path/to/dir, remove each + dir = dir!.Replace('/', Path.DirectorySeparatorChar); + var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length; + var pos = dir.LastIndexOf(Path.DirectorySeparatorChar); + while (pos > min) + { + dir = dir.Substring(0, pos); + if (Directory.EnumerateFileSystemEntries(dir).Any() == false) { - dir = dir.Substring(0, pos); - if (Directory.EnumerateFileSystemEntries(dir).Any() == false) - Directory.Delete(dir, true); - else - break; - pos = dir.LastIndexOf(Path.DirectorySeparatorChar); + Directory.Delete(dir, true); } - } - catch - { - // ugly, isn't it? but if we cannot cleanup, bah, just leave it there + else + { + break; + } + + pos = dir.LastIndexOf(Path.DirectorySeparatorChar); } } - } - - public IFileSystem InnerFileSystem => _innerFileSystem; - - private IFileSystem FileSystem - { - get + catch { - if (_isScoped is not null && _shadowFileSystem is not null) - { - var isScoped = _isScoped!(); - - // if the filesystem is created *after* shadowing starts, it won't be shadowing - // better not ignore that situation and raised a meaningful (?) exception - if ( isScoped.HasValue && isScoped.Value && _shadowFileSystem == null) - throw new Exception("The filesystems are shadowing, but this filesystem is not."); - - return isScoped.HasValue && isScoped.Value - ? _shadowFileSystem - : _innerFileSystem; - } - - return _innerFileSystem; + // ugly, isn't it? but if we cannot cleanup, bah, just leave it there } } - - public IEnumerable GetDirectories(string path) - { - return FileSystem.GetDirectories(path); - } - - public void DeleteDirectory(string path) - { - FileSystem.DeleteDirectory(path); - } - - public void DeleteDirectory(string path, bool recursive) - { - FileSystem.DeleteDirectory(path, recursive); - } - - public bool DirectoryExists(string path) - { - return FileSystem.DirectoryExists(path); - } - - public void AddFile(string path, Stream stream) - { - FileSystem.AddFile(path, stream); - } - - public void AddFile(string path, Stream stream, bool overrideExisting) - { - FileSystem.AddFile(path, stream, overrideExisting); - } - - public IEnumerable GetFiles(string path) - { - return FileSystem.GetFiles(path); - } - - public IEnumerable GetFiles(string path, string filter) - { - return FileSystem.GetFiles(path, filter); - } - - public Stream OpenFile(string path) - { - return FileSystem.OpenFile(path); - } - - public void DeleteFile(string path) - { - FileSystem.DeleteFile(path); - } - - public bool FileExists(string path) - { - return FileSystem.FileExists(path); - } - - public string GetRelativePath(string fullPathOrUrl) - { - return FileSystem.GetRelativePath(fullPathOrUrl); - } - - public string GetFullPath(string path) - { - return FileSystem.GetFullPath(path); - } - - public string GetUrl(string? path) - { - return FileSystem.GetUrl(path); - } - - public DateTimeOffset GetLastModified(string path) - { - return FileSystem.GetLastModified(path); - } - - public DateTimeOffset GetCreated(string path) - { - return FileSystem.GetCreated(path); - } - - public long GetSize(string path) - { - return FileSystem.GetSize(path); - } - - public bool CanAddPhysical => FileSystem.CanAddPhysical; - - public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) - { - FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); - } - - /// - public IFileProvider? Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null; } } diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index 649c89cb08..e68101918a 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -1,117 +1,120 @@ -using System; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class ViewHelper : IViewHelper { - public class ViewHelper : IViewHelper - { - private readonly IFileSystem _viewFileSystem; - private readonly IDefaultViewContentProvider _defaultViewContentProvider; + private readonly IDefaultViewContentProvider _defaultViewContentProvider; + private readonly IFileSystem _viewFileSystem; public ViewHelper(FileSystems fileSystems, IDefaultViewContentProvider defaultViewContentProvider) { _viewFileSystem = fileSystems.MvcViewsFileSystem ?? throw new ArgumentNullException(nameof(fileSystems)); _defaultViewContentProvider = defaultViewContentProvider ?? throw new ArgumentNullException(nameof(defaultViewContentProvider)); - } + }[Obsolete("Inject IDefaultViewContentProvider instead")] + public static string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null) + { + IDefaultViewContentProvider viewContentProvider = + StaticServiceProvider.Instance.GetRequiredService(); + return viewContentProvider.GetDefaultFileContent(layoutPageAlias, modelClassName, modelNamespace, modelNamespaceAlias); + } - public bool ViewExists(ITemplate t) => t.Alias is not null && _viewFileSystem.FileExists(ViewPath(t.Alias)); + public bool ViewExists(ITemplate t) => t.Alias is not null && _viewFileSystem.FileExists(ViewPath(t.Alias)); + public string GetFileContents(ITemplate t) + { + var viewContent = string.Empty; + var path = ViewPath(t.Alias ?? string.Empty); - public string GetFileContents(ITemplate t) + if (_viewFileSystem.FileExists(path)) { - var viewContent = ""; - var path = ViewPath(t.Alias ?? string.Empty); - - if (_viewFileSystem.FileExists(path)) + using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) { - using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) - { - viewContent = tr.ReadToEnd(); - tr.Close(); - } + viewContent = tr.ReadToEnd(); + tr.Close(); } - - return viewContent; } - public string CreateView(ITemplate t, bool overWrite = false) + return viewContent; + } + + public string CreateView(ITemplate t, bool overWrite = false) + { + string viewContent; + var path = ViewPath(t.Alias); + + if (_viewFileSystem.FileExists(path) == false || overWrite) { - string viewContent; - var path = ViewPath(t.Alias); - - if (_viewFileSystem.FileExists(path) == false || overWrite) - { - viewContent = SaveTemplateToFile(t); - } - else - { - using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) - { - viewContent = tr.ReadToEnd(); - tr.Close(); - } - } - - return viewContent; + viewContent = SaveTemplateToFile(t); } - - private string SaveTemplateToFile(ITemplate template) + else { - var design = template.Content.IsNullOrWhiteSpace() ? EnsureInheritedLayout(template) : template.Content!; - var path = ViewPath(template.Alias); - - var data = Encoding.UTF8.GetBytes(design); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); - - using (var ms = new MemoryStream(withBom)) + using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) { - _viewFileSystem.AddFile(path, ms, true); + viewContent = tr.ReadToEnd(); + tr.Close(); } - - return design; } - public string? UpdateViewFile(ITemplate t, string? currentAlias = null) - { - var path = ViewPath(t.Alias); + return viewContent; + } - if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias) + public string? UpdateViewFile(ITemplate t, string? currentAlias = null) + { + var path = ViewPath(t.Alias); + + if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias) + { + // then kill the old file.. + var oldFile = ViewPath(currentAlias); + if (_viewFileSystem.FileExists(oldFile)) { - //then kill the old file.. - var oldFile = ViewPath(currentAlias); - if (_viewFileSystem.FileExists(oldFile)) - _viewFileSystem.DeleteFile(oldFile); + _viewFileSystem.DeleteFile(oldFile); } - - var data = Encoding.UTF8.GetBytes(t.Content ?? string.Empty); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); - - using (var ms = new MemoryStream(withBom)) - { - _viewFileSystem.AddFile(path, ms, true); - } - return t.Content; } - public string ViewPath(string alias) + var data = Encoding.UTF8.GetBytes(t.Content ?? string.Empty); + var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + + using (var ms = new MemoryStream(withBom)) { - return _viewFileSystem.GetRelativePath(alias.Replace(" ", "") + ".cshtml"); + _viewFileSystem.AddFile(path, ms, true); } - private string EnsureInheritedLayout(ITemplate template) + return t.Content; + } + + public string ViewPath(string alias) => _viewFileSystem.GetRelativePath(alias.Replace(" ", string.Empty) + ".cshtml"); + + private string SaveTemplateToFile(ITemplate template) + { + var design = template.Content.IsNullOrWhiteSpace() ? EnsureInheritedLayout(template) : template.Content!; + var path = ViewPath(template.Alias); + + var data = Encoding.UTF8.GetBytes(design); + var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + + using (var ms = new MemoryStream(withBom)) { - var design = template.Content; - - if (string.IsNullOrEmpty(design)) - design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias); - - return design; + _viewFileSystem.AddFile(path, ms, true); } + + return design; + } + + private string EnsureInheritedLayout(ITemplate template) + { + var design = template.Content; + + if (string.IsNullOrEmpty(design)) + { + design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias); + } + + return design; } } diff --git a/src/Umbraco.Core/IRegisteredObject.cs b/src/Umbraco.Core/IRegisteredObject.cs index 54ac6e1a57..103e10dab1 100644 --- a/src/Umbraco.Core/IRegisteredObject.cs +++ b/src/Umbraco.Core/IRegisteredObject.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public interface IRegisteredObject { - public interface IRegisteredObject - { - void Stop(bool immediate); - } + void Stop(bool immediate); } diff --git a/src/Umbraco.Core/Install/FilePermissionTest.cs b/src/Umbraco.Core/Install/FilePermissionTest.cs index f84d9a0a7b..21c6d4f0c7 100644 --- a/src/Umbraco.Core/Install/FilePermissionTest.cs +++ b/src/Umbraco.Core/Install/FilePermissionTest.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Install +namespace Umbraco.Cms.Core.Install; + +public enum FilePermissionTest { - public enum FilePermissionTest - { - FolderCreation, - FileWritingForPackages, - FileWriting, - MediaFolderCreation - } + FolderCreation, + FileWritingForPackages, + FileWriting, + MediaFolderCreation, } diff --git a/src/Umbraco.Core/Install/IFilePermissionHelper.cs b/src/Umbraco.Core/Install/IFilePermissionHelper.cs index cfda3a396d..88b6e23cbf 100644 --- a/src/Umbraco.Core/Install/IFilePermissionHelper.cs +++ b/src/Umbraco.Core/Install/IFilePermissionHelper.cs @@ -1,20 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Install; -namespace Umbraco.Cms.Core.Install +/// +/// Helper to test File and folder permissions +/// +public interface IFilePermissionHelper { /// - /// Helper to test File and folder permissions + /// Run all tests for permissions of the required files and folders. /// - public interface IFilePermissionHelper - { - /// - /// Run all tests for permissions of the required files and folders. - /// - /// True if all permissions are correct. False otherwise. - bool RunFilePermissionTestSuite(out Dictionary> report); - - } + /// True if all permissions are correct. False otherwise. + bool RunFilePermissionTestSuite(out Dictionary> report); } diff --git a/src/Umbraco.Core/Install/InstallException.cs b/src/Umbraco.Core/Install/InstallException.cs index 2ec741d200..69e28db92c 100644 --- a/src/Umbraco.Core/Install/InstallException.cs +++ b/src/Umbraco.Core/Install/InstallException.cs @@ -1,106 +1,122 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install +namespace Umbraco.Cms.Core.Install; + +/// +/// Used for steps to be able to return a JSON structure back to the UI. +/// +/// +[Serializable] +public class InstallException : Exception { /// - /// Used for steps to be able to return a JSON structure back to the UI. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class InstallException : Exception + public InstallException() { - /// - /// Gets the view. - /// - /// - /// The view. - /// - public string? View { get; private set; } + } - /// - /// Gets the view model. - /// - /// - /// The view model. - /// - /// - /// This object is not included when serializing. - /// - public object? ViewModel { get; private set; } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public InstallException(string message) + : this(message, "error", null) + { + } - /// - /// Initializes a new instance of the class. - /// - public InstallException() - { } + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The view model. + public InstallException(string message, object viewModel) + : this(message, "error", viewModel) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public InstallException(string message) - : this(message, "error", null) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The view. + /// The view model. + public InstallException(string message, string view, object? viewModel) + : base(message) + { + View = view; + ViewModel = viewModel; + } - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The view model. - public InstallException(string message, object viewModel) - : this(message, "error", viewModel) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public InstallException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The view. - /// The view model. - public InstallException(string message, string view, object? viewModel) - : base(message) + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected InstallException(SerializationInfo info, StreamingContext context) + : base(info, context) => + View = info.GetString(nameof(View)); + + /// + /// Gets the view. + /// + /// + /// The view. + /// + public string? View { get; private set; } + + /// + /// Gets the view model. + /// + /// + /// The view model. + /// + /// + /// This object is not included when serializing. + /// + public object? ViewModel { get; private set; } + + /// + /// When overridden in a derived class, sets the with + /// information about the exception. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) { - View = view; - ViewModel = viewModel; + throw new ArgumentNullException(nameof(info)); } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public InstallException(string message, Exception innerException) - : base(message, innerException) - { } + info.AddValue(nameof(View), View); - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected InstallException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - View = info.GetString(nameof(View)); - } - - /// - /// When overridden in a derived class, sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(View), View); - - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); } } diff --git a/src/Umbraco.Core/Install/InstallStatusTracker.cs b/src/Umbraco.Core/Install/InstallStatusTracker.cs index 1ee7f685d4..5403ded3ae 100644 --- a/src/Umbraco.Core/Install/InstallStatusTracker.cs +++ b/src/Umbraco.Core/Install/InstallStatusTracker.cs @@ -1,74 +1,104 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Install +namespace Umbraco.Cms.Core.Install; + +/// +/// An internal in-memory status tracker for the current installation +/// +public class InstallStatusTracker { - /// - /// An internal in-memory status tracker for the current installation - /// - public class InstallStatusTracker + private static ConcurrentHashSet _steps = new(); + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IJsonSerializer _jsonSerializer; + + public InstallStatusTracker(IHostingEnvironment hostingEnvironment, IJsonSerializer jsonSerializer) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IJsonSerializer _jsonSerializer; + _hostingEnvironment = hostingEnvironment; + _jsonSerializer = jsonSerializer; + } - public InstallStatusTracker(IHostingEnvironment hostingEnvironment, IJsonSerializer jsonSerializer) + public static IEnumerable GetStatus() => + new List(_steps).OrderBy(x => x.ServerOrder); + + public void Reset() + { + _steps = new ConcurrentHashSet(); + ClearFiles(); + } + + private string GetFile(Guid installId) + { + var file = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + + "Install/" + + "install_" + + installId.ToString("N") + + ".txt"); + return file; + } + + public void ClearFiles() + { + var dir = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + + "Install/"); + if (Directory.Exists(dir)) { - _hostingEnvironment = hostingEnvironment; - _jsonSerializer = jsonSerializer; - } - - private static ConcurrentHashSet _steps = new ConcurrentHashSet(); - - private string GetFile(Guid installId) - { - var file = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "Install/" - + "install_" - + installId.ToString("N") - + ".txt"); - return file; - } - - public void Reset() - { - _steps = new ConcurrentHashSet(); - ClearFiles(); - } - - public void ClearFiles() - { - var dir = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "Install/"); - if (Directory.Exists(dir)) + var files = Directory.GetFiles(dir); + foreach (var f in files) { - var files = Directory.GetFiles(dir); - foreach (var f in files) + File.Delete(f); + } + } + else + { + Directory.CreateDirectory(dir); + } + } + + public IEnumerable InitializeFromFile(Guid installId) + { + // check if we have our persisted file and read it + var file = GetFile(installId); + if (File.Exists(file)) + { + IEnumerable? deserialized = + _jsonSerializer.Deserialize>( + File.ReadAllText(file)); + if (deserialized is not null) + { + foreach (InstallTrackingItem item in deserialized) { - File.Delete(f); + _steps.Add(item); } } - else - { - Directory.CreateDirectory(dir); - } + } + else + { + throw new InvalidOperationException("Cannot initialize from file, the installation file with id " + + installId + " does not exist"); } - public IEnumerable InitializeFromFile(Guid installId) + return new List(_steps); + } + + public IEnumerable Initialize(Guid installId, IEnumerable steps) + { + // if there are no steps in memory + if (_steps.Count == 0) { - //check if we have our persisted file and read it + // check if we have our persisted file and read it var file = GetFile(installId); if (File.Exists(file)) { - var deserialized = _jsonSerializer.Deserialize>( - File.ReadAllText(file)); + IEnumerable? deserialized = + _jsonSerializer.Deserialize>( + File.ReadAllText(file)); if (deserialized is not null) { - foreach (var item in deserialized) + foreach (InstallTrackingItem item in deserialized) { _steps.Add(item); } @@ -76,81 +106,51 @@ namespace Umbraco.Cms.Core.Install } else { - throw new InvalidOperationException("Cannot initialize from file, the installation file with id " + installId + " does not exist"); + ClearFiles(); + + // otherwise just create the steps in memory (brand new install) + foreach (InstallSetupStep step in steps.OrderBy(x => x.ServerOrder)) + { + _steps.Add(new InstallTrackingItem(step.Name, step.ServerOrder)); + } + + // save the file + var serialized = _jsonSerializer.Serialize(new List(_steps)); + Directory.CreateDirectory(Path.GetDirectoryName(file)!); + File.WriteAllText(file, serialized); } - return new List(_steps); } - - public IEnumerable Initialize(Guid installId, IEnumerable steps) + else { - //if there are no steps in memory - if (_steps.Count == 0) - { - //check if we have our persisted file and read it - var file = GetFile(installId); - if (File.Exists(file)) - { - var deserialized = _jsonSerializer.Deserialize>( - File.ReadAllText(file)); - if (deserialized is not null) - { - foreach (var item in deserialized) - { - _steps.Add(item); - } - } - } - else - { - ClearFiles(); - - //otherwise just create the steps in memory (brand new install) - foreach (var step in steps.OrderBy(x => x.ServerOrder)) - { - _steps.Add(new InstallTrackingItem(step.Name, step.ServerOrder)); - } - //save the file - var serialized = _jsonSerializer.Serialize(new List(_steps)); - Directory.CreateDirectory(Path.GetDirectoryName(file)!); - File.WriteAllText(file, serialized); - } - } - else - { - //ensure that the file exists with the current install id - var file = GetFile(installId); - if (File.Exists(file) == false) - { - ClearFiles(); - - //save the correct file - var serialized = _jsonSerializer.Serialize(new List(_steps)); - Directory.CreateDirectory(Path.GetDirectoryName(file)!); - File.WriteAllText(file, serialized); - } - } - - return new List(_steps); - } - - public void SetComplete(Guid installId, string name, IDictionary? additionalData = null) - { - var trackingItem = _steps.Single(x => x.Name == name); - if (additionalData != null) - { - trackingItem.AdditionalData = additionalData; - } - trackingItem.IsComplete = true; - - //save the file + // ensure that the file exists with the current install id var file = GetFile(installId); - var serialized = _jsonSerializer.Serialize(new List(_steps)); - File.WriteAllText(file, serialized); + if (File.Exists(file) == false) + { + ClearFiles(); + + // save the correct file + var serialized = _jsonSerializer.Serialize(new List(_steps)); + Directory.CreateDirectory(Path.GetDirectoryName(file)!); + File.WriteAllText(file, serialized); + } } - public static IEnumerable GetStatus() + return new List(_steps); + } + + public void SetComplete(Guid installId, string name, IDictionary? additionalData = null) + { + InstallTrackingItem trackingItem = _steps.Single(x => x.Name == name); + if (additionalData != null) { - return new List(_steps).OrderBy(x => x.ServerOrder); + trackingItem.AdditionalData = additionalData; } + + trackingItem.IsComplete = true; + + // save the file + var file = GetFile(installId); + var serialized = _jsonSerializer.Serialize(new List(_steps)); + File.WriteAllText(file, serialized); } } diff --git a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs index 14d77ecb77..40f54bab33 100644 --- a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs @@ -1,56 +1,55 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Install.InstallSteps +namespace Umbraco.Cms.Core.Install.InstallSteps; + +/// +/// Represents a step in the installation that ensure all the required permissions on files and folders are correct. +/// +[InstallSetupStep( + InstallationType.NewInstall | InstallationType.Upgrade, + "Permissions", + 0, + "", + PerformsAppRestart = true)] +public class FilePermissionsStep : InstallSetupStep { + private readonly IFilePermissionHelper _filePermissionHelper; + private readonly ILocalizedTextService _localizedTextService; + /// - /// Represents a step in the installation that ensure all the required permissions on files and folders are correct. + /// Initializes a new instance of the class. /// - [InstallSetupStep( - InstallationType.NewInstall | InstallationType.Upgrade, - "Permissions", - 0, - "", - PerformsAppRestart = true)] - public class FilePermissionsStep : InstallSetupStep + public FilePermissionsStep( + IFilePermissionHelper filePermissionHelper, + ILocalizedTextService localizedTextService) { - private readonly IFilePermissionHelper _filePermissionHelper; - private readonly ILocalizedTextService _localizedTextService; - - /// - /// Initializes a new instance of the class. - /// - public FilePermissionsStep( - IFilePermissionHelper filePermissionHelper, - ILocalizedTextService localizedTextService) - { - _filePermissionHelper = filePermissionHelper; - _localizedTextService = localizedTextService; - } - - /// - public override Task ExecuteAsync(object model) - { - // validate file permissions - var permissionsOk = _filePermissionHelper.RunFilePermissionTestSuite(out Dictionary> report); - - var translatedErrors = report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); - if (permissionsOk == false) - { - throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); - } - - return Task.FromResult(null); - } - - /// - public override bool RequiresExecution(object model) => true; + _filePermissionHelper = filePermissionHelper; + _localizedTextService = localizedTextService; } + + /// + public override Task ExecuteAsync(object model) + { + // validate file permissions + var permissionsOk = + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> report); + + var translatedErrors = + report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); + if (permissionsOk == false) + { + throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); + } + + return Task.FromResult(null); + } + + /// + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs index 7855df76f7..c8962c5fb9 100644 --- a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -9,23 +7,26 @@ using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.Install.InstallSteps -{ - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "TelemetryIdConfiguration", 0, "", - PerformsAppRestart = false)] - public class TelemetryIdentifierStep : InstallSetupStep - { - private readonly IOptions _globalSettings; - private readonly ISiteIdentifierService _siteIdentifierService; +namespace Umbraco.Cms.Core.Install.InstallSteps; - public TelemetryIdentifierStep( - IOptions globalSettings, - ISiteIdentifierService siteIdentifierService) - { - _globalSettings = globalSettings; - _siteIdentifierService = siteIdentifierService; - } +[InstallSetupStep( + InstallationType.NewInstall | InstallationType.Upgrade, + "TelemetryIdConfiguration", + 0, + "", + PerformsAppRestart = false)] +public class TelemetryIdentifierStep : InstallSetupStep +{ + private readonly IOptions _globalSettings; + private readonly ISiteIdentifierService _siteIdentifierService; + + public TelemetryIdentifierStep( + IOptions globalSettings, + ISiteIdentifierService siteIdentifierService) + { + _globalSettings = globalSettings; + _siteIdentifierService = siteIdentifierService; + } public override Task ExecuteAsync(object model) { @@ -33,14 +34,13 @@ namespace Umbraco.Cms.Core.Install.InstallSteps return Task.FromResult(null); } - public override bool RequiresExecution(object model) - { - // Verify that Json value is not empty string - // Try & get a value stored in appSettings.json - var backofficeIdentifierRaw = _globalSettings.Value.Id; + public override bool RequiresExecution(object model) + { + // Verify that Json value is not empty string + // Try & get a value stored in appSettings.json + var backofficeIdentifierRaw = _globalSettings.Value.Id; - // No need to add Id again if already found - return string.IsNullOrEmpty(backofficeIdentifierRaw); - } + // No need to add Id again if already found + return string.IsNullOrEmpty(backofficeIdentifierRaw); } } diff --git a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs index 6530983de2..763b69226e 100644 --- a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Semver; @@ -31,16 +29,22 @@ namespace Umbraco.Cms.Core.Install.InstallSteps { string FormatGuidState(string? value) { - if (string.IsNullOrWhiteSpace(value)) value = "unknown"; - else if (Guid.TryParse(value, out var currentStateGuid)) + if (string.IsNullOrWhiteSpace(value)) + { + value = "unknown"; + } + else if (Guid.TryParse(value, out Guid currentStateGuid)) + { value = currentStateGuid.ToString("N").Substring(0, 8); + } + return value; } var currentState = FormatGuidState(_runtimeState.CurrentMigrationState); var newState = FormatGuidState(_runtimeState.FinalMigrationState); var newVersion = _umbracoVersion.SemanticVersion?.ToSemanticStringWithoutBuild(); - var oldVersion = new SemVersion(_umbracoVersion.SemanticVersion?.Major ?? 0, 0, 0).ToString(); //TODO can we find the old version somehow? e.g. from current state + var oldVersion = new SemVersion(_umbracoVersion.SemanticVersion?.Major ?? 0).ToString(); //TODO can we find the old version somehow? e.g. from current state var reportUrl = $"https://our.umbraco.com/contribute/releases/compare?from={oldVersion}&to={newVersion}¬es=1"; diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index da2f61fce5..eb892d9cee 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -1,33 +1,31 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +[DataContract(Name = "database", Namespace = "")] +public class DatabaseModel { - [DataContract(Name = "database", Namespace = "")] - public class DatabaseModel - { - [DataMember(Name = "databaseProviderMetadataId")] - public Guid DatabaseProviderMetadataId { get; set; } + [DataMember(Name = "databaseProviderMetadataId")] + public Guid DatabaseProviderMetadataId { get; set; } - [DataMember(Name = "providerName")] - public string? ProviderName { get; set; } + [DataMember(Name = "providerName")] + public string? ProviderName { get; set; } - [DataMember(Name = "server")] - public string Server { get; set; } = null!; + [DataMember(Name = "server")] + public string Server { get; set; } = null!; - [DataMember(Name = "databaseName")] - public string DatabaseName { get; set; } = null!; + [DataMember(Name = "databaseName")] + public string DatabaseName { get; set; } = null!; - [DataMember(Name = "login")] - public string Login { get; set; } = null!; + [DataMember(Name = "login")] + public string Login { get; set; } = null!; - [DataMember(Name = "password")] - public string Password { get; set; } = null!; + [DataMember(Name = "password")] + public string Password { get; set; } = null!; - [DataMember(Name = "integratedAuth")] - public bool IntegratedAuth { get; set; } + [DataMember(Name = "integratedAuth")] + public bool IntegratedAuth { get; set; } - [DataMember(Name = "connectionString")] - public string? ConnectionString { get; set; } - } + [DataMember(Name = "connectionString")] + public string? ConnectionString { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallInstructions.cs b/src/Umbraco.Core/Install/Models/InstallInstructions.cs index 9dc42553e0..c86307d9b0 100644 --- a/src/Umbraco.Core/Install/Models/InstallInstructions.cs +++ b/src/Umbraco.Core/Install/Models/InstallInstructions.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models -{ - [DataContract(Name = "installInstructions", Namespace = "")] - public class InstallInstructions - { - [DataMember(Name = "instructions")] - public IDictionary? Instructions { get; set; } +namespace Umbraco.Cms.Core.Install.Models; - [DataMember(Name = "installId")] - public Guid InstallId { get; set; } - } +[DataContract(Name = "installInstructions", Namespace = "")] +public class InstallInstructions +{ + [DataMember(Name = "instructions")] + public IDictionary? Instructions { get; set; } + + [DataMember(Name = "installId")] + public Guid InstallId { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs index 43b3fc73fe..650c746998 100644 --- a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs +++ b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs @@ -1,42 +1,41 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +/// +/// Returned to the UI for each installation step that is completed +/// +[DataContract(Name = "result", Namespace = "")] +public class InstallProgressResultModel { + public InstallProgressResultModel(bool processComplete, string stepCompleted, string nextStep, string? view = null, object? viewModel = null) + { + ProcessComplete = processComplete; + StepCompleted = stepCompleted; + NextStep = nextStep; + ViewModel = viewModel; + View = view; + } /// - /// Returned to the UI for each installation step that is completed + /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless + /// explicitly specified. /// - [DataContract(Name = "result", Namespace = "")] - public class InstallProgressResultModel - { - public InstallProgressResultModel(bool processComplete, string stepCompleted, string nextStep, string? view = null, object? viewModel = null) - { - ProcessComplete = processComplete; - StepCompleted = stepCompleted; - NextStep = nextStep; - ViewModel = viewModel; - View = view; - } + [DataMember(Name = "view")] + public string? View { get; private set; } - /// - /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless explicitly specified. - /// - [DataMember(Name = "view")] - public string? View { get; private set; } + [DataMember(Name = "complete")] + public bool ProcessComplete { get; set; } - [DataMember(Name = "complete")] - public bool ProcessComplete { get; set; } + [DataMember(Name = "stepCompleted")] + public string StepCompleted { get; set; } - [DataMember(Name = "stepCompleted")] - public string StepCompleted { get; set; } + [DataMember(Name = "nextStep")] + public string NextStep { get; set; } - [DataMember(Name = "nextStep")] - public string NextStep { get; set; } - - /// - /// The view model to return to the UI if this step is returning a view (optional) - /// - [DataMember(Name = "model")] - public object? ViewModel { get; private set; } - } + /// + /// The view model to return to the UI if this step is returning a view (optional) + /// + [DataMember(Name = "model")] + public object? ViewModel { get; private set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetup.cs b/src/Umbraco.Core/Install/Models/InstallSetup.cs index 358bd92234..2a1e3ce9f7 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetup.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetup.cs @@ -1,26 +1,22 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +/// +/// Model containing all the install steps for setting up the UI +/// +[DataContract(Name = "installSetup", Namespace = "")] +public class InstallSetup { - /// - /// Model containing all the install steps for setting up the UI - /// - [DataContract(Name = "installSetup", Namespace = "")] - public class InstallSetup + public InstallSetup() { - public InstallSetup() - { - Steps = new List(); - InstallId = Guid.NewGuid(); - } - - [DataMember(Name = "installId")] - public Guid InstallId { get; private set; } - - [DataMember(Name = "steps")] - public IEnumerable Steps { get; set; } - + Steps = new List(); + InstallId = Guid.NewGuid(); } + + [DataMember(Name = "installId")] + public Guid InstallId { get; private set; } + + [DataMember(Name = "steps")] + public IEnumerable Steps { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs index 15a4c12b47..3849a09d75 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs @@ -1,47 +1,42 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +/// +/// The object returned from each installation step +/// +public class InstallSetupResult { - /// - /// The object returned from each installation step - /// - public class InstallSetupResult + public InstallSetupResult() { - public InstallSetupResult() - { - } - - public InstallSetupResult(IDictionary savedStepData, string view, object? viewModel = null) - { - ViewModel = viewModel; - SavedStepData = savedStepData; - View = view; - } - - public InstallSetupResult(IDictionary savedStepData) - { - SavedStepData = savedStepData; - } - - public InstallSetupResult(string view, object? viewModel = null) - { - ViewModel = viewModel; - View = view; - } - - /// - /// Data that is persisted to the installation file which can be used from other installation steps - /// - public IDictionary? SavedStepData { get; private set; } - - /// - /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless explicitly specified. - /// - public string? View { get; private set; } - - /// - /// The view model to return to the UI if this step is returning a view (optional) - /// - public object? ViewModel { get; private set; } } + + public InstallSetupResult(IDictionary savedStepData, string view, object? viewModel = null) + { + ViewModel = viewModel; + SavedStepData = savedStepData; + View = view; + } + + public InstallSetupResult(IDictionary savedStepData) => SavedStepData = savedStepData; + + public InstallSetupResult(string view, object? viewModel = null) + { + ViewModel = viewModel; + View = view; + } + + /// + /// Data that is persisted to the installation file which can be used from other installation steps + /// + public IDictionary? SavedStepData { get; } + + /// + /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless + /// explicitly specified. + /// + public string? View { get; } + + /// + /// The view model to return to the UI if this step is returning a view (optional) + /// + public object? ViewModel { get; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs index 766458f99f..a9d24447c6 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs @@ -1,87 +1,84 @@ -using System; using System.Runtime.Serialization; -using System.Threading.Tasks; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +/// +/// Model to give to the front-end to collect the information for each step +/// +[DataContract(Name = "step", Namespace = "")] +public abstract class InstallSetupStep : InstallSetupStep { /// - /// Model to give to the front-end to collect the information for each step + /// Defines the step model type on the server side so we can bind it /// - [DataContract(Name = "step", Namespace = "")] - public abstract class InstallSetupStep : InstallSetupStep + [IgnoreDataMember] + public override Type StepType => typeof(T); + + /// + /// The step execution method + /// + /// + /// + public abstract Task ExecuteAsync(T model); + + /// + /// Determines if this step needs to execute based on the current state of the application and/or install process + /// + /// + public abstract bool RequiresExecution(T model); +} + +[DataContract(Name = "step", Namespace = "")] +public abstract class InstallSetupStep +{ + protected InstallSetupStep() { - /// - /// Defines the step model type on the server side so we can bind it - /// - [IgnoreDataMember] - public override Type StepType => typeof(T); - - /// - /// The step execution method - /// - /// - /// - public abstract Task ExecuteAsync(T model); - - /// - /// Determines if this step needs to execute based on the current state of the application and/or install process - /// - /// - public abstract bool RequiresExecution(T model); - } - - [DataContract(Name = "step", Namespace = "")] - public abstract class InstallSetupStep - { - protected InstallSetupStep() + InstallSetupStepAttribute? att = GetType().GetCustomAttribute(false); + if (att == null) { - var att = GetType().GetCustomAttribute(false); - if (att == null) - { - throw new InvalidOperationException("Each step must be attributed"); - } - Name = att.Name; - View = att.View; - ServerOrder = att.ServerOrder; - Description = att.Description; - InstallTypeTarget = att.InstallTypeTarget; - PerformsAppRestart = att.PerformsAppRestart; + throw new InvalidOperationException("Each step must be attributed"); } - [DataMember(Name = "name")] - public string Name { get; private set; } - - [DataMember(Name = "view")] - public virtual string View { get; private set; } - - /// - /// The view model used to render the view, by default is null but can be populated - /// - [DataMember(Name = "model")] - public virtual object? ViewModel { get; private set; } - - [DataMember(Name = "description")] - public string Description { get; private set; } - - [IgnoreDataMember] - public InstallationType InstallTypeTarget { get; private set; } - - [IgnoreDataMember] - public bool PerformsAppRestart { get; private set; } - - /// - /// Defines what order this step needs to execute on the server side since the - /// steps might be shown out of order on the front-end - /// - [DataMember(Name = "serverOrder")] - public int ServerOrder { get; private set; } - - /// - /// Defines the step model type on the server side so we can bind it - /// - [IgnoreDataMember] - public abstract Type StepType { get; } - + Name = att.Name; + View = att.View; + ServerOrder = att.ServerOrder; + Description = att.Description; + InstallTypeTarget = att.InstallTypeTarget; + PerformsAppRestart = att.PerformsAppRestart; } + + [DataMember(Name = "name")] + public string Name { get; private set; } + + [DataMember(Name = "view")] + public virtual string View { get; private set; } + + /// + /// The view model used to render the view, by default is null but can be populated + /// + [DataMember(Name = "model")] + public virtual object? ViewModel { get; private set; } + + [DataMember(Name = "description")] + public string Description { get; private set; } + + [IgnoreDataMember] + public InstallationType InstallTypeTarget { get; } + + [IgnoreDataMember] + public bool PerformsAppRestart { get; } + + /// + /// Defines what order this step needs to execute on the server side since the + /// steps might be shown out of order on the front-end + /// + [DataMember(Name = "serverOrder")] + public int ServerOrder { get; private set; } + + /// + /// Defines the step model type on the server side so we can bind it + /// + [IgnoreDataMember] + public abstract Type StepType { get; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs index 7feaced052..c6d0657d33 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs @@ -1,44 +1,47 @@ -using System; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +public sealed class InstallSetupStepAttribute : Attribute { - public sealed class InstallSetupStepAttribute : Attribute + public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, string view, int serverOrder, string description) { - public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, string view, int serverOrder, string description) - { - InstallTypeTarget = installTypeTarget; - Name = name; - View = view; - ServerOrder = serverOrder; - Description = description; + InstallTypeTarget = installTypeTarget; + Name = name; + View = view; + ServerOrder = serverOrder; + Description = description; - //default - PerformsAppRestart = false; - } - - public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, int serverOrder, string description) - { - InstallTypeTarget = installTypeTarget; - Name = name; - View = string.Empty; - ServerOrder = serverOrder; - Description = description; - - //default - PerformsAppRestart = false; - } - - public InstallationType InstallTypeTarget { get; private set; } - public string Name { get; private set; } - public string View { get; private set; } - public int ServerOrder { get; private set; } - public string Description { get; private set; } - - /// - /// A flag to notify the installer that this step performs an app pool restart, this can be handy to know since if the current - /// step is performing a restart, we cannot 'look ahead' to see if the next step can execute since we won't know until the app pool - /// is restarted. - /// - public bool PerformsAppRestart { get; set; } + // default + PerformsAppRestart = false; } + + public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, int serverOrder, string description) + { + InstallTypeTarget = installTypeTarget; + Name = name; + View = string.Empty; + ServerOrder = serverOrder; + Description = description; + + // default + PerformsAppRestart = false; + } + + public InstallationType InstallTypeTarget { get; } + + public string Name { get; } + + public string View { get; } + + public int ServerOrder { get; } + + public string Description { get; } + + /// + /// A flag to notify the installer that this step performs an app pool restart, this can be handy to know since if the + /// current + /// step is performing a restart, we cannot 'look ahead' to see if the next step can execute since we won't know until + /// the app pool + /// is restarted. + /// + public bool PerformsAppRestart { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs index 3a34264d77..74170857b5 100644 --- a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs +++ b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs @@ -1,37 +1,43 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +public class InstallTrackingItem { - public class InstallTrackingItem + public InstallTrackingItem(string name, int serverOrder) { - public InstallTrackingItem(string name, int serverOrder) - { - Name = name; - ServerOrder = serverOrder; - AdditionalData = new Dictionary(); - } - - public string Name { get; set; } - public int ServerOrder { get; set; } - public bool IsComplete { get; set; } - public IDictionary AdditionalData { get; set; } - - protected bool Equals(InstallTrackingItem other) - { - return string.Equals(Name, other.Name); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((InstallTrackingItem) obj); - } - - public override int GetHashCode() - { - return Name.GetHashCode(); - } + Name = name; + ServerOrder = serverOrder; + AdditionalData = new Dictionary(); } + + public string Name { get; set; } + + public int ServerOrder { get; set; } + + public bool IsComplete { get; set; } + + public IDictionary AdditionalData { get; set; } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((InstallTrackingItem)obj); + } + + protected bool Equals(InstallTrackingItem other) => string.Equals(Name, other.Name); + + public override int GetHashCode() => Name.GetHashCode(); } diff --git a/src/Umbraco.Core/Install/Models/InstallationType.cs b/src/Umbraco.Core/Install/Models/InstallationType.cs index 99ecf8ce1f..b2b6a428fa 100644 --- a/src/Umbraco.Core/Install/Models/InstallationType.cs +++ b/src/Umbraco.Core/Install/Models/InstallationType.cs @@ -1,11 +1,8 @@ -using System; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +[Flags] +public enum InstallationType { - [Flags] - public enum InstallationType - { - NewInstall = 1 << 0, // 1 - Upgrade = 1 << 1, // 2 - } + NewInstall = 1 << 0, // 1 + Upgrade = 1 << 1, // 2 } diff --git a/src/Umbraco.Core/Install/Models/Package.cs b/src/Umbraco.Core/Install/Models/Package.cs index 3b9a204f10..9ac30ab9a7 100644 --- a/src/Umbraco.Core/Install/Models/Package.cs +++ b/src/Umbraco.Core/Install/Models/Package.cs @@ -1,16 +1,16 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +[DataContract(Name = "package")] +public class Package { - [DataContract(Name = "package")] - public class Package - { - [DataMember(Name = "name")] - public string? Name { get; set; } - [DataMember(Name = "thumbnail")] - public string? Thumbnail { get; set; } - [DataMember(Name = "id")] - public Guid Id { get; set; } - } + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } + + [DataMember(Name = "id")] + public Guid Id { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/UserModel.cs b/src/Umbraco.Core/Install/Models/UserModel.cs index 392730b3b6..61f76c795d 100644 --- a/src/Umbraco.Core/Install/Models/UserModel.cs +++ b/src/Umbraco.Core/Install/Models/UserModel.cs @@ -1,20 +1,23 @@ using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +[DataContract(Name = "user", Namespace = "")] +public class UserModel { - [DataContract(Name = "user", Namespace = "")] - public class UserModel - { - [DataMember(Name = "name")] - public string Name { get; set; } = null!; + [DataMember(Name = "name")] + public string Name { get; set; } = null!; - [DataMember(Name = "email")] - public string Email { get; set; } = null!; + [DataMember(Name = "email")] + public string Email { get; set; } = null!; - [DataMember(Name = "password")] - public string Password { get; set; } = null!; + [DataMember(Name = "password")] + public string Password { get; set; } = null!; - [DataMember(Name = "subscribeToNewsLetter")] - public bool SubscribeToNewsLetter { get; set; } - } + [DataMember(Name = "subscribeToNewsLetter")] + public bool SubscribeToNewsLetter { get; set; } + + [DataMember(Name = "telemetryLevel")] + public TelemetryLevel TelemetryLevel { get; set; } } diff --git a/src/Umbraco.Core/InstallLog.cs b/src/Umbraco.Core/InstallLog.cs index 3d8ab26af9..d0bec2097f 100644 --- a/src/Umbraco.Core/InstallLog.cs +++ b/src/Umbraco.Core/InstallLog.cs @@ -1,34 +1,52 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public class InstallLog { - public class InstallLog + public InstallLog( + Guid installId, + bool isUpgrade, + bool installCompleted, + DateTime timestamp, + int versionMajor, + int versionMinor, + int versionPatch, + string versionComment, + string error, + string? userAgent, + string dbProvider) { - public Guid InstallId { get; } - public bool IsUpgrade { get; set; } - public bool InstallCompleted { get; set; } - public DateTime Timestamp { get; set; } - public int VersionMajor { get; } - public int VersionMinor { get; } - public int VersionPatch { get; } - public string VersionComment { get; } - public string Error { get; } - public string? UserAgent { get; } - public string DbProvider { get; set; } - - public InstallLog(Guid installId, bool isUpgrade, bool installCompleted, DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string? userAgent, string dbProvider) - { - InstallId = installId; - IsUpgrade = isUpgrade; - InstallCompleted = installCompleted; - Timestamp = timestamp; - VersionMajor = versionMajor; - VersionMinor = versionMinor; - VersionPatch = versionPatch; - VersionComment = versionComment; - Error = error; - UserAgent = userAgent; - DbProvider = dbProvider; - } + InstallId = installId; + IsUpgrade = isUpgrade; + InstallCompleted = installCompleted; + Timestamp = timestamp; + VersionMajor = versionMajor; + VersionMinor = versionMinor; + VersionPatch = versionPatch; + VersionComment = versionComment; + Error = error; + UserAgent = userAgent; + DbProvider = dbProvider; } + + public Guid InstallId { get; } + + public bool IsUpgrade { get; set; } + + public bool InstallCompleted { get; set; } + + public DateTime Timestamp { get; set; } + + public int VersionMajor { get; } + + public int VersionMinor { get; } + + public int VersionPatch { get; } + + public string VersionComment { get; } + + public string Error { get; } + + public string? UserAgent { get; } + + public string DbProvider { get; set; } } diff --git a/src/Umbraco.Core/LambdaExpressionCacheKey.cs b/src/Umbraco.Core/LambdaExpressionCacheKey.cs index 123654bbe2..31ebcf688f 100644 --- a/src/Umbraco.Core/LambdaExpressionCacheKey.cs +++ b/src/Umbraco.Core/LambdaExpressionCacheKey.cs @@ -1,83 +1,77 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a simple in a form which is suitable for using as a dictionary key +/// by exposing the return type, argument types and expression string form in a single concatenated string. +/// +public struct LambdaExpressionCacheKey { /// - /// Represents a simple in a form which is suitable for using as a dictionary key - /// by exposing the return type, argument types and expression string form in a single concatenated string. + /// The argument type names of the /// - public struct LambdaExpressionCacheKey + public readonly HashSet ArgTypes; + + public LambdaExpressionCacheKey(string returnType, string expression, params string[] argTypes) { - public LambdaExpressionCacheKey(string returnType, string expression, params string[] argTypes) - { - ReturnType = returnType; - ExpressionAsString = expression; - ArgTypes = new HashSet(argTypes); - _toString = null; - } - - public LambdaExpressionCacheKey(LambdaExpression obj) - { - ReturnType = obj.ReturnType.FullName; - ExpressionAsString = obj.ToString(); - ArgTypes = new HashSet(obj.Parameters.Select(x => x.Type.FullName)); - _toString = null; - } - - /// - /// The argument type names of the - /// - public readonly HashSet ArgTypes; - - /// - /// The return type of the - /// - public readonly string? ReturnType; - - /// - /// The original string representation of the - /// - public readonly string ExpressionAsString; - - private string? _toString; - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return _toString ?? (_toString = String.Concat(String.Join("|", ArgTypes), ",", ReturnType, ",", ExpressionAsString)); - } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object? obj) - { - if (ReferenceEquals(obj, null)) return false; - var casted = (LambdaExpressionCacheKey)obj; - return casted.ToString() == ToString(); - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - return ToString().GetHashCode(); - } + ReturnType = returnType; + ExpressionAsString = expression; + ArgTypes = new HashSet(argTypes); + _toString = null; } + + public LambdaExpressionCacheKey(LambdaExpression obj) + { + ReturnType = obj.ReturnType.FullName; + ExpressionAsString = obj.ToString(); + ArgTypes = new HashSet(obj.Parameters.Select(x => x.Type.FullName)); + _toString = null; + } + + /// + /// The return type of the + /// + public readonly string? ReturnType; + + /// + /// The original string representation of the + /// + public readonly string ExpressionAsString; + + private string? _toString; + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => _toString ??= string.Concat(string.Join("|", ArgTypes), ",", ReturnType, ",", ExpressionAsString); + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(obj, null)) + { + return false; + } + + var casted = (LambdaExpressionCacheKey)obj; + return casted.ToString() == ToString(); + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() => ToString().GetHashCode(); } diff --git a/src/Umbraco.Core/Logging/DisposableTimer.cs b/src/Umbraco.Core/Logging/DisposableTimer.cs index a22ac75127..b153e096c4 100644 --- a/src/Umbraco.Core/Logging/DisposableTimer.cs +++ b/src/Umbraco.Core/Logging/DisposableTimer.cs @@ -1,172 +1,183 @@ -using System; using System.Diagnostics; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it +/// in a using (C#) statement. +/// +public class DisposableTimer : DisposableObjectSlim { - /// - /// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it in a using (C#) statement. - /// - public class DisposableTimer : DisposableObjectSlim + private readonly string _endMessage; + private readonly object[]? _endMessageArgs; + private readonly object[]? _failMessageArgs; + private readonly LogLevel _level; + private readonly ILogger _logger; + private readonly Type _loggerType; + private readonly IDisposable? _profilerStep; + private readonly int _thresholdMilliseconds; + private readonly string _timingId; + private bool _failed; + private Exception? _failException; + private string? _failMessage; + + // internal - created by profiling logger + internal DisposableTimer( + ILogger logger, + LogLevel level, + IProfiler profiler, + Type loggerType, + string startMessage, + string endMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null, + int thresholdMilliseconds = 0) { - private readonly ILogger _logger; - private readonly LogLevel _level; - private readonly Type _loggerType; - private readonly int _thresholdMilliseconds; - private readonly IDisposable? _profilerStep; - private readonly string _endMessage; - private string? _failMessage; - private readonly object[]? _endMessageArgs; - private readonly object[]? _failMessageArgs; - private Exception? _failException; - private bool _failed; - private readonly string _timingId; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _level = level; + _loggerType = loggerType ?? throw new ArgumentNullException(nameof(loggerType)); + _endMessage = endMessage; + _failMessage = failMessage; + _endMessageArgs = endMessageArgs; + _failMessageArgs = failMessageArgs; + _thresholdMilliseconds = thresholdMilliseconds < 0 ? 0 : thresholdMilliseconds; + _timingId = Guid.NewGuid().ToString("N").Substring(0, 7); // keep it short-ish - // internal - created by profiling logger - internal DisposableTimer( - ILogger logger, - LogLevel level, - IProfiler profiler, - Type loggerType, - string startMessage, - string endMessage, - string? failMessage = null, - object[]? startMessageArgs = null, - object[]? endMessageArgs = null, - object[]? failMessageArgs = null, - int thresholdMilliseconds = 0) + if (thresholdMilliseconds == 0) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _level = level; - _loggerType = loggerType ?? throw new ArgumentNullException(nameof(loggerType)); - _endMessage = endMessage; - _failMessage = failMessage; - _endMessageArgs = endMessageArgs; - _failMessageArgs = failMessageArgs; - _thresholdMilliseconds = thresholdMilliseconds < 0 ? 0 : thresholdMilliseconds; - _timingId = Guid.NewGuid().ToString("N").Substring(0, 7); // keep it short-ish + switch (_level) + { + case LogLevel.Debug: + if (startMessageArgs == null) + { + logger.LogDebug("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + } + else + { + var args = new object[startMessageArgs.Length + 1]; + startMessageArgs.CopyTo(args, 0); + args[startMessageArgs.Length] = _timingId; + logger.LogDebug(startMessage + " [Timing {TimingId}]", args); + } - if (thresholdMilliseconds == 0) + break; + case LogLevel.Information: + if (startMessageArgs == null) + { + logger.LogInformation("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + } + else + { + var args = new object[startMessageArgs.Length + 1]; + startMessageArgs.CopyTo(args, 0); + args[startMessageArgs.Length] = _timingId; + logger.LogInformation(startMessage + " [Timing {TimingId}]", args); + } + + break; + default: + throw new ArgumentOutOfRangeException(nameof(level)); + } + } + + // else aren't logging the start message, this is output to the profiler but not the log, + // we just want the log to contain the result if it's more than the minimum ms threshold. + _profilerStep = profiler?.Step(loggerType, startMessage); + } + + public Stopwatch Stopwatch { get; } = Stopwatch.StartNew(); + + /// + /// Reports a failure. + /// + /// The fail message. + /// The exception. + /// Completion of the timer will be reported as an error, with the specified message and exception. + public void Fail(string? failMessage = null, Exception? exception = null) + { + _failed = true; + _failMessage = failMessage ?? _failMessage ?? "Failed."; + _failException = exception; + } + + /// + /// Disposes resources. + /// + /// Overrides abstract class which handles required locking. + protected override void DisposeResources() + { + Stopwatch.Stop(); + + _profilerStep?.Dispose(); + + if ((Stopwatch.ElapsedMilliseconds >= _thresholdMilliseconds || _failed) + && _loggerType != null && _logger != null + && (string.IsNullOrWhiteSpace(_endMessage) == false || _failed)) + { + if (_failed) + { + if (_failMessageArgs is null) + { + _logger.LogError(_failException, "{FailMessage} ({Duration}ms) [Timing {TimingId}]", _failMessage, Stopwatch.ElapsedMilliseconds, _timingId); + } + else + { + var args = new object[_failMessageArgs.Length + 2]; + _failMessageArgs.CopyTo(args, 0); + args[_failMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; + args[_failMessageArgs.Length] = _timingId; + _logger.LogError(_failException, _failMessage + " ({Duration}ms) [Timing {TimingId}]", args); + } + } + else { switch (_level) { case LogLevel.Debug: - if (startMessageArgs == null) + if (_endMessageArgs == null) { - logger.LogDebug("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + _logger.LogDebug( + "{EndMessage} ({Duration}ms) [Timing {TimingId}]", + _endMessage, + Stopwatch.ElapsedMilliseconds, + _timingId); } else { - var args = new object[startMessageArgs.Length + 1]; - startMessageArgs.CopyTo(args, 0); - args[startMessageArgs.Length] = _timingId; - logger.LogDebug(startMessage + " [Timing {TimingId}]", args); + var args = new object[_endMessageArgs.Length + 2]; + _endMessageArgs.CopyTo(args, 0); + args[^1] = Stopwatch.ElapsedMilliseconds; + args[args.Length] = _timingId; + _logger.LogDebug(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); } + break; case LogLevel.Information: - if (startMessageArgs == null) + if (_endMessageArgs == null) { - logger.LogInformation("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + _logger.LogInformation( + "{EndMessage} ({Duration}ms) [Timing {TimingId}]", + _endMessage, + Stopwatch.ElapsedMilliseconds, + _timingId); } else { - var args = new object[startMessageArgs.Length + 1]; - startMessageArgs.CopyTo(args, 0); - args[startMessageArgs.Length] = _timingId; - logger.LogInformation(startMessage + " [Timing {TimingId}]", args); + var args = new object[_endMessageArgs.Length + 2]; + _endMessageArgs.CopyTo(args, 0); + args[_endMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; + args[_endMessageArgs.Length] = _timingId; + _logger.LogInformation(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); } + break; - default: - throw new ArgumentOutOfRangeException(nameof(level)); - } - } - // else aren't logging the start message, this is output to the profiler but not the log, - // we just want the log to contain the result if it's more than the minimum ms threshold. - - _profilerStep = profiler?.Step(loggerType, startMessage); - } - - /// - /// Reports a failure. - /// - /// The fail message. - /// The exception. - /// Completion of the timer will be reported as an error, with the specified message and exception. - public void Fail(string? failMessage = null, Exception? exception = null) - { - _failed = true; - _failMessage = failMessage ?? _failMessage ?? "Failed."; - _failException = exception; - } - - public Stopwatch Stopwatch { get; } = Stopwatch.StartNew(); - - /// - ///Disposes resources. - /// - /// Overrides abstract class which handles required locking. - protected override void DisposeResources() - { - Stopwatch.Stop(); - - _profilerStep?.Dispose(); - - if ((Stopwatch.ElapsedMilliseconds >= _thresholdMilliseconds || _failed) - && _loggerType != null && _logger != null - && (string.IsNullOrWhiteSpace(_endMessage) == false || _failed)) - { - if (_failed) - { - if (_failMessageArgs is null) - { - _logger.LogError(_failException, "{FailMessage} ({Duration}ms) [Timing {TimingId}]", _failMessage, Stopwatch.ElapsedMilliseconds, _timingId); - } - else - { - var args = new object[_failMessageArgs.Length + 2]; - _failMessageArgs.CopyTo(args, 0); - args[_failMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; - args[_failMessageArgs.Length] = _timingId; - _logger.LogError(_failException, _failMessage + " ({Duration}ms) [Timing {TimingId}]", args); - } - } - else - { - switch (_level) - { - case LogLevel.Debug: - if (_endMessageArgs == null) - { - _logger.LogDebug("{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); - } - else - { - var args = new object[_endMessageArgs.Length + 2]; - _endMessageArgs.CopyTo(args, 0); - args[args.Length - 1] = Stopwatch.ElapsedMilliseconds; - args[args.Length] = _timingId; - _logger.LogDebug(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); - } - break; - case LogLevel.Information: - if (_endMessageArgs == null) - { - _logger.LogInformation("{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); - } - else - { - var args = new object[_endMessageArgs.Length + 2]; - _endMessageArgs.CopyTo(args, 0); - args[_endMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; - args[_endMessageArgs.Length] = _timingId; - _logger.LogInformation(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); - } - break; - // filtered in the ctor - //default: - // throw new Exception(); - } + // filtered in the ctor + // default: + // throw new Exception(); } } } diff --git a/src/Umbraco.Core/Logging/ILoggingConfiguration.cs b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs index 34e4d702c6..662ee7891c 100644 --- a/src/Umbraco.Core/Logging/ILoggingConfiguration.cs +++ b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs @@ -1,11 +1,9 @@ -namespace Umbraco.Cms.Core.Logging -{ +namespace Umbraco.Cms.Core.Logging; - public interface ILoggingConfiguration - { - /// - /// Gets the physical path where logs are stored - /// - string LogDirectory { get; } - } +public interface ILoggingConfiguration +{ + /// + /// Gets the physical path where logs are stored + /// + string LogDirectory { get; } } diff --git a/src/Umbraco.Core/Logging/IMessageTemplates.cs b/src/Umbraco.Core/Logging/IMessageTemplates.cs index 99d88ce926..252f91aaa5 100644 --- a/src/Umbraco.Core/Logging/IMessageTemplates.cs +++ b/src/Umbraco.Core/Logging/IMessageTemplates.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Provides tools to support message templates. +/// +public interface IMessageTemplates { - /// - /// Provides tools to support message templates. - /// - public interface IMessageTemplates - { - string Render(string messageTemplate, params object[] args); - } + string Render(string messageTemplate, params object[] args); } diff --git a/src/Umbraco.Core/Logging/IProfiler.cs b/src/Umbraco.Core/Logging/IProfiler.cs index 4b2bf6fc48..ab580d6aae 100644 --- a/src/Umbraco.Core/Logging/IProfiler.cs +++ b/src/Umbraco.Core/Logging/IProfiler.cs @@ -1,32 +1,30 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +/// +/// Defines the profiling service. +/// +public interface IProfiler { + /// + /// Gets an that will time the code between its creation and disposal. + /// + /// The name of the step. + /// A step. + /// The returned is meant to be used within a using (...) {{ ... }} block. + IDisposable? Step(string name); /// - /// Defines the profiling service. + /// Starts the profiler. /// - public interface IProfiler - { - /// - /// Gets an that will time the code between its creation and disposal. - /// - /// The name of the step. - /// A step. - /// The returned is meant to be used within a using (...) {{ ... }} block. - IDisposable? Step(string name); + void Start(); - /// - /// Starts the profiler. - /// - void Start(); - - /// - /// Stops the profiler. - /// - /// A value indicating whether to discard results. - /// Set discardResult to true to abandon all profiling - useful when eg someone is not - /// authenticated or you want to clear the results, based upon some other mechanism. - void Stop(bool discardResults = false); - } + /// + /// Stops the profiler. + /// + /// A value indicating whether to discard results. + /// + /// Set discardResult to true to abandon all profiling - useful when eg someone is not + /// authenticated or you want to clear the results, based upon some other mechanism. + /// + void Stop(bool discardResults = false); } diff --git a/src/Umbraco.Core/Logging/IProfilerHtml.cs b/src/Umbraco.Core/Logging/IProfilerHtml.cs index 30812fc156..806ee54e7a 100644 --- a/src/Umbraco.Core/Logging/IProfilerHtml.cs +++ b/src/Umbraco.Core/Logging/IProfilerHtml.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Used to render a profiler in a web page +/// +public interface IProfilerHtml { /// - /// Used to render a profiler in a web page + /// Renders the profiling results. /// - public interface IProfilerHtml - { - /// - /// Renders the profiling results. - /// - /// The profiling results. - /// Generally used for HTML rendering. - string Render(); - } + /// The profiling results. + /// Generally used for HTML rendering. + string Render(); } diff --git a/src/Umbraco.Core/Logging/IProfilingLogger.cs b/src/Umbraco.Core/Logging/IProfilingLogger.cs index 5873619988..92c4d55f0c 100644 --- a/src/Umbraco.Core/Logging/IProfilingLogger.cs +++ b/src/Umbraco.Core/Logging/IProfilingLogger.cs @@ -1,40 +1,65 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +/// +/// Defines the profiling logging service. +/// +public interface IProfilingLogger { /// - /// Defines the profiling logging service. + /// Profiles an action and log as information messages. /// - public interface IProfilingLogger - { - /// - /// Profiles an action and log as information messages. - /// - DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null); + DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null); - /// - /// Profiles an action and log as information messages. - /// - DisposableTimer TraceDuration(string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); + /// + /// Profiles an action and log as information messages. + /// + DisposableTimer TraceDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); - /// - /// Profiles an action and log as information messages. - /// - DisposableTimer TraceDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); + /// + /// Profiles an action and log as information messages. + /// + DisposableTimer TraceDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); - /// - /// Profiles an action and log as debug messages. - /// - DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null); + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null); - /// - /// Profiles an action and log as debug messages. - /// - DisposableTimer? DebugDuration(string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer? DebugDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); - /// - /// Profiles an action and log as debug messages. - /// - DisposableTimer? DebugDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); - } + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer? DebugDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); } diff --git a/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs b/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs index c9e1b09e08..2981dd5987 100644 --- a/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs +++ b/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs @@ -1,24 +1,22 @@ -using System; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class LogHttpRequest { - public static class LogHttpRequest + private static readonly string RequestIdItemName = typeof(LogHttpRequest).Name + "+RequestId"; + + /// + /// Retrieve the id assigned to the currently-executing HTTP request, if any. + /// + /// The request id. + /// + /// true if there is a request in progress; false otherwise. + public static bool TryGetCurrentHttpRequestId(out Guid? requestId, IRequestCache requestCache) { - static readonly string RequestIdItemName = typeof(LogHttpRequest).Name + "+RequestId"; + var requestIdItem = requestCache.Get(RequestIdItemName, () => Guid.NewGuid()); + requestId = (Guid?)requestIdItem; - /// - /// Retrieve the id assigned to the currently-executing HTTP request, if any. - /// - /// The request id. - /// - /// true if there is a request in progress; false otherwise. - public static bool TryGetCurrentHttpRequestId(out Guid? requestId, IRequestCache requestCache) - { - var requestIdItem = requestCache.Get(RequestIdItemName, () => Guid.NewGuid()); - requestId = (Guid?)requestIdItem; - - return true; - } + return true; } } diff --git a/src/Umbraco.Core/Logging/LogLevel.cs b/src/Umbraco.Core/Logging/LogLevel.cs index 9e12002324..b7271ecf04 100644 --- a/src/Umbraco.Core/Logging/LogLevel.cs +++ b/src/Umbraco.Core/Logging/LogLevel.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Specifies the level of a log event. +/// +public enum LogLevel { - /// - /// Specifies the level of a log event. - /// - public enum LogLevel - { - Verbose, - Debug, - Information, - Warning, - Error, - Fatal - } + Verbose, + Debug, + Information, + Warning, + Error, + Fatal, } diff --git a/src/Umbraco.Core/Logging/LogProfiler.cs b/src/Umbraco.Core/Logging/LogProfiler.cs index 1f4b4bbe90..0504a2a1ae 100644 --- a/src/Umbraco.Core/Logging/LogProfiler.cs +++ b/src/Umbraco.Core/Logging/LogProfiler.cs @@ -1,57 +1,52 @@ -using System; using System.Diagnostics; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Implements by writing profiling results to an . +/// +public class LogProfiler : IProfiler { - /// - /// Implements by writing profiling results to an . - /// - public class LogProfiler : IProfiler + private readonly ILogger _logger; + + public LogProfiler(ILogger logger) => _logger = logger; + + /// + public IDisposable Step(string name) { - private readonly ILogger _logger; + _logger.LogDebug("Begin: {ProfileName}", name); + return new LightDisposableTimer(duration => + _logger.LogInformation("End {ProfileName} ({ProfileDuration}ms)", name, duration)); + } - public LogProfiler(ILogger logger) + /// + public void Start() + { + // the log will always be started + } + + /// + public void Stop(bool discardResults = false) + { + // the log never stops + } + + // a lightweight disposable timer + private class LightDisposableTimer : DisposableObjectSlim + { + private readonly Action _callback; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + + protected internal LightDisposableTimer(Action callback) { - _logger = logger; + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); } - /// - public IDisposable Step(string name) + protected override void DisposeResources() { - _logger.LogDebug("Begin: {ProfileName}", name); - return new LightDisposableTimer(duration => _logger.LogInformation("End {ProfileName} ({ProfileDuration}ms)", name, duration)); - } - - /// - public void Start() - { - // the log will always be started - } - - /// - public void Stop(bool discardResults = false) - { - // the log never stops - } - - // a lightweight disposable timer - private class LightDisposableTimer : DisposableObjectSlim - { - private readonly Action _callback; - private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); - - protected internal LightDisposableTimer(Action callback) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); - _callback = callback; - } - - protected override void DisposeResources() - { - _stopwatch.Stop(); - _callback(_stopwatch.ElapsedMilliseconds); - } + _stopwatch.Stop(); + _callback(_stopwatch.ElapsedMilliseconds); } } } diff --git a/src/Umbraco.Core/Logging/LoggingConfiguration.cs b/src/Umbraco.Core/Logging/LoggingConfiguration.cs index f191af3023..d2a24d24a9 100644 --- a/src/Umbraco.Core/Logging/LoggingConfiguration.cs +++ b/src/Umbraco.Core/Logging/LoggingConfiguration.cs @@ -1,14 +1,9 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +public class LoggingConfiguration : ILoggingConfiguration { - public class LoggingConfiguration : ILoggingConfiguration - { - public LoggingConfiguration(string logDirectory) - { - LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory)); - } + public LoggingConfiguration(string logDirectory) => + LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory)); - public string LogDirectory { get; } - } + public string LogDirectory { get; } } diff --git a/src/Umbraco.Core/Logging/LoggingTaskExtension.cs b/src/Umbraco.Core/Logging/LoggingTaskExtension.cs index 5a6f995dfa..950e9bb8f4 100644 --- a/src/Umbraco.Core/Logging/LoggingTaskExtension.cs +++ b/src/Umbraco.Core/Logging/LoggingTaskExtension.cs @@ -1,52 +1,50 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +internal static class LoggingTaskExtension { - internal static class LoggingTaskExtension + /// + /// This task shouldn't be waited on (as it's not guaranteed to run), and you shouldn't wait on the parent task either + /// (because it might throw an + /// exception that doesn't get handled). If you want to be waiting on something, use LogErrorsWaitable instead. + /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably + /// be handling + /// errors yourself. + /// + public static Task LogErrors(this Task task, Action logMethod) => + task.ContinueWith( + t => LogErrorsInner(t, logMethod), + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + + // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + TaskScheduler.Default); + + /// + /// This task can be waited on (as it's guaranteed to run), and you should wait on this rather than the parent task. + /// Because it's + /// guaranteed to run, it may be slower than using LogErrors, and you should consider using that method if you don't + /// want to wait. + /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably + /// be handling + /// errors yourself. + /// + public static Task LogErrorsWaitable(this Task task, Action logMethod) => + task.ContinueWith( + t => LogErrorsInner(t, logMethod), + + // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + TaskScheduler.Default); + + private static void LogErrorsInner(Task task, Action logAction) { - /// - /// This task shouldn't be waited on (as it's not guaranteed to run), and you shouldn't wait on the parent task either (because it might throw an - /// exception that doesn't get handled). If you want to be waiting on something, use LogErrorsWaitable instead. - /// - /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably be handling - /// errors yourself. - /// - public static Task LogErrors(this Task task, Action logMethod) + if (task.Exception != null) { - return task.ContinueWith( - t => LogErrorsInner(t, logMethod), - CancellationToken.None, - TaskContinuationOptions.OnlyOnFaulted, - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - } - - /// - /// This task can be waited on (as it's guaranteed to run), and you should wait on this rather than the parent task. Because it's - /// guaranteed to run, it may be slower than using LogErrors, and you should consider using that method if you don't want to wait. - /// - /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably be handling - /// errors yourself. - /// - public static Task LogErrorsWaitable(this Task task, Action logMethod) - { - return task.ContinueWith( - t => LogErrorsInner(t, logMethod), - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - } - - private static void LogErrorsInner(Task task, Action logAction) - { - if (task.Exception != null) + logAction( + "Aggregate Exception with " + task.Exception.InnerExceptions.Count + " inner exceptions: ", + task.Exception); + foreach (Exception innerException in task.Exception.InnerExceptions) { - logAction("Aggregate Exception with " + task.Exception.InnerExceptions.Count + " inner exceptions: ", task.Exception); - foreach (var innerException in task.Exception.InnerExceptions) - { - logAction("Inner exception from aggregate exception: ", innerException); - } + logAction("Inner exception from aggregate exception: ", innerException); } } } diff --git a/src/Umbraco.Core/Logging/NoopProfiler.cs b/src/Umbraco.Core/Logging/NoopProfiler.cs index 89a0307515..821728c7a6 100644 --- a/src/Umbraco.Core/Logging/NoopProfiler.cs +++ b/src/Umbraco.Core/Logging/NoopProfiler.cs @@ -1,26 +1,23 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +public class NoopProfiler : IProfiler { - public class NoopProfiler : IProfiler + private readonly VoidDisposable _disposable = new(); + + public IDisposable Step(string name) => _disposable; + + public void Start() { - private readonly VoidDisposable _disposable = new VoidDisposable(); + } - public IDisposable Step(string name) + public void Stop(bool discardResults = false) + { + } + + private class VoidDisposable : DisposableObjectSlim + { + protected override void DisposeResources() { - return _disposable; - } - - public void Start() - { } - - public void Stop(bool discardResults = false) - { } - - private class VoidDisposable : DisposableObjectSlim - { - protected override void DisposeResources() - { } } } } diff --git a/src/Umbraco.Core/Logging/ProfilerExtensions.cs b/src/Umbraco.Core/Logging/ProfilerExtensions.cs index 67739c2f38..e69506702a 100644 --- a/src/Umbraco.Core/Logging/ProfilerExtensions.cs +++ b/src/Umbraco.Core/Logging/ProfilerExtensions.cs @@ -1,39 +1,52 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +internal static class ProfilerExtensions { - internal static class ProfilerExtensions + /// + /// Gets an that will time the code between its creation and disposal, + /// prefixing the name of the step with a reporting type name. + /// + /// The reporting type. + /// The profiler. + /// The name of the step. + /// A step. + /// The returned is meant to be used within a using (...) {{ ... }} block. + internal static IDisposable? Step(this IProfiler profiler, string name) { - /// - /// Gets an that will time the code between its creation and disposal, - /// prefixing the name of the step with a reporting type name. - /// - /// The reporting type. - /// The profiler. - /// The name of the step. - /// A step. - /// The returned is meant to be used within a using (...) {{ ... }} block. - internal static IDisposable? Step(this IProfiler profiler, string name) + if (profiler == null) { - if (profiler == null) throw new ArgumentNullException(nameof(profiler)); - return profiler.Step(typeof (T), name); + throw new ArgumentNullException(nameof(profiler)); } - /// - /// Gets an that will time the code between its creation and disposal, - /// prefixing the name of the step with a reporting type name. - /// - /// The profiler. - /// The reporting type. - /// The name of the step. - /// A step. - /// The returned is meant to be used within a using (...) {{ ... }} block. - internal static IDisposable? Step(this IProfiler profiler, Type reporting, string name) + return profiler.Step(typeof(T), name); + } + + /// + /// Gets an that will time the code between its creation and disposal, + /// prefixing the name of the step with a reporting type name. + /// + /// The profiler. + /// The reporting type. + /// The name of the step. + /// A step. + /// The returned is meant to be used within a using (...) {{ ... }} block. + internal static IDisposable? Step(this IProfiler profiler, Type reporting, string name) + { + if (profiler == null) { - if (profiler == null) throw new ArgumentNullException(nameof(profiler)); - if (reporting == null) throw new ArgumentNullException(nameof(reporting)); - if (name == null) throw new ArgumentNullException(nameof(name)); - return profiler.Step($"[{reporting.Name}] {name}"); + throw new ArgumentNullException(nameof(profiler)); } + + if (reporting == null) + { + throw new ArgumentNullException(nameof(reporting)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return profiler.Step($"[{reporting.Name}] {name}"); } } diff --git a/src/Umbraco.Core/Logging/ProfilingLogger.cs b/src/Umbraco.Core/Logging/ProfilingLogger.cs index d3388bda01..997f139539 100644 --- a/src/Umbraco.Core/Logging/ProfilingLogger.cs +++ b/src/Umbraco.Core/Logging/ProfilingLogger.cs @@ -1,99 +1,145 @@ -using System; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Provides logging and profiling services. +/// +public sealed class ProfilingLogger : IProfilingLogger { /// - /// Provides logging and profiling services. + /// Initializes a new instance of the class. /// - public sealed class ProfilingLogger : IProfilingLogger + public ProfilingLogger(ILogger logger, IProfiler profiler) { - /// - /// Gets the underlying implementation. - /// - public ILogger Logger { get; } - - /// - /// Gets the underlying implementation. - /// - public IProfiler Profiler { get; } - - /// - /// Initializes a new instance of the class. - /// - public ProfilingLogger(ILogger logger, IProfiler profiler) - { - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); - } - - /// - /// Initializes a new instance of the class. - /// - public ProfilingLogger(ILogger logger, IProfiler profiler) - { - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); - } - - public DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null) - => TraceDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs); - - public DisposableTimer TraceDuration(string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => new DisposableTimer(Logger, LogLevel.Information, Profiler, typeof(T), startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); - - public DisposableTimer TraceDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => new DisposableTimer(Logger, LogLevel.Information, Profiler, loggerType, startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); - - public DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null) - => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) - ? DebugDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs) - : null; - - public DisposableTimer? DebugDuration(string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) - ? new DisposableTimer(Logger, LogLevel.Debug, Profiler, typeof(T), startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs, thresholdMilliseconds) - : null; - - public DisposableTimer? DebugDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) - ? new DisposableTimer(Logger, LogLevel.Debug, Profiler, loggerType, startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs, thresholdMilliseconds) - : null; - - #region ILogger - - public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level) - => Logger.IsEnabled(level); - - public void LogCritical(Exception exception, string messageTemplate, params object[] propertyValues) - => Logger.LogCritical(exception, messageTemplate, propertyValues); - - public void LogCritical(string messageTemplate, params object[] propertyValues) - => Logger.LogCritical(messageTemplate, propertyValues); - - public void LogError(Exception exception, string messageTemplate, params object[] propertyValues) - => Logger.LogError(exception, messageTemplate, propertyValues); - - public void LogError(string messageTemplate, params object[] propertyValues) - => Logger.LogError(messageTemplate, propertyValues); - - public void LogWarning(string messageTemplate, params object[] propertyValues) - => Logger.LogWarning(messageTemplate, propertyValues); - - public void LogWarning(Exception exception, string messageTemplate, params object[] propertyValues) - => Logger.LogWarning(exception, messageTemplate, propertyValues); - - public void LogInformation(string messageTemplate, params object[] propertyValues) - => Logger.LogInformation(messageTemplate, propertyValues); - - public void LogDebug(string messageTemplate, params object[] propertyValues) - => Logger.LogDebug(messageTemplate, propertyValues); - - public void LogTrace(string messageTemplate, params object[] propertyValues) - => Logger.LogTrace(messageTemplate, propertyValues); - - - - #endregion + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); } + + /// + /// Initializes a new instance of the class. + /// + public ProfilingLogger(ILogger logger, IProfiler profiler) + { + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); + } + + /// + /// Gets the underlying implementation. + /// + public ILogger Logger { get; } + + /// + /// Gets the underlying implementation. + /// + public IProfiler Profiler { get; } + + public DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null) + => TraceDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs); + + public DisposableTimer TraceDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => new(Logger, LogLevel.Information, Profiler, typeof(T), startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); + + public DisposableTimer TraceDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => new(Logger, LogLevel.Information, Profiler, loggerType, startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); + + public DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null) + => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) + ? DebugDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs) + : null; + + public DisposableTimer? DebugDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) + ? new DisposableTimer( + Logger, + LogLevel.Debug, + Profiler, + typeof(T), + startMessage, + completeMessage, + failMessage, + startMessageArgs, + endMessageArgs, + failMessageArgs, + thresholdMilliseconds) + : null; + + public DisposableTimer? DebugDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) + ? new DisposableTimer( + Logger, + LogLevel.Debug, + Profiler, + loggerType, + startMessage, + completeMessage, + failMessage, + startMessageArgs, + endMessageArgs, + failMessageArgs, + thresholdMilliseconds) + : null; + + #region ILogger + + public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level) + => Logger.IsEnabled(level); + + public void LogCritical(Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.LogCritical(exception, messageTemplate, propertyValues); + + public void LogCritical(string messageTemplate, params object[] propertyValues) + => Logger.LogCritical(messageTemplate, propertyValues); + + public void LogError(Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.LogError(exception, messageTemplate, propertyValues); + + public void LogError(string messageTemplate, params object[] propertyValues) + => Logger.LogError(messageTemplate, propertyValues); + + public void LogWarning(string messageTemplate, params object[] propertyValues) + => Logger.LogWarning(messageTemplate, propertyValues); + + public void LogWarning(Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.LogWarning(exception, messageTemplate, propertyValues); + + public void LogInformation(string messageTemplate, params object[] propertyValues) + => Logger.LogInformation(messageTemplate, propertyValues); + + public void LogDebug(string messageTemplate, params object[] propertyValues) + => Logger.LogDebug(messageTemplate, propertyValues); + + public void LogTrace(string messageTemplate, params object[] propertyValues) + => Logger.LogTrace(messageTemplate, propertyValues); + + #endregion } diff --git a/src/Umbraco.Core/Macros/IMacroRenderer.cs b/src/Umbraco.Core/Macros/IMacroRenderer.cs index bac3d36268..f1e7d8c383 100644 --- a/src/Umbraco.Core/Macros/IMacroRenderer.cs +++ b/src/Umbraco.Core/Macros/IMacroRenderer.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +/// +/// Renders a macro +/// +public interface IMacroRenderer { - /// - /// Renders a macro - /// - public interface IMacroRenderer - { - Task RenderAsync(string macroAlias, IPublishedContent? content, IDictionary? macroParams); - } + Task RenderAsync(string macroAlias, IPublishedContent? content, IDictionary? macroParams); } diff --git a/src/Umbraco.Core/Macros/MacroContent.cs b/src/Umbraco.Core/Macros/MacroContent.cs index 7998b00fd7..c36c630168 100644 --- a/src/Umbraco.Core/Macros/MacroContent.cs +++ b/src/Umbraco.Core/Macros/MacroContent.cs @@ -1,20 +1,17 @@ -using System; +namespace Umbraco.Cms.Core.Macros; -namespace Umbraco.Cms.Core.Macros +// represents the content of a macro +public class MacroContent { - // represents the content of a macro - public class MacroContent - { - // gets or sets the text content - public string? Text { get; set; } + // gets an empty macro content + public static MacroContent Empty { get; } = new(); - // gets or sets the date the content was generated - public DateTime Date { get; set; } = DateTime.Now; + // gets or sets the text content + public string? Text { get; set; } - // a value indicating whether the content is empty - public bool IsEmpty => Text is null; + // gets or sets the date the content was generated + public DateTime Date { get; set; } = DateTime.Now; - // gets an empty macro content - public static MacroContent Empty { get; } = new MacroContent(); - } + // a value indicating whether the content is empty + public bool IsEmpty => Text is null; } diff --git a/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs b/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs index b3c505682a..49a53f11b0 100644 --- a/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs +++ b/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs @@ -1,29 +1,28 @@ -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +public enum MacroErrorBehaviour { - public enum MacroErrorBehaviour - { - /// - /// Default umbraco behavior - show an inline error within the - /// macro but allow the page to continue rendering. - /// - Inline, + /// + /// Default umbraco behavior - show an inline error within the + /// macro but allow the page to continue rendering. + /// + Inline, - /// - /// Silently eat the error and do not display the offending macro. - /// - Silent, + /// + /// Silently eat the error and do not display the offending macro. + /// + Silent, - /// - /// Throw an exception which can be caught by the global error handler - /// defined in Application_OnError. If no such error handler is defined - /// then you'll see the Yellow Screen Of Death (YSOD) error page. - /// - Throw, + /// + /// Throw an exception which can be caught by the global error handler + /// defined in Application_OnError. If no such error handler is defined + /// then you'll see the Yellow Screen Of Death (YSOD) error page. + /// + Throw, - /// - /// Silently eat the error and display the custom content reported in - /// the error event args - /// - Content - } + /// + /// Silently eat the error and display the custom content reported in + /// the error event args + /// + Content, } diff --git a/src/Umbraco.Core/Macros/MacroModel.cs b/src/Umbraco.Core/Macros/MacroModel.cs index 5242b14d86..12649bf91c 100644 --- a/src/Umbraco.Core/Macros/MacroModel.cs +++ b/src/Umbraco.Core/Macros/MacroModel.cs @@ -1,57 +1,61 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +public class MacroModel { - public class MacroModel + public MacroModel() { - /// - /// The Macro Id - /// - public int Id { get; set; } + } - /// - /// The Macro Name - /// - public string? Name { get; set; } - - /// - /// The Macro Alias - /// - public string? Alias { get; set; } - - public string? MacroSource { get; set; } - - public int CacheDuration { get; set; } - - public bool CacheByPage { get; set; } - - public bool CacheByMember { get; set; } - - public bool RenderInEditor { get; set; } - - public string? CacheIdentifier { get; set; } - - public List Properties { get; } = new List(); - - public MacroModel() - { } - - public MacroModel(IMacro macro) + public MacroModel(IMacro? macro) + { + if (macro == null) { - if (macro == null) return; + return; + } - Id = macro.Id; - Name = macro.Name; - Alias = macro.Alias; - MacroSource = macro.MacroSource; - CacheDuration = macro.CacheDuration; - CacheByPage = macro.CacheByPage; - CacheByMember = macro.CacheByMember; - RenderInEditor = macro.UseInEditor; + Id = macro.Id; + Name = macro.Name; + Alias = macro.Alias; + MacroSource = macro.MacroSource; + CacheDuration = macro.CacheDuration; + CacheByPage = macro.CacheByPage; + CacheByMember = macro.CacheByMember; + RenderInEditor = macro.UseInEditor; - foreach (var prop in macro.Properties) - Properties.Add(new MacroPropertyModel(prop.Alias, string.Empty, prop.EditorAlias)); + foreach (IMacroProperty prop in macro.Properties) + { + Properties.Add(new MacroPropertyModel(prop.Alias, string.Empty, prop.EditorAlias)); } } + + /// + /// The Macro Id + /// + public int Id { get; set; } + + /// + /// The Macro Name + /// + public string? Name { get; set; } + + /// + /// The Macro Alias + /// + public string? Alias { get; set; } + + public string? MacroSource { get; set; } + + public int CacheDuration { get; set; } + + public bool CacheByPage { get; set; } + + public bool CacheByMember { get; set; } + + public bool RenderInEditor { get; set; } + + public string? CacheIdentifier { get; set; } + + public List Properties { get; } = new(); } diff --git a/src/Umbraco.Core/Macros/MacroPropertyModel.cs b/src/Umbraco.Core/Macros/MacroPropertyModel.cs index 643d154f21..c1022c3561 100644 --- a/src/Umbraco.Core/Macros/MacroPropertyModel.cs +++ b/src/Umbraco.Core/Macros/MacroPropertyModel.cs @@ -1,29 +1,25 @@ -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +public class MacroPropertyModel { - public class MacroPropertyModel + public MacroPropertyModel() => Key = string.Empty; + + public MacroPropertyModel(string key, string value) { - public string Key { get; set; } - - public string? Value { get; set; } - - public string? Type { get; set; } - - public MacroPropertyModel() - { - Key = string.Empty; - } - - public MacroPropertyModel(string key, string value) - { - Key = key; - Value = value; - } - - public MacroPropertyModel(string key, string value, string type) - { - Key = key; - Value = value; - Type = type; - } + Key = key; + Value = value; } + + public MacroPropertyModel(string key, string value, string type) + { + Key = key; + Value = value; + Type = type; + } + + public string Key { get; set; } + + public string? Value { get; set; } + + public string? Type { get; set; } } diff --git a/src/Umbraco.Core/Mail/IEmailSender.cs b/src/Umbraco.Core/Mail/IEmailSender.cs index 0c573c542c..2eb8cc8263 100644 --- a/src/Umbraco.Core/Mail/IEmailSender.cs +++ b/src/Umbraco.Core/Mail/IEmailSender.cs @@ -1,17 +1,15 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Mail +namespace Umbraco.Cms.Core.Mail; + +/// +/// Simple abstraction to send an email message +/// +public interface IEmailSender { - /// - /// Simple abstraction to send an email message - /// - public interface IEmailSender - { - Task SendAsync(EmailMessage message, string emailType); + Task SendAsync(EmailMessage message, string emailType); - Task SendAsync(EmailMessage message, string emailType, bool enableNotification); + Task SendAsync(EmailMessage message, string emailType, bool enableNotification); - bool CanSendRequiredEmail(); - } + bool CanSendRequiredEmail(); } diff --git a/src/Umbraco.Core/Mail/ISmsSender.cs b/src/Umbraco.Core/Mail/ISmsSender.cs index 885ad89da2..3c09bdc7e6 100644 --- a/src/Umbraco.Core/Mail/ISmsSender.cs +++ b/src/Umbraco.Core/Mail/ISmsSender.cs @@ -1,14 +1,10 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Mail; -namespace Umbraco.Cms.Core.Mail +/// +/// Service to send an SMS +/// +public interface ISmsSender { - /// - /// Service to send an SMS - /// - public interface ISmsSender - { - // borrowed from https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/common/samples/WebApplication1/Services/ISmsSender.cs#L8 - - Task SendSmsAsync(string number, string message); - } + // borrowed from https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/common/samples/WebApplication1/Services/ISmsSender.cs#L8 + Task SendSmsAsync(string number, string message); } diff --git a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs index 15e36767d9..5b1fa0923a 100644 --- a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs +++ b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs @@ -1,19 +1,18 @@ -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Mail +namespace Umbraco.Cms.Core.Mail; + +internal class NotImplementedEmailSender : IEmailSender { - internal class NotImplementedEmailSender : IEmailSender - { - public Task SendAsync(EmailMessage message, string emailType) - => throw new NotImplementedException("To send an Email ensure IEmailSender is implemented with a custom implementation"); + public Task SendAsync(EmailMessage message, string emailType) + => throw new NotImplementedException( + "To send an Email ensure IEmailSender is implemented with a custom implementation"); - public Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => - throw new NotImplementedException( - "To send an Email ensure IEmailSender is implemented with a custom implementation"); + public Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => + throw new NotImplementedException( + "To send an Email ensure IEmailSender is implemented with a custom implementation"); - public bool CanSendRequiredEmail() - => throw new NotImplementedException("To send an Email ensure IEmailSender is implemented with a custom implementation"); - } + public bool CanSendRequiredEmail() + => throw new NotImplementedException( + "To send an Email ensure IEmailSender is implemented with a custom implementation"); } diff --git a/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs b/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs index 0cb5016a1b..b3901d5ab9 100644 --- a/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs +++ b/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs @@ -1,14 +1,11 @@ -using System; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Mail; -namespace Umbraco.Cms.Core.Mail +/// +/// An that throws +/// +internal class NotImplementedSmsSender : ISmsSender { - /// - /// An that throws - /// - internal class NotImplementedSmsSender : ISmsSender - { - public Task SendSmsAsync(string number, string message) - => throw new NotImplementedException("To send an SMS ensure ISmsSender is implemented with a custom implementation"); - } + public Task SendSmsAsync(string number, string message) + => throw new NotImplementedException( + "To send an SMS ensure ISmsSender is implemented with a custom implementation"); } diff --git a/src/Umbraco.Core/Manifest/BundleOptions.cs b/src/Umbraco.Core/Manifest/BundleOptions.cs index 810efb6c45..fe04c205d9 100644 --- a/src/Umbraco.Core/Manifest/BundleOptions.cs +++ b/src/Umbraco.Core/Manifest/BundleOptions.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +public enum BundleOptions { - public enum BundleOptions - { - /// - /// The default bundling behavior for assets in the package folder. - /// - /// - /// The assets will be bundled with the typical packages bundle. - /// - Default = 0, + /// + /// The default bundling behavior for assets in the package folder. + /// + /// + /// The assets will be bundled with the typical packages bundle. + /// + Default = 0, - /// - /// The assets in the package will not be processed at all and will all be requested as individual assets. - /// - /// - /// This will essentially be a bundle that has composite processing turned off for both debug and production. - /// - None = 1, + /// + /// The assets in the package will not be processed at all and will all be requested as individual assets. + /// + /// + /// This will essentially be a bundle that has composite processing turned off for both debug and production. + /// + None = 1, - /// - /// The packages assets will be processed as it's own separate bundle. (in debug, files will not be processed) - /// - Independent = 2 - } + /// + /// The packages assets will be processed as it's own separate bundle. (in debug, files will not be processed) + /// + Independent = 2, } diff --git a/src/Umbraco.Core/Manifest/CompositePackageManifest.cs b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs index 939d635fc3..5e41681ea6 100644 --- a/src/Umbraco.Core/Manifest/CompositePackageManifest.cs +++ b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs @@ -1,67 +1,63 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// A package manifest made up of all combined manifests +/// +public class CompositePackageManifest { + public CompositePackageManifest( + IReadOnlyList propertyEditors, + IReadOnlyList parameterEditors, + IReadOnlyList gridEditors, + IReadOnlyList contentApps, + IReadOnlyList dashboards, + IReadOnlyList sections, + IReadOnlyDictionary> scripts, + IReadOnlyDictionary> stylesheets) + { + PropertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + ParameterEditors = parameterEditors ?? throw new ArgumentNullException(nameof(parameterEditors)); + GridEditors = gridEditors ?? throw new ArgumentNullException(nameof(gridEditors)); + ContentApps = contentApps ?? throw new ArgumentNullException(nameof(contentApps)); + Dashboards = dashboards ?? throw new ArgumentNullException(nameof(dashboards)); + Sections = sections ?? throw new ArgumentNullException(nameof(sections)); + Scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); + Stylesheets = stylesheets ?? throw new ArgumentNullException(nameof(stylesheets)); + } /// - /// A package manifest made up of all combined manifests + /// Gets or sets the property editors listed in the manifest. /// - public class CompositePackageManifest - { - public CompositePackageManifest( - IReadOnlyList propertyEditors, - IReadOnlyList parameterEditors, - IReadOnlyList gridEditors, - IReadOnlyList contentApps, - IReadOnlyList dashboards, - IReadOnlyList sections, - IReadOnlyDictionary> scripts, - IReadOnlyDictionary> stylesheets) - { - PropertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); - ParameterEditors = parameterEditors ?? throw new ArgumentNullException(nameof(parameterEditors)); - GridEditors = gridEditors ?? throw new ArgumentNullException(nameof(gridEditors)); - ContentApps = contentApps ?? throw new ArgumentNullException(nameof(contentApps)); - Dashboards = dashboards ?? throw new ArgumentNullException(nameof(dashboards)); - Sections = sections ?? throw new ArgumentNullException(nameof(sections)); - Scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); - Stylesheets = stylesheets ?? throw new ArgumentNullException(nameof(stylesheets)); - } + public IReadOnlyList PropertyEditors { get; } - /// - /// Gets or sets the property editors listed in the manifest. - /// - public IReadOnlyList PropertyEditors { get; } + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + public IReadOnlyList ParameterEditors { get; } - /// - /// Gets or sets the parameter editors listed in the manifest. - /// - public IReadOnlyList ParameterEditors { get; } + /// + /// Gets or sets the grid editors listed in the manifest. + /// + public IReadOnlyList GridEditors { get; } - /// - /// Gets or sets the grid editors listed in the manifest. - /// - public IReadOnlyList GridEditors { get; } + /// + /// Gets or sets the content apps listed in the manifest. + /// + public IReadOnlyList ContentApps { get; } - /// - /// Gets or sets the content apps listed in the manifest. - /// - public IReadOnlyList ContentApps { get; } + /// + /// Gets or sets the dashboards listed in the manifest. + /// + public IReadOnlyList Dashboards { get; } - /// - /// Gets or sets the dashboards listed in the manifest. - /// - public IReadOnlyList Dashboards { get; } + /// + /// Gets or sets the sections listed in the manifest. + /// + public IReadOnlyCollection Sections { get; } - /// - /// Gets or sets the sections listed in the manifest. - /// - public IReadOnlyCollection Sections { get; } + public IReadOnlyDictionary> Scripts { get; } - public IReadOnlyDictionary> Scripts { get; } - - public IReadOnlyDictionary> Stylesheets { get; } - } + public IReadOnlyDictionary> Stylesheets { get; } } diff --git a/src/Umbraco.Core/Manifest/IManifestFilter.cs b/src/Umbraco.Core/Manifest/IManifestFilter.cs index 0984f1a889..d2998a0839 100644 --- a/src/Umbraco.Core/Manifest/IManifestFilter.cs +++ b/src/Umbraco.Core/Manifest/IManifestFilter.cs @@ -1,19 +1,16 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Manifest +/// +/// Provides filtering for package manifests. +/// +public interface IManifestFilter { /// - /// Provides filtering for package manifests. + /// Filters package manifests. /// - public interface IManifestFilter - { - /// - /// Filters package manifests. - /// - /// The package manifests. - /// - /// It is possible to remove, change, or add manifests. - /// - void Filter(List manifests); - } + /// The package manifests. + /// + /// It is possible to remove, change, or add manifests. + /// + void Filter(List manifests); } diff --git a/src/Umbraco.Core/Manifest/IManifestParser.cs b/src/Umbraco.Core/Manifest/IManifestParser.cs index 09d3ccbe1c..f8b29e9f56 100644 --- a/src/Umbraco.Core/Manifest/IManifestParser.cs +++ b/src/Umbraco.Core/Manifest/IManifestParser.cs @@ -1,26 +1,23 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Manifest +public interface IManifestParser { - public interface IManifestParser - { - string AppPluginsPath { get; set; } + string AppPluginsPath { get; set; } - /// - /// Gets all manifests, merged into a single manifest object. - /// - /// - CompositePackageManifest CombinedManifest { get; } + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + CompositePackageManifest CombinedManifest { get; } - /// - /// Parses a manifest. - /// - PackageManifest ParseManifest(string text); + /// + /// Parses a manifest. + /// + PackageManifest ParseManifest(string text); - /// - /// Returns all package individual manifests - /// - /// - IEnumerable GetManifests(); - } + /// + /// Returns all package individual manifests + /// + /// + IEnumerable GetManifests(); } diff --git a/src/Umbraco.Core/Manifest/IPackageManifest.cs b/src/Umbraco.Core/Manifest/IPackageManifest.cs index 39e4878233..ba911b183c 100644 --- a/src/Umbraco.Core/Manifest/IPackageManifest.cs +++ b/src/Umbraco.Core/Manifest/IPackageManifest.cs @@ -1,65 +1,66 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +public interface IPackageManifest { - public interface IPackageManifest - { - /// - /// Gets the source path of the manifest. - /// - /// - /// Gets the full absolute file path of the manifest, - /// using system directory separators. - /// - string Source { get; set; } + /// + /// Gets the source path of the manifest. + /// + /// + /// + /// Gets the full absolute file path of the manifest, + /// using system directory separators. + /// + /// + string Source { get; set; } - /// - /// Gets or sets the scripts listed in the manifest. - /// - [DataMember(Name = "javascript")] - string[] Scripts { get; set; } + /// + /// Gets or sets the scripts listed in the manifest. + /// + [DataMember(Name = "javascript")] + string[] Scripts { get; set; } - /// - /// Gets or sets the stylesheets listed in the manifest. - /// - [DataMember(Name = "css")] - string[] Stylesheets { get; set; } + /// + /// Gets or sets the stylesheets listed in the manifest. + /// + [DataMember(Name = "css")] + string[] Stylesheets { get; set; } - /// - /// Gets or sets the property editors listed in the manifest. - /// - [DataMember(Name = "propertyEditors")] - IDataEditor[] PropertyEditors { get; set; } + /// + /// Gets or sets the property editors listed in the manifest. + /// + [DataMember(Name = "propertyEditors")] + IDataEditor[] PropertyEditors { get; set; } - /// - /// Gets or sets the parameter editors listed in the manifest. - /// - [DataMember(Name = "parameterEditors")] - IDataEditor[] ParameterEditors { get; set; } + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + [DataMember(Name = "parameterEditors")] + IDataEditor[] ParameterEditors { get; set; } - /// - /// Gets or sets the grid editors listed in the manifest. - /// - [DataMember(Name = "gridEditors")] - GridEditor[] GridEditors { get; set; } + /// + /// Gets or sets the grid editors listed in the manifest. + /// + [DataMember(Name = "gridEditors")] + GridEditor[] GridEditors { get; set; } - /// - /// Gets or sets the content apps listed in the manifest. - /// - [DataMember(Name = "contentApps")] - ManifestContentAppDefinition[] ContentApps { get; set; } + /// + /// Gets or sets the content apps listed in the manifest. + /// + [DataMember(Name = "contentApps")] + ManifestContentAppDefinition[] ContentApps { get; set; } - /// - /// Gets or sets the dashboards listed in the manifest. - /// - [DataMember(Name = "dashboards")] - ManifestDashboard[] Dashboards { get; set; } + /// + /// Gets or sets the dashboards listed in the manifest. + /// + [DataMember(Name = "dashboards")] + ManifestDashboard[] Dashboards { get; set; } - /// - /// Gets or sets the sections listed in the manifest. - /// - [DataMember(Name = "sections")] - ManifestSection[] Sections { get; set; } - } + /// + /// Gets or sets the sections listed in the manifest. + /// + [DataMember(Name = "sections")] + ManifestSection[] Sections { get; set; } } diff --git a/src/Umbraco.Core/Manifest/ManifestAssets.cs b/src/Umbraco.Core/Manifest/ManifestAssets.cs index 6532e2f63d..2bd84a1bdd 100644 --- a/src/Umbraco.Core/Manifest/ManifestAssets.cs +++ b/src/Umbraco.Core/Manifest/ManifestAssets.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Manifest +public class ManifestAssets { - public class ManifestAssets + public ManifestAssets(string? packageName, IReadOnlyList assets) { - public ManifestAssets(string? packageName, IReadOnlyList assets) - { - PackageName = packageName ?? throw new ArgumentNullException(nameof(packageName)); - Assets = assets ?? throw new ArgumentNullException(nameof(assets)); - } - - public string PackageName { get; } - public IReadOnlyList Assets { get; } + PackageName = packageName ?? throw new ArgumentNullException(nameof(packageName)); + Assets = assets ?? throw new ArgumentNullException(nameof(assets)); } + + public string PackageName { get; } + + public IReadOnlyList Assets { get; } } diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs index ed44742bc0..5bfc2a740e 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs @@ -1,75 +1,72 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +// contentApps: [ +// { +// name: 'App Name', // required +// alias: 'appAlias', // required +// weight: 0, // optional, default is 0, use values between -99 and +99 +// icon: 'icon.app', // required +// view: 'path/view.htm', // required +// show: [ // optional, default is always show +// '-content/foo', // hide for content type 'foo' +// '+content/*', // show for all other content types +// '+media/*', // show for all media types +// '+role/admin' // show for admin users. Role based permissions will override others. +// ] +// }, +// ... +// ] + +/// +/// Represents a content app definition, parsed from a manifest. +/// +/// Is used to create an actual . +[DataContract(Name = "appdef", Namespace = "")] +public class ManifestContentAppDefinition { - // contentApps: [ - // { - // name: 'App Name', // required - // alias: 'appAlias', // required - // weight: 0, // optional, default is 0, use values between -99 and +99 - // icon: 'icon.app', // required - // view: 'path/view.htm', // required - // show: [ // optional, default is always show - // '-content/foo', // hide for content type 'foo' - // '+content/*', // show for all other content types - // '+media/*', // show for all media types - // '+role/admin' // show for admin users. Role based permissions will override others. - // ] - // }, - // ... - // ] + private readonly string? _view; /// - /// Represents a content app definition, parsed from a manifest. + /// Gets or sets the name of the content app. /// - /// Is used to create an actual . - [DataContract(Name = "appdef", Namespace = "")] - public class ManifestContentAppDefinition - { - private string? _view; + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// Gets or sets the name of the content app. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + /// + /// Gets or sets the unique alias of the content app. + /// + /// + /// Must be a valid javascript identifier, ie no spaces etc. + /// + [DataMember(Name = "alias")] + public string? Alias { get; set; } - /// - /// Gets or sets the unique alias of the content app. - /// - /// - /// Must be a valid javascript identifier, ie no spaces etc. - /// - [DataMember(Name = "alias")] - public string? Alias { get; set; } + /// + /// Gets or sets the weight of the content app. + /// + [DataMember(Name = "weight")] + public int Weight { get; set; } - /// - /// Gets or sets the weight of the content app. - /// - [DataMember(Name = "weight")] - public int Weight { get; set; } + /// + /// Gets or sets the icon of the content app. + /// + /// + /// Must be a valid helveticons class name (see http://hlvticons.ch/). + /// + [DataMember(Name = "icon")] + public string? Icon { get; set; } - /// - /// Gets or sets the icon of the content app. - /// - /// - /// Must be a valid helveticons class name (see http://hlvticons.ch/). - /// - [DataMember(Name = "icon")] - public string? Icon { get; set; } + /// + /// Gets or sets the view for rendering the content app. + /// + [DataMember(Name = "view")] + public string? View { get; set; } - /// - /// Gets or sets the view for rendering the content app. - /// - [DataMember(Name = "view")] - public string? View { get; set; } - - /// - /// Gets or sets the list of 'show' conditions for the content app. - /// - [DataMember(Name = "show")] - public string[] Show { get; set; } = Array.Empty(); - - } + /// + /// Gets or sets the list of 'show' conditions for the content app. + /// + [DataMember(Name = "show")] + public string[] Show { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs index c4bc87e9a2..122ecc1cb7 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -8,182 +5,202 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +// contentApps: [ +// { +// name: 'App Name', // required +// alias: 'appAlias', // required +// weight: 0, // optional, default is 0, use values between -99 and +99 +// icon: 'icon.app', // required +// view: 'path/view.htm', // required +// show: [ // optional, default is always show +// '-content/foo', // hide for content type 'foo' +// '+content/*', // show for all other content types +// '+media/*', // show for all media types +// '-member/foo' // hide for member type 'foo' +// '+member/*' // show for all member types +// '+role/admin' // show for admin users. Role based permissions will override others. +// ] +// }, +// ... +// ] + +/// +/// Represents a content app factory, for content apps parsed from the manifest. +/// +public class ManifestContentAppFactory : IContentAppFactory { - // contentApps: [ - // { - // name: 'App Name', // required - // alias: 'appAlias', // required - // weight: 0, // optional, default is 0, use values between -99 and +99 - // icon: 'icon.app', // required - // view: 'path/view.htm', // required - // show: [ // optional, default is always show - // '-content/foo', // hide for content type 'foo' - // '+content/*', // show for all other content types - // '+media/*', // show for all media types - // '-member/foo' // hide for member type 'foo' - // '+member/*' // show for all member types - // '+role/admin' // show for admin users. Role based permissions will override others. - // ] - // }, - // ... - // ] + private readonly ManifestContentAppDefinition _definition; + private readonly IIOHelper _ioHelper; - /// - /// Represents a content app factory, for content apps parsed from the manifest. - /// - public class ManifestContentAppFactory : IContentAppFactory + private ContentApp? _app; + private ShowRule[]? _showRules; + + public ManifestContentAppFactory(ManifestContentAppDefinition definition, IIOHelper ioHelper) { - private readonly ManifestContentAppDefinition _definition; - private readonly IIOHelper _ioHelper; + _definition = definition; + _ioHelper = ioHelper; + } - public ManifestContentAppFactory(ManifestContentAppDefinition definition, IIOHelper ioHelper) + /// + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + { + string? partA, partB; + + switch (o) { - _definition = definition; - _ioHelper = ioHelper; + case IContent content: + partA = "content"; + partB = content.ContentType.Alias; + break; + + case IMedia media: + partA = "media"; + partB = media.ContentType.Alias; + break; + case IMember member: + partA = "member"; + partB = member.ContentType.Alias; + break; + case IContentType contentType: + partA = "contentType"; + partB = contentType?.Alias; + break; + case IDictionaryItem _: + partA = "dictionary"; + partB = "*"; // Not really a different type for dictionary items + break; + + default: + return null; } - private ContentApp? _app; - private ShowRule[]? _showRules; + ShowRule[] rules = _showRules ??= ShowRule.Parse(_definition.Show).ToArray(); + var userGroupsList = userGroups.ToList(); - /// - public ContentApp? GetContentAppFor(object o,IEnumerable userGroups) + var okRole = false; + var hasRole = false; + var okType = false; + var hasType = false; + + foreach (ShowRule rule in rules) { - string? partA, partB; - - switch (o) + if (rule.PartA?.InvariantEquals("role") ?? false) { - case IContent content: - partA = "content"; - partB = content.ContentType.Alias; - break; - - case IMedia media: - partA = "media"; - partB = media.ContentType.Alias; - break; - case IMember member: - partA = "member"; - partB = member.ContentType.Alias; - break; - case IContentType contentType: - partA = "contentType"; - partB = contentType?.Alias; - break; - case IDictionaryItem _: - partA = "dictionary"; - partB = "*"; //Not really a different type for dictionary items - break; - - default: - return null; - } - - var rules = _showRules ?? (_showRules = ShowRule.Parse(_definition.Show).ToArray()); - var userGroupsList = userGroups.ToList(); - - var okRole = false; - var hasRole = false; - var okType = false; - var hasType = false; - - foreach (var rule in rules) - { - if (rule.PartA?.InvariantEquals("role") ?? false) + // if roles have been ok-ed already, skip the rule + if (okRole) { - // if roles have been ok-ed already, skip the rule - if (okRole) - continue; - - // remember we have role rules - hasRole = true; - - foreach (var group in userGroupsList) - { - // if the entry does not apply, skip - if (!rule.Matches("role", group.Alias)) - continue; - - // if the entry applies, - // if it's an exclude entry, exit, do not display the content app - if (!rule.Show) - return null; - - // else ok to display, remember roles are ok, break from userGroupsList - okRole = rule.Show; - break; - } + continue; } - else // it is a type rule + + // remember we have role rules + hasRole = true; + + foreach (IReadOnlyUserGroup group in userGroupsList) { - // if type has been ok-ed already, skip the rule - if (okType) - continue; - - // remember we have type rules - hasType = true; - - // if the entry does not apply, skip it - if (!rule.Matches(partA, partB)) + // if the entry does not apply, skip + if (!rule.Matches("role", group.Alias)) + { continue; + } // if the entry applies, // if it's an exclude entry, exit, do not display the content app if (!rule.Show) - return null; - - // else ok to display, remember type rules are ok - okType = true; - } - } - - // if roles rules are specified but not ok, - // or if type roles are specified but not ok, - // cannot display the content app - if ((hasRole && !okRole) || (hasType && !okType)) - return null; - - // else - // content app can be displayed - return _app ??= new ContentApp - { - Alias = _definition.Alias, - Name = _definition.Name, - Icon = _definition.Icon, - View = _ioHelper.ResolveRelativeOrVirtualUrl(_definition.View), - Weight = _definition.Weight - }; - } - - private class ShowRule - { - private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public bool Show { get; private set; } - public string? PartA { get; private set; } - public string? PartB { get; private set; } - - public bool Matches(string? partA, string? partB) - { - return (PartA == "*" || (PartA?.InvariantEquals(partA) ?? false)) && (PartB == "*" || (PartB?.InvariantEquals(partB) ?? false)); - } - - public static IEnumerable Parse(string[] rules) - { - foreach (var rule in rules) - { - var match = ShowRegex.Match(rule); - if (!match.Success) - throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest."); - - yield return new ShowRule { - Show = match.Groups[1].Value != "-", - PartA = match.Groups[2].Value, - PartB = match.Groups[3].Value - }; + return null; + } + + // else ok to display, remember roles are ok, break from userGroupsList + okRole = rule.Show; + break; } } + + // it is a type rule + else + { + // if type has been ok-ed already, skip the rule + if (okType) + { + continue; + } + + // remember we have type rules + hasType = true; + + // if the entry does not apply, skip it + if (!rule.Matches(partA, partB)) + { + continue; + } + + // if the entry applies, + // if it's an exclude entry, exit, do not display the content app + if (!rule.Show) + { + return null; + } + + // else ok to display, remember type rules are ok + okType = true; + } } + + // if roles rules are specified but not ok, + // or if type roles are specified but not ok, + // cannot display the content app + if ((hasRole && !okRole) || (hasType && !okType)) + { + return null; + } + + // else + // content app can be displayed + return _app ??= new ContentApp + { + Alias = _definition.Alias, + Name = _definition.Name, + Icon = _definition.Icon, + View = _ioHelper.ResolveRelativeOrVirtualUrl(_definition.View), + Weight = _definition.Weight, + }; + } + + private class ShowRule + { + private static readonly Regex ShowRegex = new( + "^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public bool Show { get; private set; } + + public string? PartA { get; private set; } + + public string? PartB { get; private set; } + + public static IEnumerable Parse(string[] rules) + { + foreach (var rule in rules) + { + Match match = ShowRegex.Match(rule); + if (!match.Success) + { + throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest."); + } + + yield return new ShowRule + { + Show = match.Groups[1].Value != "-", + PartA = match.Groups[2].Value, + PartB = match.Groups[3].Value, + }; + } + } + + public bool Matches(string? partA, string? partB) => + (PartA == "*" || (PartA?.InvariantEquals(partA) ?? false)) && + (PartB == "*" || (PartB?.InvariantEquals(partB) ?? false)); } } diff --git a/src/Umbraco.Core/Manifest/ManifestDashboard.cs b/src/Umbraco.Core/Manifest/ManifestDashboard.cs index a10c3a2177..75cdf24ebe 100644 --- a/src/Umbraco.Core/Manifest/ManifestDashboard.cs +++ b/src/Umbraco.Core/Manifest/ManifestDashboard.cs @@ -1,25 +1,23 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Dashboards; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +[DataContract] +public class ManifestDashboard : IDashboard { - [DataContract] - public class ManifestDashboard : IDashboard - { - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } = null!; + [DataMember(Name = "weight")] + public int Weight { get; set; } = 100; - [DataMember(Name = "weight")] - public int Weight { get; set; } = 100; + [DataMember(Name = "alias", IsRequired = true)] + public string Alias { get; set; } = null!; - [DataMember(Name = "view", IsRequired = true)] - public string View { get; set; } = null!; + [DataMember(Name = "view", IsRequired = true)] + public string View { get; set; } = null!; - [DataMember(Name = "sections")] - public string[] Sections { get; set; } = Array.Empty(); + [DataMember(Name = "sections")] + public string[] Sections { get; set; } = Array.Empty(); - [DataMember(Name = "access")] - public IAccessRule[] AccessRules { get; set; } = Array.Empty(); - } + [DataMember(Name = "access")] + public IAccessRule[] AccessRules { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs index 9c692f69b3..a1d5cac0c1 100644 --- a/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs @@ -1,26 +1,26 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Manifest -{ - /// - /// Contains the manifest filters. - /// - public class ManifestFilterCollection : BuilderCollectionBase - { - public ManifestFilterCollection(Func> items) : base(items) - { - } +namespace Umbraco.Cms.Core.Manifest; - /// - /// Filters package manifests. - /// - /// The package manifests. - public void Filter(List manifests) +/// +/// Contains the manifest filters. +/// +public class ManifestFilterCollection : BuilderCollectionBase +{ + public ManifestFilterCollection(Func> items) + : base(items) + { + } + + /// + /// Filters package manifests. + /// + /// The package manifests. + public void Filter(List manifests) + { + foreach (IManifestFilter filter in this) { - foreach (var filter in this) - filter.Filter(manifests); + filter.Filter(manifests); } } } diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs index 00ac3609dd..5f012d10c9 100644 --- a/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs @@ -1,13 +1,13 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Manifest -{ - public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase - { - protected override ManifestFilterCollectionBuilder This => this; +namespace Umbraco.Cms.Core.Manifest; - // do NOT cache this, it's only used once - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; - } +public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override ManifestFilterCollectionBuilder This => this; + + // do NOT cache this, it's only used once + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; } diff --git a/src/Umbraco.Core/Manifest/ManifestSection.cs b/src/Umbraco.Core/Manifest/ManifestSection.cs index 864a0734e2..c7671c91e2 100644 --- a/src/Umbraco.Core/Manifest/ManifestSection.cs +++ b/src/Umbraco.Core/Manifest/ManifestSection.cs @@ -1,15 +1,14 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Sections; -namespace Umbraco.Cms.Core.Manifest -{ - [DataContract(Name = "section", Namespace = "")] - public class ManifestSection : ISection - { - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; +namespace Umbraco.Cms.Core.Manifest; - [DataMember(Name = "name")] - public string Name { get; set; } = string.Empty; - } +[DataContract(Name = "section", Namespace = "")] +public class ManifestSection : ISection +{ + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; + + [DataMember(Name = "name")] + public string Name { get; set; } = string.Empty; } diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index a71cf1f6f6..7bf07cfde9 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -1,115 +1,115 @@ -using System; -using System.IO; using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Represents the content of a package manifest. +/// +[DataContract] +public class PackageManifest { + private string? _packageName; /// - /// Represents the content of a package manifest. + /// An optional package name. If not specified then the directory name is used. /// - [DataContract] - public class PackageManifest + [DataMember(Name = "name")] + public string? PackageName { - private string? _packageName; - - /// - /// An optional package name. If not specified then the directory name is used. - /// - [DataMember(Name = "name")] - public string? PackageName + get { - get + if (!_packageName.IsNullOrWhiteSpace()) { - if (!_packageName.IsNullOrWhiteSpace()) - { - return _packageName; - } - if (!Source.IsNullOrWhiteSpace()) - { - _packageName = Path.GetFileName(Path.GetDirectoryName(Source)); - } return _packageName; } - set => _packageName = value; + + if (!Source.IsNullOrWhiteSpace()) + { + _packageName = Path.GetFileName(Path.GetDirectoryName(Source)); + } + + return _packageName; } - - [DataMember(Name = "packageView")] - public string? PackageView { get; set; } - - /// - /// Gets the source path of the manifest. - /// - /// - /// Gets the full absolute file path of the manifest, - /// using system directory separators. - /// - [IgnoreDataMember] - public string Source { get; set; } = null!; - - /// - /// Gets or sets the version of the package - /// - [DataMember(Name = "version")] - public string Version { get; set; } = string.Empty; - - /// - /// Gets or sets a value indicating whether telemetry is allowed - /// - [DataMember(Name = "allowPackageTelemetry")] - public bool AllowPackageTelemetry { get; set; } = true; - - [DataMember(Name = "bundleOptions")] - public BundleOptions BundleOptions { get; set; } - - /// - /// Gets or sets the scripts listed in the manifest. - /// - [DataMember(Name = "javascript")] - public string[] Scripts { get; set; } = Array.Empty(); - - /// - /// Gets or sets the stylesheets listed in the manifest. - /// - [DataMember(Name = "css")] - public string[] Stylesheets { get; set; } = Array.Empty(); - - /// - /// Gets or sets the property editors listed in the manifest. - /// - [DataMember(Name = "propertyEditors")] - public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); - - /// - /// Gets or sets the parameter editors listed in the manifest. - /// - [DataMember(Name = "parameterEditors")] - public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); - - /// - /// Gets or sets the grid editors listed in the manifest. - /// - [DataMember(Name = "gridEditors")] - public GridEditor[] GridEditors { get; set; } = Array.Empty(); - - /// - /// Gets or sets the content apps listed in the manifest. - /// - [DataMember(Name = "contentApps")] - public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); - - /// - /// Gets or sets the dashboards listed in the manifest. - /// - [DataMember(Name = "dashboards")] - public ManifestDashboard[] Dashboards { get; set; } = Array.Empty(); - - /// - /// Gets or sets the sections listed in the manifest. - /// - [DataMember(Name = "sections")] - public ManifestSection[] Sections { get; set; } = Array.Empty(); + set => _packageName = value; } + + [DataMember(Name = "packageView")] + public string? PackageView { get; set; } + + /// + /// Gets the source path of the manifest. + /// + /// + /// + /// Gets the full absolute file path of the manifest, + /// using system directory separators. + /// + /// + [IgnoreDataMember] + public string Source { get; set; } = null!; + + /// + /// Gets or sets the version of the package + /// + [DataMember(Name = "version")] + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether telemetry is allowed + /// + [DataMember(Name = "allowPackageTelemetry")] + public bool AllowPackageTelemetry { get; set; } = true; + + [DataMember(Name = "bundleOptions")] + public BundleOptions BundleOptions { get; set; } + + /// + /// Gets or sets the scripts listed in the manifest. + /// + [DataMember(Name = "javascript")] + public string[] Scripts { get; set; } = Array.Empty(); + + /// + /// Gets or sets the stylesheets listed in the manifest. + /// + [DataMember(Name = "css")] + public string[] Stylesheets { get; set; } = Array.Empty(); + + /// + /// Gets or sets the property editors listed in the manifest. + /// + [DataMember(Name = "propertyEditors")] + public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); + + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + [DataMember(Name = "parameterEditors")] + public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); + + /// + /// Gets or sets the grid editors listed in the manifest. + /// + [DataMember(Name = "gridEditors")] + public GridEditor[] GridEditors { get; set; } = Array.Empty(); + + /// + /// Gets or sets the content apps listed in the manifest. + /// + [DataMember(Name = "contentApps")] + public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); + + /// + /// Gets or sets the dashboards listed in the manifest. + /// + [DataMember(Name = "dashboards")] + public ManifestDashboard[] Dashboards { get; set; } = Array.Empty(); + + /// + /// Gets or sets the sections listed in the manifest. + /// + [DataMember(Name = "sections")] + public ManifestSection[] Sections { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Mapping/IMapDefinition.cs b/src/Umbraco.Core/Mapping/IMapDefinition.cs index 3d4270c93e..db836fa3b8 100644 --- a/src/Umbraco.Core/Mapping/IMapDefinition.cs +++ b/src/Umbraco.Core/Mapping/IMapDefinition.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Mapping +namespace Umbraco.Cms.Core.Mapping; + +/// +/// Defines maps for . +/// +public interface IMapDefinition { /// - /// Defines maps for . + /// Defines maps. /// - public interface IMapDefinition - { - /// - /// Defines maps. - /// - void DefineMaps(IUmbracoMapper mapper); - } + void DefineMaps(IUmbracoMapper mapper); } diff --git a/src/Umbraco.Core/Mapping/IUmbracoMapper.cs b/src/Umbraco.Core/Mapping/IUmbracoMapper.cs index c99359cbdf..5cbee7164c 100644 --- a/src/Umbraco.Core/Mapping/IUmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/IUmbracoMapper.cs @@ -1,156 +1,158 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Mapping; -namespace Umbraco.Cms.Core.Mapping +public interface IUmbracoMapper { - public interface IUmbracoMapper - { - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - void Define(); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + void Define(); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A mapping method. - void Define(Action map); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A mapping method. + void Define(Action map); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - void Define(Func ctor); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + void Define(Func ctor); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - /// A mapping method. - void Define(Func ctor, Action map); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + /// A mapping method. + void Define( + Func ctor, + Action map); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// The target object. - TTarget? Map(object? source); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// The target object. + TTarget? Map(object? source); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - TTarget? Map(object? source, Action f); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + TTarget? Map(object? source, Action f); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - TTarget? Map(object? source, MapperContext context); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + TTarget? Map(object? source, MapperContext context); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - TTarget? Map(TSource? source); + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + TTarget? Map(TSource? source); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - TTarget? Map(TSource source, Action f); + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + TTarget? Map(TSource source, Action f); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - TTarget? Map(TSource? source, MapperContext context); + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + TTarget? Map(TSource? source, MapperContext context); - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// The target object. - TTarget Map(TSource source, TTarget target); + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// The target object. + TTarget Map(TSource source, TTarget target); - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context preparation method. - /// The target object. - TTarget Map(TSource source, TTarget target, Action f); + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context preparation method. + /// The target object. + TTarget Map(TSource source, TTarget target, Action f); - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context. - /// The target object. - TTarget Map(TSource source, TTarget target, MapperContext context); + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context. + /// The target object. + TTarget Map(TSource source, TTarget target, MapperContext context); - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A list containing the target objects. - List MapEnumerable(IEnumerable source); + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A list containing the target objects. + List MapEnumerable(IEnumerable source); - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context preparation method. - /// A list containing the target objects. - List MapEnumerable(IEnumerable source, Action f); + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context preparation method. + /// A list containing the target objects. + List MapEnumerable( + IEnumerable source, + Action f); - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context. - /// A list containing the target objects. - List MapEnumerable(IEnumerable source, MapperContext context); - } + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context. + /// A list containing the target objects. + List MapEnumerable( + IEnumerable source, + MapperContext context); } diff --git a/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs b/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs index 27d4ad73d0..db35a3ffac 100644 --- a/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs +++ b/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Mapping +namespace Umbraco.Cms.Core.Mapping; + +public class MapDefinitionCollection : BuilderCollectionBase { - public class MapDefinitionCollection : BuilderCollectionBase + public MapDefinitionCollection(Func> items) + : base(items) { - public MapDefinitionCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs b/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs index 698dce1648..1ac6de5b33 100644 --- a/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs +++ b/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs @@ -1,12 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Mapping -{ - public class MapDefinitionCollectionBuilder : SetCollectionBuilderBase - { - protected override MapDefinitionCollectionBuilder This => this; +namespace Umbraco.Cms.Core.Mapping; - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; - } +public class MapDefinitionCollectionBuilder : SetCollectionBuilderBase +{ + protected override MapDefinitionCollectionBuilder This => this; + + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; } diff --git a/src/Umbraco.Core/Mapping/MapperContext.cs b/src/Umbraco.Core/Mapping/MapperContext.cs index 2355e9bd05..ef8663beeb 100644 --- a/src/Umbraco.Core/Mapping/MapperContext.cs +++ b/src/Umbraco.Core/Mapping/MapperContext.cs @@ -1,129 +1,120 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Mapping; -namespace Umbraco.Cms.Core.Mapping +/// +/// Represents a mapper context. +/// +public class MapperContext { + private readonly IUmbracoMapper _mapper; + private IDictionary? _items; + /// - /// Represents a mapper context. + /// Initializes a new instance of the class. /// - public class MapperContext + public MapperContext(IUmbracoMapper mapper) => _mapper = mapper; + + /// + /// Gets a value indicating whether the context has items. + /// + public bool HasItems => _items != null; + + /// + /// Gets the context items. + /// + public IDictionary Items => _items ??= new Dictionary(); + + #region Map + + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(object? source) + => _mapper.Map(source, this); + + // let's say this is a bad (dangerous) idea, and leave it out for now + /* + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(object source, Action f) { - private readonly IUmbracoMapper _mapper; - private IDictionary? _items; - - /// - /// Initializes a new instance of the class. - /// - public MapperContext(IUmbracoMapper mapper) - { - _mapper = mapper; - } - - /// - /// Gets a value indicating whether the context has items. - /// - public bool HasItems => _items != null; - - /// - /// Gets the context items. - /// - public IDictionary Items => _items ?? (_items = new Dictionary()); - - #region Map - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(object? source) - => _mapper.Map(source, this); - - // let's say this is a bad (dangerous) idea, and leave it out for now - /* - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(object source, Action f) - { - f(this); - return _mapper.Map(source, this); - } - */ - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(TSource? source) - => _mapper.Map(source, this); - - // let's say this is a bad (dangerous) idea, and leave it out for now - /* - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(TSource source, Action f) - { - f(this); - return _mapper.Map(source, this); - } - */ - - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// The target object. - public TTarget Map(TSource source, TTarget target) - => _mapper.Map(source, target, this); - - // let's say this is a bad (dangerous) idea, and leave it out for now - /* - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(TSource source, TTarget target, Action f) - { - f(this); - return _mapper.Map(source, target, this); - } - */ - - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source) - { - return source.Select(Map).Where(x => x is not null).ToList()!; - } - - #endregion + f(this); + return _mapper.Map(source, this); } + */ + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(TSource? source) + => _mapper.Map(source, this); + + // let's say this is a bad (dangerous) idea, and leave it out for now + /* + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(TSource source, Action f) + { + f(this); + return _mapper.Map(source, this); + } + */ + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// The target object. + public TTarget Map(TSource source, TTarget target) + => _mapper.Map(source, target, this); + + // let's say this is a bad (dangerous) idea, and leave it out for now + /* + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(TSource source, TTarget target, Action f) + { + f(this); + return _mapper.Map(source, target, this); + } + */ + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source) => + source.Select(Map).Where(x => x is not null).ToList()!; + + #endregion } diff --git a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs index b79e1a8de2..ab3c36031c 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs @@ -1,34 +1,33 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Dailymotion the popular online video-sharing platform. +/// +public class DailyMotion : EmbedProviderBase { - // TODO (V10): change base class to OEmbedProviderBase - public class DailyMotion : EmbedProviderBase + public DailyMotion(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://www.dailymotion.com/services/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"dailymotion.com/video/.*" - }; + public override string ApiEndpoint => "https://www.dailymotion.com/services/oembed"; - public override Dictionary RequestParams => new Dictionary() - { - //ApiUrl/?format=xml - {"format", "xml"} - }; + public override string[] UrlSchemeRegex => new[] { @"dailymotion.com/video/.*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new() + { + // ApiUrl/?format=xml + { "format", "xml" }, + }; - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public DailyMotion(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs index 6d745d3d49..d0a8727442 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs @@ -1,14 +1,12 @@ -using System; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +[Obsolete("Use OEmbedProviderBase instead- This will be removed in Umbraco 12")] +public abstract class EmbedProviderBase : OEmbedProviderBase { - [Obsolete("Use OEmbedProviderBase instead")] - public abstract class EmbedProviderBase : OEmbedProviderBase + protected EmbedProviderBase(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - protected EmbedProviderBase(IJsonSerializer jsonSerializer) - : base(jsonSerializer) - { - } } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs index 615d16f51c..655d68b878 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +public class EmbedProvidersCollection : BuilderCollectionBase { - public class EmbedProvidersCollection : BuilderCollectionBase + public EmbedProvidersCollection(Func> items) + : base(items) { - public EmbedProvidersCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs index f79880b61f..121785d7eb 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs @@ -1,9 +1,8 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +public class EmbedProvidersCollectionBuilder : OrderedCollectionBuilderBase { - public class EmbedProvidersCollectionBuilder : OrderedCollectionBuilderBase - { - protected override EmbedProvidersCollectionBuilder This => this; - } + protected override EmbedProvidersCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs index 2ea5fd8109..e1842ed238 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs @@ -1,37 +1,35 @@ -using System.Collections.Generic; using System.Net; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Flickr the popular online image hosting and video hosting service. +/// +public class Flickr : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Flickr : EmbedProviderBase + public Flickr(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.flickr.com/services/oembed/"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"flickr.com\/photos\/*", - @"flic.kr\/p\/*" - }; + public override string ApiEndpoint => "http://www.flickr.com/services/oembed/"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"flickr.com\/photos\/*", @"flic.kr\/p\/*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - var imageUrl = GetXmlProperty(xmlDocument, "/oembed/url"); - var imageWidth = GetXmlProperty(xmlDocument, "/oembed/width"); - var imageHeight = GetXmlProperty(xmlDocument, "/oembed/height"); - var imageTitle = GetXmlProperty(xmlDocument, "/oembed/title"); + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - return string.Format("\"{3}\"", imageUrl, imageWidth, imageHeight, WebUtility.HtmlEncode(imageTitle)); - } + var imageUrl = GetXmlProperty(xmlDocument, "/oembed/url"); + var imageWidth = GetXmlProperty(xmlDocument, "/oembed/width"); + var imageHeight = GetXmlProperty(xmlDocument, "/oembed/height"); + var imageTitle = GetXmlProperty(xmlDocument, "/oembed/title"); - public Flickr(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return string.Format("\"{3}\"", imageUrl, imageWidth, imageHeight, WebUtility.HtmlEncode(imageTitle)); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs index 2e0ea78649..cd045d7df3 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs @@ -1,33 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Getty Images supplier of stock images, editorial photography, video and music for business and consumers. +/// +public class GettyImages : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class GettyImages : EmbedProviderBase + public GettyImages(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://embed.gettyimages.com/oembed"; + } - //http://gty.im/74917285 - //http://www.gettyimages.com/detail/74917285 - public override string[] UrlSchemeRegex => new string[] - { - @"gty\.im/*", - @"gettyimages.com\/detail\/*" - }; + public override string ApiEndpoint => "http://embed.gettyimages.com/oembed"; - public override Dictionary RequestParams => new Dictionary(); + // http://gty.im/74917285 + // http://www.gettyimages.com/detail/74917285 + public override string[] UrlSchemeRegex => new[] { @"gty\.im/*", @"gettyimages.com\/detail\/*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public GettyImages(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs index 36df7e7362..3a6ad54204 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs @@ -1,34 +1,28 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Giphy.com the popular online GIFs and animated sticker provider. +/// +public class Giphy : EmbedProviderBase { - /// - /// Embed Provider for Giphy.com the popular online GIFs and animated sticker provider. - /// - /// TODO(V10) : change base class to OEmbedProviderBase - public class Giphy : EmbedProviderBase + public Giphy(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://giphy.com/services/oembed?url="; + } - public override string[] UrlSchemeRegex => new string[] - { - @"giphy\.com/*", - @"gph\.is/*" - }; + public override string ApiEndpoint => "https://giphy.com/services/oembed?url="; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"giphy\.com/*", @"gph\.is/*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Giphy(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs index 1d6bed791b..87bc0524e4 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs @@ -1,30 +1,28 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Hulu the the popular online subscription streaming service. +/// +public class Hulu : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Hulu : EmbedProviderBase + public Hulu(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.hulu.com/api/oembed.json"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"hulu.com/watch/.*" - }; + public override string ApiEndpoint => "http://www.hulu.com/api/oembed.json"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"hulu.com/watch/.*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Hulu(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs index 89179d40af..7da51b51ad 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs @@ -1,34 +1,33 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Issuu the popular platform to create interactive flipbooks, social media posts, GIFs, and more from a single piece of static content. +/// +public class Issuu : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Issuu : EmbedProviderBase + public Issuu(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://issuu.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"issuu.com/.*/docs/.*" - }; + public override string ApiEndpoint => "https://issuu.com/oembed"; - public override Dictionary RequestParams => new Dictionary() - { - //ApiUrl/?format=xml - {"format", "xml"} - }; + public override string[] UrlSchemeRegex => new[] { @"issuu.com/.*/docs/.*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new() + { + // ApiUrl/?format=xml + { "format", "xml" }, + }; - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Issuu(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs index e9ada74cf6..fecfd8606b 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs @@ -1,30 +1,28 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Kickstarter the popular online crowdfunding platform focused on creativity. +/// +public class Kickstarter : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Kickstarter : EmbedProviderBase + public Kickstarter(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.kickstarter.com/services/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"kickstarter\.com/projects/*" - }; + public override string ApiEndpoint => "http://www.kickstarter.com/services/oembed"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"kickstarter\.com/projects/*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Kickstarter(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs index f79e78b8b3..95330a6467 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs @@ -1,57 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for lottiefiles.com the popular opensource JSON-based animation format platform. +/// +public class LottieFiles : OEmbedProviderBase { - /// - /// Embed Provider for lottiefiles.com the popular opensource JSON-based animation format platform. - /// - public class LottieFiles : OEmbedProviderBase + public LottieFiles(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public LottieFiles(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { + } + public override string ApiEndpoint => "https://embed.lottiefiles.com/oembed"; + + public override string[] UrlSchemeRegex => new[] { @"lottiefiles\.com/*" }; + + public override Dictionary RequestParams => new(); + + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = this.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = this.GetJsonResponse(requestUrl); + var html = oembed?.GetHtml(); + + // LottieFiles doesn't seem to support maxwidth and maxheight via oembed + // this is therefore a hack... with regexes.. is that ok? HtmlAgility etc etc + // otherwise it always defaults to 300... + if (html is null) + { + return null; } - public override string ApiEndpoint => "https://embed.lottiefiles.com/oembed"; - - public override string[] UrlSchemeRegex => new string[] + if (maxWidth > 0 && maxHeight > 0) { - @"lottiefiles\.com/*" - }; - public override Dictionary RequestParams => new Dictionary(); - - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"" + maxWidth + "\""); + html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"" + maxHeight + "\""); + } + else { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - var html = oembed?.GetHtml(); - //LottieFiles doesn't seem to support maxwidth and maxheight via oembed - // this is therefore a hack... with regexes.. is that ok? HtmlAgility etc etc - // otherwise it always defaults to 300... - if (html is null) - { - return null; - } - - if (maxWidth > 0 && maxHeight > 0) - { - - html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"" + maxWidth + "\""); - html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"" + maxHeight + "\""); - - } - else - { - //if set to 0, let's default to 100% as an easter egg - html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"100%\""); - html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"100%\""); - } - return html; + // if set to 0, let's default to 100% as an easter egg + html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"100%\""); + html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"100%\""); } + return html; } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs index 031105033b..b09baba0db 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs @@ -1,85 +1,88 @@ -using System; -using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Text; using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +public abstract class OEmbedProviderBase : IEmbedProvider { - public abstract class OEmbedProviderBase : IEmbedProvider + private static HttpClient? _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + protected OEmbedProviderBase(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public abstract string ApiEndpoint { get; } + + public abstract string[] UrlSchemeRegex { get; } + + public abstract Dictionary RequestParams { get; } + + public abstract string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); + + public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) { - private readonly IJsonSerializer _jsonSerializer; - - protected OEmbedProviderBase(IJsonSerializer jsonSerializer) + if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) { - _jsonSerializer = jsonSerializer; + throw new ArgumentException("Not a valid URL.", nameof(url)); } - private static HttpClient? _httpClient; + var fullUrl = new StringBuilder(); - public abstract string ApiEndpoint { get; } + fullUrl.Append(ApiEndpoint); + fullUrl.Append("?url=" + WebUtility.UrlEncode(url)); - public abstract string[] UrlSchemeRegex { get; } - - public abstract Dictionary RequestParams { get; } - - public abstract string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); - - public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) + foreach (KeyValuePair param in RequestParams) { - if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) - throw new ArgumentException("Not a valid URL.", nameof(url)); - - var fullUrl = new StringBuilder(); - - fullUrl.Append(ApiEndpoint); - fullUrl.Append("?url=" + WebUtility.UrlEncode(url)); - - foreach (var param in RequestParams) - fullUrl.Append($"&{param.Key}={param.Value}"); - - if (maxWidth > 0) - fullUrl.Append("&maxwidth=" + maxWidth); - - if (maxHeight > 0) - fullUrl.Append("&maxheight=" + maxHeight); - - return fullUrl.ToString(); + fullUrl.Append($"&{param.Key}={param.Value}"); } - public virtual string DownloadResponse(string url) + if (maxWidth > 0) { - if (_httpClient == null) - _httpClient = new HttpClient(); - - using (var request = new HttpRequestMessage(HttpMethod.Get, url)) - { - var response = _httpClient.SendAsync(request).Result; - return response.Content.ReadAsStringAsync().Result; - } + fullUrl.Append("&maxwidth=" + maxWidth); } - public virtual T? GetJsonResponse(string url) where T : class + if (maxHeight > 0) { - var response = DownloadResponse(url); - return _jsonSerializer.Deserialize(response); + fullUrl.Append("&maxheight=" + maxHeight); } - public virtual XmlDocument GetXmlResponse(string url) - { - var response = DownloadResponse(url); - var doc = new XmlDocument(); - doc.LoadXml(response); + return fullUrl.ToString(); + } - return doc; + public virtual string DownloadResponse(string url) + { + if (_httpClient == null) + { + _httpClient = new HttpClient(); } - public virtual string GetXmlProperty(XmlDocument doc, string property) + using (var request = new HttpRequestMessage(HttpMethod.Get, url)) { - var selectSingleNode = doc.SelectSingleNode(property); - return selectSingleNode != null ? selectSingleNode.InnerText : string.Empty; + HttpResponseMessage response = _httpClient.SendAsync(request).Result; + return response.Content.ReadAsStringAsync().Result; } } -}; + + public virtual T? GetJsonResponse(string url) + where T : class + { + var response = DownloadResponse(url); + return _jsonSerializer.Deserialize(response); + } + + public virtual XmlDocument GetXmlResponse(string url) + { + var response = DownloadResponse(url); + var doc = new XmlDocument(); + doc.LoadXml(response); + + return doc; + } + + public virtual string GetXmlProperty(XmlDocument doc, string property) + { + XmlNode? selectSingleNode = doc.SelectSingleNode(property); + return selectSingleNode != null ? selectSingleNode.InnerText : string.Empty; + } +} diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs index f003aa841c..370d2609c7 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs @@ -1,68 +1,68 @@ using System.Net; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Wrapper class for OEmbed response +/// +[DataContract] +public class OEmbedResponse { + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "version")] + public string? Version { get; set; } + + [DataMember(Name = "title")] + public string? Title { get; set; } + + [DataMember(Name = "author_name")] + public string? AuthorName { get; set; } + + [DataMember(Name = "author_url")] + public string? AuthorUrl { get; set; } + + [DataMember(Name = "provider_name")] + public string? ProviderName { get; set; } + + [DataMember(Name = "provider_url")] + public string? ProviderUrl { get; set; } + + [DataMember(Name = "thumbnail_url")] + public string? ThumbnailUrl { get; set; } + + [DataMember(Name = "thumbnail_height")] + public double? ThumbnailHeight { get; set; } + + [DataMember(Name = "thumbnail_width")] + public double? ThumbnailWidth { get; set; } + + [DataMember(Name = "html")] + public string? Html { get; set; } + + [DataMember(Name = "url")] + public string? Url { get; set; } + + [DataMember(Name = "height")] + public double? Height { get; set; } + + [DataMember(Name = "width")] + public double? Width { get; set; } + /// - /// Wrapper class for OEmbed response + /// Gets the HTML. /// - [DataContract] - public class OEmbedResponse + /// The response HTML + public string GetHtml() { - [DataMember(Name ="type")] - public string? Type { get; set; } - - [DataMember(Name ="version")] - public string? Version { get; set; } - - [DataMember(Name ="title")] - public string? Title { get; set; } - - [DataMember(Name ="author_name")] - public string? AuthorName { get; set; } - - [DataMember(Name ="author_url")] - public string? AuthorUrl { get; set; } - - [DataMember(Name ="provider_name")] - public string? ProviderName { get; set; } - - [DataMember(Name ="provider_url")] - public string? ProviderUrl { get; set; } - - [DataMember(Name ="thumbnail_url")] - public string? ThumbnailUrl { get; set; } - - [DataMember(Name ="thumbnail_height")] - public double? ThumbnailHeight { get; set; } - - [DataMember(Name ="thumbnail_width")] - public double? ThumbnailWidth { get; set; } - - [DataMember(Name ="html")] - public string? Html { get; set; } - - [DataMember(Name ="url")] - public string? Url { get; set; } - - [DataMember(Name ="height")] - public double? Height { get; set; } - - [DataMember(Name ="width")] - public double? Width { get; set; } - - /// - /// Gets the HTML. - /// - /// The response HTML - public string GetHtml() + if (Type == "photo") { - if (Type == "photo") - { - return "\"""; - } - - return string.IsNullOrEmpty(Html) == false ? Html : string.Empty; + return "\"""; } + + return string.IsNullOrEmpty(Html) == false ? Html : string.Empty; } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs index 42e500aa5c..1791034168 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs @@ -1,30 +1,29 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for SlideShare for professional online content including presentations, infographics, documents, and videos. +/// +public class Slideshare : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Slideshare : EmbedProviderBase + public Slideshare(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.slideshare.net/api/oembed/2"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"slideshare\.net/" - }; + public override string ApiEndpoint => "http://www.slideshare.net/api/oembed/2"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"slideshare\.net/" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Slideshare(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs index 687da98697..ccb3104940 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs @@ -1,30 +1,29 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for SoundCloud the popular online audio distribution platform and music sharing provider. +/// +public class Soundcloud : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Soundcloud : EmbedProviderBase + public Soundcloud(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://soundcloud.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"soundcloud.com\/*" - }; + public override string ApiEndpoint => "https://soundcloud.com/oembed"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"soundcloud.com\/*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Soundcloud(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs index 511cbf012d..1e7981f7af 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs @@ -1,30 +1,29 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Ted that posts talks online for free distribution. +/// +public class Ted : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Ted : EmbedProviderBase + public Ted(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.ted.com/talks/oembed.xml"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"ted.com\/talks\/*" - }; + public override string ApiEndpoint => "http://www.ted.com/talks/oembed.xml"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"ted.com\/talks\/*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Ted(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs index 934ec4b5c1..af2c723533 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs @@ -1,30 +1,28 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Twitter the popular online service for microblogging and social networking. +/// +public class Twitter : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Twitter : EmbedProviderBase + public Twitter(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://publish.twitter.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"twitter.com/.*/status/.*" - }; + public override string ApiEndpoint => "http://publish.twitter.com/oembed"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"twitter.com/.*/status/.*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Twitter(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs index db324bda12..0159e59cbd 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs @@ -1,30 +1,29 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Vimeo the popular online video hosting, sharing, and services platform provider. +/// +public class Vimeo : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Vimeo : EmbedProviderBase + public Vimeo(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://vimeo.com/api/oembed.xml"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"vimeo\.com/" - }; + public override string ApiEndpoint => "https://vimeo.com/api/oembed.xml"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"vimeo\.com/" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Vimeo(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs index 3888462dbc..ceb8af99e9 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for YouTube the popular online video sharing and social media platform provider. +/// +public class YouTube : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class YouTube : EmbedProviderBase + public YouTube(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://www.youtube.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"youtu.be/.*", - @"youtube.com/watch.*" - }; + public override string ApiEndpoint => "https://www.youtube.com/oembed"; - public override Dictionary RequestParams => new Dictionary() - { - //ApiUrl/?format=json - {"format", "json"} - }; + public override string[] UrlSchemeRegex => new[] { @"youtu.be/.*", @"youtube.com/watch.*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new() + { + // ApiUrl/?format=json + { "format", "json" }, + }; - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public YouTube(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/Exif/BitConverterEx.cs b/src/Umbraco.Core/Media/Exif/BitConverterEx.cs index 6afc6e4308..f6cc50f801 100644 --- a/src/Umbraco.Core/Media/Exif/BitConverterEx.cs +++ b/src/Umbraco.Core/Media/Exif/BitConverterEx.cs @@ -1,405 +1,346 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// An endian-aware converter for converting between base data types +/// and an array of bytes. +/// +internal class BitConverterEx { + #region Public Enums + /// - /// An endian-aware converter for converting between base data types - /// and an array of bytes. + /// Represents the byte order. /// - internal class BitConverterEx + public enum ByteOrder { - #region Public Enums - /// - /// Represents the byte order. - /// - public enum ByteOrder - { - LittleEndian = 1, - BigEndian = 2, - } - #endregion - - #region Member Variables - private ByteOrder mFrom, mTo; - #endregion - - #region Constructors - public BitConverterEx(ByteOrder from, ByteOrder to) - { - mFrom = from; - mTo = to; - } - #endregion - - #region Properties - /// - /// Indicates the byte order in which data is stored in this platform. - /// - public static ByteOrder SystemByteOrder - { - get - { - return (BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian); - } - } - #endregion - - #region Predefined Values - /// - /// Returns a bit converter that converts between little-endian and system byte-order. - /// - public static BitConverterEx LittleEndian - { - get - { - return new BitConverterEx(ByteOrder.LittleEndian, BitConverterEx.SystemByteOrder); - } - } - - /// - /// Returns a bit converter that converts between big-endian and system byte-order. - /// - public static BitConverterEx BigEndian - { - get - { - return new BitConverterEx(ByteOrder.BigEndian, BitConverterEx.SystemByteOrder); - } - } - - /// - /// Returns a bit converter that does not do any byte-order conversion. - /// - public static BitConverterEx SystemEndian - { - get - { - return new BitConverterEx(BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder); - } - } - #endregion - - #region Static Methods - /// - /// Converts the given array of bytes to a Unicode character. - /// - public static char ToChar(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 2, from, to); - return BitConverter.ToChar(data, 0); - } - - /// - /// Converts the given array of bytes to a 16-bit unsigned integer. - /// - public static ushort ToUInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 2, from, to); - return BitConverter.ToUInt16(data, 0); - } - - /// - /// Converts the given array of bytes to a 32-bit unsigned integer. - /// - public static uint ToUInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 4, from, to); - return BitConverter.ToUInt32(data, 0); - } - - /// - /// Converts the given array of bytes to a 64-bit unsigned integer. - /// - public static ulong ToUInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 8, from, to); - return BitConverter.ToUInt64(data, 0); - } - - /// - /// Converts the given array of bytes to a 16-bit signed integer. - /// - public static short ToInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 2, from, to); - return BitConverter.ToInt16(data, 0); - } - - /// - /// Converts the given array of bytes to a 32-bit signed integer. - /// - public static int ToInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 4, from, to); - return BitConverter.ToInt32(data, 0); - } - - /// - /// Converts the given array of bytes to a 64-bit signed integer. - /// - public static long ToInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 8, from, to); - return BitConverter.ToInt64(data, 0); - } - - /// - /// Converts the given array of bytes to a single precision floating number. - /// - public static float ToSingle(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 4, from, to); - return BitConverter.ToSingle(data, 0); - } - - /// - /// Converts the given array of bytes to a double precision floating number. - /// - public static double ToDouble(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 8, from, to); - return BitConverter.ToDouble(data, 0); - } - - /// - /// Converts the given 16-bit unsigned integer to an array of bytes. - /// - public static byte[] GetBytes(ushort value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 32-bit unsigned integer to an array of bytes. - /// - public static byte[] GetBytes(uint value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 64-bit unsigned integer to an array of bytes. - /// - public static byte[] GetBytes(ulong value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 16-bit signed integer to an array of bytes. - /// - public static byte[] GetBytes(short value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 32-bit signed integer to an array of bytes. - /// - public static byte[] GetBytes(int value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 64-bit signed integer to an array of bytes. - /// - public static byte[] GetBytes(long value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given single precision floating-point number to an array of bytes. - /// - public static byte[] GetBytes(float value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given double precision floating-point number to an array of bytes. - /// - public static byte[] GetBytes(double value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - #endregion - - #region Instance Methods - /// - /// Converts the given array of bytes to a 16-bit unsigned integer. - /// - public char ToChar(byte[] value, long startIndex) - { - return BitConverterEx.ToChar(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 16-bit unsigned integer. - /// - public ushort ToUInt16(byte[] value, long startIndex) - { - return BitConverterEx.ToUInt16(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 32-bit unsigned integer. - /// - public uint ToUInt32(byte[] value, long startIndex) - { - return BitConverterEx.ToUInt32(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 64-bit unsigned integer. - /// - public ulong ToUInt64(byte[] value, long startIndex) - { - return BitConverterEx.ToUInt64(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 16-bit signed integer. - /// - public short ToInt16(byte[] value, long startIndex) - { - return BitConverterEx.ToInt16(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 32-bit signed integer. - /// - public int ToInt32(byte[] value, long startIndex) - { - return BitConverterEx.ToInt32(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 64-bit signed integer. - /// - public long ToInt64(byte[] value, long startIndex) - { - return BitConverterEx.ToInt64(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a single precision floating number. - /// - public float ToSingle(byte[] value, long startIndex) - { - return BitConverterEx.ToSingle(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a double precision floating number. - /// - public double ToDouble(byte[] value, long startIndex) - { - return BitConverterEx.ToDouble(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given 16-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(ushort value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 32-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(uint value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 64-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(ulong value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 16-bit signed integer to an array of bytes. - /// - public byte[] GetBytes(short value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 32-bit signed integer to an array of bytes. - /// - public byte[] GetBytes(int value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 64-bit signed integer to an array of bytes. - /// - public byte[] GetBytes(long value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given single precision floating-point number to an array of bytes. - /// - public byte[] GetBytes(float value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given double precision floating-point number to an array of bytes. - /// - public byte[] GetBytes(double value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - #endregion - - #region Private Helpers - /// - /// Reverse the array of bytes as needed. - /// - private static byte[] CheckData(byte[] value, long startIndex, long length, ByteOrder from, ByteOrder to) - { - byte[] data = new byte[length]; - Array.Copy(value, startIndex, data, 0, length); - if (from != to) - Array.Reverse(data); - return data; - } - - /// - /// Reverse the array of bytes as needed. - /// - private static byte[] CheckData(byte[] value, ByteOrder from, ByteOrder to) - { - return CheckData(value, 0, value.Length, from, to); - } - #endregion + LittleEndian = 1, + BigEndian = 2, } + + #endregion + + #region Member Variables + + private readonly ByteOrder mFrom; + private readonly ByteOrder mTo; + + #endregion + + #region Constructors + + public BitConverterEx(ByteOrder from, ByteOrder to) + { + mFrom = from; + mTo = to; + } + + #endregion + + #region Properties + + /// + /// Indicates the byte order in which data is stored in this platform. + /// + public static ByteOrder SystemByteOrder => + BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian; + + #endregion + + #region Predefined Values + + /// + /// Returns a bit converter that converts between little-endian and system byte-order. + /// + public static BitConverterEx LittleEndian => new BitConverterEx(ByteOrder.LittleEndian, SystemByteOrder); + + /// + /// Returns a bit converter that converts between big-endian and system byte-order. + /// + public static BitConverterEx BigEndian => new BitConverterEx(ByteOrder.BigEndian, SystemByteOrder); + + /// + /// Returns a bit converter that does not do any byte-order conversion. + /// + public static BitConverterEx SystemEndian => new BitConverterEx(SystemByteOrder, SystemByteOrder); + + #endregion + + #region Static Methods + + /// + /// Converts the given array of bytes to a Unicode character. + /// + public static char ToChar(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 2, from, to); + return BitConverter.ToChar(data, 0); + } + + /// + /// Converts the given array of bytes to a 16-bit unsigned integer. + /// + public static ushort ToUInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 2, from, to); + return BitConverter.ToUInt16(data, 0); + } + + /// + /// Converts the given array of bytes to a 32-bit unsigned integer. + /// + public static uint ToUInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 4, from, to); + return BitConverter.ToUInt32(data, 0); + } + + /// + /// Converts the given array of bytes to a 64-bit unsigned integer. + /// + public static ulong ToUInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 8, from, to); + return BitConverter.ToUInt64(data, 0); + } + + /// + /// Converts the given array of bytes to a 16-bit signed integer. + /// + public static short ToInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 2, from, to); + return BitConverter.ToInt16(data, 0); + } + + /// + /// Converts the given array of bytes to a 32-bit signed integer. + /// + public static int ToInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 4, from, to); + return BitConverter.ToInt32(data, 0); + } + + /// + /// Converts the given array of bytes to a 64-bit signed integer. + /// + public static long ToInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 8, from, to); + return BitConverter.ToInt64(data, 0); + } + + /// + /// Converts the given array of bytes to a single precision floating number. + /// + public static float ToSingle(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 4, from, to); + return BitConverter.ToSingle(data, 0); + } + + /// + /// Converts the given array of bytes to a double precision floating number. + /// + public static double ToDouble(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 8, from, to); + return BitConverter.ToDouble(data, 0); + } + + /// + /// Converts the given 16-bit unsigned integer to an array of bytes. + /// + public static byte[] GetBytes(ushort value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 32-bit unsigned integer to an array of bytes. + /// + public static byte[] GetBytes(uint value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 64-bit unsigned integer to an array of bytes. + /// + public static byte[] GetBytes(ulong value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 16-bit signed integer to an array of bytes. + /// + public static byte[] GetBytes(short value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 32-bit signed integer to an array of bytes. + /// + public static byte[] GetBytes(int value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 64-bit signed integer to an array of bytes. + /// + public static byte[] GetBytes(long value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given single precision floating-point number to an array of bytes. + /// + public static byte[] GetBytes(float value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given double precision floating-point number to an array of bytes. + /// + public static byte[] GetBytes(double value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + #endregion + + #region Instance Methods + + /// + /// Converts the given array of bytes to a 16-bit unsigned integer. + /// + public char ToChar(byte[] value, long startIndex) => ToChar(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 16-bit unsigned integer. + /// + public ushort ToUInt16(byte[] value, long startIndex) => ToUInt16(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 32-bit unsigned integer. + /// + public uint ToUInt32(byte[] value, long startIndex) => ToUInt32(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 64-bit unsigned integer. + /// + public ulong ToUInt64(byte[] value, long startIndex) => ToUInt64(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 16-bit signed integer. + /// + public short ToInt16(byte[] value, long startIndex) => ToInt16(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 32-bit signed integer. + /// + public int ToInt32(byte[] value, long startIndex) => ToInt32(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 64-bit signed integer. + /// + public long ToInt64(byte[] value, long startIndex) => ToInt64(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a single precision floating number. + /// + public float ToSingle(byte[] value, long startIndex) => ToSingle(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a double precision floating number. + /// + public double ToDouble(byte[] value, long startIndex) => ToDouble(value, startIndex, mFrom, mTo); + + /// + /// Converts the given 16-bit unsigned integer to an array of bytes. + /// + public byte[] GetBytes(ushort value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 32-bit unsigned integer to an array of bytes. + /// + public byte[] GetBytes(uint value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 64-bit unsigned integer to an array of bytes. + /// + public byte[] GetBytes(ulong value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 16-bit signed integer to an array of bytes. + /// + public byte[] GetBytes(short value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 32-bit signed integer to an array of bytes. + /// + public byte[] GetBytes(int value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 64-bit signed integer to an array of bytes. + /// + public byte[] GetBytes(long value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given single precision floating-point number to an array of bytes. + /// + public byte[] GetBytes(float value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given double precision floating-point number to an array of bytes. + /// + public byte[] GetBytes(double value) => GetBytes(value, mFrom, mTo); + + #endregion + + #region Private Helpers + + /// + /// Reverse the array of bytes as needed. + /// + private static byte[] CheckData(byte[] value, long startIndex, long length, ByteOrder from, ByteOrder to) + { + var data = new byte[length]; + Array.Copy(value, startIndex, data, 0, length); + if (from != to) + { + Array.Reverse(data); + } + + return data; + } + + /// + /// Reverse the array of bytes as needed. + /// + private static byte[] CheckData(byte[] value, ByteOrder from, ByteOrder to) => + CheckData(value, 0, value.Length, from, to); + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs b/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs index 74465a6684..e8dc7c4eb9 100644 --- a/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs +++ b/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs @@ -1,358 +1,392 @@ -using System; using System.Globalization; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Converts between exif data types and array of bytes. +/// +internal class ExifBitConverter : BitConverterEx { - /// - /// Converts between exif data types and array of bytes. - /// - internal class ExifBitConverter : BitConverterEx + #region Constructors + + public ExifBitConverter(ByteOrder from, ByteOrder to) + : base(from, to) { - #region Constructors - public ExifBitConverter(ByteOrder from, ByteOrder to) - : base(from, to) - { - - } - #endregion - - #region Static Methods - /// - /// Returns an ASCII string converted from the given byte array. - /// - public static string ToAscii(byte[] data, bool endatfirstnull, Encoding encoding) - { - int len = data.Length; - if (endatfirstnull) - { - len = Array.IndexOf(data, (byte)0); - if (len == -1) len = data.Length; - } - return encoding.GetString(data, 0, len); - } - - /// - /// Returns an ASCII string converted from the given byte array. - /// - public static string ToAscii(byte[] data, Encoding encoding) - { - return ToAscii(data, true, encoding); - } - - /// - /// Returns a string converted from the given byte array. - /// from the numeric value of each byte. - /// - public static string ToString(byte[] data) - { - StringBuilder sb = new StringBuilder(); - foreach (byte b in data) - sb.Append(b); - return sb.ToString(); - } - - /// - /// Returns a DateTime object converted from the given byte array. - /// - public static DateTime ToDateTime(byte[] data, bool hastime) - { - string str = ToAscii(data, Encoding.ASCII); - string[] parts = str.Split(new char[] { ':', ' ' }); - try - { - if (hastime && parts.Length == 6) - { - // yyyy:MM:dd HH:mm:ss - // This is the expected format though some cameras - // can use single digits. See Issue 21. - return new DateTime(int.Parse(parts[0], CultureInfo.InvariantCulture), int.Parse(parts[1], CultureInfo.InvariantCulture), int.Parse(parts[2], CultureInfo.InvariantCulture), int.Parse(parts[3], CultureInfo.InvariantCulture), int.Parse(parts[4], CultureInfo.InvariantCulture), int.Parse(parts[5], CultureInfo.InvariantCulture)); - } - else if (!hastime && parts.Length == 3) - { - // yyyy:MM:dd - return new DateTime(int.Parse(parts[0], CultureInfo.InvariantCulture), int.Parse(parts[1], CultureInfo.InvariantCulture), int.Parse(parts[2], CultureInfo.InvariantCulture)); - } - else - { - return DateTime.MinValue; - } - } - catch (ArgumentOutOfRangeException) - { - return DateTime.MinValue; - } - catch (ArgumentException) - { - return DateTime.MinValue; - } - } - - /// - /// Returns a DateTime object converted from the given byte array. - /// - public static DateTime ToDateTime(byte[] data) - { - return ToDateTime(data, true); - } - - /// - /// Returns an unsigned rational number converted from the first - /// eight bytes of the given byte array. The first four bytes are - /// assumed to be the numerator and the next four bytes are the - /// denominator. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.UFraction32 ToURational(byte[] data, ByteOrder frombyteorder) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, 0, num, 0, 4); - Array.Copy(data, 4, den, 0, 4); - return new MathEx.UFraction32(ToUInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToUInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - - /// - /// Returns a signed rational number converted from the first - /// eight bytes of the given byte array. The first four bytes are - /// assumed to be the numerator and the next four bytes are the - /// denominator. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.Fraction32 ToSRational(byte[] data, ByteOrder frombyteorder) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, 0, num, 0, 4); - Array.Copy(data, 4, den, 0, 4); - return new MathEx.Fraction32(ToInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - - /// - /// Returns an array of 16-bit unsigned integers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static ushort[] ToUShortArray(byte[] data, int count, ByteOrder frombyteorder) - { - ushort[] numbers = new ushort[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[2]; - Array.Copy(data, i * 2, num, 0, 2); - numbers[i] = ToUInt16(num, 0, frombyteorder, BitConverterEx.SystemByteOrder); - } - return numbers; - } - - /// - /// Returns an array of 32-bit unsigned integers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static uint[] ToUIntArray(byte[] data, int count, ByteOrder frombyteorder) - { - uint[] numbers = new uint[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - Array.Copy(data, i * 4, num, 0, 4); - numbers[i] = ToUInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder); - } - return numbers; - } - - /// - /// Returns an array of 32-bit signed integers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static int[] ToSIntArray(byte[] data, int count, ByteOrder byteorder) - { - int[] numbers = new int[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - Array.Copy(data, i * 4, num, 0, 4); - numbers[i] = ToInt32(num, 0, byteorder, BitConverterEx.SystemByteOrder); - } - return numbers; - } - - /// - /// Returns an array of unsigned rational numbers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.UFraction32[] ToURationalArray(byte[] data, int count, ByteOrder frombyteorder) - { - MathEx.UFraction32[] numbers = new MathEx.UFraction32[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, i * 8, num, 0, 4); - Array.Copy(data, i * 8 + 4, den, 0, 4); - numbers[i].Set(ToUInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToUInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - return numbers; - } - - /// - /// Returns an array of signed rational numbers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.Fraction32[] ToSRationalArray(byte[] data, int count, ByteOrder frombyteorder) - { - MathEx.Fraction32[] numbers = new MathEx.Fraction32[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, i * 8, num, 0, 4); - Array.Copy(data, i * 8 + 4, den, 0, 4); - numbers[i].Set(ToInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - return numbers; - } - - /// - /// Converts the given ascii string to an array of bytes optionally adding a null terminator. - /// - public static byte[] GetBytes(string value, bool addnull, Encoding encoding) - { - if (addnull) value += '\0'; - return encoding.GetBytes(value); - } - - /// - /// Converts the given ascii string to an array of bytes without adding a null terminator. - /// - public static byte[] GetBytes(string value, Encoding encoding) - { - return GetBytes(value, false, encoding); - } - - /// - /// Converts the given datetime to an array of bytes with a null terminator. - /// - public static byte[] GetBytes(DateTime value, bool hastime) - { - string str = ""; - if (hastime) - str = value.ToString("yyyy:MM:dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); - else - str = value.ToString("yyyy:MM:dd", System.Globalization.CultureInfo.InvariantCulture); - return GetBytes(str, true, Encoding.ASCII); - } - - /// - /// Converts the given unsigned rational number to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.UFraction32 value, ByteOrder tobyteorder) - { - byte[] num = GetBytes(value.Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value.Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] data = new byte[8]; - Array.Copy(num, 0, data, 0, 4); - Array.Copy(den, 0, data, 4, 4); - return data; - } - - /// - /// Converts the given signed rational number to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.Fraction32 value, ByteOrder tobyteorder) - { - byte[] num = GetBytes(value.Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value.Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] data = new byte[8]; - Array.Copy(num, 0, data, 0, 4); - Array.Copy(den, 0, data, 4, 4); - return data; - } - - /// - /// Converts the given array of 16-bit unsigned integers to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(ushort[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[2 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i], BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 2, 2); - } - return data; - } - - /// - /// Converts the given array of 32-bit unsigned integers to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(uint[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[4 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i], BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 4, 4); - } - return data; - } - - /// - /// Converts the given array of 32-bit signed integers to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(int[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[4 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i], BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 4, 4); - } - return data; - } - - /// - /// Converts the given array of unsigned rationals to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.UFraction32[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[8 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i].Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value[i].Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 8, 4); - Array.Copy(den, 0, data, i * 8 + 4, 4); - } - return data; - } - - /// - /// Converts the given array of signed rationals to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.Fraction32[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[8 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i].Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value[i].Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 8, 4); - Array.Copy(den, 0, data, i * 8 + 4, 4); - } - return data; - } - #endregion } + + #endregion + + #region Static Methods + + /// + /// Returns an ASCII string converted from the given byte array. + /// + public static string ToAscii(byte[] data, bool endatfirstnull, Encoding encoding) + { + var len = data.Length; + if (endatfirstnull) + { + len = Array.IndexOf(data, (byte)0); + if (len == -1) + { + len = data.Length; + } + } + + return encoding.GetString(data, 0, len); + } + + /// + /// Returns an ASCII string converted from the given byte array. + /// + public static string ToAscii(byte[] data, Encoding encoding) => ToAscii(data, true, encoding); + + /// + /// Returns a string converted from the given byte array. + /// from the numeric value of each byte. + /// + public static string ToString(byte[] data) + { + var sb = new StringBuilder(); + foreach (var b in data) + { + sb.Append(b); + } + + return sb.ToString(); + } + + /// + /// Returns a DateTime object converted from the given byte array. + /// + public static DateTime ToDateTime(byte[] data, bool hastime) + { + var str = ToAscii(data, Encoding.ASCII); + var parts = str.Split(':', ' '); + try + { + if (hastime && parts.Length == 6) + { + // yyyy:MM:dd HH:mm:ss + // This is the expected format though some cameras + // can use single digits. See Issue 21. + return new DateTime( + int.Parse(parts[0], CultureInfo.InvariantCulture), + int.Parse(parts[1], CultureInfo.InvariantCulture), + int.Parse(parts[2], CultureInfo.InvariantCulture), + int.Parse(parts[3], CultureInfo.InvariantCulture), + int.Parse(parts[4], CultureInfo.InvariantCulture), + int.Parse(parts[5], CultureInfo.InvariantCulture)); + } + + if (!hastime && parts.Length == 3) + { + // yyyy:MM:dd + return new DateTime( + int.Parse(parts[0], CultureInfo.InvariantCulture), + int.Parse(parts[1], CultureInfo.InvariantCulture), + int.Parse(parts[2], CultureInfo.InvariantCulture)); + } + + return DateTime.MinValue; + } + catch (ArgumentOutOfRangeException) + { + return DateTime.MinValue; + } + catch (ArgumentException) + { + return DateTime.MinValue; + } + } + + /// + /// Returns a DateTime object converted from the given byte array. + /// + public static DateTime ToDateTime(byte[] data) => ToDateTime(data, true); + + /// + /// Returns an unsigned rational number converted from the first + /// eight bytes of the given byte array. The first four bytes are + /// assumed to be the numerator and the next four bytes are the + /// denominator. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.UFraction32 ToURational(byte[] data, ByteOrder frombyteorder) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, 0, num, 0, 4); + Array.Copy(data, 4, den, 0, 4); + return new MathEx.UFraction32( + ToUInt32(num, 0, frombyteorder, SystemByteOrder), + ToUInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + /// + /// Returns a signed rational number converted from the first + /// eight bytes of the given byte array. The first four bytes are + /// assumed to be the numerator and the next four bytes are the + /// denominator. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.Fraction32 ToSRational(byte[] data, ByteOrder frombyteorder) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, 0, num, 0, 4); + Array.Copy(data, 4, den, 0, 4); + return new MathEx.Fraction32( + ToInt32(num, 0, frombyteorder, SystemByteOrder), + ToInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + /// + /// Returns an array of 16-bit unsigned integers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static ushort[] ToUShortArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new ushort[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[2]; + Array.Copy(data, i * 2, num, 0, 2); + numbers[i] = ToUInt16(num, 0, frombyteorder, SystemByteOrder); + } + + return numbers; + } + + /// + /// Returns an array of 32-bit unsigned integers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static uint[] ToUIntArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new uint[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + Array.Copy(data, i * 4, num, 0, 4); + numbers[i] = ToUInt32(num, 0, frombyteorder, SystemByteOrder); + } + + return numbers; + } + + /// + /// Returns an array of 32-bit signed integers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static int[] ToSIntArray(byte[] data, int count, ByteOrder byteorder) + { + var numbers = new int[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + Array.Copy(data, i * 4, num, 0, 4); + numbers[i] = ToInt32(num, 0, byteorder, SystemByteOrder); + } + + return numbers; + } + + /// + /// Returns an array of unsigned rational numbers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.UFraction32[] ToURationalArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new MathEx.UFraction32[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, i * 8, num, 0, 4); + Array.Copy(data, (i * 8) + 4, den, 0, 4); + numbers[i].Set( + ToUInt32(num, 0, frombyteorder, SystemByteOrder), + ToUInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + return numbers; + } + + /// + /// Returns an array of signed rational numbers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.Fraction32[] ToSRationalArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new MathEx.Fraction32[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, i * 8, num, 0, 4); + Array.Copy(data, (i * 8) + 4, den, 0, 4); + numbers[i].Set( + ToInt32(num, 0, frombyteorder, SystemByteOrder), + ToInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + return numbers; + } + + /// + /// Converts the given ascii string to an array of bytes optionally adding a null terminator. + /// + public static byte[] GetBytes(string value, bool addnull, Encoding encoding) + { + if (addnull) + { + value += '\0'; + } + + return encoding.GetBytes(value); + } + + /// + /// Converts the given ascii string to an array of bytes without adding a null terminator. + /// + public static byte[] GetBytes(string value, Encoding encoding) => GetBytes(value, false, encoding); + + /// + /// Converts the given datetime to an array of bytes with a null terminator. + /// + public static byte[] GetBytes(DateTime value, bool hastime) + { + var str = string.Empty; + if (hastime) + { + str = value.ToString("yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture); + } + else + { + str = value.ToString("yyyy:MM:dd", CultureInfo.InvariantCulture); + } + + return GetBytes(str, true, Encoding.ASCII); + } + + /// + /// Converts the given unsigned rational number to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.UFraction32 value, ByteOrder tobyteorder) + { + var num = GetBytes(value.Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value.Denominator, SystemByteOrder, tobyteorder); + var data = new byte[8]; + Array.Copy(num, 0, data, 0, 4); + Array.Copy(den, 0, data, 4, 4); + return data; + } + + /// + /// Converts the given signed rational number to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.Fraction32 value, ByteOrder tobyteorder) + { + var num = GetBytes(value.Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value.Denominator, SystemByteOrder, tobyteorder); + var data = new byte[8]; + Array.Copy(num, 0, data, 0, 4); + Array.Copy(den, 0, data, 4, 4); + return data; + } + + /// + /// Converts the given array of 16-bit unsigned integers to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(ushort[] value, ByteOrder tobyteorder) + { + var data = new byte[2 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 2, 2); + } + + return data; + } + + /// + /// Converts the given array of 32-bit unsigned integers to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(uint[] value, ByteOrder tobyteorder) + { + var data = new byte[4 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 4, 4); + } + + return data; + } + + /// + /// Converts the given array of 32-bit signed integers to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(int[] value, ByteOrder tobyteorder) + { + var data = new byte[4 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 4, 4); + } + + return data; + } + + /// + /// Converts the given array of unsigned rationals to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.UFraction32[] value, ByteOrder tobyteorder) + { + var data = new byte[8 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i].Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value[i].Denominator, SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 8, 4); + Array.Copy(den, 0, data, (i * 8) + 4, 4); + } + + return data; + } + + /// + /// Converts the given array of signed rationals to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.Fraction32[] value, ByteOrder tobyteorder) + { + var data = new byte[8 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i].Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value[i].Denominator, SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 8, 4); + Array.Copy(den, 0, data, (i * 8) + 4, 4); + } + + return data; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifEnums.cs b/src/Umbraco.Core/Media/Exif/ExifEnums.cs index 1ce0ec4891..5e27b8dd24 100644 --- a/src/Umbraco.Core/Media/Exif/ExifEnums.cs +++ b/src/Umbraco.Core/Media/Exif/ExifEnums.cs @@ -1,292 +1,297 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +internal enum Compression : ushort { - internal enum Compression : ushort - { - Uncompressed = 1, - CCITT1D = 2, - Group3Fax = 3, - Group4Fax = 4, - LZW = 5, - JPEG = 6, - PackBits = 32773, - } - - internal enum PhotometricInterpretation : ushort - { - WhiteIsZero = 0, - BlackIsZero = 1, - RGB = 2, - RGBPalette = 3, - TransparencyMask = 4, - CMYK = 5, - YCbCr = 6, - CIELab = 8, - } - - internal enum Orientation : ushort - { - Normal = 1, - MirroredVertically = 2, - Rotated180 = 3, - MirroredHorizontally = 4, - RotatedLeftAndMirroredVertically = 5, - RotatedRight = 6, - RotatedLeft = 7, - RotatedRightAndMirroredVertically = 8, - } - - internal enum PlanarConfiguration : ushort - { - ChunkyFormat = 1, - PlanarFormat = 2, - } - - internal enum YCbCrPositioning : ushort - { - Centered = 1, - CoSited = 2, - } - - internal enum ResolutionUnit : ushort - { - Inches = 2, - Centimeters = 3, - } - - internal enum ColorSpace : ushort - { - sRGB = 1, - Uncalibrated = 0xfff, - } - - internal enum ExposureProgram : ushort - { - NotDefined = 0, - Manual = 1, - Normal = 2, - AperturePriority = 3, - ShutterPriority = 4, - /// - /// Biased toward depth of field. - /// - Creative = 5, - /// - /// Biased toward fast shutter speed. - /// - Action = 6, - /// - /// For closeup photos with the background out of focus. - /// - Portrait = 7, - /// - /// For landscape photos with the background in focus. - /// - Landscape = 8, - } - - internal enum MeteringMode : ushort - { - Unknown = 0, - Average = 1, - CenterWeightedAverage = 2, - Spot = 3, - MultiSpot = 4, - Pattern = 5, - Partial = 6, - Other = 255, - } - - internal enum LightSource : ushort - { - Unknown = 0, - Daylight = 1, - Fluorescent = 2, - Tungsten = 3, - Flash = 4, - FineWeather = 9, - CloudyWeather = 10, - Shade = 11, - /// - /// D 5700 – 7100K - /// - DaylightFluorescent = 12, - /// - /// N 4600 – 5400K - /// - DayWhiteFluorescent = 13, - /// - /// W 3900 – 4500K - /// - CoolWhiteFluorescent = 14, - /// - /// WW 3200 – 3700K - /// - WhiteFluorescent = 15, - StandardLightA = 17, - StandardLightB = 18, - StandardLightC = 19, - D55 = 20, - D65 = 21, - D75 = 22, - D50 = 23, - ISOStudioTungsten = 24, - OtherLightSource = 255, - } - - [Flags] - internal enum Flash : ushort - { - FlashDidNotFire = 0, - StrobeReturnLightNotDetected = 4, - StrobeReturnLightDetected = 2, - FlashFired = 1, - CompulsoryFlashMode = 8, - AutoMode = 16, - NoFlashFunction = 32, - RedEyeReductionMode = 64, - } - - internal enum SensingMethod : ushort - { - NotDefined = 1, - OneChipColorAreaSensor = 2, - TwoChipColorAreaSensor = 3, - ThreeChipColorAreaSensor = 4, - ColorSequentialAreaSensor = 5, - TriLinearSensor = 7, - ColorSequentialLinearSensor = 8, - } - - internal enum FileSource : byte // UNDEFINED - { - DSC = 3, - } - - internal enum SceneType : byte // UNDEFINED - { - DirectlyPhotographedImage = 1, - } - - internal enum CustomRendered : ushort - { - NormalProcess = 0, - CustomProcess = 1, - } - - internal enum ExposureMode : ushort - { - Auto = 0, - Manual = 1, - AutoBracket = 2, - } - - internal enum WhiteBalance : ushort - { - Auto = 0, - Manual = 1, - } - - internal enum SceneCaptureType : ushort - { - Standard = 0, - Landscape = 1, - Portrait = 2, - NightScene = 3, - } - - internal enum GainControl : ushort - { - None = 0, - LowGainUp = 1, - HighGainUp = 2, - LowGainDown = 3, - HighGainDown = 4, - } - - internal enum Contrast : ushort - { - Normal = 0, - Soft = 1, - Hard = 2, - } - - internal enum Saturation : ushort - { - Normal = 0, - Low = 1, - High = 2, - } - - internal enum Sharpness : ushort - { - Normal = 0, - Soft = 1, - Hard = 2, - } - - internal enum SubjectDistanceRange : ushort - { - Unknown = 0, - Macro = 1, - CloseView = 2, - DistantView = 3, - } - - internal enum GPSLatitudeRef : byte // ASCII - { - North = 78, // 'N' - South = 83, // 'S' - } - - internal enum GPSLongitudeRef : byte // ASCII - { - West = 87, // 'W' - East = 69, // 'E' - } - - internal enum GPSAltitudeRef : byte - { - AboveSeaLevel = 0, - BelowSeaLevel = 1, - } - - internal enum GPSStatus : byte // ASCII - { - MeasurementInProgress = 65, // 'A' - MeasurementInteroperability = 86, // 'V' - } - - internal enum GPSMeasureMode : byte // ASCII - { - TwoDimensional = 50, // '2' - ThreeDimensional = 51, // '3' - } - - internal enum GPSSpeedRef : byte // ASCII - { - KilometersPerHour = 75, // 'K' - MilesPerHour = 77, // 'M' - Knots = 78, // 'N' - } - - internal enum GPSDirectionRef : byte // ASCII - { - TrueDirection = 84, // 'T' - MagneticDirection = 77, // 'M' - } - - internal enum GPSDistanceRef : byte // ASCII - { - Kilometers = 75, // 'K' - Miles = 77, // 'M' - Knots = 78, // 'N' - } - - internal enum GPSDifferential : ushort - { - MeasurementWithoutDifferentialCorrection = 0, - DifferentialCorrectionApplied = 1, - } + Uncompressed = 1, + CCITT1D = 2, + Group3Fax = 3, + Group4Fax = 4, + LZW = 5, + JPEG = 6, + PackBits = 32773, +} + +internal enum PhotometricInterpretation : ushort +{ + WhiteIsZero = 0, + BlackIsZero = 1, + RGB = 2, + RGBPalette = 3, + TransparencyMask = 4, + CMYK = 5, + YCbCr = 6, + CIELab = 8, +} + +internal enum Orientation : ushort +{ + Normal = 1, + MirroredVertically = 2, + Rotated180 = 3, + MirroredHorizontally = 4, + RotatedLeftAndMirroredVertically = 5, + RotatedRight = 6, + RotatedLeft = 7, + RotatedRightAndMirroredVertically = 8, +} + +internal enum PlanarConfiguration : ushort +{ + ChunkyFormat = 1, + PlanarFormat = 2, +} + +internal enum YCbCrPositioning : ushort +{ + Centered = 1, + CoSited = 2, +} + +internal enum ResolutionUnit : ushort +{ + Inches = 2, + Centimeters = 3, +} + +internal enum ColorSpace : ushort +{ + SRGB = 1, + Uncalibrated = 0xfff, +} + +internal enum ExposureProgram : ushort +{ + NotDefined = 0, + Manual = 1, + Normal = 2, + AperturePriority = 3, + ShutterPriority = 4, + + /// + /// Biased toward depth of field. + /// + Creative = 5, + + /// + /// Biased toward fast shutter speed. + /// + Action = 6, + + /// + /// For closeup photos with the background out of focus. + /// + Portrait = 7, + + /// + /// For landscape photos with the background in focus. + /// + Landscape = 8, +} + +internal enum MeteringMode : ushort +{ + Unknown = 0, + Average = 1, + CenterWeightedAverage = 2, + Spot = 3, + MultiSpot = 4, + Pattern = 5, + Partial = 6, + Other = 255, +} + +internal enum LightSource : ushort +{ + Unknown = 0, + Daylight = 1, + Fluorescent = 2, + Tungsten = 3, + Flash = 4, + FineWeather = 9, + CloudyWeather = 10, + Shade = 11, + + /// + /// D 5700 – 7100K + /// + DaylightFluorescent = 12, + + /// + /// N 4600 – 5400K + /// + DayWhiteFluorescent = 13, + + /// + /// W 3900 – 4500K + /// + CoolWhiteFluorescent = 14, + + /// + /// WW 3200 – 3700K + /// + WhiteFluorescent = 15, + StandardLightA = 17, + StandardLightB = 18, + StandardLightC = 19, + D55 = 20, + D65 = 21, + D75 = 22, + D50 = 23, + ISOStudioTungsten = 24, + OtherLightSource = 255, +} + +[Flags] +internal enum Flash : ushort +{ + FlashDidNotFire = 0, + StrobeReturnLightNotDetected = 4, + StrobeReturnLightDetected = 2, + FlashFired = 1, + CompulsoryFlashMode = 8, + AutoMode = 16, + NoFlashFunction = 32, + RedEyeReductionMode = 64, +} + +internal enum SensingMethod : ushort +{ + NotDefined = 1, + OneChipColorAreaSensor = 2, + TwoChipColorAreaSensor = 3, + ThreeChipColorAreaSensor = 4, + ColorSequentialAreaSensor = 5, + TriLinearSensor = 7, + ColorSequentialLinearSensor = 8, +} + +internal enum FileSource : byte // UNDEFINED +{ + DSC = 3, +} + +internal enum SceneType : byte // UNDEFINED +{ + DirectlyPhotographedImage = 1, +} + +internal enum CustomRendered : ushort +{ + NormalProcess = 0, + CustomProcess = 1, +} + +internal enum ExposureMode : ushort +{ + Auto = 0, + Manual = 1, + AutoBracket = 2, +} + +internal enum WhiteBalance : ushort +{ + Auto = 0, + Manual = 1, +} + +internal enum SceneCaptureType : ushort +{ + Standard = 0, + Landscape = 1, + Portrait = 2, + NightScene = 3, +} + +internal enum GainControl : ushort +{ + None = 0, + LowGainUp = 1, + HighGainUp = 2, + LowGainDown = 3, + HighGainDown = 4, +} + +internal enum Contrast : ushort +{ + Normal = 0, + Soft = 1, + Hard = 2, +} + +internal enum Saturation : ushort +{ + Normal = 0, + Low = 1, + High = 2, +} + +internal enum Sharpness : ushort +{ + Normal = 0, + Soft = 1, + Hard = 2, +} + +internal enum SubjectDistanceRange : ushort +{ + Unknown = 0, + Macro = 1, + CloseView = 2, + DistantView = 3, +} + +internal enum GPSLatitudeRef : byte // ASCII +{ + North = 78, // 'N' + South = 83, // 'S' +} + +internal enum GPSLongitudeRef : byte // ASCII +{ + West = 87, // 'W' + East = 69, // 'E' +} + +internal enum GPSAltitudeRef : byte +{ + AboveSeaLevel = 0, + BelowSeaLevel = 1, +} + +internal enum GPSStatus : byte // ASCII +{ + MeasurementInProgress = 65, // 'A' + MeasurementInteroperability = 86, // 'V' +} + +internal enum GPSMeasureMode : byte // ASCII +{ + TwoDimensional = 50, // '2' + ThreeDimensional = 51, // '3' +} + +internal enum GPSSpeedRef : byte // ASCII +{ + KilometersPerHour = 75, // 'K' + MilesPerHour = 77, // 'M' + Knots = 78, // 'N' +} + +internal enum GPSDirectionRef : byte // ASCII +{ + TrueDirection = 84, // 'T' + MagneticDirection = 77, // 'M' +} + +internal enum GPSDistanceRef : byte // ASCII +{ + Kilometers = 75, // 'K' + Miles = 77, // 'M' + Knots = 78, // 'N' +} + +internal enum GPSDifferential : ushort +{ + MeasurementWithoutDifferentialCorrection = 0, + DifferentialCorrectionApplied = 1, } diff --git a/src/Umbraco.Core/Media/Exif/ExifExceptions.cs b/src/Umbraco.Core/Media/Exif/ExifExceptions.cs index 3d0472c100..bd4426e15a 100644 --- a/src/Umbraco.Core/Media/Exif/ExifExceptions.cs +++ b/src/Umbraco.Core/Media/Exif/ExifExceptions.cs @@ -1,47 +1,57 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// The exception that is thrown when the format of the JPEG/EXIF file could not be understood. +/// +/// +[Serializable] +public class NotValidExifFileException : Exception { /// - /// The exception that is thrown when the format of the JPEG/EXIF file could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class NotValidExifFileException : Exception + public NotValidExifFileException() + : base("Not a valid JPEG/EXIF file.") { - /// - /// Initializes a new instance of the class. - /// - public NotValidExifFileException() - : base("Not a valid JPEG/EXIF file.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidExifFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidExifFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidExifFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public NotValidExifFileException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidExifFileException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidExifFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } diff --git a/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs b/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs index 9aa62f4ea3..ffa31f0cc1 100644 --- a/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs +++ b/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs @@ -1,374 +1,487 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif -{ - /// - /// Represents an enumerated value. - /// - internal class ExifEnumProperty : ExifProperty +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents an enumerated value. +/// +internal class ExifEnumProperty : ExifProperty where T : notnull +{ + protected bool mIsBitField; + protected T mValue; + + public ExifEnumProperty(ExifTag tag, T value, bool isbitfield) + : base(tag) { - protected T mValue; - protected bool mIsBitField; - protected override object _Value { get { return Value; } set { Value = (T)value; } } - public new T Value { get { return mValue; } set { mValue = value; } } - public bool IsBitField { get { return mIsBitField; } } + mValue = value; + mIsBitField = isbitfield; + } - static public implicit operator T(ExifEnumProperty obj) { return (T)obj.mValue; } + public ExifEnumProperty(ExifTag tag, T value) + : this(tag, value, false) + { + } - public override string? ToString() { return mValue.ToString(); } + public new T Value + { + get => mValue; + set => mValue = value; + } - public ExifEnumProperty(ExifTag tag, T value, bool isbitfield) - : base(tag) + protected override object _Value + { + get => Value; + set => Value = (T)value; + } + + public bool IsBitField => mIsBitField; + + public override ExifInterOperability Interoperability + { + get { - mValue = value; - mIsBitField = isbitfield; - } + var tagid = ExifTagFactory.GetTagID(mTag); - public ExifEnumProperty(ExifTag tag, T value) - : this(tag, value, false) - { + Type type = typeof(T); + Type basetype = Enum.GetUnderlyingType(type); - } - - public override ExifInterOperability Interoperability - { - get + if (type == typeof(FileSource) || type == typeof(SceneType)) { - ushort tagid = ExifTagFactory.GetTagID(mTag); - - Type type = typeof(T); - Type basetype = Enum.GetUnderlyingType(type); - - if (type == typeof(FileSource) || type == typeof(SceneType)) - { - // UNDEFINED - return new ExifInterOperability(tagid, 7, 1, new byte[] { (byte)((object)mValue) }); - } - else if (type == typeof(GPSLatitudeRef) || type == typeof(GPSLongitudeRef) || - type == typeof(GPSStatus) || type == typeof(GPSMeasureMode) || - type == typeof(GPSSpeedRef) || type == typeof(GPSDirectionRef) || - type == typeof(GPSDistanceRef)) - { - // ASCII - return new ExifInterOperability(tagid, 2, 2, new byte[] { (byte)((object)mValue), 0 }); - } - else if (basetype == typeof(byte)) - { - // BYTE - return new ExifInterOperability(tagid, 1, 1, new byte[] { (byte)((object)mValue) }); - } - else if (basetype == typeof(ushort)) - { - // SHORT - return new ExifInterOperability(tagid, 3, 1, ExifBitConverter.GetBytes((ushort)((object)mValue), BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - else - throw new InvalidOperationException($"An invalid enum type ({basetype.FullName}) was provided for type {type.FullName}"); + // UNDEFINED + return new ExifInterOperability(tagid, 7, 1, new[] { (byte)(object)mValue }); } + + if (type == typeof(GPSLatitudeRef) || type == typeof(GPSLongitudeRef) || + type == typeof(GPSStatus) || type == typeof(GPSMeasureMode) || + type == typeof(GPSSpeedRef) || type == typeof(GPSDirectionRef) || + type == typeof(GPSDistanceRef)) + { + // ASCII + return new ExifInterOperability(tagid, 2, 2, new byte[] { (byte)(object)mValue, 0 }); + } + + if (basetype == typeof(byte)) + { + // BYTE + return new ExifInterOperability(tagid, 1, 1, new[] { (byte)(object)mValue }); + } + + if (basetype == typeof(ushort)) + { + // SHORT + return new ExifInterOperability( + tagid, + 3, + 1, + BitConverterEx.GetBytes((ushort)(object)mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + } + + throw new InvalidOperationException( + $"An invalid enum type ({basetype.FullName}) was provided for type {type.FullName}"); } } - /// - /// Represents an ASCII string. (EXIF Specification: UNDEFINED) Used for the UserComment field. - /// - internal class ExifEncodedString : ExifProperty + public static implicit operator T(ExifEnumProperty obj) => obj.mValue; + + public override string? ToString() => mValue.ToString(); +} + +/// +/// Represents an ASCII string. (EXIF Specification: UNDEFINED) Used for the UserComment field. +/// +internal class ExifEncodedString : ExifProperty +{ + protected string mValue; + + public ExifEncodedString(ExifTag tag, string value, Encoding encoding) + : base(tag) { - protected string mValue; - private Encoding mEncoding; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } - public Encoding Encoding { get { return mEncoding; } set { mEncoding = value; } } - - static public implicit operator string(ExifEncodedString obj) { return obj.mValue; } - - public override string ToString() { return mValue; } - - public ExifEncodedString(ExifTag tag, string value, Encoding encoding) - : base(tag) - { - mValue = value; - mEncoding = encoding; - } - - public override ExifInterOperability Interoperability - { - get - { - string enc = ""; - if (mEncoding == null) - enc = "\0\0\0\0\0\0\0\0"; - else if (mEncoding.EncodingName == "US-ASCII") - enc = "ASCII\0\0\0"; - else if (mEncoding.EncodingName == "Japanese (JIS 0208-1990 and 0212-1990)") - enc = "JIS\0\0\0\0\0"; - else if (mEncoding.EncodingName == "Unicode") - enc = "Unicode\0"; - else - enc = "\0\0\0\0\0\0\0\0"; - - byte[] benc = Encoding.ASCII.GetBytes(enc); - byte[] bstr = (mEncoding == null ? Encoding.ASCII.GetBytes(mValue) : mEncoding.GetBytes(mValue)); - byte[] data = new byte[benc.Length + bstr.Length]; - Array.Copy(benc, 0, data, 0, benc.Length); - Array.Copy(bstr, 0, data, benc.Length, bstr.Length); - - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)data.Length, data); - } - } + mValue = value; + Encoding = encoding; } - /// - /// Represents an ASCII string formatted as DateTime. (EXIF Specification: ASCII) Used for the date time fields. - /// - internal class ExifDateTime : ExifProperty + public new string Value { - protected DateTime mValue; - protected override object _Value { get { return Value; } set { Value = (DateTime)value; } } - public new DateTime Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator DateTime(ExifDateTime obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString("yyyy.MM.dd HH:mm:ss"); } - - public ExifDateTime(ExifTag tag, DateTime value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 2, (uint)20, ExifBitConverter.GetBytes(mValue, true)); - } - } + get => mValue; + set => mValue = value; } - /// - /// Represents the exif version as a 4 byte ASCII string. (EXIF Specification: UNDEFINED) - /// Used for the ExifVersion, FlashpixVersion, InteroperabilityVersion and GPSVersionID fields. - /// - internal class ExifVersion : ExifProperty + protected override object _Value { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value.Substring(0, 4); } } + get => Value; + set => Value = (string)value; + } - public ExifVersion(ExifTag tag, string value) - : base(tag) + public Encoding Encoding { get; set; } + + public override ExifInterOperability Interoperability + { + get { - if (value.Length > 4) - mValue = value.Substring(0, 4); - else if (value.Length < 4) - mValue = value + new string(' ', 4 - value.Length); + var enc = string.Empty; + if (Encoding == null) + { + enc = "\0\0\0\0\0\0\0\0"; + } + else if (Encoding.EncodingName == "US-ASCII") + { + enc = "ASCII\0\0\0"; + } + else if (Encoding.EncodingName == "Japanese (JIS 0208-1990 and 0212-1990)") + { + enc = "JIS\0\0\0\0\0"; + } + else if (Encoding.EncodingName == "Unicode") + { + enc = "Unicode\0"; + } else - mValue = value; - } - - public override string ToString() - { - return mValue; - } - - public override ExifInterOperability Interoperability - { - get { - if (mTag == ExifTag.ExifVersion || mTag == ExifTag.FlashpixVersion || mTag == ExifTag.InteroperabilityVersion) - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, Encoding.ASCII.GetBytes(mValue)); - else - { - byte[] data = new byte[4]; - for (int i = 0; i < 4; i++) - data[i] = byte.Parse(mValue[0].ToString()); - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, data); - } + enc = "\0\0\0\0\0\0\0\0"; } + + var benc = Encoding.ASCII.GetBytes(enc); + var bstr = Encoding == null ? Encoding.ASCII.GetBytes(mValue) : Encoding.GetBytes(mValue); + var data = new byte[benc.Length + bstr.Length]; + Array.Copy(benc, 0, data, 0, benc.Length); + Array.Copy(bstr, 0, data, benc.Length, bstr.Length); + + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)data.Length, data); } } - /// - /// Represents the location and area of the subject (EXIF Specification: 2xSHORT) - /// The coordinate values, width, and height are expressed in relation to the - /// upper left as origin, prior to rotation processing as per the Rotation tag. - /// - internal class ExifPointSubjectArea : ExifUShortArray + public static implicit operator string(ExifEncodedString obj) => obj.mValue; + + public override string ToString() => mValue; +} + +/// +/// Represents an ASCII string formatted as DateTime. (EXIF Specification: ASCII) Used for the date time fields. +/// +internal class ExifDateTime : ExifProperty +{ + protected DateTime mValue; + + public ExifDateTime(ExifTag tag, DateTime value) + : base(tag) => + mValue = value; + + public new DateTime Value { - protected new ushort[] Value { get { return mValue; } set { mValue = value; } } - public ushort X { get { return mValue[0]; } set { mValue[0] = value; } } - public ushort Y { get { return mValue[1]; } set { mValue[1] = value; } } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("({0:d}, {1:d})", mValue[0], mValue[1]); - return sb.ToString(); - } - - public ExifPointSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) - { - - } - - public ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) - : base(tag, new ushort[] {x, y}) - { - - } + get => mValue; + set => mValue = value; } - /// - /// Represents the location and area of the subject (EXIF Specification: 3xSHORT) - /// The coordinate values, width, and height are expressed in relation to the - /// upper left as origin, prior to rotation processing as per the Rotation tag. - /// - internal class ExifCircularSubjectArea : ExifPointSubjectArea + protected override object _Value { - public ushort Diamater { get { return mValue[2]; } set { mValue[2] = value; } } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("({0:d}, {1:d}) {2:d}", mValue[0], mValue[1], mValue[2]); - return sb.ToString(); - } - - public ExifCircularSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) - { - - } - - public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) - : base(tag, new ushort[] { x, y, d }) - { - - } + get => Value; + set => Value = (DateTime)value; } - /// - /// Represents the location and area of the subject (EXIF Specification: 4xSHORT) - /// The coordinate values, width, and height are expressed in relation to the - /// upper left as origin, prior to rotation processing as per the Rotation tag. - /// - internal class ExifRectangularSubjectArea : ExifPointSubjectArea + public override ExifInterOperability Interoperability => + new(ExifTagFactory.GetTagID(mTag), 2, 20, ExifBitConverter.GetBytes(mValue, true)); + + public static implicit operator DateTime(ExifDateTime obj) => obj.mValue; + + public override string ToString() => mValue.ToString("yyyy.MM.dd HH:mm:ss"); +} + +/// +/// Represents the exif version as a 4 byte ASCII string. (EXIF Specification: UNDEFINED) +/// Used for the ExifVersion, FlashpixVersion, InteroperabilityVersion and GPSVersionID fields. +/// +internal class ExifVersion : ExifProperty +{ + protected string mValue; + + public ExifVersion(ExifTag tag, string value) + : base(tag) { - public ushort Width { get { return mValue[2]; } set { mValue[2] = value; } } - public ushort Height { get { return mValue[3]; } set { mValue[3] = value; } } - - public override string ToString() + if (value.Length > 4) { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("({0:d}, {1:d}) ({2:d} x {3:d})", mValue[0], mValue[1], mValue[2], mValue[3]); - return sb.ToString(); + mValue = value[..4]; } - - public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) + else if (value.Length < 4) { - + mValue = value + new string(' ', 4 - value.Length); } - - public ExifRectangularSubjectArea(ExifTag tag, ushort x, ushort y, ushort w, ushort h) - : base(tag, new ushort[] { x, y, w, h }) - { - - } - } - - /// - /// Represents GPS latitudes and longitudes (EXIF Specification: 3xRATIONAL) - /// - internal class GPSLatitudeLongitude : ExifURationalArray - { - protected new MathEx.UFraction32[] Value { get { return mValue; } set { mValue = value; } } - public MathEx.UFraction32 Degrees { get { return mValue[0]; } set { mValue[0] = value; } } - public MathEx.UFraction32 Minutes { get { return mValue[1]; } set { mValue[1] = value; } } - public MathEx.UFraction32 Seconds { get { return mValue[2]; } set { mValue[2] = value; } } - - public static explicit operator float(GPSLatitudeLongitude obj) { return obj.ToFloat(); } - public float ToFloat() - { - return (float)Degrees + ((float)Minutes) / 60.0f + ((float)Seconds) / 3600.0f; - } - - public override string ToString() - { - return string.Format("{0:F2}°{1:F2}'{2:F2}\"", (float)Degrees, (float)Minutes, (float)Seconds); - } - - public GPSLatitudeLongitude(ExifTag tag, MathEx.UFraction32[] value) - : base(tag, value) - { - - } - - public GPSLatitudeLongitude(ExifTag tag, float d, float m, float s) - : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) - { - - } - } - - /// - /// Represents a GPS time stamp as UTC (EXIF Specification: 3xRATIONAL) - /// - internal class GPSTimeStamp : ExifURationalArray - { - protected new MathEx.UFraction32[] Value { get { return mValue; } set { mValue = value; } } - public MathEx.UFraction32 Hour { get { return mValue[0]; } set { mValue[0] = value; } } - public MathEx.UFraction32 Minute { get { return mValue[1]; } set { mValue[1] = value; } } - public MathEx.UFraction32 Second { get { return mValue[2]; } set { mValue[2] = value; } } - - public override string ToString() - { - return string.Format("{0:F2}:{1:F2}:{2:F2}\"", (float)Hour, (float)Minute, (float)Second); - } - - public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) - : base(tag, value) - { - - } - - public GPSTimeStamp(ExifTag tag, float h, float m, float s) - : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(h), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) - { - - } - } - - /// - /// Represents an ASCII string. (EXIF Specification: BYTE) - /// Used by Windows XP. - /// - internal class WindowsByteString : ExifProperty - { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator string(WindowsByteString obj) { return obj.mValue; } - - public override string ToString() { return mValue; } - - public WindowsByteString(ExifTag tag, string value) - : base(tag) + else { mValue = value; } + } - public override ExifInterOperability Interoperability + public new string Value + { + get => mValue; + set => mValue = value[..4]; + } + + protected override object _Value + { + get => Value; + set => Value = (string)value; + } + + public override ExifInterOperability Interoperability + { + get { - get + if (mTag == ExifTag.ExifVersion || mTag == ExifTag.FlashpixVersion || + mTag == ExifTag.InteroperabilityVersion) { - byte[] data = Encoding.Unicode.GetBytes(mValue); - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, Encoding.ASCII.GetBytes(mValue)); } + + var data = new byte[4]; + for (var i = 0; i < 4; i++) + { + data[i] = byte.Parse(mValue[0].ToString()); + } + + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, data); } } + + public override string ToString() => mValue; +} + +/// +/// Represents the location and area of the subject (EXIF Specification: 2xSHORT) +/// The coordinate values, width, and height are expressed in relation to the +/// upper left as origin, prior to rotation processing as per the Rotation tag. +/// +internal class ExifPointSubjectArea : ExifUShortArray +{ + public ExifPointSubjectArea(ExifTag tag, ushort[] value) + : base(tag, value) + { + } + + public ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) + : base(tag, new[] { x, y }) + { + } + + public ushort X + { + get => mValue[0]; + set => mValue[0] = value; + } + + protected new ushort[] Value + { + get => mValue; + set => mValue = value; + } + + public ushort Y + { + get => mValue[1]; + set => mValue[1] = value; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("({0:d}, {1:d})", mValue[0], mValue[1]); + return sb.ToString(); + } +} + +/// +/// Represents the location and area of the subject (EXIF Specification: 3xSHORT) +/// The coordinate values, width, and height are expressed in relation to the +/// upper left as origin, prior to rotation processing as per the Rotation tag. +/// +internal class ExifCircularSubjectArea : ExifPointSubjectArea +{ + public ExifCircularSubjectArea(ExifTag tag, ushort[] value) + : base(tag, value) + { + } + + public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) + : base(tag, new[] { x, y, d }) + { + } + + public ushort Diamater + { + get => mValue[2]; + set => mValue[2] = value; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("({0:d}, {1:d}) {2:d}", mValue[0], mValue[1], mValue[2]); + return sb.ToString(); + } +} + +/// +/// Represents the location and area of the subject (EXIF Specification: 4xSHORT) +/// The coordinate values, width, and height are expressed in relation to the +/// upper left as origin, prior to rotation processing as per the Rotation tag. +/// +internal class ExifRectangularSubjectArea : ExifPointSubjectArea +{ + public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) + : base(tag, value) + { + } + + public ExifRectangularSubjectArea(ExifTag tag, ushort x, ushort y, ushort w, ushort h) + : base(tag, new[] { x, y, w, h }) + { + } + + public ushort Width + { + get => mValue[2]; + set => mValue[2] = value; + } + + public ushort Height + { + get => mValue[3]; + set => mValue[3] = value; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("({0:d}, {1:d}) ({2:d} x {3:d})", mValue[0], mValue[1], mValue[2], mValue[3]); + return sb.ToString(); + } +} + +/// +/// Represents GPS latitudes and longitudes (EXIF Specification: 3xRATIONAL) +/// +internal class GPSLatitudeLongitude : ExifURationalArray +{ + public GPSLatitudeLongitude(ExifTag tag, MathEx.UFraction32[] value) + : base(tag, value) + { + } + + public GPSLatitudeLongitude(ExifTag tag, float d, float m, float s) + : base(tag, new[] { new(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) + { + } + + public MathEx.UFraction32 Degrees + { + get => mValue[0]; + set => mValue[0] = value; + } + + protected new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } + + public MathEx.UFraction32 Minutes + { + get => mValue[1]; + set => mValue[1] = value; + } + + public MathEx.UFraction32 Seconds + { + get => mValue[2]; + set => mValue[2] = value; + } + + public static explicit operator float(GPSLatitudeLongitude obj) => obj.ToFloat(); + + public float ToFloat() => (float)Degrees + ((float)Minutes / 60.0f) + ((float)Seconds / 3600.0f); + + public override string ToString() => + string.Format("{0:F2}°{1:F2}'{2:F2}\"", (float)Degrees, (float)Minutes, (float)Seconds); +} + +/// +/// Represents a GPS time stamp as UTC (EXIF Specification: 3xRATIONAL) +/// +internal class GPSTimeStamp : ExifURationalArray +{ + public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) + : base(tag, value) + { + } + + public GPSTimeStamp(ExifTag tag, float h, float m, float s) + : base(tag, new[] { new(h), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) + { + } + + public MathEx.UFraction32 Hour + { + get => mValue[0]; + set => mValue[0] = value; + } + + protected new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } + + public MathEx.UFraction32 Minute + { + get => mValue[1]; + set => mValue[1] = value; + } + + public MathEx.UFraction32 Second + { + get => mValue[2]; + set => mValue[2] = value; + } + + public override string ToString() => + string.Format("{0:F2}:{1:F2}:{2:F2}\"", (float)Hour, (float)Minute, (float)Second); +} + +/// +/// Represents an ASCII string. (EXIF Specification: BYTE) +/// Used by Windows XP. +/// +internal class WindowsByteString : ExifProperty +{ + protected string mValue; + + public WindowsByteString(ExifTag tag, string value) + : base(tag) => + mValue = value; + + public new string Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (string)value; + } + + public override ExifInterOperability Interoperability + { + get + { + var data = Encoding.Unicode.GetBytes(mValue); + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); + } + } + + public static implicit operator string(WindowsByteString obj) => obj.mValue; + + public override string ToString() => mValue; } diff --git a/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs b/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs index 61d6b70f30..a07ade9963 100644 --- a/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs +++ b/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs @@ -1,131 +1,108 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Provides a custom type descriptor for an ExifFile instance. +/// +internal sealed class ExifFileTypeDescriptionProvider : TypeDescriptionProvider { - /// - /// Provides a custom type descriptor for an ExifFile instance. - /// - internal sealed class ExifFileTypeDescriptionProvider : TypeDescriptionProvider + public ExifFileTypeDescriptionProvider() + : this(TypeDescriptor.GetProvider(typeof(ImageFile))) { - public ExifFileTypeDescriptionProvider() - : this(TypeDescriptor.GetProvider(typeof(ImageFile))) - { - } + } - public ExifFileTypeDescriptionProvider(TypeDescriptionProvider parent) - : base(parent) - { - } - - /// - /// Gets a custom type descriptor for the given type and object. - /// - /// The type of object for which to retrieve the type descriptor. - /// An instance of the type. Can be null if no instance was passed to the . - /// - /// An that can provide metadata for the type. - /// - public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object? instance) - { - return new ExifFileTypeDescriptor(base.GetTypeDescriptor(objectType, instance), instance); - } + public ExifFileTypeDescriptionProvider(TypeDescriptionProvider parent) + : base(parent) + { } /// - /// Expands ExifProperty objects contained in an ExifFile as separate properties. + /// Gets a custom type descriptor for the given type and object. /// - internal sealed class ExifFileTypeDescriptor : CustomTypeDescriptor + /// The type of object for which to retrieve the type descriptor. + /// + /// An instance of the type. Can be null if no instance was passed to the + /// . + /// + /// + /// An that can provide metadata for the type. + /// + public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object? instance) => + new ExifFileTypeDescriptor(base.GetTypeDescriptor(objectType, instance), instance); +} + +/// +/// Expands ExifProperty objects contained in an ExifFile as separate properties. +/// +internal sealed class ExifFileTypeDescriptor : CustomTypeDescriptor +{ + private readonly ImageFile? owner; + + public ExifFileTypeDescriptor(ICustomTypeDescriptor? parent, object? instance) + : base(parent) => + owner = (ImageFile?)instance; + + public override PropertyDescriptorCollection GetProperties(Attribute[]? attributes) => GetProperties(); + + /// + /// Returns a collection of property descriptors for the object represented by this type descriptor. + /// + /// + /// A containing the property descriptions for the + /// object represented by this type descriptor. The default is + /// . + /// + public override PropertyDescriptorCollection GetProperties() { - ImageFile? owner; + // Enumerate the original set of properties and create our new set with it + var properties = new List(); - public ExifFileTypeDescriptor(ICustomTypeDescriptor? parent, object? instance) - : base(parent) + if (owner is not null) { - owner = (ImageFile?)instance; - } - public override PropertyDescriptorCollection GetProperties(Attribute[]? attributes) - { - return GetProperties(); - } - /// - /// Returns a collection of property descriptors for the object represented by this type descriptor. - /// - /// - /// A containing the property descriptions for the object represented by this type descriptor. The default is . - /// - public override PropertyDescriptorCollection GetProperties() - { - // Enumerate the original set of properties and create our new set with it - List properties = new List(); - - if (owner is not null) + foreach (ExifProperty prop in owner.Properties) { - foreach (ExifProperty prop in owner.Properties) - { - ExifPropertyDescriptor pd = new ExifPropertyDescriptor(prop); - properties.Add(pd); - } - } - - // Finally return the list - return new PropertyDescriptorCollection(properties.ToArray(), true); - } - } - internal sealed class ExifPropertyDescriptor : PropertyDescriptor - { - object originalValue; - ExifProperty linkedProperty; - - public ExifPropertyDescriptor(ExifProperty property) - : base(property.Name, new Attribute[] { new BrowsableAttribute(true) }) - { - linkedProperty = property; - originalValue = property.Value; - } - - public override bool CanResetValue(object component) - { - return true; - } - - public override Type ComponentType - { - get { return typeof(JPEGFile); } - } - - public override object GetValue(object? component) - { - return linkedProperty.Value; - } - - public override bool IsReadOnly - { - get { return false; } - } - - public override Type PropertyType - { - get { return linkedProperty.Value.GetType(); } - } - - public override void ResetValue(object component) - { - linkedProperty.Value = originalValue; - } - - public override void SetValue(object? component, object? value) - { - if (value is not null) - { - linkedProperty.Value = value; + var pd = new ExifPropertyDescriptor(prop); + properties.Add(pd); } } - public override bool ShouldSerializeValue(object component) - { - return false; - } + // Finally return the list + return new PropertyDescriptorCollection(properties.ToArray(), true); } } + +internal sealed class ExifPropertyDescriptor : PropertyDescriptor +{ + private readonly ExifProperty linkedProperty; + private readonly object originalValue; + + public ExifPropertyDescriptor(ExifProperty property) + : base(property.Name, new Attribute[] { new BrowsableAttribute(true) }) + { + linkedProperty = property; + originalValue = property.Value; + } + + public override Type ComponentType => typeof(JPEGFile); + + public override bool IsReadOnly => false; + + public override Type PropertyType => linkedProperty.Value.GetType(); + + public override bool CanResetValue(object component) => true; + + public override object GetValue(object? component) => linkedProperty.Value; + + public override void ResetValue(object component) => linkedProperty.Value = originalValue; + + public override void SetValue(object? component, object? value) + { + if (value is not null) + { + linkedProperty.Value = value; + } + } + + public override bool ShouldSerializeValue(object component) => false; +} diff --git a/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs b/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs index 160ee38636..d2d9f8be6a 100644 --- a/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs +++ b/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs @@ -1,60 +1,55 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents interoperability data for an exif tag in the platform byte order. +/// +internal struct ExifInterOperability { - /// - /// Represents interoperability data for an exif tag in the platform byte order. - /// - internal struct ExifInterOperability + public ExifInterOperability(ushort tagid, ushort typeid, uint count, byte[] data) { - private ushort mTagID; - private ushort mTypeID; - private uint mCount; - private byte[] mData; - - /// - /// Gets the tag ID defined in the Exif standard. - /// - public ushort TagID { get { return mTagID; } } - /// - /// Gets the type code defined in the Exif standard. - /// - /// 1 = BYTE (byte) - /// 2 = ASCII (byte array) - /// 3 = SHORT (ushort) - /// 4 = LONG (uint) - /// 5 = RATIONAL (2 x uint: numerator, denominator) - /// 6 = BYTE (sbyte) - /// 7 = UNDEFINED (byte array) - /// 8 = SSHORT (short) - /// 9 = SLONG (int) - /// 10 = SRATIONAL (2 x int: numerator, denominator) - /// 11 = FLOAT (float) - /// 12 = DOUBLE (double) - /// - /// - public ushort TypeID { get { return mTypeID; } } - /// - /// Gets the byte count or number of components. - /// - public uint Count { get { return mCount; } } - /// - /// Gets the field value as an array of bytes. - /// - public byte[] Data { get { return mData; } } - /// - /// Returns the string representation of this instance. - /// - /// - public override string ToString() - { - return string.Format("Tag: {0}, Type: {1}, Count: {2}, Data Length: {3}", mTagID, mTypeID, mCount, mData.Length); - } - - public ExifInterOperability(ushort tagid, ushort typeid, uint count, byte[] data) - { - mTagID = tagid; - mTypeID = typeid; - mCount = count; - mData = data; - } + TagID = tagid; + TypeID = typeid; + Count = count; + Data = data; } + + /// + /// Gets the tag ID defined in the Exif standard. + /// + public ushort TagID { get; } + + /// + /// Gets the type code defined in the Exif standard. + /// + /// 1 = BYTE (byte) + /// 2 = ASCII (byte array) + /// 3 = SHORT (ushort) + /// 4 = LONG (uint) + /// 5 = RATIONAL (2 x uint: numerator, denominator) + /// 6 = BYTE (sbyte) + /// 7 = UNDEFINED (byte array) + /// 8 = SSHORT (short) + /// 9 = SLONG (int) + /// 10 = SRATIONAL (2 x int: numerator, denominator) + /// 11 = FLOAT (float) + /// 12 = DOUBLE (double) + /// + /// + public ushort TypeID { get; } + + /// + /// Gets the byte count or number of components. + /// + public uint Count { get; } + + /// + /// Gets the field value as an array of bytes. + /// + public byte[] Data { get; } + + /// + /// Returns the string representation of this instance. + /// + /// + public override string ToString() => string.Format("Tag: {0}, Type: {1}, Count: {2}, Data Length: {3}", TagID, TypeID, Count, Data.Length); } diff --git a/src/Umbraco.Core/Media/Exif/ExifProperty.cs b/src/Umbraco.Core/Media/Exif/ExifProperty.cs index a3c28aabbc..6d742bb3ba 100644 --- a/src/Umbraco.Core/Media/Exif/ExifProperty.cs +++ b/src/Umbraco.Core/Media/Exif/ExifProperty.cs @@ -1,578 +1,672 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the abstract base class for an Exif property. +/// +internal abstract class ExifProperty { - /// - /// Represents the abstract base class for an Exif property. - /// - internal abstract class ExifProperty + protected IFD mIFD; + protected string? mName; + protected ExifTag mTag; + + public ExifProperty(ExifTag tag) { - protected ExifTag mTag; - protected IFD mIFD; - protected string? mName; - - /// - /// Gets the Exif tag associated with this property. - /// - public ExifTag Tag { get { return mTag; } } - /// - /// Gets the IFD section containing this property. - /// - public IFD IFD { get { return mIFD; } } - /// - /// Gets or sets the name of this property. - /// - public string Name - { - get - { - if (string.IsNullOrEmpty(mName)) - return ExifTagFactory.GetTagName(mTag); - else - return mName; - } - set - { - mName = value; - } - } - protected abstract object _Value { get; set; } - /// - /// Gets or sets the value of this property. - /// - public object Value { get { return _Value; } set { _Value = value; } } - /// - /// Gets interoperability data for this property. - /// - public abstract ExifInterOperability Interoperability { get; } - - public ExifProperty(ExifTag tag) - { - mTag = tag; - mIFD = ExifTagFactory.GetTagIFD(tag); - } + mTag = tag; + mIFD = ExifTagFactory.GetTagIFD(tag); } /// - /// Represents an 8-bit unsigned integer. (EXIF Specification: BYTE) + /// Gets the Exif tag associated with this property. /// - internal class ExifByte : ExifProperty + public ExifTag Tag => mTag; + + /// + /// Gets the IFD section containing this property. + /// + public IFD IFD => mIFD; + + /// + /// Gets or sets the name of this property. + /// + public string Name { - protected byte mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToByte(value); } } - public new byte Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator byte(ExifByte obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString(); } - - public ExifByte(ExifTag tag, byte value) - : base(tag) + get { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get + if (string.IsNullOrEmpty(mName)) { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, 1, new byte[] { mValue }); + return ExifTagFactory.GetTagName(mTag); } + + return mName; } + set => mName = value; } /// - /// Represents an array of 8-bit unsigned integers. (EXIF Specification: BYTE with count > 1) + /// Gets or sets the value of this property. /// - internal class ExifByteArray : ExifProperty + public object Value { - protected byte[] mValue; - protected override object _Value { get { return Value; } set { Value = (byte[])value; } } - public new byte[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator byte[](ExifByteArray obj) { return obj.mValue; } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (byte b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifByteArray(ExifTag tag, byte[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.Length, mValue); - } - } + get => _Value; + set => _Value = value; } + protected abstract object _Value { get; set; } + /// - /// Represents an ASCII string. (EXIF Specification: ASCII) + /// Gets interoperability data for this property. /// - internal class ExifAscii : ExifProperty + public abstract ExifInterOperability Interoperability { get; } +} + +/// +/// Represents an 8-bit unsigned integer. (EXIF Specification: BYTE) +/// +internal class ExifByte : ExifProperty +{ + protected byte mValue; + + public ExifByte(ExifTag tag, byte value) + : base(tag) => + mValue = value; + + public new byte Value { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } - - public Encoding Encoding { get; private set; } - - static public implicit operator string(ExifAscii obj) { return obj.mValue; } - - public override string ToString() { return mValue; } - - public ExifAscii(ExifTag tag, string value, Encoding encoding) - : base(tag) - { - mValue = value; - Encoding = encoding; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 2, (uint)mValue.Length + 1, ExifBitConverter.GetBytes(mValue, true, Encoding)); - } - } + get => mValue; + set => mValue = value; } - /// - /// Represents a 16-bit unsigned integer. (EXIF Specification: SHORT) - /// - internal class ExifUShort : ExifProperty + protected override object _Value { - protected ushort mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToUInt16(value); } } - public new ushort Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator ushort(ExifUShort obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString(); } - - public ExifUShort(ExifTag tag, ushort value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } + get => Value; + set => Value = Convert.ToByte(value); } - /// - /// Represents an array of 16-bit unsigned integers. - /// (EXIF Specification: SHORT with count > 1) - /// - internal class ExifUShortArray : ExifProperty + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, 1, new[] { mValue }); + + public static implicit operator byte(ExifByte obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 8-bit unsigned integers. (EXIF Specification: BYTE with count > 1) +/// +internal class ExifByteArray : ExifProperty +{ + protected byte[] mValue; + + public ExifByteArray(ExifTag tag, byte[] value) + : base(tag) => + mValue = value; + + public new byte[] Value { - protected ushort[] mValue; - protected override object _Value { get { return Value; } set { Value = (ushort[])value; } } - public new ushort[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator ushort[](ExifUShortArray obj) { return obj.mValue; } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (ushort b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifUShortArray(ExifTag tag, ushort[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + get => mValue; + set => mValue = value; } - /// - /// Represents a 32-bit unsigned integer. (EXIF Specification: LONG) - /// - internal class ExifUInt : ExifProperty + protected override object _Value { - protected uint mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToUInt32(value); } } - public new uint Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator uint(ExifUInt obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString(); } - - public ExifUInt(ExifTag tag, uint value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 4, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } + get => Value; + set => Value = (byte[])value; } - /// - /// Represents an array of 16-bit unsigned integers. - /// (EXIF Specification: LONG with count > 1) - /// - internal class ExifUIntArray : ExifProperty + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.Length, mValue); + + public static implicit operator byte[](ExifByteArray obj) => obj.mValue; + + public override string ToString() { - protected uint[] mValue; - protected override object _Value { get { return Value; } set { Value = (uint[])value; } } - public new uint[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator uint[](ExifUIntArray obj) { return obj.mValue; } - - public override string ToString() + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (uint b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); + sb.Append(b); + sb.Append(' '); } - public ExifUIntArray(ExifTag tag, uint[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents a rational number defined with a 32-bit unsigned numerator - /// and denominator. (EXIF Specification: RATIONAL) - /// - internal class ExifURational : ExifProperty - { - protected MathEx.UFraction32 mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.UFraction32)value; } } - public new MathEx.UFraction32 Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() { return mValue.ToString(); } - public float ToFloat() { return (float)mValue; } - - static public explicit operator float(ExifURational obj) { return (float)obj.mValue; } - - public uint[] ToArray() - { - return new uint[] { mValue.Numerator, mValue.Denominator }; - } - - public ExifURational(ExifTag tag, uint numerator, uint denominator) - : base(tag) - { - mValue = new MathEx.UFraction32(numerator, denominator); - } - - public ExifURational(ExifTag tag, MathEx.UFraction32 value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 5, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents an array of unsigned rational numbers. - /// (EXIF Specification: RATIONAL with count > 1) - /// - internal class ExifURationalArray : ExifProperty - { - protected MathEx.UFraction32[] mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.UFraction32[])value; } } - public new MathEx.UFraction32[] Value { get { return mValue; } set { mValue = value; } } - - static public explicit operator float[](ExifURationalArray obj) - { - float[] result = new float[obj.mValue.Length]; - for (int i = 0; i < obj.mValue.Length; i++) - result[i] = (float)obj.mValue[i]; - return result; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (MathEx.UFraction32 b in mValue) - { - sb.Append(b.ToString()); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifURationalArray(ExifTag tag, MathEx.UFraction32[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 5, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents a byte array that can take any value. (EXIF Specification: UNDEFINED) - /// - internal class ExifUndefined : ExifProperty - { - protected byte[] mValue; - protected override object _Value { get { return Value; } set { Value = (byte[])value; } } - public new byte[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator byte[](ExifUndefined obj) { return obj.mValue; } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (byte b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifUndefined(ExifTag tag, byte[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)mValue.Length, mValue); - } - } - } - - /// - /// Represents a 32-bit signed integer. (EXIF Specification: SLONG) - /// - internal class ExifSInt : ExifProperty - { - protected int mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToInt32(value); } } - public new int Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() { return mValue.ToString(); } - - static public implicit operator int(ExifSInt obj) { return obj.mValue; } - - public ExifSInt(ExifTag tag, int value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 9, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents an array of 32-bit signed integers. - /// (EXIF Specification: SLONG with count > 1) - /// - internal class ExifSIntArray : ExifProperty - { - protected int[] mValue; - protected override object _Value { get { return Value; } set { Value = (int[])value; } } - public new int[] Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (int b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - static public implicit operator int[](ExifSIntArray obj) { return obj.mValue; } - - public ExifSIntArray(ExifTag tag, int[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 9, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents a rational number defined with a 32-bit signed numerator - /// and denominator. (EXIF Specification: SRATIONAL) - /// - internal class ExifSRational : ExifProperty - { - protected MathEx.Fraction32 mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.Fraction32)value; } } - public new MathEx.Fraction32 Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() { return mValue.ToString(); } - public float ToFloat() { return (float)mValue; } - - static public explicit operator float(ExifSRational obj) { return (float)obj.mValue; } - - public int[] ToArray() - { - return new int[] { mValue.Numerator, mValue.Denominator }; - } - - public ExifSRational(ExifTag tag, int numerator, int denominator) - : base(tag) - { - mValue = new MathEx.Fraction32(numerator, denominator); - } - - public ExifSRational(ExifTag tag, MathEx.Fraction32 value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 10, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents an array of signed rational numbers. - /// (EXIF Specification: SRATIONAL with count > 1) - /// - internal class ExifSRationalArray : ExifProperty - { - protected MathEx.Fraction32[] mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.Fraction32[])value; } } - public new MathEx.Fraction32[] Value { get { return mValue; } set { mValue = value; } } - - static public explicit operator float[](ExifSRationalArray obj) - { - float[] result = new float[obj.mValue.Length]; - for (int i = 0; i < obj.mValue.Length; i++) - result[i] = (float)obj.mValue[i]; - return result; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (MathEx.Fraction32 b in mValue) - { - sb.Append(b.ToString()); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifSRationalArray(ExifTag tag, MathEx.Fraction32[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 10, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents an ASCII string. (EXIF Specification: ASCII) +/// +internal class ExifAscii : ExifProperty +{ + protected string mValue; + + public ExifAscii(ExifTag tag, string value, Encoding encoding) + : base(tag) + { + mValue = value; + Encoding = encoding; + } + + public new string Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (string)value; + } + + public Encoding Encoding { get; } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 2, + (uint)mValue.Length + 1, + ExifBitConverter.GetBytes(mValue, true, Encoding)); + + public static implicit operator string(ExifAscii obj) => obj.mValue; + + public override string ToString() => mValue; +} + +/// +/// Represents a 16-bit unsigned integer. (EXIF Specification: SHORT) +/// +internal class ExifUShort : ExifProperty +{ + protected ushort mValue; + + public ExifUShort(ExifTag tag, ushort value) + : base(tag) => + mValue = value; + + public new ushort Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = Convert.ToUInt16(value); + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 3, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + + public static implicit operator ushort(ExifUShort obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 16-bit unsigned integers. +/// (EXIF Specification: SHORT with count > 1) +/// +internal class ExifUShortArray : ExifProperty +{ + protected ushort[] mValue; + + public ExifUShortArray(ExifTag tag, ushort[] value) + : base(tag) => + mValue = value; + + public new ushort[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (ushort[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 3, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static implicit operator ushort[](ExifUShortArray obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a 32-bit unsigned integer. (EXIF Specification: LONG) +/// +internal class ExifUInt : ExifProperty +{ + protected uint mValue; + + public ExifUInt(ExifTag tag, uint value) + : base(tag) => + mValue = value; + + public new uint Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = Convert.ToUInt32(value); + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 4, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + + public static implicit operator uint(ExifUInt obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 16-bit unsigned integers. +/// (EXIF Specification: LONG with count > 1) +/// +internal class ExifUIntArray : ExifProperty +{ + protected uint[] mValue; + + public ExifUIntArray(ExifTag tag, uint[] value) + : base(tag) => + mValue = value; + + public new uint[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (uint[])value; + } + + public override ExifInterOperability Interoperability => new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static implicit operator uint[](ExifUIntArray obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a rational number defined with a 32-bit unsigned numerator +/// and denominator. (EXIF Specification: RATIONAL) +/// +internal class ExifURational : ExifProperty +{ + protected MathEx.UFraction32 mValue; + + public ExifURational(ExifTag tag, uint numerator, uint denominator) + : base(tag) => + mValue = new MathEx.UFraction32(numerator, denominator); + + public ExifURational(ExifTag tag, MathEx.UFraction32 value) + : base(tag) => + mValue = value; + + public new MathEx.UFraction32 Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.UFraction32)value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 5, + 1, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float(ExifURational obj) => (float)obj.mValue; + + public override string ToString() => mValue.ToString(); + + public float ToFloat() => (float)mValue; + + public uint[] ToArray() => new[] { mValue.Numerator, mValue.Denominator }; +} + +/// +/// Represents an array of unsigned rational numbers. +/// (EXIF Specification: RATIONAL with count > 1) +/// +internal class ExifURationalArray : ExifProperty +{ + protected MathEx.UFraction32[] mValue; + + public ExifURationalArray(ExifTag tag, MathEx.UFraction32[] value) + : base(tag) => + mValue = value; + + public new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.UFraction32[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 5, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float[](ExifURationalArray obj) + { + var result = new float[obj.mValue.Length]; + for (var i = 0; i < obj.mValue.Length; i++) + { + result[i] = (float)obj.mValue[i]; + } + + return result; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (MathEx.UFraction32 b in mValue) + { + sb.Append(b.ToString()); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a byte array that can take any value. (EXIF Specification: UNDEFINED) +/// +internal class ExifUndefined : ExifProperty +{ + protected byte[] mValue; + + public ExifUndefined(ExifTag tag, byte[] value) + : base(tag) => + mValue = value; + + public new byte[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (byte[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)mValue.Length, mValue); + + public static implicit operator byte[](ExifUndefined obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a 32-bit signed integer. (EXIF Specification: SLONG) +/// +internal class ExifSInt : ExifProperty +{ + protected int mValue; + + public ExifSInt(ExifTag tag, int value) + : base(tag) => + mValue = value; + + public new int Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = Convert.ToInt32(value); + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 9, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + + public static implicit operator int(ExifSInt obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 32-bit signed integers. +/// (EXIF Specification: SLONG with count > 1) +/// +internal class ExifSIntArray : ExifProperty +{ + protected int[] mValue; + + public ExifSIntArray(ExifTag tag, int[] value) + : base(tag) => + mValue = value; + + public new int[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (int[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 9, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static implicit operator int[](ExifSIntArray obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a rational number defined with a 32-bit signed numerator +/// and denominator. (EXIF Specification: SRATIONAL) +/// +internal class ExifSRational : ExifProperty +{ + protected MathEx.Fraction32 mValue; + + public ExifSRational(ExifTag tag, int numerator, int denominator) + : base(tag) => + mValue = new MathEx.Fraction32(numerator, denominator); + + public ExifSRational(ExifTag tag, MathEx.Fraction32 value) + : base(tag) => + mValue = value; + + public new MathEx.Fraction32 Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.Fraction32)value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 10, + 1, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float(ExifSRational obj) => (float)obj.mValue; + + public override string ToString() => mValue.ToString(); + + public float ToFloat() => (float)mValue; + + public int[] ToArray() => new[] { mValue.Numerator, mValue.Denominator }; +} + +/// +/// Represents an array of signed rational numbers. +/// (EXIF Specification: SRATIONAL with count > 1) +/// +internal class ExifSRationalArray : ExifProperty +{ + protected MathEx.Fraction32[] mValue; + + public ExifSRationalArray(ExifTag tag, MathEx.Fraction32[] value) + : base(tag) => + mValue = value; + + public new MathEx.Fraction32[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.Fraction32[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 10, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float[](ExifSRationalArray obj) + { + var result = new float[obj.mValue.Length]; + for (var i = 0; i < obj.mValue.Length; i++) + { + result[i] = (float)obj.mValue[i]; + } + + return result; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (MathEx.Fraction32 b in mValue) + { + sb.Append(b.ToString()); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs index 7114b2eb14..f77f0c89cd 100644 --- a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs +++ b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs @@ -1,384 +1,493 @@ -using System; -using System.Collections.Generic; +using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a collection of objects. +/// +internal class ExifPropertyCollection : IDictionary { - /// - /// Represents a collection of objects. - /// - internal class ExifPropertyCollection : IDictionary + #region Constructor + + internal ExifPropertyCollection(ImageFile parentFile) { - #region Member Variables - private ImageFile parent; - private Dictionary items; - #endregion - - #region Constructor - internal ExifPropertyCollection (ImageFile parentFile) - { - parent = parentFile; - items = new Dictionary (); - } - #endregion - - #region Properties - /// - /// Gets the number of elements contained in the collection. - /// - public int Count { - get { return items.Count; } - } - /// - /// Gets a collection containing the keys in this collection. - /// - public ICollection Keys { - get { return items.Keys; } - } - /// - /// Gets a collection containing the values in this collection. - /// - public ICollection Values { - get { return items.Values; } - } - /// - /// Gets or sets the with the specified key. - /// - public ExifProperty this[ExifTag key] { - get { return items[key]; } - set { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, value); - } - } - #endregion - - #region ExifProperty Collection Setters - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, byte value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifByte (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, string value) - { - if (items.ContainsKey (key)) - items.Remove (key); - if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) { - items.Add (key, new WindowsByteString (key, value)); - } else { - items.Add (key, new ExifAscii (key, value, parent.Encoding)); - } - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, ushort value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifUShort (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, int value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifSInt (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, uint value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifUInt (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, float value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURational (key, new MathEx.UFraction32 (value))); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, double value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURational (key, new MathEx.UFraction32 (value))); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, object value) - { - Type type = value.GetType (); - if (type.IsEnum) { - Type etype = typeof(ExifEnumProperty<>).MakeGenericType (new Type[] { type }); - object? prop = Activator.CreateInstance (etype, new object[] { key, value }); - if (items.ContainsKey (key)) - items.Remove (key); - if (prop is ExifProperty exifProperty) - { - items.Add (key, exifProperty); - } - } else - throw new ArgumentException ("No exif property exists for this tag.", "value"); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - /// String encoding. - public void Set (ExifTag key, string value, Encoding encoding) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifEncodedString (key, value, encoding)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, DateTime value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifDateTime (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// Angular degrees (or clock hours for a timestamp). - /// Angular minutes (or clock minutes for a timestamp). - /// Angular seconds (or clock seconds for a timestamp). - public void Set (ExifTag key, float d, float m, float s) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURationalArray (key, new MathEx.UFraction32[] { new MathEx.UFraction32 (d), new MathEx.UFraction32 (m), new MathEx.UFraction32 (s) })); - } - #endregion - - #region Instance Methods - /// - /// Adds the specified item to the collection. - /// - /// The to add to the collection. - public void Add (ExifProperty item) - { - ExifProperty? oldItem = null; - if (items.TryGetValue (item.Tag, out oldItem)) - items[item.Tag] = item; - else - items.Add (item.Tag, item); - } - /// - /// Removes all items from the collection. - /// - public void Clear () - { - items.Clear (); - } - /// - /// Determines whether the collection contains an element with the specified key. - /// - /// The key to locate in the collection. - /// - /// true if the collection contains an element with the key; otherwise, false. - /// - /// - /// is null. - public bool ContainsKey (ExifTag key) - { - return items.ContainsKey (key); - } - /// - /// Removes the element with the specified key from the collection. - /// - /// The key of the element to remove. - /// - /// true if the element is successfully removed; otherwise, false. This method also returns false if was not found in the original collection. - /// - /// - /// is null. - public bool Remove (ExifTag key) - { - return items.Remove (key); - } - /// - /// Removes all items with the given IFD from the collection. - /// - /// The IFD section to remove. - public void Remove (IFD ifd) - { - List toRemove = new List (); - foreach (KeyValuePair item in items) { - if (item.Value.IFD == ifd) - toRemove.Add (item.Key); - } - foreach (ExifTag tag in toRemove) - items.Remove (tag); - } - /// - /// Gets the value associated with the specified key. - /// - /// The key whose value to get. - /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. - /// - /// true if the collection contains an element with the specified key; otherwise, false. - /// - /// - /// is null. - public bool TryGetValue (ExifTag key, [MaybeNullWhen(false)] out ExifProperty value) - { - return items.TryGetValue (key, out value); - } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator () - { - return Values.GetEnumerator (); - } - #endregion - - #region Hidden Interface - /// - /// Adds an element with the provided key and value to the . - /// - /// The object to use as the key of the element to add. - /// The object to use as the value of the element to add. - /// - /// is null. - /// An element with the same key already exists in the . - /// The is read-only. - void IDictionary.Add (ExifTag key, ExifProperty value) - { - Add (value); - } - /// - /// Adds an item to the . - /// - /// The object to add to the . - /// The is read-only. - void ICollection>.Add (KeyValuePair item) - { - Add (item.Value); - } - bool ICollection>.Contains (KeyValuePair item) - { - throw new NotSupportedException (); - } - /// - /// Copies the elements of the to an , starting at a particular index. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. - /// The zero-based index in at which copying begins. - /// - /// is null. - /// - /// is less than 0. - /// - /// is multidimensional.-or- is equal to or greater than the length of .-or-The number of elements in the source is greater than the available space from to the end of the destination .-or-Type cannot be cast automatically to the type of the destination . - void ICollection>.CopyTo (KeyValuePair[] array, int arrayIndex) - { - if (array == null) - throw new ArgumentNullException ("array"); - if (arrayIndex < 0) - throw new ArgumentOutOfRangeException ("arrayIndex"); - if (array.Rank > 1) - throw new ArgumentException ("Destination array is multidimensional.", "array"); - if (arrayIndex >= array.Length) - throw new ArgumentException ("arrayIndex is equal to or greater than the length of destination array", "array"); - if (arrayIndex + items.Count > array.Length) - throw new ArgumentException ("There is not enough space in destination array.", "array"); - - int i = 0; - foreach (KeyValuePair item in items) { - if (i >= arrayIndex) { - array[i] = item; - } - i++; - } - } - /// - /// Gets a value indicating whether the is read-only. - /// - /// true if the is read-only; otherwise, false. - bool ICollection>.IsReadOnly { - get { return false; } - } - /// - /// Removes the first occurrence of a specific object from the . - /// - /// The object to remove from the . - /// - /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . - /// - /// The is read-only. - bool ICollection>.Remove (KeyValuePair item) - { - throw new NotSupportedException (); - } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () - { - return GetEnumerator (); - } - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// A that can be used to iterate through the collection. - /// - IEnumerator> IEnumerable>.GetEnumerator () - { - return items.GetEnumerator (); - } - #endregion + parent = parentFile; + items = new Dictionary(); } + + #endregion + + #region Member Variables + + private readonly ImageFile parent; + private readonly Dictionary items; + + #endregion + + #region Properties + + /// + /// Gets the number of elements contained in the collection. + /// + public int Count => items.Count; + + /// + /// Gets a collection containing the keys in this collection. + /// + public ICollection Keys => items.Keys; + + /// + /// Gets a collection containing the values in this collection. + /// + public ICollection Values => items.Values; + + /// + /// Gets or sets the with the specified key. + /// + public ExifProperty this[ExifTag key] + { + get => items[key]; + set + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, value); + } + } + + #endregion + + #region ExifProperty Collection Setters + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, byte value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifByte(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, string value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || + key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) + { + items.Add(key, new WindowsByteString(key, value)); + } + else + { + items.Add(key, new ExifAscii(key, value, parent.Encoding)); + } + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, ushort value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifUShort(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, int value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifSInt(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, uint value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifUInt(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, float value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifURational(key, new MathEx.UFraction32(value))); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, double value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifURational(key, new MathEx.UFraction32(value))); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, object value) + { + Type type = value.GetType(); + if (type.IsEnum) + { + Type etype = typeof(ExifEnumProperty<>).MakeGenericType(type); + var prop = Activator.CreateInstance(etype, key, value); + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + if (prop is ExifProperty exifProperty) + { + items.Add(key, exifProperty); + } + } + else + { + throw new ArgumentException("No exif property exists for this tag.", "value"); + } + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + /// String encoding. + public void Set(ExifTag key, string value, Encoding encoding) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifEncodedString(key, value, encoding)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, DateTime value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifDateTime(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// Angular degrees (or clock hours for a timestamp). + /// Angular minutes (or clock minutes for a timestamp). + /// Angular seconds (or clock seconds for a timestamp). + public void Set(ExifTag key, float d, float m, float s) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifURationalArray(key, new[] { new(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) })); + } + + #endregion + + #region Instance Methods + + /// + /// Adds the specified item to the collection. + /// + /// The to add to the collection. + public void Add(ExifProperty item) + { + ExifProperty? oldItem = null; + if (items.TryGetValue(item.Tag, out oldItem)) + { + items[item.Tag] = item; + } + else + { + items.Add(item.Tag, item); + } + } + + /// + /// Removes all items from the collection. + /// + public void Clear() => items.Clear(); + + /// + /// Determines whether the collection contains an element with the specified key. + /// + /// The key to locate in the collection. + /// + /// true if the collection contains an element with the key; otherwise, false. + /// + /// + /// is null. + /// + public bool ContainsKey(ExifTag key) => items.ContainsKey(key); + + /// + /// Removes the element with the specified key from the collection. + /// + /// The key of the element to remove. + /// + /// true if the element is successfully removed; otherwise, false. This method also returns false if + /// was not found in the original collection. + /// + /// + /// is null. + /// + public bool Remove(ExifTag key) => items.Remove(key); + + /// + /// Removes all items with the given IFD from the collection. + /// + /// The IFD section to remove. + public void Remove(IFD ifd) + { + var toRemove = new List(); + foreach (KeyValuePair item in items) + { + if (item.Value.IFD == ifd) + { + toRemove.Add(item.Key); + } + } + + foreach (ExifTag tag in toRemove) + { + items.Remove(tag); + } + } + + /// + /// Gets the value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed + /// uninitialized. + /// + /// + /// true if the collection contains an element with the specified key; otherwise, false. + /// + /// + /// is null. + /// + public bool TryGetValue(ExifTag key, [MaybeNullWhen(false)] out ExifProperty value) => + items.TryGetValue(key, out value); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() => Values.GetEnumerator(); + + #endregion + + #region Hidden Interface + + /// + /// Adds an element with the provided key and value to the . + /// + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + /// + /// is null. + /// + /// + /// An element with the same key already exists in the + /// . + /// + /// + /// The is + /// read-only. + /// + void IDictionary.Add(ExifTag key, ExifProperty value) => Add(value); + + /// + /// Adds an item to the . + /// + /// The object to add to the . + /// + /// The is + /// read-only. + /// + void ICollection>.Add(KeyValuePair item) => + Add(item.Value); + + bool ICollection>.Contains(KeyValuePair item) => + throw new NotSupportedException(); + + /// + /// Copies the elements of the to an + /// , starting at a particular index. + /// + /// + /// The one-dimensional that is the destination of the elements copied + /// from . The must have + /// zero-based indexing. + /// + /// The zero-based index in at which copying begins. + /// + /// is null. + /// + /// + /// is less than 0. + /// + /// + /// is multidimensional.-or- is equal to or greater than the + /// length of .-or-The number of elements in the source + /// is greater than the available space from + /// to the end of the destination .-or-Type + /// cannot be cast automatically to the type of the destination . + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException("arrayIndex"); + } + + if (array.Rank > 1) + { + throw new ArgumentException("Destination array is multidimensional.", "array"); + } + + if (arrayIndex >= array.Length) + { + throw new ArgumentException("arrayIndex is equal to or greater than the length of destination array", "array"); + } + + if (arrayIndex + items.Count > array.Length) + { + throw new ArgumentException("There is not enough space in destination array.", "array"); + } + + var i = 0; + foreach (KeyValuePair item in items) + { + if (i >= arrayIndex) + { + array[i] = item; + } + + i++; + } + } + + /// + /// Gets a value indicating whether the is read-only. + /// + /// true if the is read-only; otherwise, false. + bool ICollection>.IsReadOnly => false; + + /// + /// Removes the first occurrence of a specific object from the + /// . + /// + /// The object to remove from the . + /// + /// true if was successfully removed from the + /// ; otherwise, false. This method also returns false if + /// is not found in the original . + /// + /// + /// The is + /// read-only. + /// + bool ICollection>.Remove(KeyValuePair item) => + throw new NotSupportedException(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + IEnumerator> IEnumerable>.GetEnumerator() => + items.GetEnumerator(); + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs b/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs index f47cab1c35..4290dcaf7c 100644 --- a/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs +++ b/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs @@ -1,253 +1,598 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif -{ - /// - /// Creates exif properties from interoperability parameters. - /// - internal static class ExifPropertyFactory - { - #region Static Methods - /// - /// Creates an ExifProperty from the given interoperability parameters. - /// - /// The tag id of the exif property. - /// The type id of the exif property. - /// Byte or component count. - /// Field data as an array of bytes. - /// Byte order of value. - /// IFD section containing this property. - /// The encoding to be used for text metadata when the source encoding is unknown. - /// an ExifProperty initialized from the interoperability parameters. - public static ExifProperty Get(ushort tag, ushort type, uint count, byte[] value, BitConverterEx.ByteOrder byteOrder, IFD ifd, Encoding encoding) - { - BitConverterEx conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); - // Find the exif tag corresponding to given tag id - ExifTag etag = ExifTagFactory.GetExifTag(ifd, tag); +namespace Umbraco.Cms.Core.Media.Exif; - if (ifd == IFD.Zeroth) +/// +/// Creates exif properties from interoperability parameters. +/// +internal static class ExifPropertyFactory +{ + #region Static Methods + + /// + /// Creates an ExifProperty from the given interoperability parameters. + /// + /// The tag id of the exif property. + /// The type id of the exif property. + /// Byte or component count. + /// Field data as an array of bytes. + /// Byte order of value. + /// IFD section containing this property. + /// The encoding to be used for text metadata when the source encoding is unknown. + /// an ExifProperty initialized from the interoperability parameters. + public static ExifProperty Get(ushort tag, ushort type, uint count, byte[] value, BitConverterEx.ByteOrder byteOrder, IFD ifd, Encoding encoding) + { + var conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); + + // Find the exif tag corresponding to given tag id + ExifTag etag = ExifTagFactory.GetExifTag(ifd, tag); + + if (ifd == IFD.Zeroth) + { + // Compression + if (tag == 0x103) { - if (tag == 0x103) // Compression - return new ExifEnumProperty(ExifTag.Compression, (Compression)conv.ToUInt16(value, 0)); - else if (tag == 0x106) // PhotometricInterpretation - return new ExifEnumProperty(ExifTag.PhotometricInterpretation, (PhotometricInterpretation)conv.ToUInt16(value, 0)); - else if (tag == 0x112) // Orientation - return new ExifEnumProperty(ExifTag.Orientation, (Orientation)conv.ToUInt16(value, 0)); - else if (tag == 0x11c) // PlanarConfiguration - return new ExifEnumProperty(ExifTag.PlanarConfiguration, (PlanarConfiguration)conv.ToUInt16(value, 0)); - else if (tag == 0x213) // YCbCrPositioning - return new ExifEnumProperty(ExifTag.YCbCrPositioning, (YCbCrPositioning)conv.ToUInt16(value, 0)); - else if (tag == 0x128) // ResolutionUnit - return new ExifEnumProperty(ExifTag.ResolutionUnit, (ResolutionUnit)conv.ToUInt16(value, 0)); - else if (tag == 0x132) // DateTime - return new ExifDateTime(ExifTag.DateTime, ExifBitConverter.ToDateTime(value)); - else if (tag == 0x9c9b || tag == 0x9c9c || // Windows tags - tag == 0x9c9d || tag == 0x9c9e || tag == 0x9c9f) - return new WindowsByteString(etag, Encoding.Unicode.GetString(value).TrimEnd(Constants.CharArrays.NullTerminator)); + return new ExifEnumProperty(ExifTag.Compression, (Compression)conv.ToUInt16(value, 0)); } - else if (ifd == IFD.EXIF) + + // PhotometricInterpretation + if (tag == 0x106) { - if (tag == 0x9000) // ExifVersion - return new ExifVersion(ExifTag.ExifVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); - else if (tag == 0xa000) // FlashpixVersion - return new ExifVersion(ExifTag.FlashpixVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); - else if (tag == 0xa001) // ColorSpace - return new ExifEnumProperty(ExifTag.ColorSpace, (ColorSpace)conv.ToUInt16(value, 0)); - else if (tag == 0x9286) // UserComment + return new ExifEnumProperty( + ExifTag.PhotometricInterpretation, + (PhotometricInterpretation)conv.ToUInt16(value, 0)); + } + + // Orientation + if (tag == 0x112) + { + return new ExifEnumProperty(ExifTag.Orientation, (Orientation)conv.ToUInt16(value, 0)); + } + + // PlanarConfiguration + if (tag == 0x11c) + { + return new ExifEnumProperty( + ExifTag.PlanarConfiguration, + (PlanarConfiguration)conv.ToUInt16(value, 0)); + } + + // YCbCrPositioning + if (tag == 0x213) + { + return new ExifEnumProperty( + ExifTag.YCbCrPositioning, + (YCbCrPositioning)conv.ToUInt16(value, 0)); + } + + // ResolutionUnit + if (tag == 0x128) + { + return new ExifEnumProperty( + ExifTag.ResolutionUnit, + (ResolutionUnit)conv.ToUInt16(value, 0)); + } + + // DateTime + if (tag == 0x132) + { + return new ExifDateTime(ExifTag.DateTime, ExifBitConverter.ToDateTime(value)); + } + + if (tag == 0x9c9b || tag == 0x9c9c || // Windows tags + tag == 0x9c9d || tag == 0x9c9e || tag == 0x9c9f) + { + return new WindowsByteString( + etag, + Encoding.Unicode.GetString(value).TrimEnd(Constants.CharArrays.NullTerminator)); + } + } + else if (ifd == IFD.EXIF) + { + // ExifVersion + if (tag == 0x9000) + { + return new ExifVersion(ExifTag.ExifVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); + } + + // FlashpixVersion + if (tag == 0xa000) + { + return new ExifVersion(ExifTag.FlashpixVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); + } + + // ColorSpace + if (tag == 0xa001) + { + return new ExifEnumProperty(ExifTag.ColorSpace, (ColorSpace)conv.ToUInt16(value, 0)); + } + + // UserComment + if (tag == 0x9286) + { + // Default to ASCII + Encoding enc = Encoding.ASCII; + bool hasenc; + if (value.Length < 8) { - // Default to ASCII - Encoding enc = Encoding.ASCII; - bool hasenc; - if (value.Length < 8) - hasenc = false; + hasenc = false; + } + else + { + hasenc = true; + var encstr = enc.GetString(value, 0, 8); + if (string.Compare(encstr, "ASCII\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) + { + enc = Encoding.ASCII; + } + else if (string.Compare(encstr, "JIS\0\0\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) + { + enc = Encoding.GetEncoding("Japanese (JIS 0208-1990 and 0212-1990)"); + } + else if (string.Compare(encstr, "Unicode\0", StringComparison.OrdinalIgnoreCase) == 0) + { + enc = Encoding.Unicode; + } else { - hasenc = true; - string encstr = enc.GetString(value, 0, 8); - if (string.Compare(encstr, "ASCII\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) - enc = Encoding.ASCII; - else if (string.Compare(encstr, "JIS\0\0\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) - enc = Encoding.GetEncoding("Japanese (JIS 0208-1990 and 0212-1990)"); - else if (string.Compare(encstr, "Unicode\0", StringComparison.OrdinalIgnoreCase) == 0) - enc = Encoding.Unicode; - else - hasenc = false; + hasenc = false; } - - string val = (hasenc ? enc.GetString(value, 8, value.Length - 8) : enc.GetString(value)).Trim(Constants.CharArrays.NullTerminator); - - return new ExifEncodedString(ExifTag.UserComment, val, enc); } - else if (tag == 0x9003) // DateTimeOriginal - return new ExifDateTime(ExifTag.DateTimeOriginal, ExifBitConverter.ToDateTime(value)); - else if (tag == 0x9004) // DateTimeDigitized - return new ExifDateTime(ExifTag.DateTimeDigitized, ExifBitConverter.ToDateTime(value)); - else if (tag == 0x8822) // ExposureProgram - return new ExifEnumProperty(ExifTag.ExposureProgram, (ExposureProgram)conv.ToUInt16(value, 0)); - else if (tag == 0x9207) // MeteringMode - return new ExifEnumProperty(ExifTag.MeteringMode, (MeteringMode)conv.ToUInt16(value, 0)); - else if (tag == 0x9208) // LightSource - return new ExifEnumProperty(ExifTag.LightSource, (LightSource)conv.ToUInt16(value, 0)); - else if (tag == 0x9209) // Flash - return new ExifEnumProperty(ExifTag.Flash, (Flash)conv.ToUInt16(value, 0), true); - else if (tag == 0x9214) // SubjectArea + + var val = (hasenc ? enc.GetString(value, 8, value.Length - 8) : enc.GetString(value)).Trim( + Constants.CharArrays.NullTerminator); + + return new ExifEncodedString(ExifTag.UserComment, val, enc); + } + + // DateTimeOriginal + if (tag == 0x9003) + { + return new ExifDateTime(ExifTag.DateTimeOriginal, ExifBitConverter.ToDateTime(value)); + } + + // DateTimeDigitized + if (tag == 0x9004) + { + return new ExifDateTime(ExifTag.DateTimeDigitized, ExifBitConverter.ToDateTime(value)); + } + + // ExposureProgram + if (tag == 0x8822) + { + return new ExifEnumProperty( + ExifTag.ExposureProgram, + (ExposureProgram)conv.ToUInt16(value, 0)); + } + + // MeteringMode + if (tag == 0x9207) + { + return new ExifEnumProperty(ExifTag.MeteringMode, (MeteringMode)conv.ToUInt16(value, 0)); + } + + // LightSource + if (tag == 0x9208) + { + return new ExifEnumProperty(ExifTag.LightSource, (LightSource)conv.ToUInt16(value, 0)); + } + + // Flash + if (tag == 0x9209) + { + return new ExifEnumProperty(ExifTag.Flash, (Flash)conv.ToUInt16(value, 0), true); + } + + // SubjectArea + if (tag == 0x9214) + { + if (count == 3) { - if (count == 3) - return new ExifCircularSubjectArea(ExifTag.SubjectArea, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); - else if (count == 4) - return new ExifRectangularSubjectArea(ExifTag.SubjectArea, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); - else // count == 2 - return new ExifPointSubjectArea(ExifTag.SubjectArea, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + return new ExifCircularSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - else if (tag == 0xa210) // FocalPlaneResolutionUnit - return new ExifEnumProperty(ExifTag.FocalPlaneResolutionUnit, (ResolutionUnit)conv.ToUInt16(value, 0), true); - else if (tag == 0xa214) // SubjectLocation - return new ExifPointSubjectArea(ExifTag.SubjectLocation, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); - else if (tag == 0xa217) // SensingMethod - return new ExifEnumProperty(ExifTag.SensingMethod, (SensingMethod)conv.ToUInt16(value, 0), true); - else if (tag == 0xa300) // FileSource - return new ExifEnumProperty(ExifTag.FileSource, (FileSource)conv.ToUInt16(value, 0), true); - else if (tag == 0xa301) // SceneType - return new ExifEnumProperty(ExifTag.SceneType, (SceneType)conv.ToUInt16(value, 0), true); - else if (tag == 0xa401) // CustomRendered - return new ExifEnumProperty(ExifTag.CustomRendered, (CustomRendered)conv.ToUInt16(value, 0), true); - else if (tag == 0xa402) // ExposureMode - return new ExifEnumProperty(ExifTag.ExposureMode, (ExposureMode)conv.ToUInt16(value, 0), true); - else if (tag == 0xa403) // WhiteBalance - return new ExifEnumProperty(ExifTag.WhiteBalance, (WhiteBalance)conv.ToUInt16(value, 0), true); - else if (tag == 0xa406) // SceneCaptureType - return new ExifEnumProperty(ExifTag.SceneCaptureType, (SceneCaptureType)conv.ToUInt16(value, 0), true); - else if (tag == 0xa407) // GainControl - return new ExifEnumProperty(ExifTag.GainControl, (GainControl)conv.ToUInt16(value, 0), true); - else if (tag == 0xa408) // Contrast - return new ExifEnumProperty(ExifTag.Contrast, (Contrast)conv.ToUInt16(value, 0), true); - else if (tag == 0xa409) // Saturation - return new ExifEnumProperty(ExifTag.Saturation, (Saturation)conv.ToUInt16(value, 0), true); - else if (tag == 0xa40a) // Sharpness - return new ExifEnumProperty(ExifTag.Sharpness, (Sharpness)conv.ToUInt16(value, 0), true); - else if (tag == 0xa40c) // SubjectDistanceRange - return new ExifEnumProperty(ExifTag.SubjectDistanceRange, (SubjectDistanceRange)conv.ToUInt16(value, 0), true); - } - else if (ifd == IFD.GPS) - { - if (tag == 0) // GPSVersionID - return new ExifVersion(ExifTag.GPSVersionID, ExifBitConverter.ToString(value)); - else if (tag == 1) // GPSLatitudeRef - return new ExifEnumProperty(ExifTag.GPSLatitudeRef, (GPSLatitudeRef)value[0]); - else if (tag == 2) // GPSLatitude - return new GPSLatitudeLongitude(ExifTag.GPSLatitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 3) // GPSLongitudeRef - return new ExifEnumProperty(ExifTag.GPSLongitudeRef, (GPSLongitudeRef)value[0]); - else if (tag == 4) // GPSLongitude - return new GPSLatitudeLongitude(ExifTag.GPSLongitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 5) // GPSAltitudeRef - return new ExifEnumProperty(ExifTag.GPSAltitudeRef, (GPSAltitudeRef)value[0]); - else if (tag == 7) // GPSTimeStamp - return new GPSTimeStamp(ExifTag.GPSTimeStamp, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 9) // GPSStatus - return new ExifEnumProperty(ExifTag.GPSStatus, (GPSStatus)value[0]); - else if (tag == 10) // GPSMeasureMode - return new ExifEnumProperty(ExifTag.GPSMeasureMode, (GPSMeasureMode)value[0]); - else if (tag == 12) // GPSSpeedRef - return new ExifEnumProperty(ExifTag.GPSSpeedRef, (GPSSpeedRef)value[0]); - else if (tag == 14) // GPSTrackRef - return new ExifEnumProperty(ExifTag.GPSTrackRef, (GPSDirectionRef)value[0]); - else if (tag == 16) // GPSImgDirectionRef - return new ExifEnumProperty(ExifTag.GPSImgDirectionRef, (GPSDirectionRef)value[0]); - else if (tag == 19) // GPSDestLatitudeRef - return new ExifEnumProperty(ExifTag.GPSDestLatitudeRef, (GPSLatitudeRef)value[0]); - else if (tag == 20) // GPSDestLatitude - return new GPSLatitudeLongitude(ExifTag.GPSDestLatitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 21) // GPSDestLongitudeRef - return new ExifEnumProperty(ExifTag.GPSDestLongitudeRef, (GPSLongitudeRef)value[0]); - else if (tag == 22) // GPSDestLongitude - return new GPSLatitudeLongitude(ExifTag.GPSDestLongitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 23) // GPSDestBearingRef - return new ExifEnumProperty(ExifTag.GPSDestBearingRef, (GPSDirectionRef)value[0]); - else if (tag == 25) // GPSDestDistanceRef - return new ExifEnumProperty(ExifTag.GPSDestDistanceRef, (GPSDistanceRef)value[0]); - else if (tag == 29) // GPSDate - return new ExifDateTime(ExifTag.GPSDateStamp, ExifBitConverter.ToDateTime(value, false)); - else if (tag == 30) // GPSDifferential - return new ExifEnumProperty(ExifTag.GPSDifferential, (GPSDifferential)conv.ToUInt16(value, 0)); - } - else if (ifd == IFD.Interop) - { - if (tag == 1) // InteroperabilityIndex - return new ExifAscii(ExifTag.InteroperabilityIndex, ExifBitConverter.ToAscii(value, Encoding.ASCII), Encoding.ASCII); - else if (tag == 2) // InteroperabilityVersion - return new ExifVersion(ExifTag.InteroperabilityVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); - } - else if (ifd == IFD.First) - { - if (tag == 0x103) // Compression - return new ExifEnumProperty(ExifTag.ThumbnailCompression, (Compression)conv.ToUInt16(value, 0)); - else if (tag == 0x106) // PhotometricInterpretation - return new ExifEnumProperty(ExifTag.ThumbnailPhotometricInterpretation, (PhotometricInterpretation)conv.ToUInt16(value, 0)); - else if (tag == 0x112) // Orientation - return new ExifEnumProperty(ExifTag.ThumbnailOrientation, (Orientation)conv.ToUInt16(value, 0)); - else if (tag == 0x11c) // PlanarConfiguration - return new ExifEnumProperty(ExifTag.ThumbnailPlanarConfiguration, (PlanarConfiguration)conv.ToUInt16(value, 0)); - else if (tag == 0x213) // YCbCrPositioning - return new ExifEnumProperty(ExifTag.ThumbnailYCbCrPositioning, (YCbCrPositioning)conv.ToUInt16(value, 0)); - else if (tag == 0x128) // ResolutionUnit - return new ExifEnumProperty(ExifTag.ThumbnailResolutionUnit, (ResolutionUnit)conv.ToUInt16(value, 0)); - else if (tag == 0x132) // DateTime - return new ExifDateTime(ExifTag.ThumbnailDateTime, ExifBitConverter.ToDateTime(value)); + + if (count == 4) + { + return new ExifRectangularSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + } + + return new ExifPointSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - if (type == 1) // 1 = BYTE An 8-bit unsigned integer. + // FocalPlaneResolutionUnit + if (tag == 0xa210) { - if (count == 1) - return new ExifByte(etag, value[0]); - else - return new ExifByteArray(etag, value); + return new ExifEnumProperty( + ExifTag.FocalPlaneResolutionUnit, + (ResolutionUnit)conv.ToUInt16(value, 0), + true); } - else if (type == 2) // 2 = ASCII An 8-bit byte containing one 7-bit ASCII code. + + // SubjectLocation + if (tag == 0xa214) { - return new ExifAscii(etag, ExifBitConverter.ToAscii(value, encoding), encoding); + return new ExifPointSubjectArea( + ExifTag.SubjectLocation, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - else if (type == 3) // 3 = SHORT A 16-bit (2-byte) unsigned integer. + + // SensingMethod + if (tag == 0xa217) { - if (count == 1) - return new ExifUShort(etag, conv.ToUInt16(value, 0)); - else - return new ExifUShortArray(etag, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + return new ExifEnumProperty( + ExifTag.SensingMethod, + (SensingMethod)conv.ToUInt16(value, 0), + true); } - else if (type == 4) // 4 = LONG A 32-bit (4-byte) unsigned integer. + + // FileSource + if (tag == 0xa300) { - if (count == 1) - return new ExifUInt(etag, conv.ToUInt32(value, 0)); - else - return new ExifUIntArray(etag, ExifBitConverter.ToUIntArray(value, (int)count, byteOrder)); + return new ExifEnumProperty(ExifTag.FileSource, (FileSource)conv.ToUInt16(value, 0), true); } - else if (type == 5) // 5 = RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator. + + // SceneType + if (tag == 0xa301) { - if (count == 1) - return new ExifURational(etag, ExifBitConverter.ToURational(value, byteOrder)); - else - return new ExifURationalArray(etag, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + return new ExifEnumProperty(ExifTag.SceneType, (SceneType)conv.ToUInt16(value, 0), true); } - else if (type == 7) // 7 = UNDEFINED An 8-bit byte that can take any value depending on the field definition. - return new ExifUndefined(etag, value); - else if (type == 9) // 9 = SLONG A 32-bit (4-byte) signed integer (2's complement notation). + + // CustomRendered + if (tag == 0xa401) { - if (count == 1) - return new ExifSInt(etag, conv.ToInt32(value, 0)); - else - return new ExifSIntArray(etag, ExifBitConverter.ToSIntArray(value, (int)count, byteOrder)); + return new ExifEnumProperty( + ExifTag.CustomRendered, + (CustomRendered)conv.ToUInt16(value, 0), + true); } - else if (type == 10) // 10 = SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + + // ExposureMode + if (tag == 0xa402) { - if (count == 1) - return new ExifSRational(etag, ExifBitConverter.ToSRational(value, byteOrder)); - else - return new ExifSRationalArray(etag, ExifBitConverter.ToSRationalArray(value, (int)count, byteOrder)); + return new ExifEnumProperty(ExifTag.ExposureMode, (ExposureMode)conv.ToUInt16(value, 0), true); + } + + // WhiteBalance + if (tag == 0xa403) + { + return new ExifEnumProperty(ExifTag.WhiteBalance, (WhiteBalance)conv.ToUInt16(value, 0), true); + } + + // SceneCaptureType + if (tag == 0xa406) + { + return new ExifEnumProperty( + ExifTag.SceneCaptureType, + (SceneCaptureType)conv.ToUInt16(value, 0), + true); + } + + // GainControl + if (tag == 0xa407) + { + return new ExifEnumProperty(ExifTag.GainControl, (GainControl)conv.ToUInt16(value, 0), true); + } + + // Contrast + if (tag == 0xa408) + { + return new ExifEnumProperty(ExifTag.Contrast, (Contrast)conv.ToUInt16(value, 0), true); + } + + // Saturation + if (tag == 0xa409) + { + return new ExifEnumProperty(ExifTag.Saturation, (Saturation)conv.ToUInt16(value, 0), true); + } + + // Sharpness + if (tag == 0xa40a) + { + return new ExifEnumProperty(ExifTag.Sharpness, (Sharpness)conv.ToUInt16(value, 0), true); + } + + // SubjectDistanceRange + if (tag == 0xa40c) + { + return new ExifEnumProperty( + ExifTag.SubjectDistanceRange, + (SubjectDistanceRange)conv.ToUInt16(value, 0), + true); } - else - throw new ArgumentException("Unknown property type."); } - #endregion + else if (ifd == IFD.GPS) + { + // GPSVersionID + if (tag == 0) + { + return new ExifVersion(ExifTag.GPSVersionID, ExifBitConverter.ToString(value)); + } + + // GPSLatitudeRef + if (tag == 1) + { + return new ExifEnumProperty(ExifTag.GPSLatitudeRef, (GPSLatitudeRef)value[0]); + } + + // GPSLatitude + if (tag == 2) + { + return new GPSLatitudeLongitude( + ExifTag.GPSLatitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSLongitudeRef + if (tag == 3) + { + return new ExifEnumProperty(ExifTag.GPSLongitudeRef, (GPSLongitudeRef)value[0]); + } + + // GPSLongitude + if (tag == 4) + { + return new GPSLatitudeLongitude( + ExifTag.GPSLongitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSAltitudeRef + if (tag == 5) + { + return new ExifEnumProperty(ExifTag.GPSAltitudeRef, (GPSAltitudeRef)value[0]); + } + + // GPSTimeStamp + if (tag == 7) + { + return new GPSTimeStamp( + ExifTag.GPSTimeStamp, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSStatus + if (tag == 9) + { + return new ExifEnumProperty(ExifTag.GPSStatus, (GPSStatus)value[0]); + } + + // GPSMeasureMode + if (tag == 10) + { + return new ExifEnumProperty(ExifTag.GPSMeasureMode, (GPSMeasureMode)value[0]); + } + + // GPSSpeedRef + if (tag == 12) + { + return new ExifEnumProperty(ExifTag.GPSSpeedRef, (GPSSpeedRef)value[0]); + } + + // GPSTrackRef + if (tag == 14) + { + return new ExifEnumProperty(ExifTag.GPSTrackRef, (GPSDirectionRef)value[0]); + } + + // GPSImgDirectionRef + if (tag == 16) + { + return new ExifEnumProperty(ExifTag.GPSImgDirectionRef, (GPSDirectionRef)value[0]); + } + + // GPSDestLatitudeRef + if (tag == 19) + { + return new ExifEnumProperty(ExifTag.GPSDestLatitudeRef, (GPSLatitudeRef)value[0]); + } + + // GPSDestLatitude + if (tag == 20) + { + return new GPSLatitudeLongitude( + ExifTag.GPSDestLatitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSDestLongitudeRef + if (tag == 21) + { + return new ExifEnumProperty(ExifTag.GPSDestLongitudeRef, (GPSLongitudeRef)value[0]); + } + + // GPSDestLongitude + if (tag == 22) + { + return new GPSLatitudeLongitude( + ExifTag.GPSDestLongitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSDestBearingRef + if (tag == 23) + { + return new ExifEnumProperty(ExifTag.GPSDestBearingRef, (GPSDirectionRef)value[0]); + } + + // GPSDestDistanceRef + if (tag == 25) + { + return new ExifEnumProperty(ExifTag.GPSDestDistanceRef, (GPSDistanceRef)value[0]); + } + + // GPSDate + if (tag == 29) + { + return new ExifDateTime(ExifTag.GPSDateStamp, ExifBitConverter.ToDateTime(value, false)); + } + + // GPSDifferential + if (tag == 30) + { + return new ExifEnumProperty( + ExifTag.GPSDifferential, + (GPSDifferential)conv.ToUInt16(value, 0)); + } + } + else if (ifd == IFD.Interop) + { + // InteroperabilityIndex + if (tag == 1) + { + return new ExifAscii(ExifTag.InteroperabilityIndex, ExifBitConverter.ToAscii(value, Encoding.ASCII), Encoding.ASCII); + } + + // InteroperabilityVersion + if (tag == 2) + { + return new ExifVersion( + ExifTag.InteroperabilityVersion, + ExifBitConverter.ToAscii(value, Encoding.ASCII)); + } + } + else if (ifd == IFD.First) + { + // Compression + if (tag == 0x103) + { + return new ExifEnumProperty( + ExifTag.ThumbnailCompression, + (Compression)conv.ToUInt16(value, 0)); + } + + // PhotometricInterpretation + if (tag == 0x106) + { + return new ExifEnumProperty( + ExifTag.ThumbnailPhotometricInterpretation, + (PhotometricInterpretation)conv.ToUInt16(value, 0)); + } + + // Orientation + if (tag == 0x112) + { + return new ExifEnumProperty( + ExifTag.ThumbnailOrientation, + (Orientation)conv.ToUInt16(value, 0)); + } + + // PlanarConfiguration + if (tag == 0x11c) + { + return new ExifEnumProperty( + ExifTag.ThumbnailPlanarConfiguration, + (PlanarConfiguration)conv.ToUInt16(value, 0)); + } + + // YCbCrPositioning + if (tag == 0x213) + { + return new ExifEnumProperty( + ExifTag.ThumbnailYCbCrPositioning, + (YCbCrPositioning)conv.ToUInt16(value, 0)); + } + + // ResolutionUnit + if (tag == 0x128) + { + return new ExifEnumProperty( + ExifTag.ThumbnailResolutionUnit, + (ResolutionUnit)conv.ToUInt16(value, 0)); + } + + // DateTime + if (tag == 0x132) + { + return new ExifDateTime(ExifTag.ThumbnailDateTime, ExifBitConverter.ToDateTime(value)); + } + } + + // 1 = BYTE An 8-bit unsigned integer. + if (type == 1) + { + if (count == 1) + { + return new ExifByte(etag, value[0]); + } + + return new ExifByteArray(etag, value); + } + + // 2 = ASCII An 8-bit byte containing one 7-bit ASCII code. + if (type == 2) + { + return new ExifAscii(etag, ExifBitConverter.ToAscii(value, encoding), encoding); + } + + // 3 = SHORT A 16-bit (2-byte) unsigned integer. + if (type == 3) + { + if (count == 1) + { + return new ExifUShort(etag, conv.ToUInt16(value, 0)); + } + + return new ExifUShortArray(etag, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + } + + // 4 = LONG A 32-bit (4-byte) unsigned integer. + if (type == 4) + { + if (count == 1) + { + return new ExifUInt(etag, conv.ToUInt32(value, 0)); + } + + return new ExifUIntArray(etag, ExifBitConverter.ToUIntArray(value, (int)count, byteOrder)); + } + + // 5 = RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator. + if (type == 5) + { + if (count == 1) + { + return new ExifURational(etag, ExifBitConverter.ToURational(value, byteOrder)); + } + + return new ExifURationalArray(etag, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // 7 = UNDEFINED An 8-bit byte that can take any value depending on the field definition. + if (type == 7) + { + return new ExifUndefined(etag, value); + } + + // 9 = SLONG A 32-bit (4-byte) signed integer (2's complement notation). + if (type == 9) + { + if (count == 1) + { + return new ExifSInt(etag, conv.ToInt32(value, 0)); + } + + return new ExifSIntArray(etag, ExifBitConverter.ToSIntArray(value, (int)count, byteOrder)); + } + + // 10 = SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + if (type == 10) + { + if (count == 1) + { + return new ExifSRational(etag, ExifBitConverter.ToSRational(value, byteOrder)); + } + + return new ExifSRationalArray(etag, ExifBitConverter.ToSRationalArray(value, (int)count, byteOrder)); + } + + throw new ArgumentException("Unknown property type."); } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifTag.cs b/src/Umbraco.Core/Media/Exif/ExifTag.cs index 22215044b2..0ffd754836 100644 --- a/src/Umbraco.Core/Media/Exif/ExifTag.cs +++ b/src/Umbraco.Core/Media/Exif/ExifTag.cs @@ -1,290 +1,310 @@ - -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the tags associated with exif fields. +/// +internal enum ExifTag { + // **************************** + // Zeroth IFD + // **************************** + NewSubfileType = IFD.Zeroth + 254, + SubfileType = IFD.Zeroth + 255, + ImageWidth = IFD.Zeroth + 256, + ImageLength = IFD.Zeroth + 257, + BitsPerSample = IFD.Zeroth + 258, + Compression = IFD.Zeroth + 259, + PhotometricInterpretation = IFD.Zeroth + 262, + Threshholding = IFD.Zeroth + 263, + CellWidth = IFD.Zeroth + 264, + CellLength = IFD.Zeroth + 265, + FillOrder = IFD.Zeroth + 266, + DocumentName = IFD.Zeroth + 269, + ImageDescription = IFD.Zeroth + 270, + Make = IFD.Zeroth + 271, + Model = IFD.Zeroth + 272, + StripOffsets = IFD.Zeroth + 273, + Orientation = IFD.Zeroth + 274, + SamplesPerPixel = IFD.Zeroth + 277, + RowsPerStrip = IFD.Zeroth + 278, + StripByteCounts = IFD.Zeroth + 279, + MinSampleValue = IFD.Zeroth + 280, + MaxSampleValue = IFD.Zeroth + 281, + XResolution = IFD.Zeroth + 282, + YResolution = IFD.Zeroth + 283, + PlanarConfiguration = IFD.Zeroth + 284, + PageName = IFD.Zeroth + 285, + XPosition = IFD.Zeroth + 286, + YPosition = IFD.Zeroth + 287, + FreeOffsets = IFD.Zeroth + 288, + FreeByteCounts = IFD.Zeroth + 289, + GrayResponseUnit = IFD.Zeroth + 290, + GrayResponseCurve = IFD.Zeroth + 291, + T4Options = IFD.Zeroth + 292, + T6Options = IFD.Zeroth + 293, + ResolutionUnit = IFD.Zeroth + 296, + PageNumber = IFD.Zeroth + 297, + TransferFunction = IFD.Zeroth + 301, + Software = IFD.Zeroth + 305, + DateTime = IFD.Zeroth + 306, + Artist = IFD.Zeroth + 315, + HostComputer = IFD.Zeroth + 316, + Predictor = IFD.Zeroth + 317, + WhitePoint = IFD.Zeroth + 318, + PrimaryChromaticities = IFD.Zeroth + 319, + ColorMap = IFD.Zeroth + 320, + HalftoneHints = IFD.Zeroth + 321, + TileWidth = IFD.Zeroth + 322, + TileLength = IFD.Zeroth + 323, + TileOffsets = IFD.Zeroth + 324, + TileByteCounts = IFD.Zeroth + 325, + InkSet = IFD.Zeroth + 332, + InkNames = IFD.Zeroth + 333, + NumberOfInks = IFD.Zeroth + 334, + DotRange = IFD.Zeroth + 336, + TargetPrinter = IFD.Zeroth + 337, + ExtraSamples = IFD.Zeroth + 338, + SampleFormat = IFD.Zeroth + 339, + SMinSampleValue = IFD.Zeroth + 340, + SMaxSampleValue = IFD.Zeroth + 341, + TransferRange = IFD.Zeroth + 342, + JPEGProc = IFD.Zeroth + 512, + JPEGInterchangeFormat = IFD.Zeroth + 513, + JPEGInterchangeFormatLength = IFD.Zeroth + 514, + JPEGRestartInterval = IFD.Zeroth + 515, + JPEGLosslessPredictors = IFD.Zeroth + 517, + JPEGPointTransforms = IFD.Zeroth + 518, + JPEGQTables = IFD.Zeroth + 519, + JPEGDCTables = IFD.Zeroth + 520, + JPEGACTables = IFD.Zeroth + 521, + YCbCrCoefficients = IFD.Zeroth + 529, + YCbCrSubSampling = IFD.Zeroth + 530, + YCbCrPositioning = IFD.Zeroth + 531, + ReferenceBlackWhite = IFD.Zeroth + 532, + Copyright = IFD.Zeroth + 33432, + + // Pointers to other IFDs + EXIFIFDPointer = IFD.Zeroth + 34665, + GPSIFDPointer = IFD.Zeroth + 34853, + + // Windows Tags + WindowsTitle = IFD.Zeroth + 0x9c9b, + WindowsComment = IFD.Zeroth + 0x9c9c, + WindowsAuthor = IFD.Zeroth + 0x9c9d, + WindowsKeywords = IFD.Zeroth + 0x9c9e, + WindowsSubject = IFD.Zeroth + 0x9c9f, + + // Rating + Rating = IFD.Zeroth + 0x4746, + RatingPercent = IFD.Zeroth + 0x4749, + + // Microsoft specifying padding and offset tags + ZerothIFDPadding = IFD.Zeroth + 0xea1c, + + // **************************** + // EXIF Tags + // **************************** + ExifVersion = IFD.EXIF + 36864, + FlashpixVersion = IFD.EXIF + 40960, + ColorSpace = IFD.EXIF + 40961, + ComponentsConfiguration = IFD.EXIF + 37121, + CompressedBitsPerPixel = IFD.EXIF + 37122, + PixelXDimension = IFD.EXIF + 40962, + PixelYDimension = IFD.EXIF + 40963, + MakerNote = IFD.EXIF + 37500, + UserComment = IFD.EXIF + 37510, + RelatedSoundFile = IFD.EXIF + 40964, + DateTimeOriginal = IFD.EXIF + 36867, + DateTimeDigitized = IFD.EXIF + 36868, + SubSecTime = IFD.EXIF + 37520, + SubSecTimeOriginal = IFD.EXIF + 37521, + SubSecTimeDigitized = IFD.EXIF + 37522, + ExposureTime = IFD.EXIF + 33434, + FNumber = IFD.EXIF + 33437, + ExposureProgram = IFD.EXIF + 34850, + SpectralSensitivity = IFD.EXIF + 34852, + ISOSpeedRatings = IFD.EXIF + 34855, + OECF = IFD.EXIF + 34856, + ShutterSpeedValue = IFD.EXIF + 37377, + ApertureValue = IFD.EXIF + 37378, + BrightnessValue = IFD.EXIF + 37379, + ExposureBiasValue = IFD.EXIF + 37380, + MaxApertureValue = IFD.EXIF + 37381, + SubjectDistance = IFD.EXIF + 37382, + MeteringMode = IFD.EXIF + 37383, + LightSource = IFD.EXIF + 37384, + Flash = IFD.EXIF + 37385, + FocalLength = IFD.EXIF + 37386, + SubjectArea = IFD.EXIF + 37396, + FlashEnergy = IFD.EXIF + 41483, + SpatialFrequencyResponse = IFD.EXIF + 41484, + FocalPlaneXResolution = IFD.EXIF + 41486, + FocalPlaneYResolution = IFD.EXIF + 41487, + FocalPlaneResolutionUnit = IFD.EXIF + 41488, + SubjectLocation = IFD.EXIF + 41492, + ExposureIndex = IFD.EXIF + 41493, + SensingMethod = IFD.EXIF + 41495, + FileSource = IFD.EXIF + 41728, + SceneType = IFD.EXIF + 41729, + CFAPattern = IFD.EXIF + 41730, + CustomRendered = IFD.EXIF + 41985, + ExposureMode = IFD.EXIF + 41986, + WhiteBalance = IFD.EXIF + 41987, + DigitalZoomRatio = IFD.EXIF + 41988, + FocalLengthIn35mmFilm = IFD.EXIF + 41989, + SceneCaptureType = IFD.EXIF + 41990, + GainControl = IFD.EXIF + 41991, + Contrast = IFD.EXIF + 41992, + Saturation = IFD.EXIF + 41993, + Sharpness = IFD.EXIF + 41994, + DeviceSettingDescription = IFD.EXIF + 41995, + SubjectDistanceRange = IFD.EXIF + 41996, + ImageUniqueID = IFD.EXIF + 42016, + InteroperabilityIFDPointer = IFD.EXIF + 40965, + + // Microsoft specifying padding and offset tags + ExifIFDPadding = IFD.EXIF + 0xea1c, + OffsetSchema = IFD.EXIF + 0xea1d, + + // **************************** + // GPS Tags + // **************************** + GPSVersionID = IFD.GPS + 0, + GPSLatitudeRef = IFD.GPS + 1, + GPSLatitude = IFD.GPS + 2, + GPSLongitudeRef = IFD.GPS + 3, + GPSLongitude = IFD.GPS + 4, + GPSAltitudeRef = IFD.GPS + 5, + GPSAltitude = IFD.GPS + 6, + GPSTimeStamp = IFD.GPS + 7, + GPSSatellites = IFD.GPS + 8, + GPSStatus = IFD.GPS + 9, + GPSMeasureMode = IFD.GPS + 10, + GPSDOP = IFD.GPS + 11, + GPSSpeedRef = IFD.GPS + 12, + GPSSpeed = IFD.GPS + 13, + GPSTrackRef = IFD.GPS + 14, + GPSTrack = IFD.GPS + 15, + GPSImgDirectionRef = IFD.GPS + 16, + GPSImgDirection = IFD.GPS + 17, + GPSMapDatum = IFD.GPS + 18, + GPSDestLatitudeRef = IFD.GPS + 19, + GPSDestLatitude = IFD.GPS + 20, + GPSDestLongitudeRef = IFD.GPS + 21, + GPSDestLongitude = IFD.GPS + 22, + GPSDestBearingRef = IFD.GPS + 23, + GPSDestBearing = IFD.GPS + 24, + GPSDestDistanceRef = IFD.GPS + 25, + GPSDestDistance = IFD.GPS + 26, + GPSProcessingMethod = IFD.GPS + 27, + GPSAreaInformation = IFD.GPS + 28, + GPSDateStamp = IFD.GPS + 29, + GPSDifferential = IFD.GPS + 30, + + // **************************** + // InterOp Tags + // **************************** + InteroperabilityIndex = IFD.Interop + 1, + InteroperabilityVersion = IFD.Interop + 2, + RelatedImageWidth = IFD.Interop + 0x1001, + RelatedImageHeight = IFD.Interop + 0x1002, + + // **************************** + // First IFD TIFF Tags + // **************************** + ThumbnailImageWidth = IFD.First + 256, + ThumbnailImageLength = IFD.First + 257, + ThumbnailBitsPerSample = IFD.First + 258, + ThumbnailCompression = IFD.First + 259, + ThumbnailPhotometricInterpretation = IFD.First + 262, + ThumbnailOrientation = IFD.First + 274, + ThumbnailSamplesPerPixel = IFD.First + 277, + ThumbnailPlanarConfiguration = IFD.First + 284, + ThumbnailYCbCrSubSampling = IFD.First + 530, + ThumbnailYCbCrPositioning = IFD.First + 531, + ThumbnailXResolution = IFD.First + 282, + ThumbnailYResolution = IFD.First + 283, + ThumbnailResolutionUnit = IFD.First + 296, + ThumbnailStripOffsets = IFD.First + 273, + ThumbnailRowsPerStrip = IFD.First + 278, + ThumbnailStripByteCounts = IFD.First + 279, + ThumbnailJPEGInterchangeFormat = IFD.First + 513, + ThumbnailJPEGInterchangeFormatLength = IFD.First + 514, + ThumbnailTransferFunction = IFD.First + 301, + ThumbnailWhitePoint = IFD.First + 318, + ThumbnailPrimaryChromaticities = IFD.First + 319, + ThumbnailYCbCrCoefficients = IFD.First + 529, + ThumbnailReferenceBlackWhite = IFD.First + 532, + ThumbnailDateTime = IFD.First + 306, + ThumbnailImageDescription = IFD.First + 270, + ThumbnailMake = IFD.First + 271, + ThumbnailModel = IFD.First + 272, + ThumbnailSoftware = IFD.First + 305, + ThumbnailArtist = IFD.First + 315, + ThumbnailCopyright = IFD.First + 33432, + + // **************************** + // JFIF Tags + // **************************** + /// - /// Represents the tags associated with exif fields. + /// Represents the JFIF version. /// - internal enum ExifTag : int - { - // **************************** - // Zeroth IFD - // **************************** - NewSubfileType = IFD.Zeroth + 254, - SubfileType = IFD.Zeroth + 255, - ImageWidth = IFD.Zeroth + 256, - ImageLength = IFD.Zeroth + 257, - BitsPerSample = IFD.Zeroth + 258, - Compression = IFD.Zeroth + 259, - PhotometricInterpretation = IFD.Zeroth + 262, - Threshholding = IFD.Zeroth + 263, - CellWidth = IFD.Zeroth + 264, - CellLength = IFD.Zeroth + 265, - FillOrder = IFD.Zeroth + 266, - DocumentName = IFD.Zeroth + 269, - ImageDescription = IFD.Zeroth + 270, - Make = IFD.Zeroth + 271, - Model = IFD.Zeroth + 272, - StripOffsets = IFD.Zeroth + 273, - Orientation = IFD.Zeroth + 274, - SamplesPerPixel = IFD.Zeroth + 277, - RowsPerStrip = IFD.Zeroth + 278, - StripByteCounts = IFD.Zeroth + 279, - MinSampleValue = IFD.Zeroth + 280, - MaxSampleValue = IFD.Zeroth + 281, - XResolution = IFD.Zeroth + 282, - YResolution = IFD.Zeroth + 283, - PlanarConfiguration = IFD.Zeroth + 284, - PageName = IFD.Zeroth + 285, - XPosition = IFD.Zeroth + 286, - YPosition = IFD.Zeroth + 287, - FreeOffsets = IFD.Zeroth + 288, - FreeByteCounts = IFD.Zeroth + 289, - GrayResponseUnit = IFD.Zeroth + 290, - GrayResponseCurve = IFD.Zeroth + 291, - T4Options = IFD.Zeroth + 292, - T6Options = IFD.Zeroth + 293, - ResolutionUnit = IFD.Zeroth + 296, - PageNumber = IFD.Zeroth + 297, - TransferFunction = IFD.Zeroth + 301, - Software = IFD.Zeroth + 305, - DateTime = IFD.Zeroth + 306, - Artist = IFD.Zeroth + 315, - HostComputer = IFD.Zeroth + 316, - Predictor = IFD.Zeroth + 317, - WhitePoint = IFD.Zeroth + 318, - PrimaryChromaticities = IFD.Zeroth + 319, - ColorMap = IFD.Zeroth + 320, - HalftoneHints = IFD.Zeroth + 321, - TileWidth = IFD.Zeroth + 322, - TileLength = IFD.Zeroth + 323, - TileOffsets = IFD.Zeroth + 324, - TileByteCounts = IFD.Zeroth + 325, - InkSet = IFD.Zeroth + 332, - InkNames = IFD.Zeroth + 333, - NumberOfInks = IFD.Zeroth + 334, - DotRange = IFD.Zeroth + 336, - TargetPrinter = IFD.Zeroth + 337, - ExtraSamples = IFD.Zeroth + 338, - SampleFormat = IFD.Zeroth + 339, - SMinSampleValue = IFD.Zeroth + 340, - SMaxSampleValue = IFD.Zeroth + 341, - TransferRange = IFD.Zeroth + 342, - JPEGProc = IFD.Zeroth + 512, - JPEGInterchangeFormat = IFD.Zeroth + 513, - JPEGInterchangeFormatLength = IFD.Zeroth + 514, - JPEGRestartInterval = IFD.Zeroth + 515, - JPEGLosslessPredictors = IFD.Zeroth + 517, - JPEGPointTransforms = IFD.Zeroth + 518, - JPEGQTables = IFD.Zeroth + 519, - JPEGDCTables = IFD.Zeroth + 520, - JPEGACTables = IFD.Zeroth + 521, - YCbCrCoefficients = IFD.Zeroth + 529, - YCbCrSubSampling = IFD.Zeroth + 530, - YCbCrPositioning = IFD.Zeroth + 531, - ReferenceBlackWhite = IFD.Zeroth + 532, - Copyright = IFD.Zeroth + 33432, - // Pointers to other IFDs - EXIFIFDPointer = IFD.Zeroth + 34665, - GPSIFDPointer = IFD.Zeroth + 34853, - // Windows Tags - WindowsTitle = IFD.Zeroth + 0x9c9b, - WindowsComment = IFD.Zeroth + 0x9c9c, - WindowsAuthor = IFD.Zeroth + 0x9c9d, - WindowsKeywords = IFD.Zeroth + 0x9c9e, - WindowsSubject = IFD.Zeroth + 0x9c9f, - // Rating - Rating = IFD.Zeroth + 0x4746, - RatingPercent = IFD.Zeroth + 0x4749, - // Microsoft specifying padding and offset tags - ZerothIFDPadding = IFD.Zeroth + 0xea1c, - // **************************** - // EXIF Tags - // **************************** - ExifVersion = IFD.EXIF + 36864, - FlashpixVersion = IFD.EXIF + 40960, - ColorSpace = IFD.EXIF + 40961, - ComponentsConfiguration = IFD.EXIF + 37121, - CompressedBitsPerPixel = IFD.EXIF + 37122, - PixelXDimension = IFD.EXIF + 40962, - PixelYDimension = IFD.EXIF + 40963, - MakerNote = IFD.EXIF + 37500, - UserComment = IFD.EXIF + 37510, - RelatedSoundFile = IFD.EXIF + 40964, - DateTimeOriginal = IFD.EXIF + 36867, - DateTimeDigitized = IFD.EXIF + 36868, - SubSecTime = IFD.EXIF + 37520, - SubSecTimeOriginal = IFD.EXIF + 37521, - SubSecTimeDigitized = IFD.EXIF + 37522, - ExposureTime = IFD.EXIF + 33434, - FNumber = IFD.EXIF + 33437, - ExposureProgram = IFD.EXIF + 34850, - SpectralSensitivity = IFD.EXIF + 34852, - ISOSpeedRatings = IFD.EXIF + 34855, - OECF = IFD.EXIF + 34856, - ShutterSpeedValue = IFD.EXIF + 37377, - ApertureValue = IFD.EXIF + 37378, - BrightnessValue = IFD.EXIF + 37379, - ExposureBiasValue = IFD.EXIF + 37380, - MaxApertureValue = IFD.EXIF + 37381, - SubjectDistance = IFD.EXIF + 37382, - MeteringMode = IFD.EXIF + 37383, - LightSource = IFD.EXIF + 37384, - Flash = IFD.EXIF + 37385, - FocalLength = IFD.EXIF + 37386, - SubjectArea = IFD.EXIF + 37396, - FlashEnergy = IFD.EXIF + 41483, - SpatialFrequencyResponse = IFD.EXIF + 41484, - FocalPlaneXResolution = IFD.EXIF + 41486, - FocalPlaneYResolution = IFD.EXIF + 41487, - FocalPlaneResolutionUnit = IFD.EXIF + 41488, - SubjectLocation = IFD.EXIF + 41492, - ExposureIndex = IFD.EXIF + 41493, - SensingMethod = IFD.EXIF + 41495, - FileSource = IFD.EXIF + 41728, - SceneType = IFD.EXIF + 41729, - CFAPattern = IFD.EXIF + 41730, - CustomRendered = IFD.EXIF + 41985, - ExposureMode = IFD.EXIF + 41986, - WhiteBalance = IFD.EXIF + 41987, - DigitalZoomRatio = IFD.EXIF + 41988, - FocalLengthIn35mmFilm = IFD.EXIF + 41989, - SceneCaptureType = IFD.EXIF + 41990, - GainControl = IFD.EXIF + 41991, - Contrast = IFD.EXIF + 41992, - Saturation = IFD.EXIF + 41993, - Sharpness = IFD.EXIF + 41994, - DeviceSettingDescription = IFD.EXIF + 41995, - SubjectDistanceRange = IFD.EXIF + 41996, - ImageUniqueID = IFD.EXIF + 42016, - InteroperabilityIFDPointer = IFD.EXIF + 40965, - // Microsoft specifying padding and offset tags - ExifIFDPadding = IFD.EXIF + 0xea1c, - OffsetSchema = IFD.EXIF + 0xea1d, - // **************************** - // GPS Tags - // **************************** - GPSVersionID = IFD.GPS + 0, - GPSLatitudeRef = IFD.GPS + 1, - GPSLatitude = IFD.GPS + 2, - GPSLongitudeRef = IFD.GPS + 3, - GPSLongitude = IFD.GPS + 4, - GPSAltitudeRef = IFD.GPS + 5, - GPSAltitude = IFD.GPS + 6, - GPSTimeStamp = IFD.GPS + 7, - GPSSatellites = IFD.GPS + 8, - GPSStatus = IFD.GPS + 9, - GPSMeasureMode = IFD.GPS + 10, - GPSDOP = IFD.GPS + 11, - GPSSpeedRef = IFD.GPS + 12, - GPSSpeed = IFD.GPS + 13, - GPSTrackRef = IFD.GPS + 14, - GPSTrack = IFD.GPS + 15, - GPSImgDirectionRef = IFD.GPS + 16, - GPSImgDirection = IFD.GPS + 17, - GPSMapDatum = IFD.GPS + 18, - GPSDestLatitudeRef = IFD.GPS + 19, - GPSDestLatitude = IFD.GPS + 20, - GPSDestLongitudeRef = IFD.GPS + 21, - GPSDestLongitude = IFD.GPS + 22, - GPSDestBearingRef = IFD.GPS + 23, - GPSDestBearing = IFD.GPS + 24, - GPSDestDistanceRef = IFD.GPS + 25, - GPSDestDistance = IFD.GPS + 26, - GPSProcessingMethod = IFD.GPS + 27, - GPSAreaInformation = IFD.GPS + 28, - GPSDateStamp = IFD.GPS + 29, - GPSDifferential = IFD.GPS + 30, - // **************************** - // InterOp Tags - // **************************** - InteroperabilityIndex = IFD.Interop + 1, - InteroperabilityVersion = IFD.Interop + 2, - RelatedImageWidth = IFD.Interop + 0x1001, - RelatedImageHeight = IFD.Interop + 0x1002, - // **************************** - // First IFD TIFF Tags - // **************************** - ThumbnailImageWidth = IFD.First + 256, - ThumbnailImageLength = IFD.First + 257, - ThumbnailBitsPerSample = IFD.First + 258, - ThumbnailCompression = IFD.First + 259, - ThumbnailPhotometricInterpretation = IFD.First + 262, - ThumbnailOrientation = IFD.First + 274, - ThumbnailSamplesPerPixel = IFD.First + 277, - ThumbnailPlanarConfiguration = IFD.First + 284, - ThumbnailYCbCrSubSampling = IFD.First + 530, - ThumbnailYCbCrPositioning = IFD.First + 531, - ThumbnailXResolution = IFD.First + 282, - ThumbnailYResolution = IFD.First + 283, - ThumbnailResolutionUnit = IFD.First + 296, - ThumbnailStripOffsets = IFD.First + 273, - ThumbnailRowsPerStrip = IFD.First + 278, - ThumbnailStripByteCounts = IFD.First + 279, - ThumbnailJPEGInterchangeFormat = IFD.First + 513, - ThumbnailJPEGInterchangeFormatLength = IFD.First + 514, - ThumbnailTransferFunction = IFD.First + 301, - ThumbnailWhitePoint = IFD.First + 318, - ThumbnailPrimaryChromaticities = IFD.First + 319, - ThumbnailYCbCrCoefficients = IFD.First + 529, - ThumbnailReferenceBlackWhite = IFD.First + 532, - ThumbnailDateTime = IFD.First + 306, - ThumbnailImageDescription = IFD.First + 270, - ThumbnailMake = IFD.First + 271, - ThumbnailModel = IFD.First + 272, - ThumbnailSoftware = IFD.First + 305, - ThumbnailArtist = IFD.First + 315, - ThumbnailCopyright = IFD.First + 33432, - // **************************** - // JFIF Tags - // **************************** - /// - /// Represents the JFIF version. - /// - JFIFVersion = IFD.JFIF + 1, - /// - /// Represents units for X and Y densities. - /// - JFIFUnits = IFD.JFIF + 101, - /// - /// Horizontal pixel density. - /// - XDensity = IFD.JFIF + 102, - /// - /// Vertical pixel density - /// - YDensity = IFD.JFIF + 103, - /// - /// Thumbnail horizontal pixel count. - /// - JFIFXThumbnail = IFD.JFIF + 201, - /// - /// Thumbnail vertical pixel count. - /// - JFIFYThumbnail = IFD.JFIF + 202, - /// - /// JFIF JPEG thumbnail. - /// - JFIFThumbnail = IFD.JFIF + 203, - /// - /// Code which identifies the JFIF extension. - /// - JFXXExtensionCode = IFD.JFXX + 1, - /// - /// Thumbnail horizontal pixel count. - /// - JFXXXThumbnail = IFD.JFXX + 101, - /// - /// Thumbnail vertical pixel count. - /// - JFXXYThumbnail = IFD.JFXX + 102, - /// - /// The 256-Color RGB palette. - /// - JFXXPalette = IFD.JFXX + 201, - /// - /// JFIF thumbnail. The thumbnail will be either a JPEG, - /// a 256 color palette bitmap, or a 24-bit RGB bitmap. - /// - JFXXThumbnail = IFD.JFXX + 202, - } + JFIFVersion = IFD.JFIF + 1, + + /// + /// Represents units for X and Y densities. + /// + JFIFUnits = IFD.JFIF + 101, + + /// + /// Horizontal pixel density. + /// + XDensity = IFD.JFIF + 102, + + /// + /// Vertical pixel density + /// + YDensity = IFD.JFIF + 103, + + /// + /// Thumbnail horizontal pixel count. + /// + JFIFXThumbnail = IFD.JFIF + 201, + + /// + /// Thumbnail vertical pixel count. + /// + JFIFYThumbnail = IFD.JFIF + 202, + + /// + /// JFIF JPEG thumbnail. + /// + JFIFThumbnail = IFD.JFIF + 203, + + /// + /// Code which identifies the JFIF extension. + /// + JFXXExtensionCode = IFD.JFXX + 1, + + /// + /// Thumbnail horizontal pixel count. + /// + JFXXXThumbnail = IFD.JFXX + 101, + + /// + /// Thumbnail vertical pixel count. + /// + JFXXYThumbnail = IFD.JFXX + 102, + + /// + /// The 256-Color RGB palette. + /// + JFXXPalette = IFD.JFXX + 201, + + /// + /// JFIF thumbnail. The thumbnail will be either a JPEG, + /// a 256 color palette bitmap, or a 24-bit RGB bitmap. + /// + JFXXThumbnail = IFD.JFXX + 202, } diff --git a/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs b/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs index 6a5ea84944..726da925aa 100644 --- a/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs +++ b/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs @@ -1,68 +1,63 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +internal static class ExifTagFactory { - internal static class ExifTagFactory + #region Static Methods + + /// + /// Returns the ExifTag corresponding to the given tag id. + /// + public static ExifTag GetExifTag(IFD ifd, ushort tagid) => (ExifTag)(ifd + tagid); + + /// + /// Returns the tag id corresponding to the given ExifTag. + /// + public static ushort GetTagID(ExifTag exiftag) { - #region Static Methods - /// - /// Returns the ExifTag corresponding to the given tag id. - /// - public static ExifTag GetExifTag(IFD ifd, ushort tagid) - { - return (ExifTag)(ifd + tagid); - } - - /// - /// Returns the tag id corresponding to the given ExifTag. - /// - public static ushort GetTagID(ExifTag exiftag) - { - IFD ifd = GetTagIFD(exiftag); - return (ushort)((int)exiftag - (int)ifd); - } - - /// - /// Returns the IFD section containing the given tag. - /// - public static IFD GetTagIFD(ExifTag tag) - { - return (IFD)(((int)tag / 100000) * 100000); - } - - /// - /// Returns the string representation for the given exif tag. - /// - public static string GetTagName(ExifTag tag) - { - string? name = Enum.GetName(typeof(ExifTag), tag); - if (name == null) - return "Unknown"; - else - return name; - } - - /// - /// Returns the string representation for the given tag id. - /// - public static string GetTagName(IFD ifd, ushort tagid) - { - return GetTagName(GetExifTag(ifd, tagid)); - } - - /// - /// Returns the string representation for the given exif tag including - /// IFD section and tag id. - /// - public static string GetTagLongName(ExifTag tag) - { - string? ifdname = Enum.GetName(typeof(IFD), GetTagIFD(tag)); - string? name = Enum.GetName(typeof(ExifTag), tag); - if (name == null) - name = "Unknown"; - string tagidname = GetTagID(tag).ToString(); - return ifdname + ": " + name + " (" + tagidname + ")"; - } - #endregion + IFD ifd = GetTagIFD(exiftag); + return (ushort)((int)exiftag - (int)ifd); } + + /// + /// Returns the IFD section containing the given tag. + /// + public static IFD GetTagIFD(ExifTag tag) => (IFD)((int)tag / 100000 * 100000); + + /// + /// Returns the string representation for the given exif tag. + /// + public static string GetTagName(ExifTag tag) + { + var name = Enum.GetName(typeof(ExifTag), tag); + if (name == null) + { + return "Unknown"; + } + + return name; + } + + /// + /// Returns the string representation for the given tag id. + /// + public static string GetTagName(IFD ifd, ushort tagid) => GetTagName(GetExifTag(ifd, tagid)); + + /// + /// Returns the string representation for the given exif tag including + /// IFD section and tag id. + /// + public static string GetTagLongName(ExifTag tag) + { + var ifdname = Enum.GetName(typeof(IFD), GetTagIFD(tag)); + var name = Enum.GetName(typeof(ExifTag), tag); + if (name == null) + { + name = "Unknown"; + } + + var tagidname = GetTagID(tag).ToString(); + return ifdname + ": " + name + " (" + tagidname + ")"; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/IFD.cs b/src/Umbraco.Core/Media/Exif/IFD.cs index e275e8d52a..cda3cdcb69 100644 --- a/src/Umbraco.Core/Media/Exif/IFD.cs +++ b/src/Umbraco.Core/Media/Exif/IFD.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the IFD section containing tags. +/// +internal enum IFD { - /// - /// Represents the IFD section containing tags. - /// - internal enum IFD : int - { - Unknown = 0, - Zeroth = 100000, - EXIF = 200000, - GPS = 300000, - Interop = 400000, - First = 500000, - MakerNote = 600000, - JFIF = 700000, - JFXX = 800000, - } + Unknown = 0, + Zeroth = 100000, + EXIF = 200000, + GPS = 300000, + Interop = 400000, + First = 500000, + MakerNote = 600000, + JFIF = 700000, + JFXX = 800000, } diff --git a/src/Umbraco.Core/Media/Exif/ImageFile.cs b/src/Umbraco.Core/Media/Exif/ImageFile.cs index cb783d3ee9..23ea615be9 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFile.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFile.cs @@ -1,139 +1,144 @@ using System.ComponentModel; -using System.IO; using System.Text; using Umbraco.Cms.Core.Media.TypeDetector; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the base class for image files. +/// +[TypeDescriptionProvider(typeof(ExifFileTypeDescriptionProvider))] +internal abstract class ImageFile { + #region Constructor + /// - /// Represents the base class for image files. + /// Initializes a new instance of the class. /// - [TypeDescriptionProvider(typeof(ExifFileTypeDescriptionProvider))] - internal abstract class ImageFile + protected ImageFile() { - #region Constructor - /// - /// Initializes a new instance of the class. - /// - protected ImageFile () - { - Format = ImageFileFormat.Unknown; - Properties = new ExifPropertyCollection (this); - Encoding = Encoding.Default; - } - #endregion - - #region Properties - /// - /// Returns the format of the . - /// - public ImageFileFormat Format { get; protected set; } - /// - /// Gets the collection of Exif properties contained in the . - /// - public ExifPropertyCollection Properties { get; private set; } - /// - /// Gets or sets the embedded thumbnail image. - /// - public ImageFile? Thumbnail { get; set; } - /// - /// Gets or sets the Exif property with the given key. - /// - /// The Exif tag associated with the Exif property. - public ExifProperty this[ExifTag key] { - get { return Properties[key]; } - set { Properties[key] = value; } - } - /// - /// Gets the encoding used for text metadata when the source encoding is unknown. - /// - public Encoding Encoding { get; protected set; } - #endregion - - #region Instance Methods - - /// - /// Saves the to the specified file. - /// - /// A string that contains the name of the file. - public virtual void Save (string filename) - { - using (FileStream stream = new FileStream (filename, FileMode.Create, FileAccess.Write, FileShare.None)) { - Save (stream); - } - } - - /// - /// Saves the to the specified stream. - /// - /// A to save image data to. - public abstract void Save (Stream stream); - #endregion - - #region Static Methods - /// - /// Creates an from the specified file. - /// - /// A string that contains the name of the file. - /// The created from the file. - public static ImageFile? FromFile (string filename) - { - return FromFile(filename, Encoding.Default); - } - - /// - /// Creates an from the specified file. - /// - /// A string that contains the name of the file. - /// The encoding to be used for text metadata when the source encoding is unknown. - /// The created from the file. - public static ImageFile? FromFile(string filename, Encoding encoding) - { - using (FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - return FromStream(stream, encoding); - } - } - - /// - /// Creates an from the specified data stream. - /// - /// A that contains image data. - /// The created from the file. - public static ImageFile? FromStream(Stream stream) - { - return FromStream(stream, Encoding.Default); - } - - /// - /// Creates an from the specified data stream. - /// - /// A that contains image data. - /// The encoding to be used for text metadata when the source encoding is unknown. - /// The created from the file. - public static ImageFile? FromStream(Stream stream, Encoding encoding) - { - // JPEG - if (JpegDetector.IsOfType(stream)) - { - return new JPEGFile(stream, encoding); - } - - // TIFF - if (TIFFDetector.IsOfType(stream)) - { - return new TIFFFile(stream, encoding); - } - - // SVG - if (SvgDetector.IsOfType(stream)) - { - return new SvgFile(stream); - } - - // We don't know - return null; - } - #endregion + Format = ImageFileFormat.Unknown; + Properties = new ExifPropertyCollection(this); + Encoding = Encoding.Default; } + + #endregion + + #region Properties + + /// + /// Returns the format of the . + /// + public ImageFileFormat Format { get; protected set; } + + /// + /// Gets the collection of Exif properties contained in the . + /// + public ExifPropertyCollection Properties { get; } + + /// + /// Gets or sets the embedded thumbnail image. + /// + public ImageFile? Thumbnail { get; set; } + + /// + /// Gets or sets the Exif property with the given key. + /// + /// The Exif tag associated with the Exif property. + public ExifProperty this[ExifTag key] + { + get => Properties[key]; + set => Properties[key] = value; + } + + /// + /// Gets the encoding used for text metadata when the source encoding is unknown. + /// + public Encoding Encoding { get; protected set; } + + #endregion + + #region Instance Methods + + /// + /// Saves the to the specified file. + /// + /// A string that contains the name of the file. + public virtual void Save(string filename) + { + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + Save(stream); + } + } + + /// + /// Saves the to the specified stream. + /// + /// A to save image data to. + public abstract void Save(Stream stream); + + #endregion + + #region Static Methods + + /// + /// Creates an from the specified file. + /// + /// A string that contains the name of the file. + /// The created from the file. + public static ImageFile? FromFile(string filename) => FromFile(filename, Encoding.Default); + + /// + /// Creates an from the specified file. + /// + /// A string that contains the name of the file. + /// The encoding to be used for text metadata when the source encoding is unknown. + /// The created from the file. + public static ImageFile? FromFile(string filename, Encoding encoding) + { + using (var stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return FromStream(stream, encoding); + } + } + + /// + /// Creates an from the specified data stream. + /// + /// A that contains image data. + /// The created from the file. + public static ImageFile? FromStream(Stream stream) => FromStream(stream, Encoding.Default); + + /// + /// Creates an from the specified data stream. + /// + /// A that contains image data. + /// The encoding to be used for text metadata when the source encoding is unknown. + /// The created from the file. + public static ImageFile? FromStream(Stream stream, Encoding encoding) + { + // JPEG + if (JpegDetector.IsOfType(stream)) + { + return new JPEGFile(stream, encoding); + } + + // TIFF + if (TIFFDetector.IsOfType(stream)) + { + return new TIFFFile(stream, encoding); + } + + // SVG + if (SvgDetector.IsOfType(stream)) + { + return new SvgFile(stream); + } + + // We don't know + return null; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs b/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs index ed4564a486..299e7619f9 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs @@ -1,97 +1,100 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents an image file directory. +/// +internal class ImageFileDirectory { /// - /// Represents an image file directory. + /// Initializes a new instance of the class. /// - internal class ImageFileDirectory + public ImageFileDirectory() { - /// - /// The fields contained in this IFD. - /// - public List Fields { get; private set; } - /// - /// Offset to the next IFD. - /// - public uint NextIFDOffset { get; private set; } - /// - /// Compressed image data. - /// - public List Strips { get; private set; } + Fields = new List(); + Strips = new List(); + } - /// - /// Initializes a new instance of the class. - /// - public ImageFileDirectory() + /// + /// The fields contained in this IFD. + /// + public List Fields { get; } + + /// + /// Offset to the next IFD. + /// + public uint NextIFDOffset { get; private set; } + + /// + /// Compressed image data. + /// + public List Strips { get; } + + /// + /// Returns a initialized from the given byte data. + /// + /// The data. + /// The offset into . + /// The byte order of . + /// A initialized from the given byte data. + public static ImageFileDirectory FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) + { + var ifd = new ImageFileDirectory(); + var conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); + + var stripOffsets = new List(); + var stripLengths = new List(); + + // Count + var fieldcount = conv.ToUInt16(data, offset); + + // Read fields + for (uint i = 0; i < fieldcount; i++) { - Fields = new List(); - Strips = new List(); - } + var fieldoffset = offset + 2 + (12 * i); + var field = ImageFileDirectoryEntry.FromBytes(data, fieldoffset, byteOrder); + ifd.Fields.Add(field); - /// - /// Returns a initialized from the given byte data. - /// - /// The data. - /// The offset into . - /// The byte order of . - /// A initialized from the given byte data. - public static ImageFileDirectory FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) - { - ImageFileDirectory ifd = new ImageFileDirectory(); - BitConverterEx conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); - - List stripOffsets = new List(); - List stripLengths = new List(); - - // Count - ushort fieldcount = conv.ToUInt16(data, offset); - - // Read fields - for (uint i = 0; i < fieldcount; i++) + // Read strip offsets + if (field.Tag == 273) { - uint fieldoffset = offset + 2 + 12 * i; - ImageFileDirectoryEntry field = ImageFileDirectoryEntry.FromBytes(data, fieldoffset, byteOrder); - ifd.Fields.Add(field); - - // Read strip offsets - if (field.Tag == 273) + var baselen = field.Data.Length / (int)field.Count; + for (uint j = 0; j < field.Count; j++) { - int baselen = field.Data.Length / (int)field.Count; - for (uint j = 0; j < field.Count; j++) - { - byte[] val = new byte[baselen]; - Array.Copy(field.Data, j * baselen, val, 0, baselen); - uint stripOffset = (field.Type == 3 ? (uint)BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0)); - stripOffsets.Add(stripOffset); - } - } - - // Read strip lengths - if (field.Tag == 279) - { - int baselen = field.Data.Length / (int)field.Count; - for (uint j = 0; j < field.Count; j++) - { - byte[] val = new byte[baselen]; - Array.Copy(field.Data, j * baselen, val, 0, baselen); - uint stripLength = (field.Type == 3 ? (uint)BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0)); - stripLengths.Add(stripLength); - } + var val = new byte[baselen]; + Array.Copy(field.Data, j * baselen, val, 0, baselen); + var stripOffset = field.Type == 3 ? BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0); + stripOffsets.Add(stripOffset); } } - // Save strips - if (stripOffsets.Count != stripLengths.Count) - throw new NotValidTIFFileException(); - for (int i = 0; i < stripOffsets.Count; i++) - ifd.Strips.Add(new TIFFStrip(data, stripOffsets[i], stripLengths[i])); - - // Offset to next ifd - ifd.NextIFDOffset = conv.ToUInt32(data, offset + 2 + 12 * fieldcount); - - return ifd; + // Read strip lengths + if (field.Tag == 279) + { + var baselen = field.Data.Length / (int)field.Count; + for (uint j = 0; j < field.Count; j++) + { + var val = new byte[baselen]; + Array.Copy(field.Data, j * baselen, val, 0, baselen); + var stripLength = field.Type == 3 ? BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0); + stripLengths.Add(stripLength); + } + } } + + // Save strips + if (stripOffsets.Count != stripLengths.Count) + { + throw new NotValidTIFFileException(); + } + + for (var i = 0; i < stripOffsets.Count; i++) + { + ifd.Strips.Add(new TIFFStrip(data, stripOffsets[i], stripLengths[i])); + } + + // Offset to next ifd + ifd.NextIFDOffset = conv.ToUInt32(data, offset + 2 + (12 * fieldcount)); + + return ifd; } } diff --git a/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs b/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs index 7d1568afb3..a3863b6a69 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs @@ -1,117 +1,144 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents an entry in the image file directory. +/// +internal struct ImageFileDirectoryEntry { /// - /// Represents an entry in the image file directory. + /// The tag that identifies the field. /// - internal struct ImageFileDirectoryEntry + public ushort Tag; + + /// + /// Field type identifier. + /// + public ushort Type; + + /// + /// Count of Type. + /// + public uint Count; + + /// + /// Field data. + /// + public byte[] Data; + + /// + /// Initializes a new instance of the struct. + /// + /// The tag that identifies the field. + /// Field type identifier. + /// Count of Type. + /// Field data. + public ImageFileDirectoryEntry(ushort tag, ushort type, uint count, byte[] data) { - /// - /// The tag that identifies the field. - /// - public ushort Tag; - /// - /// Field type identifier. - /// - public ushort Type; - /// - /// Count of Type. - /// - public uint Count; - /// - /// Field data. - /// - public byte[] Data; + Tag = tag; + Type = type; + Count = count; + Data = data; + } - /// - /// Initializes a new instance of the struct. - /// - /// The tag that identifies the field. - /// Field type identifier. - /// Count of Type. - /// Field data. - public ImageFileDirectoryEntry(ushort tag, ushort type, uint count, byte[] data) + /// + /// Returns a initialized from the given byte data. + /// + /// The data. + /// The offset into . + /// The byte order of . + /// A initialized from the given byte data. + public static ImageFileDirectoryEntry FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) + { + // Tag ID + var tag = BitConverterEx.ToUInt16(data, offset, byteOrder, BitConverterEx.SystemByteOrder); + + // Tag Type + var type = BitConverterEx.ToUInt16(data, offset + 2, byteOrder, BitConverterEx.SystemByteOrder); + + // Count of Type + var count = BitConverterEx.ToUInt32(data, offset + 4, byteOrder, BitConverterEx.SystemByteOrder); + + // Field value or offset to field data + var value = new byte[4]; + Array.Copy(data, offset + 8, value, 0, 4); + + // Calculate the bytes we need to read + var baselength = GetBaseLength(type); + var totallength = count * baselength; + + // If field value does not fit in 4 bytes + // the value field is an offset to the actual + // field value + if (totallength > 4) { - Tag = tag; - Type = type; - Count = count; - Data = data; + var dataoffset = BitConverterEx.ToUInt32(value, 0, byteOrder, BitConverterEx.SystemByteOrder); + value = new byte[totallength]; + Array.Copy(data, dataoffset, value, 0, totallength); } - /// - /// Returns a initialized from the given byte data. - /// - /// The data. - /// The offset into . - /// The byte order of . - /// A initialized from the given byte data. - public static ImageFileDirectoryEntry FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) + // Reverse array order if byte orders are different + if (byteOrder != BitConverterEx.SystemByteOrder) { - // Tag ID - ushort tag = BitConverterEx.ToUInt16(data, offset, byteOrder, BitConverterEx.SystemByteOrder); - - // Tag Type - ushort type = BitConverterEx.ToUInt16(data, offset + 2, byteOrder, BitConverterEx.SystemByteOrder); - - // Count of Type - uint count = BitConverterEx.ToUInt32(data, offset + 4, byteOrder, BitConverterEx.SystemByteOrder); - - // Field value or offset to field data - byte[] value = new byte[4]; - Array.Copy(data, offset + 8, value, 0, 4); - - // Calculate the bytes we need to read - uint baselength = GetBaseLength(type); - uint totallength = count * baselength; - - // If field value does not fit in 4 bytes - // the value field is an offset to the actual - // field value - if (totallength > 4) + for (uint i = 0; i < count; i++) { - uint dataoffset = BitConverterEx.ToUInt32(value, 0, byteOrder, BitConverterEx.SystemByteOrder); - value = new byte[totallength]; - Array.Copy(data, dataoffset, value, 0, totallength); + var val = new byte[baselength]; + Array.Copy(value, i * baselength, val, 0, baselength); + Array.Reverse(val); + Array.Copy(val, 0, value, i * baselength, baselength); } - - // Reverse array order if byte orders are different - if (byteOrder != BitConverterEx.SystemByteOrder) - { - for (uint i = 0; i < count; i++) - { - byte[] val = new byte[baselength]; - Array.Copy(value, i * baselength, val, 0, baselength); - Array.Reverse(val); - Array.Copy(val, 0, value, i * baselength, baselength); - } - } - - return new ImageFileDirectoryEntry(tag, type, count, value); } - /// - /// Gets the base byte length for the given type. - /// - /// Type identifier. - private static uint GetBaseLength(ushort type) + return new ImageFileDirectoryEntry(tag, type, count, value); + } + + /// + /// Gets the base byte length for the given type. + /// + /// Type identifier. + private static uint GetBaseLength(ushort type) + { + // BYTE and SBYTE + if (type == 1 || type == 6) { - if (type == 1 || type == 6) // BYTE and SBYTE - return 1; - else if (type == 2 || type == 7) // ASCII and UNDEFINED - return 1; - else if (type == 3 || type == 8) // SHORT and SSHORT - return 2; - else if (type == 4 || type == 9) // LONG and SLONG - return 4; - else if (type == 5 || type == 10) // RATIONAL (2xLONG) and SRATIONAL (2xSLONG) - return 8; - else if (type == 11) // FLOAT - return 4; - else if (type == 12) // DOUBLE - return 8; - - throw new ArgumentException("Unknown type identifier.", "type"); + return 1; } + + // ASCII and UNDEFINED + if (type == 2 || type == 7) + { + return 1; + } + + // SHORT and SSHORT + if (type == 3 || type == 8) + { + return 2; + } + + // LONG and SLONG + if (type == 4 || type == 9) + { + return 4; + } + + // RATIONAL (2xLONG) and SRATIONAL (2xSLONG) + if (type == 5 || type == 10) + { + return 8; + } + + // FLOAT + if (type == 11) + { + return 4; + } + + // DOUBLE + if (type == 12) + { + return 8; + } + + throw new ArgumentException("Unknown type identifier.", "type"); } } diff --git a/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs b/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs index 09cfcce589..fe30c713b2 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the format of the . +/// +internal enum ImageFileFormat { /// - /// Represents the format of the . + /// The file is not recognized. /// - internal enum ImageFileFormat - { - /// - /// The file is not recognized. - /// - Unknown, - /// - /// The file is a JPEG/Exif or JPEG/JFIF file. - /// - JPEG, - /// - /// The file is a TIFF File. - /// - TIFF, - /// - /// The file is a SVG File. - /// - SVG, - } + Unknown, + + /// + /// The file is a JPEG/Exif or JPEG/JFIF file. + /// + JPEG, + + /// + /// The file is a TIFF File. + /// + TIFF, + + /// + /// The file is a SVG File. + /// + SVG, } diff --git a/src/Umbraco.Core/Media/Exif/JFIFEnums.cs b/src/Umbraco.Core/Media/Exif/JFIFEnums.cs index ff6b0463ed..438d7bf3d4 100644 --- a/src/Umbraco.Core/Media/Exif/JFIFEnums.cs +++ b/src/Umbraco.Core/Media/Exif/JFIFEnums.cs @@ -1,40 +1,44 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the units for the X and Y densities +/// for a JFIF file. +/// +internal enum JFIFDensityUnit : byte { /// - /// Represents the units for the X and Y densities - /// for a JFIF file. + /// No units, XDensity and YDensity specify the pixel aspect ratio. /// - internal enum JFIFDensityUnit : byte - { - /// - /// No units, XDensity and YDensity specify the pixel aspect ratio. - /// - None = 0, - /// - /// XDensity and YDensity are dots per inch. - /// - DotsPerInch = 1, - /// - /// XDensity and YDensity are dots per cm. - /// - DotsPerCm = 2, - } + None = 0, + /// - /// Represents the JFIF extension. + /// XDensity and YDensity are dots per inch. /// - internal enum JFIFExtension : byte - { - /// - /// Thumbnail coded using JPEG. - /// - ThumbnailJPEG = 0x10, - /// - /// Thumbnail stored using a 256-Color RGB palette. - /// - ThumbnailPaletteRGB = 0x11, - /// - /// Thumbnail stored using 3 bytes/pixel (24-bit) RGB values. - /// - Thumbnail24BitRGB = 0x13, - } + DotsPerInch = 1, + + /// + /// XDensity and YDensity are dots per cm. + /// + DotsPerCm = 2, +} + +/// +/// Represents the JFIF extension. +/// +internal enum JFIFExtension : byte +{ + /// + /// Thumbnail coded using JPEG. + /// + ThumbnailJPEG = 0x10, + + /// + /// Thumbnail stored using a 256-Color RGB palette. + /// + ThumbnailPaletteRGB = 0x11, + + /// + /// Thumbnail stored using 3 bytes/pixel (24-bit) RGB values. + /// + Thumbnail24BitRGB = 0x13, } diff --git a/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs b/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs index d3a0e7fb46..71ea89228d 100644 --- a/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs +++ b/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs @@ -1,67 +1,76 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents the JFIF version as a 16 bit unsigned integer. (EXIF Specification: SHORT) +/// +internal class JFIFVersion : ExifUShort { - /// - /// Represents the JFIF version as a 16 bit unsigned integer. (EXIF Specification: SHORT) - /// - internal class JFIFVersion : ExifUShort + public JFIFVersion(ExifTag tag, ushort value) + : base(tag, value) { - /// - /// Gets the major version. - /// - public byte Major { get { return (byte)(mValue >> 8); } } - /// - /// Gets the minor version. - /// - public byte Minor { get { return (byte)(mValue - (mValue >> 8) * 256); } } - - public JFIFVersion(ExifTag tag, ushort value) - : base(tag, value) - { - - } - - public override string ToString() - { - return string.Format("{0}.{1:00}", Major, Minor); - } } + /// - /// Represents a JFIF thumbnail. (EXIF Specification: BYTE) + /// Gets the major version. /// - internal class JFIFThumbnailProperty : ExifProperty - { - protected JFIFThumbnail mValue; - protected override object _Value { get { return Value; } set { Value = (JFIFThumbnail)value; } } - public new JFIFThumbnail Value { get { return mValue; } set { mValue = value; } } + public byte Major => (byte)(mValue >> 8); - public override string ToString() { return mValue.Format.ToString(); } + /// + /// Gets the minor version. + /// + public byte Minor => (byte)(mValue - ((mValue >> 8) * 256)); - public JFIFThumbnailProperty(ExifTag tag, JFIFThumbnail value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - if (mValue.Format == JFIFThumbnail.ImageFormat.BMP24Bit) - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); - else if (mValue.Format == JFIFThumbnail.ImageFormat.BMPPalette) - { - byte[] data = new byte[mValue.Palette.Length + mValue.PixelData.Length]; - Array.Copy(mValue.Palette, data, mValue.Palette.Length); - Array.Copy(mValue.PixelData, 0, data, mValue.Palette.Length, mValue.PixelData.Length); - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); - } - else if (mValue.Format == JFIFThumbnail.ImageFormat.JPEG) - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); - else - throw new InvalidOperationException("Unknown thumbnail type."); - } - } - } + public override string ToString() => string.Format("{0}.{1:00}", Major, Minor); +} + +/// +/// Represents a JFIF thumbnail. (EXIF Specification: BYTE) +/// +internal class JFIFThumbnailProperty : ExifProperty +{ + protected JFIFThumbnail mValue; + + public JFIFThumbnailProperty(ExifTag tag, JFIFThumbnail value) + : base(tag) => + mValue = value; + + public new JFIFThumbnail Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (JFIFThumbnail)value; + } + + public override ExifInterOperability Interoperability + { + get + { + if (mValue.Format == JFIFThumbnail.ImageFormat.BMP24Bit) + { + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); + } + + if (mValue.Format == JFIFThumbnail.ImageFormat.BMPPalette) + { + var data = new byte[mValue.Palette.Length + mValue.PixelData.Length]; + Array.Copy(mValue.Palette, data, mValue.Palette.Length); + Array.Copy(mValue.PixelData, 0, data, mValue.Palette.Length, mValue.PixelData.Length); + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); + } + + if (mValue.Format == JFIFThumbnail.ImageFormat.JPEG) + { + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); + } + + throw new InvalidOperationException("Unknown thumbnail type."); + } + } + + public override string ToString() => mValue.Format.ToString(); } diff --git a/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs b/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs index de9fe8f76f..cafa804c3a 100644 --- a/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs +++ b/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs @@ -1,55 +1,62 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a JFIF thumbnail. +/// +internal class JFIFThumbnail { - /// - /// Represents a JFIF thumbnail. - /// - internal class JFIFThumbnail + #region Public Enums + + public enum ImageFormat { - #region Properties - /// - /// Gets the 256 color RGB palette. - /// - public byte[] Palette { get; private set; } - /// - /// Gets raw image data. - /// - public byte[] PixelData { get; private set; } - /// - /// Gets the image format. - /// - public ImageFormat Format { get; private set; } - #endregion - - #region Public Enums - public enum ImageFormat - { - JPEG, - BMPPalette, - BMP24Bit, - } - #endregion - - #region Constructors - protected JFIFThumbnail() - { - Palette = new byte[0]; - PixelData = new byte[0]; - } - - public JFIFThumbnail(ImageFormat format, byte[] data) - : this() - { - Format = format; - PixelData = data; - } - - public JFIFThumbnail(byte[] palette, byte[] data) - : this() - { - Format = ImageFormat.BMPPalette; - Palette = palette; - PixelData = data; - } - #endregion + JPEG, + BMPPalette, + BMP24Bit, } + + #endregion + + #region Properties + + /// + /// Gets the 256 color RGB palette. + /// + public byte[] Palette { get; } + + /// + /// Gets raw image data. + /// + public byte[] PixelData { get; } + + /// + /// Gets the image format. + /// + public ImageFormat Format { get; } + + #endregion + + #region Constructors + + protected JFIFThumbnail() + { + Palette = new byte[0]; + PixelData = new byte[0]; + } + + public JFIFThumbnail(ImageFormat format, byte[] data) + : this() + { + Format = format; + PixelData = data; + } + + public JFIFThumbnail(byte[] palette, byte[] data) + : this() + { + Format = ImageFormat.BMPPalette; + Palette = palette; + PixelData = data; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs b/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs index dde0326f99..c44d6d1db0 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs @@ -1,171 +1,219 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// The exception that is thrown when the format of the JPEG file could not be understood. +/// +/// +[Serializable] +public class NotValidJPEGFileException : Exception { - - /// - /// The exception that is thrown when the format of the JPEG file could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class NotValidJPEGFileException : Exception + public NotValidJPEGFileException() + : base("Not a valid JPEG file.") { - /// - /// Initializes a new instance of the class. - /// - public NotValidJPEGFileException() - : base("Not a valid JPEG file.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidJPEGFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidJPEGFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidJPEGFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } /// - /// The exception that is thrown when the format of the TIFF file could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class NotValidTIFFileException : Exception + /// The message that describes the error. + public NotValidJPEGFileException(string message) + : base(message) { - /// - /// Initializes a new instance of the class. - /// - public NotValidTIFFileException() - : base("Not a valid TIFF file.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidTIFFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidTIFFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidTIFFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } /// - /// The exception that is thrown when the format of the TIFF header could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - internal class NotValidTIFFHeader : Exception + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidJPEGFileException(string message, Exception innerException) + : base(message, innerException) { - /// - /// Initializes a new instance of the class. - /// - public NotValidTIFFHeader() - : base("Not a valid TIFF header.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidTIFFHeader(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidTIFFHeader(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidTIFFHeader(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } /// - /// The exception that is thrown when the length of a section exceeds 64 kB. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class SectionExceeds64KBException : Exception + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidJPEGFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} + +/// +/// The exception that is thrown when the format of the TIFF file could not be understood. +/// +/// +[Serializable] +public class NotValidTIFFileException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotValidTIFFileException() + : base("Not a valid TIFF file.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public NotValidTIFFileException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidTIFFileException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidTIFFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} + +/// +/// The exception that is thrown when the length of a section exceeds 64 kB. +/// +/// +[Serializable] +public class SectionExceeds64KBException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public SectionExceeds64KBException() + : base("Section length exceeds 64 kB.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public SectionExceeds64KBException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public SectionExceeds64KBException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected SectionExceeds64KBException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} + +/// +/// The exception that is thrown when the format of the TIFF header could not be understood. +/// +/// +[Serializable] +internal class NotValidTIFFHeader : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotValidTIFFHeader() + : base("Not a valid TIFF header.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public NotValidTIFFHeader(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidTIFFHeader(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidTIFFHeader(SerializationInfo info, StreamingContext context) + : base(info, context) { - /// - /// Initializes a new instance of the class. - /// - public SectionExceeds64KBException() - : base("Section length exceeds 64 kB.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public SectionExceeds64KBException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public SectionExceeds64KBException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected SectionExceeds64KBException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } } diff --git a/src/Umbraco.Core/Media/Exif/JPEGFile.cs b/src/Umbraco.Core/Media/Exif/JPEGFile.cs index f0f732b520..bdf7208ea0 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGFile.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGFile.cs @@ -1,924 +1,1110 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the binary view of a JPEG compressed file. +/// +internal class JPEGFile : ImageFile { + #region Constructor + /// - /// Represents the binary view of a JPEG compressed file. + /// Initializes a new instance of the class. /// - internal class JPEGFile : ImageFile + /// A that contains image data. + /// The encoding to be used for text metadata when the source encoding is unknown. + protected internal JPEGFile(Stream stream, Encoding encoding) { - #region Member Variables - private JPEGSection? jfifApp0; - private JPEGSection? jfxxApp0; - private JPEGSection? exifApp1; - private uint makerNoteOffset; - private long exifIFDFieldOffset, gpsIFDFieldOffset, interopIFDFieldOffset, firstIFDFieldOffset; - private long thumbOffsetLocation, thumbSizeLocation; - private uint thumbOffsetValue, thumbSizeValue; - private bool makerNoteProcessed; - #endregion + Format = ImageFileFormat.JPEG; + Sections = new List(); + TrailingData = new byte[0]; + Encoding = encoding; - #region Properties - /// - /// Gets or sets the byte-order of the Exif properties. - /// - public BitConverterEx.ByteOrder ByteOrder { get; set; } - /// - /// Gets or sets the sections contained in the . - /// - public List Sections { get; private set; } - /// - /// Gets or sets non-standard trailing data following the End of Image (EOI) marker. - /// - public byte[] TrailingData { get; private set; } - #endregion + stream.Seek(0, SeekOrigin.Begin); - #region Constructor - /// - /// Initializes a new instance of the class. - /// - /// A that contains image data. - /// The encoding to be used for text metadata when the source encoding is unknown. - protected internal JPEGFile(Stream stream, Encoding encoding) + // Read the Start of Image (SOI) marker. SOI marker is represented + // with two bytes: 0xFF, 0xD8. + var markerbytes = new byte[2]; + if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] != 0xD8) { - Format = ImageFileFormat.JPEG; - Sections = new List(); - TrailingData = new byte[0]; - Encoding = encoding; + throw new NotValidJPEGFileException(); + } - stream.Seek(0, SeekOrigin.Begin); + stream.Seek(0, SeekOrigin.Begin); - // Read the Start of Image (SOI) marker. SOI marker is represented - // with two bytes: 0xFF, 0xD8. - byte[] markerbytes = new byte[2]; - if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] != 0xD8) - throw new NotValidJPEGFileException(); - stream.Seek(0, SeekOrigin.Begin); - - // Search and read sections until we reach the end of file. - while (stream.Position != stream.Length) + // Search and read sections until we reach the end of file. + while (stream.Position != stream.Length) + { + // Read the next section marker. Section markers are two bytes + // with values 0xFF, 0x?? where ?? must not be 0x00 or 0xFF. + if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] == 0x00 || + markerbytes[1] == 0xFF) { - // Read the next section marker. Section markers are two bytes - // with values 0xFF, 0x?? where ?? must not be 0x00 or 0xFF. - if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] == 0x00 || markerbytes[1] == 0xFF) - throw new NotValidJPEGFileException(); + throw new NotValidJPEGFileException(); + } - JPEGMarker marker = (JPEGMarker)markerbytes[1]; + var marker = (JPEGMarker)markerbytes[1]; - byte[] header = new byte[0]; - // SOI, EOI and RST markers do not contain any header - if (marker != JPEGMarker.SOI && marker != JPEGMarker.EOI && !(marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + var header = new byte[0]; + + // SOI, EOI and RST markers do not contain any header + if (marker != JPEGMarker.SOI && marker != JPEGMarker.EOI && + !(marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + { + // Length of the header including the length bytes. + // This value is a 16-bit unsigned integer + // in big endian byte-order. + var lengthbytes = new byte[2]; + if (stream.Read(lengthbytes, 0, 2) != 2) { - // Length of the header including the length bytes. - // This value is a 16-bit unsigned integer - // in big endian byte-order. - byte[] lengthbytes = new byte[2]; - if (stream.Read(lengthbytes, 0, 2) != 2) - throw new NotValidJPEGFileException(); - long length = (long)BitConverterEx.BigEndian.ToUInt16(lengthbytes, 0); - - // Read section header. - header = new byte[length - 2]; - int bytestoread = header.Length; - while (bytestoread > 0) - { - int count = Math.Min(bytestoread, 4 * 1024); - int bytesread = stream.Read(header, header.Length - bytestoread, count); - if (bytesread == 0) - throw new NotValidJPEGFileException(); - bytestoread -= bytesread; - } + throw new NotValidJPEGFileException(); } - // Start of Scan (SOS) sections and RST sections are immediately - // followed by entropy coded data. For that, we need to read until - // the next section marker once we reach a SOS or RST. - byte[] entropydata = new byte[0]; - if (marker == JPEGMarker.SOS || (marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + long length = BitConverterEx.BigEndian.ToUInt16(lengthbytes, 0); + + // Read section header. + header = new byte[length - 2]; + var bytestoread = header.Length; + while (bytestoread > 0) { - long position = stream.Position; - - // Search for the next section marker - while (true) + var count = Math.Min(bytestoread, 4 * 1024); + var bytesread = stream.Read(header, header.Length - bytestoread, count); + if (bytesread == 0) { - // Search for an 0xFF indicating start of a marker - int nextbyte = 0; - do - { - nextbyte = stream.ReadByte(); - if (nextbyte == -1) - throw new NotValidJPEGFileException(); - } while ((byte)nextbyte != 0xFF); + throw new NotValidJPEGFileException(); + } - // Skip filler bytes (0xFF) - do - { - nextbyte = stream.ReadByte(); - if (nextbyte == -1) - throw new NotValidJPEGFileException(); - } while ((byte)nextbyte == 0xFF); + bytestoread -= bytesread; + } + } - // Looks like a section marker. The next byte must not be 0x00. - if ((byte)nextbyte != 0x00) - { - // We reached a section marker. Calculate the - // length of the entropy coded data. - stream.Seek(-2, SeekOrigin.Current); - long edlength = stream.Position - position; - stream.Seek(-edlength, SeekOrigin.Current); + // Start of Scan (SOS) sections and RST sections are immediately + // followed by entropy coded data. For that, we need to read until + // the next section marker once we reach a SOS or RST. + var entropydata = new byte[0]; + if (marker == JPEGMarker.SOS || (marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + { + var position = stream.Position; - // Read entropy coded data - entropydata = new byte[edlength]; - int bytestoread = entropydata.Length; - while (bytestoread > 0) + // Search for the next section marker + while (true) + { + // Search for an 0xFF indicating start of a marker + var nextbyte = 0; + do + { + nextbyte = stream.ReadByte(); + if (nextbyte == -1) + { + throw new NotValidJPEGFileException(); + } + } + while ((byte)nextbyte != 0xFF); + + // Skip filler bytes (0xFF) + do + { + nextbyte = stream.ReadByte(); + if (nextbyte == -1) + { + throw new NotValidJPEGFileException(); + } + } + while ((byte)nextbyte == 0xFF); + + // Looks like a section marker. The next byte must not be 0x00. + if ((byte)nextbyte != 0x00) + { + // We reached a section marker. Calculate the + // length of the entropy coded data. + stream.Seek(-2, SeekOrigin.Current); + var edlength = stream.Position - position; + stream.Seek(-edlength, SeekOrigin.Current); + + // Read entropy coded data + entropydata = new byte[edlength]; + var bytestoread = entropydata.Length; + while (bytestoread > 0) + { + var count = Math.Min(bytestoread, 4 * 1024); + var bytesread = stream.Read(entropydata, entropydata.Length - bytestoread, count); + if (bytesread == 0) { - int count = Math.Min(bytestoread, 4 * 1024); - int bytesread = stream.Read(entropydata, entropydata.Length - bytestoread, count); - if (bytesread == 0) - throw new NotValidJPEGFileException(); - bytestoread -= bytesread; + throw new NotValidJPEGFileException(); } - break; + bytestoread -= bytesread; } + + break; } } + } - // Store section. - JPEGSection section = new JPEGSection(marker, header, entropydata); - Sections.Add(section); + // Store section. + var section = new JPEGSection(marker, header, entropydata); + Sections.Add(section); - // Some propriety formats store data past the EOI marker - if (marker == JPEGMarker.EOI) + // Some propriety formats store data past the EOI marker + if (marker == JPEGMarker.EOI) + { + var bytestoread = (int)(stream.Length - stream.Position); + TrailingData = new byte[bytestoread]; + while (bytestoread > 0) { - int bytestoread = (int)(stream.Length - stream.Position); - TrailingData = new byte[bytestoread]; - while (bytestoread > 0) + var count = Math.Min(bytestoread, 4 * 1024); + var bytesread = stream.Read(TrailingData, TrailingData.Length - bytestoread, count); + if (bytesread == 0) { - int count = (int)Math.Min(bytestoread, 4 * 1024); - int bytesread = stream.Read(TrailingData, TrailingData.Length - bytestoread, count); - if (bytesread == 0) - throw new NotValidJPEGFileException(); - bytestoread -= bytesread; + throw new NotValidJPEGFileException(); } + + bytestoread -= bytesread; } } - - // Read metadata sections - ReadJFIFAPP0(); - ReadJFXXAPP0(); - ReadExifAPP1(); - - // Process the maker note - makerNoteProcessed = false; } - #endregion - #region Instance Methods - /// - /// Saves the JPEG/Exif image to the given stream. - /// - /// The path to the JPEG/Exif file. - /// Determines whether the maker note offset of - /// the original file will be preserved. - public void Save(Stream stream, bool preserveMakerNote) + // Read metadata sections + ReadJFIFAPP0(); + ReadJFXXAPP0(); + ReadExifAPP1(); + + // Process the maker note + _makerNoteProcessed = false; + } + + #endregion + + #region Member Variables + + private JPEGSection? _jfifApp0; + private JPEGSection? _jfxxApp0; + private JPEGSection? _exifApp1; + private uint _makerNoteOffset; + private long _exifIfdFieldOffset; + private long _gpsIfdFieldOffset; + private long _interopIfdFieldOffset; + private long _firstIfdFieldOffset; + private long _thumbOffsetLocation; + private long _thumbSizeLocation; + private uint _thumbOffsetValue; + private uint _thumbSizeValue; + private readonly bool _makerNoteProcessed; + + #endregion + + #region Properties + + /// + /// Gets or sets the byte-order of the Exif properties. + /// + public BitConverterEx.ByteOrder ByteOrder { get; set; } + + /// + /// Gets or sets the sections contained in the . + /// + public List Sections { get; } + + /// + /// Gets or sets non-standard trailing data following the End of Image (EOI) marker. + /// + public byte[] TrailingData { get; } + + #endregion + + #region Instance Methods + + /// + /// Saves the JPEG/Exif image to the given stream. + /// + /// The stream of the JPEG/Exif file. + /// + /// Determines whether the maker note offset of + /// the original file will be preserved. + /// + public void Save(Stream stream, bool preserveMakerNote) + { + WriteJFIFApp0(); + WriteJFXXApp0(); + WriteExifApp1(preserveMakerNote); + + // Write sections + foreach (JPEGSection section in Sections) { - WriteJFIFApp0(); - WriteJFXXApp0(); - WriteExifApp1(preserveMakerNote); - - // Write sections - foreach (JPEGSection section in Sections) + // Section header (including length bytes and section marker) + // must not exceed 64 kB. + if (section.Header.Length + 2 + 2 > 64 * 1024) { - // Section header (including length bytes and section marker) - // must not exceed 64 kB. - if (section.Header.Length + 2 + 2 > 64 * 1024) - throw new SectionExceeds64KBException(); + throw new SectionExceeds64KBException(); + } - // APP sections must have a header. - // Otherwise skip the entire section. - if (section.Marker >= JPEGMarker.APP0 && section.Marker <= JPEGMarker.APP15 && section.Header.Length == 0) - continue; + // APP sections must have a header. + // Otherwise skip the entire section. + if (section.Marker >= JPEGMarker.APP0 && section.Marker <= JPEGMarker.APP15 && section.Header.Length == 0) + { + continue; + } - // Write section marker - stream.Write(new byte[] { 0xFF, (byte)section.Marker }, 0, 2); + // Write section marker + stream.Write(new byte[] { 0xFF, (byte)section.Marker }, 0, 2); - // SOI, EOI and RST markers do not contain any header - if (section.Marker != JPEGMarker.SOI && section.Marker != JPEGMarker.EOI && !(section.Marker >= JPEGMarker.RST0 && section.Marker <= JPEGMarker.RST7)) + // SOI, EOI and RST markers do not contain any header + if (section.Marker != JPEGMarker.SOI && section.Marker != JPEGMarker.EOI && + !(section.Marker >= JPEGMarker.RST0 && section.Marker <= JPEGMarker.RST7)) + { + // Header length including the length field itself + stream.Write(BitConverterEx.BigEndian.GetBytes((ushort)(section.Header.Length + 2)), 0, 2); + + // Write section header + if (section.Header.Length != 0) { - // Header length including the length field itself - stream.Write(BitConverterEx.BigEndian.GetBytes((ushort)(section.Header.Length + 2)), 0, 2); - - // Write section header - if (section.Header.Length != 0) - stream.Write(section.Header, 0, section.Header.Length); + stream.Write(section.Header, 0, section.Header.Length); } - - // Write entropy coded data - if (section.EntropyData.Length != 0) - stream.Write(section.EntropyData, 0, section.EntropyData.Length); } - // Write trailing data, if any - if (TrailingData.Length != 0) - stream.Write(TrailingData, 0, TrailingData.Length); - } - - /// - /// Saves the JPEG/Exif image with the given filename. - /// - /// The path to the JPEG/Exif file. - /// Determines whether the maker note offset of - /// the original file will be preserved. - public void Save(string filename, bool preserveMakerNote) - { - using (FileStream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + // Write entropy coded data + if (section.EntropyData.Length != 0) { - Save(stream, preserveMakerNote); + stream.Write(section.EntropyData, 0, section.EntropyData.Length); } } - /// - /// Saves the JPEG/Exif image with the given filename. - /// - /// The path to the JPEG/Exif file. - public override void Save(string filename) + // Write trailing data, if any + if (TrailingData.Length != 0) { - Save(filename, true); + stream.Write(TrailingData, 0, TrailingData.Length); + } + } + + /// + /// Saves the JPEG/Exif image with the given filename. + /// + /// The path to the JPEG/Exif file. + /// + /// Determines whether the maker note offset of + /// the original file will be preserved. + /// + public void Save(string filename, bool preserveMakerNote) + { + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + Save(stream, preserveMakerNote); + } + } + + /// + /// Saves the JPEG/Exif image with the given filename. + /// + /// The path to the JPEG/Exif file. + public override void Save(string filename) => Save(filename, true); + + /// + /// Saves the JPEG/Exif image to the given stream. + /// + /// The stream of the JPEG/Exif file. + public override void Save(Stream stream) => Save(stream, true); + + #endregion + + #region Private Helper Methods + + /// + /// Reads the APP0 section containing JFIF metadata. + /// + private void ReadJFIFAPP0() + { + // Find the APP0 section containing JFIF metadata + _jfifApp0 = Sections.Find(a => a.Marker == JPEGMarker.APP0 && + a.Header.Length >= 5 && + Encoding.ASCII.GetString(a.Header, 0, 5) == "JFIF\0"); + + // If there is no APP0 section, return. + if (_jfifApp0 == null) + { + return; } - /// - /// Saves the JPEG/Exif image to the given stream. - /// - /// The path to the JPEG/Exif file. - public override void Save(Stream stream) + var header = _jfifApp0.Header; + BitConverterEx jfifConv = BitConverterEx.BigEndian; + + // Version + var version = jfifConv.ToUInt16(header, 5); + Properties.Add(new JFIFVersion(ExifTag.JFIFVersion, version)); + + // Units + var unit = header[7]; + Properties.Add(new ExifEnumProperty(ExifTag.JFIFUnits, (JFIFDensityUnit)unit)); + + // X and Y densities + var xdensity = jfifConv.ToUInt16(header, 8); + Properties.Add(new ExifUShort(ExifTag.XDensity, xdensity)); + var ydensity = jfifConv.ToUInt16(header, 10); + Properties.Add(new ExifUShort(ExifTag.YDensity, ydensity)); + + // Thumbnails pixel count + var xthumbnail = header[12]; + Properties.Add(new ExifByte(ExifTag.JFIFXThumbnail, xthumbnail)); + var ythumbnail = header[13]; + Properties.Add(new ExifByte(ExifTag.JFIFYThumbnail, ythumbnail)); + + // Read JFIF thumbnail + var n = xthumbnail * ythumbnail; + var jfifThumbnail = new byte[n]; + Array.Copy(header, 14, jfifThumbnail, 0, n); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFIFThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, jfifThumbnail))); + } + + /// + /// Replaces the contents of the APP0 section with the JFIF properties. + /// + private bool WriteJFIFApp0() + { + // Which IFD sections do we have? + var ifdjfef = new List(); + foreach (ExifProperty prop in Properties) { - Save(stream, true); + if (prop.IFD == IFD.JFIF) + { + ifdjfef.Add(prop); + } } - #endregion - - #region Private Helper Methods - /// - /// Reads the APP0 section containing JFIF metadata. - /// - private void ReadJFIFAPP0() + if (ifdjfef.Count == 0) { - // Find the APP0 section containing JFIF metadata - jfifApp0 = Sections.Find(a => (a.Marker == JPEGMarker.APP0) && - a.Header.Length >= 5 && - (Encoding.ASCII.GetString(a.Header, 0, 5) == "JFIF\0")); + // Nothing to write + return false; + } - // If there is no APP0 section, return. - if (jfifApp0 == null) - return; + // Create a memory stream to write the APP0 section to + var ms = new MemoryStream(); - byte[] header = jfifApp0.Header; - BitConverterEx jfifConv = BitConverterEx.BigEndian; + // JFIF identifier + ms.Write(Encoding.ASCII.GetBytes("JFIF\0"), 0, 5); - // Version - ushort version = jfifConv.ToUInt16(header, 5); - Properties.Add(new JFIFVersion(ExifTag.JFIFVersion, version)); + // Write tags + foreach (ExifProperty prop in ifdjfef) + { + ExifInterOperability interop = prop.Interoperability; + var data = interop.Data; + if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) + { + Array.Reverse(data); + } - // Units - byte unit = header[7]; - Properties.Add(new ExifEnumProperty(ExifTag.JFIFUnits, (JFIFDensityUnit)unit)); + ms.Write(data, 0, data.Length); + } - // X and Y densities - ushort xdensity = jfifConv.ToUInt16(header, 8); - Properties.Add(new ExifUShort(ExifTag.XDensity, xdensity)); - ushort ydensity = jfifConv.ToUInt16(header, 10); - Properties.Add(new ExifUShort(ExifTag.YDensity, ydensity)); + ms.Close(); + // Return APP0 header + if (_jfifApp0 is not null) + { + _jfifApp0.Header = ms.ToArray(); + return true; + } + + return false; + } + + /// + /// Reads the APP0 section containing JFIF extension metadata. + /// + private void ReadJFXXAPP0() + { + // Find the APP0 section containing JFIF metadata + _jfxxApp0 = Sections.Find(a => a.Marker == JPEGMarker.APP0 && + a.Header.Length >= 5 && + Encoding.ASCII.GetString(a.Header, 0, 5) == "JFXX\0"); + + // If there is no APP0 section, return. + if (_jfxxApp0 == null) + { + return; + } + + var header = _jfxxApp0.Header; + + // Version + var version = (JFIFExtension)header[5]; + Properties.Add(new ExifEnumProperty(ExifTag.JFXXExtensionCode, version)); + + // Read thumbnail + if (version == JFIFExtension.ThumbnailJPEG) + { + var data = new byte[header.Length - 6]; + Array.Copy(header, 6, data, 0, data.Length); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, data))); + } + else if (version == JFIFExtension.Thumbnail24BitRGB) + { // Thumbnails pixel count - byte xthumbnail = header[12]; - Properties.Add(new ExifByte(ExifTag.JFIFXThumbnail, xthumbnail)); - byte ythumbnail = header[13]; - Properties.Add(new ExifByte(ExifTag.JFIFYThumbnail, ythumbnail)); - - // Read JFIF thumbnail - int n = xthumbnail * ythumbnail; - byte[] jfifThumbnail = new byte[n]; - Array.Copy(header, 14, jfifThumbnail, 0, n); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFIFThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, jfifThumbnail))); + var xthumbnail = header[6]; + Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); + var ythumbnail = header[7]; + Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); + var data = new byte[3 * xthumbnail * ythumbnail]; + Array.Copy(header, 8, data, 0, data.Length); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.BMP24Bit, data))); } - /// - /// Replaces the contents of the APP0 section with the JFIF properties. - /// - private bool WriteJFIFApp0() + else if (version == JFIFExtension.ThumbnailPaletteRGB) { - // Which IFD sections do we have? - List ifdjfef = new List(); - foreach (ExifProperty prop in Properties) + // Thumbnails pixel count + var xthumbnail = header[6]; + Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); + var ythumbnail = header[7]; + Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); + var palette = new byte[768]; + Array.Copy(header, 8, palette, 0, palette.Length); + var data = new byte[xthumbnail * ythumbnail]; + Array.Copy(header, 8 + 768, data, 0, data.Length); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(palette, data))); + } + } + + /// + /// Replaces the contents of the APP0 section with the JFIF extension properties. + /// + private bool WriteJFXXApp0() + { + // Which IFD sections do we have? + var ifdjfef = new List(); + foreach (ExifProperty prop in Properties) + { + if (prop.IFD == IFD.JFXX) { - if (prop.IFD == IFD.JFIF) - ifdjfef.Add(prop); + ifdjfef.Add(prop); + } + } + + if (ifdjfef.Count == 0) + { + // Nothing to write + return false; + } + + // Create a memory stream to write the APP0 section to + var ms = new MemoryStream(); + + // JFIF identifier + ms.Write(Encoding.ASCII.GetBytes("JFXX\0"), 0, 5); + + // Write tags + foreach (ExifProperty prop in ifdjfef) + { + ExifInterOperability interop = prop.Interoperability; + var data = interop.Data; + if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) + { + Array.Reverse(data); } - if (ifdjfef.Count == 0) - { - // Nothing to write - return false; - } + ms.Write(data, 0, data.Length); + } + ms.Close(); - // Create a memory stream to write the APP0 section to - MemoryStream ms = new MemoryStream(); - - // JFIF identifier - ms.Write(Encoding.ASCII.GetBytes("JFIF\0"), 0, 5); - - // Write tags - foreach (ExifProperty prop in ifdjfef) - { - ExifInterOperability interop = prop.Interoperability; - byte[] data = interop.Data; - if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) - Array.Reverse(data); - ms.Write(data, 0, data.Length); - } - - ms.Close(); - + if (_jfxxApp0 is not null) + { // Return APP0 header - if (jfifApp0 is not null) - { - jfifApp0.Header = ms.ToArray(); - return true; - } - - return false; + _jfxxApp0.Header = ms.ToArray(); + return true; } - /// - /// Reads the APP0 section containing JFIF extension metadata. - /// - private void ReadJFXXAPP0() + return false; + } + + /// + /// Reads the APP1 section containing Exif metadata. + /// + private void ReadExifAPP1() + { + // Find the APP1 section containing Exif metadata + _exifApp1 = Sections.Find(a => a.Marker == JPEGMarker.APP1 && + a.Header.Length >= 6 && + Encoding.ASCII.GetString(a.Header, 0, 6) == "Exif\0\0"); + + // If there is no APP1 section, add a new one after the last APP0 section (if any). + if (_exifApp1 == null) { - // Find the APP0 section containing JFIF metadata - jfxxApp0 = Sections.Find(a => (a.Marker == JPEGMarker.APP0) && - a.Header.Length >= 5 && - (Encoding.ASCII.GetString(a.Header, 0, 5) == "JFXX\0")); - - // If there is no APP0 section, return. - if (jfxxApp0 == null) - return; - - byte[] header = jfxxApp0.Header; - - // Version - JFIFExtension version = (JFIFExtension)header[5]; - Properties.Add(new ExifEnumProperty(ExifTag.JFXXExtensionCode, version)); - - // Read thumbnail - if (version == JFIFExtension.ThumbnailJPEG) + var insertionIndex = Sections.FindLastIndex(a => a.Marker == JPEGMarker.APP0); + if (insertionIndex == -1) { - byte[] data = new byte[header.Length - 6]; - Array.Copy(header, 6, data, 0, data.Length); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, data))); - } - else if (version == JFIFExtension.Thumbnail24BitRGB) - { - // Thumbnails pixel count - byte xthumbnail = header[6]; - Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); - byte ythumbnail = header[7]; - Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); - byte[] data = new byte[3 * xthumbnail * ythumbnail]; - Array.Copy(header, 8, data, 0, data.Length); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.BMP24Bit, data))); - } - else if (version == JFIFExtension.ThumbnailPaletteRGB) - { - // Thumbnails pixel count - byte xthumbnail = header[6]; - Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); - byte ythumbnail = header[7]; - Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); - byte[] palette = new byte[768]; - Array.Copy(header, 8, palette, 0, palette.Length); - byte[] data = new byte[xthumbnail * ythumbnail]; - Array.Copy(header, 8 + 768, data, 0, data.Length); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(palette, data))); - } - } - /// - /// Replaces the contents of the APP0 section with the JFIF extension properties. - /// - private bool WriteJFXXApp0() - { - // Which IFD sections do we have? - List ifdjfef = new List(); - foreach (ExifProperty prop in Properties) - { - if (prop.IFD == IFD.JFXX) - ifdjfef.Add(prop); + insertionIndex = 0; } - if (ifdjfef.Count == 0) + insertionIndex++; + _exifApp1 = new JPEGSection(JPEGMarker.APP1); + Sections.Insert(insertionIndex, _exifApp1); + if (BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian) { - // Nothing to write - return false; - } - - // Create a memory stream to write the APP0 section to - MemoryStream ms = new MemoryStream(); - - // JFIF identifier - ms.Write(Encoding.ASCII.GetBytes("JFXX\0"), 0, 5); - - // Write tags - foreach (ExifProperty prop in ifdjfef) - { - ExifInterOperability interop = prop.Interoperability; - byte[] data = interop.Data; - if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) - Array.Reverse(data); - ms.Write(data, 0, data.Length); - } - - ms.Close(); - - if (jfxxApp0 is not null) - { - // Return APP0 header - jfxxApp0.Header = ms.ToArray(); - return true; - } - - return false; - } - - /// - /// Reads the APP1 section containing Exif metadata. - /// - private void ReadExifAPP1() - { - // Find the APP1 section containing Exif metadata - exifApp1 = Sections.Find(a => (a.Marker == JPEGMarker.APP1) && - a.Header.Length >= 6 && - (Encoding.ASCII.GetString(a.Header, 0, 6) == "Exif\0\0")); - - // If there is no APP1 section, add a new one after the last APP0 section (if any). - if (exifApp1 == null) - { - int insertionIndex = Sections.FindLastIndex(a => a.Marker == JPEGMarker.APP0); - if (insertionIndex == -1) insertionIndex = 0; - insertionIndex++; - exifApp1 = new JPEGSection(JPEGMarker.APP1); - Sections.Insert(insertionIndex, exifApp1); - if (BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian) - ByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else - ByteOrder = BitConverterEx.ByteOrder.BigEndian; - return; - } - - byte[] header = exifApp1.Header; - SortedList ifdqueue = new SortedList(); - makerNoteOffset = 0; - - // TIFF header - int tiffoffset = 6; - if (header[tiffoffset] == 0x49 && header[tiffoffset + 1] == 0x49) ByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (header[tiffoffset] == 0x4D && header[tiffoffset + 1] == 0x4D) + } + else + { ByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidExifFileException(); - - // TIFF header may have a different byte order - BitConverterEx.ByteOrder tiffByteOrder = ByteOrder; - if (BitConverterEx.LittleEndian.ToUInt16(header, tiffoffset + 2) == 42) - tiffByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (BitConverterEx.BigEndian.ToUInt16(header, tiffoffset + 2) == 42) - tiffByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidExifFileException(); - - // Offset to 0th IFD - int ifd0offset = (int)BitConverterEx.ToUInt32(header, tiffoffset + 4, tiffByteOrder, BitConverterEx.SystemByteOrder); - ifdqueue.Add(ifd0offset, IFD.Zeroth); - - BitConverterEx conv = new BitConverterEx(ByteOrder, BitConverterEx.SystemByteOrder); - int thumboffset = -1; - int thumblength = 0; - int thumbtype = -1; - // Read IFDs - while (ifdqueue.Count != 0) - { - int ifdoffset = tiffoffset + ifdqueue.Keys[0]; - IFD currentifd = ifdqueue.Values[0]; - ifdqueue.RemoveAt(0); - - // Field count - ushort fieldcount = conv.ToUInt16(header, ifdoffset); - for (short i = 0; i < fieldcount; i++) - { - // Read field info - int fieldoffset = ifdoffset + 2 + 12 * i; - ushort tag = conv.ToUInt16(header, fieldoffset); - ushort type = conv.ToUInt16(header, fieldoffset + 2); - uint count = conv.ToUInt32(header, fieldoffset + 4); - byte[] value = new byte[4]; - Array.Copy(header, fieldoffset + 8, value, 0, 4); - - // Fields containing offsets to other IFDs - if (currentifd == IFD.Zeroth && tag == 0x8769) - { - int exififdpointer = (int)conv.ToUInt32(value, 0); - ifdqueue.Add(exififdpointer, IFD.EXIF); - } - else if (currentifd == IFD.Zeroth && tag == 0x8825) - { - int gpsifdpointer = (int)conv.ToUInt32(value, 0); - ifdqueue.Add(gpsifdpointer, IFD.GPS); - } - else if (currentifd == IFD.EXIF && tag == 0xa005) - { - int interopifdpointer = (int)conv.ToUInt32(value, 0); - ifdqueue.Add(interopifdpointer, IFD.Interop); - } - - // Save the offset to maker note data - if (currentifd == IFD.EXIF && tag == 37500) - makerNoteOffset = conv.ToUInt32(value, 0); - - // Calculate the bytes we need to read - uint baselength = 0; - if (type == 1 || type == 2 || type == 7) - baselength = 1; - else if (type == 3) - baselength = 2; - else if (type == 4 || type == 9) - baselength = 4; - else if (type == 5 || type == 10) - baselength = 8; - uint totallength = count * baselength; - - // If field value does not fit in 4 bytes - // the value field is an offset to the actual - // field value - int fieldposition = 0; - if (totallength > 4) - { - fieldposition = tiffoffset + (int)conv.ToUInt32(value, 0); - value = new byte[totallength]; - Array.Copy(header, fieldposition, value, 0, totallength); - } - - // Compressed thumbnail data - if (currentifd == IFD.First && tag == 0x201) - { - thumbtype = 0; - thumboffset = (int)conv.ToUInt32(value, 0); - } - else if (currentifd == IFD.First && tag == 0x202) - thumblength = (int)conv.ToUInt32(value, 0); - - // Uncompressed thumbnail data - if (currentifd == IFD.First && tag == 0x111) - { - thumbtype = 1; - // Offset to first strip - if (type == 3) - thumboffset = (int)conv.ToUInt16(value, 0); - else - thumboffset = (int)conv.ToUInt32(value, 0); - } - else if (currentifd == IFD.First && tag == 0x117) - { - thumblength = 0; - for (int j = 0; j < count; j++) - { - if (type == 3) - thumblength += (int)conv.ToUInt16(value, 0); - else - thumblength += (int)conv.ToUInt32(value, 0); - } - } - - // Create the exif property from the interop data - ExifProperty prop = ExifPropertyFactory.Get(tag, type, count, value, ByteOrder, currentifd, Encoding); - Properties.Add(prop); - } - - // 1st IFD pointer - int firstifdpointer = (int)conv.ToUInt32(header, ifdoffset + 2 + 12 * fieldcount); - if (firstifdpointer != 0) - ifdqueue.Add(firstifdpointer, IFD.First); - // Read thumbnail - if (thumboffset != -1 && thumblength != 0 && Thumbnail == null) - { - if (thumbtype == 0) - { - using (MemoryStream ts = new MemoryStream(header, tiffoffset + thumboffset, thumblength)) - { - Thumbnail = ImageFile.FromStream(ts); - } - } - } } + + return; } - /// - /// Replaces the contents of the APP1 section with the Exif properties. - /// - private bool WriteExifApp1(bool preserveMakerNote) + var header = _exifApp1.Header; + var ifdqueue = new SortedList(); + _makerNoteOffset = 0; + + // TIFF header + var tiffoffset = 6; + if (header[tiffoffset] == 0x49 && header[tiffoffset + 1] == 0x49) { - // Zero out IFD field offsets. We will fill those as we write the IFD sections - exifIFDFieldOffset = 0; - gpsIFDFieldOffset = 0; - interopIFDFieldOffset = 0; - firstIFDFieldOffset = 0; - // We also do not know the location of the embedded thumbnail yet - thumbOffsetLocation = 0; - thumbOffsetValue = 0; - thumbSizeLocation = 0; - thumbSizeValue = 0; - // Write thumbnail tags if they are missing, remove otherwise - if (Thumbnail == null) - { - Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormat); - Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormatLength); - } - else - { - if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormat)) - Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormat, 0)); - if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormatLength)) - Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormatLength, 0)); - } - - // Which IFD sections do we have? - Dictionary ifdzeroth = new Dictionary(); - Dictionary ifdexif = new Dictionary(); - Dictionary ifdgps = new Dictionary(); - Dictionary ifdinterop = new Dictionary(); - Dictionary ifdfirst = new Dictionary(); - - foreach (ExifProperty prop in Properties) - { - switch (prop.IFD) - { - case IFD.Zeroth: - ifdzeroth.Add(prop.Tag, prop); - break; - case IFD.EXIF: - ifdexif.Add(prop.Tag, prop); - break; - case IFD.GPS: - ifdgps.Add(prop.Tag, prop); - break; - case IFD.Interop: - ifdinterop.Add(prop.Tag, prop); - break; - case IFD.First: - ifdfirst.Add(prop.Tag, prop); - break; - } - } - - // Add IFD pointers if they are missing - // We will write the pointer values later on - if (ifdexif.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) - ifdzeroth.Add(ExifTag.EXIFIFDPointer, new ExifUInt(ExifTag.EXIFIFDPointer, 0)); - if (ifdgps.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) - ifdzeroth.Add(ExifTag.GPSIFDPointer, new ExifUInt(ExifTag.GPSIFDPointer, 0)); - if (ifdinterop.Count != 0 && !ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) - ifdexif.Add(ExifTag.InteroperabilityIFDPointer, new ExifUInt(ExifTag.InteroperabilityIFDPointer, 0)); - - // Remove IFD pointers if IFD sections are missing - if (ifdexif.Count == 0 && ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) - ifdzeroth.Remove(ExifTag.EXIFIFDPointer); - if (ifdgps.Count == 0 && ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) - ifdzeroth.Remove(ExifTag.GPSIFDPointer); - if (ifdinterop.Count == 0 && ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) - ifdexif.Remove(ExifTag.InteroperabilityIFDPointer); - - if (ifdzeroth.Count == 0 && ifdgps.Count == 0 && ifdinterop.Count == 0 && ifdfirst.Count == 0 && Thumbnail == null) - { - // Nothing to write - return false; - } - - // We will need these BitConverters to write byte-ordered data - BitConverterEx bceExif = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); - - // Create a memory stream to write the APP1 section to - MemoryStream ms = new MemoryStream(); - - // Exif identifier - ms.Write(Encoding.ASCII.GetBytes("Exif\0\0"), 0, 6); - - // TIFF header - // Byte order - long tiffoffset = ms.Position; - ms.Write((ByteOrder == BitConverterEx.ByteOrder.LittleEndian ? new byte[] { 0x49, 0x49 } : new byte[] { 0x4D, 0x4D }), 0, 2); - // TIFF ID - ms.Write(bceExif.GetBytes((ushort)42), 0, 2); - // Offset to 0th IFD - ms.Write(bceExif.GetBytes((uint)8), 0, 4); - - // Write IFDs - WriteIFD(ms, ifdzeroth, IFD.Zeroth, tiffoffset, preserveMakerNote); - uint exififdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdexif, IFD.EXIF, tiffoffset, preserveMakerNote); - uint gpsifdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdgps, IFD.GPS, tiffoffset, preserveMakerNote); - uint interopifdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdinterop, IFD.Interop, tiffoffset, preserveMakerNote); - uint firstifdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdfirst, IFD.First, tiffoffset, preserveMakerNote); - - // Now that we now the location of IFDs we can go back and write IFD offsets - if (exifIFDFieldOffset != 0) - { - ms.Seek(exifIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(exififdrelativeoffset), 0, 4); - } - if (gpsIFDFieldOffset != 0) - { - ms.Seek(gpsIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(gpsifdrelativeoffset), 0, 4); - } - if (interopIFDFieldOffset != 0) - { - ms.Seek(interopIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(interopifdrelativeoffset), 0, 4); - } - if (firstIFDFieldOffset != 0) - { - ms.Seek(firstIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(firstifdrelativeoffset), 0, 4); - } - // We can write thumbnail location now - if (thumbOffsetLocation != 0) - { - ms.Seek(thumbOffsetLocation, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(thumbOffsetValue), 0, 4); - } - if (thumbSizeLocation != 0) - { - ms.Seek(thumbSizeLocation, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(thumbSizeValue), 0, 4); - } - - ms.Close(); - - if (exifApp1 is not null) - { - // Return APP1 header - exifApp1.Header = ms.ToArray(); - return true; - } - - return false; + ByteOrder = BitConverterEx.ByteOrder.LittleEndian; + } + else if (header[tiffoffset] == 0x4D && header[tiffoffset + 1] == 0x4D) + { + ByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidExifFileException(); } - private void WriteIFD(MemoryStream stream, Dictionary ifd, IFD ifdtype, long tiffoffset, bool preserveMakerNote) + // TIFF header may have a different byte order + BitConverterEx.ByteOrder tiffByteOrder = ByteOrder; + if (BitConverterEx.LittleEndian.ToUInt16(header, tiffoffset + 2) == 42) { - BitConverterEx conv = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + tiffByteOrder = BitConverterEx.ByteOrder.LittleEndian; + } + else if (BitConverterEx.BigEndian.ToUInt16(header, tiffoffset + 2) == 42) + { + tiffByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidExifFileException(); + } - // Create a queue of fields to write - Queue fieldqueue = new Queue(); - foreach (ExifProperty prop in ifd.Values) - if (prop.Tag != ExifTag.MakerNote) - fieldqueue.Enqueue(prop); - // Push the maker note data to the end - if (ifd.ContainsKey(ExifTag.MakerNote)) - fieldqueue.Enqueue(ifd[ExifTag.MakerNote]); + // Offset to 0th IFD + var ifd0offset = (int)BitConverterEx.ToUInt32(header, tiffoffset + 4, tiffByteOrder, BitConverterEx.SystemByteOrder); + ifdqueue.Add(ifd0offset, IFD.Zeroth); - // Offset to start of field data from start of TIFF header - uint dataoffset = (uint)(2 + ifd.Count * 12 + 4 + stream.Position - tiffoffset); - uint currentdataoffset = dataoffset; - long absolutedataoffset = stream.Position + (2 + ifd.Count * 12 + 4); + var conv = new BitConverterEx(ByteOrder, BitConverterEx.SystemByteOrder); + var thumboffset = -1; + var thumblength = 0; + var thumbtype = -1; + + // Read IFDs + while (ifdqueue.Count != 0) + { + var ifdoffset = tiffoffset + ifdqueue.Keys[0]; + IFD currentifd = ifdqueue.Values[0]; + ifdqueue.RemoveAt(0); - bool makernotewritten = false; // Field count - stream.Write(conv.GetBytes((ushort)ifd.Count), 0, 2); - // Fields - while (fieldqueue.Count != 0) + var fieldcount = conv.ToUInt16(header, ifdoffset); + for (short i = 0; i < fieldcount; i++) { - ExifProperty field = fieldqueue.Dequeue(); - ExifInterOperability interop = field.Interoperability; - - uint fillerbytecount = 0; - - // Try to preserve the makernote data offset - if (!makernotewritten && - !makerNoteProcessed && - makerNoteOffset != 0 && - ifdtype == IFD.EXIF && - field.Tag != ExifTag.MakerNote && - interop.Data.Length > 4 && - currentdataoffset + interop.Data.Length > makerNoteOffset && - ifd.ContainsKey(ExifTag.MakerNote)) - { - // Delay writing this field until we write the creator's note data - fieldqueue.Enqueue(field); - continue; - } - else if (field.Tag == ExifTag.MakerNote) - { - makernotewritten = true; - // We may need to write filler bytes to preserve maker note offset - if (preserveMakerNote && !makerNoteProcessed && (makerNoteOffset > currentdataoffset)) - fillerbytecount = makerNoteOffset - currentdataoffset; - else - fillerbytecount = 0; - } - - // Tag - stream.Write(conv.GetBytes(interop.TagID), 0, 2); - // Type - stream.Write(conv.GetBytes(interop.TypeID), 0, 2); - // Count - stream.Write(conv.GetBytes(interop.Count), 0, 4); - // Field data - byte[] data = interop.Data; - if (ByteOrder != BitConverterEx.SystemByteOrder && - (interop.TypeID == 3 || interop.TypeID == 4 || interop.TypeID == 9 || - interop.TypeID == 5 || interop.TypeID == 10)) - { - int vlen = 4; - if (interop.TypeID == 3) vlen = 2; - int n = data.Length / vlen; - - for (int i = 0; i < n; i++) - Array.Reverse(data, i * vlen, vlen); - } + // Read field info + var fieldoffset = ifdoffset + 2 + (12 * i); + var tag = conv.ToUInt16(header, fieldoffset); + var type = conv.ToUInt16(header, fieldoffset + 2); + var count = conv.ToUInt32(header, fieldoffset + 4); + var value = new byte[4]; + Array.Copy(header, fieldoffset + 8, value, 0, 4); // Fields containing offsets to other IFDs - // Just store their offsets, we will write the values later on when we know the lengths of IFDs - if (ifdtype == IFD.Zeroth && interop.TagID == 0x8769) - exifIFDFieldOffset = stream.Position; - else if (ifdtype == IFD.Zeroth && interop.TagID == 0x8825) - gpsIFDFieldOffset = stream.Position; - else if (ifdtype == IFD.EXIF && interop.TagID == 0xa005) - interopIFDFieldOffset = stream.Position; - else if (ifdtype == IFD.First && interop.TagID == 0x201) - thumbOffsetLocation = stream.Position; - else if (ifdtype == IFD.First && interop.TagID == 0x202) - thumbSizeLocation = stream.Position; + if (currentifd == IFD.Zeroth && tag == 0x8769) + { + var exififdpointer = (int)conv.ToUInt32(value, 0); + ifdqueue.Add(exififdpointer, IFD.EXIF); + } + else if (currentifd == IFD.Zeroth && tag == 0x8825) + { + var gpsifdpointer = (int)conv.ToUInt32(value, 0); + ifdqueue.Add(gpsifdpointer, IFD.GPS); + } + else if (currentifd == IFD.EXIF && tag == 0xa005) + { + var interopifdpointer = (int)conv.ToUInt32(value, 0); + ifdqueue.Add(interopifdpointer, IFD.Interop); + } - // Write 4 byte field value or field data - if (data.Length <= 4) + // Save the offset to maker note data + if (currentifd == IFD.EXIF && tag == 37500) { - stream.Write(data, 0, data.Length); - for (int i = data.Length; i < 4; i++) - stream.WriteByte(0); + _makerNoteOffset = conv.ToUInt32(value, 0); } - else + + // Calculate the bytes we need to read + uint baselength = 0; + if (type == 1 || type == 2 || type == 7) { - // Pointer to data area relative to TIFF header - stream.Write(conv.GetBytes(currentdataoffset + fillerbytecount), 0, 4); - // Actual data - long currentoffset = stream.Position; - stream.Seek(absolutedataoffset, SeekOrigin.Begin); - // Write filler bytes - for (int i = 0; i < fillerbytecount; i++) - stream.WriteByte(0xFF); - stream.Write(data, 0, data.Length); - stream.Seek(currentoffset, SeekOrigin.Begin); - // Increment pointers - currentdataoffset += fillerbytecount + (uint)data.Length; - absolutedataoffset += fillerbytecount + data.Length; + baselength = 1; } + else if (type == 3) + { + baselength = 2; + } + else if (type == 4 || type == 9) + { + baselength = 4; + } + else if (type == 5 || type == 10) + { + baselength = 8; + } + + var totallength = count * baselength; + + // If field value does not fit in 4 bytes + // the value field is an offset to the actual + // field value + var fieldposition = 0; + if (totallength > 4) + { + fieldposition = tiffoffset + (int)conv.ToUInt32(value, 0); + value = new byte[totallength]; + Array.Copy(header, fieldposition, value, 0, totallength); + } + + // Compressed thumbnail data + if (currentifd == IFD.First && tag == 0x201) + { + thumbtype = 0; + thumboffset = (int)conv.ToUInt32(value, 0); + } + else if (currentifd == IFD.First && tag == 0x202) + { + thumblength = (int)conv.ToUInt32(value, 0); + } + + // Uncompressed thumbnail data + if (currentifd == IFD.First && tag == 0x111) + { + thumbtype = 1; + + // Offset to first strip + if (type == 3) + { + thumboffset = conv.ToUInt16(value, 0); + } + else + { + thumboffset = (int)conv.ToUInt32(value, 0); + } + } + else if (currentifd == IFD.First && tag == 0x117) + { + thumblength = 0; + for (var j = 0; j < count; j++) + { + if (type == 3) + { + thumblength += conv.ToUInt16(value, 0); + } + else + { + thumblength += (int)conv.ToUInt32(value, 0); + } + } + } + + // Create the exif property from the interop data + ExifProperty prop = ExifPropertyFactory.Get(tag, type, count, value, ByteOrder, currentifd, Encoding); + Properties.Add(prop); } - // Offset to 1st IFD - // We will write zeros for now. This will be filled after we write all IFDs - if (ifdtype == IFD.Zeroth) - firstIFDFieldOffset = stream.Position; - stream.Write(new byte[] { 0, 0, 0, 0 }, 0, 4); - // Seek to end of IFD - stream.Seek(absolutedataoffset, SeekOrigin.Begin); - - // Write thumbnail data - if (ifdtype == IFD.First) + // 1st IFD pointer + var firstifdpointer = (int)conv.ToUInt32(header, ifdoffset + 2 + (12 * fieldcount)); + if (firstifdpointer != 0) { - if (Thumbnail != null) + ifdqueue.Add(firstifdpointer, IFD.First); + } + + // Read thumbnail + if (thumboffset != -1 && thumblength != 0 && Thumbnail == null) + { + if (thumbtype == 0) { - MemoryStream ts = new MemoryStream(); - Thumbnail.Save(ts); - ts.Close(); - byte[] thumb = ts.ToArray(); - thumbOffsetValue = (uint)(stream.Position - tiffoffset); - thumbSizeValue = (uint)thumb.Length; - stream.Write(thumb, 0, thumb.Length); - ts.Dispose(); - } - else - { - thumbOffsetValue = 0; - thumbSizeValue = 0; + using (var ts = new MemoryStream(header, tiffoffset + thumboffset, thumblength)) + { + Thumbnail = FromStream(ts); + } } } } - #endregion } + + /// + /// Replaces the contents of the APP1 section with the Exif properties. + /// + private bool WriteExifApp1(bool preserveMakerNote) + { + // Zero out IFD field offsets. We will fill those as we write the IFD sections + _exifIfdFieldOffset = 0; + _gpsIfdFieldOffset = 0; + _interopIfdFieldOffset = 0; + _firstIfdFieldOffset = 0; + + // We also do not know the location of the embedded thumbnail yet + _thumbOffsetLocation = 0; + _thumbOffsetValue = 0; + _thumbSizeLocation = 0; + _thumbSizeValue = 0; + + // Write thumbnail tags if they are missing, remove otherwise + if (Thumbnail == null) + { + Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormat); + Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormatLength); + } + else + { + if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormat)) + { + Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormat, 0)); + } + + if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormatLength)) + { + Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormatLength, 0)); + } + } + + // Which IFD sections do we have? + var ifdzeroth = new Dictionary(); + var ifdexif = new Dictionary(); + var ifdgps = new Dictionary(); + var ifdinterop = new Dictionary(); + var ifdfirst = new Dictionary(); + + foreach (ExifProperty prop in Properties) + { + switch (prop.IFD) + { + case IFD.Zeroth: + ifdzeroth.Add(prop.Tag, prop); + break; + case IFD.EXIF: + ifdexif.Add(prop.Tag, prop); + break; + case IFD.GPS: + ifdgps.Add(prop.Tag, prop); + break; + case IFD.Interop: + ifdinterop.Add(prop.Tag, prop); + break; + case IFD.First: + ifdfirst.Add(prop.Tag, prop); + break; + } + } + + // Add IFD pointers if they are missing + // We will write the pointer values later on + if (ifdexif.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) + { + ifdzeroth.Add(ExifTag.EXIFIFDPointer, new ExifUInt(ExifTag.EXIFIFDPointer, 0)); + } + + if (ifdgps.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) + { + ifdzeroth.Add(ExifTag.GPSIFDPointer, new ExifUInt(ExifTag.GPSIFDPointer, 0)); + } + + if (ifdinterop.Count != 0 && !ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) + { + ifdexif.Add(ExifTag.InteroperabilityIFDPointer, new ExifUInt(ExifTag.InteroperabilityIFDPointer, 0)); + } + + // Remove IFD pointers if IFD sections are missing + if (ifdexif.Count == 0 && ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) + { + ifdzeroth.Remove(ExifTag.EXIFIFDPointer); + } + + if (ifdgps.Count == 0 && ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) + { + ifdzeroth.Remove(ExifTag.GPSIFDPointer); + } + + if (ifdinterop.Count == 0 && ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) + { + ifdexif.Remove(ExifTag.InteroperabilityIFDPointer); + } + + if (ifdzeroth.Count == 0 && ifdgps.Count == 0 && ifdinterop.Count == 0 && ifdfirst.Count == 0 && + Thumbnail == null) + { + // Nothing to write + return false; + } + + // We will need these BitConverters to write byte-ordered data + var bceExif = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + + // Create a memory stream to write the APP1 section to + var ms = new MemoryStream(); + + // Exif identifier + ms.Write(Encoding.ASCII.GetBytes("Exif\0\0"), 0, 6); + + // TIFF header + // Byte order + var tiffoffset = ms.Position; + ms.Write(ByteOrder == BitConverterEx.ByteOrder.LittleEndian ? new byte[] { 0x49, 0x49 } : new byte[] { 0x4D, 0x4D }, 0, 2); + + // TIFF ID + ms.Write(bceExif.GetBytes((ushort)42), 0, 2); + + // Offset to 0th IFD + ms.Write(bceExif.GetBytes((uint)8), 0, 4); + + // Write IFDs + WriteIFD(ms, ifdzeroth, IFD.Zeroth, tiffoffset, preserveMakerNote); + var exififdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdexif, IFD.EXIF, tiffoffset, preserveMakerNote); + var gpsifdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdgps, IFD.GPS, tiffoffset, preserveMakerNote); + var interopifdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdinterop, IFD.Interop, tiffoffset, preserveMakerNote); + var firstifdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdfirst, IFD.First, tiffoffset, preserveMakerNote); + + // Now that we now the location of IFDs we can go back and write IFD offsets + if (_exifIfdFieldOffset != 0) + { + ms.Seek(_exifIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(exififdrelativeoffset), 0, 4); + } + + if (_gpsIfdFieldOffset != 0) + { + ms.Seek(_gpsIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(gpsifdrelativeoffset), 0, 4); + } + + if (_interopIfdFieldOffset != 0) + { + ms.Seek(_interopIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(interopifdrelativeoffset), 0, 4); + } + + if (_firstIfdFieldOffset != 0) + { + ms.Seek(_firstIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(firstifdrelativeoffset), 0, 4); + } + + // We can write thumbnail location now + if (_thumbOffsetLocation != 0) + { + ms.Seek(_thumbOffsetLocation, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(_thumbOffsetValue), 0, 4); + } + + if (_thumbSizeLocation != 0) + { + ms.Seek(_thumbSizeLocation, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(_thumbSizeValue), 0, 4); + } + + ms.Close(); + + if (_exifApp1 is not null) + { + // Return APP1 header + _exifApp1.Header = ms.ToArray(); + return true; + } + + return false; + } + + private void WriteIFD(MemoryStream stream, Dictionary ifd, IFD ifdtype, long tiffoffset, bool preserveMakerNote) + { + var conv = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + + // Create a queue of fields to write + var fieldqueue = new Queue(); + foreach (ExifProperty prop in ifd.Values) + { + if (prop.Tag != ExifTag.MakerNote) + { + fieldqueue.Enqueue(prop); + } + } + + // Push the maker note data to the end + if (ifd.ContainsKey(ExifTag.MakerNote)) + { + fieldqueue.Enqueue(ifd[ExifTag.MakerNote]); + } + + // Offset to start of field data from start of TIFF header + var dataoffset = (uint)(2 + (ifd.Count * 12) + 4 + stream.Position - tiffoffset); + var currentdataoffset = dataoffset; + var absolutedataoffset = stream.Position + (2 + (ifd.Count * 12) + 4); + + var makernotewritten = false; + + // Field count + stream.Write(conv.GetBytes((ushort)ifd.Count), 0, 2); + + // Fields + while (fieldqueue.Count != 0) + { + ExifProperty field = fieldqueue.Dequeue(); + ExifInterOperability interop = field.Interoperability; + + uint fillerbytecount = 0; + + // Try to preserve the makernote data offset + if (!makernotewritten && + !_makerNoteProcessed && + _makerNoteOffset != 0 && + ifdtype == IFD.EXIF && + field.Tag != ExifTag.MakerNote && + interop.Data.Length > 4 && + currentdataoffset + interop.Data.Length > _makerNoteOffset && + ifd.ContainsKey(ExifTag.MakerNote)) + { + // Delay writing this field until we write the creator's note data + fieldqueue.Enqueue(field); + continue; + } + + if (field.Tag == ExifTag.MakerNote) + { + makernotewritten = true; + + // We may need to write filler bytes to preserve maker note offset + if (preserveMakerNote && !_makerNoteProcessed && _makerNoteOffset > currentdataoffset) + { + fillerbytecount = _makerNoteOffset - currentdataoffset; + } + else + { + fillerbytecount = 0; + } + } + + // Tag + stream.Write(conv.GetBytes(interop.TagID), 0, 2); + + // Type + stream.Write(conv.GetBytes(interop.TypeID), 0, 2); + + // Count + stream.Write(conv.GetBytes(interop.Count), 0, 4); + + // Field data + var data = interop.Data; + if (ByteOrder != BitConverterEx.SystemByteOrder && + (interop.TypeID == 3 || interop.TypeID == 4 || interop.TypeID == 9 || + interop.TypeID == 5 || interop.TypeID == 10)) + { + var vlen = 4; + if (interop.TypeID == 3) + { + vlen = 2; + } + + var n = data.Length / vlen; + + for (var i = 0; i < n; i++) + { + Array.Reverse(data, i * vlen, vlen); + } + } + + // Fields containing offsets to other IFDs + // Just store their offsets, we will write the values later on when we know the lengths of IFDs + if (ifdtype == IFD.Zeroth && interop.TagID == 0x8769) + { + _exifIfdFieldOffset = stream.Position; + } + else if (ifdtype == IFD.Zeroth && interop.TagID == 0x8825) + { + _gpsIfdFieldOffset = stream.Position; + } + else if (ifdtype == IFD.EXIF && interop.TagID == 0xa005) + { + _interopIfdFieldOffset = stream.Position; + } + else if (ifdtype == IFD.First && interop.TagID == 0x201) + { + _thumbOffsetLocation = stream.Position; + } + else if (ifdtype == IFD.First && interop.TagID == 0x202) + { + _thumbSizeLocation = stream.Position; + } + + // Write 4 byte field value or field data + if (data.Length <= 4) + { + stream.Write(data, 0, data.Length); + for (var i = data.Length; i < 4; i++) + { + stream.WriteByte(0); + } + } + else + { + // Pointer to data area relative to TIFF header + stream.Write(conv.GetBytes(currentdataoffset + fillerbytecount), 0, 4); + + // Actual data + var currentoffset = stream.Position; + stream.Seek(absolutedataoffset, SeekOrigin.Begin); + + // Write filler bytes + for (var i = 0; i < fillerbytecount; i++) + { + stream.WriteByte(0xFF); + } + + stream.Write(data, 0, data.Length); + stream.Seek(currentoffset, SeekOrigin.Begin); + + // Increment pointers + currentdataoffset += fillerbytecount + (uint)data.Length; + absolutedataoffset += fillerbytecount + data.Length; + } + } + + // Offset to 1st IFD + // We will write zeros for now. This will be filled after we write all IFDs + if (ifdtype == IFD.Zeroth) + { + _firstIfdFieldOffset = stream.Position; + } + + stream.Write(new byte[] { 0, 0, 0, 0 }, 0, 4); + + // Seek to end of IFD + stream.Seek(absolutedataoffset, SeekOrigin.Begin); + + // Write thumbnail data + if (ifdtype == IFD.First) + { + if (Thumbnail != null) + { + var ts = new MemoryStream(); + Thumbnail.Save(ts); + ts.Close(); + var thumb = ts.ToArray(); + _thumbOffsetValue = (uint)(stream.Position - tiffoffset); + _thumbSizeValue = (uint)thumb.Length; + stream.Write(thumb, 0, thumb.Length); + ts.Dispose(); + } + else + { + _thumbOffsetValue = 0; + _thumbSizeValue = 0; + } + } + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/JPEGMarker.cs b/src/Umbraco.Core/Media/Exif/JPEGMarker.cs index a7a3b4a9b1..3912d87e82 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGMarker.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGMarker.cs @@ -1,85 +1,95 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a JPEG marker byte. +/// +internal enum JPEGMarker : byte { - /// - /// Represents a JPEG marker byte. - /// - internal enum JPEGMarker : byte - { - // Start Of Frame markers, non-differential, Huffman coding - SOF0 = 0xc0, - SOF1 = 0xc1, - SOF2 = 0xc2, - SOF3 = 0xc3, - // Start Of Frame markers, differential, Huffman coding - SOF5 = 0xc5, - SOF6 = 0xc6, - SOF7 = 0xc7, - // Start Of Frame markers, non-differential, arithmetic coding - JPG = 0xc8, - SOF9 = 0xc9, - SOF10 = 0xca, - SOF11 = 0xcb, - // Start Of Frame markers, differential, arithmetic coding - SOF13 = 0xcd, - SOF14 = 0xce, - SOF15 = 0xcf, - // Huffman table specification - DHT = 0xc4, - // Arithmetic coding conditioning specification - DAC = 0xcc, - // Restart interval termination - RST0 = 0xd0, - RST1 = 0xd1, - RST2 = 0xd2, - RST3 = 0xd3, - RST4 = 0xd4, - RST5 = 0xd5, - RST6 = 0xd6, - RST7 = 0xd7, - // Other markers - SOI = 0xd8, - EOI = 0xd9, - SOS = 0xda, - DQT = 0xdb, - DNL = 0xdc, - DRI = 0xdd, - DHP = 0xde, - EXP = 0xdf, - // application segments - APP0 = 0xe0, - APP1 = 0xe1, - APP2 = 0xe2, - APP3 = 0xe3, - APP4 = 0xe4, - APP5 = 0xe5, - APP6 = 0xe6, - APP7 = 0xe7, - APP8 = 0xe8, - APP9 = 0xe9, - APP10 = 0xea, - APP11 = 0xeb, - APP12 = 0xec, - APP13 = 0xed, - APP14 = 0xee, - APP15 = 0xef, - // JPEG extensions - JPG0 = 0xf0, - JPG1 = 0xf1, - JPG2 = 0xf2, - JPG3 = 0xf3, - JPG4 = 0xf4, - JPG5 = 0xf5, - JPG6 = 0xf6, - JPG7 = 0xf7, - JPG8 = 0xf8, - JPG9 = 0xf9, - JPG10 = 0xfa, - JPG11 = 0xfb, - JP1G2 = 0xfc, - JPG13 = 0xfd, - // Comment - COM = 0xfe, - // Temporary - TEM = 0x01, - } + // Start Of Frame markers, non-differential, Huffman coding + SOF0 = 0xc0, + SOF1 = 0xc1, + SOF2 = 0xc2, + SOF3 = 0xc3, + + // Start Of Frame markers, differential, Huffman coding + SOF5 = 0xc5, + SOF6 = 0xc6, + SOF7 = 0xc7, + + // Start Of Frame markers, non-differential, arithmetic coding + JPG = 0xc8, + SOF9 = 0xc9, + SOF10 = 0xca, + SOF11 = 0xcb, + + // Start Of Frame markers, differential, arithmetic coding + SOF13 = 0xcd, + SOF14 = 0xce, + SOF15 = 0xcf, + + // Huffman table specification + DHT = 0xc4, + + // Arithmetic coding conditioning specification + DAC = 0xcc, + + // Restart interval termination + RST0 = 0xd0, + RST1 = 0xd1, + RST2 = 0xd2, + RST3 = 0xd3, + RST4 = 0xd4, + RST5 = 0xd5, + RST6 = 0xd6, + RST7 = 0xd7, + + // Other markers + SOI = 0xd8, + EOI = 0xd9, + SOS = 0xda, + DQT = 0xdb, + DNL = 0xdc, + DRI = 0xdd, + DHP = 0xde, + EXP = 0xdf, + + // application segments + APP0 = 0xe0, + APP1 = 0xe1, + APP2 = 0xe2, + APP3 = 0xe3, + APP4 = 0xe4, + APP5 = 0xe5, + APP6 = 0xe6, + APP7 = 0xe7, + APP8 = 0xe8, + APP9 = 0xe9, + APP10 = 0xea, + APP11 = 0xeb, + APP12 = 0xec, + APP13 = 0xed, + APP14 = 0xee, + APP15 = 0xef, + + // JPEG extensions + JPG0 = 0xf0, + JPG1 = 0xf1, + JPG2 = 0xf2, + JPG3 = 0xf3, + JPG4 = 0xf4, + JPG5 = 0xf5, + JPG6 = 0xf6, + JPG7 = 0xf7, + JPG8 = 0xf8, + JPG9 = 0xf9, + JPG10 = 0xfa, + JPG11 = 0xfb, + JP1G2 = 0xfc, + JPG13 = 0xfd, + + // Comment + COM = 0xfe, + + // Temporary + TEM = 0x01, } diff --git a/src/Umbraco.Core/Media/Exif/JPEGSection.cs b/src/Umbraco.Core/Media/Exif/JPEGSection.cs index 07dd488384..787b04b056 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGSection.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGSection.cs @@ -1,63 +1,66 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the memory view of a JPEG section. +/// A JPEG section is the data between markers of the JPEG file. +/// +internal class JPEGSection { + #region Instance Methods + /// - /// Represents the memory view of a JPEG section. - /// A JPEG section is the data between markers of the JPEG file. + /// Returns a string representation of the current section. /// - internal class JPEGSection + /// A System.String that represents the current section. + public override string ToString() => string.Format("{0} => Header: {1} bytes, Entropy Data: {2} bytes", Marker, Header.Length, EntropyData.Length); + + #endregion + + #region Properties + + /// + /// The marker byte representing the section. + /// + public JPEGMarker Marker { get; } + + /// + /// Section header as a byte array. This is different from the header + /// definition in JPEG specification in that it does not include the + /// two byte section length. + /// + public byte[] Header { get; set; } + + /// + /// For the SOS and RST markers, this contains the entropy coded data. + /// + public byte[] EntropyData { get; set; } + + #endregion + + #region Constructors + + /// + /// Constructs a JPEGSection represented by the marker byte and containing + /// the given data. + /// + /// The marker byte representing the section. + /// Section data. + /// Entropy coded data. + public JPEGSection(JPEGMarker marker, byte[] data, byte[] entropydata) { - #region Properties - /// - /// The marker byte representing the section. - /// - public JPEGMarker Marker { get; private set; } - /// - /// Section header as a byte array. This is different from the header - /// definition in JPEG specification in that it does not include the - /// two byte section length. - /// - public byte[] Header { get; set; } - /// - /// For the SOS and RST markers, this contains the entropy coded data. - /// - public byte[] EntropyData { get; set; } - #endregion - - #region Constructors - /// - /// Constructs a JPEGSection represented by the marker byte and containing - /// the given data. - /// - /// The marker byte representing the section. - /// Section data. - /// Entropy coded data. - public JPEGSection(JPEGMarker marker, byte[] data, byte[] entropydata) - { - Marker = marker; - Header = data; - EntropyData = entropydata; - } - - /// - /// Constructs a JPEGSection represented by the marker byte. - /// - /// The marker byte representing the section. - public JPEGSection(JPEGMarker marker) - : this(marker, new byte[0], new byte[0]) - { - - } - #endregion - - #region Instance Methods - /// - /// Returns a string representation of the current section. - /// - /// A System.String that represents the current section. - public override string ToString() - { - return string.Format("{0} => Header: {1} bytes, Entropy Data: {2} bytes", Marker, Header.Length, EntropyData.Length); - } - #endregion + Marker = marker; + Header = data; + EntropyData = entropydata; } + + /// + /// Constructs a JPEGSection represented by the marker byte. + /// + /// The marker byte representing the section. + public JPEGSection(JPEGMarker marker) + : this(marker, new byte[0], new byte[0]) + { + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/MathEx.cs b/src/Umbraco.Core/Media/Exif/MathEx.cs index d49ccf924f..fbf5f2dbde 100644 --- a/src/Umbraco.Core/Media/Exif/MathEx.cs +++ b/src/Umbraco.Core/Media/Exif/MathEx.cs @@ -1,1370 +1,1329 @@ -using System; -using System.Globalization; +using System.Globalization; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Contains extended Math functions. +/// +internal static class MathEx { /// - /// Contains extended Math functions. + /// Returns the greatest common divisor of two numbers. /// - internal static class MathEx + /// First number. + /// Second number. + public static uint GCD(uint a, uint b) { - /// - /// Returns the greatest common divisor of two numbers. - /// - /// First number. - /// Second number. - public static uint GCD(uint a, uint b) + while (b != 0) { - while (b != 0) - { - uint rem = a % b; - a = b; - b = rem; - } - - return a; + var rem = a % b; + a = b; + b = rem; } - /// - /// Returns the greatest common divisor of two numbers. - /// - /// First number. - /// Second number. - public static ulong GCD(ulong a, ulong b) - { - while (b != 0) - { - ulong rem = a % b; - a = b; - b = rem; - } + return a; + } - return a; + /// + /// Returns the greatest common divisor of two numbers. + /// + /// First number. + /// Second number. + public static ulong GCD(ulong a, ulong b) + { + while (b != 0) + { + var rem = a % b; + a = b; + b = rem; } - /// - /// Represents a generic rational number represented by 32-bit signed numerator and denominator. - /// - public struct Fraction32 : IComparable, IFormattable, IComparable, IEquatable - { - #region Constants - private const uint MaximumIterations = 10000000; - #endregion + return a; + } - #region Member Variables - private bool mIsNegative; - private int mNumerator; - private int mDenominator; - private double mError; - #endregion + /// + /// Represents a generic rational number represented by 32-bit signed numerator and denominator. + /// + public struct Fraction32 : IComparable, IFormattable, IComparable, IEquatable + { + #region Constants - #region Properties - /// - /// Gets or sets the numerator. - /// - public int Numerator - { - get - { - return (mIsNegative ? -1 : 1) * mNumerator; - } - set - { - if (value < 0) - { - mIsNegative = true; - mNumerator = -1 * value; - } - else - { - mIsNegative = false; - mNumerator = value; - } - Reduce(ref mNumerator, ref mDenominator); - } - } - /// - /// Gets or sets the denominator. - /// - public int Denominator - { - get - { - return ((int)mDenominator); - } - set - { - mDenominator = System.Math.Abs(value); - Reduce(ref mNumerator, ref mDenominator); - } - } + private const uint MaximumIterations = 10000000; - /// - /// Gets the error term. - /// - public double Error - { - get - { - return mError; - } - } + #endregion - /// - /// Gets or sets a value determining id the fraction is a negative value. - /// - public bool IsNegative - { - get - { - return mIsNegative; - } - set - { - mIsNegative = value; - } - } - #endregion + #region Member Variables - #region Predefined Values - public static readonly Fraction32 NaN = new Fraction32(0, 0); - public static readonly Fraction32 NegativeInfinity = new Fraction32(-1, 0); - public static readonly Fraction32 PositiveInfinity = new Fraction32(1, 0); - #endregion + private int mNumerator; + private int mDenominator; - #region Static Methods - /// - /// Returns a value indicating whether the specified number evaluates to a value - /// that is not a number. - /// - /// A fraction. - /// true if f evaluates to Fraction.NaN; otherwise, false. - public static bool IsNan(Fraction32 f) - { - return (f.Numerator == 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to negative - /// infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.NegativeInfinity; otherwise, false. - public static bool IsNegativeInfinity(Fraction32 f) - { - return (f.Numerator < 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to positive - /// infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.PositiveInfinity; otherwise, false. - public static bool IsPositiveInfinity(Fraction32 f) - { - return (f.Numerator > 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to negative - /// or positive infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.NegativeInfinity or Fraction.PositiveInfinity; otherwise, false. - public static bool IsInfinity(Fraction32 f) - { - return (f.Denominator == 0); - } - /// - /// Returns the multiplicative inverse of a given value. - /// - /// A fraction. - /// Multiplicative inverse of f. - public static Fraction32 Inverse(Fraction32 f) - { - return new Fraction32(f.Denominator, f.Numerator); - } + #endregion - /// - /// Converts the string representation of a fraction to a fraction object. - /// - /// A string formatted as numerator/denominator - /// A fraction object converted from s. - /// s is null - /// s is not in the correct format - /// - /// s represents a number less than System.UInt32.MinValue or greater than - /// System.UInt32.MaxValue. - /// - public static Fraction32 Parse(string s) - { - return FromString(s); - } - - /// - /// Converts the string representation of a fraction to a fraction object. - /// A return value indicates whether the conversion succeeded. - /// - /// A string formatted as numerator/denominator - /// true if s was converted successfully; otherwise, false. - public static bool TryParse(string s, out Fraction32 f) - { - try - { - f = Parse(s); - return true; - } - catch - { - f = new Fraction32(); - return false; - } - } - #endregion - - #region Operators - #region Arithmetic Operators - // Multiplication - public static Fraction32 operator *(Fraction32 f, int n) - { - return new Fraction32(f.Numerator * n, f.Denominator * System.Math.Abs(n)); - } - public static Fraction32 operator *(int n, Fraction32 f) - { - return f * n; - } - public static Fraction32 operator *(Fraction32 f, float n) - { - return new Fraction32(((float)f) * n); - } - public static Fraction32 operator *(float n, Fraction32 f) - { - return f * n; - } - public static Fraction32 operator *(Fraction32 f, double n) - { - return new Fraction32(((double)f) * n); - } - public static Fraction32 operator *(double n, Fraction32 f) - { - return f * n; - } - public static Fraction32 operator *(Fraction32 f1, Fraction32 f2) - { - return new Fraction32(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); - } - // Division - public static Fraction32 operator /(Fraction32 f, int n) - { - return new Fraction32(f.Numerator / n, f.Denominator / System.Math.Abs(n)); - } - public static Fraction32 operator /(Fraction32 f, float n) - { - return new Fraction32(((float)f) / n); - } - public static Fraction32 operator /(Fraction32 f, double n) - { - return new Fraction32(((double)f) / n); - } - public static Fraction32 operator /(Fraction32 f1, Fraction32 f2) - { - return f1 * Inverse(f2); - } - // Addition - public static Fraction32 operator +(Fraction32 f, int n) - { - return f + new Fraction32(n, 1); - } - public static Fraction32 operator +(int n, Fraction32 f) - { - return f + n; - } - public static Fraction32 operator +(Fraction32 f, float n) - { - return new Fraction32(((float)f) + n); - } - public static Fraction32 operator +(float n, Fraction32 f) - { - return f + n; - } - public static Fraction32 operator +(Fraction32 f, double n) - { - return new Fraction32(((double)f) + n); - } - public static Fraction32 operator +(double n, Fraction32 f) - { - return f + n; - } - public static Fraction32 operator +(Fraction32 f1, Fraction32 f2) - { - int n1 = f1.Numerator, d1 = f1.Denominator; - int n2 = f2.Numerator, d2 = f2.Denominator; - - return new Fraction32(n1 * d2 + n2 * d1, d1 * d2); - } - // Subtraction - public static Fraction32 operator -(Fraction32 f, int n) - { - return f - new Fraction32(n, 1); - } - public static Fraction32 operator -(int n, Fraction32 f) - { - return new Fraction32(n, 1) - f; - } - public static Fraction32 operator -(Fraction32 f, float n) - { - return new Fraction32(((float)f) - n); - } - public static Fraction32 operator -(float n, Fraction32 f) - { - return new Fraction32(n) - f; - } - public static Fraction32 operator -(Fraction32 f, double n) - { - return new Fraction32(((double)f) - n); - } - public static Fraction32 operator -(double n, Fraction32 f) - { - return new Fraction32(n) - f; - } - public static Fraction32 operator -(Fraction32 f1, Fraction32 f2) - { - int n1 = f1.Numerator, d1 = f1.Denominator; - int n2 = f2.Numerator, d2 = f2.Denominator; - - return new Fraction32(n1 * d2 - n2 * d1, d1 * d2); - } - // Increment - public static Fraction32 operator ++(Fraction32 f) - { - return f + new Fraction32(1, 1); - } - // Decrement - public static Fraction32 operator --(Fraction32 f) - { - return f - new Fraction32(1, 1); - } - #endregion - #region Casts To Integral Types - public static explicit operator int(Fraction32 f) - { - return f.Numerator / f.Denominator; - } - public static explicit operator float(Fraction32 f) - { - return ((float)f.Numerator) / ((float)f.Denominator); - } - public static explicit operator double(Fraction32 f) - { - return ((double)f.Numerator) / ((double)f.Denominator); - } - #endregion - #region Comparison Operators - public static bool operator ==(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator == f2.Numerator) && (f1.Denominator == f2.Denominator); - } - public static bool operator !=(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator != f2.Numerator) || (f1.Denominator != f2.Denominator); - } - public static bool operator <(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator * f2.Denominator) < (f2.Numerator * f1.Denominator); - } - public static bool operator >(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator * f2.Denominator) > (f2.Numerator * f1.Denominator); - } - #endregion - #endregion - - #region Constructors - private Fraction32(int numerator, int denominator, double error) - { - mIsNegative = false; - if (numerator < 0) - { - numerator = -numerator; - mIsNegative = !mIsNegative; - } - if (denominator < 0) - { - denominator = -denominator; - mIsNegative = !mIsNegative; - } - - mNumerator = numerator; - mDenominator = denominator; - mError = error; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - public Fraction32(int numerator, int denominator) - : this(numerator, denominator, 0) - { - - } - - public Fraction32(int numerator) - : this(numerator, (int)1) - { - - } - - public Fraction32(Fraction32 f) - : this(f.Numerator, f.Denominator, f.Error) - { - - } - - public Fraction32(float value) - : this((double)value) - { - - } - - public Fraction32(double value) - : this(FromDouble(value)) - { - - } - - public Fraction32(string s) - : this(FromString(s)) - { - - } - #endregion - - #region Instance Methods - /// - /// Sets the value of this instance to the fraction represented - /// by the given numerator and denominator. - /// - /// The new numerator. - /// The new denominator. - public void Set(int numerator, int denominator) - { - mIsNegative = false; - if (numerator < 0) - { - mIsNegative = !mIsNegative; - numerator = -numerator; - } - if (denominator < 0) - { - mIsNegative = !mIsNegative; - denominator = -denominator; - } - - mNumerator = numerator; - mDenominator = denominator; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another object to compare to. - /// true if obj and this instance are the same type and represent - /// the same value; otherwise, false. - public override bool Equals(object? obj) - { - if (obj == null) - return false; - - if (obj is Fraction32) - return Equals((Fraction32)obj); - else - return false; - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another fraction object to compare to. - /// true if obj and this instance represent the same value; - /// otherwise, false. - public bool Equals(Fraction32 obj) - { - return (mIsNegative == obj.IsNegative) && (mNumerator == obj.Numerator) && (mDenominator == obj.Denominator); - } - - /// - /// Returns the hash code for this instance. - /// - /// A 32-bit signed integer that is the hash code for this instance. - public override int GetHashCode() - { - return mDenominator ^ ((mIsNegative ? -1 : 1) * mNumerator); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by format and provider. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string? format, IFormatProvider? formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString(format, formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format, formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// The string representation of the value of this instance as - /// specified by format. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string format) - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString(format)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by provider. - /// - public string ToString(IFormatProvider formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString(formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A string formatted as numerator/denominator. - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString()); - sb.Append('/'); - sb.Append(mDenominator.ToString()); - return sb.ToString(); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An object to compare, or null. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - /// obj is not a Fraction. - public int CompareTo(object? obj) - { - if (!(obj is Fraction32)) - throw new ArgumentException("obj must be of type Fraction", "obj"); - - return CompareTo((Fraction32)obj); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An fraction to compare with this instance. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - public int CompareTo(Fraction32 obj) - { - if (this < obj) - return -1; - else if (this > obj) - return 1; - return 0; - } - #endregion - - #region Private Helper Methods - /// - /// Converts the given floating-point number to its rational representation. - /// - /// The floating-point number to be converted. - /// The rational representation of value. - private static Fraction32 FromDouble(double value) - { - if (double.IsNaN(value)) - return Fraction32.NaN; - else if (double.IsNegativeInfinity(value)) - return Fraction32.NegativeInfinity; - else if (double.IsPositiveInfinity(value)) - return Fraction32.PositiveInfinity; - - bool isneg = (value < 0); - if (isneg) value = -value; - - double f = value; - double forg = f; - int lnum = 0; - int lden = 1; - int num = 1; - int den = 0; - double lasterr = 1.0; - int a = 0; - int currIteration = 0; - while (true) - { - if (++currIteration > MaximumIterations) break; - - a = (int)Math.Floor(f); - f = f - (double)a; - if (Math.Abs(f) < double.Epsilon) - break; - f = 1.0 / f; - if (double.IsInfinity(f)) - break; - int cnum = num * a + lnum; - int cden = den * a + lden; - if (Math.Abs((double)cnum / (double)cden - forg) < double.Epsilon) - break; - double err = ((double)cnum / (double)cden - (double)num / (double)den) / ((double)num / (double)den); - // Are we converging? - if (err >= lasterr) - break; - lasterr = err; - lnum = num; - lden = den; - num = cnum; - den = cden; - } - - if (den > 0) - lasterr = value - ((double)num / (double)den); - else - lasterr = double.PositiveInfinity; - - return new Fraction32((isneg ? -1 : 1) * num, den, lasterr); - } - - /// Converts the string representation of a fraction to a Fraction type. - /// The input string formatted as numerator/denominator. - /// s is null. - /// s is not formatted as numerator/denominator. - /// - /// s represents numbers less than System.Int32.MinValue or greater than - /// System.Int32.MaxValue. - /// - private static Fraction32 FromString(string s) - { - if (s == null) - throw new ArgumentNullException("s"); - - string[] sa = s.Split(Constants.CharArrays.ForwardSlash); - int numerator = 1; - int denominator = 1; - - if (sa.Length == 1) - { - // Try to parse as int - if (int.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) - { - denominator = 1; - } - else - { - // Parse as double - double dval = double.Parse(sa[0]); - return FromDouble(dval); - } - } - else if (sa.Length == 2) - { - numerator = int.Parse(sa[0], CultureInfo.InvariantCulture); - denominator = int.Parse(sa[1], CultureInfo.InvariantCulture); - } - else - throw new FormatException("The input string must be formatted as n/d where n and d are integers"); - - return new Fraction32(numerator, denominator); - } - - /// - /// Reduces the given numerator and denominator by dividing with their - /// greatest common divisor. - /// - /// numerator to be reduced. - /// denominator to be reduced. - private static void Reduce(ref int numerator, ref int denominator) - { - uint gcd = MathEx.GCD((uint)numerator, (uint)denominator); - if (gcd == 0) gcd = 1; - numerator = numerator / (int)gcd; - denominator = denominator / (int)gcd; - } - #endregion - } + #region Properties /// - /// Represents a generic rational number represented by 32-bit unsigned numerator and denominator. + /// Gets or sets the numerator. /// - public struct UFraction32 : IComparable, IFormattable, IComparable, IEquatable + public int Numerator { - #region Constants - private const uint MaximumIterations = 10000000; - #endregion - - #region Member Variables - private uint mNumerator; - private uint mDenominator; - private double mError; - #endregion - - #region Properties - /// - /// Gets or sets the numerator. - /// - public uint Numerator - { - get - { - return mNumerator; - } - set - { - mNumerator = value; - Reduce(ref mNumerator, ref mDenominator); - } - } - /// - /// Gets or sets the denominator. - /// - public uint Denominator - { - get - { - return mDenominator; - } - set - { - mDenominator = value; - Reduce(ref mNumerator, ref mDenominator); - } - } - - - /// - /// Gets the error term. - /// - public double Error - { - get - { - return mError; - } - } - #endregion - - #region Predefined Values - public static readonly UFraction32 NaN = new UFraction32(0, 0); - public static readonly UFraction32 Infinity = new UFraction32(1, 0); - #endregion - - #region Static Methods - /// - /// Returns a value indicating whether the specified number evaluates to a value - /// that is not a number. - /// - /// A fraction. - /// true if f evaluates to Fraction.NaN; otherwise, false. - public static bool IsNan(UFraction32 f) - { - return (f.Numerator == 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.Infinity; otherwise, false. - public static bool IsInfinity(UFraction32 f) - { - return (f.Denominator == 0); - } - - /// - /// Converts the string representation of a fraction to a fraction object. - /// - /// A string formatted as numerator/denominator - /// A fraction object converted from s. - /// s is null - /// s is not in the correct format - /// - /// s represents a number less than System.UInt32.MinValue or greater than - /// System.UInt32.MaxValue. - /// - public static UFraction32 Parse(string s) - { - return FromString(s); - } - - /// - /// Converts the string representation of a fraction to a fraction object. - /// A return value indicates whether the conversion succeeded. - /// - /// A string formatted as numerator/denominator - /// true if s was converted successfully; otherwise, false. - public static bool TryParse(string s, out UFraction32 f) - { - try - { - f = Parse(s); - return true; - } - catch - { - f = new UFraction32(); - return false; - } - } - #endregion - - #region Operators - #region Arithmetic Operators - // Multiplication - public static UFraction32 operator *(UFraction32 f, uint n) - { - return new UFraction32(f.Numerator * n, f.Denominator * n); - } - public static UFraction32 operator *(uint n, UFraction32 f) - { - return f * n; - } - public static UFraction32 operator *(UFraction32 f, float n) - { - return new UFraction32(((float)f) * n); - } - public static UFraction32 operator *(float n, UFraction32 f) - { - return f * n; - } - public static UFraction32 operator *(UFraction32 f, double n) - { - return new UFraction32(((double)f) * n); - } - public static UFraction32 operator *(double n, UFraction32 f) - { - return f * n; - } - public static UFraction32 operator *(UFraction32 f1, UFraction32 f2) - { - return new UFraction32(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); - } - // Division - public static UFraction32 operator /(UFraction32 f, uint n) - { - return new UFraction32(f.Numerator / n, f.Denominator / n); - } - public static UFraction32 operator /(UFraction32 f, float n) - { - return new UFraction32(((float)f) / n); - } - public static UFraction32 operator /(UFraction32 f, double n) - { - return new UFraction32(((double)f) / n); - } - public static UFraction32 operator /(UFraction32 f1, UFraction32 f2) - { - return f1 * Inverse(f2); - } - // Addition - public static UFraction32 operator +(UFraction32 f, uint n) - { - return f + new UFraction32(n, 1); - } - public static UFraction32 operator +(uint n, UFraction32 f) - { - return f + n; - } - public static UFraction32 operator +(UFraction32 f, float n) - { - return new UFraction32(((float)f) + n); - } - public static UFraction32 operator +(float n, UFraction32 f) - { - return f + n; - } - public static UFraction32 operator +(UFraction32 f, double n) - { - return new UFraction32(((double)f) + n); - } - public static UFraction32 operator +(double n, UFraction32 f) - { - return f + n; - } - public static UFraction32 operator +(UFraction32 f1, UFraction32 f2) - { - uint n1 = f1.Numerator, d1 = f1.Denominator; - uint n2 = f2.Numerator, d2 = f2.Denominator; - - return new UFraction32(n1 * d2 + n2 * d1, d1 * d2); - } - // Subtraction - public static UFraction32 operator -(UFraction32 f, uint n) - { - return f - new UFraction32(n, 1); - } - public static UFraction32 operator -(uint n, UFraction32 f) - { - return new UFraction32(n, 1) - f; - } - public static UFraction32 operator -(UFraction32 f, float n) - { - return new UFraction32(((float)f) - n); - } - public static UFraction32 operator -(float n, UFraction32 f) - { - return new UFraction32(n) - f; - } - public static UFraction32 operator -(UFraction32 f, double n) - { - return new UFraction32(((double)f) - n); - } - public static UFraction32 operator -(double n, UFraction32 f) - { - return new UFraction32(n) - f; - } - public static UFraction32 operator -(UFraction32 f1, UFraction32 f2) - { - uint n1 = f1.Numerator, d1 = f1.Denominator; - uint n2 = f2.Numerator, d2 = f2.Denominator; - - return new UFraction32(n1 * d2 - n2 * d1, d1 * d2); - } - // Increment - public static UFraction32 operator ++(UFraction32 f) - { - return f + new UFraction32(1, 1); - } - // Decrement - public static UFraction32 operator --(UFraction32 f) - { - return f - new UFraction32(1, 1); - } - #endregion - #region Casts To Integral Types - public static explicit operator uint(UFraction32 f) - { - return ((uint)f.Numerator) / ((uint)f.Denominator); - } - public static explicit operator float(UFraction32 f) - { - return ((float)f.Numerator) / ((float)f.Denominator); - } - public static explicit operator double(UFraction32 f) - { - return ((double)f.Numerator) / ((double)f.Denominator); - } - #endregion - #region Comparison Operators - public static bool operator ==(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator == f2.Numerator) && (f1.Denominator == f2.Denominator); - } - public static bool operator !=(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator != f2.Numerator) || (f1.Denominator != f2.Denominator); - } - public static bool operator <(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator * f2.Denominator) < (f2.Numerator * f1.Denominator); - } - public static bool operator >(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator * f2.Denominator) > (f2.Numerator * f1.Denominator); - } - #endregion - #endregion - - #region Constructors - public UFraction32(uint numerator, uint denominator, double error) - { - mNumerator = numerator; - mDenominator = denominator; - mError = error; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - public UFraction32(uint numerator, uint denominator) - : this(numerator, denominator, 0) - { - - } - - public UFraction32(uint numerator) - : this(numerator, (uint)1) - { - - } - - public UFraction32(UFraction32 f) - : this(f.Numerator, f.Denominator, f.Error) - { - - } - - public UFraction32(float value) - : this((double)value) - { - - } - - public UFraction32(double value) - : this(FromDouble(value)) - { - - } - - public UFraction32(string s) - : this(FromString(s)) - { - - } - #endregion - - #region Instance Methods - /// - /// Sets the value of this instance to the fraction represented - /// by the given numerator and denominator. - /// - /// The new numerator. - /// The new denominator. - public void Set(uint numerator, uint denominator) - { - mNumerator = numerator; - mDenominator = denominator; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - /// - /// Returns the multiplicative inverse of a given value. - /// - /// A fraction. - /// Multiplicative inverse of f. - public static UFraction32 Inverse(UFraction32 f) - { - return new UFraction32(f.Denominator, f.Numerator); - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another object to compare to. - /// true if obj and this instance are the same type and represent - /// the same value; otherwise, false. - public override bool Equals(object? obj) - { - if (obj == null) - return false; - - if (obj is UFraction32) - return Equals((UFraction32)obj); - else - return false; - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another fraction object to compare to. - /// true if obj and this instance represent the same value; - /// otherwise, false. - public bool Equals(UFraction32 obj) - { - return (mNumerator == obj.Numerator) && (mDenominator == obj.Denominator); - } - - /// - /// Returns the hash code for this instance. - /// - /// A 32-bit signed integer that is the hash code for this instance. - public override int GetHashCode() - { - return ((int)mDenominator) ^ ((int)mNumerator); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by format and provider. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string? format, IFormatProvider? formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(format, formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format, formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// The string representation of the value of this instance as - /// specified by format. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string format) - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(format)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by provider. - /// - public string ToString(IFormatProvider formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A string formatted as numerator/denominator. - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString()); - sb.Append('/'); - sb.Append(mDenominator.ToString()); - return sb.ToString(); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An object to compare, or null. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - /// obj is not a Fraction. - public int CompareTo(object? obj) - { - if (!(obj is UFraction32)) - throw new ArgumentException("obj must be of type UFraction32", "obj"); - - return CompareTo((UFraction32)obj); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An fraction to compare with this instance. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - public int CompareTo(UFraction32 obj) - { - if (this < obj) - return -1; - else if (this > obj) - return 1; - return 0; - } - #endregion - - #region Private Helper Methods - /// - /// Converts the given floating-point number to its rational representation. - /// - /// The floating-point number to be converted. - /// The rational representation of value. - private static UFraction32 FromDouble(double value) + get => (IsNegative ? -1 : 1) * mNumerator; + set { if (value < 0) - throw new ArgumentException("value cannot be negative.", "value"); - - if (double.IsNaN(value)) - return UFraction32.NaN; - else if (double.IsInfinity(value)) - return UFraction32.Infinity; - - double f = value; - double forg = f; - uint lnum = 0; - uint lden = 1; - uint num = 1; - uint den = 0; - double lasterr = 1.0; - uint a = 0; - int currIteration = 0; - while (true) { - if (++currIteration > MaximumIterations) break; - - a = (uint)Math.Floor(f); - f = f - (double)a; - if (Math.Abs(f) < double.Epsilon) - break; - f = 1.0 / f; - if (double.IsInfinity(f)) - break; - uint cnum = num * a + lnum; - uint cden = den * a + lden; - if (Math.Abs((double)cnum / (double)cden - forg) < double.Epsilon) - break; - double err = ((double)cnum / (double)cden - (double)num / (double)den) / ((double)num / (double)den); - // Are we converging? - if (err >= lasterr) - break; - lasterr = err; - lnum = num; - lden = den; - num = cnum; - den = cden; - } - uint fnum = num * a + lnum; - uint fden = den * a + lden; - - if (fden > 0) - lasterr = value - ((double)fnum / (double)fden); - else - lasterr = double.PositiveInfinity; - - return new UFraction32(fnum, fden, lasterr); - } - - /// Converts the string representation of a fraction to a Fraction type. - /// The input string formatted as numerator/denominator. - /// s is null. - /// s is not formatted as numerator/denominator. - /// - /// s represents numbers less than System.UInt32.MinValue or greater than - /// System.UInt32.MaxValue. - /// - private static UFraction32 FromString(string s) - { - if (s == null) - throw new ArgumentNullException("s"); - - string[] sa = s.Split(Constants.CharArrays.ForwardSlash); - uint numerator = 1; - uint denominator = 1; - - if (sa.Length == 1) - { - // Try to parse as uint - if (uint.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) - { - denominator = 1; - } - else - { - // Parse as double - double dval = double.Parse(sa[0]); - return FromDouble(dval); - } - } - else if (sa.Length == 2) - { - numerator = uint.Parse(sa[0], CultureInfo.InvariantCulture); - denominator = uint.Parse(sa[1], CultureInfo.InvariantCulture); + IsNegative = true; + mNumerator = -1 * value; } else - throw new FormatException("The input string must be formatted as n/d where n and d are integers"); + { + IsNegative = false; + mNumerator = value; + } - return new UFraction32(numerator, denominator); + Reduce(ref mNumerator, ref mDenominator); } - - /// - /// Reduces the given numerator and denominator by dividing with their - /// greatest common divisor. - /// - /// numerator to be reduced. - /// denominator to be reduced. - private static void Reduce(ref uint numerator, ref uint denominator) - { - uint gcd = MathEx.GCD(numerator, denominator); - numerator = numerator / gcd; - denominator = denominator / gcd; - } - #endregion } + + /// + /// Gets or sets the denominator. + /// + public int Denominator + { + get => mDenominator; + set + { + mDenominator = Math.Abs(value); + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Gets the error term. + /// + public double Error { get; } + + /// + /// Gets or sets a value determining id the fraction is a negative value. + /// + public bool IsNegative { get; set; } + + #endregion + + #region Predefined Values + + public static readonly Fraction32 NaN = new(0, 0); + public static readonly Fraction32 NegativeInfinity = new(-1, 0); + public static readonly Fraction32 PositiveInfinity = new(1, 0); + + #endregion + + #region Static Methods + + /// + /// Returns a value indicating whether the specified number evaluates to a value + /// that is not a number. + /// + /// A fraction. + /// true if f evaluates to Fraction.NaN; otherwise, false. + public static bool IsNan(Fraction32 f) => f.Numerator == 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to negative + /// infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.NegativeInfinity; otherwise, false. + public static bool IsNegativeInfinity(Fraction32 f) => f.Numerator < 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to positive + /// infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.PositiveInfinity; otherwise, false. + public static bool IsPositiveInfinity(Fraction32 f) => f.Numerator > 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to negative + /// or positive infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.NegativeInfinity or Fraction.PositiveInfinity; otherwise, false. + public static bool IsInfinity(Fraction32 f) => f.Denominator == 0; + + /// + /// Returns the multiplicative inverse of a given value. + /// + /// A fraction. + /// Multiplicative inverse of f. + public static Fraction32 Inverse(Fraction32 f) => new(f.Denominator, f.Numerator); + + /// + /// Converts the string representation of a fraction to a fraction object. + /// + /// A string formatted as numerator/denominator + /// A fraction object converted from s. + /// s is null + /// s is not in the correct format + /// + /// s represents a number less than System.UInt32.MinValue or greater than + /// System.UInt32.MaxValue. + /// + public static Fraction32 Parse(string s) => FromString(s); + + /// + /// Converts the string representation of a fraction to a fraction object. + /// A return value indicates whether the conversion succeeded. + /// + /// A string formatted as numerator/denominator + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string s, out Fraction32 f) + { + try + { + f = Parse(s); + return true; + } + catch + { + f = new Fraction32(); + return false; + } + } + + #endregion + + #region Operators + + #region Arithmetic Operators + + // Multiplication + public static Fraction32 operator *(Fraction32 f, int n) => new(f.Numerator * n, f.Denominator * Math.Abs(n)); + + public static Fraction32 operator *(int n, Fraction32 f) => f * n; + + public static Fraction32 operator *(Fraction32 f, float n) => new((float)f * n); + + public static Fraction32 operator *(float n, Fraction32 f) => f * n; + + public static Fraction32 operator *(Fraction32 f, double n) => new((double)f * n); + + public static Fraction32 operator *(double n, Fraction32 f) => f * n; + + public static Fraction32 operator *(Fraction32 f1, Fraction32 f2) => + new(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); + + // Division + public static Fraction32 operator /(Fraction32 f, int n) => new(f.Numerator / n, f.Denominator / Math.Abs(n)); + + public static Fraction32 operator /(Fraction32 f, float n) => new((float)f / n); + + public static Fraction32 operator /(Fraction32 f, double n) => new((double)f / n); + + public static Fraction32 operator /(Fraction32 f1, Fraction32 f2) => f1 * Inverse(f2); + + // Addition + public static Fraction32 operator +(Fraction32 f, int n) => f + new Fraction32(n, 1); + + public static Fraction32 operator +(int n, Fraction32 f) => f + n; + + public static Fraction32 operator +(Fraction32 f, float n) => new((float)f + n); + + public static Fraction32 operator +(float n, Fraction32 f) => f + n; + + public static Fraction32 operator +(Fraction32 f, double n) => new((double)f + n); + + public static Fraction32 operator +(double n, Fraction32 f) => f + n; + + public static Fraction32 operator +(Fraction32 f1, Fraction32 f2) + { + int n1 = f1.Numerator, d1 = f1.Denominator; + int n2 = f2.Numerator, d2 = f2.Denominator; + + return new Fraction32((n1 * d2) + (n2 * d1), d1 * d2); + } + + // Subtraction + public static Fraction32 operator -(Fraction32 f, int n) => f - new Fraction32(n, 1); + + public static Fraction32 operator -(int n, Fraction32 f) => new Fraction32(n, 1) - f; + + public static Fraction32 operator -(Fraction32 f, float n) => new((float)f - n); + + public static Fraction32 operator -(float n, Fraction32 f) => new Fraction32(n) - f; + + public static Fraction32 operator -(Fraction32 f, double n) => new((double)f - n); + + public static Fraction32 operator -(double n, Fraction32 f) => new Fraction32(n) - f; + + public static Fraction32 operator -(Fraction32 f1, Fraction32 f2) + { + int n1 = f1.Numerator, d1 = f1.Denominator; + int n2 = f2.Numerator, d2 = f2.Denominator; + + return new Fraction32((n1 * d2) - (n2 * d1), d1 * d2); + } + + // Increment + public static Fraction32 operator ++(Fraction32 f) => f + new Fraction32(1, 1); + + // Decrement + public static Fraction32 operator --(Fraction32 f) => f - new Fraction32(1, 1); + + #endregion + + #region Casts To Integral Types + + public static explicit operator int(Fraction32 f) => f.Numerator / f.Denominator; + + public static explicit operator float(Fraction32 f) => f.Numerator / (float)f.Denominator; + + public static explicit operator double(Fraction32 f) => f.Numerator / (double)f.Denominator; + + #endregion + + #region Comparison Operators + + public static bool operator ==(Fraction32 f1, Fraction32 f2) => + f1.Numerator == f2.Numerator && f1.Denominator == f2.Denominator; + + public static bool operator !=(Fraction32 f1, Fraction32 f2) => + f1.Numerator != f2.Numerator || f1.Denominator != f2.Denominator; + + public static bool operator <(Fraction32 f1, Fraction32 f2) => + f1.Numerator * f2.Denominator < f2.Numerator * f1.Denominator; + + public static bool operator >(Fraction32 f1, Fraction32 f2) => + f1.Numerator * f2.Denominator > f2.Numerator * f1.Denominator; + + #endregion + + #endregion + + #region Constructors + + private Fraction32(int numerator, int denominator, double error) + { + IsNegative = false; + if (numerator < 0) + { + numerator = -numerator; + IsNegative = !IsNegative; + } + + if (denominator < 0) + { + denominator = -denominator; + IsNegative = !IsNegative; + } + + mNumerator = numerator; + mDenominator = denominator; + Error = error; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + public Fraction32(int numerator, int denominator) + : this(numerator, denominator, 0) + { + } + + public Fraction32(int numerator) + : this(numerator, 1) + { + } + + public Fraction32(Fraction32 f) + : this(f.Numerator, f.Denominator, f.Error) + { + } + + public Fraction32(float value) + : this((double)value) + { + } + + public Fraction32(double value) + : this(FromDouble(value)) + { + } + + public Fraction32(string s) + : this(FromString(s)) + { + } + + #endregion + + #region Instance Methods + + /// + /// Sets the value of this instance to the fraction represented + /// by the given numerator and denominator. + /// + /// The new numerator. + /// The new denominator. + public void Set(int numerator, int denominator) + { + IsNegative = false; + if (numerator < 0) + { + IsNegative = !IsNegative; + numerator = -numerator; + } + + if (denominator < 0) + { + IsNegative = !IsNegative; + denominator = -denominator; + } + + mNumerator = numerator; + mDenominator = denominator; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another object to compare to. + /// + /// true if obj and this instance are the same type and represent + /// the same value; otherwise, false. + /// + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is Fraction32) + { + return Equals((Fraction32)obj); + } + + return false; + } + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another fraction object to compare to. + /// + /// true if obj and this instance represent the same value; + /// otherwise, false. + /// + public bool Equals(Fraction32 obj) => IsNegative == obj.IsNegative && mNumerator == obj.Numerator && + mDenominator == obj.Denominator; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer that is the hash code for this instance. + public override int GetHashCode() => mDenominator ^ ((IsNegative ? -1 : 1) * mNumerator); + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by format and provider. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString(format, formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format, formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// The string representation of the value of this instance as + /// specified by format. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string format) + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString(format)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by provider. + /// + public string ToString(IFormatProvider formatProvider) + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString(formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A string formatted as numerator/denominator. + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString()); + sb.Append('/'); + sb.Append(mDenominator.ToString()); + return sb.ToString(); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An object to compare, or null. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + /// obj is not a Fraction. + public int CompareTo(object? obj) + { + if (!(obj is Fraction32)) + { + throw new ArgumentException("obj must be of type Fraction", "obj"); + } + + return CompareTo((Fraction32)obj); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An fraction to compare with this instance. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + public int CompareTo(Fraction32 obj) + { + if (this < obj) + { + return -1; + } + + if (this > obj) + { + return 1; + } + + return 0; + } + + #endregion + + #region Private Helper Methods + + /// + /// Converts the given floating-point number to its rational representation. + /// + /// The floating-point number to be converted. + /// The rational representation of value. + private static Fraction32 FromDouble(double value) + { + if (double.IsNaN(value)) + { + return NaN; + } + + if (double.IsNegativeInfinity(value)) + { + return NegativeInfinity; + } + + if (double.IsPositiveInfinity(value)) + { + return PositiveInfinity; + } + + var isneg = value < 0; + if (isneg) + { + value = -value; + } + + var f = value; + var forg = f; + var lnum = 0; + var lden = 1; + var num = 1; + var den = 0; + var lasterr = 1.0; + var a = 0; + var currIteration = 0; + while (true) + { + if (++currIteration > MaximumIterations) + { + break; + } + + a = (int)Math.Floor(f); + f = f - a; + if (Math.Abs(f) < double.Epsilon) + { + break; + } + + f = 1.0 / f; + if (double.IsInfinity(f)) + { + break; + } + + var cnum = (num * a) + lnum; + var cden = (den * a) + lden; + if (Math.Abs((cnum / (double)cden) - forg) < double.Epsilon) + { + break; + } + + var err = ((cnum / (double)cden) - (num / (double)den)) / (num / (double)den); + + // Are we converging? + if (err >= lasterr) + { + break; + } + + lasterr = err; + lnum = num; + lden = den; + num = cnum; + den = cden; + } + + if (den > 0) + { + lasterr = value - (num / (double)den); + } + else + { + lasterr = double.PositiveInfinity; + } + + return new Fraction32((isneg ? -1 : 1) * num, den, lasterr); + } + + /// Converts the string representation of a fraction to a Fraction type. + /// The input string formatted as numerator/denominator. + /// s is null. + /// s is not formatted as numerator/denominator. + /// + /// s represents numbers less than System.Int32.MinValue or greater than + /// System.Int32.MaxValue. + /// + private static Fraction32 FromString(string s) + { + if (s == null) + { + throw new ArgumentNullException("s"); + } + + var sa = s.Split(Constants.CharArrays.ForwardSlash); + var numerator = 1; + var denominator = 1; + + if (sa.Length == 1) + { + // Try to parse as int + if (int.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) + { + denominator = 1; + } + else + { + // Parse as double + var dval = double.Parse(sa[0]); + return FromDouble(dval); + } + } + else if (sa.Length == 2) + { + numerator = int.Parse(sa[0], CultureInfo.InvariantCulture); + denominator = int.Parse(sa[1], CultureInfo.InvariantCulture); + } + else + { + throw new FormatException("The input string must be formatted as n/d where n and d are integers"); + } + + return new Fraction32(numerator, denominator); + } + + /// + /// Reduces the given numerator and denominator by dividing with their + /// greatest common divisor. + /// + /// numerator to be reduced. + /// denominator to be reduced. + private static void Reduce(ref int numerator, ref int denominator) + { + var gcd = GCD((uint)numerator, (uint)denominator); + if (gcd == 0) + { + gcd = 1; + } + + numerator = numerator / (int)gcd; + denominator = denominator / (int)gcd; + } + + #endregion + } + + /// + /// Represents a generic rational number represented by 32-bit unsigned numerator and denominator. + /// + public struct UFraction32 : IComparable, IFormattable, IComparable, IEquatable + { + #region Constants + + private const uint MaximumIterations = 10000000; + + #endregion + + #region Member Variables + + private uint mNumerator; + private uint mDenominator; + + #endregion + + #region Properties + + /// + /// Gets or sets the numerator. + /// + public uint Numerator + { + get => mNumerator; + set + { + mNumerator = value; + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Gets or sets the denominator. + /// + public uint Denominator + { + get => mDenominator; + set + { + mDenominator = value; + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Gets the error term. + /// + public double Error { get; } + + #endregion + + #region Predefined Values + + public static readonly UFraction32 NaN = new(0, 0); + public static readonly UFraction32 Infinity = new(1, 0); + + #endregion + + #region Static Methods + + /// + /// Returns a value indicating whether the specified number evaluates to a value + /// that is not a number. + /// + /// A fraction. + /// true if f evaluates to Fraction.NaN; otherwise, false. + public static bool IsNan(UFraction32 f) => f.Numerator == 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.Infinity; otherwise, false. + public static bool IsInfinity(UFraction32 f) => f.Denominator == 0; + + /// + /// Converts the string representation of a fraction to a fraction object. + /// + /// A string formatted as numerator/denominator + /// A fraction object converted from s. + /// s is null + /// s is not in the correct format + /// + /// s represents a number less than System.UInt32.MinValue or greater than + /// System.UInt32.MaxValue. + /// + public static UFraction32 Parse(string s) => FromString(s); + + /// + /// Converts the string representation of a fraction to a fraction object. + /// A return value indicates whether the conversion succeeded. + /// + /// A string formatted as numerator/denominator + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string s, out UFraction32 f) + { + try + { + f = Parse(s); + return true; + } + catch + { + f = new UFraction32(); + return false; + } + } + + #endregion + + #region Operators + + #region Arithmetic Operators + + // Multiplication + public static UFraction32 operator *(UFraction32 f, uint n) => new(f.Numerator * n, f.Denominator * n); + + public static UFraction32 operator *(uint n, UFraction32 f) => f * n; + + public static UFraction32 operator *(UFraction32 f, float n) => new((float)f * n); + + public static UFraction32 operator *(float n, UFraction32 f) => f * n; + + public static UFraction32 operator *(UFraction32 f, double n) => new((double)f * n); + + public static UFraction32 operator *(double n, UFraction32 f) => f * n; + + public static UFraction32 operator *(UFraction32 f1, UFraction32 f2) => + new(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); + + // Division + public static UFraction32 operator /(UFraction32 f, uint n) => new(f.Numerator / n, f.Denominator / n); + + public static UFraction32 operator /(UFraction32 f, float n) => new((float)f / n); + + public static UFraction32 operator /(UFraction32 f, double n) => new((double)f / n); + + public static UFraction32 operator /(UFraction32 f1, UFraction32 f2) => f1 * Inverse(f2); + + // Addition + public static UFraction32 operator +(UFraction32 f, uint n) => f + new UFraction32(n, 1); + + public static UFraction32 operator +(uint n, UFraction32 f) => f + n; + + public static UFraction32 operator +(UFraction32 f, float n) => new((float)f + n); + + public static UFraction32 operator +(float n, UFraction32 f) => f + n; + + public static UFraction32 operator +(UFraction32 f, double n) => new((double)f + n); + + public static UFraction32 operator +(double n, UFraction32 f) => f + n; + + public static UFraction32 operator +(UFraction32 f1, UFraction32 f2) + { + uint n1 = f1.Numerator, d1 = f1.Denominator; + uint n2 = f2.Numerator, d2 = f2.Denominator; + + return new UFraction32((n1 * d2) + (n2 * d1), d1 * d2); + } + + // Subtraction + public static UFraction32 operator -(UFraction32 f, uint n) => f - new UFraction32(n, 1); + + public static UFraction32 operator -(uint n, UFraction32 f) => new UFraction32(n, 1) - f; + + public static UFraction32 operator -(UFraction32 f, float n) => new((float)f - n); + + public static UFraction32 operator -(float n, UFraction32 f) => new UFraction32(n) - f; + + public static UFraction32 operator -(UFraction32 f, double n) => new((double)f - n); + + public static UFraction32 operator -(double n, UFraction32 f) => new UFraction32(n) - f; + + public static UFraction32 operator -(UFraction32 f1, UFraction32 f2) + { + uint n1 = f1.Numerator, d1 = f1.Denominator; + uint n2 = f2.Numerator, d2 = f2.Denominator; + + return new UFraction32((n1 * d2) - (n2 * d1), d1 * d2); + } + + // Increment + public static UFraction32 operator ++(UFraction32 f) => f + new UFraction32(1, 1); + + // Decrement + public static UFraction32 operator --(UFraction32 f) => f - new UFraction32(1, 1); + + #endregion + + #region Casts To Integral Types + + public static explicit operator uint(UFraction32 f) => f.Numerator / f.Denominator; + + public static explicit operator float(UFraction32 f) => f.Numerator / (float)f.Denominator; + public static explicit operator double(UFraction32 f) => f.Numerator / (double)f.Denominator; + + #endregion + + #region Comparison Operators + + public static bool operator ==(UFraction32 f1, UFraction32 f2) => + f1.Numerator == f2.Numerator && f1.Denominator == f2.Denominator; + + public static bool operator !=(UFraction32 f1, UFraction32 f2) => + f1.Numerator != f2.Numerator || f1.Denominator != f2.Denominator; + + public static bool operator <(UFraction32 f1, UFraction32 f2) => + f1.Numerator * f2.Denominator < f2.Numerator * f1.Denominator; + + public static bool operator >(UFraction32 f1, UFraction32 f2) => + f1.Numerator * f2.Denominator > f2.Numerator * f1.Denominator; + + #endregion + + #endregion + + #region Constructors + + public UFraction32(uint numerator, uint denominator, double error) + { + mNumerator = numerator; + mDenominator = denominator; + Error = error; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + public UFraction32(uint numerator, uint denominator) + : this(numerator, denominator, 0) + { + } + + public UFraction32(uint numerator) + : this(numerator, 1) + { + } + + public UFraction32(UFraction32 f) + : this(f.Numerator, f.Denominator, f.Error) + { + } + + public UFraction32(float value) + : this((double)value) + { + } + + public UFraction32(double value) + : this(FromDouble(value)) + { + } + + public UFraction32(string s) + : this(FromString(s)) + { + } + + #endregion + + #region Instance Methods + + /// + /// Sets the value of this instance to the fraction represented + /// by the given numerator and denominator. + /// + /// The new numerator. + /// The new denominator. + public void Set(uint numerator, uint denominator) + { + mNumerator = numerator; + mDenominator = denominator; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Returns the multiplicative inverse of a given value. + /// + /// A fraction. + /// Multiplicative inverse of f. + public static UFraction32 Inverse(UFraction32 f) => new(f.Denominator, f.Numerator); + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another object to compare to. + /// + /// true if obj and this instance are the same type and represent + /// the same value; otherwise, false. + /// + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is UFraction32) + { + return Equals((UFraction32)obj); + } + + return false; + } + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another fraction object to compare to. + /// + /// true if obj and this instance represent the same value; + /// otherwise, false. + /// + public bool Equals(UFraction32 obj) => mNumerator == obj.Numerator && mDenominator == obj.Denominator; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer that is the hash code for this instance. + public override int GetHashCode() => (int)mDenominator ^ (int)mNumerator; + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by format and provider. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString(format, formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format, formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// The string representation of the value of this instance as + /// specified by format. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string format) + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString(format)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by provider. + /// + public string ToString(IFormatProvider formatProvider) + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString(formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A string formatted as numerator/denominator. + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString()); + sb.Append('/'); + sb.Append(mDenominator.ToString()); + return sb.ToString(); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An object to compare, or null. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + /// obj is not a Fraction. + public int CompareTo(object? obj) + { + if (!(obj is UFraction32)) + { + throw new ArgumentException("obj must be of type UFraction32", "obj"); + } + + return CompareTo((UFraction32)obj); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An fraction to compare with this instance. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + public int CompareTo(UFraction32 obj) + { + if (this < obj) + { + return -1; + } + + if (this > obj) + { + return 1; + } + + return 0; + } + + #endregion + + #region Private Helper Methods + + /// + /// Converts the given floating-point number to its rational representation. + /// + /// The floating-point number to be converted. + /// The rational representation of value. + private static UFraction32 FromDouble(double value) + { + if (value < 0) + { + throw new ArgumentException("value cannot be negative.", "value"); + } + + if (double.IsNaN(value)) + { + return NaN; + } + + if (double.IsInfinity(value)) + { + return Infinity; + } + + var f = value; + var forg = f; + uint lnum = 0; + uint lden = 1; + uint num = 1; + uint den = 0; + var lasterr = 1.0; + uint a = 0; + var currIteration = 0; + while (true) + { + if (++currIteration > MaximumIterations) + { + break; + } + + a = (uint)Math.Floor(f); + f = f - a; + if (Math.Abs(f) < double.Epsilon) + { + break; + } + + f = 1.0 / f; + if (double.IsInfinity(f)) + { + break; + } + + var cnum = (num * a) + lnum; + var cden = (den * a) + lden; + if (Math.Abs((cnum / (double)cden) - forg) < double.Epsilon) + { + break; + } + + var err = ((cnum / (double)cden) - (num / (double)den)) / (num / (double)den); + + // Are we converging? + if (err >= lasterr) + { + break; + } + + lasterr = err; + lnum = num; + lden = den; + num = cnum; + den = cden; + } + + var fnum = (num * a) + lnum; + var fden = (den * a) + lden; + + if (fden > 0) + { + lasterr = value - (fnum / (double)fden); + } + else + { + lasterr = double.PositiveInfinity; + } + + return new UFraction32(fnum, fden, lasterr); + } + + /// Converts the string representation of a fraction to a Fraction type. + /// The input string formatted as numerator/denominator. + /// s is null. + /// s is not formatted as numerator/denominator. + /// + /// s represents numbers less than System.UInt32.MinValue or greater than + /// System.UInt32.MaxValue. + /// + private static UFraction32 FromString(string s) + { + if (s == null) + { + throw new ArgumentNullException("s"); + } + + var sa = s.Split(Constants.CharArrays.ForwardSlash); + uint numerator = 1; + uint denominator = 1; + + if (sa.Length == 1) + { + // Try to parse as uint + if (uint.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) + { + denominator = 1; + } + else + { + // Parse as double + var dval = double.Parse(sa[0]); + return FromDouble(dval); + } + } + else if (sa.Length == 2) + { + numerator = uint.Parse(sa[0], CultureInfo.InvariantCulture); + denominator = uint.Parse(sa[1], CultureInfo.InvariantCulture); + } + else + { + throw new FormatException("The input string must be formatted as n/d where n and d are integers"); + } + + return new UFraction32(numerator, denominator); + } + + /// + /// Reduces the given numerator and denominator by dividing with their + /// greatest common divisor. + /// + /// numerator to be reduced. + /// denominator to be reduced. + private static void Reduce(ref uint numerator, ref uint denominator) + { + var gcd = GCD(numerator, denominator); + numerator = numerator / gcd; + denominator = denominator / gcd; + } + + #endregion } } diff --git a/src/Umbraco.Core/Media/Exif/SvgFile.cs b/src/Umbraco.Core/Media/Exif/SvgFile.cs index b83aebe1fb..08326e634c 100644 --- a/src/Umbraco.Core/Media/Exif/SvgFile.cs +++ b/src/Umbraco.Core/Media/Exif/SvgFile.cs @@ -1,32 +1,31 @@ -using System.Globalization; -using System.IO; -using System.Linq; +using System.Globalization; using System.Xml.Linq; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +internal class SvgFile : ImageFile { - internal class SvgFile : ImageFile + public SvgFile(Stream fileStream) { - public SvgFile(Stream fileStream) - { - fileStream.Position = 0; + fileStream.Position = 0; - var document = XDocument.Load(fileStream); //if it throws an exception the ugly try catch in MediaFileSystem will catch it + var document = + XDocument.Load(fileStream); // if it throws an exception the ugly try catch in MediaFileSystem will catch it - var width = document.Root?.Attributes().Where(x => x.Name == "width").Select(x => x.Value).FirstOrDefault(); - var height = document.Root?.Attributes().Where(x => x.Name == "height").Select(x => x.Value).FirstOrDefault(); + var width = document.Root?.Attributes().Where(x => x.Name == "width").Select(x => x.Value).FirstOrDefault(); + var height = document.Root?.Attributes().Where(x => x.Name == "height").Select(x => x.Value).FirstOrDefault(); - Properties.Add(new ExifSInt(ExifTag.PixelYDimension, - height == null ? Constants.Conventions.Media.DefaultSize : int.Parse(height, CultureInfo.InvariantCulture))); - Properties.Add(new ExifSInt(ExifTag.PixelXDimension, - width == null ? Constants.Conventions.Media.DefaultSize : int.Parse(width, CultureInfo.InvariantCulture))); + Properties.Add(new ExifSInt( + ExifTag.PixelYDimension, + height == null ? Constants.Conventions.Media.DefaultSize : int.Parse(height, CultureInfo.InvariantCulture))); + Properties.Add(new ExifSInt( + ExifTag.PixelXDimension, + width == null ? Constants.Conventions.Media.DefaultSize : int.Parse(width, CultureInfo.InvariantCulture))); - Format = ImageFileFormat.SVG; - } - - public override void Save(Stream stream) - { - } + Format = ImageFileFormat.SVG; + } + public override void Save(Stream stream) + { } } diff --git a/src/Umbraco.Core/Media/Exif/TIFFFile.cs b/src/Umbraco.Core/Media/Exif/TIFFFile.cs index 8841e8337b..2ae27c46dc 100644 --- a/src/Umbraco.Core/Media/Exif/TIFFFile.cs +++ b/src/Umbraco.Core/Media/Exif/TIFFFile.cs @@ -1,166 +1,186 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the binary view of a TIFF file. +/// +internal class TIFFFile : ImageFile { + #region Constructor + /// - /// Represents the binary view of a TIFF file. + /// Initializes a new instance of the class from the + /// specified data stream. /// - internal class TIFFFile : ImageFile + /// A that contains image data. + /// The encoding to be used for text metadata when the source encoding is unknown. + protected internal TIFFFile(Stream stream, Encoding encoding) { - #region Properties - /// - /// Gets the TIFF header. - /// - public TIFFHeader TIFFHeader { get; private set; } - /// - /// Gets the image file directories. - /// - public List IFDs { get; private set; } - #endregion + Format = ImageFileFormat.TIFF; + IFDs = new List(); + Encoding = encoding; - #region Constructor - /// - /// Initializes a new instance of the class from the - /// specified data stream. - /// - /// A that contains image data. - /// The encoding to be used for text metadata when the source encoding is unknown. - protected internal TIFFFile(Stream stream, System.Text.Encoding encoding) + // Read the entire stream + var data = Utility.GetStreamBytes(stream); + + // Read the TIFF header + TIFFHeader = TIFFHeader.FromBytes(data, 0); + var nextIFDOffset = TIFFHeader.IFDOffset; + if (nextIFDOffset == 0) { - Format = ImageFileFormat.TIFF; - IFDs = new List(); - Encoding = encoding; - - // Read the entire stream - byte[] data = Utility.GetStreamBytes(stream); - - // Read the TIFF header - TIFFHeader = TIFFHeader.FromBytes(data, 0); - uint nextIFDOffset = TIFFHeader.IFDOffset; - if (nextIFDOffset == 0) - throw new NotValidTIFFileException("The first IFD offset is zero."); - - // Read IFDs in order - while (nextIFDOffset != 0) - { - ImageFileDirectory ifd = ImageFileDirectory.FromBytes(data, nextIFDOffset, TIFFHeader.ByteOrder); - nextIFDOffset = ifd.NextIFDOffset; - IFDs.Add(ifd); - } - - // Process IFDs - // TODO: Add support for multiple frames - foreach (ImageFileDirectoryEntry field in IFDs[0].Fields) - { - Properties.Add(ExifPropertyFactory.Get(field.Tag, field.Type, field.Count, field.Data, BitConverterEx.SystemByteOrder, IFD.Zeroth, Encoding)); - } - } - #endregion - - #region Instance Methods - /// - /// Saves the to the given stream. - /// - /// The data stream used to save the image. - public override void Save(Stream stream) - { - BitConverterEx conv = BitConverterEx.SystemEndian; - - // Write TIFF header - uint ifdoffset = 8; - // Byte order - stream.Write((BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian ? new byte[] { 0x49, 0x49 } : new byte[] { 0x4D, 0x4D }), 0, 2); - // TIFF ID - stream.Write(conv.GetBytes((ushort)42), 0, 2); - // Offset to 0th IFD, will be corrected below - stream.Write(conv.GetBytes(ifdoffset), 0, 4); - - // Write IFD sections - for (int i = 0; i < IFDs.Count; i++) - { - ImageFileDirectory ifd = IFDs[i]; - - // Save the location of IFD offset - long ifdLocation = stream.Position - 4; - - // Write strips first - byte[] stripOffsets = new byte[4 * ifd.Strips.Count]; - byte[] stripLengths = new byte[4 * ifd.Strips.Count]; - uint stripOffset = ifdoffset; - for (int j = 0; j < ifd.Strips.Count; j++) - { - byte[] stripData = ifd.Strips[j].Data; - byte[] oBytes = BitConverter.GetBytes(stripOffset); - byte[] lBytes = BitConverter.GetBytes((uint)stripData.Length); - Array.Copy(oBytes, 0, stripOffsets, 4 * j, 4); - Array.Copy(lBytes, 0, stripLengths, 4 * j, 4); - stream.Write(stripData, 0, stripData.Length); - stripOffset += (uint)stripData.Length; - } - - // Remove old strip tags - for (int j = ifd.Fields.Count - 1; j > 0; j--) - { - ushort tag = ifd.Fields[j].Tag; - if (tag == 273 || tag == 279) - ifd.Fields.RemoveAt(j); - } - // Write new strip tags - ifd.Fields.Add(new ImageFileDirectoryEntry(273, 4, (uint)ifd.Strips.Count, stripOffsets)); - ifd.Fields.Add(new ImageFileDirectoryEntry(279, 4, (uint)ifd.Strips.Count, stripLengths)); - - // Write fields after strips - ifdoffset = stripOffset; - - // Correct IFD offset - long currentLocation = stream.Position; - stream.Seek(ifdLocation, SeekOrigin.Begin); - stream.Write(conv.GetBytes(ifdoffset), 0, 4); - stream.Seek(currentLocation, SeekOrigin.Begin); - - // Offset to field data - uint dataOffset = ifdoffset + 2 + (uint)ifd.Fields.Count * 12 + 4; - - // Field count - stream.Write(conv.GetBytes((ushort)ifd.Fields.Count), 0, 2); - - // Fields - foreach (ImageFileDirectoryEntry field in ifd.Fields) - { - // Tag - stream.Write(conv.GetBytes(field.Tag), 0, 2); - // Type - stream.Write(conv.GetBytes(field.Type), 0, 2); - // Count - stream.Write(conv.GetBytes(field.Count), 0, 4); - - // Field data - byte[] data = field.Data; - if (data.Length <= 4) - { - stream.Write(data, 0, data.Length); - for (int j = data.Length; j < 4; j++) - stream.WriteByte(0); - } - else - { - stream.Write(conv.GetBytes(dataOffset), 0, 4); - long currentOffset = stream.Position; - stream.Seek(dataOffset, SeekOrigin.Begin); - stream.Write(data, 0, data.Length); - dataOffset += (uint)data.Length; - stream.Seek(currentOffset, SeekOrigin.Begin); - } - } - - // Offset to next IFD - ifdoffset = dataOffset; - stream.Write(conv.GetBytes(i == IFDs.Count - 1 ? 0 : ifdoffset), 0, 4); - } + throw new NotValidTIFFileException("The first IFD offset is zero."); } - #endregion + // Read IFDs in order + while (nextIFDOffset != 0) + { + var ifd = ImageFileDirectory.FromBytes(data, nextIFDOffset, TIFFHeader.ByteOrder); + nextIFDOffset = ifd.NextIFDOffset; + IFDs.Add(ifd); + } + + // Process IFDs + // TODO: Add support for multiple frames + foreach (ImageFileDirectoryEntry field in IFDs[0].Fields) + { + Properties.Add(ExifPropertyFactory.Get(field.Tag, field.Type, field.Count, field.Data, BitConverterEx.SystemByteOrder, IFD.Zeroth, Encoding)); + } } + + #endregion + + #region Properties + + /// + /// Gets the TIFF header. + /// + public TIFFHeader TIFFHeader { get; } + + #endregion + + #region Instance Methods + + /// + /// Saves the to the given stream. + /// + /// The data stream used to save the image. + public override void Save(Stream stream) + { + BitConverterEx conv = BitConverterEx.SystemEndian; + + // Write TIFF header + uint ifdoffset = 8; + + // Byte order + stream.Write( + BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian + ? new byte[] { 0x49, 0x49 } + : new byte[] { 0x4D, 0x4D }, + 0, + 2); + + // TIFF ID + stream.Write(conv.GetBytes((ushort)42), 0, 2); + + // Offset to 0th IFD, will be corrected below + stream.Write(conv.GetBytes(ifdoffset), 0, 4); + + // Write IFD sections + for (var i = 0; i < IFDs.Count; i++) + { + ImageFileDirectory ifd = IFDs[i]; + + // Save the location of IFD offset + var ifdLocation = stream.Position - 4; + + // Write strips first + var stripOffsets = new byte[4 * ifd.Strips.Count]; + var stripLengths = new byte[4 * ifd.Strips.Count]; + var stripOffset = ifdoffset; + for (var j = 0; j < ifd.Strips.Count; j++) + { + var stripData = ifd.Strips[j].Data; + var oBytes = BitConverter.GetBytes(stripOffset); + var lBytes = BitConverter.GetBytes((uint)stripData.Length); + Array.Copy(oBytes, 0, stripOffsets, 4 * j, 4); + Array.Copy(lBytes, 0, stripLengths, 4 * j, 4); + stream.Write(stripData, 0, stripData.Length); + stripOffset += (uint)stripData.Length; + } + + // Remove old strip tags + for (var j = ifd.Fields.Count - 1; j > 0; j--) + { + var tag = ifd.Fields[j].Tag; + if (tag == 273 || tag == 279) + { + ifd.Fields.RemoveAt(j); + } + } + + // Write new strip tags + ifd.Fields.Add(new ImageFileDirectoryEntry(273, 4, (uint)ifd.Strips.Count, stripOffsets)); + ifd.Fields.Add(new ImageFileDirectoryEntry(279, 4, (uint)ifd.Strips.Count, stripLengths)); + + // Write fields after strips + ifdoffset = stripOffset; + + // Correct IFD offset + var currentLocation = stream.Position; + stream.Seek(ifdLocation, SeekOrigin.Begin); + stream.Write(conv.GetBytes(ifdoffset), 0, 4); + stream.Seek(currentLocation, SeekOrigin.Begin); + + // Offset to field data + var dataOffset = ifdoffset + 2 + ((uint)ifd.Fields.Count * 12) + 4; + + // Field count + stream.Write(conv.GetBytes((ushort)ifd.Fields.Count), 0, 2); + + // Fields + foreach (ImageFileDirectoryEntry field in ifd.Fields) + { + // Tag + stream.Write(conv.GetBytes(field.Tag), 0, 2); + + // Type + stream.Write(conv.GetBytes(field.Type), 0, 2); + + // Count + stream.Write(conv.GetBytes(field.Count), 0, 4); + + // Field data + var data = field.Data; + if (data.Length <= 4) + { + stream.Write(data, 0, data.Length); + for (var j = data.Length; j < 4; j++) + { + stream.WriteByte(0); + } + } + else + { + stream.Write(conv.GetBytes(dataOffset), 0, 4); + var currentOffset = stream.Position; + stream.Seek(dataOffset, SeekOrigin.Begin); + stream.Write(data, 0, data.Length); + dataOffset += (uint)data.Length; + stream.Seek(currentOffset, SeekOrigin.Begin); + } + } + + // Offset to next IFD + ifdoffset = dataOffset; + stream.Write(conv.GetBytes(i == IFDs.Count - 1 ? 0 : ifdoffset), 0, 4); + } + } + + /// + /// Gets the image file directories. + /// + public List IFDs { get; } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/TIFFHeader.cs b/src/Umbraco.Core/Media/Exif/TIFFHeader.cs index ac7c503d0c..54a79d90b4 100644 --- a/src/Umbraco.Core/Media/Exif/TIFFHeader.cs +++ b/src/Umbraco.Core/Media/Exif/TIFFHeader.cs @@ -1,78 +1,98 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a TIFF Header. +/// +internal struct TIFFHeader { /// - /// Represents a TIFF Header. + /// The byte order of the image file. /// - internal struct TIFFHeader + public BitConverterEx.ByteOrder ByteOrder; + + /// + /// TIFF ID. This value should always be 42. + /// + public byte ID; + + /// + /// The offset to the first IFD section from the + /// start of the TIFF header. + /// + public uint IFDOffset; + + /// + /// The byte order of the TIFF header itself. + /// + public BitConverterEx.ByteOrder TIFFHeaderByteOrder; + + /// + /// Initializes a new instance of the struct. + /// + /// The byte order. + /// The TIFF ID. This value should always be 42. + /// + /// The offset to the first IFD section from the + /// start of the TIFF header. + /// + /// The byte order of the TIFF header itself. + public TIFFHeader(BitConverterEx.ByteOrder byteOrder, byte id, uint ifdOffset, BitConverterEx.ByteOrder headerByteOrder) { - /// - /// The byte order of the image file. - /// - public BitConverterEx.ByteOrder ByteOrder; - /// - /// TIFF ID. This value should always be 42. - /// - public byte ID; - /// - /// The offset to the first IFD section from the - /// start of the TIFF header. - /// - public uint IFDOffset; - /// - /// The byte order of the TIFF header itself. - /// - public BitConverterEx.ByteOrder TIFFHeaderByteOrder; - - /// - /// Initializes a new instance of the struct. - /// - /// The byte order. - /// The TIFF ID. This value should always be 42. - /// The offset to the first IFD section from the - /// start of the TIFF header. - /// The byte order of the TIFF header itself. - public TIFFHeader(BitConverterEx.ByteOrder byteOrder, byte id, uint ifdOffset, BitConverterEx.ByteOrder headerByteOrder) + if (id != 42) { - if (id != 42) - throw new NotValidTIFFHeader(); - - ByteOrder = byteOrder; - ID = id; - IFDOffset = ifdOffset; - TIFFHeaderByteOrder = headerByteOrder; + throw new NotValidTIFFHeader(); } - /// - /// Returns a initialized from the given byte data. - /// - /// The data. - /// The offset into . - /// A initialized from the given byte data. - public static TIFFHeader FromBytes(byte[] data, int offset) + ByteOrder = byteOrder; + ID = id; + IFDOffset = ifdOffset; + TIFFHeaderByteOrder = headerByteOrder; + } + + /// + /// Returns a initialized from the given byte data. + /// + /// The data. + /// The offset into . + /// A initialized from the given byte data. + public static TIFFHeader FromBytes(byte[] data, int offset) + { + var header = default(TIFFHeader); + + // TIFF header + if (data[offset] == 0x49 && data[offset + 1] == 0x49) { - TIFFHeader header = new TIFFHeader(); - - // TIFF header - if (data[offset] == 0x49 && data[offset + 1] == 0x49) - header.ByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (data[offset] == 0x4D && data[offset + 1] == 0x4D) - header.ByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidTIFFHeader(); - - // TIFF header may have a different byte order - if (BitConverterEx.LittleEndian.ToUInt16(data, offset + 2) == 42) - header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (BitConverterEx.BigEndian.ToUInt16(data, offset + 2) == 42) - header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidTIFFHeader(); - header.ID = 42; - - // IFD offset - header.IFDOffset = BitConverterEx.ToUInt32(data, offset + 4, header.TIFFHeaderByteOrder, BitConverterEx.SystemByteOrder); - - return header; + header.ByteOrder = BitConverterEx.ByteOrder.LittleEndian; } + else if (data[offset] == 0x4D && data[offset + 1] == 0x4D) + { + header.ByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidTIFFHeader(); + } + + // TIFF header may have a different byte order + if (BitConverterEx.LittleEndian.ToUInt16(data, offset + 2) == 42) + { + header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.LittleEndian; + } + else if (BitConverterEx.BigEndian.ToUInt16(data, offset + 2) == 42) + { + header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidTIFFHeader(); + } + + header.ID = 42; + + // IFD offset + header.IFDOffset = + BitConverterEx.ToUInt32(data, offset + 4, header.TIFFHeaderByteOrder, BitConverterEx.SystemByteOrder); + + return header; } } diff --git a/src/Umbraco.Core/Media/Exif/TIFFStrip.cs b/src/Umbraco.Core/Media/Exif/TIFFStrip.cs index 9930961e20..8bf91abde6 100644 --- a/src/Umbraco.Core/Media/Exif/TIFFStrip.cs +++ b/src/Umbraco.Core/Media/Exif/TIFFStrip.cs @@ -1,27 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents a strip of compressed image data in a TIFF file. +/// +internal class TIFFStrip { /// - /// Represents a strip of compressed image data in a TIFF file. + /// Initializes a new instance of the class. /// - internal class TIFFStrip + /// The byte array to copy strip from. + /// The offset to the beginning of strip. + /// The length of strip. + public TIFFStrip(byte[] data, uint offset, uint length) { - /// - /// Compressed image data contained in this strip. - /// - public byte[] Data { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// The byte array to copy strip from. - /// The offset to the beginning of strip. - /// The length of strip. - public TIFFStrip(byte[] data, uint offset, uint length) - { - Data = new byte[length]; - Array.Copy(data, offset, Data, 0, length); - } + Data = new byte[length]; + Array.Copy(data, offset, Data, 0, length); } + + /// + /// Compressed image data contained in this strip. + /// + public byte[] Data { get; } } diff --git a/src/Umbraco.Core/Media/Exif/Utility.cs b/src/Umbraco.Core/Media/Exif/Utility.cs index 033b97ecc7..1ce1b1cdc7 100644 --- a/src/Umbraco.Core/Media/Exif/Utility.cs +++ b/src/Umbraco.Core/Media/Exif/Utility.cs @@ -1,30 +1,29 @@ -using System.IO; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Contains utility functions. +/// +internal class Utility { /// - /// Contains utility functions. + /// Reads the entire stream and returns its contents as a byte array. /// - internal class Utility + /// The to read. + /// Contents of the as a byte array. + public static byte[] GetStreamBytes(Stream stream) { - /// - /// Reads the entire stream and returns its contents as a byte array. - /// - /// The to read. - /// Contents of the as a byte array. - public static byte[] GetStreamBytes(Stream stream) + using (var mem = new MemoryStream()) { - using (MemoryStream mem = new MemoryStream()) + stream.Seek(0, SeekOrigin.Begin); + + var b = new byte[32768]; + int r; + while ((r = stream.Read(b, 0, b.Length)) > 0) { - stream.Seek(0, SeekOrigin.Begin); - - byte[] b = new byte[32768]; - int r; - while ((r = stream.Read(b, 0, b.Length)) > 0) - mem.Write(b, 0, r); - - return mem.ToArray(); + mem.Write(b, 0, r); } + + return mem.ToArray(); } } } diff --git a/src/Umbraco.Core/Media/IEmbedProvider.cs b/src/Umbraco.Core/Media/IEmbedProvider.cs index e7937904bd..6760243ce6 100644 --- a/src/Umbraco.Core/Media/IEmbedProvider.cs +++ b/src/Umbraco.Core/Media/IEmbedProvider.cs @@ -1,25 +1,22 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Media; -namespace Umbraco.Cms.Core.Media +public interface IEmbedProvider { - public interface IEmbedProvider - { - /// - /// The OEmbed API Endpoint - /// - string ApiEndpoint { get; } + /// + /// The OEmbed API Endpoint + /// + string ApiEndpoint { get; } - /// - /// A string array of Regex patterns to match against the pasted OEmbed URL - /// - string[] UrlSchemeRegex { get; } + /// + /// A string array of Regex patterns to match against the pasted OEmbed URL + /// + string[] UrlSchemeRegex { get; } - /// - /// A collection of querystring request parameters to append to the API URL - /// - /// ?key=value&key2=value2 - Dictionary RequestParams { get; } + /// + /// A collection of querystring request parameters to append to the API URL + /// + /// ?key=value&key2=value2 + Dictionary RequestParams { get; } - string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); - } + string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); } diff --git a/src/Umbraco.Core/Media/IImageDimensionExtractor.cs b/src/Umbraco.Core/Media/IImageDimensionExtractor.cs index 2eaf632e54..67f11415d3 100644 --- a/src/Umbraco.Core/Media/IImageDimensionExtractor.cs +++ b/src/Umbraco.Core/Media/IImageDimensionExtractor.cs @@ -1,10 +1,8 @@ using System.Drawing; -using System.IO; -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +public interface IImageDimensionExtractor { - public interface IImageDimensionExtractor - { - public Size? GetDimensions(Stream? stream); - } + public Size? GetDimensions(Stream? stream); } diff --git a/src/Umbraco.Core/Media/IImageUrlGenerator.cs b/src/Umbraco.Core/Media/IImageUrlGenerator.cs index 25bb1ac899..d8fdf72005 100644 --- a/src/Umbraco.Core/Media/IImageUrlGenerator.cs +++ b/src/Umbraco.Core/Media/IImageUrlGenerator.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +/// +/// Exposes a method that generates an image URL based on the specified options. +/// +public interface IImageUrlGenerator { /// - /// Exposes a method that generates an image URL based on the specified options. + /// Gets the supported image file types/extensions. /// - public interface IImageUrlGenerator - { - /// - /// Gets the supported image file types/extensions. - /// - /// - /// The supported image file types/extensions. - /// - IEnumerable SupportedImageFileTypes { get; } + /// + /// The supported image file types/extensions. + /// + IEnumerable SupportedImageFileTypes { get; } - /// - /// Gets the image URL based on the specified . - /// - /// The image URL generation options. - /// - /// The generated image URL. - /// - string? GetImageUrl(ImageUrlGenerationOptions options); - } + /// + /// Gets the image URL based on the specified . + /// + /// The image URL generation options. + /// + /// The generated image URL. + /// + string? GetImageUrl(ImageUrlGenerationOptions options); } diff --git a/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs b/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs index 9cdc6869f4..ab904f2094 100644 --- a/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs +++ b/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs @@ -1,29 +1,31 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Media; -namespace Umbraco.Extensions -{ - public static class ImageUrlGeneratorExtensions - { - /// - /// Gets a value indicating whether the file extension corresponds to a supported image. - /// - /// The image URL generator implementation that provides detail on which image extensions are supported. - /// The file extension. - /// - /// A value indicating whether the file extension corresponds to an image. - /// - /// imageUrlGenerator - public static bool IsSupportedImageFormat(this IImageUrlGenerator imageUrlGenerator, string extension) - { - if (imageUrlGenerator == null) - { - throw new ArgumentNullException(nameof(imageUrlGenerator)); - } +namespace Umbraco.Extensions; - return string.IsNullOrWhiteSpace(extension) == false && - imageUrlGenerator.SupportedImageFileTypes.InvariantContains(extension.TrimStart(Constants.CharArrays.Period)); +public static class ImageUrlGeneratorExtensions +{ + /// + /// Gets a value indicating whether the file extension corresponds to a supported image. + /// + /// + /// The image URL generator implementation that provides detail on which image extensions + /// are supported. + /// + /// The file extension. + /// + /// A value indicating whether the file extension corresponds to an image. + /// + /// imageUrlGenerator + public static bool IsSupportedImageFormat(this IImageUrlGenerator imageUrlGenerator, string extension) + { + if (imageUrlGenerator == null) + { + throw new ArgumentNullException(nameof(imageUrlGenerator)); } + + return string.IsNullOrWhiteSpace(extension) == false && + imageUrlGenerator.SupportedImageFileTypes.InvariantContains( + extension.TrimStart(Constants.CharArrays.Period)); } } diff --git a/src/Umbraco.Core/Media/OEmbedResult.cs b/src/Umbraco.Core/Media/OEmbedResult.cs index b370efc1ae..3e4834521d 100644 --- a/src/Umbraco.Core/Media/OEmbedResult.cs +++ b/src/Umbraco.Core/Media/OEmbedResult.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +public class OEmbedResult { - public class OEmbedResult - { - public OEmbedStatus OEmbedStatus { get; set; } - public bool SupportsDimensions { get; set; } - public string? Markup { get; set; } - } + public OEmbedStatus OEmbedStatus { get; set; } + + public bool SupportsDimensions { get; set; } + + public string? Markup { get; set; } } diff --git a/src/Umbraco.Core/Media/OEmbedStatus.cs b/src/Umbraco.Core/Media/OEmbedStatus.cs index 268fc1cd0d..1903643d5e 100644 --- a/src/Umbraco.Core/Media/OEmbedStatus.cs +++ b/src/Umbraco.Core/Media/OEmbedStatus.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +public enum OEmbedStatus { - public enum OEmbedStatus - { - NotSupported, - Error, - Success - } + NotSupported, + Error, + Success, } diff --git a/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs b/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs index 0481323a4a..e89d8e159d 100644 --- a/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs @@ -1,13 +1,10 @@ -using System.IO; +namespace Umbraco.Cms.Core.Media.TypeDetector; -namespace Umbraco.Cms.Core.Media.TypeDetector +public class JpegDetector : RasterizedTypeDetector { - public class JpegDetector : RasterizedTypeDetector + public static bool IsOfType(Stream fileStream) { - public static bool IsOfType(Stream fileStream) - { - var header = GetFileHeader(fileStream); - return header != null && header[0] == 0xff && header[1] == 0xD8; - } + var header = GetFileHeader(fileStream); + return header != null && header[0] == 0xff && header[1] == 0xD8; } } diff --git a/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs b/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs index 167fbe5e0e..6f4e7a8a86 100644 --- a/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs @@ -1,20 +1,19 @@ -using System.IO; +namespace Umbraco.Cms.Core.Media.TypeDetector; -namespace Umbraco.Cms.Core.Media.TypeDetector +public abstract class RasterizedTypeDetector { - public abstract class RasterizedTypeDetector + public static byte[]? GetFileHeader(Stream fileStream) { - public static byte[]? GetFileHeader(Stream fileStream) + fileStream.Seek(0, SeekOrigin.Begin); + var header = new byte[8]; + fileStream.Seek(0, SeekOrigin.Begin); + + // Invalid header + if (fileStream.Read(header, 0, header.Length) != header.Length) { - fileStream.Seek(0, SeekOrigin.Begin); - var header = new byte[8]; - fileStream.Seek(0, SeekOrigin.Begin); - - // Invalid header - if (fileStream.Read(header, 0, header.Length) != header.Length) - return null; - - return header; + return null; } + + return header; } } diff --git a/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs b/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs index 81f13b199d..c790806b9b 100644 --- a/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs @@ -1,24 +1,22 @@ -using System.IO; using System.Xml.Linq; -namespace Umbraco.Cms.Core.Media.TypeDetector +namespace Umbraco.Cms.Core.Media.TypeDetector; + +public class SvgDetector { - public class SvgDetector + public static bool IsOfType(Stream fileStream) { - public static bool IsOfType(Stream fileStream) + var document = new XDocument(); + + try { - var document = new XDocument(); - - try - { - document = XDocument.Load(fileStream); - } - catch (System.Exception) - { - return false; - } - - return document.Root?.Name.LocalName == "svg"; + document = XDocument.Load(fileStream); } + catch (Exception) + { + return false; + } + + return document.Root?.Name.LocalName == "svg"; } } diff --git a/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs b/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs index 1eda8efe7a..5581c81a62 100644 --- a/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs @@ -1,24 +1,24 @@ -using System.IO; using System.Text; -namespace Umbraco.Cms.Core.Media.TypeDetector +namespace Umbraco.Cms.Core.Media.TypeDetector; + +public class TIFFDetector { - public class TIFFDetector + public static bool IsOfType(Stream fileStream) { - public static bool IsOfType(Stream fileStream) + var tiffHeader = GetFileHeader(fileStream); + return (tiffHeader != null && tiffHeader == "MM\x00\x2a") || tiffHeader == "II\x2a\x00"; + } + + public static string? GetFileHeader(Stream fileStream) + { + var header = RasterizedTypeDetector.GetFileHeader(fileStream); + if (header == null) { - var tiffHeader = GetFileHeader(fileStream); - return tiffHeader != null && tiffHeader == "MM\x00\x2a" || tiffHeader == "II\x2a\x00"; + return null; } - public static string? GetFileHeader(Stream fileStream) - { - var header = RasterizedTypeDetector.GetFileHeader(fileStream); - if (header == null) - return null; - - var tiffHeader = Encoding.ASCII.GetString(header, 0, 4); - return tiffHeader; - } + var tiffHeader = Encoding.ASCII.GetString(header, 0, 4); + return tiffHeader; } } diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 459866a8d9..6a5ffd23d7 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -1,162 +1,218 @@ -using System; using System.Drawing; -using System.IO; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Media -{ - /// - /// Provides methods to manage auto-fill properties for upload fields. - /// - public class UploadAutoFillProperties - { - private readonly MediaFileManager _mediaFileManager; - private readonly ILogger _logger; - private readonly IImageUrlGenerator _imageUrlGenerator; - private readonly IImageDimensionExtractor _imageDimensionExtractor; +namespace Umbraco.Cms.Core.Media; - public UploadAutoFillProperties( - MediaFileManager mediaFileManager, - ILogger logger, - IImageUrlGenerator imageUrlGenerator, - IImageDimensionExtractor imageDimensionExtractor) +/// +/// Provides methods to manage auto-fill properties for upload fields. +/// +public class UploadAutoFillProperties +{ + private readonly IImageDimensionExtractor _imageDimensionExtractor; + private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly ILogger _logger; + private readonly MediaFileManager _mediaFileManager; + + public UploadAutoFillProperties( + MediaFileManager mediaFileManager, + ILogger logger, + IImageUrlGenerator imageUrlGenerator, + IImageDimensionExtractor imageDimensionExtractor) + { + _mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _imageUrlGenerator = imageUrlGenerator ?? throw new ArgumentNullException(nameof(imageUrlGenerator)); + _imageDimensionExtractor = + imageDimensionExtractor ?? throw new ArgumentNullException(nameof(imageDimensionExtractor)); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + /// Variation language. + /// Variation segment. + public void Reset(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + { + if (content == null) { - _mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _imageUrlGenerator = imageUrlGenerator ?? throw new ArgumentNullException(nameof(imageUrlGenerator)); - _imageDimensionExtractor = imageDimensionExtractor ?? throw new ArgumentNullException(nameof(imageDimensionExtractor)); + throw new ArgumentNullException(nameof(content)); } - /// - /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. - /// - /// The content item. - /// The auto-fill configuration. - /// Variation language. - /// Variation segment. - public void Reset(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + if (autoFillConfig == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + throw new ArgumentNullException(nameof(autoFillConfig)); + } + ResetProperties(content, autoFillConfig, culture, segment); + } + + /// + /// Populates the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + /// The filesystem path to the uploaded file. + /// The parameter is the path relative to the filesystem. + /// Variation language. + /// Variation segment. + public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, string? culture, string? segment) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); + } + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace()) + { ResetProperties(content, autoFillConfig, culture, segment); } - - /// - /// Populates the auto-fill properties of a content item, for a specified auto-fill configuration. - /// - /// The content item. - /// The auto-fill configuration. - /// The filesystem path to the uploaded file. - /// The parameter is the path relative to the filesystem. - /// Variation language. - /// Variation segment. - public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, string? culture, string? segment) + else { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); - - // no file = reset, file = auto-fill - if (filepath.IsNullOrWhiteSpace()) + // it might not exist if the media item has been created programatically but doesn't have a file persisted yet. + if (_mediaFileManager.FileSystem.FileExists(filepath)) { - ResetProperties(content, autoFillConfig, culture, segment); - } - else - { - // it might not exist if the media item has been created programatically but doesn't have a file persisted yet. - if (_mediaFileManager.FileSystem.FileExists(filepath)) + // if anything goes wrong, just reset the properties + try { - // if anything goes wrong, just reset the properties - try + using (Stream filestream = _mediaFileManager.FileSystem.OpenFile(filepath)) { - using (Stream filestream = _mediaFileManager.FileSystem.OpenFile(filepath)) - { - SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); - ResetProperties(content, autoFillConfig, culture, segment); + SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); } } + catch (Exception ex) + { + _logger.LogError(ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); + ResetProperties(content, autoFillConfig, culture, segment); + } } } + } - /// - /// Populates the auto-fill properties of a content item. - /// - /// The content item. - /// - /// The filesystem-relative filepath, or null to clear properties. - /// The stream containing the file data. - /// Variation language. - /// Variation segment. - public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, string culture, string segment) + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// + /// The filesystem-relative filepath, or null to clear properties. + /// The stream containing the file data. + /// Variation language. + /// Variation segment. + public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, string culture, string segment) + { + if (content == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); - - // no file = reset, file = auto-fill - if (filepath.IsNullOrWhiteSpace() || filestream == null) - { - ResetProperties(content, autoFillConfig, culture, segment); - } - else - { - SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); - } + throw new ArgumentNullException(nameof(content)); } - private void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream? filestream, string? culture, string? segment) + if (autoFillConfig == null) { - var extension = (Path.GetExtension(filepath) ?? string.Empty).TrimStart(Constants.CharArrays.Period); - - var size = _imageUrlGenerator.IsSupportedImageFormat(extension) - ? _imageDimensionExtractor.GetDimensions(filestream) ?? (Size?)new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize) - : null; - - SetProperties(content, autoFillConfig, size, filestream?.Length, extension, culture, segment); + throw new ArgumentNullException(nameof(autoFillConfig)); } - private static void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, Size? size, long? length, string extension, string? culture, string? segment) + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace() || filestream == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + ResetProperties(content, autoFillConfig, culture, segment); + } + else + { + SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); + } + } - if (!string.IsNullOrWhiteSpace(autoFillConfig.WidthFieldAlias) && content.Properties.Contains(autoFillConfig.WidthFieldAlias)) - content.Properties[autoFillConfig.WidthFieldAlias]!.SetValue(size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty, culture, segment); - - if (!string.IsNullOrWhiteSpace(autoFillConfig.HeightFieldAlias) && content.Properties.Contains(autoFillConfig.HeightFieldAlias)) - content.Properties[autoFillConfig.HeightFieldAlias]!.SetValue(size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty, culture, segment); - - if (!string.IsNullOrWhiteSpace(autoFillConfig.LengthFieldAlias) && content.Properties.Contains(autoFillConfig.LengthFieldAlias)) - content.Properties[autoFillConfig.LengthFieldAlias]!.SetValue(length, culture, segment); - - if (!string.IsNullOrWhiteSpace(autoFillConfig.ExtensionFieldAlias) && content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) - content.Properties[autoFillConfig.ExtensionFieldAlias]!.SetValue(extension, culture, segment); + private static void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, Size? size, long? length, string extension, string? culture, string? segment) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); } - private static void ResetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + if (autoFillConfig == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + throw new ArgumentNullException(nameof(autoFillConfig)); + } - if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) - content.Properties[autoFillConfig.WidthFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.WidthFieldAlias) && + content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + { + content.Properties[autoFillConfig.WidthFieldAlias]!.SetValue( + size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty, culture, segment); + } - if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) - content.Properties[autoFillConfig.HeightFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.HeightFieldAlias) && + content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + { + content.Properties[autoFillConfig.HeightFieldAlias]!.SetValue( + size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty, culture, segment); + } - if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) - content.Properties[autoFillConfig.LengthFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.LengthFieldAlias) && + content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + { + content.Properties[autoFillConfig.LengthFieldAlias]!.SetValue(length, culture, segment); + } - if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) - content.Properties[autoFillConfig.ExtensionFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.ExtensionFieldAlias) && + content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + { + content.Properties[autoFillConfig.ExtensionFieldAlias]!.SetValue(extension, culture, segment); + } + } + + private void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream? filestream, string? culture, string? segment) + { + var extension = (Path.GetExtension(filepath) ?? string.Empty).TrimStart(Constants.CharArrays.Period); + + Size? size = _imageUrlGenerator.IsSupportedImageFormat(extension) + ? _imageDimensionExtractor.GetDimensions(filestream) ?? + (Size?)new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize) + : null; + + SetProperties(content, autoFillConfig, size, filestream?.Length, extension, culture, segment); + } + + private static void ResetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); + } + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + { + content.Properties[autoFillConfig.WidthFieldAlias]?.SetValue(string.Empty, culture, segment); + } + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + { + content.Properties[autoFillConfig.HeightFieldAlias]?.SetValue(string.Empty, culture, segment); + } + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + { + content.Properties[autoFillConfig.LengthFieldAlias]?.SetValue(string.Empty, culture, segment); + } + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + { + content.Properties[autoFillConfig.ExtensionFieldAlias]?.SetValue(string.Empty, culture, segment); } } } diff --git a/src/Umbraco.Core/Models/AnchorsModel.cs b/src/Umbraco.Core/Models/AnchorsModel.cs index 466751c82d..90faa01da1 100644 --- a/src/Umbraco.Core/Models/AnchorsModel.cs +++ b/src/Umbraco.Core/Models/AnchorsModel.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class AnchorsModel { - public class AnchorsModel - { - public string? RteContent { get; set; } - } + public string? RteContent { get; set; } } diff --git a/src/Umbraco.Core/Models/AuditEntry.cs b/src/Umbraco.Core/Models/AuditEntry.cs index e0bb52375b..9d1b4dfcef 100644 --- a/src/Umbraco.Core/Models/AuditEntry.cs +++ b/src/Umbraco.Core/Models/AuditEntry.cs @@ -1,78 +1,76 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an audited event. +/// +[Serializable] +[DataContract(IsReference = true)] +public class AuditEntry : EntityBase, IAuditEntry { - /// - /// Represents an audited event. - /// - [Serializable] - [DataContract(IsReference = true)] - public class AuditEntry : EntityBase, IAuditEntry + private string? _affectedDetails; + private int _affectedUserId; + private string? _eventDetails; + private string? _eventType; + private string? _performingDetails; + private string? _performingIp; + private int _performingUserId; + + /// + public int PerformingUserId { - private int _performingUserId; - private string? _performingDetails; - private string? _performingIp; - private int _affectedUserId; - private string? _affectedDetails; - private string? _eventType; - private string? _eventDetails; + get => _performingUserId; + set => SetPropertyValueAndDetectChanges(value, ref _performingUserId, nameof(PerformingUserId)); + } - /// - public int PerformingUserId - { - get => _performingUserId; - set => SetPropertyValueAndDetectChanges(value, ref _performingUserId, nameof(PerformingUserId)); - } + /// + public string? PerformingDetails + { + get => _performingDetails; + set => SetPropertyValueAndDetectChanges(value, ref _performingDetails, nameof(PerformingDetails)); + } - /// - public string? PerformingDetails - { - get => _performingDetails; - set => SetPropertyValueAndDetectChanges(value, ref _performingDetails, nameof(PerformingDetails)); - } + /// + public string? PerformingIp + { + get => _performingIp; + set => SetPropertyValueAndDetectChanges(value, ref _performingIp, nameof(PerformingIp)); + } - /// - public string? PerformingIp - { - get => _performingIp; - set => SetPropertyValueAndDetectChanges(value, ref _performingIp, nameof(PerformingIp)); - } + /// + public DateTime EventDateUtc + { + get => CreateDate; + set => CreateDate = value; + } - /// - public DateTime EventDateUtc - { - get => CreateDate; - set => CreateDate = value; - } + /// + public int AffectedUserId + { + get => _affectedUserId; + set => SetPropertyValueAndDetectChanges(value, ref _affectedUserId, nameof(AffectedUserId)); + } - /// - public int AffectedUserId - { - get => _affectedUserId; - set => SetPropertyValueAndDetectChanges(value, ref _affectedUserId, nameof(AffectedUserId)); - } + /// + public string? AffectedDetails + { + get => _affectedDetails; + set => SetPropertyValueAndDetectChanges(value, ref _affectedDetails, nameof(AffectedDetails)); + } - /// - public string? AffectedDetails - { - get => _affectedDetails; - set => SetPropertyValueAndDetectChanges(value, ref _affectedDetails, nameof(AffectedDetails)); - } + /// + public string? EventType + { + get => _eventType; + set => SetPropertyValueAndDetectChanges(value, ref _eventType, nameof(EventType)); + } - /// - public string? EventType - { - get => _eventType; - set => SetPropertyValueAndDetectChanges(value, ref _eventType, nameof(EventType)); - } - - /// - public string? EventDetails - { - get => _eventDetails; - set => SetPropertyValueAndDetectChanges(value, ref _eventDetails, nameof(EventDetails)); - } + /// + public string? EventDetails + { + get => _eventDetails; + set => SetPropertyValueAndDetectChanges(value, ref _eventDetails, nameof(EventDetails)); } } diff --git a/src/Umbraco.Core/Models/AuditItem.cs b/src/Umbraco.Core/Models/AuditItem.cs index 83ecad0878..bbfca724aa 100644 --- a/src/Umbraco.Core/Models/AuditItem.cs +++ b/src/Umbraco.Core/Models/AuditItem.cs @@ -1,39 +1,38 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public sealed class AuditItem : EntityBase, IAuditItem { - public sealed class AuditItem : EntityBase, IAuditItem + /// + /// Initializes a new instance of the class. + /// + public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null) { - /// - /// Initializes a new instance of the class. - /// - public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null) - { - DisableChangeTracking(); + DisableChangeTracking(); - Id = objectId; - Comment = comment; - AuditType = type; - UserId = userId; - EntityType = entityType; - Parameters = parameters; + Id = objectId; + Comment = comment; + AuditType = type; + UserId = userId; + EntityType = entityType; + Parameters = parameters; - EnableChangeTracking(); - } - - /// - public AuditType AuditType { get; } - - /// - public string? EntityType { get; } - - /// - public int UserId { get; } - - /// - public string? Comment { get; } - - /// - public string? Parameters { get; } + EnableChangeTracking(); } + + /// + public AuditType AuditType { get; } + + /// + public string? EntityType { get; } + + /// + public int UserId { get; } + + /// + public string? Comment { get; } + + /// + public string? Parameters { get; } } diff --git a/src/Umbraco.Core/Models/AuditType.cs b/src/Umbraco.Core/Models/AuditType.cs index b6a36be5ff..6a3e528273 100644 --- a/src/Umbraco.Core/Models/AuditType.cs +++ b/src/Umbraco.Core/Models/AuditType.cs @@ -1,128 +1,127 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines audit types. +/// +public enum AuditType { /// - /// Defines audit types. + /// New node(s) being added. /// - public enum AuditType - { - /// - /// New node(s) being added. - /// - New, + New, - /// - /// Node(s) being saved. - /// - Save, + /// + /// Node(s) being saved. + /// + Save, - /// - /// Variant(s) being saved. - /// - SaveVariant, + /// + /// Variant(s) being saved. + /// + SaveVariant, - /// - /// Node(s) being opened. - /// - Open, + /// + /// Node(s) being opened. + /// + Open, - /// - /// Node(s) being deleted. - /// - Delete, + /// + /// Node(s) being deleted. + /// + Delete, - /// - /// Node(s) being published. - /// - Publish, + /// + /// Node(s) being published. + /// + Publish, - /// - /// Variant(s) being published. - /// - PublishVariant, + /// + /// Variant(s) being published. + /// + PublishVariant, - /// - /// Node(s) being sent to publishing. - /// - SendToPublish, + /// + /// Node(s) being sent to publishing. + /// + SendToPublish, - /// - /// Variant(s) being sent to publishing. - /// - SendToPublishVariant, + /// + /// Variant(s) being sent to publishing. + /// + SendToPublishVariant, - /// - /// Node(s) being unpublished. - /// - Unpublish, + /// + /// Node(s) being unpublished. + /// + Unpublish, - /// - /// Variant(s) being unpublished. - /// - UnpublishVariant, + /// + /// Variant(s) being unpublished. + /// + UnpublishVariant, - /// - /// Node(s) being moved. - /// - Move, + /// + /// Node(s) being moved. + /// + Move, - /// - /// Node(s) being copied. - /// - Copy, + /// + /// Node(s) being copied. + /// + Copy, - /// - /// Node(s) being assigned domains. - /// - AssignDomain, + /// + /// Node(s) being assigned domains. + /// + AssignDomain, - /// - /// Node(s) public access changing. - /// - PublicAccess, + /// + /// Node(s) public access changing. + /// + PublicAccess, - /// - /// Node(s) being sorted. - /// - Sort, + /// + /// Node(s) being sorted. + /// + Sort, - /// - /// Notification(s) being sent to user. - /// - Notify, + /// + /// Notification(s) being sent to user. + /// + Notify, - /// - /// General system audit message. - /// - System, + /// + /// General system audit message. + /// + System, - /// - /// Node's content being rolled back to a previous version. - /// - RollBack, + /// + /// Node's content being rolled back to a previous version. + /// + RollBack, - /// - /// Package being installed. - /// - PackagerInstall, + /// + /// Package being installed. + /// + PackagerInstall, - /// - /// Package being uninstalled. - /// - PackagerUninstall, + /// + /// Package being uninstalled. + /// + PackagerUninstall, - /// - /// Custom audit message. - /// - Custom, + /// + /// Custom audit message. + /// + Custom, - /// - /// Content version preventCleanup set to true - /// - ContentVersionPreventCleanup, + /// + /// Content version preventCleanup set to true + /// + ContentVersionPreventCleanup, - /// - /// Content version preventCleanup set to false - /// - ContentVersionEnableCleanup - } + /// + /// Content version preventCleanup set to false + /// + ContentVersionEnableCleanup, } diff --git a/src/Umbraco.Core/Models/BackOfficeTour.cs b/src/Umbraco.Core/Models/BackOfficeTour.cs index d6a5d8971e..a7a9d3a5c3 100644 --- a/src/Umbraco.Core/Models/BackOfficeTour.cs +++ b/src/Umbraco.Core/Models/BackOfficeTour.cs @@ -1,47 +1,42 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing a tour. +/// +[DataContract(Name = "tour", Namespace = "")] +public class BackOfficeTour { - /// - /// A model representing a tour. - /// - [DataContract(Name = "tour", Namespace = "")] - public class BackOfficeTour - { - public BackOfficeTour() - { - RequiredSections = new List(); - } + public BackOfficeTour() => RequiredSections = new List(); - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; - [DataMember(Name = "group")] - public string? Group { get; set; } + [DataMember(Name = "group")] + public string? Group { get; set; } - [DataMember(Name = "groupOrder")] - public int GroupOrder { get; set; } + [DataMember(Name = "groupOrder")] + public int GroupOrder { get; set; } - [DataMember(Name = "hidden")] - public bool Hidden { get; set; } + [DataMember(Name = "hidden")] + public bool Hidden { get; set; } - [DataMember(Name = "allowDisable")] - public bool AllowDisable { get; set; } + [DataMember(Name = "allowDisable")] + public bool AllowDisable { get; set; } - [DataMember(Name = "requiredSections")] - public List RequiredSections { get; set; } + [DataMember(Name = "requiredSections")] + public List RequiredSections { get; set; } - [DataMember(Name = "steps")] - public BackOfficeTourStep[]? Steps { get; set; } + [DataMember(Name = "steps")] + public BackOfficeTourStep[]? Steps { get; set; } - [DataMember(Name = "culture")] - public string? Culture { get; set; } + [DataMember(Name = "culture")] + public string? Culture { get; set; } - [DataMember(Name = "contentType")] - public string? ContentType { get; set; } - } + [DataMember(Name = "contentType")] + public string? ContentType { get; set; } } diff --git a/src/Umbraco.Core/Models/BackOfficeTourFile.cs b/src/Umbraco.Core/Models/BackOfficeTourFile.cs index 21b769f94e..bc0a5cea3b 100644 --- a/src/Umbraco.Core/Models/BackOfficeTourFile.cs +++ b/src/Umbraco.Core/Models/BackOfficeTourFile.cs @@ -1,35 +1,30 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing the file used to load a tour. +/// +[DataContract(Name = "tourFile", Namespace = "")] +public class BackOfficeTourFile { + public BackOfficeTourFile() => Tours = new List(); + /// - /// A model representing the file used to load a tour. + /// The file name for the tour /// - [DataContract(Name = "tourFile", Namespace = "")] - public class BackOfficeTourFile - { - public BackOfficeTourFile() - { - Tours = new List(); - } + [DataMember(Name = "fileName")] + public string? FileName { get; set; } - /// - /// The file name for the tour - /// - [DataMember(Name = "fileName")] - public string? FileName { get; set; } + /// + /// The plugin folder that the tour comes from + /// + /// + /// If this is null it means it's a Core tour + /// + [DataMember(Name = "pluginName")] + public string? PluginName { get; set; } - /// - /// The plugin folder that the tour comes from - /// - /// - /// If this is null it means it's a Core tour - /// - [DataMember(Name = "pluginName")] - public string? PluginName { get; set; } - - [DataMember(Name = "tours")] - public IEnumerable Tours { get; set; } - } + [DataMember(Name = "tours")] + public IEnumerable Tours { get; set; } } diff --git a/src/Umbraco.Core/Models/BackOfficeTourStep.cs b/src/Umbraco.Core/Models/BackOfficeTourStep.cs index aa2aaf7f53..296dcf8bc4 100644 --- a/src/Umbraco.Core/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Core/Models/BackOfficeTourStep.cs @@ -1,34 +1,43 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing a step in a tour. +/// +[DataContract(Name = "step", Namespace = "")] +public class BackOfficeTourStep { - /// - /// A model representing a step in a tour. - /// - [DataContract(Name = "step", Namespace = "")] - public class BackOfficeTourStep - { - [DataMember(Name = "title")] - public string? Title { get; set; } - [DataMember(Name = "content")] - public string? Content { get; set; } - [DataMember(Name = "type")] - public string? Type { get; set; } - [DataMember(Name = "element")] - public string? Element { get; set; } - [DataMember(Name = "elementPreventClick")] - public bool ElementPreventClick { get; set; } - [DataMember(Name = "backdropOpacity")] - public float? BackdropOpacity { get; set; } - [DataMember(Name = "event")] - public string? Event { get; set; } - [DataMember(Name = "view")] - public string? View { get; set; } - [DataMember(Name = "eventElement")] - public string? EventElement { get; set; } - [DataMember(Name = "customProperties")] - public object? CustomProperties { get; set; } - [DataMember(Name = "skipStepIfVisible")] - public string? SkipStepIfVisible { get; set; } - } + [DataMember(Name = "title")] + public string? Title { get; set; } + + [DataMember(Name = "content")] + public string? Content { get; set; } + + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "element")] + public string? Element { get; set; } + + [DataMember(Name = "elementPreventClick")] + public bool ElementPreventClick { get; set; } + + [DataMember(Name = "backdropOpacity")] + public float? BackdropOpacity { get; set; } + + [DataMember(Name = "event")] + public string? Event { get; set; } + + [DataMember(Name = "view")] + public string? View { get; set; } + + [DataMember(Name = "eventElement")] + public string? EventElement { get; set; } + + [DataMember(Name = "customProperties")] + public object? CustomProperties { get; set; } + + [DataMember(Name = "skipStepIfVisible")] + public string? SkipStepIfVisible { get; set; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs index 400649ff05..ee158f9bd8 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs @@ -1,130 +1,126 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a layout item for the Block List editor. +/// +/// +[DataContract(Name = "block", Namespace = "")] +public class BlockListItem : IBlockReference { /// - /// Represents a layout item for the Block List editor. + /// Initializes a new instance of the class. /// - /// - [DataContract(Name = "block", Namespace = "")] - public class BlockListItem : IBlockReference + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + /// + /// contentUdi + /// or + /// content + /// + public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) { - /// - /// Initializes a new instance of the class. - /// - /// The content UDI. - /// The content. - /// The settings UDI. - /// The settings. - /// contentUdi - /// or - /// content - public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) - { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); - Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; - Settings = settings; - } - - /// - /// Gets the content UDI. - /// - /// - /// The content UDI. - /// - [DataMember(Name = "contentUdi")] - public Udi ContentUdi { get; } - - /// - /// Gets the content. - /// - /// - /// The content. - /// - [DataMember(Name = "content")] - public IPublishedElement Content { get; } - - /// - /// Gets the settings UDI. - /// - /// - /// The settings UDI. - /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } - - /// - /// Gets the settings. - /// - /// - /// The settings. - /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + SettingsUdi = settingsUdi; + Settings = settings; } /// - /// Represents a layout item with a generic content type for the Block List editor. + /// Gets the content. /// - /// The type of the content. - /// - public class BlockListItem : BlockListItem - where T : IPublishedElement - { - /// - /// Initializes a new instance of the class. - /// - /// The content UDI. - /// The content. - /// The settings UDI. - /// The settings. - public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) - : base(contentUdi, content, settingsUdi, settings) - { - Content = content; - } - - /// - /// Gets the content. - /// - /// - /// The content. - /// - public new T Content { get; } - } + /// + /// The content. + /// + [DataMember(Name = "content")] + public IPublishedElement Content { get; } /// - /// Represents a layout item with generic content and settings types for the Block List editor. + /// Gets the settings UDI. /// - /// The type of the content. - /// The type of the settings. - /// - public class BlockListItem : BlockListItem - where TContent : IPublishedElement - where TSettings : IPublishedElement - { - /// - /// Initializes a new instance of the class. - /// - /// The content udi. - /// The content. - /// The settings udi. - /// The settings. - public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) - : base(contentUdi, content, settingsUdi, settings) - { - Settings = settings; - } + /// + /// The settings UDI. + /// + [DataMember(Name = "settingsUdi")] + public Udi SettingsUdi { get; } - /// - /// Gets the settings. - /// - /// - /// The settings. - /// - public new TSettings Settings { get; } - } + /// + /// Gets the content UDI. + /// + /// + /// The content UDI. + /// + [DataMember(Name = "contentUdi")] + public Udi ContentUdi { get; } + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + [DataMember(Name = "settings")] + public IPublishedElement Settings { get; } +} + +/// +/// Represents a layout item with a generic content type for the Block List editor. +/// +/// The type of the content. +/// +public class BlockListItem : BlockListItem + where T : IPublishedElement +{ + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) + : base(contentUdi, content, settingsUdi, settings) => + Content = content; + + /// + /// Gets the content. + /// + /// + /// The content. + /// + public new T Content { get; } +} + +/// +/// Represents a layout item with generic content and settings types for the Block List editor. +/// +/// The type of the content. +/// The type of the settings. +/// +public class BlockListItem : BlockListItem + where TContent : IPublishedElement + where TSettings : IPublishedElement +{ + /// + /// Initializes a new instance of the class. + /// + /// The content udi. + /// The content. + /// The settings udi. + /// The settings. + public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) + : base(contentUdi, content, settingsUdi, settings) => + Settings = settings; + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + public new TSettings Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 33a711520b..79afb67d40 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -1,63 +1,63 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// The strongly typed model for the Block List editor. +/// +/// +[DataContract(Name = "blockList", Namespace = "")] +public class BlockListModel : ReadOnlyCollection { /// - /// The strongly typed model for the Block List editor. + /// Initializes a new instance of the class. /// - /// - [DataContract(Name = "blockList", Namespace = "")] - public class BlockListModel : ReadOnlyCollection + /// The list to wrap. + public BlockListModel(IList list) + : base(list) { - /// - /// Gets the empty . - /// - /// - /// The empty . - /// - public static BlockListModel Empty { get; } = new BlockListModel(); - - /// - /// Prevents a default instance of the class from being created. - /// - private BlockListModel() - : this(new List()) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The list to wrap. - public BlockListModel(IList list) - : base(list) - { } - - /// - /// Gets the with the specified content key. - /// - /// - /// The . - /// - /// The content key. - /// - /// The with the specified content key. - /// - public BlockListItem? this[Guid contentKey] => this.FirstOrDefault(x => x.Content.Key == contentKey); - - /// - /// Gets the with the specified content UDI. - /// - /// - /// The . - /// - /// The content UDI. - /// - /// The with the specified content UDI. - /// - public BlockListItem? this[Udi contentUdi] => contentUdi is GuidUdi guidUdi ? this.FirstOrDefault(x => x.Content.Key == guidUdi.Guid) : null; } + + /// + /// Prevents a default instance of the class from being created. + /// + private BlockListModel() + : this(new List()) + { + } + + /// + /// Gets the empty . + /// + /// + /// The empty . + /// + public static BlockListModel Empty { get; } = new(); + + /// + /// Gets the with the specified content key. + /// + /// + /// The . + /// + /// The content key. + /// + /// The with the specified content key. + /// + public BlockListItem? this[Guid contentKey] => this.FirstOrDefault(x => x.Content.Key == contentKey); + + /// + /// Gets the with the specified content UDI. + /// + /// + /// The . + /// + /// The content UDI. + /// + /// The with the specified content UDI. + /// + public BlockListItem? this[Udi contentUdi] => contentUdi is GuidUdi guidUdi + ? this.FirstOrDefault(x => x.Content.Key == guidUdi.Guid) + : null; } diff --git a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs index f8677490ee..96a81641fa 100644 --- a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs +++ b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs @@ -1,36 +1,32 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Blocks; -namespace Umbraco.Cms.Core.Models.Blocks +public struct ContentAndSettingsReference : IEquatable { - public struct ContentAndSettingsReference : IEquatable + public ContentAndSettingsReference(Udi? contentUdi, Udi? settingsUdi) { - public ContentAndSettingsReference(Udi? contentUdi, Udi? settingsUdi) - { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); - SettingsUdi = settingsUdi; - } - - public Udi ContentUdi { get; } - - public Udi? SettingsUdi { get; } - - public override bool Equals(object? obj) => obj is ContentAndSettingsReference reference && Equals(reference); - - public bool Equals(ContentAndSettingsReference other) => other != null - && EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi) - && EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi); - - public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); - - public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) - { - return left.Equals(right); - } - - public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right) - { - return !(left == right); - } + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + SettingsUdi = settingsUdi; } + + public Udi ContentUdi { get; } + + public Udi? SettingsUdi { get; } + + public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) => + left.Equals(right); + + public override bool Equals(object? obj) => obj is ContentAndSettingsReference reference && Equals(reference); + + public bool Equals(ContentAndSettingsReference other) => other != null + && EqualityComparer.Default.Equals( + ContentUdi, + other.ContentUdi) + && EqualityComparer.Default.Equals( + SettingsUdi, + other.SettingsUdi); + + public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); + + public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right) => + !(left == right); } diff --git a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs index 48c2b85637..44533407d2 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs @@ -1,37 +1,38 @@ -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a data item reference for a Block Editor implementation. +/// +/// +/// See: +/// https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed +/// +public interface IBlockReference { /// - /// Represents a data item reference for a Block Editor implementation. + /// Gets the content UDI. /// - /// - /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed - /// - public interface IBlockReference - { - /// - /// Gets the content UDI. - /// - /// - /// The content UDI. - /// - Udi ContentUdi { get; } - } - - /// - /// Represents a data item reference with settings for a Block editor implementation. - /// - /// The type of the settings. - /// - /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed - /// - public interface IBlockReference : IBlockReference - { - /// - /// Gets the settings. - /// - /// - /// The settings. - /// - TSettings Settings { get; } - } + /// + /// The content UDI. + /// + Udi ContentUdi { get; } +} + +/// +/// Represents a data item reference with settings for a Block editor implementation. +/// +/// The type of the settings. +/// +/// See: +/// https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed +/// +public interface IBlockReference : IBlockReference +{ + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + TSettings Settings { get; } } diff --git a/src/Umbraco.Core/Models/CacheInstruction.cs b/src/Umbraco.Core/Models/CacheInstruction.cs index 5434f443a0..a93ec030c8 100644 --- a/src/Umbraco.Core/Models/CacheInstruction.cs +++ b/src/Umbraco.Core/Models/CacheInstruction.cs @@ -1,51 +1,48 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a cache instruction. +/// +[Serializable] +[DataContract(IsReference = true)] +public class CacheInstruction { /// - /// Represents a cache instruction. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class CacheInstruction + public CacheInstruction(int id, DateTime utcStamp, string instructions, string originIdentity, int instructionCount) { - /// - /// Initializes a new instance of the class. - /// - public CacheInstruction(int id, DateTime utcStamp, string instructions, string originIdentity, int instructionCount) - { - Id = id; - UtcStamp = utcStamp; - Instructions = instructions; - OriginIdentity = originIdentity; - InstructionCount = instructionCount; - } - - /// - /// Cache instruction Id. - /// - public int Id { get; } - - /// - /// Cache instruction created date. - /// - public DateTime UtcStamp { get; } - - /// - /// Serialized instructions. - /// - public string Instructions { get; } - - /// - /// Identity of server originating the instruction. - /// - public string OriginIdentity { get; } - - /// - /// Count of instructions. - /// - public int InstructionCount { get; } - + Id = id; + UtcStamp = utcStamp; + Instructions = instructions; + OriginIdentity = originIdentity; + InstructionCount = instructionCount; } + + /// + /// Cache instruction Id. + /// + public int Id { get; } + + /// + /// Cache instruction created date. + /// + public DateTime UtcStamp { get; } + + /// + /// Serialized instructions. + /// + public string Instructions { get; } + + /// + /// Identity of server originating the instruction. + /// + public string OriginIdentity { get; } + + /// + /// Count of instructions. + /// + public int InstructionCount { get; } } diff --git a/src/Umbraco.Core/Models/ChangingPasswordModel.cs b/src/Umbraco.Core/Models/ChangingPasswordModel.cs index be19f13b75..946bcde9ab 100644 --- a/src/Umbraco.Core/Models/ChangingPasswordModel.cs +++ b/src/Umbraco.Core/Models/ChangingPasswordModel.cs @@ -1,29 +1,28 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing the data required to set a member/user password depending on the provider installed. +/// +public class ChangingPasswordModel { /// - /// A model representing the data required to set a member/user password depending on the provider installed. + /// The password value /// - public class ChangingPasswordModel - { - /// - /// The password value - /// - [DataMember(Name = "newPassword")] - public string? NewPassword { get; set; } + [DataMember(Name = "newPassword")] + public string? NewPassword { get; set; } - /// - /// The old password - used to change a password when: EnablePasswordRetrieval = false - /// - [DataMember(Name = "oldPassword")] - public string? OldPassword { get; set; } + /// + /// The old password - used to change a password when: EnablePasswordRetrieval = false + /// + [DataMember(Name = "oldPassword")] + public string? OldPassword { get; set; } - /// - /// The ID of the current user/member requesting the password change - /// For users, required to allow changing password without the entire UserSave model - /// - [DataMember(Name = "id")] - public int Id { get; set; } - } + /// + /// The ID of the current user/member requesting the password change + /// For users, required to allow changing password without the entire UserSave model + /// + [DataMember(Name = "id")] + public int Id { get; set; } } diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs index 2354c67b1e..e71f040ba8 100644 --- a/src/Umbraco.Core/Models/Consent.cs +++ b/src/Umbraco.Core/Models/Consent.cs @@ -1,86 +1,96 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a consent. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Consent : EntityBase, IConsent { + private string? _action; + private string? _comment; + private string? _context; + private bool _current; + private string? _source; + private ConsentState _state; + /// - /// Represents a consent. + /// Gets the previous states of this consent. /// - [Serializable] - [DataContract(IsReference = true)] - public class Consent : EntityBase, IConsent + public List? HistoryInternal { get; set; } + + /// + public bool Current { - private bool _current; - private string? _source; - private string? _context; - private string? _action; - private ConsentState _state; - private string? _comment; - - /// - public bool Current - { - get => _current; - set => SetPropertyValueAndDetectChanges(value, ref _current, nameof(Current)); - } - - /// - public string? Source - { - get => _source; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _source, nameof(Source)); - } - } - - /// - public string? Context - { - get => _context; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _context, nameof(Context)); - } - } - - /// - public string? Action - { - get => _action; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _action, nameof(Action)); - } - } - - /// - public ConsentState State - { - get => _state; - // note: we probably should validate the state here, but since the - // enum is [Flags] with many combinations, this could be expensive - set => SetPropertyValueAndDetectChanges(value, ref _state, nameof(State)); - } - - /// - public string? Comment - { - get => _comment; - set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); - } - - /// - public IEnumerable? History => HistoryInternal; - - /// - /// Gets the previous states of this consent. - /// - public List? HistoryInternal { get; set; } + get => _current; + set => SetPropertyValueAndDetectChanges(value, ref _current, nameof(Current)); } + + /// + public string? Source + { + get => _source; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(nameof(value)); + } + + SetPropertyValueAndDetectChanges(value, ref _source, nameof(Source)); + } + } + + /// + public string? Context + { + get => _context; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(nameof(value)); + } + + SetPropertyValueAndDetectChanges(value, ref _context, nameof(Context)); + } + } + + /// + public string? Action + { + get => _action; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(nameof(value)); + } + + SetPropertyValueAndDetectChanges(value, ref _action, nameof(Action)); + } + } + + /// + public ConsentState State + { + get => _state; + + // note: we probably should validate the state here, but since the + // enum is [Flags] with many combinations, this could be expensive + set => SetPropertyValueAndDetectChanges(value, ref _state, nameof(State)); + } + + /// + public string? Comment + { + get => _comment; + set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); + } + + /// + public IEnumerable? History => HistoryInternal; } diff --git a/src/Umbraco.Core/Models/ConsentExtensions.cs b/src/Umbraco.Core/Models/ConsentExtensions.cs index b95c7b66f9..1dc6cadde8 100644 --- a/src/Umbraco.Core/Models/ConsentExtensions.cs +++ b/src/Umbraco.Core/Models/ConsentExtensions.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the interface. +/// +public static class ConsentExtensions { /// - /// Provides extension methods for the interface. + /// Determines whether the consent is granted. /// - public static class ConsentExtensions - { - /// - /// Determines whether the consent is granted. - /// - public static bool IsGranted(this IConsent consent) => (consent.State & ConsentState.Granted) > 0; + public static bool IsGranted(this IConsent consent) => (consent.State & ConsentState.Granted) > 0; - /// - /// Determines whether the consent is revoked. - /// - public static bool IsRevoked(this IConsent consent) => (consent.State & ConsentState.Revoked) > 0; - } + /// + /// Determines whether the consent is revoked. + /// + public static bool IsRevoked(this IConsent consent) => (consent.State & ConsentState.Revoked) > 0; } diff --git a/src/Umbraco.Core/Models/ConsentState.cs b/src/Umbraco.Core/Models/ConsentState.cs index 0828561ff8..8a6846b28c 100644 --- a/src/Umbraco.Core/Models/ConsentState.cs +++ b/src/Umbraco.Core/Models/ConsentState.cs @@ -1,38 +1,35 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents the state of a consent. +/// +[Flags] +public enum ConsentState // : int { + // note - this is a [Flags] enumeration + // on can create detailed flags such as: + // GrantedOptIn = Granted | 0x0001 + // GrandedByForce = Granted | 0x0002 + // + // 16 situations for each Pending/Granted/Revoked should be ok + /// - /// Represents the state of a consent. + /// There is no consent. /// - [Flags] - public enum ConsentState // : int - { - // note - this is a [Flags] enumeration - // on can create detailed flags such as: - //GrantedOptIn = Granted | 0x0001 - //GrandedByForce = Granted | 0x0002 - // - // 16 situations for each Pending/Granted/Revoked should be ok + None = 0, - /// - /// There is no consent. - /// - None = 0, + /// + /// Consent is pending and has not been granted yet. + /// + Pending = 0x10000, - /// - /// Consent is pending and has not been granted yet. - /// - Pending = 0x10000, + /// + /// Consent has been granted. + /// + Granted = 0x20000, - /// - /// Consent has been granted. - /// - Granted = 0x20000, - - /// - /// Consent has been revoked. - /// - Revoked = 0x40000 - } + /// + /// Consent has been revoked. + /// + Revoked = 0x40000, } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index bc77e52624..4e251a323e 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -1,470 +1,567 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Content object +/// +[Serializable] +[DataContract(IsReference = true)] +public class Content : ContentBase, IContent { + private HashSet? _editedCultures; + private bool _published; + private PublishedState _publishedState; + private ContentCultureInfosCollection? _publishInfos; + private int? _templateId; + /// - /// Represents a Content object + /// Constructor for creating a Content object /// - [Serializable] - [DataContract(IsReference = true)] - public class Content : ContentBase, IContent + /// Name of the content + /// Parent object + /// ContentType for the current Content object + /// An optional culture. + public Content(string name, IContent parent, IContentType contentType, string? culture = null) + : this(name, parent, contentType, new PropertyCollection(), culture) { - private int? _templateId; - private bool _published; - private PublishedState _publishedState; - private HashSet? _editedCultures; - private ContentCultureInfosCollection? _publishInfos; + } - #region Used for change tracking + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Parent object + /// ContentType for the current Content object + /// The identifier of the user creating the Content object + /// An optional culture. + public Content(string name, IContent parent, IContentType contentType, int userId, string? culture = null) + : this(name, parent, contentType, new PropertyCollection(), culture) + { + CreatorId = userId; + WriterId = userId; + } - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _currentPublishCultureChanges; - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _previousPublishCultureChanges; - - #endregion - - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Parent object - /// ContentType for the current Content object - /// An optional culture. - public Content(string name, IContent parent, IContentType contentType, string? culture = null) - : this(name, parent, contentType, new PropertyCollection(), culture) - { } - - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Parent object - /// ContentType for the current Content object - /// The identifier of the user creating the Content object - /// An optional culture. - public Content(string name, IContent parent, IContentType contentType, int userId, string? culture = null) - : this(name, parent, contentType, new PropertyCollection(), culture) + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Parent object + /// ContentType for the current Content object + /// Collection of properties + /// An optional culture. + public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, string? culture = null) + : base(name, parent, contentType, properties, culture) + { + if (contentType == null) { - CreatorId = userId; - WriterId = userId; + throw new ArgumentNullException(nameof(contentType)); } - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Parent object - /// ContentType for the current Content object - /// Collection of properties - /// An optional culture. - public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, string? culture = null) - : base(name, parent, contentType, properties, culture) + _publishedState = PublishedState.Unpublished; + PublishedVersionId = 0; + } + + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Id of the Parent content + /// ContentType for the current Content object + /// An optional culture. + public Content(string? name, int parentId, IContentType? contentType, string? culture = null) + : this(name, parentId, contentType, new PropertyCollection(), culture) + { + } + + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Id of the Parent content + /// ContentType for the current Content object + /// The identifier of the user creating the Content object + /// An optional culture. + public Content(string name, int parentId, IContentType contentType, int userId, string? culture = null) + : this(name, parentId, contentType, new PropertyCollection(), culture) + { + CreatorId = userId; + WriterId = userId; + } + + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Id of the Parent content + /// ContentType for the current Content object + /// Collection of properties + /// An optional culture. + public Content(string? name, int parentId, IContentType? contentType, PropertyCollection properties, string? culture = null) + : base(name, parentId, contentType, properties, culture) + { + if (contentType == null) { - if (contentType == null) throw new ArgumentNullException(nameof(contentType)); - _publishedState = PublishedState.Unpublished; - PublishedVersionId = 0; + throw new ArgumentNullException(nameof(contentType)); } - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Id of the Parent content - /// ContentType for the current Content object - /// An optional culture. - public Content(string? name, int parentId, IContentType? contentType, string? culture = null) - : this(name, parentId, contentType, new PropertyCollection(), culture) - { } + _publishedState = PublishedState.Unpublished; + PublishedVersionId = 0; + } - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Id of the Parent content - /// ContentType for the current Content object - /// The identifier of the user creating the Content object - /// An optional culture. - public Content(string name, int parentId, IContentType contentType, int userId, string? culture = null) - : this(name, parentId, contentType, new PropertyCollection(), culture) + /// + /// Gets or sets the template used by the Content. + /// This is used to override the default one from the ContentType. + /// + /// + /// If no template is explicitly set on the Content object, + /// the Default template from the ContentType will be returned. + /// + [DataMember] + public int? TemplateId + { + get => _templateId; + set => SetPropertyValueAndDetectChanges(value, ref _templateId, nameof(TemplateId)); + } + + /// + /// Gets or sets a value indicating whether this content item is published or not. + /// + /// + /// the setter is should only be invoked from + /// - the ContentFactory when creating a content entity from a dto + /// - the ContentRepository when updating a content entity + /// + [DataMember] + public bool Published + { + get => _published; + set { - CreatorId = userId; - WriterId = userId; - } - - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Id of the Parent content - /// ContentType for the current Content object - /// Collection of properties - /// An optional culture. - public Content(string? name, int parentId, IContentType? contentType, PropertyCollection properties, string? culture = null) - : base(name, parentId, contentType, properties, culture) - { - if (contentType == null) throw new ArgumentNullException(nameof(contentType)); - _publishedState = PublishedState.Unpublished; - PublishedVersionId = 0; - } - - /// - /// Gets or sets the template used by the Content. - /// This is used to override the default one from the ContentType. - /// - /// - /// If no template is explicitly set on the Content object, - /// the Default template from the ContentType will be returned. - /// - [DataMember] - public int? TemplateId - { - get => _templateId; - set => SetPropertyValueAndDetectChanges(value, ref _templateId, nameof(TemplateId)); - } - - /// - /// Gets or sets a value indicating whether this content item is published or not. - /// - /// - /// the setter is should only be invoked from - /// - the ContentFactory when creating a content entity from a dto - /// - the ContentRepository when updating a content entity - /// - [DataMember] - public bool Published - { - get => _published; - set - { - SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published)); - _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; - } - } - - /// - /// Gets the published state of the content item. - /// - /// The state should be Published or Unpublished, depending on whether Published - /// is true or false, but can also temporarily be Publishing or Unpublishing when the - /// content item is about to be saved. - [DataMember] - public PublishedState PublishedState - { - get => _publishedState; - set - { - if (value != PublishedState.Publishing && value != PublishedState.Unpublishing) - throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted."); - _publishedState = value; - } - } - - [IgnoreDataMember] - public bool Edited { get; set; } - - /// - [IgnoreDataMember] - public DateTime? PublishDate { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public int? PublisherId { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public int? PublishTemplateId { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public string? PublishName { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public IEnumerable? EditedCultures - { - get => CultureInfos?.Keys.Where(IsCultureEdited); - set => _editedCultures = value == null ? null : new HashSet(value, StringComparer.OrdinalIgnoreCase); - } - - /// - [IgnoreDataMember] - public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); - - /// - public bool IsCulturePublished(string culture) - // just check _publishInfos - // a non-available culture could not become published anyways - => !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture); - - /// - public bool IsCultureEdited(string culture) - => IsCultureAvailable(culture) && // is available, and - (!IsCulturePublished(culture) || // is not published, or - (_editedCultures != null && _editedCultures.Contains(culture))); // is edited - - /// - [IgnoreDataMember] - public ContentCultureInfosCollection? PublishCultureInfos - { - get - { - if (_publishInfos != null) return _publishInfos; - _publishInfos = new ContentCultureInfosCollection(); - _publishInfos.CollectionChanged += PublishNamesCollectionChanged; - return _publishInfos; - } - set - { - if (_publishInfos != null) - { - _publishInfos.ClearCollectionChangedEvents(); - } - - _publishInfos = value; - if (_publishInfos != null) - { - _publishInfos.CollectionChanged += PublishNamesCollectionChanged; - } - } - } - - /// - public string? GetPublishName(string? culture) - { - if (culture.IsNullOrWhiteSpace()) return PublishName; - if (!ContentType.VariesByCulture()) return null; - if (_publishInfos == null) return null; - return _publishInfos.TryGetValue(culture!, out var infos) ? infos.Name : null; - } - - /// - public DateTime? GetPublishDate(string culture) - { - if (culture.IsNullOrWhiteSpace()) return PublishDate; - if (!ContentType.VariesByCulture()) return null; - if (_publishInfos == null) return null; - return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?)null; - } - - /// - /// Handles culture infos collection changes. - /// - private void PublishNamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(PublishCultureInfos)); - - //we don't need to handle other actions, only add/remove, however we could implement Replace and track updated cultures in _updatedCultures too - //which would allows us to continue doing WasCulturePublished, but don't think we need it anymore - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - { - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentPublishCultureChanges.addedCultures == null) _currentPublishCultureChanges.addedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentPublishCultureChanges.addedCultures.Add(cultureInfo.Culture); - _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); - _currentPublishCultureChanges.removedCultures?.Remove(cultureInfo.Culture); - } - break; - } - case NotifyCollectionChangedAction.Remove: - { - //remove listening for changes - var cultureInfo = e.OldItems?.Cast().First(); - if (_currentPublishCultureChanges.removedCultures == null) _currentPublishCultureChanges.removedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentPublishCultureChanges.removedCultures.Add(cultureInfo.Culture); - _currentPublishCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); - _currentPublishCultureChanges.addedCultures?.Remove(cultureInfo.Culture); - } - break; - } - case NotifyCollectionChangedAction.Replace: - { - //replace occurs when an Update occurs - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); - } - break; - } - } - } - - [IgnoreDataMember] - public int PublishedVersionId { get; set; } - - [DataMember] - public bool Blueprint { get; set; } - - /// - /// Changes the for the current content object - /// - /// New ContentType for this content - /// Leaves PropertyTypes intact after change - internal void ChangeContentType(IContentType contentType) - { - ChangeContentType(contentType, false); - } - - /// - /// Changes the for the current content object and removes PropertyTypes, - /// which are not part of the new ContentType. - /// - /// New ContentType for this content - /// Boolean indicating whether to clear PropertyTypes upon change - internal void ChangeContentType(IContentType contentType, bool clearProperties) - { - ChangeContentType(new SimpleContentType(contentType)); - - if (clearProperties) - Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes); - else - Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); - - Properties.ClearCollectionChangedEvents(); // be sure not to double add - Properties.CollectionChanged += PropertiesChanged; - } - - public override void ResetWereDirtyProperties() - { - base.ResetWereDirtyProperties(); - _previousPublishCultureChanges.updatedCultures = null; - _previousPublishCultureChanges.removedCultures = null; - _previousPublishCultureChanges.addedCultures = null; - } - - public override void ResetDirtyProperties(bool rememberDirty) - { - base.ResetDirtyProperties(rememberDirty); - - if (rememberDirty) - { - _previousPublishCultureChanges.addedCultures = _currentPublishCultureChanges.addedCultures == null || _currentPublishCultureChanges.addedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousPublishCultureChanges.removedCultures = _currentPublishCultureChanges.removedCultures == null || _currentPublishCultureChanges.removedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousPublishCultureChanges.updatedCultures = _currentPublishCultureChanges.updatedCultures == null || _currentPublishCultureChanges.updatedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); - } - else - { - _previousPublishCultureChanges.addedCultures = null; - _previousPublishCultureChanges.removedCultures = null; - _previousPublishCultureChanges.updatedCultures = null; - } - _currentPublishCultureChanges.addedCultures?.Clear(); - _currentPublishCultureChanges.removedCultures?.Clear(); - _currentPublishCultureChanges.updatedCultures?.Clear(); - - // take care of the published state + SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published)); _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; - - if (_publishInfos == null) return; - - foreach (var infos in _publishInfos) - infos.ResetDirtyProperties(rememberDirty); - } - - /// - /// Overridden to check special keys. - public override bool IsPropertyDirty(string propertyName) - { - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); - return _currentPublishCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); - return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); - return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return base.IsPropertyDirty(propertyName); - } - - /// - /// Overridden to check special keys. - public override bool WasPropertyDirty(string propertyName) - { - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); - return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); - return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); - return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return base.WasPropertyDirty(propertyName); - } - - /// - /// Creates a deep clone of the current entity with its identity and it's property identities reset - /// - /// - public IContent DeepCloneWithResetIdentities() - { - var clone = (Content)DeepClone(); - clone.Key = Guid.Empty; - clone.VersionId = clone.PublishedVersionId = 0; - clone.ResetIdentity(); - - foreach (var property in clone.Properties) - property.ResetIdentity(); - - return clone; - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedContent = (Content)clone; - - //fixme - need to reset change tracking bits - - //if culture infos exist then deal with event bindings - if (clonedContent._publishInfos != null) - { - clonedContent._publishInfos.ClearCollectionChangedEvents(); //clear this event handler if any - clonedContent._publishInfos = (ContentCultureInfosCollection?)_publishInfos?.DeepClone(); //manually deep clone - if (clonedContent._publishInfos is not null) - { - clonedContent._publishInfos.CollectionChanged += - clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler - } - } - - clonedContent._currentPublishCultureChanges.updatedCultures = null; - clonedContent._currentPublishCultureChanges.addedCultures = null; - clonedContent._currentPublishCultureChanges.removedCultures = null; - - clonedContent._previousPublishCultureChanges.updatedCultures = null; - clonedContent._previousPublishCultureChanges.addedCultures = null; - clonedContent._previousPublishCultureChanges.removedCultures = null; } } + + /// + /// Gets the published state of the content item. + /// + /// + /// The state should be Published or Unpublished, depending on whether Published + /// is true or false, but can also temporarily be Publishing or Unpublishing when the + /// content item is about to be saved. + /// + [DataMember] + public PublishedState PublishedState + { + get => _publishedState; + set + { + if (value != PublishedState.Publishing && value != PublishedState.Unpublishing) + { + throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted."); + } + + _publishedState = value; + } + } + + [IgnoreDataMember] + public bool Edited { get; set; } + + /// + [IgnoreDataMember] + public DateTime? PublishDate { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public int? PublisherId { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public int? PublishTemplateId { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public string? PublishName { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public IEnumerable? EditedCultures + { + get => CultureInfos?.Keys.Where(IsCultureEdited); + set => _editedCultures = value == null ? null : new HashSet(value, StringComparer.OrdinalIgnoreCase); + } + + /// + [IgnoreDataMember] + public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); + + /// + public bool IsCulturePublished(string culture) + + // just check _publishInfos + // a non-available culture could not become published anyways + => !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture); + + /// + public bool IsCultureEdited(string culture) + => IsCultureAvailable(culture) && // is available, and + (!IsCulturePublished(culture) || // is not published, or + (_editedCultures != null && _editedCultures.Contains(culture))); // is edited + + /// + [IgnoreDataMember] + public ContentCultureInfosCollection? PublishCultureInfos + { + get + { + if (_publishInfos != null) + { + return _publishInfos; + } + + _publishInfos = new ContentCultureInfosCollection(); + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + return _publishInfos; + } + + set + { + if (_publishInfos != null) + { + _publishInfos.ClearCollectionChangedEvents(); + } + + _publishInfos = value; + if (_publishInfos != null) + { + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + } + } + } + + /// + public string? GetPublishName(string? culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return PublishName; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } + + if (_publishInfos == null) + { + return null; + } + + return _publishInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null; + } + + /// + public DateTime? GetPublishDate(string culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return PublishDate; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } + + if (_publishInfos == null) + { + return null; + } + + return _publishInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null; + } + + [IgnoreDataMember] + public int PublishedVersionId { get; set; } + + [DataMember] + public bool Blueprint { get; set; } + + public override void ResetWereDirtyProperties() + { + base.ResetWereDirtyProperties(); + _previousPublishCultureChanges.updatedCultures = null; + _previousPublishCultureChanges.removedCultures = null; + _previousPublishCultureChanges.addedCultures = null; + } + + public override void ResetDirtyProperties(bool rememberDirty) + { + base.ResetDirtyProperties(rememberDirty); + + if (rememberDirty) + { + _previousPublishCultureChanges.addedCultures = + _currentPublishCultureChanges.addedCultures == null || + _currentPublishCultureChanges.addedCultures.Count == 0 + ? null + : new HashSet(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousPublishCultureChanges.removedCultures = + _currentPublishCultureChanges.removedCultures == null || + _currentPublishCultureChanges.removedCultures.Count == 0 + ? null + : new HashSet(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousPublishCultureChanges.updatedCultures = + _currentPublishCultureChanges.updatedCultures == null || + _currentPublishCultureChanges.updatedCultures.Count == 0 + ? null + : new HashSet(_currentPublishCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); + } + else + { + _previousPublishCultureChanges.addedCultures = null; + _previousPublishCultureChanges.removedCultures = null; + _previousPublishCultureChanges.updatedCultures = null; + } + + _currentPublishCultureChanges.addedCultures?.Clear(); + _currentPublishCultureChanges.removedCultures?.Clear(); + _currentPublishCultureChanges.updatedCultures?.Clear(); + + // take care of the published state + _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; + + if (_publishInfos == null) + { + return; + } + + foreach (ContentCultureInfos infos in _publishInfos) + { + infos.ResetDirtyProperties(rememberDirty); + } + } + + /// + /// Overridden to check special keys. + public override bool IsPropertyDirty(string propertyName) + { + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); + return _currentPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); + return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); + return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return base.IsPropertyDirty(propertyName); + } + + /// + /// Overridden to check special keys. + public override bool WasPropertyDirty(string propertyName) + { + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); + return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); + return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); + return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return base.WasPropertyDirty(propertyName); + } + + /// + /// Creates a deep clone of the current entity with its identity and it's property identities reset + /// + /// + public IContent DeepCloneWithResetIdentities() + { + var clone = (Content)DeepClone(); + clone.Key = Guid.Empty; + clone.VersionId = clone.PublishedVersionId = 0; + clone.ResetIdentity(); + + foreach (IProperty property in clone.Properties) + { + property.ResetIdentity(); + } + + return clone; + } + + /// + /// Handles culture infos collection changes. + /// + private void PublishNamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(PublishCultureInfos)); + + // we don't need to handle other actions, only add/remove, however we could implement Replace and track updated cultures in _updatedCultures too + // which would allows us to continue doing WasCulturePublished, but don't think we need it anymore + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentPublishCultureChanges.addedCultures == null) + { + _currentPublishCultureChanges.addedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (_currentPublishCultureChanges.updatedCultures == null) + { + _currentPublishCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentPublishCultureChanges.addedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.removedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + // Remove listening for changes + ContentCultureInfos? cultureInfo = e.OldItems?.Cast().First(); + if (_currentPublishCultureChanges.removedCultures == null) + { + _currentPublishCultureChanges.removedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentPublishCultureChanges.removedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); + _currentPublishCultureChanges.addedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Replace occurs when an Update occurs + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentPublishCultureChanges.updatedCultures == null) + { + _currentPublishCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); + } + + break; + } + } + } + + /// + /// Changes the for the current content object + /// + /// New ContentType for this content + /// Leaves PropertyTypes intact after change + internal void ChangeContentType(IContentType contentType) => ChangeContentType(contentType, false); + + /// + /// Changes the for the current content object and removes PropertyTypes, + /// which are not part of the new ContentType. + /// + /// New ContentType for this content + /// Boolean indicating whether to clear PropertyTypes upon change + internal void ChangeContentType(IContentType contentType, bool clearProperties) + { + ChangeContentType(new SimpleContentType(contentType)); + + if (clearProperties) + { + Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes); + } + else + { + Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + } + + Properties.ClearCollectionChangedEvents(); // be sure not to double add + Properties.CollectionChanged += PropertiesChanged; + } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedContent = (Content)clone; + + // fixme - need to reset change tracking bits + + // if culture infos exist then deal with event bindings + if (clonedContent._publishInfos != null) + { + // Clear this event handler if any + clonedContent._publishInfos.ClearCollectionChangedEvents(); + + // Manually deep clone + clonedContent._publishInfos = (ContentCultureInfosCollection?)_publishInfos?.DeepClone(); + if (clonedContent._publishInfos is not null) + { + // Re-assign correct event handler + clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; + } + } + + clonedContent._currentPublishCultureChanges.updatedCultures = null; + clonedContent._currentPublishCultureChanges.addedCultures = null; + clonedContent._currentPublishCultureChanges.removedCultures = null; + + clonedContent._previousPublishCultureChanges.updatedCultures = null; + clonedContent._previousPublishCultureChanges.addedCultures = null; + clonedContent._previousPublishCultureChanges.removedCultures = null; + } + + #region Used for change tracking + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _currentPublishCultureChanges; + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _previousPublishCultureChanges; + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index d9223130d6..e9fcc61e7c 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -1,530 +1,639 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; +using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract class for base Content properties and methods +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentType.Alias}")] +public abstract class ContentBase : TreeEntityBase, IContentBase { + private int _contentTypeId; + private ContentCultureInfosCollection? _cultureInfos; + private IPropertyCollection _properties; + private int _writerId; + /// - /// Represents an abstract class for base Content properties and methods + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentType.Alias}")] - public abstract class ContentBase : TreeEntityBase, IContentBase + protected ContentBase(string? name, int parentId, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) + : this(name, contentType, properties, culture) { - private int _contentTypeId; - private int _writerId; - private IPropertyCollection _properties; - private ContentCultureInfosCollection? _cultureInfos; - internal IReadOnlyList AllPropertyTypes { get; } - - #region Used for change tracking - - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _currentCultureChanges; - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _previousCultureChanges; - - public static class ChangeTrackingPrefix + if (parentId == 0) { - public const string UpdatedCulture = "_updatedCulture_"; - public const string ChangedCulture = "_changedCulture_"; - public const string PublishedCulture = "_publishedCulture_"; - public const string UnpublishedCulture = "_unpublishedCulture_"; - public const string AddedCulture = "_addedCulture_"; - public const string RemovedCulture = "_removedCulture_"; + throw new ArgumentOutOfRangeException(nameof(parentId)); } - #endregion + ParentId = parentId; + } - /// - /// Initializes a new instance of the class. - /// - protected ContentBase(string? name, int parentId, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) - : this(name, contentType, properties, culture) + /// + /// Initializes a new instance of the class. + /// + protected ContentBase(string? name, IContentBase? parent, IContentTypeComposition contentType, IPropertyCollection properties, string? culture = null) + : this(name, contentType, properties, culture) + { + if (parent == null) { - if (parentId == 0) throw new ArgumentOutOfRangeException(nameof(parentId)); - ParentId = parentId; + throw new ArgumentNullException(nameof(parent)); } - /// - /// Initializes a new instance of the class. - /// - protected ContentBase(string? name, IContentBase? parent, IContentTypeComposition contentType, IPropertyCollection properties, string? culture = null) - : this(name, contentType, properties, culture) + SetParent(parent); + } + + private ContentBase(string? name, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) + { + ContentType = contentType?.ToSimple() ?? throw new ArgumentNullException(nameof(contentType)); + + // initially, all new instances have + Id = 0; // no identity + VersionId = 0; // no versions + + SetCultureName(name, culture); + + _contentTypeId = contentType.Id; + _properties = properties ?? throw new ArgumentNullException(nameof(properties)); + _properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + + // track all property types on this content type, these can never change during the lifetime of this single instance + // there is no real extra memory overhead of doing this since these property types are already cached on this object via the + // properties already. + AllPropertyTypes = new List(contentType.CompositionPropertyTypes); + } + + internal IReadOnlyList AllPropertyTypes { get; } + + [IgnoreDataMember] + public ISimpleContentType ContentType { get; private set; } + + /// + /// Id of the user who wrote/updated this entity + /// + [DataMember] + public int WriterId + { + get => _writerId; + set => SetPropertyValueAndDetectChanges(value, ref _writerId, nameof(WriterId)); + } + + [IgnoreDataMember] + public int VersionId { get; set; } + + /// + /// Integer Id of the default ContentType + /// + [DataMember] + public int ContentTypeId + { + get { - if (parent == null) throw new ArgumentNullException(nameof(parent)); - SetParent(parent); - } - - private ContentBase(string? name, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) - { - ContentType = contentType?.ToSimple() ?? throw new ArgumentNullException(nameof(contentType)); - - // initially, all new instances have - Id = 0; // no identity - VersionId = 0; // no versions - - SetCultureName(name, culture); - - _contentTypeId = contentType.Id; - _properties = properties ?? throw new ArgumentNullException(nameof(properties)); - _properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); - - //track all property types on this content type, these can never change during the lifetime of this single instance - //there is no real extra memory overhead of doing this since these property types are already cached on this object via the - //properties already. - AllPropertyTypes = new List(contentType.CompositionPropertyTypes); - } - - [IgnoreDataMember] - public ISimpleContentType ContentType { get; private set; } - - public void ChangeContentType(ISimpleContentType contentType) - { - ContentType = contentType; - ContentTypeId = contentType.Id; - } - - protected void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(Properties)); - } - - /// - /// Id of the user who wrote/updated this entity - /// - [DataMember] - public int WriterId - { - get => _writerId; - set => SetPropertyValueAndDetectChanges(value, ref _writerId, nameof(WriterId)); - } - - [IgnoreDataMember] - public int VersionId { get; set; } - - /// - /// Integer Id of the default ContentType - /// - [DataMember] - public int ContentTypeId - { - get + // There will be cases where this has not been updated to reflect the true content type ID. + // This will occur when inserting new content. + if (_contentTypeId == 0 && ContentType != null) { - //There will be cases where this has not been updated to reflect the true content type ID. - //This will occur when inserting new content. - if (_contentTypeId == 0 && ContentType != null) - { - _contentTypeId = ContentType.Id; - } - return _contentTypeId; + _contentTypeId = ContentType.Id; } - private set => SetPropertyValueAndDetectChanges(value, ref _contentTypeId, nameof(ContentTypeId)); + + return _contentTypeId; } + private set => SetPropertyValueAndDetectChanges(value, ref _contentTypeId, nameof(ContentTypeId)); + } - /// - /// Gets or sets the collection of properties for the entity. - /// - /// - /// Marked DoNotClone since we'll manually clone the underlying field to deal with the event handling - /// - [DataMember] - [DoNotClone] - public IPropertyCollection Properties + /// + /// Gets or sets the collection of properties for the entity. + /// + /// + /// Marked DoNotClone since we'll manually clone the underlying field to deal with the event handling + /// + [DataMember] + [DoNotClone] + public IPropertyCollection Properties + { + get => _properties; + set { - get => _properties; - set + if (_properties != null) { - if (_properties != null) - { - _properties.ClearCollectionChangedEvents(); - } + _properties.ClearCollectionChangedEvents(); + } - _properties = value; - _properties.CollectionChanged += PropertiesChanged; + _properties = value; + _properties.CollectionChanged += PropertiesChanged; + } + } + + public void ChangeContentType(ISimpleContentType contentType) + { + ContentType = contentType; + ContentTypeId = contentType.Id; + } + + protected void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + OnPropertyChanged(nameof(Properties)); + + /// + /// + /// Overridden to deal with specific object instances + /// + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedContent = (ContentBase)clone; + + // Need to manually clone this since it's not settable + clonedContent.ContentType = ContentType; + + // If culture infos exist then deal with event bindings + if (clonedContent._cultureInfos != null) + { + clonedContent._cultureInfos.ClearCollectionChangedEvents(); // clear this event handler if any + clonedContent._cultureInfos = + (ContentCultureInfosCollection?)_cultureInfos?.DeepClone(); // manually deep clone + if (clonedContent._cultureInfos is not null) + { + clonedContent._cultureInfos.CollectionChanged += + clonedContent.CultureInfosCollectionChanged; // re-assign correct event handler } } - #region Cultures - - // notes - common rules - // - setting a variant value on an invariant content type throws - // - getting a variant value on an invariant content type returns null - // - setting and getting the invariant value is always possible - // - setting a null value clears the value - - /// - public IEnumerable AvailableCultures - => _cultureInfos?.Keys ?? Enumerable.Empty(); - - /// - public bool IsCultureAvailable(string culture) - => _cultureInfos != null && _cultureInfos.ContainsKey(culture); - - /// - [DataMember] - public ContentCultureInfosCollection? CultureInfos + // if properties exist then deal with event bindings + if (clonedContent._properties != null) { - get + clonedContent._properties.ClearCollectionChangedEvents(); // clear this event handler if any + clonedContent._properties = (IPropertyCollection)_properties.DeepClone(); // manually deep clone + clonedContent._properties.CollectionChanged += + clonedContent.PropertiesChanged; // re-assign correct event handler + } + + clonedContent._currentCultureChanges.updatedCultures = null; + clonedContent._currentCultureChanges.addedCultures = null; + clonedContent._currentCultureChanges.removedCultures = null; + + clonedContent._previousCultureChanges.updatedCultures = null; + clonedContent._previousCultureChanges.addedCultures = null; + clonedContent._previousCultureChanges.removedCultures = null; + } + + #region Used for change tracking + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _currentCultureChanges; + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _previousCultureChanges; + + public static class ChangeTrackingPrefix + { + public const string UpdatedCulture = "_updatedCulture_"; + public const string ChangedCulture = "_changedCulture_"; + public const string PublishedCulture = "_publishedCulture_"; + public const string UnpublishedCulture = "_unpublishedCulture_"; + public const string AddedCulture = "_addedCulture_"; + public const string RemovedCulture = "_removedCulture_"; + } + + #endregion + + #region Cultures + + // notes - common rules + // - setting a variant value on an invariant content type throws + // - getting a variant value on an invariant content type returns null + // - setting and getting the invariant value is always possible + // - setting a null value clears the value + + /// + public IEnumerable AvailableCultures + => _cultureInfos?.Keys ?? Enumerable.Empty(); + + /// + public bool IsCultureAvailable(string culture) + => _cultureInfos != null && _cultureInfos.ContainsKey(culture); + + /// + [DataMember] + public ContentCultureInfosCollection? CultureInfos + { + get + { + if (_cultureInfos != null) { - if (_cultureInfos != null) return _cultureInfos; - _cultureInfos = new ContentCultureInfosCollection(); - _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; return _cultureInfos; } - set + + _cultureInfos = new ContentCultureInfosCollection(); + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; + return _cultureInfos; + } + + set + { + if (_cultureInfos != null) { - if (_cultureInfos != null) - { - _cultureInfos.ClearCollectionChangedEvents(); - } - _cultureInfos = value; - if (_cultureInfos != null) - { - _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; - } + _cultureInfos.ClearCollectionChangedEvents(); + } + + _cultureInfos = value; + if (_cultureInfos != null) + { + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; } } + } - /// - public string? GetCultureName(string? culture) + /// + public string? GetCultureName(string? culture) + { + if (culture.IsNullOrWhiteSpace()) { - if (culture.IsNullOrWhiteSpace()) return Name; - if (!ContentType.VariesByCulture()) return null; - if (_cultureInfos == null) return null; - return _cultureInfos.TryGetValue(culture!, out var infos) ? infos.Name : null; + return Name; } - /// - public DateTime? GetUpdateDate(string culture) + if (!ContentType.VariesByCulture()) { - if (culture.IsNullOrWhiteSpace()) return null; - if (!ContentType.VariesByCulture()) return null; - if (_cultureInfos == null) return null; - return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?)null; + return null; } - /// - public void SetCultureName(string? name, string? culture) + if (_cultureInfos == null) { - if (ContentType.VariesByCulture()) // set on variant content type - { - if (culture.IsNullOrWhiteSpace()) // invariant is ok - { - Name = name; // may be null - } - else if (name.IsNullOrWhiteSpace()) // clear - { - ClearCultureInfo(culture!); - } - else // set - { - this.SetCultureInfo(culture!, name, DateTime.Now); - } - } - else // set on invariant content type - { - if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok - throw new NotSupportedException("Content type does not vary by culture."); + return null; + } + return _cultureInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null; + } + + /// + public DateTime? GetUpdateDate(string culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return null; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } + + if (_cultureInfos == null) + { + return null; + } + + return _cultureInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null; + } + + /// + public void SetCultureName(string? name, string? culture) + { + // set on variant content type + if (ContentType.VariesByCulture()) + { + // invariant is ok + if (culture.IsNullOrWhiteSpace()) + { Name = name; // may be null } - } - private void ClearCultureInfo(string culture) - { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - if (_cultureInfos == null) return; - _cultureInfos.Remove(culture); - if (_cultureInfos.Count == 0) - _cultureInfos = null; - } - - /// - /// Handles culture infos collection changes. - /// - private void CultureInfosCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(CultureInfos)); - - switch (e.Action) + // clear + else if (name.IsNullOrWhiteSpace()) { - case NotifyCollectionChangedAction.Add: - { - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentCultureChanges.addedCultures == null) _currentCultureChanges.addedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentCultureChanges.addedCultures.Add(cultureInfo.Culture); - _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); - _currentCultureChanges.removedCultures?.Remove(cultureInfo.Culture); - } + ClearCultureInfo(culture!); + } - break; - } - case NotifyCollectionChangedAction.Remove: - { - //remove listening for changes - var cultureInfo = e.OldItems?.Cast().First(); - if (_currentCultureChanges.removedCultures == null) _currentCultureChanges.removedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentCultureChanges.removedCultures.Add(cultureInfo.Culture); - _currentCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); - _currentCultureChanges.addedCultures?.Remove(cultureInfo.Culture); - } - - break; - } - case NotifyCollectionChangedAction.Replace: - { - //replace occurs when an Update occurs - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); - } - - break; - } + // set + else + { + this.SetCultureInfo(culture!, name, DateTime.Now); } } - #endregion - - #region Has, Get, Set, Publish Property Value - - /// - public bool HasProperty(string propertyTypeAlias) - => Properties.Contains(propertyTypeAlias); - - /// - public object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) + // set on invariant content type + else { - return Properties.TryGetValue(propertyTypeAlias, out var property) - ? property?.GetValue(culture, segment, published) - : null; + // invariant is NOT ok + if (!culture.IsNullOrWhiteSpace()) + { + throw new NotSupportedException("Content type does not vary by culture."); + } + + Name = name; // may be null + } + } + + private void ClearCultureInfo(string culture) + { + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); } - /// - public TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) + if (string.IsNullOrWhiteSpace(culture)) { - if (!Properties.TryGetValue(propertyTypeAlias, out var property)) - return default; - - var convertAttempt = property?.GetValue(culture, segment, published).TryConvertTo(); - return convertAttempt?.Success is not null && (convertAttempt?.Success ?? false) ? convertAttempt.Value.Result : default; + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); } - /// - public void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null) + if (_cultureInfos == null) { - if (!Properties.TryGetValue(propertyTypeAlias, out var property)) - throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); - - property?.SetValue(value, culture, segment); - - //bump the culture to be flagged for updating - this.TouchCulture(culture); + return; } - #endregion - - #region Dirty - - public override void ResetWereDirtyProperties() + _cultureInfos.Remove(culture); + if (_cultureInfos.Count == 0) + { + _cultureInfos = null; + } + } + + /// + /// Handles culture infos collection changes. + /// + private void CultureInfosCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(CultureInfos)); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentCultureChanges.addedCultures == null) + { + _currentCultureChanges.addedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (_currentCultureChanges.updatedCultures == null) + { + _currentCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentCultureChanges.addedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.removedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + // Remove listening for changes + ContentCultureInfos? cultureInfo = e.OldItems?.Cast().First(); + if (_currentCultureChanges.removedCultures == null) + { + _currentCultureChanges.removedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentCultureChanges.removedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); + _currentCultureChanges.addedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Replace occurs when an Update occurs + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentCultureChanges.updatedCultures == null) + { + _currentCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); + } + + break; + } + } + } + + #endregion + + #region Has, Get, Set, Publish Property Value + + /// + public bool HasProperty(string propertyTypeAlias) + => Properties.Contains(propertyTypeAlias); + + /// + public object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) => + Properties.TryGetValue(propertyTypeAlias, out IProperty? property) + ? property?.GetValue(culture, segment, published) + : null; + + /// + public TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) + { + if (!Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) + { + return default; + } + + Attempt? convertAttempt = property?.GetValue(culture, segment, published).TryConvertTo(); + return convertAttempt?.Success is not null && (convertAttempt?.Success ?? false) + ? convertAttempt.Value.Result + : default; + } + + /// + public void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null) + { + if (!Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) + { + throw new InvalidOperationException( + $"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); + } + + property?.SetValue(value, culture, segment); + + // bump the culture to be flagged for updating + this.TouchCulture(culture); + } + + #endregion + + #region Dirty + + public override void ResetWereDirtyProperties() + { + base.ResetWereDirtyProperties(); + _previousCultureChanges.addedCultures = null; + _previousCultureChanges.removedCultures = null; + _previousCultureChanges.updatedCultures = null; + } + + /// + /// Overridden to include user properties. + public override void ResetDirtyProperties(bool rememberDirty) + { + base.ResetDirtyProperties(rememberDirty); + + if (rememberDirty) + { + _previousCultureChanges.addedCultures = + _currentCultureChanges.addedCultures == null || _currentCultureChanges.addedCultures.Count == 0 + ? null + : new HashSet( + _currentCultureChanges.addedCultures, + StringComparer.InvariantCultureIgnoreCase); + _previousCultureChanges.removedCultures = + _currentCultureChanges.removedCultures == null || _currentCultureChanges.removedCultures.Count == 0 + ? null + : new HashSet( + _currentCultureChanges.removedCultures, + StringComparer.InvariantCultureIgnoreCase); + _previousCultureChanges.updatedCultures = + _currentCultureChanges.updatedCultures == null || _currentCultureChanges.updatedCultures.Count == 0 + ? null + : new HashSet( + _currentCultureChanges.updatedCultures, + StringComparer.InvariantCultureIgnoreCase); + } + else { - base.ResetWereDirtyProperties(); _previousCultureChanges.addedCultures = null; _previousCultureChanges.removedCultures = null; _previousCultureChanges.updatedCultures = null; } - /// - /// Overridden to include user properties. - public override void ResetDirtyProperties(bool rememberDirty) + _currentCultureChanges.addedCultures?.Clear(); + _currentCultureChanges.removedCultures?.Clear(); + _currentCultureChanges.updatedCultures?.Clear(); + + // also reset dirty changes made to user's properties + foreach (IProperty prop in Properties) { - base.ResetDirtyProperties(rememberDirty); - - if (rememberDirty) - { - _previousCultureChanges.addedCultures = _currentCultureChanges.addedCultures == null || _currentCultureChanges.addedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousCultureChanges.removedCultures = _currentCultureChanges.removedCultures == null || _currentCultureChanges.removedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousCultureChanges.updatedCultures = _currentCultureChanges.updatedCultures == null || _currentCultureChanges.updatedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); - } - else - { - _previousCultureChanges.addedCultures = null; - _previousCultureChanges.removedCultures = null; - _previousCultureChanges.updatedCultures = null; - } - _currentCultureChanges.addedCultures?.Clear(); - _currentCultureChanges.removedCultures?.Clear(); - _currentCultureChanges.updatedCultures?.Clear(); - - // also reset dirty changes made to user's properties - foreach (var prop in Properties) - prop.ResetDirtyProperties(rememberDirty); - - // take care of culture infos - if (_cultureInfos == null) return; - - foreach (var cultureInfo in _cultureInfos) - cultureInfo.ResetDirtyProperties(rememberDirty); + prop.ResetDirtyProperties(rememberDirty); } - /// - /// Overridden to include user properties. - public override bool IsDirty() + // take care of culture infos + if (_cultureInfos == null) { - return IsEntityDirty() || this.IsAnyUserPropertyDirty(); + return; } - /// - /// Overridden to include user properties. - public override bool WasDirty() + foreach (ContentCultureInfos cultureInfo in _cultureInfos) { - return WasEntityDirty() || this.WasAnyUserPropertyDirty(); - } - - /// - /// Gets a value indicating whether the current entity's own properties (not user) are dirty. - /// - public bool IsEntityDirty() - { - return base.IsDirty(); - } - - /// - /// Gets a value indicating whether the current entity's own properties (not user) were dirty. - /// - public bool WasEntityDirty() - { - return base.WasDirty(); - } - - /// - /// Overridden to include user properties. - public override bool IsPropertyDirty(string propertyName) - { - if (base.IsPropertyDirty(propertyName)) - return true; - - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); - return _currentCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); - return _currentCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); - return _currentCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return Properties.Contains(propertyName) && (Properties[propertyName]?.IsDirty() ?? false); - } - - /// - /// Overridden to include user properties. - public override bool WasPropertyDirty(string propertyName) - { - if (base.WasPropertyDirty(propertyName)) - return true; - - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); - return _previousCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); - return _previousCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); - return _previousCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return Properties.Contains(propertyName) && (Properties[propertyName]?.WasDirty() ?? false); - } - - /// - /// Overridden to include user properties. - public override IEnumerable GetDirtyProperties() - { - var instanceProperties = base.GetDirtyProperties(); - var propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); - return instanceProperties.Concat(propertyTypes); - } - - /// - /// Overridden to include user properties. - public override IEnumerable GetWereDirtyProperties() - { - var instanceProperties = base.GetWereDirtyProperties(); - var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); - return instanceProperties.Concat(propertyTypes); - } - - #endregion - - /// - /// - /// Overridden to deal with specific object instances - /// - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedContent = (ContentBase)clone; - - //need to manually clone this since it's not settable - clonedContent.ContentType = ContentType; - - //if culture infos exist then deal with event bindings - if (clonedContent._cultureInfos != null) - { - clonedContent._cultureInfos.ClearCollectionChangedEvents(); //clear this event handler if any - clonedContent._cultureInfos = (ContentCultureInfosCollection?)_cultureInfos?.DeepClone(); //manually deep clone - if (clonedContent._cultureInfos is not null) - { - clonedContent._cultureInfos.CollectionChanged += - clonedContent.CultureInfosCollectionChanged; //re-assign correct event handler - } - } - - //if properties exist then deal with event bindings - if (clonedContent._properties != null) - { - clonedContent._properties.ClearCollectionChangedEvents(); //clear this event handler if any - clonedContent._properties = (IPropertyCollection)_properties.DeepClone(); //manually deep clone - clonedContent._properties.CollectionChanged += clonedContent.PropertiesChanged; //re-assign correct event handler - } - - clonedContent._currentCultureChanges.updatedCultures = null; - clonedContent._currentCultureChanges.addedCultures = null; - clonedContent._currentCultureChanges.removedCultures = null; - - clonedContent._previousCultureChanges.updatedCultures = null; - clonedContent._previousCultureChanges.addedCultures = null; - clonedContent._previousCultureChanges.removedCultures = null; + cultureInfo.ResetDirtyProperties(rememberDirty); } } + + /// + /// Overridden to include user properties. + public override bool IsDirty() => IsEntityDirty() || this.IsAnyUserPropertyDirty(); + + /// + /// Overridden to include user properties. + public override bool WasDirty() => WasEntityDirty() || this.WasAnyUserPropertyDirty(); + + /// + /// Gets a value indicating whether the current entity's own properties (not user) are dirty. + /// + public bool IsEntityDirty() => base.IsDirty(); + + /// + /// Gets a value indicating whether the current entity's own properties (not user) were dirty. + /// + public bool WasEntityDirty() => base.WasDirty(); + + /// + /// Overridden to include user properties. + public override bool IsPropertyDirty(string propertyName) + { + if (base.IsPropertyDirty(propertyName)) + { + return true; + } + + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); + return _currentCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); + return _currentCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); + return _currentCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return Properties.Contains(propertyName) && (Properties[propertyName]?.IsDirty() ?? false); + } + + /// + /// Overridden to include user properties. + public override bool WasPropertyDirty(string propertyName) + { + if (base.WasPropertyDirty(propertyName)) + { + return true; + } + + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); + return _previousCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); + return _previousCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); + return _previousCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return Properties.Contains(propertyName) && (Properties[propertyName]?.WasDirty() ?? false); + } + + /// + /// Overridden to include user properties. + public override IEnumerable GetDirtyProperties() + { + IEnumerable instanceProperties = base.GetDirtyProperties(); + IEnumerable propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); + return instanceProperties.Concat(propertyTypes); + } + + /// + /// Overridden to include user properties. + public override IEnumerable GetWereDirtyProperties() + { + IEnumerable instanceProperties = base.GetWereDirtyProperties(); + IEnumerable propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); + return instanceProperties.Concat(propertyTypes); + } + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentBaseExtensions.cs b/src/Umbraco.Core/Models/ContentBaseExtensions.cs index b81cf398bf..656db0f82f 100644 --- a/src/Umbraco.Core/Models/ContentBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentBaseExtensions.cs @@ -1,43 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to IContentBase to get URL segments. +/// +public static class ContentBaseExtensions { + private static DefaultUrlSegmentProvider? _defaultUrlSegmentProvider; + /// - /// Provides extension methods to IContentBase to get URL segments. + /// Gets the URL segment for a specified content and culture. /// - public static class ContentBaseExtensions + /// The content. + /// + /// + /// The culture. + /// The URL segment. + public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable urlSegmentProviders, string? culture = null) { - /// - /// Gets the URL segment for a specified content and culture. - /// - /// The content. - /// - /// - /// The culture. - /// The URL segment. - public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable urlSegmentProviders, string? culture = null) + if (content == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); - - var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); - if (url == null) - { - if (s_defaultUrlSegmentProvider == null) - { - s_defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper); - } - - url = s_defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe - } - - return url; + throw new ArgumentNullException(nameof(content)); } - private static DefaultUrlSegmentProvider? s_defaultUrlSegmentProvider; + if (urlSegmentProviders == null) + { + throw new ArgumentNullException(nameof(urlSegmentProviders)); + } + + var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); + if (url == null) + { + if (_defaultUrlSegmentProvider == null) + { + _defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper); + } + + url = _defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe + } + + return url; } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfos.cs b/src/Umbraco.Core/Models/ContentCultureInfos.cs index 47f0765d63..8975c1fc58 100644 --- a/src/Umbraco.Core/Models/ContentCultureInfos.cs +++ b/src/Umbraco.Core/Models/ContentCultureInfos.cs @@ -1,109 +1,106 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The name of a content variant for a given culture +/// +public class ContentCultureInfos : BeingDirtyBase, IDeepCloneable, IEquatable { + private DateTime _date; + private string? _name; + /// - /// The name of a content variant for a given culture + /// Initializes a new instance of the class. /// - public class ContentCultureInfos : BeingDirtyBase, IDeepCloneable, IEquatable + public ContentCultureInfos(string culture) { - private DateTime _date; - private string? _name; - - /// - /// Initializes a new instance of the class. - /// - public ContentCultureInfos(string culture) + if (culture == null) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - Culture = culture; + throw new ArgumentNullException(nameof(culture)); } - /// - /// Initializes a new instance of the class. - /// - /// Used for cloning, without change tracking. - internal ContentCultureInfos(ContentCultureInfos other) - : this(other.Culture) + if (string.IsNullOrWhiteSpace(culture)) { - _name = other.Name; - _date = other.Date; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); } - /// - /// Gets the culture. - /// - public string Culture { get; } + Culture = culture; + } - /// - /// Gets the name. - /// - public string? Name + /// + /// Initializes a new instance of the class. + /// + /// Used for cloning, without change tracking. + internal ContentCultureInfos(ContentCultureInfos other) + : this(other.Culture) + { + _name = other.Name; + _date = other.Date; + } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the name. + /// + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets the date. + /// + public DateTime Date + { + get => _date; + set => SetPropertyValueAndDetectChanges(value, ref _date, nameof(Date)); + } + + /// + public object DeepClone() => new ContentCultureInfos(this); + + /// + public bool Equals(ContentCultureInfos? other) => other != null && Culture == other.Culture && Name == other.Name; + + /// + public override bool Equals(object? obj) => obj is ContentCultureInfos other && Equals(other); + + /// + public override int GetHashCode() + { + var hashCode = 479558943; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Culture); + if (Name is not null) { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Name); } - /// - /// Gets the date. - /// - public DateTime Date - { - get => _date; - set => SetPropertyValueAndDetectChanges(value, ref _date, nameof(Date)); - } + return hashCode; + } - /// - public object DeepClone() - { - return new ContentCultureInfos(this); - } + /// + /// Deconstructs into culture and name. + /// + public void Deconstruct(out string culture, out string? name) + { + culture = Culture; + name = Name; + } - /// - public override bool Equals(object? obj) - { - return obj is ContentCultureInfos other && Equals(other); - } - - /// - public bool Equals(ContentCultureInfos? other) - { - return other != null && Culture == other.Culture && Name == other.Name; - } - - /// - public override int GetHashCode() - { - var hashCode = 479558943; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Culture); - if (Name is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name); - } - - return hashCode; - } - - /// - /// Deconstructs into culture and name. - /// - public void Deconstruct(out string culture, out string? name) - { - culture = Culture; - name = Name; - } - - /// - /// Deconstructs into culture, name and date. - /// - public void Deconstruct(out string culture, out string? name, out DateTime date) - { - Deconstruct(out culture, out name); - date = Date; - } + /// + /// Deconstructs into culture, name and date. + /// + public void Deconstruct(out string culture, out string? name, out DateTime date) + { + Deconstruct(out culture, out name); + date = Date; } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs index 0d480ede6d..cf8a2f0328 100644 --- a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs +++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs @@ -1,60 +1,65 @@ -using System; using System.Collections.Specialized; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The culture names of a content's variants +/// +public class ContentCultureInfosCollection : ObservableDictionary, IDeepCloneable { /// - /// The culture names of a content's variants + /// Initializes a new instance of the class. /// - public class ContentCultureInfosCollection : ObservableDictionary, IDeepCloneable + public ContentCultureInfosCollection() + : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) { - /// - /// Initializes a new instance of the class. - /// - public ContentCultureInfosCollection() - : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) - { } + } - /// - /// Adds or updates a instance. - /// - public void AddOrUpdate(string culture, string? name, DateTime date) + /// + public object DeepClone() + { + var clone = new ContentCultureInfosCollection(); + + foreach (ContentCultureInfos item in this) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - culture = culture.ToLowerInvariant(); - - if (TryGetValue(culture, out var item)) - { - item.Name = name; - item.Date = date; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, item)); - } - else - { - Add(new ContentCultureInfos(culture) - { - Name = name, - Date = date - }); - } + var itemClone = (ContentCultureInfos)item.DeepClone(); + itemClone.ResetDirtyProperties(false); + clone.Add(itemClone); } - /// - public object DeepClone() + return clone; + } + + /// + /// Adds or updates a instance. + /// + public void AddOrUpdate(string culture, string? name, DateTime date) + { + if (culture == null) { - var clone = new ContentCultureInfosCollection(); + throw new ArgumentNullException(nameof(culture)); + } - foreach (var item in this) - { - var itemClone = (ContentCultureInfos) item.DeepClone(); - itemClone.ResetDirtyProperties(false); - clone.Add(itemClone); - } + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } - return clone; + culture = culture.ToLowerInvariant(); + + if (TryGetValue(culture, out ContentCultureInfos item)) + { + item.Name = name; + item.Date = date; + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, item)); + } + else + { + Add(new ContentCultureInfos(culture) { Name = name, Date = date }); } } } diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs index 8a13a26e40..f4ad0b0dfc 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs @@ -1,48 +1,42 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class ContentDataIntegrityReport { - public class ContentDataIntegrityReport + public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues) => + DetectedIssues = detectedIssues; + + public enum IssueType { - public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues) - { - DetectedIssues = detectedIssues; - } + /// + /// The item's level and path are inconsistent with it's parent's path and level + /// + InvalidPathAndLevelByParentId, - public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed); + /// + /// The item's path doesn't contain all required parts + /// + InvalidPathEmpty, - public IReadOnlyDictionary DetectedIssues { get; } + /// + /// The item's path parts are inconsistent with it's level value + /// + InvalidPathLevelMismatch, - public IReadOnlyDictionary FixedIssues - => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value); + /// + /// The item's path does not end with it's own ID + /// + InvalidPathById, - public enum IssueType - { - /// - /// The item's level and path are inconsistent with it's parent's path and level - /// - InvalidPathAndLevelByParentId, - - /// - /// The item's path doesn't contain all required parts - /// - InvalidPathEmpty, - - /// - /// The item's path parts are inconsistent with it's level value - /// - InvalidPathLevelMismatch, - - /// - /// The item's path does not end with it's own ID - /// - InvalidPathById, - - /// - /// The item's path does not have it's parent Id as the 2nd last entry - /// - InvalidPathByParentId, - } + /// + /// The item's path does not have it's parent Id as the 2nd last entry + /// + InvalidPathByParentId, } + + public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed); + + public IReadOnlyDictionary DetectedIssues { get; } + + public IReadOnlyDictionary FixedIssues + => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value); } diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs index e6138addbc..fe0ebce549 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs @@ -1,13 +1,10 @@ -namespace Umbraco.Cms.Core.Models -{ - public class ContentDataIntegrityReportEntry - { - public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) - { - IssueType = issueType; - } +namespace Umbraco.Cms.Core.Models; - public ContentDataIntegrityReport.IssueType IssueType { get; } - public bool Fixed { get; set; } - } +public class ContentDataIntegrityReportEntry +{ + public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) => IssueType = issueType; + + public ContentDataIntegrityReport.IssueType IssueType { get; } + + public bool Fixed { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs index 52ea3d4032..40657069ff 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Models -{ - public class ContentDataIntegrityReportOptions - { - /// - /// Set to true to try to automatically resolve data integrity issues - /// - public bool FixIssues { get; set; } +namespace Umbraco.Cms.Core.Models; - // TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc... - // things like Tag data consistency, etc... - } +public class ContentDataIntegrityReportOptions +{ + /// + /// Set to true to try to automatically resolve data integrity issues + /// + public bool FixIssues { get; set; } + + // TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc... + // things like Tag data consistency, etc... } diff --git a/src/Umbraco.Core/Models/ContentEditing/AssignedContentPermissions.cs b/src/Umbraco.Core/Models/ContentEditing/AssignedContentPermissions.cs index 2c73bcd590..18229d2124 100644 --- a/src/Umbraco.Core/Models/ContentEditing/AssignedContentPermissions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/AssignedContentPermissions.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The permissions assigned to a content node +/// +/// +/// The underlying data such as Name, etc... is that of the Content item +/// +[DataContract(Name = "contentPermissions", Namespace = "")] +public class AssignedContentPermissions : EntityBasic { /// - /// The permissions assigned to a content node + /// The assigned permissions to the content item organized by permission group name /// - /// - /// The underlying data such as Name, etc... is that of the Content item - /// - [DataContract(Name = "contentPermissions", Namespace = "")] - public class AssignedContentPermissions : EntityBasic - { - /// - /// The assigned permissions to the content item organized by permission group name - /// - [DataMember(Name = "permissions")] - public IDictionary>? AssignedPermissions { get; set; } - } + [DataMember(Name = "permissions")] + public IDictionary>? AssignedPermissions { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs b/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs index b6aca05515..867784d19d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs @@ -1,42 +1,40 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The user group permissions assigned to a content node +/// +/// +/// The underlying data such as Name, etc... is that of the User Group +/// +[DataContract(Name = "userGroupPermissions", Namespace = "")] +public class AssignedUserGroupPermissions : EntityBasic { /// - /// The user group permissions assigned to a content node + /// The assigned permissions for the user group organized by permission group name /// - /// - /// The underlying data such as Name, etc... is that of the User Group - /// - [DataContract(Name = "userGroupPermissions", Namespace = "")] - public class AssignedUserGroupPermissions : EntityBasic + [DataMember(Name = "permissions")] + public IDictionary>? AssignedPermissions { get; set; } + + /// + /// The default permissions for the user group organized by permission group name + /// + [DataMember(Name = "defaultPermissions")] + public IDictionary>? DefaultPermissions { get; set; } + + public static IDictionary> ClonePermissions( + IDictionary>? permissions) { - /// - /// The assigned permissions for the user group organized by permission group name - /// - [DataMember(Name = "permissions")] - public IDictionary>? AssignedPermissions { get; set; } - - /// - /// The default permissions for the user group organized by permission group name - /// - [DataMember(Name = "defaultPermissions")] - public IDictionary>? DefaultPermissions { get; set; } - - public static IDictionary> ClonePermissions(IDictionary>? permissions) + var result = new Dictionary>(); + if (permissions is not null) { - var result = new Dictionary>(); - if (permissions is not null) + foreach (KeyValuePair> permission in permissions) { - foreach (var permission in permissions) - { - result[permission.Key] = new List(permission.Value.Select(x => (Permission)x.Clone())); - } + result[permission.Key] = new List(permission.Value.Select(x => (Permission)x.Clone())); } - - return result; } + + return result; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs b/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs index 5f40ace6ca..e7b744bd59 100644 --- a/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs +++ b/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs @@ -1,36 +1,34 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "auditLog", Namespace = "")] +public class AuditLog { - [DataContract(Name = "auditLog", Namespace = "")] - public class AuditLog - { - [DataMember(Name = "userId")] - public int UserId { get; set; } + [DataMember(Name = "userId")] + public int UserId { get; set; } - [DataMember(Name = "userName")] - public string? UserName { get; set; } + [DataMember(Name = "userName")] + public string? UserName { get; set; } - [DataMember(Name = "userAvatars")] - public string[]? UserAvatars { get; set; } + [DataMember(Name = "userAvatars")] + public string[]? UserAvatars { get; set; } - [DataMember(Name = "nodeId")] - public int NodeId { get; set; } + [DataMember(Name = "nodeId")] + public int NodeId { get; set; } - [DataMember(Name = "timestamp")] - public DateTime Timestamp { get; set; } + [DataMember(Name = "timestamp")] + public DateTime Timestamp { get; set; } - [DataMember(Name = "logType")] - public string? LogType { get; set; } + [DataMember(Name = "logType")] + public string? LogType { get; set; } - [DataMember(Name = "entityType")] - public string? EntityType { get; set; } + [DataMember(Name = "entityType")] + public string? EntityType { get; set; } - [DataMember(Name = "comment")] - public string? Comment { get; set; } + [DataMember(Name = "comment")] + public string? Comment { get; set; } - [DataMember(Name = "parameters")] - public string? Parameters { get; set; } - } + [DataMember(Name = "parameters")] + public string? Parameters { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs b/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs index 982f46d912..1cf1e60e25 100644 --- a/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs +++ b/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs @@ -1,29 +1,27 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "notification", Namespace = "")] +public class BackOfficeNotification { - [DataContract(Name = "notification", Namespace = "")] - public class BackOfficeNotification + public BackOfficeNotification() { - public BackOfficeNotification() - { - - } - - public BackOfficeNotification(string header, string message, NotificationStyle notificationType) - { - Header = header; - Message = message; - NotificationType = notificationType; - } - - [DataMember(Name = "header")] - public string? Header { get; set; } - - [DataMember(Name = "message")] - public string? Message { get; set; } - - [DataMember(Name = "type")] - public NotificationStyle NotificationType { get; set; } } + + public BackOfficeNotification(string header, string message, NotificationStyle notificationType) + { + Header = header; + Message = message; + NotificationType = notificationType; + } + + [DataMember(Name = "header")] + public string? Header { get; set; } + + [DataMember(Name = "message")] + public string? Message { get; set; } + + [DataMember(Name = "type")] + public NotificationStyle NotificationType { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs index 37243c702c..b172fccb5a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs @@ -1,76 +1,70 @@ -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "scriptFile", Namespace = "")] +public class CodeFileDisplay : INotificationModel, IValidatableObject { - [DataContract(Name = "scriptFile", Namespace = "")] - public class CodeFileDisplay : INotificationModel, IValidatableObject + public CodeFileDisplay() => Notifications = new List(); + + /// + /// VirtualPath is the path to the file on disk + /// /views/partials/file.cshtml + /// + [DataMember(Name = "virtualPath", IsRequired = true)] + public string? VirtualPath { get; set; } + + /// + /// Path represents the path used by the backoffice tree + /// For files stored on disk, this is a URL encoded, comma separated + /// path to the file, always starting with -1. + /// -1,Partials,Parials%2FFolder,Partials%2FFolder%2FFile.cshtml + /// + [DataMember(Name = "path")] + [ReadOnly(true)] + public string? Path { get; set; } + + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } + + [DataMember(Name = "content", IsRequired = true)] + public string? Content { get; set; } + + [DataMember(Name = "fileType", IsRequired = true)] + public string? FileType { get; set; } + + [DataMember(Name = "snippet")] + [ReadOnly(true)] + public string? Snippet { get; set; } + + [DataMember(Name = "id")] + [ReadOnly(true)] + public string? Id { get; set; } + + public List Notifications { get; } + + /// + /// Some custom validation is required for valid file names + /// + /// + /// + public IEnumerable Validate(ValidationContext validationContext) { - public CodeFileDisplay() + var illegalChars = System.IO.Path.GetInvalidFileNameChars(); + if (Name?.ContainsAny(illegalChars) ?? false) { - Notifications = new List(); + yield return new ValidationResult( + "The file name cannot contain illegal characters", + new[] { "Name" }); } - - /// - /// VirtualPath is the path to the file on disk - /// /views/partials/file.cshtml - /// - [DataMember(Name = "virtualPath", IsRequired = true)] - public string? VirtualPath { get; set; } - - /// - /// Path represents the path used by the backoffice tree - /// For files stored on disk, this is a URL encoded, comma separated - /// path to the file, always starting with -1. - /// - /// -1,Partials,Parials%2FFolder,Partials%2FFolder%2FFile.cshtml - /// - [DataMember(Name = "path")] - [ReadOnly(true)] - public string? Path { get; set; } - - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } - - [DataMember(Name = "content", IsRequired = true)] - public string? Content { get; set; } - - [DataMember(Name = "fileType", IsRequired = true)] - public string? FileType { get; set; } - - [DataMember(Name = "snippet")] - [ReadOnly(true)] - public string? Snippet { get; set; } - - [DataMember(Name = "id")] - [ReadOnly(true)] - public string? Id { get; set; } - - public List Notifications { get; private set; } - - /// - /// Some custom validation is required for valid file names - /// - /// - /// - public IEnumerable Validate(ValidationContext validationContext) + else if (System.IO.Path.GetFileNameWithoutExtension(Name).IsNullOrWhiteSpace()) { - var illegalChars = System.IO.Path.GetInvalidFileNameChars(); - if (Name?.ContainsAny(illegalChars) ?? false) - { - yield return new ValidationResult( - "The file name cannot contain illegal characters", - new[] { "Name" }); - } - else if (System.IO.Path.GetFileNameWithoutExtension(Name).IsNullOrWhiteSpace()) - { - yield return new ValidationResult( - "The file name cannot be empty", - new[] { "Name" }); - } + yield return new ValidationResult( + "The file name cannot be empty", + new[] { "Name" }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs index 3dc88df3dd..02c32adc54 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs @@ -1,78 +1,78 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content app. +/// +/// +/// Content apps are editor extensions. +/// +[DataContract(Name = "app", Namespace = "")] +public class ContentApp { /// - /// Represents a content app. + /// Gets the name of the content app. + /// + [DataMember(Name = "name")] + public string? Name { get; set; } + + /// + /// Gets the unique alias of the content app. /// /// - /// Content apps are editor extensions. + /// Must be a valid javascript identifier, ie no spaces etc. /// - [DataContract(Name = "app", Namespace = "")] - public class ContentApp - { - /// - /// Gets the name of the content app. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - /// - /// Gets the unique alias of the content app. - /// - /// - /// Must be a valid javascript identifier, ie no spaces etc. - /// - [DataMember(Name = "alias")] - public string? Alias { get; set; } + /// + /// Gets or sets the weight of the content app. + /// + /// + /// Content apps are ordered by weight, from left (lowest values) to right (highest values). + /// Some built-in apps have special weights: listview is -666, content is -100 and infos is +100. + /// + /// The default weight is 0, meaning somewhere in-between content and infos, but weight could + /// be used for ordering between user-level apps, or anything really. + /// + /// + [DataMember(Name = "weight")] + public int Weight { get; set; } - /// - /// Gets or sets the weight of the content app. - /// - /// - /// Content apps are ordered by weight, from left (lowest values) to right (highest values). - /// Some built-in apps have special weights: listview is -666, content is -100 and infos is +100. - /// The default weight is 0, meaning somewhere in-between content and infos, but weight could - /// be used for ordering between user-level apps, or anything really. - /// - [DataMember(Name = "weight")] - public int Weight { get; set; } + /// + /// Gets the icon of the content app. + /// + /// + /// Must be a valid helveticons class name (see http://hlvticons.ch/). + /// + [DataMember(Name = "icon")] + public string? Icon { get; set; } - /// - /// Gets the icon of the content app. - /// - /// - /// Must be a valid helveticons class name (see http://hlvticons.ch/). - /// - [DataMember(Name = "icon")] - public string? Icon { get; set; } + /// + /// Gets the view for rendering the content app. + /// + [DataMember(Name = "view")] + public string? View { get; set; } - /// - /// Gets the view for rendering the content app. - /// - [DataMember(Name = "view")] - public string? View { get; set; } + /// + /// The view model specific to this app + /// + [DataMember(Name = "viewModel")] + public object? ViewModel { get; set; } - /// - /// The view model specific to this app - /// - [DataMember(Name = "viewModel")] - public object? ViewModel { get; set; } + /// + /// Gets a value indicating whether the app is active. + /// + /// + /// Normally reserved for Angular to deal with but in some cases this can be set on the server side. + /// + [DataMember(Name = "active")] + public bool Active { get; set; } - /// - /// Gets a value indicating whether the app is active. - /// - /// - /// Normally reserved for Angular to deal with but in some cases this can be set on the server side. - /// - [DataMember(Name = "active")] - public bool Active { get; set; } - - /// - /// Gets or sets the content app badge. - /// - [DataMember(Name = "badge")] - public ContentAppBadge? Badge { get; set; } - } + /// + /// Gets or sets the content app badge. + /// + [DataMember(Name = "badge")] + public ContentAppBadge? Badge { get; set; } } - diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs index 4e1089c97b..7a50073d17 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs @@ -1,37 +1,33 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content app badge +/// +[DataContract(Name = "badge", Namespace = "")] +public class ContentAppBadge { /// - /// Represents a content app badge + /// Initializes a new instance of the class. /// - [DataContract(Name = "badge", Namespace = "")] - public class ContentAppBadge - { - /// - /// Initializes a new instance of the class. - /// - public ContentAppBadge() - { - this.Type = ContentAppBadgeType.Default; - } + public ContentAppBadge() => Type = ContentAppBadgeType.Default; - /// - /// Gets or sets the number displayed in the badge - /// - [DataMember(Name = "count")] - public int Count { get; set; } + /// + /// Gets or sets the number displayed in the badge + /// + [DataMember(Name = "count")] + public int Count { get; set; } - /// - /// Gets or sets the type of badge to display - /// - /// - /// This controls the background color of the badge. - /// Warning will display a dark yellow badge - /// Alert will display a red badge - /// Default will display a turquoise badge - /// - [DataMember(Name = "type")] - public ContentAppBadgeType Type { get; set; } - } + /// + /// Gets or sets the type of badge to display + /// + /// + /// This controls the background color of the badge. + /// Warning will display a dark yellow badge + /// Alert will display a red badge + /// Default will display a turquoise badge + /// + [DataMember(Name = "type")] + public ContentAppBadgeType Type { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs index 9bcadd1383..718b36db33 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs @@ -1,25 +1,24 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +// TODO: This was marked with `[StringEnumConverter]` to inform the serializer +// to serialize the values to string instead of INT (which is the default) +// so we need to either invent our own attribute and make the implementation aware of it +// or ... something else? + +/// +/// Represent the content app badge types +/// +[DataContract(Name = "contentAppBadgeType")] +public enum ContentAppBadgeType { - // TODO: This was marked with `[StringEnumConverter]` to inform the serializer - // to serialize the values to string instead of INT (which is the default) - // so we need to either invent our own attribute and make the implementation aware of it - // or ... something else? + [EnumMember(Value = "default")] + Default = 0, - /// - /// Represent the content app badge types - /// - [DataContract(Name = "contentAppBadgeType")] - public enum ContentAppBadgeType - { - [EnumMember(Value = "default")] - Default = 0, + [EnumMember(Value = "warning")] + Warning = 1, - [EnumMember(Value = "warning")] - Warning = 1, - - [EnumMember(Value = "alert")] - Alert = 2 - } + [EnumMember(Value = "alert")] + Alert = 2, } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs index d7f026aeab..241cde46b4 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs @@ -1,62 +1,61 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a content item to be saved +/// +[DataContract(Name = "content", Namespace = "")] +public abstract class ContentBaseSave : ContentItemBasic, IContentSave + where TPersisted : IContentBase { - /// - /// A model representing a content item to be saved - /// - [DataContract(Name = "content", Namespace = "")] - public abstract class ContentBaseSave : ContentItemBasic, IContentSave - where TPersisted : IContentBase + protected ContentBaseSave() => UploadedFiles = new List(); + + #region IContentSave + + /// + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } + + [DataMember(Name = "properties")] + public override IEnumerable Properties { - protected ContentBaseSave() - { - UploadedFiles = new List(); - } - - #region IContentSave - /// - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } - - [DataMember(Name = "properties")] - public override IEnumerable Properties - { - get => base.Properties; - set => base.Properties = value; - } - - [IgnoreDataMember] - public List UploadedFiles { get; } - - //These need explicit implementation because we are using internal models - /// - [IgnoreDataMember] - TPersisted IContentSave.PersistedContent { get; set; } = default!; - - //Non explicit internal getter so we don't need to explicitly cast in our own code - [IgnoreDataMember] - public TPersisted PersistedContent - { - get => ((IContentSave)this).PersistedContent; - set => ((IContentSave) this).PersistedContent = value; - } - - /// - /// The property DTO object is used to gather all required property data including data type information etc... for use with validation - used during inbound model binding - /// - /// - /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need - /// instead of having to look up all the data individually. - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } - - #endregion + get => base.Properties; + set => base.Properties = value; } + + [IgnoreDataMember] + public List UploadedFiles { get; } + + // These need explicit implementation because we are using internal models + + /// + [IgnoreDataMember] + TPersisted IContentSave.PersistedContent { get; set; } = default!; + + // Non explicit internal getter so we don't need to explicitly cast in our own code + [IgnoreDataMember] + public TPersisted PersistedContent + { + get => ((IContentSave)this).PersistedContent; + set => ((IContentSave)this).PersistedContent = value; + } + + /// + /// The property DTO object is used to gather all required property data including data type information etc... for use + /// with validation - used during inbound model binding + /// + /// + /// We basically use this object to hydrate all required data from the database into one object so we can validate + /// everything we need + /// instead of having to look up all the data individually. + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs b/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs index 86af3de89a..ca24b08567 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - [DataContract(Name = "ContentDomainsAndCulture")] - public class ContentDomainsAndCulture - { - [DataMember(Name = "domains")] - public IEnumerable? Domains { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "language")] - public string? Language { get; set; } - } +[DataContract(Name = "ContentDomainsAndCulture")] +public class ContentDomainsAndCulture +{ + [DataMember(Name = "domains")] + public IEnumerable? Domains { get; set; } + + [DataMember(Name = "language")] + public string? Language { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs index 9b1fcdc129..fd277308f7 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs @@ -1,106 +1,105 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a basic content item +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentItemBasic : EntityBasic { + [DataMember(Name = "updateDate")] + public DateTime UpdateDate { get; set; } + + [DataMember(Name = "createDate")] + public DateTime CreateDate { get; set; } + /// - /// A model representing a basic content item + /// Boolean indicating if this item is published or not based on it's /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemBasic : EntityBasic + [DataMember(Name = "published")] + public bool Published => State == ContentSavedState.Published || State == ContentSavedState.PublishedPendingChanges; + + /// + /// Determines if the content item is a draft + /// + [DataMember(Name = "edited")] + public bool Edited { get; set; } + + [DataMember(Name = "owner")] + public UserProfile? Owner { get; set; } + + [DataMember(Name = "updater")] + public UserProfile? Updater { get; set; } + + public int ContentTypeId { get; set; } + + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + /// + /// The saved/published state of an item + /// + /// + /// This is nullable since it's only relevant for content (non-content like media + members will be null) + /// + [DataMember(Name = "state")] + public ContentSavedState? State { get; set; } + + [DataMember(Name = "variesByCulture")] + public bool VariesByCulture { get; set; } + + public override bool Equals(object? obj) { - [DataMember(Name = "updateDate")] - public DateTime UpdateDate { get; set; } - - [DataMember(Name = "createDate")] - public DateTime CreateDate { get; set; } - - /// - /// Boolean indicating if this item is published or not based on it's - /// - [DataMember(Name = "published")] - public bool Published => State == ContentSavedState.Published || State == ContentSavedState.PublishedPendingChanges; - - /// - /// Determines if the content item is a draft - /// - [DataMember(Name = "edited")] - public bool Edited { get; set; } - - [DataMember(Name = "owner")] - public UserProfile? Owner { get; set; } - - [DataMember(Name = "updater")] - public UserProfile? Updater { get; set; } - - public int ContentTypeId { get; set; } - - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; - - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - - /// - /// The saved/published state of an item - /// - /// - /// This is nullable since it's only relevant for content (non-content like media + members will be null) - /// - [DataMember(Name = "state")] - public ContentSavedState? State { get; set; } - - [DataMember(Name = "variesByCulture")] - public bool VariesByCulture { get; set; } - - protected bool Equals(ContentItemBasic other) + if (ReferenceEquals(null, obj)) { - return Id == other.Id; + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - var other = obj as ContentItemBasic; - return other != null && Equals(other); + return true; } - public override int GetHashCode() - { - if (Id is not null) - { - return Id.GetHashCode(); - } - - return base.GetHashCode(); - } + return obj is ContentItemBasic other && Equals(other); } - /// - /// A model representing a basic content item with properties - /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemBasic : ContentItemBasic, IContentProperties - where T : ContentPropertyBasic + protected bool Equals(ContentItemBasic other) => Id == other.Id; + + public override int GetHashCode() { - public ContentItemBasic() + if (Id is not null) { - //ensure its not null - _properties = Enumerable.Empty(); + return Id.GetHashCode(); } - private IEnumerable _properties; - - [DataMember(Name = "properties")] - public virtual IEnumerable Properties - { - get => _properties; - set => _properties = value; - } + return base.GetHashCode(); + } +} + +/// +/// A model representing a basic content item with properties +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentItemBasic : ContentItemBasic, IContentProperties + where T : ContentPropertyBasic +{ + private IEnumerable _properties; + + public ContentItemBasic() => + + // ensure its not null + _properties = Enumerable.Empty(); + + [DataMember(Name = "properties")] + public virtual IEnumerable Properties + { + get => _properties; + set => _properties = value; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs index 874f2f085a..eb800791a2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs @@ -1,219 +1,225 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - public class ContentItemDisplay : ContentItemDisplay { } +namespace Umbraco.Cms.Core.Models.ContentEditing; - public class ContentItemDisplayWithSchedule : ContentItemDisplay { } +public class ContentItemDisplay : ContentItemDisplay +{ +} + +public class ContentItemDisplayWithSchedule : ContentItemDisplay +{ +} + +/// +/// A model representing a content item to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentItemDisplay : + INotificationModel, + IErrorModel // ListViewAwareContentItemDisplayBase + where TVariant : ContentVariantDisplay +{ + public ContentItemDisplay() + { + AllowPreview = true; + Notifications = new List(); + Errors = new Dictionary(); + Variants = new List(); + ContentApps = new List(); + } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } + + [DataMember(Name = "udi")] + [ReadOnly(true)] + public Udi? Udi { get; set; } + + [DataMember(Name = "icon")] + public string? Icon { get; set; } + + [DataMember(Name = "trashed")] + [ReadOnly(true)] + public bool Trashed { get; set; } /// - /// A model representing a content item to be displayed in the back office + /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemDisplay : INotificationModel, IErrorModel //ListViewAwareContentItemDisplayBase - where TVariant : ContentVariantDisplay - { - public ContentItemDisplay() - { - AllowPreview = true; - Notifications = new List(); - Errors = new Dictionary(); - Variants = new List(); - ContentApps = new List(); - } + [DataMember(Name = "key")] + public Guid? Key { get; set; } - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int? ParentId { get; set; } - [DataMember(Name = "udi")] - [ReadOnly(true)] - public Udi? Udi { get; set; } + /// + /// The path of the entity + /// + [DataMember(Name = "path")] + public string? Path { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } + /// + /// A collection of content variants + /// + /// + /// If a content item is invariant, this collection will only contain one item, else it will contain all culture + /// variants + /// + [DataMember(Name = "variants")] + public IEnumerable Variants { get; set; } - [DataMember(Name = "trashed")] - [ReadOnly(true)] - public bool Trashed { get; set; } + [DataMember(Name = "owner")] + public UserProfile? Owner { get; set; } - /// - /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider - /// - [DataMember(Name = "key")] - public Guid? Key { get; set; } + [DataMember(Name = "updater")] + public UserProfile? Updater { get; set; } - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int? ParentId { get; set; } + /// + /// The name of the content type + /// + [DataMember(Name = "contentTypeName")] + public string? ContentTypeName { get; set; } - /// - /// The path of the entity - /// - [DataMember(Name = "path")] - public string? Path { get; set; } + /// + /// Indicates if the content is configured as a list view container + /// + [DataMember(Name = "isContainer")] + public bool IsContainer { get; set; } - /// - /// A collection of content variants - /// - /// - /// If a content item is invariant, this collection will only contain one item, else it will contain all culture variants - /// - [DataMember(Name = "variants")] - public IEnumerable Variants { get; set; } + /// + /// Indicates if the content is configured as an element + /// + [DataMember(Name = "isElement")] + public bool IsElement { get; set; } - [DataMember(Name = "owner")] - public UserProfile? Owner { get; set; } + /// + /// Property indicating if this item is part of a list view parent + /// + [DataMember(Name = "isChildOfListView")] + public bool IsChildOfListView { get; set; } - [DataMember(Name = "updater")] - public UserProfile? Updater { get; set; } + /// + /// Property for the entity's individual tree node URL + /// + /// + /// This is required if the item is a child of a list view since the tree won't actually be loaded, + /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) + /// + [DataMember(Name = "treeNodeUrl")] + public string? TreeNodeUrl { get; set; } - /// - /// The name of the content type - /// - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } + [DataMember(Name = "contentTypeId")] + public int? ContentTypeId { get; set; } - /// - /// Indicates if the content is configured as a list view container - /// - [DataMember(Name = "isContainer")] - public bool IsContainer { get; set; } + [DataMember(Name = "contentTypeKey")] + public Guid ContentTypeKey { get; set; } - /// - /// Indicates if the content is configured as an element - /// - [DataMember(Name = "isElement")] - public bool IsElement { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; - /// - /// Property indicating if this item is part of a list view parent - /// - [DataMember(Name = "isChildOfListView")] - public bool IsChildOfListView { get; set; } + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } - /// - /// Property for the entity's individual tree node URL - /// - /// - /// This is required if the item is a child of a list view since the tree won't actually be loaded, - /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) - /// - [DataMember(Name = "treeNodeUrl")] - public string? TreeNodeUrl { get; set; } + /// + /// This is the last updated date for the entire content object regardless of variants + /// + /// + /// Each variant has it's own update date assigned as well + /// + [DataMember(Name = "updateDate")] + public DateTime UpdateDate { get; set; } - [DataMember(Name = "contentTypeId")] - public int? ContentTypeId { get; set; } + [DataMember(Name = "template")] + public string? TemplateAlias { get; set; } - [DataMember(Name = "contentTypeKey")] - public Guid ContentTypeKey { get; set; } + [DataMember(Name = "templateId")] + public int TemplateId { get; set; } - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; + [DataMember(Name = "allowedTemplates")] + public IDictionary? AllowedTemplates { get; set; } - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } + [DataMember(Name = "documentType")] + public ContentTypeBasic? DocumentType { get; set; } - /// - /// This is the last updated date for the entire content object regardless of variants - /// - /// - /// Each variant has it's own update date assigned as well - /// - [DataMember(Name = "updateDate")] - public DateTime UpdateDate { get; set; } + [DataMember(Name = "urls")] + public UrlInfo[]? Urls { get; set; } - [DataMember(Name = "template")] - public string? TemplateAlias { get; set; } + /// + /// Determines whether previewing is allowed for this node + /// + /// + /// By default this is true but by using events developers can toggle this off for certain documents if there is + /// nothing to preview + /// + [DataMember(Name = "allowPreview")] + public bool AllowPreview { get; set; } - [DataMember(Name = "templateId")] - public int TemplateId { get; set; } + /// + /// The allowed 'actions' based on the user's permissions - Create, Update, Publish, Send to publish + /// + /// + /// Each char represents a button which we can then map on the front-end to the correct actions + /// + [DataMember(Name = "allowedActions")] + public IEnumerable? AllowedActions { get; set; } - [DataMember(Name = "allowedTemplates")] - public IDictionary? AllowedTemplates { get; set; } + [DataMember(Name = "isBlueprint")] + public bool IsBlueprint { get; set; } - [DataMember(Name = "documentType")] - public ContentTypeBasic? DocumentType { get; set; } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } - [DataMember(Name = "urls")] - public UrlInfo[]? Urls { get; set; } + /// + /// The real persisted content object - used during inbound model binding + /// + /// + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public IContent? PersistedContent { get; set; } - /// - /// Determines whether previewing is allowed for this node - /// - /// - /// By default this is true but by using events developers can toggle this off for certain documents if there is nothing to preview - /// - [DataMember(Name = "allowPreview")] - public bool AllowPreview { get; set; } + /// + /// The DTO object used to gather all required content data including data type information etc... for use with + /// validation - used during inbound model binding + /// + /// + /// We basically use this object to hydrate all required data from the database into one object so we can validate + /// everything we need + /// instead of having to look up all the data individually. + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public ContentPropertyCollectionDto? ContentDto { get; set; } - /// - /// The allowed 'actions' based on the user's permissions - Create, Update, Publish, Send to publish - /// - /// - /// Each char represents a button which we can then map on the front-end to the correct actions - /// - [DataMember(Name = "allowedActions")] - public IEnumerable? AllowedActions { get; set; } + /// + /// A collection of extra data that is available for this specific entity/entity type + /// + [DataMember(Name = "metaData")] + [ReadOnly(true)] + public IDictionary? AdditionalData { get; private set; } - [DataMember(Name = "isBlueprint")] - public bool IsBlueprint { get; set; } + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. + /// + [DataMember(Name = "ModelState")] + [ReadOnly(true)] + public IDictionary Errors { get; set; } - [DataMember(Name = "apps")] - public IEnumerable ContentApps { get; set; } - - /// - /// The real persisted content object - used during inbound model binding - /// - /// - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public IContent? PersistedContent { get; set; } - - /// - /// The DTO object used to gather all required content data including data type information etc... for use with validation - used during inbound model binding - /// - /// - /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need - /// instead of having to look up all the data individually. - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public ContentPropertyCollectionDto? ContentDto { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. - /// - [DataMember(Name = "ModelState")] - [ReadOnly(true)] - public IDictionary Errors { get; set; } - - /// - /// A collection of extra data that is available for this specific entity/entity type - /// - [DataMember(Name = "metaData")] - [ReadOnly(true)] - public IDictionary? AdditionalData { get; private set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs index a06fa62b1a..1adf69371b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs @@ -1,49 +1,46 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel + where T : ContentPropertyBasic { - public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel - where T : ContentPropertyBasic + protected ContentItemDisplayBase() { - protected ContentItemDisplayBase() - { - Notifications = new List(); - Errors = new Dictionary(); - } - - /// - /// The name of the content type - /// - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } - - /// - /// Indicates if the content is configured as a list view container - /// - [DataMember(Name = "isContainer")] - public bool IsContainer { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. - /// - [DataMember(Name = "ModelState")] - [ReadOnly(true)] - public IDictionary Errors { get; set; } + Notifications = new List(); + Errors = new Dictionary(); } + + /// + /// The name of the content type + /// + [DataMember(Name = "contentTypeName")] + public string? ContentTypeName { get; set; } + + /// + /// Indicates if the content is configured as a list view container + /// + [DataMember(Name = "isContainer")] + public bool IsContainer { get; set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. + /// + [DataMember(Name = "ModelState")] + [ReadOnly(true)] + public IDictionary Errors { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs index fed33c52b0..400436421b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs @@ -1,66 +1,64 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a content item to be saved +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentItemSave : IContentSave { - /// - /// A model representing a content item to be saved - /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemSave : IContentSave + public ContentItemSave() { - public ContentItemSave() - { - UploadedFiles = new List(); - Variants = new List(); - } - - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } - - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } - - [DataMember(Name = "variants", IsRequired = true)] - public IEnumerable Variants { get; set; } - - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; - - /// - /// The template alias to save - /// - [DataMember(Name = "templateAlias")] - public string? TemplateAlias { get; set; } - - #region IContentSave - - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } - - [IgnoreDataMember] - public List UploadedFiles { get; } - - //These need explicit implementation because we are using internal models - /// - [IgnoreDataMember] - IContent IContentSave.PersistedContent { get; set; } = null!; - - //Non explicit internal getter so we don't need to explicitly cast in our own code - [IgnoreDataMember] - public IContent PersistedContent - { - get => ((IContentSave)this).PersistedContent; - set => ((IContentSave)this).PersistedContent = value; - } - - #endregion - + UploadedFiles = new List(); + Variants = new List(); } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } + + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } + + [DataMember(Name = "variants", IsRequired = true)] + public IEnumerable Variants { get; set; } + + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; + + /// + /// The template alias to save + /// + [DataMember(Name = "templateAlias")] + public string? TemplateAlias { get; set; } + + #region IContentSave + + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } + + [IgnoreDataMember] + public List UploadedFiles { get; } + + // These need explicit implementation because we are using internal models + + /// + [IgnoreDataMember] + IContent IContentSave.PersistedContent { get; set; } = null!; + + // Non explicit internal getter so we don't need to explicitly cast in our own code + [IgnoreDataMember] + public IContent PersistedContent + { + get => ((IContentSave)this).PersistedContent; + set => ((IContentSave)this).PersistedContent = value; + } + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs index ee5a4600d4..c9ac6b2847 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs @@ -1,73 +1,78 @@ -using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content property to be saved +/// +[DataContract(Name = "property", Namespace = "")] +public class ContentPropertyBasic { /// - /// Represents a content property to be saved + /// This is the PropertyData ID /// - [DataContract(Name = "property", Namespace = "")] - public class ContentPropertyBasic - { - /// - /// This is the PropertyData ID - /// - /// - /// This is not really used for anything - /// - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } + /// + /// This is not really used for anything + /// + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } - [DataMember(Name = "dataTypeKey", IsRequired = false)] - [ReadOnly(true)] - public Guid DataTypeKey { get; set; } + [DataMember(Name = "dataTypeKey", IsRequired = false)] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } - [DataMember(Name = "value")] - public object? Value { get; set; } + [DataMember(Name = "value")] + public object? Value { get; set; } - [DataMember(Name = "alias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string Alias { get; set; } = null!; + [DataMember(Name = "alias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string Alias { get; set; } = null!; - [DataMember(Name = "editor", IsRequired = false)] - public string? Editor { get; set; } + [DataMember(Name = "editor", IsRequired = false)] + public string? Editor { get; set; } - /// - /// Flags the property to denote that it can contain sensitive data - /// - [DataMember(Name = "isSensitive", IsRequired = false)] - public bool IsSensitive { get; set; } + /// + /// Flags the property to denote that it can contain sensitive data + /// + [DataMember(Name = "isSensitive", IsRequired = false)] + public bool IsSensitive { get; set; } - /// - /// The culture of the property - /// - /// - /// If this is a variant property then this culture value will be the same as it's variant culture but if this - /// is an invariant property then this will be a null value. - /// - [DataMember(Name = "culture")] - [ReadOnly(true)] - public string? Culture { get; set; } + /// + /// The culture of the property + /// + /// + /// If this is a variant property then this culture value will be the same as it's variant culture but if this + /// is an invariant property then this will be a null value. + /// + [DataMember(Name = "culture")] + [ReadOnly(true)] + public string? Culture { get; set; } - /// - /// The segment of the property - /// - /// - /// The segment value of a property can always be null but can only have a non-null value - /// when the property can be varied by segment. - /// - [DataMember(Name = "segment")] - [ReadOnly(true)] - public string? Segment { get; set; } + /// + /// The segment of the property + /// + /// + /// The segment value of a property can always be null but can only have a non-null value + /// when the property can be varied by segment. + /// + [DataMember(Name = "segment")] + [ReadOnly(true)] + public string? Segment { get; set; } - /// - /// Used internally during model mapping - /// - [IgnoreDataMember] - public IDataEditor? PropertyEditor { get; set; } - } + /// + /// Used internally during model mapping + /// + [IgnoreDataMember] + public IDataEditor? PropertyEditor { get; set; } + + /// + /// Used internally during model mapping + /// + [DataMember(Name = "supportsReadOnly")] + [ReadOnly(true)] + public bool SupportsReadOnly { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs index 3c772c0866..35423f19a8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs @@ -1,21 +1,14 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +/// +/// Used to map property values when saving content/media/members +/// +/// +/// This is only used during mapping operations, it is not used for angular purposes +/// +public class ContentPropertyCollectionDto : IContentProperties { - /// - /// Used to map property values when saving content/media/members - /// - /// - /// This is only used during mapping operations, it is not used for angular purposes - /// - public class ContentPropertyCollectionDto : IContentProperties - { - public ContentPropertyCollectionDto() - { - Properties = Enumerable.Empty(); - } + public ContentPropertyCollectionDto() => Properties = Enumerable.Empty(); - public IEnumerable Properties { get; set; } - } + public IEnumerable Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs index b31caaa901..d0f2b9aed6 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs @@ -1,45 +1,48 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content property that is displayed in the UI +/// +[DataContract(Name = "property", Namespace = "")] +public class ContentPropertyDisplay : ContentPropertyBasic { - /// - /// Represents a content property that is displayed in the UI - /// - [DataContract(Name = "property", Namespace = "")] - public class ContentPropertyDisplay : ContentPropertyBasic + public ContentPropertyDisplay() { - public ContentPropertyDisplay() - { - Config = new Dictionary(); - Validation = new PropertyTypeValidation(); - } - - [DataMember(Name = "label", IsRequired = true)] - [Required] - public string? Label { get; set; } - - [DataMember(Name = "description")] - public string? Description { get; set; } - - [DataMember(Name = "view", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string? View { get; set; } - - [DataMember(Name = "config")] - public IDictionary? Config { get; set; } - - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } - - [DataMember(Name = "labelOnTop")] - public bool? LabelOnTop { get; set; } - - [DataMember(Name = "validation")] - public PropertyTypeValidation Validation { get; set; } - - [DataMember(Name = "readonly")] - public bool Readonly { get; set; } + Config = new Dictionary(); + Validation = new PropertyTypeValidation(); } + + [DataMember(Name = "label", IsRequired = true)] + [Required] + public string? Label { get; set; } + + [DataMember(Name = "description")] + public string? Description { get; set; } + + [DataMember(Name = "view", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string? View { get; set; } + + [Obsolete("The value type parameter of the dictionary will be made nullable in V11, use ConfigNullable instead.")] + [DataMember(Name = "config")] + public IDictionary? Config { get; set; } + + // TODO: Obsolete in V11. + [IgnoreDataMember] + public IDictionary? ConfigNullable { get => Config!; set => Config = value!; } + + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } + + [DataMember(Name = "labelOnTop")] + public bool? LabelOnTop { get; set; } + + [DataMember(Name = "validation")] + public PropertyTypeValidation Validation { get; set; } + + [DataMember(Name = "readonly")] + public bool Readonly { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs index d40a25805e..b0045bb038 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs @@ -1,24 +1,23 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content property from the database +/// +public class ContentPropertyDto : ContentPropertyBasic { - /// - /// Represents a content property from the database - /// - public class ContentPropertyDto : ContentPropertyBasic - { - public IDataType? DataType { get; set; } + public IDataType? DataType { get; set; } - public string? Label { get; set; } + public string? Label { get; set; } - public string? Description { get; set; } + public string? Description { get; set; } - public bool? IsRequired { get; set; } + public bool? IsRequired { get; set; } - public bool? LabelOnTop { get; set; } + public bool? LabelOnTop { get; set; } - public string? IsRequiredMessage { get; set; } + public string? IsRequiredMessage { get; set; } - public string? ValidationRegExp { get; set; } + public string? ValidationRegExp { get; set; } - public string? ValidationRegExpMessage { get; set; } - } + public string? ValidationRegExpMessage { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs b/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs index 99ea69b364..62bf29ce82 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs @@ -1,27 +1,25 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentRedirectUrl", Namespace = "")] +public class ContentRedirectUrl { - [DataContract(Name = "contentRedirectUrl", Namespace = "")] - public class ContentRedirectUrl - { - [DataMember(Name = "redirectId")] - public Guid RedirectId { get; set; } + [DataMember(Name = "redirectId")] + public Guid RedirectId { get; set; } - [DataMember(Name = "originalUrl")] - public string? OriginalUrl { get; set; } + [DataMember(Name = "originalUrl")] + public string? OriginalUrl { get; set; } - [DataMember(Name = "destinationUrl")] - public string? DestinationUrl { get; set; } + [DataMember(Name = "destinationUrl")] + public string? DestinationUrl { get; set; } - [DataMember(Name = "createDateUtc")] - public DateTime CreateDateUtc { get; set; } + [DataMember(Name = "createDateUtc")] + public DateTime CreateDateUtc { get; set; } - [DataMember(Name = "contentId")] - public int ContentId { get; set; } + [DataMember(Name = "contentId")] + public int ContentId { get; set; } - [DataMember(Name = "culture")] - public string? Culture { get; set; } - } + [DataMember(Name = "culture")] + public string? Culture { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs index 3beb970564..889b03db6d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs @@ -1,68 +1,69 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The action associated with saving a content item +/// +public enum ContentSaveAction { /// - /// The action associated with saving a content item + /// Saves the content item, no publish /// - public enum ContentSaveAction - { - /// - /// Saves the content item, no publish - /// - Save = 0, + Save = 0, - /// - /// Creates a new content item - /// - SaveNew = 1, + /// + /// Creates a new content item + /// + SaveNew = 1, - /// - /// Saves and publishes the content item - /// - Publish = 2, + /// + /// Saves and publishes the content item + /// + Publish = 2, - /// - /// Creates and publishes a new content item - /// - PublishNew = 3, + /// + /// Creates and publishes a new content item + /// + PublishNew = 3, - /// - /// Saves and sends publish notification - /// - SendPublish = 4, + /// + /// Saves and sends publish notification + /// + SendPublish = 4, - /// - /// Creates and sends publish notification - /// - SendPublishNew = 5, + /// + /// Creates and sends publish notification + /// + SendPublishNew = 5, - /// - /// Saves and schedules publishing - /// - Schedule = 6, + /// + /// Saves and schedules publishing + /// + Schedule = 6, - /// - /// Creates and schedules publishing - /// - ScheduleNew = 7, + /// + /// Creates and schedules publishing + /// + ScheduleNew = 7, - /// - /// Saves and publishes the content item including all descendants that have a published version - /// - PublishWithDescendants = 8, + /// + /// Saves and publishes the content item including all descendants that have a published version + /// + PublishWithDescendants = 8, - /// - /// Creates and publishes the content item including all descendants that have a published version - /// - PublishWithDescendantsNew = 9, + /// + /// Creates and publishes the content item including all descendants that have a published version + /// + PublishWithDescendantsNew = 9, - /// - /// Saves and publishes the content item including all descendants regardless of whether they have a published version or not - /// - PublishWithDescendantsForce = 10, + /// + /// Saves and publishes the content item including all descendants regardless of whether they have a published version + /// or not + /// + PublishWithDescendantsForce = 10, - /// - /// Creates and publishes the content item including all descendants regardless of whether they have a published version or not - /// - PublishWithDescendantsForceNew = 11 - } + /// + /// Creates and publishes the content item including all descendants regardless of whether they have a published + /// version or not + /// + PublishWithDescendantsForceNew = 11, } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs index 00fce177b4..1635141934 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The saved state of a content item +/// +public enum ContentSavedState { /// - /// The saved state of a content item + /// The item isn't created yet /// - public enum ContentSavedState - { - /// - /// The item isn't created yet - /// - NotCreated = 1, + NotCreated = 1, - /// - /// The item is saved but isn't published - /// - Draft = 2, + /// + /// The item is saved but isn't published + /// + Draft = 2, - /// - /// The item is published and there are no pending changes - /// - Published = 3, + /// + /// The item is published and there are no pending changes + /// + Published = 3, - /// - /// The item is published and there are pending changes - /// - PublishedPendingChanges = 4 - } + /// + /// The item is published and there are pending changes + /// + PublishedPendingChanges = 4, } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs index 80c24b8cc0..17d751760d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs @@ -1,31 +1,28 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a new sort order for a content/media item +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentSortOrder { /// - /// A model representing a new sort order for a content/media item + /// The parent Id of the nodes being sorted /// - [DataContract(Name = "content", Namespace = "")] - public class ContentSortOrder - { - /// - /// The parent Id of the nodes being sorted - /// - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } - - /// - /// An array of integer Ids representing the sort order - /// - /// - /// Of course all of these Ids should be at the same level in the hierarchy!! - /// - [DataMember(Name = "idSortOrder", IsRequired = true)] - [Required] - public int[]? IdSortOrder { get; set; } - - } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } + /// + /// An array of integer Ids representing the sort order + /// + /// + /// Of course all of these Ids should be at the same level in the hierarchy!! + /// + [DataMember(Name = "idSortOrder", IsRequired = true)] + [Required] + public int[]? IdSortOrder { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs index f46d8dc8f8..90dd6ce5c9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs @@ -1,109 +1,108 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A basic version of a content type +/// +/// +/// Generally used to return the minimal amount of data about a content type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class ContentTypeBasic : EntityBasic { - /// - /// A basic version of a content type - /// - /// - /// Generally used to return the minimal amount of data about a content type - /// - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeBasic : EntityBasic + public ContentTypeBasic() { - public ContentTypeBasic() - { - Blueprints = new Dictionary(); - Alias = string.Empty; - } - - /// - /// Overridden to apply our own validation attributes since this is not always required for other classes - /// - [Required] - [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] - [DataMember(Name = "alias")] - public override string Alias { get; set; } - - [DataMember(Name = "updateDate")] - [ReadOnly(true)] - public DateTime UpdateDate { get; set; } - - [DataMember(Name = "createDate")] - [ReadOnly(true)] - public DateTime CreateDate { get; set; } - - [DataMember(Name = "description")] - public string? Description { get; set; } - - [DataMember(Name = "thumbnail")] - public string? Thumbnail { get; set; } - - /// - /// Returns true if the icon represents a CSS class instead of a file path - /// - [DataMember(Name = "iconIsClass")] - [ReadOnly(true)] - public bool IconIsClass - { - get - { - if (Icon.IsNullOrWhiteSpace()) - { - return true; - } - //if it starts with a '.' or doesn't contain a '.' at all then it is a class - return (Icon?.StartsWith(".") ?? false) || Icon?.Contains(".") == false; - } - } - - /// - /// Returns the icon file path if the icon is not a class, otherwise returns an empty string - /// - [DataMember(Name = "iconFilePath")] - [ReadOnly(true)] - public string? IconFilePath { get; set; } - - /// - /// Returns true if the icon represents a CSS class instead of a file path - /// - [DataMember(Name = "thumbnailIsClass")] - [ReadOnly(true)] - public bool ThumbnailIsClass - { - get - { - if (Thumbnail.IsNullOrWhiteSpace()) - { - return true; - } - //if it starts with a '.' or doesn't contain a '.' at all then it is a class - return (Thumbnail?.StartsWith(".") ?? false) || Thumbnail?.Contains(".") == false; - } - } - - /// - /// Returns the icon file path if the icon is not a class, otherwise returns an empty string - /// - [DataMember(Name = "thumbnailFilePath")] - [ReadOnly(true)] - public string? ThumbnailFilePath { get; set; } - - [DataMember(Name = "blueprints")] - [ReadOnly(true)] - public IDictionary Blueprints { get; set; } - - [DataMember(Name = "isContainer")] - [ReadOnly(true)] - public bool IsContainer { get; set; } - - [DataMember(Name = "isElement")] - [ReadOnly(true)] - public bool IsElement { get; set; } + Blueprints = new Dictionary(); + Alias = string.Empty; } + + /// + /// Overridden to apply our own validation attributes since this is not always required for other classes + /// + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public override string Alias { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + [DataMember(Name = "description")] + public string? Description { get; set; } + + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } + + /// + /// Returns true if the icon represents a CSS class instead of a file path + /// + [DataMember(Name = "iconIsClass")] + [ReadOnly(true)] + public bool IconIsClass + { + get + { + if (Icon.IsNullOrWhiteSpace()) + { + return true; + } + + // if it starts with a '.' or doesn't contain a '.' at all then it is a class + return (Icon?.StartsWith(".") ?? false) || Icon?.Contains(".") == false; + } + } + + /// + /// Returns the icon file path if the icon is not a class, otherwise returns an empty string + /// + [DataMember(Name = "iconFilePath")] + [ReadOnly(true)] + public string? IconFilePath { get; set; } + + /// + /// Returns true if the icon represents a CSS class instead of a file path + /// + [DataMember(Name = "thumbnailIsClass")] + [ReadOnly(true)] + public bool ThumbnailIsClass + { + get + { + if (Thumbnail.IsNullOrWhiteSpace()) + { + return true; + } + + // if it starts with a '.' or doesn't contain a '.' at all then it is a class + return (Thumbnail?.StartsWith(".") ?? false) || Thumbnail?.Contains(".") == false; + } + } + + /// + /// Returns the icon file path if the icon is not a class, otherwise returns an empty string + /// + [DataMember(Name = "thumbnailFilePath")] + [ReadOnly(true)] + public string? ThumbnailFilePath { get; set; } + + [DataMember(Name = "blueprints")] + [ReadOnly(true)] + public IDictionary Blueprints { get; set; } + + [DataMember(Name = "isContainer")] + [ReadOnly(true)] + public bool IsContainer { get; set; } + + [DataMember(Name = "isElement")] + [ReadOnly(true)] + public bool IsElement { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs index 4eb8563a6e..030923d291 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -1,74 +1,69 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel { - public abstract class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel + protected ContentTypeCompositionDisplay() { - protected ContentTypeCompositionDisplay() - { - //initialize collections so at least their never null - AllowedContentTypes = new List(); - CompositeContentTypes = new List(); - Notifications = new List(); - } - - //name, alias, icon, thumb, desc, inherited from basic - - [DataMember(Name = "listViewEditorName")] - [ReadOnly(true)] - public string? ListViewEditorName { get; set; } - - //Allowed child types - [DataMember(Name = "allowedContentTypes")] - public IEnumerable? AllowedContentTypes { get; set; } - - //Compositions - [DataMember(Name = "compositeContentTypes")] - public IEnumerable CompositeContentTypes { get; set; } - - //Locked compositions - [DataMember(Name = "lockedCompositeContentTypes")] - public IEnumerable? LockedCompositeContentTypes { get; set; } - - [DataMember(Name = "allowAsRoot")] - public bool AllowAsRoot { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. - /// - [DataMember(Name = "ModelState")] - [ReadOnly(true)] - public IDictionary? Errors { get; set; } + // initialize collections so at least their never null + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); + Notifications = new List(); } - [DataContract(Name = "contentType", Namespace = "")] - public abstract class ContentTypeCompositionDisplay : ContentTypeCompositionDisplay - where TPropertyTypeDisplay : PropertyTypeDisplay - { - protected ContentTypeCompositionDisplay() - { - //initialize collections so at least their never null - Groups = new List>(); - } + // name, alias, icon, thumb, desc, inherited from basic + [DataMember(Name = "listViewEditorName")] + [ReadOnly(true)] + public string? ListViewEditorName { get; set; } - //Tabs - [DataMember(Name = "groups")] - public IEnumerable> Groups { get; set; } - } + // Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable? AllowedContentTypes { get; set; } + + // Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } + + // Locked compositions + [DataMember(Name = "lockedCompositeContentTypes")] + public IEnumerable? LockedCompositeContentTypes { get; set; } + + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. + /// + [DataMember(Name = "ModelState")] + [ReadOnly(true)] + public IDictionary? Errors { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } +} + +[DataContract(Name = "contentType", Namespace = "")] +public abstract class ContentTypeCompositionDisplay : ContentTypeCompositionDisplay + where TPropertyTypeDisplay : PropertyTypeDisplay +{ + protected ContentTypeCompositionDisplay() => + + // initialize collections so at least their never null + Groups = new List>(); + + // Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs index 55c4a07cfd..d6ad7c7ba2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs @@ -1,121 +1,120 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Abstract model used to save content types +/// +[DataContract(Name = "contentType", Namespace = "")] +public abstract class ContentTypeSave : ContentTypeBasic, IValidatableObject { - /// - /// Abstract model used to save content types - /// - [DataContract(Name = "contentType", Namespace = "")] - public abstract class ContentTypeSave : ContentTypeBasic, IValidatableObject + protected ContentTypeSave() { - protected ContentTypeSave() - { - AllowedContentTypes = new List(); - CompositeContentTypes = new List(); - } - - //Compositions - [DataMember(Name = "compositeContentTypes")] - public IEnumerable CompositeContentTypes { get; set; } - - [DataMember(Name = "allowAsRoot")] - public bool AllowAsRoot { get; set; } - - //Allowed child types - [DataMember(Name = "allowedContentTypes")] - public IEnumerable AllowedContentTypes { get; set; } - - [DataMember(Name = "historyCleanup")] - public HistoryCleanupViewModel? HistoryCleanup { get; set; } - - /// - /// Custom validation - /// - /// - /// - public virtual IEnumerable Validate(ValidationContext validationContext) - { - if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) - yield return new ValidationResult("Composite Content Type value cannot be null", new[] { "CompositeContentTypes" }); - } + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); } + // Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } + + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } + + // Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable AllowedContentTypes { get; set; } + + [DataMember(Name = "historyCleanup")] + public HistoryCleanupViewModel? HistoryCleanup { get; set; } + /// - /// Abstract model used to save content types + /// Custom validation /// - /// - [DataContract(Name = "contentType", Namespace = "")] - public abstract class ContentTypeSave : ContentTypeSave - where TPropertyType : PropertyTypeBasic + /// + /// + public virtual IEnumerable Validate(ValidationContext validationContext) { - protected ContentTypeSave() + if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) { - Groups = new List>(); - } - - /// - /// A rule for defining how a content type can be varied - /// - /// - /// This is only supported on document types right now but in the future it could be media types too - /// - [DataMember(Name = "allowCultureVariant")] - public bool AllowCultureVariant { get; set; } - - [DataMember(Name = "allowSegmentVariant")] - public bool AllowSegmentVariant { get; set; } - - //Tabs - [DataMember(Name = "groups")] - public IEnumerable> Groups { get; set; } - - /// - /// Custom validation - /// - /// - /// - public override IEnumerable Validate(ValidationContext validationContext) - { - foreach (var validationResult in base.Validate(validationContext)) - { - yield return validationResult; - } - - foreach (var duplicateGroupAlias in Groups.GroupBy(x => x.Alias).Where(x => x.Count() > 1)) - { - var lastGroupIndex = Groups.IndexOf(duplicateGroupAlias.Last()); - yield return new ValidationResult("Duplicate aliases are not allowed: " + duplicateGroupAlias.Key, new[] - { - // TODO: We don't display the alias yet, so add the validation message to the name - $"Groups[{lastGroupIndex}].Name" - }); - } - - foreach (var duplicateGroupName in Groups.GroupBy(x => (x.GetParentAlias(), x.Name)).Where(x => x.Count() > 1)) - { - var lastGroupIndex = Groups.IndexOf(duplicateGroupName.Last()); - yield return new ValidationResult("Duplicate names are not allowed", new[] - { - $"Groups[{lastGroupIndex}].Name" - }); - } - - foreach (var duplicatePropertyAlias in Groups.SelectMany(x => x.Properties).GroupBy(x => x.Alias).Where(x => x.Count() > 1)) - { - var lastProperty = duplicatePropertyAlias.Last(); - var propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty)); - var lastPropertyIndex = propertyGroup.Properties.IndexOf(lastProperty); - var propertyGroupIndex = Groups.IndexOf(propertyGroup); - - yield return new ValidationResult("Duplicate property aliases not allowed: " + duplicatePropertyAlias.Key, new[] - { - $"Groups[{propertyGroupIndex}].Properties[{lastPropertyIndex}].Alias" - }); - } + yield return new ValidationResult( + "Composite Content Type value cannot be null", + new[] { "CompositeContentTypes" }); + } + } +} + +/// +/// Abstract model used to save content types +/// +/// +[DataContract(Name = "contentType", Namespace = "")] +public abstract class ContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic +{ + protected ContentTypeSave() => Groups = new List>(); + + /// + /// A rule for defining how a content type can be varied + /// + /// + /// This is only supported on document types right now but in the future it could be media types too + /// + [DataMember(Name = "allowCultureVariant")] + public bool AllowCultureVariant { get; set; } + + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } + + // Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) + { + foreach (ValidationResult validationResult in base.Validate(validationContext)) + { + yield return validationResult; + } + + foreach (IGrouping> duplicateGroupAlias in Groups + .GroupBy(x => x.Alias).Where(x => x.Count() > 1)) + { + var lastGroupIndex = Groups.IndexOf(duplicateGroupAlias.Last()); + yield return new ValidationResult("Duplicate aliases are not allowed: " + duplicateGroupAlias.Key, new[] + { + // TODO: We don't display the alias yet, so add the validation message to the name + $"Groups[{lastGroupIndex}].Name", + }); + } + + foreach (IGrouping<(string?, string? Name), PropertyGroupBasic> duplicateGroupName in Groups + .GroupBy(x => (x.GetParentAlias(), x.Name)).Where(x => x.Count() > 1)) + { + var lastGroupIndex = Groups.IndexOf(duplicateGroupName.Last()); + yield return new ValidationResult( + "Duplicate names are not allowed", + new[] { $"Groups[{lastGroupIndex}].Name" }); + } + + foreach (IGrouping duplicatePropertyAlias in Groups.SelectMany(x => x.Properties) + .GroupBy(x => x.Alias).Where(x => x.Count() > 1)) + { + TPropertyType lastProperty = duplicatePropertyAlias.Last(); + PropertyGroupBasic propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty)); + var lastPropertyIndex = propertyGroup.Properties.IndexOf(lastProperty); + var propertyGroupIndex = Groups.IndexOf(propertyGroup); + + yield return new ValidationResult( + "Duplicate property aliases not allowed: " + duplicatePropertyAlias.Key, + new[] { $"Groups[{propertyGroupIndex}].Properties[{lastPropertyIndex}].Alias" }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs index 57b1c98d54..476e772743 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs @@ -1,26 +1,25 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model for retrieving multiple content types based on their aliases. +/// +[DataContract(Name = "contentTypes", Namespace = "")] +public class ContentTypesByAliases { /// - /// A model for retrieving multiple content types based on their aliases. + /// Id of the parent of the content type. /// - [DataContract(Name = "contentTypes", Namespace = "")] - public class ContentTypesByAliases - { - /// - /// Id of the parent of the content type. - /// - [DataMember(Name = "parentId")] - [Required] - public int ParentId { get; set; } + [DataMember(Name = "parentId")] + [Required] + public int ParentId { get; set; } - /// - /// The alias of every content type to get. - /// - [DataMember(Name = "contentTypeAliases")] - [Required] - public string[]? ContentTypeAliases { get; set; } - } + /// + /// The alias of every content type to get. + /// + [DataMember(Name = "contentTypeAliases")] + [Required] + public string[]? ContentTypeAliases { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs index 0a2bea7f88..2b728c04da 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs @@ -1,27 +1,25 @@ -using System; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model for retrieving multiple content types based on their keys. +/// +[DataContract(Name = "contentTypes", Namespace = "")] +public class ContentTypesByKeys { /// - /// A model for retrieving multiple content types based on their keys. + /// ID of the parent of the content type. /// - [DataContract(Name = "contentTypes", Namespace = "")] - public class ContentTypesByKeys - { - /// - /// ID of the parent of the content type. - /// - [DataMember(Name = "parentId")] - [Required] - public int ParentId { get; set; } + [DataMember(Name = "parentId")] + [Required] + public int ParentId { get; set; } - /// - /// The id of every content type to get. - /// - [DataMember(Name = "contentTypeKeys")] - [Required] - public Guid[]? ContentTypeKeys { get; set; } - } + /// + /// The id of every content type to get. + /// + [DataMember(Name = "contentTypeKeys")] + [Required] + public Guid[]? ContentTypeKeys { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs index f7ea69f7ce..ed9568590f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs @@ -1,73 +1,69 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Validation; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentVariant", Namespace = "")] +public class ContentVariantSave : IContentProperties { - [DataContract(Name = "contentVariant", Namespace = "")] - public class ContentVariantSave : IContentProperties - { - public ContentVariantSave() - { - Properties = new List(); - } + public ContentVariantSave() => Properties = new List(); - [DataMember(Name = "name", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - [MaxLength(255, ErrorMessage ="Name must be less than 255 characters")] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + [MaxLength(255, ErrorMessage = "Name must be less than 255 characters")] + public string? Name { get; set; } - [DataMember(Name = "properties")] - public IEnumerable Properties { get; set; } + /// + /// The culture of this variant, if this is invariant than this is null or empty + /// + [DataMember(Name = "culture")] + public string? Culture { get; set; } - /// - /// The culture of this variant, if this is invariant than this is null or empty - /// - [DataMember(Name = "culture")] - public string? Culture { get; set; } + /// + /// The segment of this variant, if this is invariant than this is null or empty + /// + [DataMember(Name = "segment")] + public string? Segment { get; set; } - /// - /// The segment of this variant, if this is invariant than this is null or empty - /// - [DataMember(Name = "segment")] - public string? Segment { get; set; } + /// + /// Indicates if the variant should be updated + /// + /// + /// If this is false, this variant data will not be updated at all + /// + [DataMember(Name = "save")] + public bool Save { get; set; } - /// - /// Indicates if the variant should be updated - /// - /// - /// If this is false, this variant data will not be updated at all - /// - [DataMember(Name = "save")] - public bool Save { get; set; } + /// + /// Indicates if the variant should be published + /// + /// + /// This option will have no affect if is false. + /// This is not used to unpublish. + /// + [DataMember(Name = "publish")] + public bool Publish { get; set; } - /// - /// Indicates if the variant should be published - /// - /// - /// This option will have no affect if is false. - /// This is not used to unpublish. - /// - [DataMember(Name = "publish")] - public bool Publish { get; set; } + [DataMember(Name = "expireDate")] + public DateTime? ExpireDate { get; set; } - [DataMember(Name = "expireDate")] - public DateTime? ExpireDate { get; set; } + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } - [DataMember(Name = "releaseDate")] - public DateTime? ReleaseDate { get; set; } + /// + /// The property DTO object is used to gather all required property data including data type information etc... for use + /// with validation - used during inbound model binding + /// + /// + /// We basically use this object to hydrate all required data from the database into one object so we can validate + /// everything we need + /// instead of having to look up all the data individually. + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } - /// - /// The property DTO object is used to gather all required property data including data type information etc... for use with validation - used during inbound model binding - /// - /// - /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need - /// instead of having to look up all the data individually. - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } - } + [DataMember(Name = "properties")] + public IEnumerable Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs index 44f0b31c25..6418a7bca7 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs @@ -1,82 +1,84 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the variant info for a content item +/// +[DataContract(Name = "contentVariant", Namespace = "")] +public class ContentVariantDisplay : ITabbedContent, IContentProperties, INotificationModel { + public ContentVariantDisplay() + { + Tabs = new List>(); + Notifications = new List(); + AllowedActions = Enumerable.Empty(); + } + + [DataMember(Name = "allowedActions", IsRequired = true)] + public IEnumerable AllowedActions { get; set; } + + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } + + [DataMember(Name = "displayName")] + public string? DisplayName { get; set; } + /// - /// Represents the variant info for a content item + /// The language/culture assigned to this content variation /// - [DataContract(Name = "contentVariant", Namespace = "")] - public class ContentVariantDisplay : ITabbedContent, IContentProperties, INotificationModel - { - public ContentVariantDisplay() - { - Tabs = new List>(); - Notifications = new List(); - } + /// + /// If this is null it means this content variant is an invariant culture + /// + [DataMember(Name = "language")] + public Language? Language { get; set; } - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "segment")] + public string? Segment { get; set; } - [DataMember(Name = "displayName")] - public string? DisplayName { get; set; } + [DataMember(Name = "state")] + public ContentSavedState State { get; set; } - /// - /// Defines the tabs containing display properties - /// - [DataMember(Name = "tabs")] - public IEnumerable> Tabs { get; set; } + [DataMember(Name = "updateDate")] + public DateTime UpdateDate { get; set; } - /// - /// Internal property used for tests to get all properties from all tabs - /// - [IgnoreDataMember] - IEnumerable IContentProperties.Properties => Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); + [DataMember(Name = "createDate")] + public DateTime CreateDate { get; set; } - /// - /// The language/culture assigned to this content variation - /// - /// - /// If this is null it means this content variant is an invariant culture - /// - [DataMember(Name = "language")] - public Language? Language { get; set; } + [DataMember(Name = "publishDate")] + public DateTime? PublishDate { get; set; } - [DataMember(Name = "segment")] - public string? Segment { get; set; } + /// + /// Internal property used for tests to get all properties from all tabs + /// + [IgnoreDataMember] + IEnumerable IContentProperties.Properties => + Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); - [DataMember(Name = "state")] - public ContentSavedState State { get; set; } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + /// + /// The notifications assigned to a variant are currently only used to show custom messages in the save/publish + /// dialogs. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } - [DataMember(Name = "updateDate")] - public DateTime UpdateDate { get; set; } - - [DataMember(Name = "createDate")] - public DateTime CreateDate { get; set; } - - [DataMember(Name = "publishDate")] - public DateTime? PublishDate { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - /// - /// The notifications assigned to a variant are currently only used to show custom messages in the save/publish dialogs. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - } - - public class ContentVariantScheduleDisplay : ContentVariantDisplay - { - [DataMember(Name = "releaseDate")] - public DateTime? ReleaseDate { get; set; } - - [DataMember(Name = "expireDate")] - public DateTime? ExpireDate { get; set; } - } + /// + /// Defines the tabs containing display properties + /// + [DataMember(Name = "tabs")] + public IEnumerable> Tabs { get; set; } +} + +public class ContentVariantScheduleDisplay : ContentVariantDisplay +{ + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } + + [DataMember(Name = "expireDate")] + public DateTime? ExpireDate { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs b/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs index b1db2759f0..a6f99ab586 100644 --- a/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs +++ b/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The result of creating a content type collection in the UI +/// +[DataContract(Name = "contentTypeCollection", Namespace = "")] +public class CreatedContentTypeCollectionResult { - /// - /// The result of creating a content type collection in the UI - /// - [DataContract(Name = "contentTypeCollection", Namespace = "")] - public class CreatedContentTypeCollectionResult - { - [DataMember(Name = "collectionId")] - public int CollectionId { get; set; } + [DataMember(Name = "collectionId")] + public int CollectionId { get; set; } - [DataMember(Name = "containerId")] - public int ContainerId { get; set; } - } + [DataMember(Name = "containerId")] + public int ContainerId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs index 153f495a70..7bb0d427ea 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs @@ -1,27 +1,26 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The basic data type information +/// +[DataContract(Name = "dataType", Namespace = "")] +public class DataTypeBasic : EntityBasic { /// - /// The basic data type information + /// Whether or not this is a system data type, in which case it cannot be deleted /// - [DataContract(Name = "dataType", Namespace = "")] - public class DataTypeBasic : EntityBasic - { - /// - /// Whether or not this is a system data type, in which case it cannot be deleted - /// - [DataMember(Name = "isSystem")] - [ReadOnly(true)] - public bool IsSystemDataType { get; set; } + [DataMember(Name = "isSystem")] + [ReadOnly(true)] + public bool IsSystemDataType { get; set; } - [DataMember(Name = "group")] - [ReadOnly(true)] - public string? Group { get; set; } + [DataMember(Name = "group")] + [ReadOnly(true)] + public string? Group { get; set; } - [DataMember(Name = "hasPrevalues")] - [ReadOnly(true)] - public bool HasPrevalues { get; set; } - } + [DataMember(Name = "hasPrevalues")] + [ReadOnly(true)] + public bool HasPrevalues { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs index 97a2177167..a324bb4bce 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs @@ -1,42 +1,40 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a datatype configuration field model for editing. +/// +[DataContract(Name = "preValue", Namespace = "")] +public class DataTypeConfigurationFieldDisplay : DataTypeConfigurationFieldSave { /// - /// Represents a datatype configuration field model for editing. + /// The name to display for this pre-value field /// - [DataContract(Name = "preValue", Namespace = "")] - public class DataTypeConfigurationFieldDisplay : DataTypeConfigurationFieldSave - { - /// - /// The name to display for this pre-value field - /// - [DataMember(Name = "label", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "label", IsRequired = true)] + public string? Name { get; set; } - /// - /// The description to display for this pre-value field - /// - [DataMember(Name = "description")] - public string? Description { get; set; } + /// + /// The description to display for this pre-value field + /// + [DataMember(Name = "description")] + public string? Description { get; set; } - /// - /// Specifies whether to hide the label for the pre-value - /// - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } + /// + /// Specifies whether to hide the label for the pre-value + /// + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } - /// - /// The view to render for the field - /// - [DataMember(Name = "view", IsRequired = true)] - public string? View { get; set; } + /// + /// The view to render for the field + /// + [DataMember(Name = "view", IsRequired = true)] + public string? View { get; set; } - /// - /// This allows for custom configuration to be injected into the pre-value editor - /// - [DataMember(Name = "config")] - public IDictionary? Config { get; set; } - } + /// + /// This allows for custom configuration to be injected into the pre-value editor + /// + [DataMember(Name = "config")] + public IDictionary? Config { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs index a82a6eb257..514f9b8618 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs @@ -1,23 +1,22 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a datatype configuration field model for editing. +/// +[DataContract(Name = "preValue", Namespace = "")] +public class DataTypeConfigurationFieldSave { /// - /// Represents a datatype configuration field model for editing. + /// Gets or sets the configuration field key. /// - [DataContract(Name = "preValue", Namespace = "")] - public class DataTypeConfigurationFieldSave - { - /// - /// Gets or sets the configuration field key. - /// - [DataMember(Name = "key", IsRequired = true)] - public string Key { get; set; } = null!; + [DataMember(Name = "key", IsRequired = true)] + public string Key { get; set; } = null!; - /// - /// Gets or sets the configuration field value. - /// - [DataMember(Name = "value", IsRequired = true)] - public object? Value { get; set; } - } + /// + /// Gets or sets the configuration field value. + /// + [DataMember(Name = "value", IsRequired = true)] + public object? Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs index cbe5552b1e..7f3c93d126 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs @@ -1,37 +1,32 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a data type that is being edited +/// +[DataContract(Name = "dataType", Namespace = "")] +public class DataTypeDisplay : DataTypeBasic, INotificationModel { + public DataTypeDisplay() => Notifications = new List(); + /// - /// Represents a data type that is being edited + /// The alias of the property editor /// - [DataContract(Name = "dataType", Namespace = "")] - public class DataTypeDisplay : DataTypeBasic, INotificationModel - { - public DataTypeDisplay() - { - Notifications = new List(); - } + [DataMember(Name = "selectedEditor", IsRequired = true)] + [Required] + public string? SelectedEditor { get; set; } - /// - /// The alias of the property editor - /// - [DataMember(Name = "selectedEditor", IsRequired = true)] - [Required] - public string? SelectedEditor { get; set; } + [DataMember(Name = "availableEditors")] + public IEnumerable? AvailableEditors { get; set; } - [DataMember(Name = "availableEditors")] - public IEnumerable? AvailableEditors { get; set; } + [DataMember(Name = "preValues")] + public IEnumerable? PreValues { get; set; } - [DataMember(Name = "preValues")] - public IEnumerable? PreValues { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeHasValuesDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeHasValuesDisplay.cs new file mode 100644 index 0000000000..4db43ad391 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeHasValuesDisplay.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "dataTypeHasValuesDisplay")] +public class DataTypeHasValuesDisplay +{ + public DataTypeHasValuesDisplay(int id, bool hasValues) + { + Id = id; + HasValues = hasValues; + } + + [DataMember(Name = "id")] + public int Id { get; } + + [DataMember(Name = "hasValues")] + public bool HasValues { get; } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs index c8699472d5..47711fc0a3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs @@ -1,36 +1,33 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "dataTypeReferences", Namespace = "")] +public class DataTypeReferences { - [DataContract(Name = "dataTypeReferences", Namespace = "")] - public class DataTypeReferences + [DataMember(Name = "documentTypes")] + public IEnumerable DocumentTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "mediaTypes")] + public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "memberTypes")] + public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); + + [DataContract(Name = "contentType", Namespace = "")] + public class ContentTypeReferences : EntityBasic { - [DataMember(Name = "documentTypes")] - public IEnumerable DocumentTypes { get; set; } = Enumerable.Empty(); + [DataMember(Name = "properties")] + public object? Properties { get; set; } - [DataMember(Name = "mediaTypes")] - public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); - - [DataMember(Name = "memberTypes")] - public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); - - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeReferences : EntityBasic + [DataContract(Name = "property", Namespace = "")] + public class PropertyTypeReferences { - [DataMember(Name = "properties")] - public object? Properties { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataContract(Name = "property", Namespace = "")] - public class PropertyTypeReferences - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "alias")] - public string? Alias { get; set; } - } + [DataMember(Name = "alias")] + public string? Alias { get; set; } } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs index 3795e42782..8968fb0795 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs @@ -1,49 +1,47 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a datatype model for editing. +/// +[DataContract(Name = "dataType", Namespace = "")] +public class DataTypeSave : EntityBasic { /// - /// Represents a datatype model for editing. + /// Gets or sets the action to perform. /// - [DataContract(Name = "dataType", Namespace = "")] - public class DataTypeSave : EntityBasic - { - /// - /// Gets or sets the action to perform. - /// - /// - /// Some values (publish) are illegal here. - /// - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } + /// + /// Some values (publish) are illegal here. + /// + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } - /// - /// Gets or sets the datatype editor. - /// - [DataMember(Name = "selectedEditor", IsRequired = true)] - [Required] - public string? EditorAlias { get; set; } + /// + /// Gets or sets the datatype editor. + /// + [DataMember(Name = "selectedEditor", IsRequired = true)] + [Required] + public string? EditorAlias { get; set; } - /// - /// Gets or sets the datatype configuration fields. - /// - [DataMember(Name = "preValues")] - public IEnumerable? ConfigurationFields { get; set; } + /// + /// Gets or sets the datatype configuration fields. + /// + [DataMember(Name = "preValues")] + public IEnumerable? ConfigurationFields { get; set; } - /// - /// Gets or sets the persisted data type. - /// - [IgnoreDataMember] - public IDataType? PersistedDataType { get; set; } + /// + /// Gets or sets the persisted data type. + /// + [IgnoreDataMember] + public IDataType? PersistedDataType { get; set; } - /// - /// Gets or sets the property editor. - /// - [IgnoreDataMember] - public IDataEditor? PropertyEditor { get; set; } - } + /// + /// Gets or sets the property editor. + /// + [IgnoreDataMember] + public IDataEditor? PropertyEditor { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs index d8cfaf1104..59e5bffb4b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs @@ -1,48 +1,45 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary display model +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class DictionaryDisplay : EntityBasic, INotificationModel { /// - /// The dictionary display model + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class DictionaryDisplay : EntityBasic, INotificationModel + public DictionaryDisplay() { - /// - /// Initializes a new instance of the class. - /// - public DictionaryDisplay() - { - Notifications = new List(); - Translations = new List(); - ContentApps = new List(); - } - - /// - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - - /// - /// Gets or sets the parent id. - /// - [DataMember(Name = "parentId")] - public new Guid ParentId { get; set; } - - /// - /// Gets the translations. - /// - [DataMember(Name = "translations")] - public List Translations { get; private set; } - - /// - /// Apps for the dictionary item - /// - [DataMember(Name = "apps")] - public List ContentApps { get; private set; } + Notifications = new List(); + Translations = new List(); + ContentApps = new List(); } + + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } + + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } + + /// + /// Apps for the dictionary item + /// + [DataMember(Name = "apps")] + public List ContentApps { get; private set; } + + /// + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs index adf279c412..15aab0c7ef 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs @@ -1,44 +1,39 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary overview display. +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class DictionaryOverviewDisplay { /// - /// The dictionary overview display. + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class DictionaryOverviewDisplay - { - /// - /// Initializes a new instance of the class. - /// - public DictionaryOverviewDisplay() - { - Translations = new List(); - } + public DictionaryOverviewDisplay() => Translations = new List(); - /// - /// Gets or sets the key. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + /// + /// Gets or sets the key. + /// + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// Gets or sets the id. - /// - [DataMember(Name = "id")] - public int Id { get; set; } + /// + /// Gets or sets the id. + /// + [DataMember(Name = "id")] + public int Id { get; set; } - /// - /// Gets or sets the level. - /// - [DataMember(Name = "level")] - public int Level { get; set; } + /// + /// Gets or sets the level. + /// + [DataMember(Name = "level")] + public int Level { get; set; } - /// - /// Gets or sets the translations. - /// - [DataMember(Name = "translations")] - public List Translations { get; set; } - } + /// + /// Gets or sets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs index 00d8b339f9..9e534820fa 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs @@ -1,23 +1,22 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary translation overview display. +/// +[DataContract(Name = "dictionaryTranslation", Namespace = "")] +public class DictionaryOverviewTranslationDisplay { /// - /// The dictionary translation overview display. + /// Gets or sets the display name. /// - [DataContract(Name = "dictionaryTranslation", Namespace = "")] - public class DictionaryOverviewTranslationDisplay - { - /// - /// Gets or sets the display name. - /// - [DataMember(Name = "displayName")] - public string? DisplayName { get; set; } + [DataMember(Name = "displayName")] + public string? DisplayName { get; set; } - /// - /// Gets or sets a value indicating whether has translation. - /// - [DataMember(Name = "hasTranslation")] - public bool HasTranslation { get; set; } - } + /// + /// Gets or sets a value indicating whether has translation. + /// + [DataMember(Name = "hasTranslation")] + public bool HasTranslation { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs b/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs index 0e652e7160..85585c45ba 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs @@ -1,39 +1,33 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Dictionary Save model +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class DictionarySave : EntityBasic { /// - /// Dictionary Save model + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class DictionarySave : EntityBasic - { - /// - /// Initializes a new instance of the class. - /// - public DictionarySave() - { - Translations = new List(); - } + public DictionarySave() => Translations = new List(); - /// - /// Gets or sets a value indicating whether name is dirty. - /// - [DataMember(Name = "nameIsDirty")] - public bool NameIsDirty { get; set; } + /// + /// Gets or sets a value indicating whether name is dirty. + /// + [DataMember(Name = "nameIsDirty")] + public bool NameIsDirty { get; set; } - /// - /// Gets the translations. - /// - [DataMember(Name = "translations")] - public List Translations { get; private set; } + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } - /// - /// Gets or sets the parent id. - /// - [DataMember(Name = "parentId")] - public new Guid ParentId { get; set; } - } + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs index 4ad4002b77..afd36b6acc 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs @@ -1,18 +1,17 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// +/// The dictionary translation display model +/// +[DataContract(Name = "dictionaryTranslation", Namespace = "")] +public class DictionaryTranslationDisplay : DictionaryTranslationSave { - /// /// - /// The dictionary translation display model + /// Gets or sets the display name. /// - [DataContract(Name = "dictionaryTranslation", Namespace = "")] - public class DictionaryTranslationDisplay : DictionaryTranslationSave - { - /// - /// Gets or sets the display name. - /// - [DataMember(Name = "displayName")] - public string? DisplayName { get; set; } - } + [DataMember(Name = "displayName")] + public string? DisplayName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs index aa42abbf56..cf58bcb2ec 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs @@ -1,29 +1,28 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary translation save model +/// +[DataContract(Name = "dictionaryTranslation", Namespace = "")] +public class DictionaryTranslationSave { /// - /// The dictionary translation save model + /// Gets or sets the ISO code. /// - [DataContract(Name = "dictionaryTranslation", Namespace = "")] - public class DictionaryTranslationSave - { - /// - /// Gets or sets the ISO code. - /// - [DataMember(Name = "isoCode")] - public string? IsoCode { get; set; } + [DataMember(Name = "isoCode")] + public string? IsoCode { get; set; } - /// - /// Gets or sets the translation. - /// - [DataMember(Name = "translation")] - public string Translation { get; set; } = null!; + /// + /// Gets or sets the translation. + /// + [DataMember(Name = "translation")] + public string Translation { get; set; } = null!; - /// - /// Gets or sets the language id. - /// - [DataMember(Name = "languageId")] - public int LanguageId { get; set; } - } + /// + /// Gets or sets the language id. + /// + [DataMember(Name = "languageId")] + public int LanguageId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs index 6f56c92292..3c292a7e6a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs @@ -1,34 +1,33 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentType", Namespace = "")] +public class DocumentTypeDisplay : ContentTypeCompositionDisplay { - [DataContract(Name = "contentType", Namespace = "")] - public class DocumentTypeDisplay : ContentTypeCompositionDisplay - { - public DocumentTypeDisplay() => - //initialize collections so at least their never null - AllowedTemplates = new List(); + public DocumentTypeDisplay() => - //name, alias, icon, thumb, desc, inherited from the content type + // initialize collections so at least their never null + AllowedTemplates = new List(); - // Templates - [DataMember(Name = "allowedTemplates")] - public IEnumerable AllowedTemplates { get; set; } + // name, alias, icon, thumb, desc, inherited from the content type - [DataMember(Name = "defaultTemplate")] - public EntityBasic? DefaultTemplate { get; set; } + // Templates + [DataMember(Name = "allowedTemplates")] + public IEnumerable AllowedTemplates { get; set; } - [DataMember(Name = "allowCultureVariant")] - public bool AllowCultureVariant { get; set; } + [DataMember(Name = "defaultTemplate")] + public EntityBasic? DefaultTemplate { get; set; } - [DataMember(Name = "allowSegmentVariant")] - public bool AllowSegmentVariant { get; set; } + [DataMember(Name = "allowCultureVariant")] + public bool AllowCultureVariant { get; set; } - [DataMember(Name = "apps")] - public IEnumerable? ContentApps { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } - [DataMember(Name = "historyCleanup")] - public HistoryCleanupViewModel? HistoryCleanup { get; set; } - } + [DataMember(Name = "apps")] + public IEnumerable? ContentApps { get; set; } + + [DataMember(Name = "historyCleanup")] + public HistoryCleanupViewModel? HistoryCleanup { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs index 2e509ea5db..af13e88f9b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs @@ -1,43 +1,42 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Model used to save a document type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class DocumentTypeSave : ContentTypeSave { /// - /// Model used to save a document type + /// The list of allowed templates to assign (template alias) /// - [DataContract(Name = "contentType", Namespace = "")] - public class DocumentTypeSave : ContentTypeSave + [DataMember(Name = "allowedTemplates")] + public IEnumerable? AllowedTemplates { get; set; } + + /// + /// The default template to assign (template alias) + /// + [DataMember(Name = "defaultTemplate")] + public string? DefaultTemplate { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) { - /// - /// The list of allowed templates to assign (template alias) - /// - [DataMember(Name = "allowedTemplates")] - public IEnumerable? AllowedTemplates { get; set; } - - /// - /// The default template to assign (template alias) - /// - [DataMember(Name = "defaultTemplate")] - public string? DefaultTemplate { get; set; } - - /// - /// Custom validation - /// - /// - /// - public override IEnumerable Validate(ValidationContext validationContext) + if (AllowedTemplates?.Any(x => x.IsNullOrWhiteSpace()) ?? false) { - if (AllowedTemplates?.Any(x => x.IsNullOrWhiteSpace()) ?? false) - yield return new ValidationResult("Template value cannot be null", new[] { "AllowedTemplates" }); + yield return new ValidationResult("Template value cannot be null", new[] { "AllowedTemplates" }); + } - foreach (var v in base.Validate(validationContext)) - { - yield return v; - } + foreach (ValidationResult v in base.Validate(validationContext)) + { + yield return v; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs index 573909a610..7a6a584438 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs @@ -1,26 +1,25 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "DomainDisplay")] +public class DomainDisplay { - [DataContract(Name = "DomainDisplay")] - public class DomainDisplay + public DomainDisplay(string name, int lang) { - public DomainDisplay(string name, int lang) - { - Name = name; - Lang = lang; - } - - [DataMember(Name = "name")] - public string Name { get; } - - [DataMember(Name = "lang")] - public int Lang { get; } - - [DataMember(Name = "duplicate")] - public bool Duplicate { get; set; } - - [DataMember(Name = "other")] - public string? Other { get; set; } + Name = name; + Lang = lang; } + + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "lang")] + public int Lang { get; } + + [DataMember(Name = "duplicate")] + public bool Duplicate { get; set; } + + [DataMember(Name = "other")] + public string? Other { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs b/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs index a91e740e79..391616b8dc 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "DomainSave")] +public class DomainSave { - [DataContract(Name = "DomainSave")] - public class DomainSave - { - [DataMember(Name = "valid")] - public bool Valid { get; set; } + [DataMember(Name = "valid")] + public bool Valid { get; set; } - [DataMember(Name = "nodeId")] - public int NodeId { get; set; } + [DataMember(Name = "nodeId")] + public int NodeId { get; set; } - [DataMember(Name = "language")] - public int Language { get; set; } + [DataMember(Name = "language")] + public int Language { get; set; } - [DataMember(Name = "domains")] - public DomainDisplay[]? Domains { get; set; } - } + [DataMember(Name = "domains")] + public DomainDisplay[]? Domains { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs b/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs index 6c8c1b50e3..0920e45f29 100644 --- a/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs +++ b/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs @@ -1,26 +1,25 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing the navigation ("apps") inside an editor in the back office +/// +[DataContract(Name = "user", Namespace = "")] +public class EditorNavigation { - /// - /// A model representing the navigation ("apps") inside an editor in the back office - /// - [DataContract(Name = "user", Namespace = "")] - public class EditorNavigation - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } + [DataMember(Name = "icon")] + public string? Icon { get; set; } - [DataMember(Name = "view")] - public string? View { get; set; } + [DataMember(Name = "view")] + public string? View { get; set; } - [DataMember(Name = "active")] - public bool Active { get; set; } - } + [DataMember(Name = "active")] + public bool Active { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs index 772da930e9..36a837e8e8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs @@ -1,70 +1,68 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Validation; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "entity", Namespace = "")] +public class EntityBasic { - [DataContract(Name = "entity", Namespace = "")] - public class EntityBasic + public EntityBasic() { - public EntityBasic() - { - AdditionalData = new Dictionary(); - Alias = string.Empty; - Path = string.Empty; - } - - [DataMember(Name = "name", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - public string? Name { get; set; } - - [DataMember(Name = "id", IsRequired = true)] - [Required] - public object? Id { get; set; } - - [DataMember(Name = "udi")] - [ReadOnly(true)] - public Udi? Udi { get; set; } - - [DataMember(Name = "icon")] - public string? Icon { get; set; } - - [DataMember(Name = "trashed")] - [ReadOnly(true)] - public bool Trashed { get; set; } - - /// - /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider - /// - [DataMember(Name = "key")] - public Guid Key { get; set; } - - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } - - /// - /// This will only be populated for some entities like macros - /// - /// - /// It is possible to override this to specify different validation attributes if required - /// - [DataMember(Name = "alias")] - public virtual string Alias { get; set; } - - /// - /// The path of the entity - /// - [DataMember(Name = "path")] - public string Path { get; set; } - /// - /// A collection of extra data that is available for this specific entity/entity type - /// - [DataMember(Name = "metaData")] - [ReadOnly(true)] - public IDictionary AdditionalData { get; private set; } + AdditionalData = new Dictionary(); + Alias = string.Empty; + Path = string.Empty; } + + [DataMember(Name = "name", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + public string? Name { get; set; } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public object? Id { get; set; } + + [DataMember(Name = "udi")] + [ReadOnly(true)] + public Udi? Udi { get; set; } + + [DataMember(Name = "icon")] + public string? Icon { get; set; } + + [DataMember(Name = "trashed")] + [ReadOnly(true)] + public bool Trashed { get; set; } + + /// + /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider + /// + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } + + /// + /// This will only be populated for some entities like macros + /// + /// + /// It is possible to override this to specify different validation attributes if required + /// + [DataMember(Name = "alias")] + public virtual string Alias { get; set; } + + /// + /// The path of the entity + /// + [DataMember(Name = "path")] + public string Path { get; set; } + + /// + /// A collection of extra data that is available for this specific entity/entity type + /// + [DataMember(Name = "metaData")] + [ReadOnly(true)] + public IDictionary AdditionalData { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs b/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs index ff77e3aeb5..f345d881b6 100644 --- a/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs +++ b/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs @@ -1,25 +1,23 @@ using System.Collections; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "searchResults", Namespace = "")] +public class EntitySearchResults : IEnumerable { + private readonly IEnumerable _results; - [DataContract(Name = "searchResults", Namespace = "")] - public class EntitySearchResults : IEnumerable + public EntitySearchResults(IEnumerable results, long totalFound) { - private readonly IEnumerable _results; - - public EntitySearchResults(IEnumerable results, long totalFound) - { - _results = results; - TotalResults = totalFound; - } - - [DataMember(Name = "totalResults")] - public long TotalResults { get; set; } - - public IEnumerator GetEnumerator() => _results.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_results).GetEnumerator(); + _results = results; + TotalResults = totalFound; } + + [DataMember(Name = "totalResults")] + public long TotalResults { get; set; } + + public IEnumerator GetEnumerator() => _results.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_results).GetEnumerator(); } diff --git a/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs index d73687c039..c3f49d5b7d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs +++ b/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class GetAvailableCompositionsFilter { - public class GetAvailableCompositionsFilter - { - public int ContentTypeId { get; set; } + public int ContentTypeId { get; set; } - /// - /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. - /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot - /// be looked up via the db, they need to be passed in. - /// - public string[]? FilterPropertyTypes { get; set; } + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have + /// these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to + /// it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + public string[]? FilterPropertyTypes { get; set; } - /// - /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out - /// along with any content types that have matching property types that are included in the filtered content types - /// - public string[]? FilterContentTypes { get; set; } + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing + /// those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + public string[]? FilterContentTypes { get; set; } - /// - /// Wether the content type is currently marked as an element type - /// - public bool IsElement { get; set; } - } + /// + /// Wether the content type is currently marked as an element type + /// + public bool IsElement { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs index a0d9bbbcb3..386ca5f12f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs +++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs @@ -1,34 +1,33 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "historyCleanup", Namespace = "")] +public class HistoryCleanup : BeingDirtyBase { - [DataContract(Name = "historyCleanup", Namespace = "")] - public class HistoryCleanup : BeingDirtyBase + private int? _keepAllVersionsNewerThanDays; + private int? _keepLatestVersionPerDayForDays; + private bool _preventCleanup; + + [DataMember(Name = "preventCleanup")] + public bool PreventCleanup { - private bool _preventCleanup; - private int? _keepAllVersionsNewerThanDays; - private int? _keepLatestVersionPerDayForDays; + get => _preventCleanup; + set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup)); + } - [DataMember(Name = "preventCleanup")] - public bool PreventCleanup - { - get => _preventCleanup; - set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup)); - } + [DataMember(Name = "keepAllVersionsNewerThanDays")] + public int? KeepAllVersionsNewerThanDays + { + get => _keepAllVersionsNewerThanDays; + set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays)); + } - [DataMember(Name = "keepAllVersionsNewerThanDays")] - public int? KeepAllVersionsNewerThanDays - { - get => _keepAllVersionsNewerThanDays; - set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays)); - } - - [DataMember(Name = "keepLatestVersionPerDayForDays")] - public int? KeepLatestVersionPerDayForDays - { - get => _keepLatestVersionPerDayForDays; - set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays, nameof(KeepLatestVersionPerDayForDays)); - } + [DataMember(Name = "keepLatestVersionPerDayForDays")] + public int? KeepLatestVersionPerDayForDays + { + get => _keepLatestVersionPerDayForDays; + set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays, nameof(KeepLatestVersionPerDayForDays)); } } diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs index 303ff4eda3..1860dc8feb 100644 --- a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs @@ -1,18 +1,16 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "historyCleanup", Namespace = "")] +public class HistoryCleanupViewModel : HistoryCleanup { - [DataContract(Name = "historyCleanup", Namespace = "")] - public class HistoryCleanupViewModel : HistoryCleanup - { + [DataMember(Name = "globalEnableCleanup")] + public bool GlobalEnableCleanup { get; set; } - [DataMember(Name = "globalEnableCleanup")] - public bool GlobalEnableCleanup { get; set; } + [DataMember(Name = "globalKeepAllVersionsNewerThanDays")] + public int? GlobalKeepAllVersionsNewerThanDays { get; set; } - [DataMember(Name = "globalKeepAllVersionsNewerThanDays")] - public int? GlobalKeepAllVersionsNewerThanDays { get; set; } - - [DataMember(Name = "globalKeepLatestVersionPerDayForDays")] - public int? GlobalKeepLatestVersionPerDayForDays { get; set; } - } + [DataMember(Name = "globalKeepLatestVersionPerDayForDays")] + public int? GlobalKeepLatestVersionPerDayForDays { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs index fc263a3b91..e0216f66f8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs @@ -1,23 +1,24 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content app factory. +/// +public interface IContentAppFactory { /// - /// Represents a content app factory. + /// Gets the content app for an object. /// - public interface IContentAppFactory - { - /// - /// Gets the content app for an object. - /// - /// The source object. - /// The content app for the object, or null. - /// - /// The definition must determine, based on , whether - /// the content app should be displayed or not, and return either a - /// instance, or null. - /// - ContentApp? GetContentAppFor(object source, IEnumerable userGroups); - } + /// The source object. + /// The user groups + /// The content app for the object, or null. + /// + /// + /// The definition must determine, based on , whether + /// the content app should be displayed or not, and return either a + /// instance, or null. + /// + /// + ContentApp? GetContentAppFor(object source, IEnumerable userGroups); } diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs b/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs index ca8b2439c2..3520c078b1 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +public interface IContentProperties + where T : ContentPropertyBasic { - - public interface IContentProperties - where T : ContentPropertyBasic - { - IEnumerable Properties { get; } - } + IEnumerable Properties { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs index dfaf183479..effccf95fa 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs @@ -1,23 +1,23 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// An interface exposes the shared parts of content, media, members that we use during model binding in order to share +/// logic +/// +/// +public interface IContentSave : IHaveUploadedFiles + where TPersisted : IContentBase { /// - /// An interface exposes the shared parts of content, media, members that we use during model binding in order to share logic + /// The action to perform when saving this content item /// - /// - public interface IContentSave : IHaveUploadedFiles - where TPersisted : IContentBase - { - /// - /// The action to perform when saving this content item - /// - ContentSaveAction Action { get; set; } + ContentSaveAction Action { get; set; } - /// - /// The real persisted content object - used during inbound model binding - /// - /// - /// This is not used for outgoing model information. - /// - TPersisted PersistedContent { get; set; } - } + /// + /// The real persisted content object - used during inbound model binding + /// + /// + /// This is not used for outgoing model information. + /// + TPersisted PersistedContent { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs b/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs index 4352771cac..9607146eda 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +public interface IErrorModel { - public interface IErrorModel - { - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - IDictionary Errors { get; set; } - } + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// + IDictionary Errors { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs b/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs index a1d4198427..7e467ff124 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public interface IHaveUploadedFiles { - public interface IHaveUploadedFiles - { - List UploadedFiles { get; } - } + List UploadedFiles { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs b/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs index ac104c0e1b..15b75a82cf 100644 --- a/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public interface INotificationModel { - public interface INotificationModel - { - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - List? Notifications { get; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + List? Notifications { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs b/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs index 3f1d847151..13f7375c3d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +public interface ITabbedContent + where T : ContentPropertyBasic { - - public interface ITabbedContent - where T : ContentPropertyBasic - { - IEnumerable> Tabs { get; } - } + IEnumerable> Tabs { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/Language.cs b/src/Umbraco.Core/Models/ContentEditing/Language.cs index 0a0ed03a2a..15e63eabed 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Language.cs @@ -1,28 +1,27 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "language", Namespace = "")] +public class Language { - [DataContract(Name = "language", Namespace = "")] - public class Language - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "culture", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string IsoCode { get; set; } = null!; + [DataMember(Name = "culture", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string IsoCode { get; set; } = null!; - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "isDefault")] - public bool IsDefault { get; set; } + [DataMember(Name = "isDefault")] + public bool IsDefault { get; set; } - [DataMember(Name = "isMandatory")] - public bool IsMandatory { get; set; } + [DataMember(Name = "isMandatory")] + public bool IsMandatory { get; set; } - [DataMember(Name = "fallbackLanguageId")] - public int? FallbackLanguageId { get; set; } - } + [DataMember(Name = "fallbackLanguageId")] + public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs index 551065c566..9b7bde570d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs @@ -1,32 +1,31 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "link", Namespace = "")] +public class LinkDisplay { - [DataContract(Name = "link", Namespace = "")] - public class LinkDisplay - { - [DataMember(Name = "icon")] - public string? Icon { get; set; } + [DataMember(Name = "icon")] + public string? Icon { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "published")] - public bool Published { get; set; } + [DataMember(Name = "published")] + public bool Published { get; set; } - [DataMember(Name = "queryString")] - public string? QueryString { get; set; } + [DataMember(Name = "queryString")] + public string? QueryString { get; set; } - [DataMember(Name = "target")] - public string? Target { get; set; } + [DataMember(Name = "target")] + public string? Target { get; set; } - [DataMember(Name = "trashed")] - public bool Trashed { get; set; } + [DataMember(Name = "trashed")] + public bool Trashed { get; set; } - [DataMember(Name = "udi")] - public GuidUdi? Udi { get; set; } + [DataMember(Name = "udi")] + public GuidUdi? Udi { get; set; } - [DataMember(Name = "url")] - public string? Url { get; set; } - } + [DataMember(Name = "url")] + public string? Url { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs b/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs index 729a086864..1add8da7d8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs @@ -1,28 +1,27 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// An abstract model representing a content item that can be contained in a list view +/// +/// +public abstract class ListViewAwareContentItemDisplayBase : ContentItemDisplayBase + where T : ContentPropertyBasic { /// - /// An abstract model representing a content item that can be contained in a list view + /// Property indicating if this item is part of a list view parent /// - /// - public abstract class ListViewAwareContentItemDisplayBase : ContentItemDisplayBase - where T : ContentPropertyBasic - { - /// - /// Property indicating if this item is part of a list view parent - /// - [DataMember(Name = "isChildOfListView")] - public bool IsChildOfListView { get; set; } + [DataMember(Name = "isChildOfListView")] + public bool IsChildOfListView { get; set; } - /// - /// Property for the entity's individual tree node URL - /// - /// - /// This is required if the item is a child of a list view since the tree won't actually be loaded, - /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) - /// - [DataMember(Name = "treeNodeUrl")] - public string? TreeNodeUrl { get; set; } - } + /// + /// Property for the entity's individual tree node URL + /// + /// + /// This is required if the item is a child of a list view since the tree won't actually be loaded, + /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) + /// + [DataMember(Name = "treeNodeUrl")] + public string? TreeNodeUrl { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs index f794143aab..9919004a50 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs @@ -1,67 +1,65 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The macro display model +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class MacroDisplay : EntityBasic, INotificationModel { /// - /// The macro display model + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class MacroDisplay : EntityBasic, INotificationModel + public MacroDisplay() { - /// - /// Initializes a new instance of the class. - /// - public MacroDisplay() - { - Notifications = new List(); - Parameters = new List(); - } - - /// - [DataMember(Name = "notifications")] - public List Notifications { get; } - - /// - /// Gets or sets a value indicating whether the macro can be used in a rich text editor. - /// - [DataMember(Name = "useInEditor")] - public bool UseInEditor { get; set; } - - /// - /// Gets or sets a value indicating whether the macro should be rendered a rich text editor. - /// - [DataMember(Name = "renderInEditor")] - public bool RenderInEditor { get; set; } - - /// - /// Gets or sets the cache period. - /// - [DataMember(Name = "cachePeriod")] - public int CachePeriod { get; set; } - - /// - /// Gets or sets a value indicating whether the macro should be cached by page - /// - [DataMember(Name = "cacheByPage")] - public bool CacheByPage { get; set; } - - /// - /// Gets or sets a value indicating whether the macro should be cached by user - /// - [DataMember(Name = "cacheByUser")] - public bool CacheByUser { get; set; } - - /// - /// Gets or sets the view. - /// - [DataMember(Name = "view")] - public string View { get; set; } = null!; - - /// - /// Gets or sets the parameters. - /// - [DataMember(Name = "parameters")] - public IEnumerable Parameters { get; set; } + Notifications = new List(); + Parameters = new List(); } + + /// + /// Gets or sets a value indicating whether the macro can be used in a rich text editor. + /// + [DataMember(Name = "useInEditor")] + public bool UseInEditor { get; set; } + + /// + /// Gets or sets a value indicating whether the macro should be rendered a rich text editor. + /// + [DataMember(Name = "renderInEditor")] + public bool RenderInEditor { get; set; } + + /// + /// Gets or sets the cache period. + /// + [DataMember(Name = "cachePeriod")] + public int CachePeriod { get; set; } + + /// + /// Gets or sets a value indicating whether the macro should be cached by page + /// + [DataMember(Name = "cacheByPage")] + public bool CacheByPage { get; set; } + + /// + /// Gets or sets a value indicating whether the macro should be cached by user + /// + [DataMember(Name = "cacheByUser")] + public bool CacheByUser { get; set; } + + /// + /// Gets or sets the view. + /// + [DataMember(Name = "view")] + public string View { get; set; } = null!; + + /// + /// Gets or sets the parameters. + /// + [DataMember(Name = "parameters")] + public IEnumerable Parameters { get; set; } + + /// + [DataMember(Name = "notifications")] + public List Notifications { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs b/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs index 233a58cd08..3db1cd5820 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs @@ -1,43 +1,41 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a macro parameter with an editor +/// +[DataContract(Name = "macroParameter", Namespace = "")] +public class MacroParameter { + [DataMember(Name = "alias", IsRequired = true)] + [Required] + public string Alias { get; set; } = null!; + + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + /// - /// Represents a macro parameter with an editor + /// The editor view to render for this parameter /// - [DataContract(Name = "macroParameter", Namespace = "")] - public class MacroParameter - { - [DataMember(Name = "alias", IsRequired = true)] - [Required] - public string Alias { get; set; } = null!; + [DataMember(Name = "view", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string? View { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + /// + /// The configuration for this parameter editor + /// + [DataMember(Name = "config", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public IDictionary? Configuration { get; set; } - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - - /// - /// The editor view to render for this parameter - /// - [DataMember(Name = "view", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string? View { get; set; } - - /// - /// The configuration for this parameter editor - /// - [DataMember(Name = "config", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public IDictionary? Configuration { get; set; } - - /// - /// Since we don't post this back this isn't currently really used on the server side - /// - [DataMember(Name = "value")] - public object? Value { get; set; } - } + /// + /// Since we don't post this back this isn't currently really used on the server side + /// + [DataMember(Name = "value")] + public object? Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs index 8cd630d66f..3a532fcc12 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs @@ -1,35 +1,34 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The macro parameter display. +/// +[DataContract(Name = "parameter", Namespace = "")] +public class MacroParameterDisplay { /// - /// The macro parameter display. + /// Gets or sets the key. /// - [DataContract(Name = "parameter", Namespace = "")] - public class MacroParameterDisplay - { - /// - /// Gets or sets the key. - /// - [DataMember(Name = "key")] - public string Key { get; set; } = null!; + [DataMember(Name = "key")] + public string Key { get; set; } = null!; - /// - /// Gets or sets the label. - /// - [DataMember(Name = "label")] - public string? Label { get; set; } + /// + /// Gets or sets the label. + /// + [DataMember(Name = "label")] + public string? Label { get; set; } - /// - /// Gets or sets the editor. - /// - [DataMember(Name = "editor")] - public string Editor { get; set; } = null!; + /// + /// Gets or sets the editor. + /// + [DataMember(Name = "editor")] + public string Editor { get; set; } = null!; - /// - /// Gets or sets the id. - /// - [DataMember(Name = "id")] - public int Id { get; set; } - } + /// + /// Gets or sets the id. + /// + [DataMember(Name = "id")] + public int Id { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs index a56911f707..784e5510fb 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs @@ -1,26 +1,21 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a media item to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class MediaItemDisplay : ListViewAwareContentItemDisplayBase { - /// - /// A model representing a media item to be displayed in the back office - /// - [DataContract(Name = "content", Namespace = "")] - public class MediaItemDisplay : ListViewAwareContentItemDisplayBase - { - public MediaItemDisplay() - { - ContentApps = new List(); - } + public MediaItemDisplay() => ContentApps = new List(); - [DataMember(Name = "contentType")] - public ContentTypeBasic? ContentType { get; set; } + [DataMember(Name = "contentType")] + public ContentTypeBasic? ContentType { get; set; } - [DataMember(Name = "mediaLink")] - public string? MediaLink { get; set; } + [DataMember(Name = "mediaLink")] + public string? MediaLink { get; set; } - [DataMember(Name = "apps")] - public IEnumerable ContentApps { get; set; } - } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs b/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs index 06c201ab67..7bac43b25d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a media item to be saved +/// +[DataContract(Name = "content", Namespace = "")] +public class MediaItemSave : ContentBaseSave { - /// - /// A model representing a media item to be saved - /// - [DataContract(Name = "content", Namespace = "")] - public class MediaItemSave : ContentBaseSave - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs index 2c7c50550d..899be95040 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs @@ -1,11 +1,10 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentType", Namespace = "")] +public class MediaTypeDisplay : ContentTypeCompositionDisplay { - [DataContract(Name = "contentType", Namespace = "")] - public class MediaTypeDisplay : ContentTypeCompositionDisplay - { - [DataMember(Name = "isSystemMediaType")] - public bool IsSystemMediaType { get; set; } - } + [DataMember(Name = "isSystemMediaType")] + public bool IsSystemMediaType { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs index 1ef2a1988b..b3fdeea1e2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Model used to save a media type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class MediaTypeSave : ContentTypeSave { - /// - /// Model used to save a media type - /// - [DataContract(Name = "contentType", Namespace = "")] - public class MediaTypeSave : ContentTypeSave - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs b/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs index d148d88921..7ef1ce5f72 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Used for basic member information +/// +public class MemberBasic : ContentItemBasic { - /// - /// Used for basic member information - /// - public class MemberBasic : ContentItemBasic + [DataMember(Name = "username")] + public string? Username { get; set; } + + [DataMember(Name = "email")] + public string? Email { get; set; } + + [DataMember(Name = "properties")] + public override IEnumerable Properties { - [DataMember(Name = "username")] - public string? Username { get; set; } - - [DataMember(Name = "email")] - public string? Email { get; set; } - - [DataMember(Name = "properties")] - public override IEnumerable Properties - { - get => base.Properties; - set => base.Properties = value; - } + get => base.Properties; + set => base.Properties = value; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs index 5448c40b1e..161c085d35 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs @@ -1,51 +1,46 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a member to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class MemberDisplay : ListViewAwareContentItemDisplayBase { - /// - /// A model representing a member to be displayed in the back office - /// - [DataContract(Name = "content", Namespace = "")] - public class MemberDisplay : ListViewAwareContentItemDisplayBase - { - public MemberDisplay() - { - // MemberProviderFieldMapping = new Dictionary(); - ContentApps = new List(); - } + public MemberDisplay() => - [DataMember(Name = "contentType")] - public ContentTypeBasic? ContentType { get; set; } + // MemberProviderFieldMapping = new Dictionary(); + ContentApps = new List(); - [DataMember(Name = "username")] - public string? Username { get; set; } + [DataMember(Name = "contentType")] + public ContentTypeBasic? ContentType { get; set; } - [DataMember(Name = "email")] - public string? Email { get; set; } + [DataMember(Name = "username")] + public string? Username { get; set; } - [DataMember(Name = "isLockedOut")] - public bool IsLockedOut { get; set; } + [DataMember(Name = "email")] + public string? Email { get; set; } - [DataMember(Name = "isApproved")] - public bool IsApproved { get; set; } + [DataMember(Name = "isLockedOut")] + public bool IsLockedOut { get; set; } - //[DataMember(Name = "membershipScenario")] - //public MembershipScenario MembershipScenario { get; set; } + [DataMember(Name = "isApproved")] + public bool IsApproved { get; set; } - // /// - // /// This is used to indicate how to map the membership provider properties to the save model, this mapping - // /// will change if a developer has opted to have custom member property aliases specified in their membership provider config, - // /// or if we are editing a member that is not an Umbraco member (custom provider) - // /// - // [DataMember(Name = "fieldConfig")] - // public IDictionary MemberProviderFieldMapping { get; set; } + // [DataMember(Name = "membershipScenario")] + // public MembershipScenario MembershipScenario { get; set; } - [DataMember(Name = "apps")] - public IEnumerable ContentApps { get; set; } + // /// + // /// This is used to indicate how to map the membership provider properties to the save model, this mapping + // /// will change if a developer has opted to have custom member property aliases specified in their membership provider config, + // /// or if we are editing a member that is not an Umbraco member (custom provider) + // /// + // [DataMember(Name = "fieldConfig")] + // public IDictionary MemberProviderFieldMapping { get; set; } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } - - [DataMember(Name = "membershipProperties")] - public IEnumerable? MembershipProperties { get; set; } - } + [DataMember(Name = "membershipProperties")] + public IEnumerable? MembershipProperties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs index 2d930727aa..0804fd53d7 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs @@ -1,20 +1,15 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - [DataContract(Name = "memberGroup", Namespace = "")] - public class MemberGroupDisplay : EntityBasic, INotificationModel - { - public MemberGroupDisplay() - { - Notifications = new List(); - } +namespace Umbraco.Cms.Core.Models.ContentEditing; - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - } +[DataContract(Name = "memberGroup", Namespace = "")] +public class MemberGroupDisplay : EntityBasic, INotificationModel +{ + public MemberGroupDisplay() => Notifications = new List(); + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs b/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs index 2b863a758d..292d410625 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs @@ -1,9 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "memberGroup", Namespace = "")] +public class MemberGroupSave : EntityBasic { - [DataContract(Name = "memberGroup", Namespace = "")] - public class MemberGroupSave : EntityBasic - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs index c4a5382e84..cd89f46fc6 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a member list to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class MemberListDisplay : ContentItemDisplayBase { - /// - /// A model representing a member list to be displayed in the back office - /// - [DataContract(Name = "content", Namespace = "")] - public class MemberListDisplay : ContentItemDisplayBase - { - [DataMember(Name = "apps")] - public IEnumerable? ContentApps { get; set; } - } + [DataMember(Name = "apps")] + public IEnumerable? ContentApps { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs index b25f2ae5c8..9ef0ebf3b9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Basic member property type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class MemberPropertyTypeBasic : PropertyTypeBasic { - /// - /// Basic member property type - /// - [DataContract(Name = "contentType", Namespace = "")] - public class MemberPropertyTypeBasic : PropertyTypeBasic - { - [DataMember(Name = "showOnMemberProfile")] - public bool MemberCanViewProperty { get; set; } + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } - [DataMember(Name = "memberCanEdit")] - public bool MemberCanEditProperty { get; set; } + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } - [DataMember(Name = "isSensitiveData")] - public bool IsSensitiveData { get; set; } - } + [DataMember(Name = "isSensitiveData")] + public bool IsSensitiveData { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs index 873883c8db..1038440974 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyType")] +public class MemberPropertyTypeDisplay : PropertyTypeDisplay { - [DataContract(Name = "propertyType")] - public class MemberPropertyTypeDisplay : PropertyTypeDisplay - { - [DataMember(Name = "showOnMemberProfile")] - public bool MemberCanViewProperty { get; set; } + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } - [DataMember(Name = "memberCanEdit")] - public bool MemberCanEditProperty { get; set; } + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } - [DataMember(Name = "isSensitiveData")] - public bool IsSensitiveData { get; set; } - } + [DataMember(Name = "isSensitiveData")] + public bool IsSensitiveData { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs b/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs index 903c87341a..2963618e1b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs @@ -1,48 +1,48 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Validation; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +public class MemberSave : ContentBaseSave { - /// - public class MemberSave : ContentBaseSave + [DataMember(Name = "username", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + public string Username { get; set; } = null!; + + [DataMember(Name = "email", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + [EmailAddress] + public string Email { get; set; } = null!; + + [DataMember(Name = "password")] + public ChangingPasswordModel? Password { get; set; } + + [DataMember(Name = "memberGroups")] + public IEnumerable? Groups { get; set; } + + /// + /// Returns the value from the Comments property + /// + public string? Comments => GetPropertyValue(Constants.Conventions.Member.Comments); + + [DataMember(Name = "isLockedOut")] + public bool IsLockedOut { get; set; } + + [DataMember(Name = "isApproved")] + public bool IsApproved { get; set; } + + private T? GetPropertyValue(string alias) { - - [DataMember(Name = "username", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - public string Username { get; set; } = null!; - - [DataMember(Name = "email", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - [EmailAddress] - public string Email { get; set; } = null!; - - [DataMember(Name = "password")] - public ChangingPasswordModel? Password { get; set; } - - [DataMember(Name = "memberGroups")] - public IEnumerable? Groups { get; set; } - - /// - /// Returns the value from the Comments property - /// - public string? Comments => GetPropertyValue(Constants.Conventions.Member.Comments); - - [DataMember(Name = "isLockedOut")] - public bool IsLockedOut { get; set; } - - [DataMember(Name = "isApproved")] - public bool IsApproved { get; set; } - - private T? GetPropertyValue(string alias) + ContentPropertyBasic? prop = Properties.FirstOrDefault(x => x.Alias == alias); + if (prop == null) { - var prop = Properties.FirstOrDefault(x => x.Alias == alias); - if (prop == null) return default; - var converted = prop.Value.TryConvertTo(); - return converted.Result ?? default; + return default; } + + Attempt converted = prop.Value.TryConvertTo(); + return converted.Result ?? default; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs index 67e390f378..ea8aa5c1e3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs @@ -1,9 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentType", Namespace = "")] +public class MemberTypeDisplay : ContentTypeCompositionDisplay { - [DataContract(Name = "contentType", Namespace = "")] - public class MemberTypeDisplay : ContentTypeCompositionDisplay - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs index 80ac46ae09..59a6047494 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Model used to save a member type +/// +public class MemberTypeSave : ContentTypeSave { - /// - /// Model used to save a member type - /// - public class MemberTypeSave : ContentTypeSave - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs b/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs index 5a93ae94c9..5a65111345 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs @@ -1,70 +1,80 @@ -using System.Linq; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class MessagesExtensions { - public static class MessagesExtensions + public static void AddNotification(this INotificationModel model, string header, string msg, NotificationStyle type) { - public static void AddNotification(this INotificationModel model, string header, string msg, NotificationStyle type) + if (model.Exists(header, msg, type)) { - if (model.Exists(header, msg, type)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = type - }); + return; } - public static void AddSuccessNotification(this INotificationModel model, string header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Success)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Success - }); - } - - public static void AddErrorNotification(this INotificationModel model, string? header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Error)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Error - }); - } - - public static void AddWarningNotification(this INotificationModel model, string header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Warning)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Warning - }); - } - - public static void AddInfoNotification(this INotificationModel model, string header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Info)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Info - }); - } - - private static bool Exists(this INotificationModel model, string? header, string message, NotificationStyle notificationType) => model.Notifications?.Any(x => (x.Header?.InvariantEquals(header) ?? false) && (x.Message?.InvariantEquals(message) ?? false) && x.NotificationType == notificationType) ?? false; + model.Notifications?.Add(new BackOfficeNotification { Header = header, Message = msg, NotificationType = type }); } + + public static void AddSuccessNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Success)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Success, + }); + } + + public static void AddErrorNotification(this INotificationModel model, string? header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Error)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Error, + }); + } + + public static void AddWarningNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Warning)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Warning, + }); + } + + public static void AddInfoNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Info)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Info, + }); + } + + private static bool Exists(this INotificationModel model, string? header, string message, NotificationStyle notificationType) => model.Notifications?.Any(x => + (x.Header?.InvariantEquals(header) ?? false) && (x.Message?.InvariantEquals(message) ?? false) && + x.NotificationType == notificationType) ?? false; } diff --git a/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs b/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs index d79be81725..56275bfb6c 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs @@ -1,31 +1,30 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A generic model supporting notifications, this is useful for returning any model type to include notifications from +/// api controllers +/// +/// +[DataContract(Name = "model", Namespace = "")] +public class ModelWithNotifications : INotificationModel { - /// - /// A generic model supporting notifications, this is useful for returning any model type to include notifications from api controllers - /// - /// - [DataContract(Name = "model", Namespace = "")] - public class ModelWithNotifications : INotificationModel + public ModelWithNotifications(T value) { - public ModelWithNotifications(T value) - { - Value = value; - Notifications = new List(); - } - - /// - /// The generic value - /// - [DataMember(Name = "value")] - public T Value { get; private set; } - - /// - /// The notifications - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + Value = value; + Notifications = new List(); } + + /// + /// The generic value + /// + [DataMember(Name = "value")] + public T Value { get; private set; } + + /// + /// The notifications + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs b/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs index c27cf70ccf..ecbcc027f4 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs @@ -1,41 +1,39 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a model for moving or copying +/// +[DataContract(Name = "content", Namespace = "")] +public class MoveOrCopy { /// - /// A model representing a model for moving or copying + /// The Id of the node to move or copy to /// - [DataContract(Name = "content", Namespace = "")] - public class MoveOrCopy - { - /// - /// The Id of the node to move or copy to - /// - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } - /// - /// The id of the node to move or copy - /// - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } + /// + /// The id of the node to move or copy + /// + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } - /// - /// Boolean indicating whether copying the object should create a relation to it's original - /// - [DataMember(Name = "relateToOriginal", IsRequired = true)] - [Required] - public bool RelateToOriginal { get; set; } - - /// - /// Boolean indicating whether copying the object should be recursive - /// - [DataMember(Name = "recursive", IsRequired = true)] - [Required] - public bool Recursive { get; set; } - } + /// + /// Boolean indicating whether copying the object should create a relation to it's original + /// + [DataMember(Name = "relateToOriginal", IsRequired = true)] + [Required] + public bool RelateToOriginal { get; set; } + /// + /// Boolean indicating whether copying the object should be recursive + /// + [DataMember(Name = "recursive", IsRequired = true)] + [Required] + public bool Recursive { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs b/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs index a8c17d1850..1fe9e9b525 100644 --- a/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs +++ b/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs @@ -1,29 +1,32 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract] +public enum NotificationStyle { - [DataContract] - public enum NotificationStyle - { - /// - /// Save icon - /// - Save = 0, - /// - /// Info icon - /// - Info = 1, - /// - /// Error icon - /// - Error = 2, - /// - /// Success icon - /// - Success = 3, - /// - /// Warning icon - /// - Warning = 4 - } + /// + /// Save icon + /// + Save = 0, + + /// + /// Info icon + /// + Info = 1, + + /// + /// Error icon + /// + Error = 2, + + /// + /// Success icon + /// + Success = 3, + + /// + /// Warning icon + /// + Warning = 4, } diff --git a/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs b/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs index ee4029cab3..603ec953b0 100644 --- a/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs +++ b/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "notifySetting", Namespace = "")] +public class NotifySetting { - [DataContract(Name = "notifySetting", Namespace = "")] - public class NotifySetting - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "checked")] - public bool Checked { get; set; } + [DataMember(Name = "checked")] + public bool Checked { get; set; } - /// - /// The letter from the IAction - /// - [DataMember(Name = "notifyCode")] - public string? NotifyCode { get; set; } - } + /// + /// The letter from the IAction + /// + [DataMember(Name = "notifyCode")] + public string? NotifyCode { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs b/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs index 4682b752b9..c2f69218b3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs @@ -1,15 +1,13 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - [DataContract(Name = "objectType", Namespace = "")] - public class ObjectType - { - [DataMember(Name = "name")] - public string? Name { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "id")] - public Guid Id { get; set; } - } +[DataContract(Name = "objectType", Namespace = "")] +public class ObjectType +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "id")] + public Guid Id { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/Permission.cs b/src/Umbraco.Core/Models/ContentEditing/Permission.cs index c6e446fc39..9bdb664579 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Permission.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Permission.cs @@ -1,38 +1,33 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "permission", Namespace = "")] +public class Permission : ICloneable { - [DataContract(Name = "permission", Namespace = "")] - public class Permission : ICloneable - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "description")] - public string? Description { get; set; } + [DataMember(Name = "description")] + public string? Description { get; set; } - [DataMember(Name = "checked")] - public bool Checked { get; set; } + [DataMember(Name = "checked")] + public bool Checked { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } + [DataMember(Name = "icon")] + public string? Icon { get; set; } - /// - /// We'll use this to map the categories but it wont' be returned in the json - /// - [IgnoreDataMember] - public string Category { get; set; } = null!; + /// + /// We'll use this to map the categories but it wont' be returned in the json + /// + [IgnoreDataMember] + public string Category { get; set; } = null!; - /// - /// The letter from the IAction - /// - [DataMember(Name = "permissionCode")] - public string? PermissionCode { get; set; } + /// + /// The letter from the IAction + /// + [DataMember(Name = "permissionCode")] + public string? PermissionCode { get; set; } - public object Clone() - { - return this.MemberwiseClone(); - } - } + public object Clone() => MemberwiseClone(); } diff --git a/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs b/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs index 69029c961a..5d71369141 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs @@ -1,24 +1,23 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - /// - /// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the - /// temporary files that were created. - /// - [DataContract] - public class PostedFiles : IHaveUploadedFiles, INotificationModel - { - public PostedFiles() - { - UploadedFiles = new List(); - Notifications = new List(); - } - public List UploadedFiles { get; private set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } +/// +/// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the +/// temporary files that were created. +/// +[DataContract] +public class PostedFiles : IHaveUploadedFiles, INotificationModel +{ + public PostedFiles() + { + UploadedFiles = new List(); + Notifications = new List(); } + + public List UploadedFiles { get; } + + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs b/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs index 56ca1c1907..79769559db 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs @@ -1,17 +1,16 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - /// - /// Used to create a folder with the MediaController - /// - [DataContract] - public class PostedFolder - { - [DataMember(Name = "parentId")] - public string? ParentId { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "name")] - public string? Name { get; set; } - } +/// +/// Used to create a folder with the MediaController +/// +[DataContract] +public class PostedFolder +{ + [DataMember(Name = "parentId")] + public string? ParentId { get; set; } + + [DataMember(Name = "name")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs index b73f2897e7..498537cf1e 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Defines an available property editor to be able to select for a data type +/// +[DataContract(Name = "propertyEditor", Namespace = "")] +public class PropertyEditorBasic { - /// - /// Defines an available property editor to be able to select for a data type - /// - [DataContract(Name = "propertyEditor", Namespace = "")] - public class PropertyEditorBasic - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } - } + [DataMember(Name = "icon")] + public string? Icon { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs index 0431fb270f..5b45776a8e 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs @@ -1,66 +1,62 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyGroup", Namespace = "")] +public abstract class PropertyGroupBasic { - [DataContract(Name = "propertyGroup", Namespace = "")] - public abstract class PropertyGroupBasic - { - /// - /// Gets the special generic properties tab identifier. - /// - public const int GenericPropertiesGroupId = -666; + /// + /// Gets the special generic properties tab identifier. + /// + public const int GenericPropertiesGroupId = -666; - /// - /// Gets a value indicating whether this tab is the generic properties tab. - /// - [IgnoreDataMember] - public bool IsGenericProperties => Id == GenericPropertiesGroupId; + /// + /// Gets a value indicating whether this tab is the generic properties tab. + /// + [IgnoreDataMember] + public bool IsGenericProperties => Id == GenericPropertiesGroupId; - /// - /// Gets a value indicating whether the property group is inherited through - /// content types composition. - /// - /// A property group can be inherited and defined on the content type - /// currently being edited, at the same time. Inherited is true when there exists at least - /// one property group higher in the composition, with the same alias. - [DataMember(Name = "inherited")] - public bool Inherited { get; set; } + /// + /// Gets a value indicating whether the property group is inherited through + /// content types composition. + /// + /// + /// A property group can be inherited and defined on the content type + /// currently being edited, at the same time. Inherited is true when there exists at least + /// one property group higher in the composition, with the same alias. + /// + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } - // needed - so we can handle alias renames - [DataMember(Name = "id")] - public int Id { get; set; } + // needed - so we can handle alias renames + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "key")] - public Guid Key { get; set; } + [DataMember(Name = "key")] + public Guid Key { get; set; } - [DataMember(Name = "type")] - public PropertyGroupType Type { get; set; } + [DataMember(Name = "type")] + public PropertyGroupType Type { get; set; } - [Required] - [DataMember(Name = "name")] - public string? Name { get; set; } + [Required] + [DataMember(Name = "name")] + public string? Name { get; set; } - [Required] - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; + [Required] + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - } - - [DataContract(Name = "propertyGroup", Namespace = "")] - public class PropertyGroupBasic : PropertyGroupBasic - where TPropertyType: PropertyTypeBasic - { - public PropertyGroupBasic() - { - Properties = new List(); - } - - [DataMember(Name = "properties")] - public IEnumerable Properties { get; set; } - } + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } +} + +[DataContract(Name = "propertyGroup", Namespace = "")] +public class PropertyGroupBasic : PropertyGroupBasic + where TPropertyType : PropertyTypeBasic +{ + public PropertyGroupBasic() => Properties = new List(); + + [DataMember(Name = "properties")] + public IEnumerable Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs index 6f1317f3eb..4e3b530f99 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +internal static class PropertyGroupBasicExtensions { - internal static class PropertyGroupBasicExtensions - { - public static string? GetParentAlias(this PropertyGroupBasic propertyGroup) - => PropertyGroupExtensions.GetParentAlias(propertyGroup.Alias); - } + public static string? GetParentAlias(this PropertyGroupBasic propertyGroup) + => PropertyGroupExtensions.GetParentAlias(propertyGroup.Alias); } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs index a543d85347..67a200cf65 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs @@ -1,39 +1,37 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyGroup", Namespace = "")] +public class PropertyGroupDisplay : PropertyGroupBasic + where TPropertyTypeDisplay : PropertyTypeDisplay { - [DataContract(Name = "propertyGroup", Namespace = "")] - public class PropertyGroupDisplay : PropertyGroupBasic - where TPropertyTypeDisplay : PropertyTypeDisplay + public PropertyGroupDisplay() { - public PropertyGroupDisplay() - { - Properties = new List(); - ParentTabContentTypeNames = new List(); - ParentTabContentTypes = new List(); - } - - /// - /// Gets the context content type. - /// - [DataMember(Name = "contentTypeId")] - [ReadOnly(true)] - public int ContentTypeId { get; set; } - - /// - /// Gets the identifiers of the content types that define this group. - /// - [DataMember(Name = "parentTabContentTypes")] - [ReadOnly(true)] - public IEnumerable ParentTabContentTypes { get; set; } - - /// - /// Gets the name of the content types that define this group. - /// - [DataMember(Name = "parentTabContentTypeNames")] - [ReadOnly(true)] - public IEnumerable ParentTabContentTypeNames { get; set; } + Properties = new List(); + ParentTabContentTypeNames = new List(); + ParentTabContentTypes = new List(); } + + /// + /// Gets the context content type. + /// + [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] + public int ContentTypeId { get; set; } + + /// + /// Gets the identifiers of the content types that define this group. + /// + [DataMember(Name = "parentTabContentTypes")] + [ReadOnly(true)] + public IEnumerable ParentTabContentTypes { get; set; } + + /// + /// Gets the name of the content types that define this group. + /// + [DataMember(Name = "parentTabContentTypeNames")] + [ReadOnly(true)] + public IEnumerable ParentTabContentTypeNames { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs index 0aded31a18..4574e62cde 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs @@ -1,72 +1,72 @@ -using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyType")] +public class PropertyTypeBasic { - [DataContract(Name = "propertyType")] - public class PropertyTypeBasic - { - /// - /// Gets a value indicating whether the property type is inherited through - /// content types composition. - /// - /// Inherited is true when the property is defined by a content type - /// higher in the composition, and not by the content type currently being - /// edited. - [DataMember(Name = "inherited")] - public bool Inherited { get; set; } + /// + /// Gets a value indicating whether the property type is inherited through + /// content types composition. + /// + /// + /// Inherited is true when the property is defined by a content type + /// higher in the composition, and not by the content type currently being + /// edited. + /// + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } - // needed - so we can handle alias renames - [DataMember(Name = "id")] - public int Id { get; set; } + // needed - so we can handle alias renames + [DataMember(Name = "id")] + public int Id { get; set; } - [Required] - [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; - [DataMember(Name = "description")] - public string? Description { get; set; } + [DataMember(Name = "description")] + public string? Description { get; set; } - [DataMember(Name = "validation")] - public PropertyTypeValidation? Validation { get; set; } + [DataMember(Name = "validation")] + public PropertyTypeValidation? Validation { get; set; } - [DataMember(Name = "label")] - [Required] - public string Label { get; set; } = null!; + [DataMember(Name = "label")] + [Required] + public string Label { get; set; } = null!; - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } - [DataMember(Name = "dataTypeId")] - [Required] - public int DataTypeId { get; set; } + [DataMember(Name = "dataTypeId")] + [Required] + public int DataTypeId { get; set; } - [DataMember(Name = "dataTypeKey")] - [ReadOnly(true)] - public Guid DataTypeKey { get; set; } + [DataMember(Name = "dataTypeKey")] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } - [DataMember(Name = "dataTypeName")] - [ReadOnly(true)] - public string? DataTypeName { get; set; } + [DataMember(Name = "dataTypeName")] + [ReadOnly(true)] + public string? DataTypeName { get; set; } - [DataMember(Name = "dataTypeIcon")] - [ReadOnly(true)] - public string? DataTypeIcon { get; set; } + [DataMember(Name = "dataTypeIcon")] + [ReadOnly(true)] + public string? DataTypeIcon { get; set; } - //SD: Is this really needed ? - [DataMember(Name = "groupId")] - public int GroupId { get; set; } + // SD: Is this really needed ? + [DataMember(Name = "groupId")] + public int GroupId { get; set; } - [DataMember(Name = "allowCultureVariant")] - public bool AllowCultureVariant { get; set; } + [DataMember(Name = "allowCultureVariant")] + public bool AllowCultureVariant { get; set; } - [DataMember(Name = "allowSegmentVariant")] - public bool AllowSegmentVariant { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } - [DataMember(Name = "labelOnTop")] - public bool LabelOnTop { get; set; } - } + [DataMember(Name = "labelOnTop")] + public bool LabelOnTop { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs index 5ca3e4de5c..926ea50106 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs @@ -1,48 +1,47 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyType")] +public class PropertyTypeDisplay : PropertyTypeBasic { - [DataContract(Name = "propertyType")] - public class PropertyTypeDisplay : PropertyTypeBasic - { - [DataMember(Name = "editor")] - [ReadOnly(true)] - public string? Editor { get; set; } + [DataMember(Name = "editor")] + [ReadOnly(true)] + public string? Editor { get; set; } - [DataMember(Name = "view")] - [ReadOnly(true)] - public string? View { get; set; } + [DataMember(Name = "view")] + [ReadOnly(true)] + public string? View { get; set; } - [DataMember(Name = "config")] - [ReadOnly(true)] - public IDictionary? Config { get; set; } + [DataMember(Name = "config")] + [ReadOnly(true)] + public IDictionary? Config { get; set; } - /// - /// Gets a value indicating whether this property should be locked when editing. - /// - /// This is used for built in properties like the default MemberType - /// properties that should not be editable from the backoffice. - [DataMember(Name = "locked")] - [ReadOnly(true)] - public bool Locked { get; set; } + /// + /// Gets a value indicating whether this property should be locked when editing. + /// + /// + /// This is used for built in properties like the default MemberType + /// properties that should not be editable from the backoffice. + /// + [DataMember(Name = "locked")] + [ReadOnly(true)] + public bool Locked { get; set; } - /// - /// This is required for the UI editor to know if this particular property belongs to - /// an inherited item or the current item. - /// - [DataMember(Name = "contentTypeId")] - [ReadOnly(true)] - public int ContentTypeId { get; set; } + /// + /// This is required for the UI editor to know if this particular property belongs to + /// an inherited item or the current item. + /// + [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] + public int ContentTypeId { get; set; } - /// - /// This is required for the UI editor to know which content type name this property belongs - /// to based on the property inheritance structure - /// - [DataMember(Name = "contentTypeName")] - [ReadOnly(true)] - public string? ContentTypeName { get; set; } - - } + /// + /// This is required for the UI editor to know which content type name this property belongs + /// to based on the property inheritance structure + /// + [DataMember(Name = "contentTypeName")] + [ReadOnly(true)] + public string? ContentTypeName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeHasValuesDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeHasValuesDisplay.cs new file mode 100644 index 0000000000..6b5094ab1c --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeHasValuesDisplay.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyTypeHasValuesDisplay")] +public class PropertyTypeHasValuesDisplay +{ + public PropertyTypeHasValuesDisplay(string propertyTypeAlias, bool hasValues) + { + PropertyTypeAlias = propertyTypeAlias; + HasValues = hasValues; + } + + [DataMember(Name = "propertyTypeAlias")] + public string PropertyTypeAlias { get; } + + [DataMember(Name = "hasValues")] + public bool HasValues { get; } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs index 5db1ab8139..76e9547c07 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs @@ -1,23 +1,22 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// An object representing the property type validation settings +/// +[DataContract(Name = "propertyValidation", Namespace = "")] +public class PropertyTypeValidation { - /// - /// An object representing the property type validation settings - /// - [DataContract(Name = "propertyValidation", Namespace = "")] - public class PropertyTypeValidation - { - [DataMember(Name = "mandatory")] - public bool Mandatory { get; set; } + [DataMember(Name = "mandatory")] + public bool Mandatory { get; set; } - [DataMember(Name = "mandatoryMessage")] - public string? MandatoryMessage { get; set; } + [DataMember(Name = "mandatoryMessage")] + public string? MandatoryMessage { get; set; } - [DataMember(Name = "pattern")] - public string? Pattern { get; set; } + [DataMember(Name = "pattern")] + public string? Pattern { get; set; } - [DataMember(Name = "patternMessage")] - public string? PatternMessage { get; set; } - } + [DataMember(Name = "patternMessage")] + public string? PatternMessage { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs b/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs index 199ca34ceb..1c21aec033 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "publicAccess", Namespace = "")] +public class PublicAccess { - [DataContract(Name = "publicAccess", Namespace = "")] - public class PublicAccess - { - [DataMember(Name = "groups")] - public MemberGroupDisplay[]? Groups { get; set; } + [DataMember(Name = "groups")] + public MemberGroupDisplay[]? Groups { get; set; } - [DataMember(Name = "loginPage")] - public EntityBasic? LoginPage { get; set; } + [DataMember(Name = "loginPage")] + public EntityBasic? LoginPage { get; set; } - [DataMember(Name = "errorPage")] - public EntityBasic? ErrorPage { get; set; } + [DataMember(Name = "errorPage")] + public EntityBasic? ErrorPage { get; set; } - [DataMember(Name = "members")] - public MemberDisplay[]? Members { get; set; } - } + [DataMember(Name = "members")] + public MemberDisplay[]? Members { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs b/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs index e4b026b6eb..8a1a8d91c9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "redirectUrlSearchResult", Namespace = "")] +public class RedirectUrlSearchResult { - [DataContract(Name = "redirectUrlSearchResult", Namespace = "")] - public class RedirectUrlSearchResult - { - [DataMember(Name = "searchResults")] - public IEnumerable? SearchResults { get; set; } + [DataMember(Name = "searchResults")] + public IEnumerable? SearchResults { get; set; } - [DataMember(Name = "totalCount")] - public long TotalCount { get; set; } + [DataMember(Name = "totalCount")] + public long TotalCount { get; set; } - [DataMember(Name = "pageCount")] - public int PageCount { get; set; } + [DataMember(Name = "pageCount")] + public int PageCount { get; set; } - [DataMember(Name = "currentPage")] - public int CurrentPage { get; set; } - } + [DataMember(Name = "currentPage")] + public int CurrentPage { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs index 0decb18414..d4cb960251 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs @@ -1,52 +1,50 @@ -using System; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "relation", Namespace = "")] +public class RelationDisplay { - [DataContract(Name = "relation", Namespace = "")] - public class RelationDisplay - { - /// - /// Gets or sets the Parent Id of the Relation (Source). - /// - [DataMember(Name = "parentId")] - [ReadOnly(true)] - public int ParentId { get; set; } + /// + /// Gets or sets the Parent Id of the Relation (Source). + /// + [DataMember(Name = "parentId")] + [ReadOnly(true)] + public int ParentId { get; set; } - /// - /// Gets or sets the Parent Name of the relation (Source). - /// - [DataMember(Name = "parentName")] - [ReadOnly(true)] - public string? ParentName { get; set; } + /// + /// Gets or sets the Parent Name of the relation (Source). + /// + [DataMember(Name = "parentName")] + [ReadOnly(true)] + public string? ParentName { get; set; } - /// - /// Gets or sets the Child Id of the Relation (Destination). - /// - [DataMember(Name = "childId")] - [ReadOnly(true)] - public int ChildId { get; set; } + /// + /// Gets or sets the Child Id of the Relation (Destination). + /// + [DataMember(Name = "childId")] + [ReadOnly(true)] + public int ChildId { get; set; } - /// - /// Gets or sets the Child Name of the relation (Destination). - /// - [DataMember(Name = "childName")] - [ReadOnly(true)] - public string? ChildName { get; set; } + /// + /// Gets or sets the Child Name of the relation (Destination). + /// + [DataMember(Name = "childName")] + [ReadOnly(true)] + public string? ChildName { get; set; } - /// - /// Gets or sets the date when the Relation was created. - /// - [DataMember(Name = "createDate")] - [ReadOnly(true)] - public DateTime CreateDate { get; set; } + /// + /// Gets or sets the date when the Relation was created. + /// + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } - /// - /// Gets or sets a comment for the Relation. - /// - [DataMember(Name = "comment")] - [ReadOnly(true)] - public string? Comment { get; set; } - } + /// + /// Gets or sets a comment for the Relation. + /// + [DataMember(Name = "comment")] + [ReadOnly(true)] + public string? Comment { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs index 906fdf3a40..b6168e13d5 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs @@ -1,65 +1,59 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "relationType", Namespace = "")] +public class RelationTypeDisplay : EntityBasic, INotificationModel { - [DataContract(Name = "relationType", Namespace = "")] - public class RelationTypeDisplay : EntityBasic, INotificationModel - { - public RelationTypeDisplay() - { - Notifications = new List(); - } + public RelationTypeDisplay() => Notifications = new List(); - [DataMember(Name = "isSystemRelationType")] - public bool IsSystemRelationType { get; set; } + [DataMember(Name = "isSystemRelationType")] + public bool IsSystemRelationType { get; set; } - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember(Name = "isBidirectional", IsRequired = true)] - public bool IsBidirectional { get; set; } + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember(Name = "isBidirectional", IsRequired = true)] + public bool IsBidirectional { get; set; } - /// - /// Gets or sets the Parents object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember(Name = "parentObjectType", IsRequired = true)] - public Guid? ParentObjectType { get; set; } + /// + /// Gets or sets the Parents object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember(Name = "parentObjectType", IsRequired = true)] + public Guid? ParentObjectType { get; set; } - /// - /// Gets or sets the Parent's object type name. - /// - [DataMember(Name = "parentObjectTypeName")] - [ReadOnly(true)] - public string? ParentObjectTypeName { get; set; } + /// + /// Gets or sets the Parent's object type name. + /// + [DataMember(Name = "parentObjectTypeName")] + [ReadOnly(true)] + public string? ParentObjectTypeName { get; set; } - /// - /// Gets or sets the Child's object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember(Name = "childObjectType", IsRequired = true)] - public Guid? ChildObjectType { get; set; } + /// + /// Gets or sets the Child's object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember(Name = "childObjectType", IsRequired = true)] + public Guid? ChildObjectType { get; set; } - /// - /// Gets or sets the Child's object type name. - /// - [DataMember(Name = "childObjectTypeName")] - [ReadOnly(true)] - public string? ChildObjectTypeName { get; set; } + /// + /// Gets or sets the Child's object type name. + /// + [DataMember(Name = "childObjectTypeName")] + [ReadOnly(true)] + public string? ChildObjectTypeName { get; set; } - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } - /// - /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. - /// - [DataMember(Name = "isDependency", IsRequired = true)] - public bool IsDependency { get; set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs index f541158095..910d2827f7 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs @@ -1,33 +1,31 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "relationType", Namespace = "")] +public class RelationTypeSave : EntityBasic { - [DataContract(Name = "relationType", Namespace = "")] - public class RelationTypeSave : EntityBasic - { - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember(Name = "isBidirectional", IsRequired = true)] - public bool IsBidirectional { get; set; } + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember(Name = "isBidirectional", IsRequired = true)] + public bool IsBidirectional { get; set; } - /// - /// Gets or sets the parent object type ID. - /// - [DataMember(Name = "parentObjectType", IsRequired = false)] - public Guid? ParentObjectType { get; set; } + /// + /// Gets or sets the parent object type ID. + /// + [DataMember(Name = "parentObjectType", IsRequired = false)] + public Guid? ParentObjectType { get; set; } - /// - /// Gets or sets the child object type ID. - /// - [DataMember(Name = "childObjectType", IsRequired = false)] - public Guid? ChildObjectType { get; set; } + /// + /// Gets or sets the child object type ID. + /// + [DataMember(Name = "childObjectType", IsRequired = false)] + public Guid? ChildObjectType { get; set; } - /// - /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. - /// - [DataMember(Name = "isDependency", IsRequired = true)] - public bool IsDependency { get; set; } - } + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs index 06fcc5d124..782f34c88c 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs @@ -1,24 +1,23 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public enum RichTextEditorCommandMode { - [DataContract(Name = "richtexteditorcommand", Namespace = "")] - public class RichTextEditorCommand - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - [DataMember(Name = "mode")] - public RichTextEditorCommandMode Mode { get; set; } - } - - public enum RichTextEditorCommandMode - { - Insert, - Selection, - All - } + Insert, + Selection, + All, +} + +[DataContract(Name = "richtexteditorcommand", Namespace = "")] +public class RichTextEditorCommand +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "alias")] + public string? Alias { get; set; } + + [DataMember(Name = "mode")] + public RichTextEditorCommandMode Mode { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs index e80b25f4ae..c621aa8c59 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "richtexteditorconfiguration", Namespace = "")] +public class RichTextEditorConfiguration { - [DataContract(Name = "richtexteditorconfiguration", Namespace = "")] - public class RichTextEditorConfiguration - { - [DataMember(Name = "plugins")] - public IEnumerable? Plugins { get; set; } + [DataMember(Name = "plugins")] + public IEnumerable? Plugins { get; set; } - [DataMember(Name = "commands")] - public IEnumerable? Commands { get; set; } + [DataMember(Name = "commands")] + public IEnumerable? Commands { get; set; } - [DataMember(Name = "validElements")] - public string? ValidElements { get; set; } + [DataMember(Name = "validElements")] + public string? ValidElements { get; set; } - [DataMember(Name = "inValidElements")] - public string? InvalidElements { get; set; } + [DataMember(Name = "inValidElements")] + public string? InvalidElements { get; set; } - [DataMember(Name = "customConfig")] - public IDictionary? CustomConfig { get; set; } - } + [DataMember(Name = "customConfig")] + public IDictionary? CustomConfig { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs index 3740f47fc6..c35eb1e18c 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs @@ -1,11 +1,10 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "richtexteditorplugin", Namespace = "")] +public class RichTextEditorPlugin { - [DataContract(Name = "richtexteditorplugin", Namespace = "")] - public class RichTextEditorPlugin - { - [DataMember(Name = "name")] - public string? Name { get; set; } - } + [DataMember(Name = "name")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs b/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs index ca0e3ff9af..dfd4511aa1 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs @@ -1,21 +1,19 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "rollbackVersion", Namespace = "")] +public class RollbackVersion { - [DataContract(Name = "rollbackVersion", Namespace = "")] - public class RollbackVersion - { - [DataMember(Name = "versionId")] - public int VersionId { get; set; } + [DataMember(Name = "versionId")] + public int VersionId { get; set; } - [DataMember(Name = "versionDate")] - public DateTime? VersionDate { get; set; } + [DataMember(Name = "versionDate")] + public DateTime? VersionDate { get; set; } - [DataMember(Name = "versionAuthorId")] - public int VersionAuthorId { get; set; } + [DataMember(Name = "versionAuthorId")] + public int VersionAuthorId { get; set; } - [DataMember(Name = "versionAuthorName")] - public string? VersionAuthorName { get; set; } - } + [DataMember(Name = "versionAuthorName")] + public string? VersionAuthorName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs b/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs index 53facfe990..8a7fc53605 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "result", Namespace = "")] +public class SearchResult { - [DataContract(Name = "result", Namespace = "")] - public class SearchResult - { - [DataMember(Name = "id")] - public string? Id { get; set; } + [DataMember(Name = "id")] + public string? Id { get; set; } - [DataMember(Name = "score")] - public float Score { get; set; } + [DataMember(Name = "score")] + public float Score { get; set; } - [DataMember(Name = "fieldCount")] - public int FieldCount => Values?.Count ?? 0; + [DataMember(Name = "fieldCount")] + public int FieldCount => Values?.Count ?? 0; - [DataMember(Name = "values")] - public IReadOnlyDictionary>? Values { get; set; } - } + [DataMember(Name = "values")] + public IReadOnlyDictionary>? Values { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs b/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs index e2fc1ff2d7..f86ffc232a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs @@ -1,16 +1,13 @@ -using System.Collections; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "searchResult", Namespace = "")] +public class SearchResultEntity : EntityBasic { - [DataContract(Name = "searchResult", Namespace = "")] - public class SearchResultEntity : EntityBasic - { - /// - /// The score of the search result - /// - [DataMember(Name = "score")] - public float Score { get; set; } - } + /// + /// The score of the search result + /// + [DataMember(Name = "score")] + public float Score { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs b/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs index 2d550a4457..fb7b0fc101 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs @@ -1,22 +1,15 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "results", Namespace = "")] +public class SearchResults { - [DataContract(Name = "results", Namespace = "")] - public class SearchResults - { - public static SearchResults Empty() => new SearchResults - { - Results = Enumerable.Empty(), - TotalRecords = 0 - }; + [DataMember(Name = "totalRecords")] + public long TotalRecords { get; set; } - [DataMember(Name = "totalRecords")] - public long TotalRecords { get; set; } + [DataMember(Name = "results")] + public IEnumerable? Results { get; set; } - [DataMember(Name = "results")] - public IEnumerable? Results { get; set; } - } + public static SearchResults Empty() => new() { Results = Enumerable.Empty(), TotalRecords = 0 }; } diff --git a/src/Umbraco.Core/Models/ContentEditing/Section.cs b/src/Umbraco.Core/Models/ContentEditing/Section.cs index 558d73b49b..68d34822c3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Section.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Section.cs @@ -1,24 +1,23 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a section (application) in the back office +/// +[DataContract(Name = "section", Namespace = "")] +public class Section { + [DataMember(Name = "name")] + public string Name { get; set; } = null!; + + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; + /// - /// Represents a section (application) in the back office + /// In some cases a custom route path can be specified so that when clicking on a section it goes to this + /// path instead of the normal dashboard path /// - [DataContract(Name = "section", Namespace = "")] - public class Section - { - [DataMember(Name = "name")] - public string Name { get; set; } = null!; - - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; - - /// - /// In some cases a custom route path can be specified so that when clicking on a section it goes to this - /// path instead of the normal dashboard path - /// - [DataMember(Name = "routePath")] - public string? RoutePath { get; set; } - } + [DataMember(Name = "routePath")] + public string? RoutePath { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs b/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs index e6db2b933a..9fe429cf3f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs @@ -1,31 +1,24 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "notificationModel", Namespace = "")] +public class SimpleNotificationModel : INotificationModel { - [DataContract(Name = "notificationModel", Namespace = "")] - public class SimpleNotificationModel : INotificationModel - { - public SimpleNotificationModel() - { - Notifications = new List(); - } + public SimpleNotificationModel() => Notifications = new List(); - public SimpleNotificationModel(params BackOfficeNotification[] notifications) - { - Notifications = new List(notifications); - } + public SimpleNotificationModel(params BackOfficeNotification[] notifications) => + Notifications = new List(notifications); - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + /// + /// A default message + /// + [DataMember(Name = "message")] + public string? Message { get; set; } - /// - /// A default message - /// - [DataMember(Name = "message")] - public string? Message { get; set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs index 39e2027b27..48b3d71cac 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs @@ -1,14 +1,13 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "scriptFile", Namespace = "")] +public class SnippetDisplay { - [DataContract(Name = "scriptFile", Namespace = "")] - public class SnippetDisplay - { - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } - [DataMember(Name = "fileName", IsRequired = true)] - public string? FileName { get; set; } - } + [DataMember(Name = "fileName", IsRequired = true)] + public string? FileName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs b/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs index 11d3b814c1..6a8d7c14fe 100644 --- a/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs +++ b/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs @@ -1,14 +1,13 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "stylesheet", Namespace = "")] +public class Stylesheet { - [DataContract(Name = "stylesheet", Namespace = "")] - public class Stylesheet - { - [DataMember(Name="name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "path")] - public string? Path { get; set; } - } + [DataMember(Name = "path")] + public string? Path { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs b/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs index c5f827300a..f7af3d984f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs +++ b/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "stylesheetRule", Namespace = "")] +public class StylesheetRule { - [DataContract(Name = "stylesheetRule", Namespace = "")] - public class StylesheetRule - { - [DataMember(Name = "name")] - public string Name { get; set; } = null!; + [DataMember(Name = "name")] + public string Name { get; set; } = null!; - [DataMember(Name = "selector")] - public string Selector { get; set; } = null!; + [DataMember(Name = "selector")] + public string Selector { get; set; } = null!; - [DataMember(Name = "styles")] - public string Styles { get; set; } = null!; - } + [DataMember(Name = "styles")] + public string Styles { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/ContentEditing/Tab.cs b/src/Umbraco.Core/Models/ContentEditing/Tab.cs index 4bcd824670..ab1e92d340 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Tab.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Tab.cs @@ -1,40 +1,37 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a tab in the UI +/// +[DataContract(Name = "tab", Namespace = "")] +public class Tab { + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "active")] + public bool IsActive { get; set; } + + [DataMember(Name = "label")] + public string? Label { get; set; } + + [DataMember(Name = "alias")] + public string? Alias { get; set; } + /// - /// Represents a tab in the UI + /// The expanded state of the tab /// - [DataContract(Name = "tab", Namespace = "")] - public class Tab - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "open")] + public bool Expanded { get; set; } = true; - [DataMember(Name = "key")] - public Guid Key { get; set; } - - [DataMember(Name = "type")] - public string? Type { get; set; } - - [DataMember(Name = "active")] - public bool IsActive { get; set; } - - [DataMember(Name = "label")] - public string? Label { get; set; } - - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - /// - /// The expanded state of the tab - /// - [DataMember(Name = "open")] - public bool Expanded { get; set; } = true; - - [DataMember(Name = "properties")] - public IEnumerable? Properties { get; set; } - } + [DataMember(Name = "properties")] + public IEnumerable? Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs b/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs index afc64e7faf..c47424cdf0 100644 --- a/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs +++ b/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs @@ -1,35 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class TabbedContentItem : ContentItemBasic, ITabbedContent + where T : ContentPropertyBasic { - public abstract class TabbedContentItem : ContentItemBasic, ITabbedContent where T : ContentPropertyBasic + protected TabbedContentItem() => Tabs = new List>(); + + /// + /// Override the properties property to ensure we don't serialize this + /// and to simply return the properties based on the properties in the tabs collection + /// + /// + /// This property cannot be set + /// + [IgnoreDataMember] + public override IEnumerable Properties { - protected TabbedContentItem() - { - Tabs = new List>(); - } - - /// - /// Defines the tabs containing display properties - /// - [DataMember(Name = "tabs")] - public IEnumerable> Tabs { get; set; } - - /// - /// Override the properties property to ensure we don't serialize this - /// and to simply return the properties based on the properties in the tabs collection - /// - /// - /// This property cannot be set - /// - [IgnoreDataMember] - public override IEnumerable Properties - { - get => Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); - set => throw new NotImplementedException(); - } + get => Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); + set => throw new NotImplementedException(); } + + /// + /// Defines the tabs containing display properties + /// + [DataMember(Name = "tabs")] + public IEnumerable> Tabs { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs index fd67d55936..b6dadcdc2a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs @@ -1,47 +1,43 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "template", Namespace = "")] +public class TemplateDisplay : INotificationModel { - [DataContract(Name = "template", Namespace = "")] - public class TemplateDisplay : INotificationModel - { + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "id")] - public int Id { get; set; } + [Required] + [DataMember(Name = "name")] + public string? Name { get; set; } - [Required] - [DataMember(Name = "name")] - public string? Name { get; set; } + [Required] + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; - [Required] - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; + [DataMember(Name = "key")] + public Guid Key { get; set; } - [DataMember(Name = "key")] - public Guid Key { get; set; } + [DataMember(Name = "content")] + public string? Content { get; set; } - [DataMember(Name = "content")] - public string? Content { get; set; } + [DataMember(Name = "path")] + public string? Path { get; set; } - [DataMember(Name = "path")] - public string? Path { get; set; } + [DataMember(Name = "virtualPath")] + public string? VirtualPath { get; set; } - [DataMember(Name = "virtualPath")] - public string? VirtualPath { get; set; } + [DataMember(Name = "masterTemplateAlias")] + public string? MasterTemplateAlias { get; set; } - [DataMember(Name = "masterTemplateAlias")] - public string? MasterTemplateAlias { get; set; } + [DataMember(Name = "isMasterTemplate")] + public bool IsMasterTemplate { get; set; } - [DataMember(Name = "isMasterTemplate")] - public bool IsMasterTemplate { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List? Notifications { get; private set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List? Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs b/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs index 99533facc8..f1b3dea9b2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs +++ b/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs @@ -1,34 +1,32 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a search result by entity type +/// +[DataContract(Name = "searchResult", Namespace = "")] +public class TreeSearchResult { + [DataMember(Name = "appAlias")] + public string? AppAlias { get; set; } + + [DataMember(Name = "treeAlias")] + public string? TreeAlias { get; set; } + /// - /// Represents a search result by entity type + /// This is optional but if specified should be the name of an angular service to format the search result. /// - [DataContract(Name = "searchResult", Namespace = "")] - public class TreeSearchResult - { - [DataMember(Name = "appAlias")] - public string? AppAlias { get; set; } + [DataMember(Name = "jsSvc")] + public string? JsFormatterService { get; set; } - [DataMember(Name = "treeAlias")] - public string? TreeAlias { get; set; } + /// + /// This is optional but if specified should be the name of a method on the jsSvc angular service to use, if not + /// specified than it will expect the method to be called `format(searchResult, appAlias, treeAlias)` + /// + [DataMember(Name = "jsMethod")] + public string? JsFormatterMethod { get; set; } - /// - /// This is optional but if specified should be the name of an angular service to format the search result. - /// - [DataMember(Name = "jsSvc")] - public string? JsFormatterService { get; set; } - - /// - /// This is optional but if specified should be the name of a method on the jsSvc angular service to use, if not - /// specified than it will expect the method to be called `format(searchResult, appAlias, treeAlias)` - /// - [DataMember(Name = "jsMethod")] - public string? JsFormatterMethod { get; set; } - - [DataMember(Name = "results")] - public IEnumerable? Results { get; set; } - } + [DataMember(Name = "results")] + public IEnumerable? Results { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs b/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs index c77500c531..e089093174 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs @@ -1,98 +1,97 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the type's of Umbraco entities that can be resolved from the EntityController +/// +public enum UmbracoEntityTypes { /// - /// Represents the type's of Umbraco entities that can be resolved from the EntityController + /// Language /// - public enum UmbracoEntityTypes - { - /// - /// Language - /// - Language, + Language, - /// - /// User - /// - User, + /// + /// User + /// + User, - /// - /// Macro - /// - Macro, + /// + /// Macro + /// + Macro, - /// - /// Document - /// - Document, + /// + /// Document + /// + Document, - /// - /// Media - /// - Media, + /// + /// Media + /// + Media, - /// - /// Member Type - /// - MemberType, + /// + /// Member Type + /// + MemberType, - /// - /// Template - /// - Template, + /// + /// Template + /// + Template, - /// - /// Member Group - /// - MemberGroup, + /// + /// Member Group + /// + MemberGroup, - /// - /// "Media Type - /// - MediaType, + /// + /// "Media Type + /// + MediaType, - /// - /// Document Type - /// - DocumentType, + /// + /// Document Type + /// + DocumentType, - /// - /// Stylesheet - /// - Stylesheet, + /// + /// Stylesheet + /// + Stylesheet, - /// - /// Script - /// - Script, + /// + /// Script + /// + Script, - /// - /// Partial View - /// - PartialView, + /// + /// Partial View + /// + PartialView, - /// - /// Member - /// - Member, + /// + /// Member + /// + Member, - /// - /// Data Type - /// - DataType, + /// + /// Data Type + /// + DataType, - /// - /// Property Type - /// - PropertyType, + /// + /// Property Type + /// + PropertyType, - /// - /// Property Group - /// - PropertyGroup, + /// + /// Property Group + /// + PropertyGroup, - /// - /// Dictionary Item - /// - DictionaryItem - } + /// + /// Dictionary Item + /// + DictionaryItem, } diff --git a/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs b/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs index 7a4e6d28d8..cc77bf5dbf 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Used to unpublish content and variants +/// +[DataContract(Name = "unpublish", Namespace = "")] +public class UnpublishContent { - /// - /// Used to unpublish content and variants - /// - [DataContract(Name = "unpublish", Namespace = "")] - public class UnpublishContent - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "cultures")] - public string[]? Cultures { get; set; } - } + [DataMember(Name = "cultures")] + public string[]? Cultures { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs b/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs index 0e8c711e83..1a732ed017 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "urlAndAnchors", Namespace = "")] +public class UrlAndAnchors { - [DataContract(Name = "urlAndAnchors", Namespace = "")] - public class UrlAndAnchors + public UrlAndAnchors(string url, IEnumerable anchorValues) { - public UrlAndAnchors(string url, IEnumerable anchorValues) - { - Url = url; - AnchorValues = anchorValues; - } - - [DataMember(Name = "url")] - public string Url { get; } - - [DataMember(Name = "anchorValues")] - public IEnumerable AnchorValues { get; } + Url = url; + AnchorValues = anchorValues; } + + [DataMember(Name = "url")] + public string Url { get; } + + [DataMember(Name = "anchorValues")] + public IEnumerable AnchorValues { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs b/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs index b2dc4ceb4a..6d20e54bfa 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs @@ -1,68 +1,65 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The user model used for paging and listing users in the UI +/// +[DataContract(Name = "user", Namespace = "")] +[ReadOnly(true)] +public class UserBasic : EntityBasic, INotificationModel { - /// - /// The user model used for paging and listing users in the UI - /// - [DataContract(Name = "user", Namespace = "")] - [ReadOnly(true)] - public class UserBasic : EntityBasic, INotificationModel + public UserBasic() { - public UserBasic() - { - Notifications = new List(); - UserGroups = new List(); - } - - [DataMember(Name = "username")] - public string? Username { get; set; } - - /// - /// The MD5 lowercase hash of the email which can be used by gravatar - /// - [DataMember(Name = "emailHash")] - public string? EmailHash { get; set; } - - [DataMember(Name = "lastLoginDate")] - public DateTime? LastLoginDate { get; set; } - - /// - /// Returns a list of different size avatars - /// - [DataMember(Name = "avatars")] - public string[]? Avatars { get; set; } - - [DataMember(Name = "userState")] - public UserState UserState { get; set; } - - [DataMember(Name = "culture", IsRequired = true)] - public string? Culture { get; set; } - - [DataMember(Name = "email", IsRequired = true)] - public string? Email { get; set; } - - /// - /// The list of group aliases assigned to the user - /// - [DataMember(Name = "userGroups")] - public IEnumerable UserGroups { get; set; } - - /// - /// This is an info flag to denote if this object is the equivalent of the currently logged in user - /// - [DataMember(Name = "isCurrentUser")] - [ReadOnly(true)] - public bool IsCurrentUser { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + Notifications = new List(); + UserGroups = new List(); } + + [DataMember(Name = "username")] + public string? Username { get; set; } + + /// + /// The MD5 lowercase hash of the email which can be used by gravatar + /// + [DataMember(Name = "emailHash")] + public string? EmailHash { get; set; } + + [DataMember(Name = "lastLoginDate")] + public DateTime? LastLoginDate { get; set; } + + /// + /// Returns a list of different size avatars + /// + [DataMember(Name = "avatars")] + public string[]? Avatars { get; set; } + + [DataMember(Name = "userState")] + public UserState UserState { get; set; } + + [DataMember(Name = "culture", IsRequired = true)] + public string? Culture { get; set; } + + [DataMember(Name = "email", IsRequired = true)] + public string? Email { get; set; } + + /// + /// The list of group aliases assigned to the user + /// + [DataMember(Name = "userGroups")] + public IEnumerable UserGroups { get; set; } + + /// + /// This is an info flag to denote if this object is the equivalent of the currently logged in user + /// + [DataMember(Name = "isCurrentUser")] + [ReadOnly(true)] + public bool IsCurrentUser { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs index 01c2bcb70c..38ec7281a4 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs @@ -1,62 +1,68 @@ -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents information for the current user +/// +[DataContract(Name = "user", Namespace = "")] +public class UserDetail : UserProfile { + [DataMember(Name = "email", IsRequired = true)] + [Required] + public string? Email { get; set; } + + [DataMember(Name = "locale", IsRequired = true)] + [Required] + public string? Culture { get; set; } + /// - /// Represents information for the current user + /// The MD5 lowercase hash of the email which can be used by gravatar /// - [DataContract(Name = "user", Namespace = "")] - public class UserDetail : UserProfile - { - [DataMember(Name = "email", IsRequired = true)] - [Required] - public string? Email { get; set; } + [DataMember(Name = "emailHash")] + public string? EmailHash { get; set; } - [DataMember(Name = "locale", IsRequired = true)] - [Required] - public string? Culture { get; set; } + [ReadOnly(true)] + [DataMember(Name = "userGroups")] + public string?[]? UserGroups { get; set; } - /// - /// The MD5 lowercase hash of the email which can be used by gravatar - /// - [DataMember(Name = "emailHash")] - public string? EmailHash { get; set; } + /// + /// Gets/sets the number of seconds for the user's auth ticket to expire + /// + [DataMember(Name = "remainingAuthSeconds")] + public double SecondsUntilTimeout { get; set; } - [ReadOnly(true)] - [DataMember(Name = "userGroups")] - public string?[]? UserGroups { get; set; } + /// + /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups + /// they're assigned to + /// + [DataMember(Name = "startContentIds")] + public int[]? StartContentIds { get; set; } - /// - /// Gets/sets the number of seconds for the user's auth ticket to expire - /// - [DataMember(Name = "remainingAuthSeconds")] - public double SecondsUntilTimeout { get; set; } + /// + /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups + /// they're assigned to + /// + [DataMember(Name = "startMediaIds")] + public int[]? StartMediaIds { get; set; } - /// - /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to - /// - [DataMember(Name = "startContentIds")] - public int[]? StartContentIds { get; set; } + /// + /// Returns a list of different size avatars + /// + [DataMember(Name = "avatars")] + public string[]? Avatars { get; set; } - /// - /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to - /// - [DataMember(Name = "startMediaIds")] - public int[]? StartMediaIds { get; set; } + /// + /// A list of sections the user is allowed to view. + /// + [DataMember(Name = "allowedSections")] + public IEnumerable? AllowedSections { get; set; } - /// - /// Returns a list of different size avatars - /// - [DataMember(Name = "avatars")] - public string[]? Avatars { get; set; } - - /// - /// A list of sections the user is allowed to view. - /// - [DataMember(Name = "allowedSections")] - public IEnumerable? AllowedSections { get; set; } - } + /// + /// A list of language culcure codes the user is allowed to view. + /// + [DataMember(Name = "allowedLanguageIds")] + public IEnumerable? AllowedLanguageIds { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs index 20e517cefc..4b300c17a9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs @@ -1,81 +1,78 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a user that is being edited +/// +[DataContract(Name = "user", Namespace = "")] +[ReadOnly(true)] +public class UserDisplay : UserBasic { - /// - /// Represents a user that is being edited - /// - [DataContract(Name = "user", Namespace = "")] - [ReadOnly(true)] - public class UserDisplay : UserBasic + public UserDisplay() { - public UserDisplay() - { - AvailableCultures = new Dictionary(); - StartContentIds = new List(); - StartMediaIds = new List(); - Navigation = new List(); - } - - [DataMember(Name = "navigation")] - [ReadOnly(true)] - public IEnumerable Navigation { get; set; } - - /// - /// Gets the available cultures (i.e. to populate a drop down) - /// The key is the culture stored in the database, the value is the Name - /// - [DataMember(Name = "availableCultures")] - public IDictionary AvailableCultures { get; set; } - - [DataMember(Name = "startContentIds")] - public IEnumerable StartContentIds { get; set; } - - [DataMember(Name = "startMediaIds")] - public IEnumerable StartMediaIds { get; set; } - - /// - /// If the password is reset on save, this value will be populated - /// - [DataMember(Name = "resetPasswordValue")] - [ReadOnly(true)] - public string? ResetPasswordValue { get; set; } - - /// - /// A readonly value showing the user's current calculated start content ids - /// - [DataMember(Name = "calculatedStartContentIds")] - [ReadOnly(true)] - public IEnumerable? CalculatedStartContentIds { get; set; } - - /// - /// A readonly value showing the user's current calculated start media ids - /// - [DataMember(Name = "calculatedStartMediaIds")] - [ReadOnly(true)] - public IEnumerable? CalculatedStartMediaIds { get; set; } - - [DataMember(Name = "failedPasswordAttempts")] - [ReadOnly(true)] - public int FailedPasswordAttempts { get; set; } - - [DataMember(Name = "lastLockoutDate")] - [ReadOnly(true)] - public DateTime? LastLockoutDate { get; set; } - - [DataMember(Name = "lastPasswordChangeDate")] - [ReadOnly(true)] - public DateTime? LastPasswordChangeDate { get; set; } - - [DataMember(Name = "createDate")] - [ReadOnly(true)] - public DateTime CreateDate { get; set; } - - [DataMember(Name = "updateDate")] - [ReadOnly(true)] - public DateTime UpdateDate { get; set; } + AvailableCultures = new Dictionary(); + StartContentIds = new List(); + StartMediaIds = new List(); + Navigation = new List(); } + + [DataMember(Name = "navigation")] + [ReadOnly(true)] + public IEnumerable Navigation { get; set; } + + /// + /// Gets the available cultures (i.e. to populate a drop down) + /// The key is the culture stored in the database, the value is the Name + /// + [DataMember(Name = "availableCultures")] + public IDictionary AvailableCultures { get; set; } + + [DataMember(Name = "startContentIds")] + public IEnumerable StartContentIds { get; set; } + + [DataMember(Name = "startMediaIds")] + public IEnumerable StartMediaIds { get; set; } + + /// + /// If the password is reset on save, this value will be populated + /// + [DataMember(Name = "resetPasswordValue")] + [ReadOnly(true)] + public string? ResetPasswordValue { get; set; } + + /// + /// A readonly value showing the user's current calculated start content ids + /// + [DataMember(Name = "calculatedStartContentIds")] + [ReadOnly(true)] + public IEnumerable? CalculatedStartContentIds { get; set; } + + /// + /// A readonly value showing the user's current calculated start media ids + /// + [DataMember(Name = "calculatedStartMediaIds")] + [ReadOnly(true)] + public IEnumerable? CalculatedStartMediaIds { get; set; } + + [DataMember(Name = "failedPasswordAttempts")] + [ReadOnly(true)] + public int FailedPasswordAttempts { get; set; } + + [DataMember(Name = "lastLockoutDate")] + [ReadOnly(true)] + public DateTime? LastLockoutDate { get; set; } + + [DataMember(Name = "lastPasswordChangeDate")] + [ReadOnly(true)] + public DateTime? LastPasswordChangeDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs index ffcfde8368..5780214476 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs @@ -1,43 +1,47 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "userGroup", Namespace = "")] +public class UserGroupBasic : EntityBasic, INotificationModel { - [DataContract(Name = "userGroup", Namespace = "")] - public class UserGroupBasic : EntityBasic, INotificationModel + public UserGroupBasic() { - public UserGroupBasic() - { - Notifications = new List(); - Sections = Enumerable.Empty
(); - } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - - [DataMember(Name = "sections")] - public IEnumerable
Sections { get; set; } - - [DataMember(Name = "contentStartNode")] - public EntityBasic? ContentStartNode { get; set; } - - [DataMember(Name = "mediaStartNode")] - public EntityBasic? MediaStartNode { get; set; } - - /// - /// The number of users assigned to this group - /// - [DataMember(Name = "userCount")] - public int UserCount { get; set; } - - /// - /// Is the user group a system group e.g. "Administrators", "Sensitive data" or "Translators" - /// - [DataMember(Name = "isSystemUserGroup")] - public bool IsSystemUserGroup { get; set; } + Notifications = new List(); + Languages = Enumerable.Empty(); + Sections = Enumerable.Empty
(); } + + [DataMember(Name = "languages")] + public IEnumerable Languages { get; set; } + + [DataMember(Name = "sections")] + public IEnumerable
Sections { get; set; } + + [DataMember(Name = "contentStartNode")] + public EntityBasic? ContentStartNode { get; set; } + + [DataMember(Name = "mediaStartNode")] + public EntityBasic? MediaStartNode { get; set; } + + [DataMember(Name = "hasAccessToAllLanguages")] + public bool HasAccessToAllLanguages { get; set; } + + /// + /// The number of users assigned to this group + /// + [DataMember(Name = "userCount")] + public int UserCount { get; set; } + + /// + /// Is the user group a system group e.g. "Administrators", "Sensitive data" or "Translators" + /// + [DataMember(Name = "isSystemUserGroup")] + public bool IsSystemUserGroup { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs index 697a0a2100..30cca62c4a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs @@ -1,31 +1,28 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "userGroup", Namespace = "")] +public class UserGroupDisplay : UserGroupBasic { - [DataContract(Name = "userGroup", Namespace = "")] - public class UserGroupDisplay : UserGroupBasic + public UserGroupDisplay() { - public UserGroupDisplay() - { - Users = Enumerable.Empty(); - AssignedPermissions = Enumerable.Empty(); - } - - [DataMember(Name = "users")] - public IEnumerable Users { get; set; } - - /// - /// The default permissions for the user group organized by permission group name - /// - [DataMember(Name = "defaultPermissions")] - public IDictionary>? DefaultPermissions { get; set; } - - /// - /// The assigned permissions for the user group organized by permission group name - /// - [DataMember(Name = "assignedPermissions")] - public IEnumerable AssignedPermissions { get; set; } + Users = Enumerable.Empty(); + AssignedPermissions = Enumerable.Empty(); } + + [DataMember(Name = "users")] + public IEnumerable Users { get; set; } + + /// + /// The default permissions for the user group organized by permission group name + /// + [DataMember(Name = "defaultPermissions")] + public IDictionary>? DefaultPermissions { get; set; } + + /// + /// The assigned permissions for the user group organized by permission group name + /// + [DataMember(Name = "assignedPermissions")] + public IEnumerable AssignedPermissions { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs index bc9ac96331..1e648f949f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs @@ -1,33 +1,25 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Used to assign user group permissions to a content node +/// +[DataContract(Name = "contentPermission", Namespace = "")] +public class UserGroupPermissionsSave { + public UserGroupPermissionsSave() => AssignedPermissions = new Dictionary>(); + + // TODO: we should have an option to clear the permissions assigned to this node and instead just have them inherit - yes once we actually have inheritance! + [DataMember(Name = "contentId", IsRequired = true)] + [Required] + public int ContentId { get; set; } + /// - /// Used to assign user group permissions to a content node + /// A dictionary of permissions to assign, the key is the user group id /// - [DataContract(Name = "contentPermission", Namespace = "")] - public class UserGroupPermissionsSave - { - public UserGroupPermissionsSave() - { - AssignedPermissions = new Dictionary>(); - } - - // TODO: we should have an option to clear the permissions assigned to this node and instead just have them inherit - yes once we actually have inheritance! - - [DataMember(Name = "contentId", IsRequired = true)] - [Required] - public int ContentId { get; set; } - - /// - /// A dictionary of permissions to assign, the key is the user group id - /// - [DataMember(Name = "permissions")] - public IDictionary> AssignedPermissions { get; set; } - } + [DataMember(Name = "permissions")] + public IDictionary> AssignedPermissions { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs index 1bf7923817..a646c10337 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs @@ -1,77 +1,87 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "userGroup", Namespace = "")] +public class UserGroupSave : EntityBasic, IValidatableObject { - [DataContract(Name = "userGroup", Namespace = "")] - public class UserGroupSave : EntityBasic, IValidatableObject + /// + /// The action to perform when saving this user group + /// + /// + /// If either of the Publish actions are specified an exception will be thrown. + /// + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } + + [DataMember(Name = "alias", IsRequired = true)] + [Required] + public override string Alias { get; set; } = string.Empty; + + [DataMember(Name = "sections")] + public IEnumerable? Sections { get; set; } + + [DataMember(Name = "users")] + public IEnumerable? Users { get; set; } + + [DataMember(Name = "startContentId")] + public int? StartContentId { get; set; } + + [DataMember(Name = "startMediaId")] + public int? StartMediaId { get; set; } + + [DataMember(Name = "hasAccessToAllLanguages")] + public bool HasAccessToAllLanguages { get; set; } + + /// + /// The list of letters (permission codes) to assign as the default for the user group + /// + [DataMember(Name = "defaultPermissions")] + public IEnumerable? DefaultPermissions { get; set; } + + /// + /// The assigned permissions for content + /// + /// + /// The key is the content id and the list is the list of letters (permission codes) to assign + /// + [DataMember(Name = "assignedPermissions")] + public IDictionary>? AssignedPermissions { get; set; } + + /// + /// The ids of allowed languages + /// + [DataMember(Name = "allowedLanguages")] + public IEnumerable? AllowedLanguages { get; set; } + + /// + /// The real persisted user group + /// + [IgnoreDataMember] + public IUserGroup? PersistedUserGroup { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) { - /// - /// The action to perform when saving this user group - /// - /// - /// If either of the Publish actions are specified an exception will be thrown. - /// - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } - - [DataMember(Name = "alias", IsRequired = true)] - [Required] - public override string Alias { get; set; } = string.Empty; - - [DataMember(Name = "sections")] - public IEnumerable? Sections { get; set; } - - [DataMember(Name = "users")] - public IEnumerable? Users { get; set; } - - [DataMember(Name = "startContentId")] - public int? StartContentId { get; set; } - - [DataMember(Name = "startMediaId")] - public int? StartMediaId { get; set; } - - /// - /// The list of letters (permission codes) to assign as the default for the user group - /// - [DataMember(Name = "defaultPermissions")] - public IEnumerable? DefaultPermissions { get; set; } - - /// - /// The assigned permissions for content - /// - /// - /// The key is the content id and the list is the list of letters (permission codes) to assign - /// - [DataMember(Name = "assignedPermissions")] - public IDictionary>? AssignedPermissions { get; set; } - - /// - /// The real persisted user group - /// - [IgnoreDataMember] - public IUserGroup? PersistedUserGroup { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) + if (DefaultPermissions?.Any(x => x.IsNullOrWhiteSpace()) ?? false) { - if (DefaultPermissions?.Any(x => x.IsNullOrWhiteSpace()) ?? false) - { - yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); - } + yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); + } - if (AssignedPermissions is not null) + if (AssignedPermissions is not null) + { + foreach (KeyValuePair> assignedPermission in AssignedPermissions) { - foreach (var assignedPermission in AssignedPermissions) + foreach (var permission in assignedPermission.Value) { - foreach (var permission in assignedPermission.Value) + if (permission.IsNullOrWhiteSpace()) { - if (permission.IsNullOrWhiteSpace()) - yield return new ValidationResult("A permission value cannot be null or empty", new[] { "AssignedPermissions" }); + yield return new ValidationResult( + "A permission value cannot be null or empty", + new[] { "AssignedPermissions" }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs b/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs index 7b3014369a..02a10b45af 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs @@ -1,44 +1,48 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the data used to invite a user +/// +[DataContract(Name = "user", Namespace = "")] +public class UserInvite : EntityBasic, IValidatableObject { - /// - /// Represents the data used to invite a user - /// - [DataContract(Name = "user", Namespace = "")] - public class UserInvite : EntityBasic, IValidatableObject + [DataMember(Name = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } = null!; + + [DataMember(Name = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } = null!; + + [DataMember(Name = "username")] + public string? Username { get; set; } + + [DataMember(Name = "message")] + public string? Message { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) { - [DataMember(Name = "userGroups")] - [Required] - public IEnumerable UserGroups { get; set; } = null!; - - [DataMember(Name = "email", IsRequired = true)] - [Required] - [EmailAddress] - public string Email { get; set; } = null!; - - [DataMember(Name = "username")] - public string? Username { get; set; } - - [DataMember(Name = "message")] - public string? Message { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) + if (UserGroups.Any() == false) { - if (UserGroups.Any() == false) - yield return new ValidationResult("A user must be assigned to at least one group", new[] { nameof(UserGroups) }); + yield return new ValidationResult( + "A user must be assigned to at least one group", + new[] { nameof(UserGroups) }); + } - var securitySettings = validationContext.GetRequiredService>(); + IOptionsSnapshot securitySettings = + validationContext.GetRequiredService>(); - if (securitySettings.Value.UsernameIsEmail == false && Username.IsNullOrWhiteSpace()) - yield return new ValidationResult("A username cannot be empty", new[] { nameof(Username) }); + if (securitySettings.Value.UsernameIsEmail == false && Username.IsNullOrWhiteSpace()) + { + yield return new ValidationResult("A username cannot be empty", new[] { nameof(Username) }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs b/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs index 9ade7735e7..441972e8bc 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs @@ -1,27 +1,21 @@ -using System; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A bare minimum structure that represents a user, usually attached to other objects +/// +[DataContract(Name = "user", Namespace = "")] +public class UserProfile : IComparable { - /// - /// A bare minimum structure that represents a user, usually attached to other objects - /// - [DataContract(Name = "user", Namespace = "")] - public class UserProfile : IComparable - { - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int UserId { get; set; } + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int UserId { get; set; } - [DataMember(Name = "name", IsRequired = true)] - [Required] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + [Required] + public string? Name { get; set; } - - int IComparable.CompareTo(object? obj) - { - return String.Compare(Name, ((UserProfile?)obj)?.Name, StringComparison.Ordinal); - } - } + int IComparable.CompareTo(object? obj) => string.Compare(Name, ((UserProfile?)obj)?.Name, StringComparison.Ordinal); } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserSave.cs index 6e03248a31..e0a3d41d4f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserSave.cs @@ -1,55 +1,56 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the data used to persist a user +/// +/// +/// This will be different from the model used to display a user and we don't want to "Overpost" data back to the +/// server, +/// and there will most likely be different bits of data required for updating passwords which will be different from +/// the +/// data used to display vs save +/// +[DataContract(Name = "user", Namespace = "")] +public class UserSave : EntityBasic, IValidatableObject { - /// - /// Represents the data used to persist a user - /// - /// - /// This will be different from the model used to display a user and we don't want to "Overpost" data back to the server, - /// and there will most likely be different bits of data required for updating passwords which will be different from the - /// data used to display vs save - /// - [DataContract(Name = "user", Namespace = "")] - public class UserSave : EntityBasic, IValidatableObject + [DataMember(Name = "changePassword", IsRequired = true)] + public ChangingPasswordModel? ChangePassword { get; set; } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public new int Id { get; set; } + + [DataMember(Name = "username", IsRequired = true)] + [Required] + public string Username { get; set; } = null!; + + [DataMember(Name = "culture", IsRequired = true)] + [Required] + public string Culture { get; set; } = null!; + + [DataMember(Name = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } = null!; + + [DataMember(Name = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } = null!; + + [DataMember(Name = "startContentIds")] + public int[]? StartContentIds { get; set; } + + [DataMember(Name = "startMediaIds")] + public int[]? StartMediaIds { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) { - [DataMember(Name = "changePassword", IsRequired = true)] - public ChangingPasswordModel? ChangePassword { get; set; } - - [DataMember(Name = "id", IsRequired = true)] - [Required] - public new int Id { get; set; } - - [DataMember(Name = "username", IsRequired = true)] - [Required] - public string Username { get; set; } = null!; - - [DataMember(Name = "culture", IsRequired = true)] - [Required] - public string Culture { get; set; } = null!; - - [DataMember(Name = "email", IsRequired = true)] - [Required] - [EmailAddress] - public string Email { get; set; } = null!; - - [DataMember(Name = "userGroups")] - [Required] - public IEnumerable UserGroups { get; set; } = null!; - - [DataMember(Name = "startContentIds")] - public int[]? StartContentIds { get; set; } - - [DataMember(Name = "startMediaIds")] - public int[]? StartMediaIds { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) + if (UserGroups.Any() == false) { - if (UserGroups.Any() == false) - yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); + yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); } } } diff --git a/src/Umbraco.Core/Models/ContentModel.cs b/src/Umbraco.Core/Models/ContentModel.cs index cead39f019..5d81ea367e 100644 --- a/src/Umbraco.Core/Models/ContentModel.cs +++ b/src/Umbraco.Core/Models/ContentModel.cs @@ -1,21 +1,20 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the model for the current Umbraco view. +/// +public class ContentModel : IContentModel { /// - /// Represents the model for the current Umbraco view. + /// Initializes a new instance of the class with a content. /// - public class ContentModel : IContentModel - { - /// - /// Initializes a new instance of the class with a content. - /// - public ContentModel(IPublishedContent? content) => Content = content ?? throw new ArgumentNullException(nameof(content)); + public ContentModel(IPublishedContent? content) => + Content = content ?? throw new ArgumentNullException(nameof(content)); - /// - /// Gets the content. - /// - public IPublishedContent Content { get; } - } + /// + /// Gets the content. + /// + public IPublishedContent Content { get; } } diff --git a/src/Umbraco.Core/Models/ContentModelOfTContent.cs b/src/Umbraco.Core/Models/ContentModelOfTContent.cs index ab882342b5..32889331e0 100644 --- a/src/Umbraco.Core/Models/ContentModelOfTContent.cs +++ b/src/Umbraco.Core/Models/ContentModelOfTContent.cs @@ -1,19 +1,18 @@ using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models -{ - public class ContentModel : ContentModel - where TContent : IPublishedContent - { - /// - /// Initializes a new instance of the class with a content. - /// - public ContentModel(TContent content) - : base(content) => Content = content; +namespace Umbraco.Cms.Core.Models; - /// - /// Gets the content. - /// - public new TContent Content { get; } - } +public class ContentModel : ContentModel + where TContent : IPublishedContent +{ + /// + /// Initializes a new instance of the class with a content. + /// + public ContentModel(TContent content) + : base(content) => Content = content; + + /// + /// Gets the content. + /// + public new TContent Content { get; } } diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 4ab39f1669..d76194aa64 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -1,353 +1,438 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods used to manipulate content variations by the document repository +/// +public static class ContentRepositoryExtensions { - /// - /// Extension methods used to manipulate content variations by the document repository - /// - public static class ContentRepositoryExtensions + public static void SetCultureInfo(this IContentBase content, string? culture, string? name, DateTime date) { - public static void SetCultureInfo(this IContentBase content, string? culture, string? name, DateTime date) + if (name == null) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - content.CultureInfos?.AddOrUpdate(culture, name, date); + throw new ArgumentNullException(nameof(name)); } - /// - /// Updates a culture date, if the culture exists. - /// - public static void TouchCulture(this IContentBase content, string? culture) + if (string.IsNullOrWhiteSpace(name)) { - if (culture.IsNullOrWhiteSpace() || content.CultureInfos is null) - { - return; - } - - if (!content.CultureInfos.TryGetValue(culture!, out var infos)) - { - return; - } - - content.CultureInfos?.AddOrUpdate(culture!, infos.Name, DateTime.Now); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Used to synchronize all culture dates to the same date if they've been modified - /// - /// - /// - /// - /// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible that - /// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact same time. - /// - public static void AdjustDates(this IContent content, DateTime date, bool publishing) + if (culture == null) { - if (content.EditedCultures is not null) + throw new ArgumentNullException(nameof(culture)); + } + + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } + + content.CultureInfos?.AddOrUpdate(culture, name, date); + } + + /// + /// Updates a culture date, if the culture exists. + /// + public static void TouchCulture(this IContentBase content, string? culture) + { + if (culture.IsNullOrWhiteSpace() || content.CultureInfos is null) + { + return; + } + + if (!content.CultureInfos.TryGetValue(culture!, out ContentCultureInfos infos)) + { + return; + } + + content.CultureInfos?.AddOrUpdate(culture!, infos.Name, DateTime.Now); + } + + /// + /// Used to synchronize all culture dates to the same date if they've been modified + /// + /// + /// + /// + /// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible + /// that + /// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact + /// same time. + /// + public static void AdjustDates(this IContent content, DateTime date, bool publishing) + { + if (content.EditedCultures is not null) + { + foreach (var culture in content.EditedCultures.ToList()) { - foreach(var culture in content.EditedCultures.ToList()) - { - if (content.CultureInfos is null) - { - continue; - } - - if (!content.CultureInfos.TryGetValue(culture, out var editedInfos)) - { - continue; - } - - // if it's not dirty, it means it hasn't changed so there's nothing to adjust - if (!editedInfos.IsDirty()) - { - continue; - } - - content.CultureInfos?.AddOrUpdate(culture, editedInfos?.Name, date); - } - } - - - if (!publishing) - { - return; - } - - foreach (var culture in content.PublishedCultures.ToList()) - { - if (content.PublishCultureInfos is null) + if (content.CultureInfos is null) { continue; } - if (!content.PublishCultureInfos.TryGetValue(culture, out ContentCultureInfos publishInfos)) + + if (!content.CultureInfos.TryGetValue(culture, out ContentCultureInfos editedInfos)) { continue; } // if it's not dirty, it means it hasn't changed so there's nothing to adjust - if (!publishInfos.IsDirty()) + if (!editedInfos.IsDirty()) { continue; } - content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date); + content.CultureInfos?.AddOrUpdate(culture, editedInfos?.Name, date); + } + } - if (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos infos) ?? false) + if (!publishing) + { + return; + } + + foreach (var culture in content.PublishedCultures.ToList()) + { + if (content.PublishCultureInfos is null) + { + continue; + } + + if (!content.PublishCultureInfos.TryGetValue(culture, out ContentCultureInfos publishInfos)) + { + continue; + } + + // if it's not dirty, it means it hasn't changed so there's nothing to adjust + if (!publishInfos.IsDirty()) + { + continue; + } + + content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date); + + if (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos infos) ?? false) + { + SetCultureInfo(content, culture, infos.Name, date); + } + } + } + + /// + /// Gets the cultures that have been flagged for unpublishing. + /// + /// Gets cultures for which content.UnpublishCulture() has been invoked. + public static IReadOnlyList? GetCulturesUnpublishing(this IContent content) + { + if (!content.Published || !content.ContentType.VariesByCulture() || + !content.IsPropertyDirty("PublishCultureInfos")) + { + return Array.Empty(); + } + + IEnumerable? culturesUnpublishing = content.CultureInfos?.Values + .Where(x => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + x.Culture)) + .Select(x => x.Culture); + + return culturesUnpublishing?.ToList(); + } + + /// + /// Copies values from another document. + /// + public static void CopyFrom(this IContent content, IContent other, string? culture = "*") + { + if (other.ContentTypeId != content.ContentTypeId) + { + throw new InvalidOperationException("Cannot copy values from a different content type."); + } + + culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) + { + throw new NotSupportedException( + $"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + } + + // copying from the same Id and VersionPk + var copyingFromSelf = content.Id == other.Id && content.VersionId == other.VersionId; + var published = copyingFromSelf; + + // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails + + // clear all existing properties for the specified culture + foreach (IProperty property in content.Properties) + { + // each property type may or may not support the variation + if (!property.PropertyType?.SupportsVariation(culture, "*", true) ?? false) + { + continue; + } + + foreach (IPropertyValue pvalue in property.Values) + { + if ((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && + (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) { - SetCultureInfo(content, culture, infos.Name, date); + property.SetValue(null, pvalue.Culture, pvalue.Segment); } } } - /// - /// Gets the cultures that have been flagged for unpublishing. - /// - /// Gets cultures for which content.UnpublishCulture() has been invoked. - public static IReadOnlyList? GetCulturesUnpublishing(this IContent content) + // copy properties from 'other' + IPropertyCollection otherProperties = other.Properties; + foreach (IProperty otherProperty in otherProperties) { - if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos")) - return Array.Empty(); - - var culturesUnpublishing = content.CultureInfos?.Values - .Where(x => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + x.Culture)) - .Select(x => x.Culture); - - return culturesUnpublishing?.ToList(); - } - - /// - /// Copies values from another document. - /// - public static void CopyFrom(this IContent content, IContent other, string? culture = "*") - { - if (other.ContentTypeId != content.ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); - - // the variation should be supported by the content type properties - // if the content type is invariant, only '*' and 'null' is ok - // if the content type varies, everything is ok because some properties may be invariant - if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); - - // copying from the same Id and VersionPk - var copyingFromSelf = content.Id == other.Id && content.VersionId == other.VersionId; - var published = copyingFromSelf; - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties for the specified culture - foreach (var property in content.Properties) + if (!otherProperty?.PropertyType?.SupportsVariation(culture, "*", true) ?? true) { - // each property type may or may not support the variation - if (!property.PropertyType?.SupportsVariation(culture, "*", wildcards: true) ?? false) - continue; + continue; + } - foreach (var pvalue in property.Values) - if ((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) ?? false) && + var alias = otherProperty?.PropertyType.Alias; + if (otherProperty is not null && alias is not null) + { + foreach (IPropertyValue pvalue in otherProperty.Values) + { + if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, true) && (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) { - property.SetValue(null, pvalue.Culture, pvalue.Segment); - } - } - - // copy properties from 'other' - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - if (!otherProperty?.PropertyType?.SupportsVariation(culture, "*", wildcards: true) ?? true) - continue; - - var alias = otherProperty?.PropertyType.Alias; - if (otherProperty is not null && alias is not null) - { - foreach (var pvalue in otherProperty.Values) - { - if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && - (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) - { - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - content.SetValue(alias, value, pvalue.Culture, pvalue.Segment); - } + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + content.SetValue(alias, value, pvalue.Culture, pvalue.Segment); } } } + } - // copy names, too + // copy names, too + if (culture == "*") + { + content.CultureInfos?.Clear(); + content.CultureInfos = null; + } - if (culture == "*") + if (culture == null || culture == "*") + { + content.Name = other.Name; + } + + // ReSharper disable once UseDeconstruction + if (other.CultureInfos is not null) + { + foreach (ContentCultureInfos cultureInfo in other.CultureInfos) { - content.CultureInfos?.Clear(); - content.CultureInfos = null; - } - - if (culture == null || culture == "*") - content.Name = other.Name; - - // ReSharper disable once UseDeconstruction - if (other.CultureInfos is not null) - { - foreach (var cultureInfo in other.CultureInfos) + if (culture == "*" || culture == cultureInfo.Culture) { - if (culture == "*" || culture == cultureInfo.Culture) - content.SetCultureName(cultureInfo.Name, cultureInfo.Culture); + content.SetCultureName(cultureInfo.Name, cultureInfo.Culture); } } } + } - public static void SetPublishInfo(this IContent content, string? culture, string? name, DateTime date) + public static void SetPublishInfo(this IContent content, string? culture, string? name, DateTime date) + { + if (name == null) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - content.PublishCultureInfos?.AddOrUpdate(culture, name, date); + throw new ArgumentNullException(nameof(name)); } - // sets the edited cultures on the content - public static void SetCultureEdited(this IContent content, IEnumerable? cultures) + if (string.IsNullOrWhiteSpace(name)) { - if (cultures == null) - content.EditedCultures = null; - else - { - var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace())!, StringComparer.OrdinalIgnoreCase); - content.EditedCultures = editedCultures.Count > 0 ? editedCultures : null; - } + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Sets the publishing values for names and properties. - /// - /// - /// - /// A value indicating whether it was possible to publish the names and values for the specified - /// culture(s). The method may fail if required names are not set, but it does NOT validate property data - public static bool PublishCulture(this IContent content, CultureImpact? impact) + if (culture == null) { - if (impact == null) throw new ArgumentNullException(nameof(impact)); + throw new ArgumentNullException(nameof(culture)); + } - // the variation should be supported by the content type properties - // if the content type is invariant, only '*' and 'null' is ok - // if the content type varies, everything is ok because some properties may be invariant - if (!content.ContentType.SupportsPropertyVariation(impact.Culture, "*", true)) - throw new NotSupportedException($"Culture \"{impact.Culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } - // set names - if (impact.ImpactsAllCultures) + content.PublishCultureInfos?.AddOrUpdate(culture, name, date); + } + + // sets the edited cultures on the content + public static void SetCultureEdited(this IContent content, IEnumerable? cultures) + { + if (cultures == null) + { + content.EditedCultures = null; + } + else + { + var editedCultures = new HashSet( + cultures.Where(x => !x.IsNullOrWhiteSpace())!, + StringComparer.OrdinalIgnoreCase); + content.EditedCultures = editedCultures.Count > 0 ? editedCultures : null; + } + } + + /// + /// Sets the publishing values for names and properties. + /// + /// + /// + /// + /// A value indicating whether it was possible to publish the names and values for the specified + /// culture(s). The method may fail if required names are not set, but it does NOT validate property data + /// + public static bool PublishCulture(this IContent content, CultureImpact? impact) + { + if (impact == null) + { + throw new ArgumentNullException(nameof(impact)); + } + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!content.ContentType.SupportsPropertyVariation(impact.Culture, "*", true)) + { + throw new NotSupportedException($"Culture \"{impact.Culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + } + + // set names + if (impact.ImpactsAllCultures) + { + // does NOT contain the invariant culture + foreach (var culture in content.AvailableCultures) { - foreach (var c in content.AvailableCultures) // does NOT contain the invariant culture - { - var name = content.GetCultureName(c); - if (string.IsNullOrWhiteSpace(name)) - return false; - content.SetPublishInfo(c, name, DateTime.Now); - } - } - else if (impact.ImpactsOnlyInvariantCulture) - { - if (string.IsNullOrWhiteSpace(content.Name)) - return false; - // PublishName set by repository - nothing to do here - } - else if (impact.ImpactsExplicitCulture) - { - var name = content.GetCultureName(impact.Culture); + var name = content.GetCultureName(culture); if (string.IsNullOrWhiteSpace(name)) + { return false; - content.SetPublishInfo(impact.Culture, name, DateTime.Now); + } + + content.SetPublishInfo(culture, name, DateTime.Now); + } + } + else if (impact.ImpactsOnlyInvariantCulture) + { + if (string.IsNullOrWhiteSpace(content.Name)) + { + return false; + } + // PublishName set by repository - nothing to do here + } + else if (impact.ImpactsExplicitCulture) + { + var name = content.GetCultureName(impact.Culture); + if (string.IsNullOrWhiteSpace(name)) + { + return false; } - // set values - // property.PublishValues only publishes what is valid, variation-wise, - // but accepts any culture arg: null, all, specific - foreach (var property in content.Properties) - { - // for the specified culture (null or all or specific) - property.PublishValues(impact.Culture); + content.SetPublishInfo(impact.Culture, name, DateTime.Now); + } - // maybe the specified culture did not impact the invariant culture, so PublishValues - // above would skip it, yet it *also* impacts invariant properties - if (impact.ImpactsAlsoInvariantProperties) - property.PublishValues(null); + // set values + // property.PublishValues only publishes what is valid, variation-wise, + // but accepts any culture arg: null, all, specific + foreach (IProperty property in content.Properties) + { + // for the specified culture (null or all or specific) + property.PublishValues(impact.Culture); + + // maybe the specified culture did not impact the invariant culture, so PublishValues + // above would skip it, yet it *also* impacts invariant properties + if (impact.ImpactsAlsoInvariantProperties && (property.PropertyType.VariesByCulture() is false || impact.ImpactsOnlyDefaultCulture)) + { + property.PublishValues(null); + } + } + + content.PublishedState = PublishedState.Publishing; + return true; + } + + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool UnpublishCulture(this IContent content, string? culture = "*") + { + culture = culture?.NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) + { + throw new NotSupportedException( + $"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + } + + var keepProcessing = true; + + if (culture == "*") + { + // all cultures + content.ClearPublishInfos(); + } + else + { + // one single culture + keepProcessing = content.ClearPublishInfo(culture); + } + + if (keepProcessing) + { + // property.PublishValues only publishes what is valid, variation-wise + foreach (IProperty property in content.Properties) + { + property.UnpublishValues(culture); } content.PublishedState = PublishedState.Publishing; - return true; } - /// - /// Returns false if the culture is already unpublished - /// - /// - /// - /// - public static bool UnpublishCulture(this IContent content, string? culture = "*") + return keepProcessing; + } + + public static void ClearPublishInfos(this IContent content) => content.PublishCultureInfos = null; + + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool ClearPublishInfo(this IContent content, string? culture) + { + if (culture == null) { - culture = culture?.NullOrWhiteSpaceAsNull(); - - // the variation should be supported by the content type properties - if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); - - var keepProcessing = true; - - if (culture == "*") - { - // all cultures - content.ClearPublishInfos(); - } - else - { - // one single culture - keepProcessing = content.ClearPublishInfo(culture); - } - - if (keepProcessing) - { - // property.PublishValues only publishes what is valid, variation-wise - foreach (var property in content.Properties) - property.UnpublishValues(culture); - - content.PublishedState = PublishedState.Publishing; - } - - return keepProcessing; + throw new ArgumentNullException(nameof(culture)); } - public static void ClearPublishInfos(this IContent content) + if (string.IsNullOrWhiteSpace(culture)) { - content.PublishCultureInfos = null; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); } - /// - /// Returns false if the culture is already unpublished - /// - /// - /// - /// - public static bool ClearPublishInfo(this IContent content, string? culture) + var removed = content.PublishCultureInfos?.Remove(culture); + if (removed ?? false) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - var removed = content.PublishCultureInfos?.Remove(culture); - if (removed ?? false) - { - // set the culture to be dirty - it's been modified - content.TouchCulture(culture); - } - return removed ?? false; + // set the culture to be dirty - it's been modified + content.TouchCulture(culture); } + + return removed ?? false; } } diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs index 77526f254a..18d254a9aa 100644 --- a/src/Umbraco.Core/Models/ContentSchedule.cs +++ b/src/Umbraco.Core/Models/ContentSchedule.cs @@ -1,78 +1,74 @@ -using System; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a scheduled action for a document. +/// +[Serializable] +[DataContract(IsReference = true)] +public class ContentSchedule : IDeepCloneable { /// - /// Represents a scheduled action for a document. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class ContentSchedule : IDeepCloneable + public ContentSchedule(string culture, DateTime date, ContentScheduleAction action) { - /// - /// Initializes a new instance of the class. - /// - public ContentSchedule(string culture, DateTime date, ContentScheduleAction action) - { - Id = Guid.Empty; // will be assigned by document repository - Culture = culture; - Date = date; - Action = action; - } - - /// - /// Initializes a new instance of the class. - /// - public ContentSchedule(Guid id, string culture, DateTime date, ContentScheduleAction action) - { - Id = id; - Culture = culture; - Date = date; - Action = action; - } - - /// - /// Gets the unique identifier of the document targeted by the scheduled action. - /// - [DataMember] - public Guid Id { get; set; } - - /// - /// Gets the culture of the scheduled action. - /// - /// - /// string.Empty represents the invariant culture. - /// - [DataMember] - public string Culture { get; } - - /// - /// Gets the date of the scheduled action. - /// - [DataMember] - public DateTime Date { get; } - - /// - /// Gets the action to take. - /// - [DataMember] - public ContentScheduleAction Action { get; } - - public override bool Equals(object? obj) - => obj is ContentSchedule other && Equals(other); - - public bool Equals(ContentSchedule other) - { - // don't compare Ids, two ContentSchedule are equal if they are for the same change - // for the same culture, on the same date - and the collection deals w/duplicates - return Culture.InvariantEquals(other.Culture) && Date == other.Date && Action == other.Action; - } - - public object DeepClone() - { - return new ContentSchedule(Id, Culture, Date, Action); - } + Id = Guid.Empty; // will be assigned by document repository + Culture = culture; + Date = date; + Action = action; } + + /// + /// Initializes a new instance of the class. + /// + public ContentSchedule(Guid id, string culture, DateTime date, ContentScheduleAction action) + { + Id = id; + Culture = culture; + Date = date; + Action = action; + } + + /// + /// Gets the unique identifier of the document targeted by the scheduled action. + /// + [DataMember] + public Guid Id { get; set; } + + /// + /// Gets the culture of the scheduled action. + /// + /// + /// string.Empty represents the invariant culture. + /// + [DataMember] + public string Culture { get; } + + /// + /// Gets the date of the scheduled action. + /// + [DataMember] + public DateTime Date { get; } + + /// + /// Gets the action to take. + /// + [DataMember] + public ContentScheduleAction Action { get; } + + public object DeepClone() => new ContentSchedule(Id, Culture, Date, Action); + + public override bool Equals(object? obj) + => obj is ContentSchedule other && Equals(other); + + public bool Equals(ContentSchedule other) => + + // don't compare Ids, two ContentSchedule are equal if they are for the same change + // for the same culture, on the same date - and the collection deals w/duplicates + Culture.InvariantEquals(other.Culture) && Date == other.Date && Action == other.Action; + + public override int GetHashCode() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Models/ContentScheduleAction.cs b/src/Umbraco.Core/Models/ContentScheduleAction.cs index 03be526814..d6a50b994b 100644 --- a/src/Umbraco.Core/Models/ContentScheduleAction.cs +++ b/src/Umbraco.Core/Models/ContentScheduleAction.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines scheduled actions for documents. +/// +public enum ContentScheduleAction { /// - /// Defines scheduled actions for documents. + /// Release the document. /// - public enum ContentScheduleAction - { - /// - /// Release the document. - /// - Release, + Release, - /// - /// Expire the document. - /// - Expire - } + /// + /// Expire the document. + /// + Expire, } diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index 12a53fd103..4fb90779de 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -1,242 +1,261 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable { - public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable + // underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed + private readonly Dictionary> _schedule + = new(StringComparer.InvariantCultureIgnoreCase); + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + /// + /// Returns all schedules registered + /// + /// + public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); + + public object DeepClone() { - //underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed - private readonly Dictionary> _schedule - = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); - - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; - - private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + var clone = new ContentScheduleCollection(); + foreach (KeyValuePair> cultureSched in _schedule) { - CollectionChanged?.Invoke(this, args); - } - - /// - /// Add an existing schedule - /// - /// - public void Add(ContentSchedule schedule) - { - if (!_schedule.TryGetValue(schedule.Culture, out var changes)) + var list = new SortedList(); + foreach (KeyValuePair schedEntry in cultureSched.Value) { - changes = new SortedList(); - _schedule[schedule.Culture] = changes; + list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); } - // TODO: Below will throw if there are duplicate dates added, validate/return bool? - changes.Add(schedule.Date, schedule); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule)); + clone._schedule[cultureSched.Key] = list; } - /// - /// Adds a new schedule for invariant content - /// - /// - /// - public bool Add(DateTime? releaseDate, DateTime? expireDate) + return clone; + } + + public bool Equals(ContentScheduleCollection? other) + { + if (other == null) { - return Add(string.Empty, releaseDate, expireDate); + return false; } - /// - /// Adds a new schedule for a culture - /// - /// - /// - /// - /// true if successfully added, false if validation fails - public bool Add(string? culture, DateTime? releaseDate, DateTime? expireDate) + Dictionary> thisSched = _schedule; + Dictionary> thatSched = other._schedule; + + if (thisSched.Count != thatSched.Count) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) + return false; + } + + foreach ((var culture, SortedList thisList) in thisSched) + { + // if culture is missing, or actions differ, false + if (!thatSched.TryGetValue(culture, out SortedList? thatList) || + !thatList.SequenceEqual(thisList)) + { return false; - - if (!releaseDate.HasValue && !expireDate.HasValue) return false; - - // TODO: Do we allow passing in a release or expiry date that is before now? - - if (!_schedule.TryGetValue(culture, out var changes)) - { - changes = new SortedList(); - _schedule[culture] = changes; } - - // TODO: Below will throw if there are duplicate dates added, should validate/return bool? - // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? - - if (releaseDate.HasValue) - { - var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); - changes.Add(releaseDate.Value, entry); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); - } - - if (expireDate.HasValue) - { - var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); - changes.Add(expireDate.Value, entry); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); - } - - return true; } - /// - /// Remove a scheduled change - /// - /// - public void Remove(ContentSchedule change) + return true; + } + + public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) + { + var schedule = new ContentScheduleCollection(); + schedule.Add(string.Empty, release, expire); + return schedule; + } + + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + /// + /// Add an existing schedule + /// + /// + public void Add(ContentSchedule schedule) + { + if (!_schedule.TryGetValue(schedule.Culture, out SortedList? changes)) { - if (_schedule.TryGetValue(change.Culture, out var s)) + changes = new SortedList(); + _schedule[schedule.Culture] = changes; + } + + // TODO: Below will throw if there are duplicate dates added, validate/return bool? + changes.Add(schedule.Date, schedule); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule)); + } + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); + + /// + /// Adds a new schedule for invariant content + /// + /// + /// + public bool Add(DateTime? releaseDate, DateTime? expireDate) => Add(string.Empty, releaseDate, expireDate); + + /// + /// Adds a new schedule for a culture + /// + /// + /// + /// + /// true if successfully added, false if validation fails + public bool Add(string? culture, DateTime? releaseDate, DateTime? expireDate) + { + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) + { + return false; + } + + if (!releaseDate.HasValue && !expireDate.HasValue) + { + return false; + } + + // TODO: Do we allow passing in a release or expiry date that is before now? + if (!_schedule.TryGetValue(culture, out SortedList? changes)) + { + changes = new SortedList(); + _schedule[culture] = changes; + } + + // TODO: Below will throw if there are duplicate dates added, should validate/return bool? + // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? + if (releaseDate.HasValue) + { + var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); + changes.Add(releaseDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + if (expireDate.HasValue) + { + var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); + changes.Add(expireDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + return true; + } + + /// + /// Remove a scheduled change + /// + /// + public void Remove(ContentSchedule change) + { + if (_schedule.TryGetValue(change.Culture, out SortedList? s)) + { + var removed = s.Remove(change.Date); + if (removed) { - var removed = s.Remove(change.Date); - if (removed) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); + if (s.Count == 0) { - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); - if (s.Count == 0) - _schedule.Remove(change.Culture); + _schedule.Remove(change.Culture); } - } } - - /// - /// Clear all of the scheduled change type for invariant content - /// - /// - /// If specified, will clear all entries with dates less than or equal to the value - public void Clear(ContentScheduleAction action, DateTime? changeDate = null) - { - Clear(string.Empty, action, changeDate); - } - - /// - /// Clear all of the scheduled change type for the culture - /// - /// - /// - /// If specified, will clear all entries with dates less than or equal to the value - public void Clear(string? culture, ContentScheduleAction action, DateTime? date = null) - { - if (culture is null || !_schedule.TryGetValue(culture, out var schedules)) - return; - - var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)).ToList(); - - foreach (var remove in removes) - { - var removed = schedules.Remove(remove.Value.Date); - if (!removed) - continue; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); - } - - if (schedules.Count == 0) - _schedule.Remove(culture); - } - - /// - /// Returns all pending schedules based on the date and type provided - /// - /// - /// - /// - public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) - { - return _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); - } - - /// - /// Gets the schedule for invariant content - /// - /// - public IEnumerable GetSchedule(ContentScheduleAction? action = null) - { - return GetSchedule(string.Empty, action); - } - - /// - /// Gets the schedule for a culture - /// - /// - /// - /// - public IEnumerable GetSchedule(string? culture, ContentScheduleAction? action = null) - { - if (culture is not null && _schedule.TryGetValue(culture, out var changes)) - return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); - return Enumerable.Empty(); - } - - /// - /// Returns all schedules registered - /// - /// - public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); - - public object DeepClone() - { - var clone = new ContentScheduleCollection(); - foreach(var cultureSched in _schedule) - { - var list = new SortedList(); - foreach (var schedEntry in cultureSched.Value) - list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); - clone._schedule[cultureSched.Key] = list; - } - return clone; - } - - public override bool Equals(object? obj) - => obj is ContentScheduleCollection other && Equals(other); - - public bool Equals(ContentScheduleCollection? other) - { - if (other == null) return false; - - var thisSched = _schedule; - var thatSched = other._schedule; - - if (thisSched.Count != thatSched.Count) - return false; - - foreach (var (culture, thisList) in thisSched) - { - // if culture is missing, or actions differ, false - if (!thatSched.TryGetValue(culture, out var thatList) || !thatList.SequenceEqual(thisList)) - return false; - } - - return true; - } - - public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) - { - var schedule = new ContentScheduleCollection(); - schedule.Add(string.Empty, release, expire); - return schedule; - } - - public static ContentScheduleCollection CreateWithEntry(string culture, DateTime? release, DateTime? expire) - { - var schedule = new ContentScheduleCollection(); - schedule.Add(culture, release, expire); - return schedule; - } + } + + /// + /// Clear all of the scheduled change type for invariant content + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(ContentScheduleAction action, DateTime? changeDate = null) => + Clear(string.Empty, action, changeDate); + + /// + /// Clear all of the scheduled change type for the culture + /// + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(string? culture, ContentScheduleAction action, DateTime? date = null) + { + if (culture is null || !_schedule.TryGetValue(culture, out SortedList? schedules)) + { + return; + } + + var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)) + .ToList(); + + foreach (KeyValuePair remove in removes) + { + var removed = schedules.Remove(remove.Value.Date); + if (!removed) + { + continue; + } + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); + } + + if (schedules.Count == 0) + { + _schedule.Remove(culture); + } + } + + /// + /// Returns all pending schedules based on the date and type provided + /// + /// + /// + /// + public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) => + _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); + + /// + /// Gets the schedule for invariant content + /// + /// + public IEnumerable GetSchedule(ContentScheduleAction? action = null) => + GetSchedule(string.Empty, action); + + /// + /// Gets the schedule for a culture + /// + /// + /// + /// + public IEnumerable GetSchedule(string? culture, ContentScheduleAction? action = null) + { + if (culture is not null && _schedule.TryGetValue(culture, out SortedList? changes)) + { + return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); + } + + return Enumerable.Empty(); + } + + public override bool Equals(object? obj) + => obj is ContentScheduleCollection other && Equals(other); + + public static ContentScheduleCollection CreateWithEntry(string culture, DateTime? release, DateTime? expire) + { + var schedule = new ContentScheduleCollection(); + schedule.Add(culture, release, expire); + return schedule; + } + + public override int GetHashCode() + { + throw new NotImplementedException(); } } diff --git a/src/Umbraco.Core/Models/ContentStatus.cs b/src/Umbraco.Core/Models/ContentStatus.cs index 15d5d59861..1fd1eeaa8a 100644 --- a/src/Umbraco.Core/Models/ContentStatus.cs +++ b/src/Umbraco.Core/Models/ContentStatus.cs @@ -1,46 +1,44 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Describes the states of a document, with regard to (schedule) publishing. +/// +[Serializable] +[DataContract] +public enum ContentStatus { + // typical flow: + // Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired + /// - /// Describes the states of a document, with regard to (schedule) publishing. + /// The document is not trashed, and not published. /// - [Serializable] - [DataContract] - public enum ContentStatus - { - // typical flow: - // Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired + [EnumMember] + Unpublished, - /// - /// The document is not trashed, and not published. - /// - [EnumMember] - Unpublished, + /// + /// The document is published. + /// + [EnumMember] + Published, - /// - /// The document is published. - /// - [EnumMember] - Published, + /// + /// The document is not trashed, not published, after being unpublished by a scheduled action. + /// + [EnumMember] + Expired, - /// - /// The document is not trashed, not published, after being unpublished by a scheduled action. - /// - [EnumMember] - Expired, + /// + /// The document is trashed. + /// + [EnumMember] + Trashed, - /// - /// The document is trashed. - /// - [EnumMember] - Trashed, - - /// - /// The document is not trashed, not published, and pending publication by a scheduled action. - /// - [EnumMember] - AwaitingRelease - } + /// + /// The document is not trashed, not published, and pending publication by a scheduled action. + /// + [EnumMember] + AwaitingRelease, } diff --git a/src/Umbraco.Core/Models/ContentTagsExtensions.cs b/src/Umbraco.Core/Models/ContentTagsExtensions.cs index 0dacd78844..1d52300460 100644 --- a/src/Umbraco.Core/Models/ContentTagsExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTagsExtensions.cs @@ -1,55 +1,74 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the class, to manage tags. +/// +public static class ContentTagsExtensions { /// - /// Provides extension methods for the class, to manage tags. + /// Assign tags. /// - public static class ContentTagsExtensions + /// The content item. + /// + /// The property alias. + /// The tags. + /// A value indicating whether to merge the tags with existing tags instead of replacing them. + /// A culture, for multi-lingual properties. + /// + /// + public static void AssignTags( + this IContentBase content, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + string propertyTypeAlias, + IEnumerable tags, + bool merge = false, + string? culture = null) => + content + .GetTagProperty(propertyTypeAlias) + .AssignTags(propertyEditors, dataTypeService, serializer, tags, merge, culture); + + /// + /// Remove tags. + /// + /// The content item. + /// + /// The property alias. + /// The tags. + /// A culture, for multi-lingual properties. + /// + /// + public static void RemoveTags( + this IContentBase content, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + string propertyTypeAlias, + IEnumerable tags, + string? culture = null) => + content.GetTagProperty(propertyTypeAlias) + .RemoveTags(propertyEditors, dataTypeService, serializer, tags, culture); + + // gets and validates the property + private static IProperty GetTagProperty(this IContentBase content, string propertyTypeAlias) { - /// - /// Assign tags. - /// - /// The content item. - /// - /// The property alias. - /// The tags. - /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// A culture, for multi-lingual properties. - /// - public static void AssignTags(this IContentBase content, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string propertyTypeAlias, IEnumerable tags, bool merge = false, string? culture = null) + if (content == null) { - content.GetTagProperty(propertyTypeAlias).AssignTags(propertyEditors, dataTypeService, serializer, tags, merge, culture); + throw new ArgumentNullException(nameof(content)); } - /// - /// Remove tags. - /// - /// The content item. - /// - /// The property alias. - /// The tags. - /// A culture, for multi-lingual properties. - /// - public static void RemoveTags(this IContentBase content, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string propertyTypeAlias, IEnumerable tags, string? culture = null) + IProperty? property = content.Properties[propertyTypeAlias]; + if (property != null) { - content.GetTagProperty(propertyTypeAlias).RemoveTags(propertyEditors, dataTypeService, serializer, tags, culture); + return property; } - // gets and validates the property - private static IProperty GetTagProperty(this IContentBase content, string propertyTypeAlias) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - - var property = content.Properties[propertyTypeAlias]; - if (property != null) return property; - - throw new IndexOutOfRangeException($"Could not find a property with alias \"{propertyTypeAlias}\"."); - } + throw new IndexOutOfRangeException($"Could not find a property with alias \"{propertyTypeAlias}\"."); } } diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 1e011bbd07..c31f5f72ff 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -1,175 +1,172 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the content type that a object is based on +/// +[Serializable] +[DataContract(IsReference = true)] +public class ContentType : ContentTypeCompositionBase, IContentType { + public const bool SupportsPublishingConst = true; + + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> TemplateComparer = new( + (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), + templates => templates.GetHashCode()); + + private IEnumerable? _allowedTemplates; + + private int _defaultTemplate; + + private HistoryCleanup? _historyCleanup; + /// - /// Represents the content type that a object is based on + /// Constuctor for creating a ContentType with the parent's id. /// - [Serializable] - [DataContract(IsReference = true)] - public class ContentType : ContentTypeCompositionBase, IContentType + /// Only use this for creating ContentTypes at the root (with ParentId -1). + /// + /// + public ContentType(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - public const bool SupportsPublishingConst = true; + _allowedTemplates = new List(); + HistoryCleanup = new HistoryCleanup(); + } - // Custom comparer for enumerable - private static readonly DelegateEqualityComparer> TemplateComparer = new ( - (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), - templates => templates.GetHashCode()); + /// + /// Constuctor for creating a ContentType with the parent as an inherited type. + /// + /// Use this to ensure inheritance from parent. + /// + /// + /// + public ContentType(IShortStringHelper shortStringHelper, IContentType parent, string alias) + : base(shortStringHelper, parent, alias) + { + _allowedTemplates = new List(); + HistoryCleanup = new HistoryCleanup(); + } - private IEnumerable? _allowedTemplates; + /// + public override bool SupportsPublishing => SupportsPublishingConst; - private int _defaultTemplate; + /// + /// Gets or sets the alias of the default Template. + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity + /// + [IgnoreDataMember] + public ITemplate? DefaultTemplate => + AllowedTemplates?.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); - /// - /// Constuctor for creating a ContentType with the parent's id. - /// - /// Only use this for creating ContentTypes at the root (with ParentId -1). - /// - public ContentType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId) + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); + + [DataMember] + public int DefaultTemplateId + { + get => _defaultTemplate; + set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId)); + } + + /// + /// Gets or Sets a list of Templates which are allowed for the ContentType + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity + /// + [DataMember] + public IEnumerable? AllowedTemplates + { + get => _allowedTemplates; + set { - _allowedTemplates = new List(); - HistoryCleanup = new HistoryCleanup(); - } + SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); - - /// - /// Constuctor for creating a ContentType with the parent as an inherited type. - /// - /// Use this to ensure inheritance from parent. - /// - /// - public ContentType(IShortStringHelper shortStringHelper, IContentType parent, string alias) - : base(shortStringHelper, parent, alias) - { - _allowedTemplates = new List(); - HistoryCleanup = new HistoryCleanup(); - } - - /// - public override bool SupportsPublishing => SupportsPublishingConst; - - /// - public override ISimpleContentType ToSimple() => new SimpleContentType(this); - - /// - /// Gets or sets the alias of the default Template. - /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! - /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, - /// we should not store direct entity - /// - [IgnoreDataMember] - public ITemplate? DefaultTemplate => - AllowedTemplates?.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); - - - [DataMember] - public int DefaultTemplateId - { - get => _defaultTemplate; - set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId)); - } - - /// - /// Gets or Sets a list of Templates which are allowed for the ContentType - /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! - /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, - /// we should not store direct entity - /// - [DataMember] - public IEnumerable? AllowedTemplates - { - get => _allowedTemplates; - set - { - SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); - - if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false) - { - DefaultTemplateId = 0; - } - } - } - - private HistoryCleanup? _historyCleanup; - - public HistoryCleanup? HistoryCleanup - { - get => _historyCleanup; - set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup)); - } - - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template id to check - /// True if AllowedTemplates contains the templateId else False - public bool IsAllowedTemplate(int templateId) => - AllowedTemplates == null - ? false - : AllowedTemplates.Any(t => t.Id == templateId); - - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template alias to check - /// True if AllowedTemplates contains the templateAlias else False - public bool IsAllowedTemplate(string templateAlias) => - AllowedTemplates == null - ? false - : AllowedTemplates.Any(t => t.Alias.Equals(templateAlias, StringComparison.InvariantCultureIgnoreCase)); - - /// - /// Sets the default template for the ContentType - /// - /// Default - public void SetDefaultTemplate(ITemplate? template) - { - if (template == null) + if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false) { DefaultTemplateId = 0; - return; - } - - DefaultTemplateId = template.Id; - if (_allowedTemplates?.Any(x => x != null && x.Id == template.Id) == false) - { - var templates = AllowedTemplates?.ToList(); - templates?.Add(template); - AllowedTemplates = templates; } } - - /// - /// Removes a template from the list of allowed templates - /// - /// to remove - /// True if template was removed, otherwise False - public bool RemoveTemplate(ITemplate template) - { - if (DefaultTemplateId == template.Id) - { - DefaultTemplateId = default; - } - - var templates = AllowedTemplates?.ToList(); - ITemplate? remove = templates?.FirstOrDefault(x => x.Id == template.Id); - var result = remove is not null && templates is not null && templates.Remove(remove); - AllowedTemplates = templates; - - return result; - } - - /// - IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => - (IContentType)DeepCloneWithResetIdentities(newAlias); - - /// - public override bool IsDirty() => base.IsDirty() || (HistoryCleanup?.IsDirty() ?? false); } + + public HistoryCleanup? HistoryCleanup + { + get => _historyCleanup; + set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup)); + } + + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template id to check + /// True if AllowedTemplates contains the templateId else False + public bool IsAllowedTemplate(int templateId) => + AllowedTemplates == null + ? false + : AllowedTemplates.Any(t => t.Id == templateId); + + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template alias to check + /// True if AllowedTemplates contains the templateAlias else False + public bool IsAllowedTemplate(string templateAlias) => + AllowedTemplates == null + ? false + : AllowedTemplates.Any(t => t.Alias.Equals(templateAlias, StringComparison.InvariantCultureIgnoreCase)); + + /// + /// Sets the default template for the ContentType + /// + /// Default + public void SetDefaultTemplate(ITemplate? template) + { + if (template == null) + { + DefaultTemplateId = 0; + return; + } + + DefaultTemplateId = template.Id; + if (_allowedTemplates?.Any(x => x != null && x.Id == template.Id) == false) + { + var templates = AllowedTemplates?.ToList(); + templates?.Add(template); + AllowedTemplates = templates; + } + } + + /// + /// Removes a template from the list of allowed templates + /// + /// to remove + /// True if template was removed, otherwise False + public bool RemoveTemplate(ITemplate template) + { + if (DefaultTemplateId == template.Id) + { + DefaultTemplateId = default; + } + + var templates = AllowedTemplates?.ToList(); + ITemplate? remove = templates?.FirstOrDefault(x => x.Id == template.Id); + var result = remove is not null && templates is not null && templates.Remove(remove); + AllowedTemplates = templates; + + return result; + } + + /// + IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => + (IContentType)DeepCloneWithResetIdentities(newAlias); + + /// + public override bool IsDirty() => base.IsDirty() || (HistoryCleanup?.IsDirty() ?? false); } diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs index 529ae0bbe6..c4ab790dfe 100644 --- a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs @@ -1,17 +1,17 @@ -namespace Umbraco.Cms.Core.Models -{ - /// - /// Used when determining available compositions for a given content type - /// - public class ContentTypeAvailableCompositionsResult - { - public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) - { - Composition = composition; - Allowed = allowed; - } +namespace Umbraco.Cms.Core.Models; - public IContentTypeComposition Composition { get; private set; } - public bool Allowed { get; private set; } +/// +/// Used when determining available compositions for a given content type +/// +public class ContentTypeAvailableCompositionsResult +{ + public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) + { + Composition = composition; + Allowed = allowed; } + + public IContentTypeComposition Composition { get; } + + public bool Allowed { get; } } diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs index 180552cd74..4dc268faf3 100644 --- a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs @@ -1,26 +1,25 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Used when determining available compositions for a given content type +/// +public class ContentTypeAvailableCompositionsResults { - /// - /// Used when determining available compositions for a given content type - /// - public class ContentTypeAvailableCompositionsResults + public ContentTypeAvailableCompositionsResults() { - public ContentTypeAvailableCompositionsResults() - { - Ancestors = Enumerable.Empty(); - Results = Enumerable.Empty(); - } - - public ContentTypeAvailableCompositionsResults(IEnumerable ancestors, IEnumerable results) - { - Ancestors = ancestors; - Results = results; - } - - public IEnumerable Ancestors { get; private set; } - public IEnumerable Results { get; private set; } + Ancestors = Enumerable.Empty(); + Results = Enumerable.Empty(); } + + public ContentTypeAvailableCompositionsResults( + IEnumerable ancestors, + IEnumerable results) + { + Ancestors = ancestors; + Results = results; + } + + public IEnumerable Ancestors { get; } + + public IEnumerable Results { get; } } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index bf7cd0d8e3..6131e1b680 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -1,527 +1,542 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract class for base ContentType properties and methods +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] +public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase { - /// - /// Represents an abstract class for base ContentType properties and methods - /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] - public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> ContentTypeSortComparer = + new( + (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), + sorts => sorts.GetHashCode()); + + private readonly IShortStringHelper _shortStringHelper; + + private string _alias; + private bool _allowedAsRoot; // note: only one that's not 'pure element type' + private IEnumerable? _allowedContentTypes; + private string? _description; + private bool _hasPropertyTypeBeenRemoved; + private string? _icon = "icon-folder"; + private bool _isContainer; + private bool _isElement; + private PropertyGroupCollection _propertyGroups; + private string? _thumbnail = "folder.png"; + private ContentVariation _variations; + + protected ContentTypeBase(IShortStringHelper shortStringHelper, int parentId) { - private readonly IShortStringHelper _shortStringHelper; - - private string _alias; - private string? _description; - private string? _icon = "icon-folder"; - private string? _thumbnail = "folder.png"; - private bool _allowedAsRoot; // note: only one that's not 'pure element type' - private bool _isContainer; - private bool _isElement; - private PropertyGroupCollection _propertyGroups; - private PropertyTypeCollection _noGroupPropertyTypes; - private IEnumerable? _allowedContentTypes; - private bool _hasPropertyTypeBeenRemoved; - private ContentVariation _variations; - - protected ContentTypeBase(IShortStringHelper shortStringHelper, int parentId) + _alias = string.Empty; + _shortStringHelper = shortStringHelper; + if (parentId == 0) { - _alias = string.Empty; - _shortStringHelper = shortStringHelper; - if (parentId == 0) throw new ArgumentOutOfRangeException(nameof(parentId)); - ParentId = parentId; - - _allowedContentTypes = new List(); - _propertyGroups = new PropertyGroupCollection(); - - // actually OK as IsPublishing is constant - // ReSharper disable once VirtualMemberCallInConstructor - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; - - _variations = ContentVariation.Nothing; + throw new ArgumentOutOfRangeException(nameof(parentId)); } - protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent) - : this(shortStringHelper, parent, string.Empty) - { } + ParentId = parentId; - protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent, string alias) + _allowedContentTypes = new List(); + _propertyGroups = new PropertyGroupCollection(); + + // actually OK as IsPublishing is constant + // ReSharper disable once VirtualMemberCallInConstructor + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; + + _variations = ContentVariation.Nothing; + } + + protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent) + : this(shortStringHelper, parent, string.Empty) + { + } + + protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent, string alias) + { + if (parent == null) { - if (parent == null) throw new ArgumentNullException(nameof(parent)); - SetParent(parent); - - _shortStringHelper = shortStringHelper; - _alias = alias; - _allowedContentTypes = new List(); - _propertyGroups = new PropertyGroupCollection(); - - // actually OK as IsPublishing is constant - // ReSharper disable once VirtualMemberCallInConstructor - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; - - _variations = ContentVariation.Nothing; + throw new ArgumentNullException(nameof(parent)); } - public abstract ISimpleContentType ToSimple(); + SetParent(parent); - /// - /// Gets a value indicating whether the content type supports publishing. - /// - /// - /// A publishing content type supports draft and published values for properties. - /// It is possible to retrieve either the draft (default) or published value of a property. - /// Setting the value always sets the draft value, which then needs to be published. - /// A non-publishing content type only supports one value for properties. Getting - /// the draft or published value of a property returns the same thing, and publishing - /// a value property has no effect. - /// - public abstract bool SupportsPublishing { get; } + _shortStringHelper = shortStringHelper; + _alias = alias; + _allowedContentTypes = new List(); + _propertyGroups = new PropertyGroupCollection(); - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> ContentTypeSortComparer = - new DelegateEqualityComparer>( - (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), - sorts => sorts.GetHashCode()); + // actually OK as IsPublishing is constant + // ReSharper disable once VirtualMemberCallInConstructor + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; - protected void PropertyGroupsChanged(object? sender, NotifyCollectionChangedEventArgs e) + _variations = ContentVariation.Nothing; + } + + /// + /// Gets a value indicating whether the content type supports publishing. + /// + /// + /// + /// A publishing content type supports draft and published values for properties. + /// It is possible to retrieve either the draft (default) or published value of a property. + /// Setting the value always sets the draft value, which then needs to be published. + /// + /// + /// A non-publishing content type only supports one value for properties. Getting + /// the draft or published value of a property returns the same thing, and publishing + /// a value property has no effect. + /// + /// + public abstract bool SupportsPublishing { get; } + + /// + /// The Alias of the ContentType + /// + [DataMember] + public virtual string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), + ref _alias!, + nameof(Alias)); + } + + /// + /// A boolean flag indicating if a property type has been removed from this instance. + /// + /// + /// This is currently (specifically) used in order to know that we need to refresh the content cache which + /// needs to occur when a property has been removed from a content type + /// + [IgnoreDataMember] + internal bool HasPropertyTypeBeenRemoved + { + get => _hasPropertyTypeBeenRemoved; + private set { - OnPropertyChanged(nameof(PropertyGroups)); + _hasPropertyTypeBeenRemoved = value; + OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved)); } + } - protected void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) + /// + /// PropertyTypes that are not part of a PropertyGroup + /// + [IgnoreDataMember] + + // TODO: should we mark this as EditorBrowsable hidden since it really isn't ever used? + internal PropertyTypeCollection PropertyTypeCollection { get; private set; } + + public abstract ISimpleContentType ToSimple(); + + /// + /// Description for the ContentType + /// + [DataMember] + public string? Description + { + get => _description; + set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); + } + + /// + /// Name of the icon (sprite class) used to identify the ContentType + /// + [DataMember] + public string? Icon + { + get => _icon; + set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); + } + + /// + /// Name of the thumbnail used to identify the ContentType + /// + [DataMember] + public string? Thumbnail + { + get => _thumbnail; + set => SetPropertyValueAndDetectChanges(value, ref _thumbnail, nameof(Thumbnail)); + } + + /// + /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root + /// + [DataMember] + public bool AllowedAsRoot + { + get => _allowedAsRoot; + set => SetPropertyValueAndDetectChanges(value, ref _allowedAsRoot, nameof(AllowedAsRoot)); + } + + /// + /// Gets or Sets a boolean indicating whether this ContentType is a Container + /// + /// + /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. + /// + [DataMember] + public bool IsContainer + { + get => _isContainer; + set => SetPropertyValueAndDetectChanges(value, ref _isContainer, nameof(IsContainer)); + } + + /// + [DataMember] + public bool IsElement + { + get => _isElement; + set => SetPropertyValueAndDetectChanges(value, ref _isElement, nameof(IsElement)); + } + + /// + /// Gets or sets a list of integer Ids for allowed ContentTypes + /// + [DataMember] + public IEnumerable? AllowedContentTypes + { + get => _allowedContentTypes; + set => SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, nameof(AllowedContentTypes), ContentTypeSortComparer); + } + + /// + /// Gets or sets the content variation of the content type. + /// + public virtual ContentVariation Variations + { + get => _variations; + set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); + } + + /// + /// + /// A PropertyGroup corresponds to a Tab in the UI + /// Marked DoNotClone because we will manually deal with cloning and the event handlers + /// + [DataMember] + [DoNotClone] + public PropertyGroupCollection PropertyGroups + { + get => _propertyGroups; + set { - //enable this to detect duplicate property aliases. We do want this, however making this change in a - //patch release might be a little dangerous - - ////detect if there are any duplicate aliases - this cannot be allowed - //if (e.Action == NotifyCollectionChangedAction.Add - // || e.Action == NotifyCollectionChangedAction.Replace) - //{ - // var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); - // if (allAliases.HasDuplicates(false)) - // { - // var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); - // throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); - // } - //} - - OnPropertyChanged(nameof(PropertyTypes)); + _propertyGroups = value; + _propertyGroups.CollectionChanged += PropertyGroupsChanged; + PropertyGroupsChanged( + _propertyGroups, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + } - /// - /// The Alias of the ContentType - /// - [DataMember] - public virtual string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges( - value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), - ref _alias!, - nameof(Alias)); - } + /// + public bool SupportsVariation(string culture, string segment, bool wildcards = false) => - /// - /// Description for the ContentType - /// - [DataMember] - public string? Description - { - get => _description; - set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); - } + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, true, wildcards, false); - /// - /// Name of the icon (sprite class) used to identify the ContentType - /// - [DataMember] - public string? Icon - { - get => _icon; - set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); - } + /// + public bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false) => - /// - /// Name of the thumbnail used to identify the ContentType - /// - [DataMember] - public string? Thumbnail - { - get => _thumbnail; - set => SetPropertyValueAndDetectChanges(value, ref _thumbnail, nameof(Thumbnail)); - } + // non-exact validation: can accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, false, true, false); - /// - /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root - /// - [DataMember] - public bool AllowedAsRoot - { - get => _allowedAsRoot; - set => SetPropertyValueAndDetectChanges(value, ref _allowedAsRoot, nameof(AllowedAsRoot)); - } + /// + [IgnoreDataMember] + [DoNotClone] + public IEnumerable PropertyTypes => + PropertyTypeCollection.Union(PropertyGroups.SelectMany(x => x.PropertyTypes!)); - /// - /// Gets or Sets a boolean indicating whether this ContentType is a Container - /// - /// - /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. - /// - [DataMember] - public bool IsContainer + /// + [DoNotClone] + public IEnumerable NoGroupPropertyTypes + { + get => PropertyTypeCollection; + set { - get => _isContainer; - set => SetPropertyValueAndDetectChanges(value, ref _isContainer, nameof(IsContainer)); - } - - /// - [DataMember] - public bool IsElement - { - get => _isElement; - set => SetPropertyValueAndDetectChanges(value, ref _isElement, nameof(IsElement)); - } - - /// - /// Gets or sets a list of integer Ids for allowed ContentTypes - /// - [DataMember] - public IEnumerable? AllowedContentTypes - { - get => _allowedContentTypes; - set => SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, nameof(AllowedContentTypes), - ContentTypeSortComparer); - } - - /// - /// Gets or sets the content variation of the content type. - /// - public virtual ContentVariation Variations - { - get => _variations; - set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); - } - - /// - public bool SupportsVariation(string culture, string segment, bool wildcards = false) - { - // exact validation: cannot accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, true, wildcards, false); - } - - /// - public bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false) - { - // non-exact validation: can accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, false, true, false); - } - - /// - /// - /// A PropertyGroup corresponds to a Tab in the UI - /// Marked DoNotClone because we will manually deal with cloning and the event handlers - /// - [DataMember] - [DoNotClone] - public PropertyGroupCollection PropertyGroups - { - get => _propertyGroups; - set + if (PropertyTypeCollection != null) { - _propertyGroups = value; - _propertyGroups.CollectionChanged += PropertyGroupsChanged; - PropertyGroupsChanged(_propertyGroups, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - /// - [IgnoreDataMember] - [DoNotClone] - public IEnumerable PropertyTypes - { - get - { - return _noGroupPropertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes!)); - } - } - - /// - [DoNotClone] - public IEnumerable NoGroupPropertyTypes - { - get => _noGroupPropertyTypes; - set - { - if (_noGroupPropertyTypes != null) - { - _noGroupPropertyTypes.ClearCollectionChangedEvents(); - } - - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing, value); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; - PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - /// - /// A boolean flag indicating if a property type has been removed from this instance. - /// - /// - /// This is currently (specifically) used in order to know that we need to refresh the content cache which - /// needs to occur when a property has been removed from a content type - /// - [IgnoreDataMember] - internal bool HasPropertyTypeBeenRemoved - { - get => _hasPropertyTypeBeenRemoved; - private set - { - _hasPropertyTypeBeenRemoved = value; - OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved)); - } - } - - /// - /// Checks whether a PropertyType with a given alias already exists - /// - /// Alias of the PropertyType - /// Returns True if a PropertyType with the passed in alias exists, otherwise False - public abstract bool PropertyTypeExists(string? alias); - - /// - public abstract bool AddPropertyGroup(string alias, string name); - - /// - public abstract bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); - - /// - /// Adds a PropertyType, which does not belong to a PropertyGroup. - /// - /// to add - /// Returns True if PropertyType was added, otherwise False - public bool AddPropertyType(IPropertyType propertyType) - { - if (PropertyTypeExists(propertyType.Alias) == false) - { - _noGroupPropertyTypes.Add(propertyType); - return true; + PropertyTypeCollection.ClearCollectionChangedEvents(); } - return false; + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing, value); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; + PropertyTypesChanged( + PropertyTypeCollection, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + } - /// - /// Moves a PropertyType to a specified PropertyGroup - /// - /// Alias of the PropertyType to move - /// Alias of the PropertyGroup to move the PropertyType to - /// - /// If is null then the property is moved back to - /// "generic properties" ie does not have a tab anymore. - public bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias) + /// + /// Checks whether a PropertyType with a given alias already exists + /// + /// Alias of the PropertyType + /// Returns True if a PropertyType with the passed in alias exists, otherwise False + public abstract bool PropertyTypeExists(string? alias); + + /// + public abstract bool AddPropertyGroup(string alias, string name); + + /// + public abstract bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); + + /// + /// Adds a PropertyType, which does not belong to a PropertyGroup. + /// + /// to add + /// Returns True if PropertyType was added, otherwise False + public bool AddPropertyType(IPropertyType propertyType) + { + if (PropertyTypeExists(propertyType.Alias) == false) { - // get property, ensure it exists - var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); - if (propertyType == null) return false; - - // get new group, if required, and ensure it exists - PropertyGroup? newPropertyGroup = null; - if (propertyGroupAlias != null) - { - var index = PropertyGroups.IndexOfKey(propertyGroupAlias); - if (index == -1) return false; - - newPropertyGroup = PropertyGroups[index]; - } - - // get old group - var oldPropertyGroup = PropertyGroups.FirstOrDefault(x => - x.PropertyTypes?.Any(y => y.Alias == propertyTypeAlias) ?? false); - - // set new group - propertyType.PropertyGroupId = newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); - - // remove from old group, if any - add to new group, if any - oldPropertyGroup?.PropertyTypes?.RemoveItem(propertyTypeAlias); - newPropertyGroup?.PropertyTypes?.Add(propertyType); - + PropertyTypeCollection.Add(propertyType); return true; } - /// - /// Removes a PropertyType from the current ContentType - /// - /// Alias of the to remove - public void RemovePropertyType(string alias) + return false; + } + + /// + /// Moves a PropertyType to a specified PropertyGroup + /// + /// Alias of the PropertyType to move + /// Alias of the PropertyGroup to move the PropertyType to + /// + /// + /// If is null then the property is moved back to + /// "generic properties" ie does not have a tab anymore. + /// + public bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias) + { + // get property, ensure it exists + IPropertyType? propertyType = PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + if (propertyType == null) { - //check through each property group to see if we can remove the property type by alias from it - foreach (var propertyGroup in PropertyGroups) + return false; + } + + // get new group, if required, and ensure it exists + PropertyGroup? newPropertyGroup = null; + if (propertyGroupAlias != null) + { + var index = PropertyGroups.IndexOfKey(propertyGroupAlias); + if (index == -1) { - if (propertyGroup.PropertyTypes?.RemoveItem(alias) ?? false) - { - if (!HasPropertyTypeBeenRemoved) - { - HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(nameof(PropertyTypes)); - } - break; - } + return false; } - //check through each local property type collection (not assigned to a tab) - if (_noGroupPropertyTypes.RemoveItem(alias)) + newPropertyGroup = PropertyGroups[index]; + } + + // get old group + PropertyGroup? oldPropertyGroup = PropertyGroups.FirstOrDefault(x => x.PropertyTypes?.Any(y => y.Alias == propertyTypeAlias) ?? false); + + // set new group + propertyType.PropertyGroupId = + newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); + + // remove from old group, if any - add to new group, if any + oldPropertyGroup?.PropertyTypes?.RemoveItem(propertyTypeAlias); + newPropertyGroup?.PropertyTypes?.Add(propertyType); + + return true; + } + + /// + /// Removes a PropertyType from the current ContentType + /// + /// Alias of the to remove + public void RemovePropertyType(string alias) + { + // check through each property group to see if we can remove the property type by alias from it + foreach (PropertyGroup propertyGroup in PropertyGroups) + { + if (propertyGroup.PropertyTypes?.RemoveItem(alias) ?? false) { if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; OnPropertyChanged(nameof(PropertyTypes)); } + + break; } } - /// - /// Removes a PropertyGroup from the current ContentType - /// - /// Alias of the to remove - public void RemovePropertyGroup(string alias) + // check through each local property type collection (not assigned to a tab) + if (PropertyTypeCollection.RemoveItem(alias)) { - // if no group exists with that alias, do nothing - var index = PropertyGroups.IndexOfKey(alias); - if (index == -1) return; - - var group = PropertyGroups[index]; - - // first remove the group - PropertyGroups.Remove(group); - - if (group.PropertyTypes is not null) + if (!HasPropertyTypeBeenRemoved) { - // Then re-assign the group's properties to no group - foreach (var property in group.PropertyTypes) + HasPropertyTypeBeenRemoved = true; + OnPropertyChanged(nameof(PropertyTypes)); + } + } + } + + /// + /// Removes a PropertyGroup from the current ContentType + /// + /// Alias of the to remove + public void RemovePropertyGroup(string alias) + { + // if no group exists with that alias, do nothing + var index = PropertyGroups.IndexOfKey(alias); + if (index == -1) + { + return; + } + + PropertyGroup group = PropertyGroups[index]; + + // first remove the group + PropertyGroups.Remove(group); + + if (group.PropertyTypes is not null) + { + // Then re-assign the group's properties to no group + foreach (IPropertyType property in group.PropertyTypes) + { + property.PropertyGroupId = null; + PropertyTypeCollection.Add(property); + } + } + + OnPropertyChanged(nameof(PropertyGroups)); + } + + /// + /// Indicates whether the current entity is dirty. + /// + /// True if entity is dirty, otherwise False + public override bool IsDirty() + { + var dirtyEntity = base.IsDirty(); + + var dirtyGroups = PropertyGroups.Any(x => x.IsDirty()); + var dirtyTypes = PropertyTypes.Any(x => x.IsDirty()); + + return dirtyEntity || dirtyGroups || dirtyTypes; + } + + /// + /// Resets dirty properties by clearing the dictionary used to track changes. + /// + /// + /// Please note that resetting the dirty properties could potentially + /// obstruct the saving of a new or updated entity. + /// + public override void ResetDirtyProperties() + { + base.ResetDirtyProperties(); + + // loop through each property group to reset the property types + var propertiesReset = new List(); + + foreach (PropertyGroup propertyGroup in PropertyGroups) + { + propertyGroup.ResetDirtyProperties(); + if (propertyGroup.PropertyTypes is not null) + { + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) { - property.PropertyGroupId = null; - _noGroupPropertyTypes.Add(property); + propertyType.ResetDirtyProperties(); + propertiesReset.Add(propertyType.Id); } } - - OnPropertyChanged(nameof(PropertyGroups)); } - /// - /// PropertyTypes that are not part of a PropertyGroup - /// - [IgnoreDataMember] - // TODO: should we mark this as EditorBrowsable hidden since it really isn't ever used? - internal PropertyTypeCollection PropertyTypeCollection => _noGroupPropertyTypes; - - /// - /// Indicates whether the current entity is dirty. - /// - /// True if entity is dirty, otherwise False - public override bool IsDirty() + // then loop through our property type collection since some might not exist on a property group + // but don't re-reset ones we've already done. + foreach (IPropertyType propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false)) { - bool dirtyEntity = base.IsDirty(); + propertyType.ResetDirtyProperties(); + } + } - bool dirtyGroups = PropertyGroups.Any(x => x.IsDirty()); - bool dirtyTypes = PropertyTypes.Any(x => x.IsDirty()); - - return dirtyEntity || dirtyGroups || dirtyTypes; + public ContentTypeBase DeepCloneWithResetIdentities(string alias) + { + var clone = (ContentTypeBase)DeepClone(); + clone.Alias = alias; + clone.Key = Guid.Empty; + foreach (PropertyGroup propertyGroup in clone.PropertyGroups) + { + propertyGroup.ResetIdentity(); + propertyGroup.ResetDirtyProperties(false); } - /// - /// Resets dirty properties by clearing the dictionary used to track changes. - /// - /// - /// Please note that resetting the dirty properties could potentially - /// obstruct the saving of a new or updated entity. - /// - public override void ResetDirtyProperties() + foreach (IPropertyType propertyType in clone.PropertyTypes) { - base.ResetDirtyProperties(); - - //loop through each property group to reset the property types - var propertiesReset = new List(); - - foreach (var propertyGroup in PropertyGroups) - { - propertyGroup.ResetDirtyProperties(); - if (propertyGroup.PropertyTypes is not null) - { - foreach (var propertyType in propertyGroup.PropertyTypes) - { - propertyType.ResetDirtyProperties(); - propertiesReset.Add(propertyType.Id); - } - } - } - - //then loop through our property type collection since some might not exist on a property group - //but don't re-reset ones we've already done. - foreach (var propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false)) - { - propertyType.ResetDirtyProperties(); - } + propertyType.ResetIdentity(); + propertyType.ResetDirtyProperties(false); } - protected override void PerformDeepClone(object clone) + clone.ResetIdentity(); + clone.ResetDirtyProperties(false); + return clone; + } + + protected void PropertyGroupsChanged(object? sender, NotifyCollectionChangedEventArgs e) => + OnPropertyChanged(nameof(PropertyGroups)); + + protected void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + + // enable this to detect duplicate property aliases. We do want this, however making this change in a + // patch release might be a little dangerous + ////detect if there are any duplicate aliases - this cannot be allowed + // if (e.Action == NotifyCollectionChangedAction.Add + // || e.Action == NotifyCollectionChangedAction.Replace) + // { + // var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); + // if (allAliases.HasDuplicates(false)) + // { + // var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); + // throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); + // } + // } + OnPropertyChanged(nameof(PropertyTypes)); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (ContentTypeBase)clone; + + if (clonedEntity.PropertyTypeCollection != null) { - base.PerformDeepClone(clone); - - var clonedEntity = (ContentTypeBase) clone; - - if (clonedEntity._noGroupPropertyTypes != null) - { - //need to manually wire up the event handlers for the property type collections - we've ensured - // its ignored from the auto-clone process because its return values are unions, not raw and - // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 - - clonedEntity._noGroupPropertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any - clonedEntity._noGroupPropertyTypes = (PropertyTypeCollection) _noGroupPropertyTypes.DeepClone(); //manually deep clone - clonedEntity._noGroupPropertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler - } - - if (clonedEntity._propertyGroups != null) - { - clonedEntity._propertyGroups.ClearCollectionChangedEvents(); //clear this event handler if any - clonedEntity._propertyGroups = (PropertyGroupCollection) _propertyGroups.DeepClone(); //manually deep clone - clonedEntity._propertyGroups.CollectionChanged += clonedEntity.PropertyGroupsChanged; //re-assign correct event handler - } + // need to manually wire up the event handlers for the property type collections - we've ensured + // its ignored from the auto-clone process because its return values are unions, not raw and + // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 + clonedEntity.PropertyTypeCollection.ClearCollectionChangedEvents(); // clear this event handler if any + clonedEntity.PropertyTypeCollection = + (PropertyTypeCollection)PropertyTypeCollection.DeepClone(); // manually deep clone + clonedEntity.PropertyTypeCollection.CollectionChanged += + clonedEntity.PropertyTypesChanged; // re-assign correct event handler } - public ContentTypeBase DeepCloneWithResetIdentities(string alias) + if (clonedEntity._propertyGroups != null) { - var clone = (ContentTypeBase)DeepClone(); - clone.Alias = alias; - clone.Key = Guid.Empty; - foreach (var propertyGroup in clone.PropertyGroups) - { - propertyGroup.ResetIdentity(); - propertyGroup.ResetDirtyProperties(false); - } - foreach (var propertyType in clone.PropertyTypes) - { - propertyType.ResetIdentity(); - propertyType.ResetDirtyProperties(false); - } - - clone.ResetIdentity(); - clone.ResetDirtyProperties(false); - return clone; + clonedEntity._propertyGroups.ClearCollectionChangedEvents(); // clear this event handler if any + clonedEntity._propertyGroups = (PropertyGroupCollection)_propertyGroups.DeepClone(); // manually deep clone + clonedEntity._propertyGroups.CollectionChanged += + clonedEntity.PropertyGroupsChanged; // re-assign correct event handler } } } diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index d771efa12b..12e0e5a138 100644 --- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs @@ -1,64 +1,79 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions methods for . +/// +public static class ContentTypeBaseExtensions { - /// - /// Provides extensions methods for . - /// - public static class ContentTypeBaseExtensions + public static PublishedItemType GetItemType(this IContentTypeBase contentType) { - public static PublishedItemType GetItemType(this IContentTypeBase contentType) + Type type = contentType.GetType(); + PublishedItemType itemType = PublishedItemType.Unknown; + if (contentType.IsElement) { - var type = contentType.GetType(); - var itemType = PublishedItemType.Unknown; - if (contentType.IsElement) itemType = PublishedItemType.Element; - else if (typeof(IContentType).IsAssignableFrom(type)) itemType = PublishedItemType.Content; - else if (typeof(IMediaType).IsAssignableFrom(type)) itemType = PublishedItemType.Media; - else if (typeof(IMemberType).IsAssignableFrom(type)) itemType = PublishedItemType.Member; - return itemType; + itemType = PublishedItemType.Element; + } + else if (typeof(IContentType).IsAssignableFrom(type)) + { + itemType = PublishedItemType.Content; + } + else if (typeof(IMediaType).IsAssignableFrom(type)) + { + itemType = PublishedItemType.Media; + } + else if (typeof(IMemberType).IsAssignableFrom(type)) + { + itemType = PublishedItemType.Member; } - /// - /// Used to check if any property type was changed between variant/invariant - /// - /// - /// - public static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType) - { - return contentType.WasPropertyTypeVariationChanged(out var _); - } + return itemType; + } - /// - /// Used to check if any property type was changed between variant/invariant - /// - /// - /// - internal static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType, out IReadOnlyCollection aliases) - { - var a = new List(); + /// + /// Used to check if any property type was changed between variant/invariant + /// + /// + /// + public static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType) => + contentType.WasPropertyTypeVariationChanged(out IReadOnlyCollection _); - // property variation change? - var hasAnyPropertyVariationChanged = contentType.PropertyTypes.Any(propertyType => + /// + /// Used to check if any property type was changed between variant/invariant + /// + /// + /// + /// + internal static bool WasPropertyTypeVariationChanged( + this IContentTypeBase contentType, + out IReadOnlyCollection aliases) + { + var a = new List(); + + // property variation change? + var hasAnyPropertyVariationChanged = contentType.PropertyTypes.Any(propertyType => + { + // skip new properties + // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly + var isNewProperty = propertyType.WasPropertyDirty("Id"); + if (isNewProperty) { - // skip new properties - // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly - var isNewProperty = propertyType.WasPropertyDirty("Id"); - if (isNewProperty) return false; + return false; + } - // variation change? - var dirty = propertyType.WasPropertyDirty("Variations"); - if (dirty) - a.Add(propertyType.Alias); + // variation change? + var dirty = propertyType.WasPropertyDirty("Variations"); + if (dirty) + { + a.Add(propertyType.Alias); + } - return dirty; + return dirty; + }); - }); - - aliases = a; - return hasAnyPropertyVariationChanged; - } + aliases = a; + return hasAnyPropertyVariationChanged; } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 18dc1189f2..b7b9af6231 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -1,319 +1,338 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract class for composition specific ContentType properties and methods +/// +[Serializable] +[DataContract(IsReference = true)] +public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentTypeComposition { - /// - /// Represents an abstract class for composition specific ContentType properties and methods - /// - [Serializable] - [DataContract(IsReference = true)] - public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentTypeComposition + private List _contentTypeComposition = new(); + private List _removedContentTypeKeyTracker = new(); + + protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - private List _contentTypeComposition = new List(); - private List _removedContentTypeKeyTracker = new List(); + } - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId) - : base(shortStringHelper, parentId) - { } + protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent) + : this(shortStringHelper, parent, string.Empty) + { + } - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper,IContentTypeComposition parent) - : this(shortStringHelper, parent, string.Empty) - { } + protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) + : base(shortStringHelper, parent, alias) => + AddContentType(parent); - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) - : base(shortStringHelper, parent, alias) + public IEnumerable RemovedContentTypes => _removedContentTypeKeyTracker; + + /// + /// Gets or sets the content types that compose this content type. + /// + [DataMember] + public IEnumerable ContentTypeComposition + { + get => _contentTypeComposition; + set { - AddContentType(parent); + _contentTypeComposition = value.ToList(); + OnPropertyChanged(nameof(ContentTypeComposition)); } + } - public IEnumerable RemovedContentTypes => _removedContentTypeKeyTracker; - - /// - /// Gets or sets the content types that compose this content type. - /// - [DataMember] - public IEnumerable ContentTypeComposition + /// + [IgnoreDataMember] + public IEnumerable CompositionPropertyGroups + { + get { - get => _contentTypeComposition; - set + // we need to "acquire" composition groups and properties here, ie get our own clones, + // so that we can change their variation according to this content type variations. + // + // it would be nice to cache the resulting enumerable, but alas we cannot, otherwise + // any change to compositions are ignored and that breaks many things - and tracking + // changes to refresh the cache would be expensive. + void AcquireProperty(IPropertyType propertyType) { - _contentTypeComposition = value.ToList(); - OnPropertyChanged(nameof(ContentTypeComposition)); + propertyType.Variations &= Variations; + propertyType.ResetDirtyProperties(false); } - } - /// - [IgnoreDataMember] - public IEnumerable CompositionPropertyGroups - { - get - { - // we need to "acquire" composition groups and properties here, ie get our own clones, - // so that we can change their variation according to this content type variations. - // - // it would be nice to cache the resulting enumerable, but alas we cannot, otherwise - // any change to compositions are ignored and that breaks many things - and tracking - // changes to refresh the cache would be expensive. - - void AcquireProperty(IPropertyType propertyType) + return PropertyGroups.Union(ContentTypeComposition.SelectMany(x => x.CompositionPropertyGroups) + .Select(group => { - propertyType.Variations &= Variations; - propertyType.ResetDirtyProperties(false); - } - - return PropertyGroups.Union(ContentTypeComposition.SelectMany(x => x.CompositionPropertyGroups) - .Select(group => + group = (PropertyGroup)group.DeepClone(); + if (group.PropertyTypes is not null) { - group = (PropertyGroup) group.DeepClone(); - if (group.PropertyTypes is not null) + foreach (IPropertyType property in group.PropertyTypes) { - foreach (var property in group.PropertyTypes) - AcquireProperty(property); + AcquireProperty(property); } - return group; - })); - } + } + + return group; + })); } + } - /// - [IgnoreDataMember] - public IEnumerable CompositionPropertyTypes + /// + [IgnoreDataMember] + public IEnumerable CompositionPropertyTypes + { + get { - get + // we need to "acquire" composition properties here, ie get our own clones, + // so that we can change their variation according to this content type variations. + // + // see note in CompositionPropertyGroups for comments on caching the resulting enumerable + IPropertyType AcquireProperty(IPropertyType propertyType) { - // we need to "acquire" composition properties here, ie get our own clones, - // so that we can change their variation according to this content type variations. - // - // see note in CompositionPropertyGroups for comments on caching the resulting enumerable - - IPropertyType AcquireProperty(IPropertyType propertyType) - { - propertyType = (IPropertyType) propertyType.DeepClone(); - propertyType.Variations &= Variations; - propertyType.ResetDirtyProperties(false); - return propertyType; - } - - return ContentTypeComposition - .SelectMany(x => x.CompositionPropertyTypes) - .Select(AcquireProperty) - .Union(PropertyTypes); + propertyType = (IPropertyType)propertyType.DeepClone(); + propertyType.Variations &= Variations; + propertyType.ResetDirtyProperties(false); + return propertyType; } + + return ContentTypeComposition + .SelectMany(x => x.CompositionPropertyTypes) + .Select(AcquireProperty) + .Union(PropertyTypes); } + } - /// - public IEnumerable GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes(); + /// + public IEnumerable GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes(); - private IEnumerable GetRawComposedPropertyTypes(bool start = true) + /// + /// Adds a content type to the composition. + /// + /// The content type to add. + /// True if the content type was added, otherwise false. + public bool AddContentType(IContentTypeComposition? contentType) + { + if (contentType is null) { - var propertyTypes = ContentTypeComposition - .Cast() - .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); - - if (!start) - propertyTypes = propertyTypes.Union(PropertyTypes); - - return propertyTypes; - } - - /// - /// Adds a content type to the composition. - /// - /// The content type to add. - /// True if the content type was added, otherwise false. - public bool AddContentType(IContentTypeComposition? contentType) - { - if (contentType is null) - { - return false; - } - if (contentType.ContentTypeComposition.Any(x => x.CompositionAliases().Any(ContentTypeCompositionExists))) - return false; - - if (string.IsNullOrEmpty(Alias) == false && Alias.Equals(contentType.Alias)) - return false; - - if (ContentTypeCompositionExists(contentType.Alias) == false) - { - // Before we actually go ahead and add the ContentType as a Composition we ensure that we don't - // end up with duplicate PropertyType aliases - in which case we throw an exception. - var conflictingPropertyTypeAliases = CompositionPropertyTypes.SelectMany( - x => contentType.CompositionPropertyTypes - .Where(y => y.Alias.Equals(x.Alias, StringComparison.InvariantCultureIgnoreCase)) - .Select(p => p.Alias)).ToList(); - - if (conflictingPropertyTypeAliases.Any()) - throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); - - _contentTypeComposition.Add(contentType); - - OnPropertyChanged(nameof(ContentTypeComposition)); - - return true; - } - return false; } - /// - /// Removes a content type with a specified alias from the composition. - /// - /// The alias of the content type to remove. - /// True if the content type was removed, otherwise false. - public bool RemoveContentType(string alias) + if (contentType.ContentTypeComposition.Any(x => x.CompositionAliases().Any(ContentTypeCompositionExists))) { - if (ContentTypeCompositionExists(alias)) - { - var contentTypeComposition = ContentTypeComposition.FirstOrDefault(x => x.Alias == alias); - if (contentTypeComposition == null) // You can't remove a composition from another composition - return false; - - _removedContentTypeKeyTracker.Add(contentTypeComposition.Id); - - // If the ContentType we are removing has Compositions of its own these needs to be removed as well - var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); - if (compositionIdsToRemove.Any()) - _removedContentTypeKeyTracker.AddRange(compositionIdsToRemove); - - OnPropertyChanged(nameof(ContentTypeComposition)); - - return _contentTypeComposition.Remove(contentTypeComposition); - } - return false; } - /// - /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes - /// - /// Alias of a - /// True if ContentType with alias exists, otherwise returns False - public bool ContentTypeCompositionExists(string alias) + if (string.IsNullOrEmpty(Alias) == false && Alias.Equals(contentType.Alias)) { - if (ContentTypeComposition.Any(x => x.Alias.Equals(alias))) - return true; - - if (ContentTypeComposition.Any(x => x.ContentTypeCompositionExists(alias))) - return true; - return false; } - /// - /// Checks whether a PropertyType with a given alias already exists - /// - /// Alias of the PropertyType - /// Returns True if a PropertyType with the passed in alias exists, otherwise False - public override bool PropertyTypeExists(string? alias) => CompositionPropertyTypes.Any(x => x.Alias == alias); - - /// - public override bool AddPropertyGroup(string alias, string name) => AddAndReturnPropertyGroup(alias, name) != null; - - private PropertyGroup? AddAndReturnPropertyGroup(string alias, string name) + if (ContentTypeCompositionExists(contentType.Alias) == false) { - // Ensure we don't have it already - if (PropertyGroups.Contains(alias)) - return null; + // Before we actually go ahead and add the ContentType as a Composition we ensure that we don't + // end up with duplicate PropertyType aliases - in which case we throw an exception. + var conflictingPropertyTypeAliases = CompositionPropertyTypes.SelectMany( + x => contentType.CompositionPropertyTypes + .Where(y => y.Alias.Equals(x.Alias, StringComparison.InvariantCultureIgnoreCase)) + .Select(p => p.Alias)).ToList(); - // Add new group - var group = new PropertyGroup(SupportsPublishing) + if (conflictingPropertyTypeAliases.Any()) { - Alias = alias, - Name = name - }; - - // check if it is inherited - there might be more than 1 but we want the 1st, to - // reuse its sort order - if there are more than 1 and they have different sort - // orders... there isn't much we can do anyways - var inheritGroup = CompositionPropertyGroups.FirstOrDefault(x => x.Alias == alias); - if (inheritGroup == null) - { - // no, just local, set sort order - var lastGroup = PropertyGroups.LastOrDefault(); - if (lastGroup != null) - group.SortOrder = lastGroup.SortOrder + 1; - } - else - { - // yes, inherited, re-use sort order - group.SortOrder = inheritGroup.SortOrder; + throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); } - // add - PropertyGroups.Add(group); + _contentTypeComposition.Add(contentType); - return group; - } - - /// - public override bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null) - { - // ensure no duplicate alias - over all composition properties - if (PropertyTypeExists(propertyType.Alias)) - return false; - - // get and ensure a group local to this content type - PropertyGroup? group; - var index = PropertyGroups.IndexOfKey(propertyGroupAlias); - if (index != -1) - { - group = PropertyGroups[index]; - } - else if (!string.IsNullOrEmpty(propertyGroupName)) - { - group = AddAndReturnPropertyGroup(propertyGroupAlias, propertyGroupName); - if (group == null) - { - return false; - } - } - else - { - // No group name specified, so we can't create a new one and add the property type - return false; - } - - // add property to group - propertyType.PropertyGroupId = new Lazy(() => group.Id); - group.PropertyTypes?.Add(propertyType); + OnPropertyChanged(nameof(ContentTypeComposition)); return true; } - /// - /// Gets a list of ContentType aliases from the current composition - /// - /// An enumerable list of string aliases - /// Does not contain the alias of the Current ContentType - public IEnumerable CompositionAliases() - => ContentTypeComposition - .Select(x => x.Alias) - .Union(ContentTypeComposition.SelectMany(x => x.CompositionAliases())); + return false; + } - /// - /// Gets a list of ContentType Ids from the current composition - /// - /// An enumerable list of integer ids - /// Does not contain the Id of the Current ContentType - public IEnumerable CompositionIds() - => ContentTypeComposition - .Select(x => x.Id) - .Union(ContentTypeComposition.SelectMany(x => x.CompositionIds())); - - protected override void PerformDeepClone(object clone) + /// + /// Removes a content type with a specified alias from the composition. + /// + /// The alias of the content type to remove. + /// True if the content type was removed, otherwise false. + public bool RemoveContentType(string alias) + { + if (ContentTypeCompositionExists(alias)) { - base.PerformDeepClone(clone); + IContentTypeComposition? contentTypeComposition = ContentTypeComposition.FirstOrDefault(x => x.Alias == alias); - var clonedEntity = (ContentTypeCompositionBase)clone; + // You can't remove a composition from another composition + if (contentTypeComposition == null) + { + return false; + } - // need to manually assign since this is an internal field and will not be automatically mapped - clonedEntity._removedContentTypeKeyTracker = new List(); - clonedEntity._contentTypeComposition = ContentTypeComposition.Select(x => (IContentTypeComposition)x.DeepClone()).ToList(); + _removedContentTypeKeyTracker.Add(contentTypeComposition.Id); + + // If the ContentType we are removing has Compositions of its own these needs to be removed as well + var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); + if (compositionIdsToRemove.Any()) + { + _removedContentTypeKeyTracker.AddRange(compositionIdsToRemove); + } + + OnPropertyChanged(nameof(ContentTypeComposition)); + + return _contentTypeComposition.Remove(contentTypeComposition); } + + return false; + } + + /// + /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes + /// + /// Alias of a + /// True if ContentType with alias exists, otherwise returns False + public bool ContentTypeCompositionExists(string alias) + { + if (ContentTypeComposition.Any(x => x.Alias.Equals(alias))) + { + return true; + } + + if (ContentTypeComposition.Any(x => x.ContentTypeCompositionExists(alias))) + { + return true; + } + + return false; + } + + /// + /// Checks whether a PropertyType with a given alias already exists + /// + /// Alias of the PropertyType + /// Returns True if a PropertyType with the passed in alias exists, otherwise False + public override bool PropertyTypeExists(string? alias) => CompositionPropertyTypes.Any(x => x.Alias == alias); + + /// + public override bool AddPropertyGroup(string alias, string name) => AddAndReturnPropertyGroup(alias, name) != null; + + /// + public override bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null) + { + // ensure no duplicate alias - over all composition properties + if (PropertyTypeExists(propertyType.Alias)) + { + return false; + } + + // get and ensure a group local to this content type + PropertyGroup? group; + var index = PropertyGroups.IndexOfKey(propertyGroupAlias); + if (index != -1) + { + group = PropertyGroups[index]; + } + else if (!string.IsNullOrEmpty(propertyGroupName)) + { + group = AddAndReturnPropertyGroup(propertyGroupAlias, propertyGroupName); + if (group == null) + { + return false; + } + } + else + { + // No group name specified, so we can't create a new one and add the property type + return false; + } + + // add property to group + propertyType.PropertyGroupId = new Lazy(() => group.Id); + group.PropertyTypes?.Add(propertyType); + + return true; + } + + /// + /// Gets a list of ContentType aliases from the current composition + /// + /// An enumerable list of string aliases + /// Does not contain the alias of the Current ContentType + public IEnumerable CompositionAliases() + => ContentTypeComposition + .Select(x => x.Alias) + .Union(ContentTypeComposition.SelectMany(x => x.CompositionAliases())); + + /// + /// Gets a list of ContentType Ids from the current composition + /// + /// An enumerable list of integer ids + /// Does not contain the Id of the Current ContentType + public IEnumerable CompositionIds() + => ContentTypeComposition + .Select(x => x.Id) + .Union(ContentTypeComposition.SelectMany(x => x.CompositionIds())); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (ContentTypeCompositionBase)clone; + + // need to manually assign since this is an internal field and will not be automatically mapped + clonedEntity._removedContentTypeKeyTracker = new List(); + clonedEntity._contentTypeComposition = + ContentTypeComposition.Select(x => (IContentTypeComposition)x.DeepClone()).ToList(); + } + + private IEnumerable GetRawComposedPropertyTypes(bool start = true) + { + IEnumerable propertyTypes = ContentTypeComposition + .Cast() + .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); + + if (!start) + { + propertyTypes = propertyTypes.Union(PropertyTypes); + } + + return propertyTypes; + } + + private PropertyGroup? AddAndReturnPropertyGroup(string alias, string name) + { + // Ensure we don't have it already + if (PropertyGroups.Contains(alias)) + { + return null; + } + + // Add new group + var group = new PropertyGroup(SupportsPublishing) { Alias = alias, Name = name }; + + // check if it is inherited - there might be more than 1 but we want the 1st, to + // reuse its sort order - if there are more than 1 and they have different sort + // orders... there isn't much we can do anyways + PropertyGroup? inheritGroup = CompositionPropertyGroups.FirstOrDefault(x => x.Alias == alias); + if (inheritGroup == null) + { + // no, just local, set sort order + PropertyGroup? lastGroup = PropertyGroups.LastOrDefault(); + if (lastGroup != null) + { + group.SortOrder = lastGroup.SortOrder + 1; + } + } + else + { + // yes, inherited, re-use sort order + group.SortOrder = inheritGroup.SortOrder; + } + + // add + PropertyGroups.Add(group); + + return group; } } diff --git a/src/Umbraco.Core/Models/ContentTypeImportModel.cs b/src/Umbraco.Core/Models/ContentTypeImportModel.cs index 49d09c6821..5de62fcffa 100644 --- a/src/Umbraco.Core/Models/ContentTypeImportModel.cs +++ b/src/Umbraco.Core/Models/ContentTypeImportModel.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "contentTypeImportModel")] +public class ContentTypeImportModel : INotificationModel { - [DataContract(Name = "contentTypeImportModel")] - public class ContentTypeImportModel : INotificationModel - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "notifications")] - public List Notifications { get; } = new List(); + [DataMember(Name = "tempFileName")] + public string? TempFileName { get; set; } - [DataMember(Name = "tempFileName")] - public string? TempFileName { get; set; } - } + [DataMember(Name = "notifications")] + public List Notifications { get; } = new(); } diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index e7a11bad47..e10d650cac 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -1,78 +1,86 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a POCO for setting sort order on a ContentType reference +/// +public class ContentTypeSort : IValueObject, IDeepCloneable { - /// - /// Represents a POCO for setting sort order on a ContentType reference - /// - public class ContentTypeSort : IValueObject, IDeepCloneable + // this parameterless ctor should never be used BUT is required by AutoMapper in EntityMapperProfile + public ContentTypeSort() { - // this parameterless ctor should never be used BUT is required by AutoMapper in EntityMapperProfile - public ContentTypeSort() { } + } - /// - /// Initializes a new instance of the class. - /// - public ContentTypeSort(int id, int sortOrder) + /// + /// Initializes a new instance of the class. + /// + public ContentTypeSort(int id, int sortOrder) + { + Id = new Lazy(() => id); + SortOrder = sortOrder; + } + + public ContentTypeSort(Lazy id, int sortOrder, string alias) + { + Id = id; + SortOrder = sortOrder; + Alias = alias; + } + + /// + /// Gets or sets the Id of the ContentType + /// + public Lazy Id { get; set; } = new(() => 0); + + /// + /// Gets or sets the Sort Order of the ContentType + /// + public int SortOrder { get; set; } + + /// + /// Gets or sets the Alias of the ContentType + /// + public string Alias { get; set; } = string.Empty; + + public object DeepClone() + { + var clone = (ContentTypeSort)MemberwiseClone(); + var id = Id.Value; + clone.Id = new Lazy(() => id); + return clone; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - Id = new Lazy(() => id); - SortOrder = sortOrder; + return false; } - public ContentTypeSort(Lazy id, int sortOrder, string @alias) + if (ReferenceEquals(this, obj)) { - Id = id; - SortOrder = sortOrder; - Alias = alias; + return true; } - /// - /// Gets or sets the Id of the ContentType - /// - public Lazy Id { get; set; } = new Lazy(() => 0); - - /// - /// Gets or sets the Sort Order of the ContentType - /// - public int SortOrder { get; set; } - - /// - /// Gets or sets the Alias of the ContentType - /// - public string Alias { get; set; } = string.Empty; - - - public object DeepClone() + if (obj.GetType() != GetType()) { - var clone = (ContentTypeSort)MemberwiseClone(); - var id = Id.Value; - clone.Id = new Lazy(() => id); - return clone; + return false; } - protected bool Equals(ContentTypeSort other) - { - return Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); - } + return Equals((ContentTypeSort)obj); + } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((ContentTypeSort) obj); - } + protected bool Equals(ContentTypeSort other) => + Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked - { - //The hash code will just be the alias if one is assigned, otherwise it will be the hash code of the Id. - //In some cases the alias can be null of the non lazy ctor is used, in that case, the lazy Id will already have a value created. - return Alias != null ? Alias.GetHashCode() : (Id.Value.GetHashCode() * 397); - } + // The hash code will just be the alias if one is assigned, otherwise it will be the hash code of the Id. + // In some cases the alias can be null of the non lazy ctor is used, in that case, the lazy Id will already have a value created. + return Alias != null ? Alias.GetHashCode() : Id.Value.GetHashCode() * 397; } - } } diff --git a/src/Umbraco.Core/Models/ContentVariation.cs b/src/Umbraco.Core/Models/ContentVariation.cs index 00c7f197a8..019da0eee0 100644 --- a/src/Umbraco.Core/Models/ContentVariation.cs +++ b/src/Umbraco.Core/Models/ContentVariation.cs @@ -1,37 +1,36 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Indicates how values can vary. +/// +/// +/// Values can vary by nothing, or culture, or segment, or both. +/// +/// Varying by culture implies that each culture version of a document can +/// be available or not, and published or not, individually. Varying by segment +/// is a property-level thing. +/// +/// +[Flags] +public enum ContentVariation : byte { /// - /// Indicates how values can vary. + /// Values do not vary. /// - /// - /// Values can vary by nothing, or culture, or segment, or both. - /// Varying by culture implies that each culture version of a document can - /// be available or not, and published or not, individually. Varying by segment - /// is a property-level thing. - /// - [Flags] - public enum ContentVariation : byte - { - /// - /// Values do not vary. - /// - Nothing = 0, + Nothing = 0, - /// - /// Values vary by culture. - /// - Culture = 1, + /// + /// Values vary by culture. + /// + Culture = 1, - /// - /// Values vary by segment. - /// - Segment = 2, + /// + /// Values vary by segment. + /// + Segment = 2, - /// - /// Values vary by culture and segment. - /// - CultureAndSegment = Culture | Segment - } + /// + /// Values vary by culture and segment. + /// + CultureAndSegment = Culture | Segment, } diff --git a/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs index 5fa0e98958..7d7cc6c578 100644 --- a/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs +++ b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs @@ -1,17 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class ContentVersionCleanupPolicySettings { - public class ContentVersionCleanupPolicySettings - { - public int ContentTypeId { get; set; } + public int ContentTypeId { get; set; } - public bool PreventCleanup { get; set; } + public bool PreventCleanup { get; set; } - public int? KeepAllVersionsNewerThanDays { get; set; } + public int? KeepAllVersionsNewerThanDays { get; set; } - public int? KeepLatestVersionPerDayForDays { get; set; } + public int? KeepLatestVersionPerDayForDays { get; set; } - public DateTime Updated { get; set; } - } + public DateTime Updated { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentVersionMeta.cs b/src/Umbraco.Core/Models/ContentVersionMeta.cs index dbcd8540a0..cf95257716 100644 --- a/src/Umbraco.Core/Models/ContentVersionMeta.cs +++ b/src/Umbraco.Core/Models/ContentVersionMeta.cs @@ -1,45 +1,51 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class ContentVersionMeta { - public class ContentVersionMeta + public ContentVersionMeta() { - public int ContentId { get; } - public int ContentTypeId { get; } - public int VersionId { get; } - public int UserId { get; } - - public DateTime VersionDate { get; } - public bool CurrentPublishedVersion { get; } - public bool CurrentDraftVersion { get; } - public bool PreventCleanup { get; } - public string? Username { get; } - - public ContentVersionMeta() { } - - public ContentVersionMeta( - int versionId, - int contentId, - int contentTypeId, - int userId, - DateTime versionDate, - bool currentPublishedVersion, - bool currentDraftVersion, - bool preventCleanup, - string username) - { - VersionId = versionId; - ContentId = contentId; - ContentTypeId = contentTypeId; - - UserId = userId; - VersionDate = versionDate; - CurrentPublishedVersion = currentPublishedVersion; - CurrentDraftVersion = currentDraftVersion; - PreventCleanup = preventCleanup; - Username = username; - } - - public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}"; } + + public ContentVersionMeta( + int versionId, + int contentId, + int contentTypeId, + int userId, + DateTime versionDate, + bool currentPublishedVersion, + bool currentDraftVersion, + bool preventCleanup, + string username) + { + VersionId = versionId; + ContentId = contentId; + ContentTypeId = contentTypeId; + + UserId = userId; + VersionDate = versionDate; + CurrentPublishedVersion = currentPublishedVersion; + CurrentDraftVersion = currentDraftVersion; + PreventCleanup = preventCleanup; + Username = username; + } + + public int ContentId { get; } + + public int ContentTypeId { get; } + + public int VersionId { get; } + + public int UserId { get; } + + public DateTime VersionDate { get; } + + public bool CurrentPublishedVersion { get; } + + public bool CurrentDraftVersion { get; } + + public bool PreventCleanup { get; } + + public string? Username { get; } + + public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}"; } diff --git a/src/Umbraco.Core/Models/CultureImpact.cs b/src/Umbraco.Core/Models/CultureImpact.cs index fec02093d7..684f1d058c 100644 --- a/src/Umbraco.Core/Models/CultureImpact.cs +++ b/src/Umbraco.Core/Models/CultureImpact.cs @@ -1,258 +1,326 @@ -using System; -using System.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the impact of a culture set. +/// +/// +/// +/// A set of cultures can be either all cultures (including the invariant culture), or +/// the invariant culture, or a specific culture. +/// +/// +public sealed class CultureImpact { /// - /// Represents the impact of a culture set. + /// Initializes a new instance of the class. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + /// A value indicating if publishing invariant properties from non-default language. + internal CultureImpact(string? culture, bool isDefault = false, bool allowEditInvariantFromNonDefault = false) + { + if (culture != null && culture.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Culture \"\" is not valid here."); + } + + Culture = culture; + + if ((culture == null || culture == "*") && isDefault) + { + throw new ArgumentException("The invariant or 'all' culture can not be the default culture."); + } + + ImpactsOnlyDefaultCulture = isDefault; + + AllowEditInvariantFromNonDefault = allowEditInvariantFromNonDefault; + } + + [Flags] + public enum Behavior : byte + { + AllCultures = 1, + InvariantCulture = 2, + ExplicitCulture = 4, + InvariantProperties = 8, + } + + /// + /// Gets the impact of 'all' cultures (including the invariant culture). + /// + public static CultureImpact All { get; } = new("*"); + + /// + /// Gets the impact of the invariant culture. + /// + public static CultureImpact Invariant { get; } = new(null); + + /// + /// Gets the culture code. /// /// - /// A set of cultures can be either all cultures (including the invariant culture), or - /// the invariant culture, or a specific culture. + /// Can be null (invariant) or * (all cultures) or a specific culture code. /// - public sealed class CultureImpact + public string? Culture { get; } + + /// + /// Gets a value indicating whether this impact impacts all cultures, including, + /// indirectly, the invariant culture. + /// + public bool ImpactsAllCultures => Culture == "*"; + + /// + /// Gets a value indicating whether this impact impacts only the invariant culture, + /// directly, not because all cultures are impacted. + /// + public bool ImpactsOnlyInvariantCulture => Culture == null; + + /// + /// Gets a value indicating whether this impact impacts an implicit culture. + /// + /// + /// And then it does not impact the invariant culture. The impacted + /// explicit culture could be the default culture. + /// + public bool ImpactsExplicitCulture => Culture != null && Culture != "*"; + + /// + /// Gets a value indicating whether this impact impacts the default culture, directly, + /// not because all cultures are impacted. + /// + public bool ImpactsOnlyDefaultCulture { get; } + + /// + /// Gets a value indicating whether this impact impacts the invariant properties, either + /// directly, or because all cultures are impacted, or because the default culture is impacted. + /// + public bool ImpactsInvariantProperties => Culture == null || Culture == "*" || ImpactsOnlyDefaultCulture; + + /// + /// Gets a value indicating whether this also impact impacts the invariant properties, + /// even though it does not impact the invariant culture, neither directly (ImpactsInvariantCulture) + /// nor indirectly (ImpactsAllCultures). + /// + public bool ImpactsAlsoInvariantProperties => !ImpactsOnlyInvariantCulture && + !ImpactsAllCultures && + (ImpactsOnlyDefaultCulture || AllowEditInvariantFromNonDefault); + + public Behavior CultureBehavior { - /// - /// Utility method to return the culture used for invariant property errors based on what cultures are being actively saved, - /// the default culture and the state of the current content item - /// - /// - /// - /// - /// - public static string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures, string? defaultCulture) + get { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (savingCultures == null) throw new ArgumentNullException(nameof(savingCultures)); - if (savingCultures.Length == 0) throw new ArgumentException(nameof(savingCultures)); - - var cultureForInvariantErrors = savingCultures.Any(x => x.InvariantEquals(defaultCulture)) - //the default culture is being flagged for saving so use it - ? defaultCulture - //If the content has no published version, we need to affiliate validation with the first variant being saved. - //If the content has a published version we will not affiliate the validation with any culture (null) - : !content.Published ? savingCultures[0] : null; - - return cultureForInvariantErrors; - } - - /// - /// Initializes a new instance of the class. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - private CultureImpact(string? culture, bool isDefault = false) - { - if (culture != null && culture.IsNullOrWhiteSpace()) - throw new ArgumentException("Culture \"\" is not valid here."); - - Culture = culture; - - if ((culture == null || culture == "*") && isDefault) - throw new ArgumentException("The invariant or 'all' culture can not be the default culture."); - - ImpactsOnlyDefaultCulture = isDefault; - } - - /// - /// Gets the impact of 'all' cultures (including the invariant culture). - /// - public static CultureImpact All { get; } = new CultureImpact("*"); - - /// - /// Gets the impact of the invariant culture. - /// - public static CultureImpact Invariant { get; } = new CultureImpact(null); - - /// - /// Creates an impact instance representing the impact of a specific culture. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - public static CultureImpact Explicit(string? culture, bool isDefault) - { - if (culture == null) - throw new ArgumentException("Culture is not explicit."); - if (culture.IsNullOrWhiteSpace()) - throw new ArgumentException("Culture \"\" is not explicit."); - if (culture == "*") - throw new ArgumentException("Culture \"*\" is not explicit."); - - return new CultureImpact(culture, isDefault); - } - - /// - /// Creates an impact instance representing the impact of a culture set, - /// in the context of a content item variation. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - /// The content item. - /// - /// Validates that the culture is compatible with the variation. - /// - public static CultureImpact? Create(string culture, bool isDefault, IContent content) - { - // throws if not successful - TryCreate(culture, isDefault, content.ContentType.Variations, true, out var impact); - return impact; - } - - /// - /// Tries to create an impact instance representing the impact of a culture set, - /// in the context of a content item variation. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - /// A content variation. - /// A value indicating whether to throw if the impact cannot be created. - /// The impact if it could be created, otherwise null. - /// A value indicating whether the impact could be created. - /// - /// Validates that the culture is compatible with the variation. - /// - internal static bool TryCreate(string culture, bool isDefault, ContentVariation variation, bool throwOnFail, out CultureImpact? impact) - { - impact = null; - - // if culture is invariant... - if (culture == null) + // null can only be invariant + if (Culture == null) { - // ... then variation must not vary by culture ... - if (variation.VariesByCulture()) - { - if (throwOnFail) - throw new InvalidOperationException("The invariant culture is not compatible with a varying variation."); - return false; - } - - // ... and it cannot be default - if (isDefault) - { - if (throwOnFail) - throw new InvalidOperationException("The invariant culture can not be the default culture."); - return false; - } - - impact = Invariant; - return true; + return Behavior.InvariantCulture | Behavior.InvariantProperties; } - // if culture is 'all'... - if (culture == "*") + // * is All which means its also invariant properties since this will include the default language + if (Culture == "*") { - // ... it cannot be default - if (isDefault) - { - if (throwOnFail) - throw new InvalidOperationException("The 'all' culture can not be the default culture."); - return false; - } - - // if variation does not vary by culture, then impact is invariant - impact = variation.VariesByCulture() ? All : Invariant; - return true; + return Behavior.AllCultures | Behavior.InvariantProperties; } - // neither null nor "*" - cannot be the empty string - if (culture.IsNullOrWhiteSpace()) + // else it's explicit + Behavior result = Behavior.ExplicitCulture; + + // if the explicit culture is the default, then the behavior is also InvariantProperties + if (ImpactsOnlyDefaultCulture) + { + result |= Behavior.InvariantProperties; + } + + return result; + } + } + + /// + /// Utility method to return the culture used for invariant property errors based on what cultures are being actively + /// saved, + /// the default culture and the state of the current content item + /// + /// + /// + /// + /// + public static string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures, + string? defaultCulture) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (savingCultures == null) + { + throw new ArgumentNullException(nameof(savingCultures)); + } + + if (savingCultures.Length == 0) + { + throw new ArgumentException(nameof(savingCultures)); + } + + var cultureForInvariantErrors = savingCultures.Any(x => x.InvariantEquals(defaultCulture)) + + // the default culture is being flagged for saving so use it + ? defaultCulture + + // If the content has no published version, we need to affiliate validation with the first variant being saved. + // If the content has a published version we will not affiliate the validation with any culture (null) + : !content.Published + ? savingCultures[0] + : null; + + return cultureForInvariantErrors; + } + + /// + /// Creates an impact instance representing the impact of a specific culture. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + /// A value indicating if publishing invariant properties from non-default language. + [Obsolete("Use ICultureImpactService instead.")] + public static CultureImpact Explicit(string? culture, bool isDefault) + { + if (culture == null) + { + throw new ArgumentException("Culture is not explicit."); + } + + if (culture.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Culture \"\" is not explicit."); + } + + if (culture == "*") + { + throw new ArgumentException("Culture \"*\" is not explicit."); + } + + return new CultureImpact(culture, isDefault); + } + + /// + /// Creates an impact instance representing the impact of a culture set, + /// in the context of a content item variation. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + /// The content item. + /// A value indicating if publishing invariant properties from non-default language. + /// + /// Validates that the culture is compatible with the variation. + /// + [Obsolete("Use ICultureImpactService instead, scheduled for removal in V12")] + public static CultureImpact? Create(string culture, bool isDefault, IContent content) + { + // throws if not successful + TryCreate(culture, isDefault, content.ContentType.Variations, true, false, out CultureImpact? impact); + return impact; + } + + /// + /// Tries to create an impact instance representing the impact of a culture set, + /// in the context of a content item variation. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + /// A content variation. + /// A value indicating whether to throw if the impact cannot be created. + /// A value indicating if publishing invariant properties from non-default language. + /// The impact if it could be created, otherwise null. + /// A value indicating whether the impact could be created. + /// + /// Validates that the culture is compatible with the variation. + /// + // Remove this once Create() can be removed (V12), this already lives in CultureImpactFactory + [Obsolete("Please use the CultureImpactFactory instead, scheduled for removal in v12")] + internal static bool TryCreate(string culture, bool isDefault, ContentVariation variation, bool throwOnFail, + bool editInvariantFromNonDefault, out CultureImpact? impact) + { + impact = null; + + // if culture is invariant... + if (culture == null) + { + // ... then variation must not vary by culture ... + if (variation.VariesByCulture()) { if (throwOnFail) - throw new ArgumentException("Cannot be the empty string.", nameof(culture)); + { + throw new InvalidOperationException( + "The invariant culture is not compatible with a varying variation."); + } + return false; } - // if culture is specific, then variation must vary - if (!variation.VariesByCulture()) + // ... and it cannot be default + if (isDefault) { if (throwOnFail) - throw new InvalidOperationException($"The variant culture {culture} is not compatible with an invariant variation."); + { + throw new InvalidOperationException("The invariant culture can not be the default culture."); + } + return false; } - // return specific impact - impact = new CultureImpact(culture, isDefault); + impact = Invariant; return true; } - /// - /// Gets the culture code. - /// - /// - /// Can be null (invariant) or * (all cultures) or a specific culture code. - /// - public string? Culture { get; } - - /// - /// Gets a value indicating whether this impact impacts all cultures, including, - /// indirectly, the invariant culture. - /// - public bool ImpactsAllCultures => Culture == "*"; - - /// - /// Gets a value indicating whether this impact impacts only the invariant culture, - /// directly, not because all cultures are impacted. - /// - public bool ImpactsOnlyInvariantCulture => Culture == null; - - /// - /// Gets a value indicating whether this impact impacts an implicit culture. - /// - /// And then it does not impact the invariant culture. The impacted - /// explicit culture could be the default culture. - public bool ImpactsExplicitCulture => Culture != null && Culture != "*"; - - /// - /// Gets a value indicating whether this impact impacts the default culture, directly, - /// not because all cultures are impacted. - /// - public bool ImpactsOnlyDefaultCulture {get; } - - /// - /// Gets a value indicating whether this impact impacts the invariant properties, either - /// directly, or because all cultures are impacted, or because the default culture is impacted. - /// - public bool ImpactsInvariantProperties => Culture == null || Culture == "*" || ImpactsOnlyDefaultCulture; - - /// - /// Gets a value indicating whether this also impact impacts the invariant properties, - /// even though it does not impact the invariant culture, neither directly (ImpactsInvariantCulture) - /// nor indirectly (ImpactsAllCultures). - /// - public bool ImpactsAlsoInvariantProperties => !ImpactsOnlyInvariantCulture && - !ImpactsAllCultures && - ImpactsOnlyDefaultCulture; - - public Behavior CultureBehavior + // if culture is 'all'... + if (culture == "*") { - get + // ... it cannot be default + if (isDefault) { - //null can only be invariant - if (Culture == null) return Behavior.InvariantCulture | Behavior.InvariantProperties; + if (throwOnFail) + { + throw new InvalidOperationException("The 'all' culture can not be the default culture."); + } - // * is All which means its also invariant properties since this will include the default language - if (Culture == "*") return (Behavior.AllCultures | Behavior.InvariantProperties); - - //else it's explicit - var result = Behavior.ExplicitCulture; - - //if the explicit culture is the default, then the behavior is also InvariantProperties - if (ImpactsOnlyDefaultCulture) - result |= Behavior.InvariantProperties; - - return result; + return false; } + + // if variation does not vary by culture, then impact is invariant + impact = variation.VariesByCulture() ? All : Invariant; + return true; } - - [Flags] - public enum Behavior : byte + // neither null nor "*" - cannot be the empty string + if (culture.IsNullOrWhiteSpace()) { - AllCultures = 1, - InvariantCulture = 2, - ExplicitCulture = 4, - InvariantProperties = 8 + if (throwOnFail) + { + throw new ArgumentException("Cannot be the empty string.", nameof(culture)); + } + + return false; } + + // if culture is specific, then variation must vary + if (!variation.VariesByCulture()) + { + if (throwOnFail) + { + throw new InvalidOperationException( + $"The variant culture {culture} is not compatible with an invariant variation."); + } + + return false; + } + + // return specific impact + impact = new CultureImpact(culture, isDefault, editInvariantFromNonDefault); + return true; } + + + public bool AllowEditInvariantFromNonDefault { get; } } diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index 6b33f07385..630ef338bd 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -1,196 +1,224 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +[Serializable] +[DataContract(IsReference = true)] +public class DataType : TreeEntityBase, IDataType { + private readonly IConfigurationEditorJsonSerializer _serializer; + private object? _configuration; + private string? _configurationJson; + private ValueStorageType _databaseType; + private IDataEditor? _editor; + private bool _hasConfiguration; + /// - /// Implements . + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class DataType : TreeEntityBase, IDataType + public DataType(IDataEditor? editor, IConfigurationEditorJsonSerializer serializer, int parentId = -1) { - private IDataEditor? _editor; - private ValueStorageType _databaseType; - private readonly IConfigurationEditorJsonSerializer _serializer; - private object? _configuration; - private bool _hasConfiguration; - private string? _configurationJson; + _editor = editor ?? throw new ArgumentNullException(nameof(editor)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(editor)); + ParentId = parentId; - /// - /// Initializes a new instance of the class. - /// - public DataType(IDataEditor? editor, IConfigurationEditorJsonSerializer serializer, int parentId = -1) + // set a default configuration + Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject; + } + + /// + [IgnoreDataMember] + public IDataEditor? Editor + { + get => _editor; + set { - _editor = editor ?? throw new ArgumentNullException(nameof(editor)); - _serializer = serializer ?? throw new ArgumentNullException(nameof(editor)); - ParentId = parentId; - - // set a default configuration - Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject; - } - - /// - [IgnoreDataMember] - public IDataEditor? Editor - { - get => _editor; - set + // ignore if no change + if (_editor?.Alias == value?.Alias) { - // ignore if no change - if (_editor?.Alias == value?.Alias) return; - OnPropertyChanged(nameof(Editor)); - - // try to map the existing configuration to the new configuration - // simulate saving to db and reloading (ie go via json) - var configuration = Configuration; - var json = _serializer.Serialize(configuration); - _editor = value; - - try - { - Configuration = _editor?.GetConfigurationEditor().FromDatabase(json, _serializer); - } - catch (Exception e) - { - throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); - } + return; } - } - /// - [DataMember] - public string EditorAlias => _editor?.Alias ?? string.Empty; + OnPropertyChanged(nameof(Editor)); - /// - [DataMember] - public ValueStorageType DatabaseType - { - get => _databaseType; - set => SetPropertyValueAndDetectChanges(value, ref _databaseType, nameof(DatabaseType)); - } + // try to map the existing configuration to the new configuration + // simulate saving to db and reloading (ie go via json) + var configuration = Configuration; + var json = _serializer.Serialize(configuration); + _editor = value; - /// - [DataMember] - public object? Configuration - { - get + try { - // if we know we have a configuration (which may be null), return it - // if we don't have an editor, then we have no configuration, return null - // else, use the editor to get the configuration object - - if (_hasConfiguration) return _configuration; - - try - { - _configuration = _editor?.GetConfigurationEditor().FromDatabase(_configurationJson, _serializer); - } - catch (Exception e) - { - throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); - } - - _hasConfiguration = true; - _configurationJson = null; - - return _configuration; + Configuration = _editor?.GetConfigurationEditor().FromDatabase(json, _serializer); } - set + catch (Exception e) { - if (value == null) - throw new ArgumentNullException(nameof(value)); - - // we don't support re-assigning the same object - // configurations are kinda non-mutable, mainly because detecting changes would be a pain - if (_configuration == value) // reference comparison - throw new ArgumentException("Configurations are kinda non-mutable. Do not reassign the same object.", nameof(value)); - - // validate configuration type - if (!_editor?.GetConfigurationEditor().IsConfiguration(value) ?? true) - throw new ArgumentException($"Value of type {value.GetType().Name} cannot be a configuration for editor {_editor?.Alias}, expecting.", nameof(value)); - - // extract database type from configuration object, if appropriate - if (value is IConfigureValueType valueTypeConfiguration) - DatabaseType = ValueTypes.ToStorageType(valueTypeConfiguration.ValueType); - - // extract database type from dictionary, if appropriate - if (value is IDictionary dictionaryConfiguration - && dictionaryConfiguration.TryGetValue(Constants.PropertyEditors.ConfigurationKeys.DataValueType, out var valueTypeObject) - && valueTypeObject is string valueTypeString - && ValueTypes.IsValue(valueTypeString)) - DatabaseType = ValueTypes.ToStorageType(valueTypeString); - - _configuration = value; - _hasConfiguration = true; - _configurationJson = null; - - // it's always a change - OnPropertyChanged(nameof(Configuration)); - } - } - - /// - /// Lazily set the configuration as a serialized json string. - /// - /// - /// Will be de-serialized on-demand. - /// This method is meant to be used when building entities from database, exclusively. - /// It does NOT register a property change to dirty. It ignores the fact that the configuration - /// may contain the database type, because the datatype DTO should also contain that database - /// type, and they should be the same. - /// Think before using! - /// - public void SetLazyConfiguration(string? configurationJson) - { - _hasConfiguration = false; - _configuration = null; - _configurationJson = configurationJson; - } - - /// - /// Gets a lazy configuration. - /// - /// - /// The configuration object will be lazily de-serialized. - /// This method is meant to be used when creating published datatypes, exclusively. - /// Think before using! - /// - internal Lazy GetLazyConfiguration() - { - // note: in both cases, make sure we capture what we need - we don't want - // to capture a reference to this full, potentially heavy, DataType instance. - - if (_hasConfiguration) - { - // if configuration has already been de-serialized, return - var capturedConfiguration = _configuration; - return new Lazy(() => capturedConfiguration); - } - else - { - // else, create a Lazy de-serializer - var capturedConfiguration = _configurationJson; - var capturedEditor = _editor; - return new Lazy(() => - { - try - { - return capturedEditor?.GetConfigurationEditor().FromDatabase(capturedConfiguration, _serializer); - } - catch (Exception e) - { - throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); - } - }); + throw new InvalidOperationException( + $"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + e); } } } + + /// + [DataMember] + public string EditorAlias => _editor?.Alias ?? string.Empty; + + /// + [DataMember] + public ValueStorageType DatabaseType + { + get => _databaseType; + set => SetPropertyValueAndDetectChanges(value, ref _databaseType, nameof(DatabaseType)); + } + + /// + [DataMember] + public object? Configuration + { + get + { + // if we know we have a configuration (which may be null), return it + // if we don't have an editor, then we have no configuration, return null + // else, use the editor to get the configuration object + if (_hasConfiguration) + { + return _configuration; + } + + try + { + _configuration = _editor?.GetConfigurationEditor().FromDatabase(_configurationJson, _serializer); + } + catch (Exception e) + { + throw new InvalidOperationException( + $"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + e); + } + + _hasConfiguration = true; + _configurationJson = null; + + return _configuration; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // we don't support re-assigning the same object + // configurations are kinda non-mutable, mainly because detecting changes would be a pain + // reference comparison + if (_configuration == value) + { + throw new ArgumentException( + "Configurations are kinda non-mutable. Do not reassign the same object.", + nameof(value)); + } + + // validate configuration type + if (!_editor?.GetConfigurationEditor().IsConfiguration(value) ?? true) + { + throw new ArgumentException( + $"Value of type {value.GetType().Name} cannot be a configuration for editor {_editor?.Alias}, expecting.", + nameof(value)); + } + + // extract database type from configuration object, if appropriate + if (value is IConfigureValueType valueTypeConfiguration) + { + DatabaseType = ValueTypes.ToStorageType(valueTypeConfiguration.ValueType); + } + + // extract database type from dictionary, if appropriate + if (value is IDictionary dictionaryConfiguration + && dictionaryConfiguration.TryGetValue( + Constants.PropertyEditors.ConfigurationKeys.DataValueType, + out var valueTypeObject) + && valueTypeObject is string valueTypeString + && ValueTypes.IsValue(valueTypeString)) + { + DatabaseType = ValueTypes.ToStorageType(valueTypeString); + } + + _configuration = value; + _hasConfiguration = true; + _configurationJson = null; + + // it's always a change + OnPropertyChanged(nameof(Configuration)); + } + } + + /// + /// Lazily set the configuration as a serialized json string. + /// + /// + /// Will be de-serialized on-demand. + /// + /// This method is meant to be used when building entities from database, exclusively. + /// It does NOT register a property change to dirty. It ignores the fact that the configuration + /// may contain the database type, because the datatype DTO should also contain that database + /// type, and they should be the same. + /// + /// Think before using! + /// + public void SetLazyConfiguration(string? configurationJson) + { + _hasConfiguration = false; + _configuration = null; + _configurationJson = configurationJson; + } + + /// + /// Gets a lazy configuration. + /// + /// + /// The configuration object will be lazily de-serialized. + /// This method is meant to be used when creating published datatypes, exclusively. + /// Think before using! + /// + internal Lazy GetLazyConfiguration() + { + // note: in both cases, make sure we capture what we need - we don't want + // to capture a reference to this full, potentially heavy, DataType instance. + if (_hasConfiguration) + { + // if configuration has already been de-serialized, return + var capturedConfiguration = _configuration; + return new Lazy(() => capturedConfiguration); + } + else + { + // else, create a Lazy de-serializer + var capturedConfiguration = _configurationJson; + IDataEditor? capturedEditor = _editor; + return new Lazy(() => + { + try + { + return capturedEditor?.GetConfigurationEditor().FromDatabase(capturedConfiguration, _serializer); + } + catch (Exception e) + { + throw new InvalidOperationException( + $"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + e); + } + }); + } + } } diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index 10419dca88..791f7b248b 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -1,95 +1,88 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions methods for . +/// +public static class DataTypeExtensions { - /// - /// Provides extensions methods for . - /// - public static class DataTypeExtensions + private static readonly ISet IdsOfBuildInDataTypes = new HashSet { - /// - /// Gets the configuration object. - /// - /// The expected type of the configuration object. - /// This datatype. - /// When the datatype configuration is not of the expected type. - public static T? ConfigurationAs(this IDataType dataType) - where T : class + Constants.DataTypes.Guids.ContentPickerGuid, + Constants.DataTypes.Guids.MemberPickerGuid, + Constants.DataTypes.Guids.MediaPickerGuid, + Constants.DataTypes.Guids.MultipleMediaPickerGuid, + Constants.DataTypes.Guids.RelatedLinksGuid, + Constants.DataTypes.Guids.MemberGuid, + Constants.DataTypes.Guids.ImageCropperGuid, + Constants.DataTypes.Guids.TagsGuid, + Constants.DataTypes.Guids.ListViewContentGuid, + Constants.DataTypes.Guids.ListViewMediaGuid, + Constants.DataTypes.Guids.ListViewMembersGuid, + Constants.DataTypes.Guids.DatePickerWithTimeGuid, + Constants.DataTypes.Guids.ApprovedColorGuid, + Constants.DataTypes.Guids.DropdownMultipleGuid, + Constants.DataTypes.Guids.RadioboxGuid, + Constants.DataTypes.Guids.DatePickerGuid, + Constants.DataTypes.Guids.DropdownGuid, + Constants.DataTypes.Guids.CheckboxListGuid, + Constants.DataTypes.Guids.CheckboxGuid, + Constants.DataTypes.Guids.NumericGuid, + Constants.DataTypes.Guids.RichtextEditorGuid, + Constants.DataTypes.Guids.TextstringGuid, + Constants.DataTypes.Guids.TextareaGuid, + Constants.DataTypes.Guids.UploadGuid, + Constants.DataTypes.Guids.UploadArticleGuid, + Constants.DataTypes.Guids.UploadAudioGuid, + Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Constants.DataTypes.Guids.UploadVideoGuid, + Constants.DataTypes.Guids.LabelStringGuid, + Constants.DataTypes.Guids.LabelDecimalGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + Constants.DataTypes.Guids.LabelBigIntGuid, + Constants.DataTypes.Guids.LabelTimeGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + }; + + /// + /// Gets the configuration object. + /// + /// The expected type of the configuration object. + /// This datatype. + /// When the datatype configuration is not of the expected type. + public static T? ConfigurationAs(this IDataType dataType) + where T : class + { + if (dataType == null) { - if (dataType == null) - throw new ArgumentNullException(nameof(dataType)); - - var configuration = dataType.Configuration; - - switch (configuration) - { - case null: - return null; - case T configurationAsT: - return configurationAsT; - } - - throw new InvalidCastException($"Cannot cast dataType configuration, of type {configuration.GetType().Name}, to {typeof(T).Name}."); + throw new ArgumentNullException(nameof(dataType)); } - private static readonly ISet IdsOfBuildInDataTypes = new HashSet() - { - Constants.DataTypes.Guids.ContentPickerGuid, - Constants.DataTypes.Guids.MemberPickerGuid, - Constants.DataTypes.Guids.MediaPickerGuid, - Constants.DataTypes.Guids.MultipleMediaPickerGuid, - Constants.DataTypes.Guids.RelatedLinksGuid, - Constants.DataTypes.Guids.MemberGuid, - Constants.DataTypes.Guids.ImageCropperGuid, - Constants.DataTypes.Guids.TagsGuid, - Constants.DataTypes.Guids.ListViewContentGuid, - Constants.DataTypes.Guids.ListViewMediaGuid, - Constants.DataTypes.Guids.ListViewMembersGuid, - Constants.DataTypes.Guids.DatePickerWithTimeGuid, - Constants.DataTypes.Guids.ApprovedColorGuid, - Constants.DataTypes.Guids.DropdownMultipleGuid, - Constants.DataTypes.Guids.RadioboxGuid, - Constants.DataTypes.Guids.DatePickerGuid, - Constants.DataTypes.Guids.DropdownGuid, - Constants.DataTypes.Guids.CheckboxListGuid, - Constants.DataTypes.Guids.CheckboxGuid, - Constants.DataTypes.Guids.NumericGuid, - Constants.DataTypes.Guids.RichtextEditorGuid, - Constants.DataTypes.Guids.TextstringGuid, - Constants.DataTypes.Guids.TextareaGuid, - Constants.DataTypes.Guids.UploadGuid, - Constants.DataTypes.Guids.UploadArticleGuid, - Constants.DataTypes.Guids.UploadAudioGuid, - Constants.DataTypes.Guids.UploadVectorGraphicsGuid, - Constants.DataTypes.Guids.UploadVideoGuid, - Constants.DataTypes.Guids.LabelStringGuid, - Constants.DataTypes.Guids.LabelDecimalGuid, - Constants.DataTypes.Guids.LabelDateTimeGuid, - Constants.DataTypes.Guids.LabelBigIntGuid, - Constants.DataTypes.Guids.LabelTimeGuid, - Constants.DataTypes.Guids.LabelDateTimeGuid, - }; + var configuration = dataType.Configuration; - /// - /// Returns true if this date type is build-in/default. - /// - /// The data type definition. - /// - public static bool IsBuildInDataType(this IDataType dataType) + switch (configuration) { - return IsBuildInDataType(dataType.Key); - } - - /// - /// Returns true if this date type is build-in/default. - /// - public static bool IsBuildInDataType(Guid key) - { - return IdsOfBuildInDataTypes.Contains(key); + case null: + return null; + case T configurationAsT: + return configurationAsT; } + throw new InvalidCastException( + $"Cannot cast dataType configuration, of type {configuration.GetType().Name}, to {typeof(T).Name}."); } + + /// + /// Returns true if this date type is build-in/default. + /// + /// The data type definition. + /// + public static bool IsBuildInDataType(this IDataType dataType) => IsBuildInDataType(dataType.Key); + + /// + /// Returns true if this date type is build-in/default. + /// + public static bool IsBuildInDataType(Guid key) => IdsOfBuildInDataTypes.Contains(key); } diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index 4dc293641c..ce34dab6f1 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -1,205 +1,215 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Models -{ - public static class DeepCloneHelper - { - /// - /// Stores the metadata for the properties for a given type so we know how to create them - /// - private struct ClonePropertyInfo - { - public ClonePropertyInfo(PropertyInfo propertyInfo) : this() - { - if (propertyInfo == null) throw new ArgumentNullException("propertyInfo"); - PropertyInfo = propertyInfo; - } +namespace Umbraco.Cms.Core.Models; - public PropertyInfo PropertyInfo { get; private set; } - public bool IsDeepCloneable { get; set; } - public Type? GenericListType { get; set; } - public bool IsList - { - get { return GenericListType != null; } - } +public static class DeepCloneHelper +{ + /// + /// Used to avoid constant reflection (perf) + /// + private static readonly ConcurrentDictionary PropCache = new(); + + /// + /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the + /// outcome is 'output') + /// + /// + /// + /// + public static void DeepCloneRefProperties(IDeepCloneable input, IDeepCloneable output) + { + Type inputType = input.GetType(); + Type outputType = output.GetType(); + + if (inputType != outputType) + { + throw new InvalidOperationException("Both the input and output types must be the same"); } - /// - /// Used to avoid constant reflection (perf) - /// - private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); + // get the property metadata from cache so we only have to figure this out once per type + ClonePropertyInfo[] refProperties = PropCache.GetOrAdd(inputType, type => + inputType.GetProperties() + .Select(propertyInfo => + { + if ( - /// - /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the outcome is 'output') - /// - /// - /// - /// - public static void DeepCloneRefProperties(IDeepCloneable input, IDeepCloneable output) - { - var inputType = input.GetType(); - var outputType = output.GetType(); + // is not attributed with the ignore clone attribute + propertyInfo.GetCustomAttribute() != null - if (inputType != outputType) - { - throw new InvalidOperationException("Both the input and output types must be the same"); - } + // reference type but not string + || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof(string) - //get the property metadata from cache so we only have to figure this out once per type - var refProperties = PropCache.GetOrAdd(inputType, type => - inputType.GetProperties() - .Select(propertyInfo => + // settable + || propertyInfo.CanWrite == false + + // non-indexed + || propertyInfo.GetIndexParameters().Any()) { - if ( - //is not attributed with the ignore clone attribute - propertyInfo.GetCustomAttribute() != null - //reference type but not string - || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof (string) - //settable - || propertyInfo.CanWrite == false - //non-indexed - || propertyInfo.GetIndexParameters().Any()) + return null; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + { + return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) + && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + { + if (propertyInfo.PropertyType.IsGenericType + && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>))) + { + // if it is a IEnumerable<>, IReadOnlyCollection, IList or ICollection<> we'll use a List<> since it implements them all + Type genericType = + typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); + return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; + } + + if (propertyInfo.PropertyType.IsArray + || (propertyInfo.PropertyType.IsInterface && + propertyInfo.PropertyType.IsGenericType == false)) + { + // if its an array, we'll create a list to work with first and then convert to array later + // otherwise if its just a regular derivative of IEnumerable, we can use a list too + return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; + } + + // skip instead of trying to create instance of abstract or interface + if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) { return null; } - - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + // its a custom IEnumerable, we'll try to create it + try { - return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; - } + var custom = Activator.CreateInstance(propertyInfo.PropertyType); - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) - && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) - { - if (propertyInfo.PropertyType.IsGenericType - && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>))) - { - //if it is a IEnumerable<>, IReadOnlyCollection, IList or ICollection<> we'll use a List<> since it implements them all - var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); - return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; - } - if (propertyInfo.PropertyType.IsArray - || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) - { - //if its an array, we'll create a list to work with first and then convert to array later - //otherwise if its just a regular derivative of IEnumerable, we can use a list too - return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; - } - //skip instead of trying to create instance of abstract or interface - if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) + // if it's an IList we can work with it, otherwise we cannot + if (custom is not IList) { return null; } - //its a custom IEnumerable, we'll try to create it - try - { - var custom = Activator.CreateInstance(propertyInfo.PropertyType); - //if it's an IList we can work with it, otherwise we cannot - var newList = custom as IList; - if (newList == null) - { - return null; - } - return new ClonePropertyInfo(propertyInfo) {GenericListType = propertyInfo.PropertyType}; - } - catch (Exception) - { - //could not create this type so we'll skip it - return null; - } + return new ClonePropertyInfo(propertyInfo) { GenericListType = propertyInfo.PropertyType }; } - return new ClonePropertyInfo(propertyInfo); - }) - .Where(x => x.HasValue) - .Select(x => x!.Value) - .ToArray()); + catch (Exception) + { + // could not create this type so we'll skip it + return null; + } + } - foreach (var clonePropertyInfo in refProperties) + return new ClonePropertyInfo(propertyInfo); + }) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .ToArray()); + + foreach (ClonePropertyInfo clonePropertyInfo in refProperties) + { + if (clonePropertyInfo.IsDeepCloneable) { - if (clonePropertyInfo.IsDeepCloneable) - { - //this ref property is also deep cloneable so clone it - var result = (IDeepCloneable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); + // this ref property is also deep cloneable so clone it + var result = (IDeepCloneable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); - if (result != null) - { - //set the cloned value to the property - clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); - } + if (result != null) + { + // set the cloned value to the property + clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); } - else if (clonePropertyInfo.IsList) + } + else if (clonePropertyInfo.IsList) + { + var enumerable = (IEnumerable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); + if (enumerable == null) { - var enumerable = (IEnumerable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); - if (enumerable == null) continue; + continue; + } - var newList = clonePropertyInfo.GenericListType is not null ? (IList?)Activator.CreateInstance(clonePropertyInfo.GenericListType) : null; + IList? newList = clonePropertyInfo.GenericListType is not null + ? (IList?)Activator.CreateInstance(clonePropertyInfo.GenericListType) + : null; - var isUsableType = true; + var isUsableType = true; - //now clone each item - foreach (var o in enumerable) + // now clone each item + foreach (var o in enumerable) + { + // first check if the item is deep cloneable and copy that way + if (o is IDeepCloneable dc) { - //first check if the item is deep cloneable and copy that way - var dc = o as IDeepCloneable; - if (dc != null) - { - newList?.Add(dc.DeepClone()); - } - else if (o is string || o.GetType().IsValueType) - { - //check if the item is a value type or a string, then we can just use it - newList?.Add(o); - } - else - { - //this will occur if the item is not a string or value type or IDeepCloneable, in this case we cannot - // clone each element, we'll need to skip this property, people will have to manually clone this list - isUsableType = false; - break; - } + newList?.Add(dc.DeepClone()); } - - //if this was not usable, skip this property - if (isUsableType == false) + else if (o is string || o.GetType().IsValueType) { - continue; - } - - if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) - { - //need to convert to array - var arr = (object?[]?)Activator.CreateInstance(clonePropertyInfo.PropertyInfo.PropertyType, newList?.Count ?? 0); - for (int i = 0; i < newList?.Count; i++) - { - if (arr != null) - { - arr[i] = newList[i]; - } - } - - //set the cloned collection - clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); + // check if the item is a value type or a string, then we can just use it + newList?.Add(o); } else { - //set the cloned collection - clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); + // this will occur if the item is not a string or value type or IDeepCloneable, in this case we cannot + // clone each element, we'll need to skip this property, people will have to manually clone this list + isUsableType = false; + break; + } + } + + // if this was not usable, skip this property + if (isUsableType == false) + { + continue; + } + + if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) + { + // need to convert to array + var arr = (object?[]?)Activator.CreateInstance( + clonePropertyInfo.PropertyInfo.PropertyType, + newList?.Count ?? 0); + for (var i = 0; i < newList?.Count; i++) + { + if (arr != null) + { + arr[i] = newList[i]; + } } + // set the cloned collection + clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); + } + else + { + // set the cloned collection + clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); } } } + } + /// + /// Stores the metadata for the properties for a given type so we know how to create them + /// + private struct ClonePropertyInfo + { + public ClonePropertyInfo(PropertyInfo propertyInfo) + : this() + { + PropertyInfo = propertyInfo ?? throw new ArgumentNullException("propertyInfo"); + } + + public PropertyInfo PropertyInfo { get; } + + public bool IsDeepCloneable { get; set; } + + public Type? GenericListType { get; set; } + + public bool IsList => GenericListType != null; } } diff --git a/src/Umbraco.Core/Models/DictionaryImportModel.cs b/src/Umbraco.Core/Models/DictionaryImportModel.cs new file mode 100644 index 0000000000..2507a6a1ec --- /dev/null +++ b/src/Umbraco.Core/Models/DictionaryImportModel.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract(Name = "dictionaryImportModel")] + public class DictionaryImportModel + { + [DataMember(Name = "dictionaryItems")] + public List? DictionaryItems { get; set; } + + [DataMember(Name = "tempFileName")] + public string? TempFileName { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 14cd3bb2e5..7473cef60f 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -1,83 +1,82 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Dictionary Item +/// +[Serializable] +[DataContract(IsReference = true)] +public class DictionaryItem : EntityBase, IDictionaryItem { - /// - /// Represents a Dictionary Item - /// - [Serializable] - [DataContract(IsReference = true)] - public class DictionaryItem : EntityBase, IDictionaryItem - { - public Func? GetLanguage { get; set; } - private Guid? _parentId; - private string _itemKey; - private IEnumerable _translations; - - public DictionaryItem(string itemKey) - : this(null, itemKey) - {} - - public DictionaryItem(Guid? parentId, string itemKey) - { - _parentId = parentId; - _itemKey = itemKey; - _translations = new List(); - } - - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> DictionaryTranslationComparer = - new DelegateEqualityComparer>( + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> + DictionaryTranslationComparer = + new( (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), enumerable => enumerable.GetHashCode()); - /// - /// Gets or Sets the Parent Id of the Dictionary Item - /// - [DataMember] - public Guid? ParentId - { - get { return _parentId; } - set { SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); } - } + private string _itemKey; + private Guid? _parentId; + private IEnumerable _translations; - /// - /// Gets or sets the Key for the Dictionary Item - /// - [DataMember] - public string ItemKey - { - get { return _itemKey; } - set { SetPropertyValueAndDetectChanges(value, ref _itemKey!, nameof(ItemKey)); } - } + public DictionaryItem(string itemKey) + : this(null, itemKey) + { + } - /// - /// Gets or sets a list of translations for the Dictionary Item - /// - [DataMember] - public IEnumerable Translations + public DictionaryItem(Guid? parentId, string itemKey) + { + _parentId = parentId; + _itemKey = itemKey; + _translations = new List(); + } + + public Func? GetLanguage { get; set; } + + /// + /// Gets or Sets the Parent Id of the Dictionary Item + /// + [DataMember] + public Guid? ParentId + { + get => _parentId; + set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + } + + /// + /// Gets or sets the Key for the Dictionary Item + /// + [DataMember] + public string ItemKey + { + get => _itemKey; + set => SetPropertyValueAndDetectChanges(value, ref _itemKey!, nameof(ItemKey)); + } + + /// + /// Gets or sets a list of translations for the Dictionary Item + /// + [DataMember] + public IEnumerable Translations + { + get => _translations; + set { - get { return _translations; } - set + IDictionaryTranslation[] asArray = value.ToArray(); + + // ensure the language callback is set on each translation + if (GetLanguage != null) { - var asArray = value?.ToArray(); - //ensure the language callback is set on each translation - if (GetLanguage != null && asArray is not null) + foreach (DictionaryTranslation translation in asArray.OfType()) { - foreach (var translation in asArray.OfType()) - { - translation.GetLanguage = GetLanguage; - } + translation.GetLanguage = GetLanguage; } - - SetPropertyValueAndDetectChanges(asArray, ref _translations!, nameof(Translations), - DictionaryTranslationComparer); } + + SetPropertyValueAndDetectChanges(asArray, ref _translations!, nameof(Translations), DictionaryTranslationComparer); } } } diff --git a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs index 137680aa27..3e6c051201 100644 --- a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs +++ b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs @@ -1,31 +1,29 @@ -using System.Linq; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions -{ - public static class DictionaryItemExtensions - { - /// - /// Returns the translation value for the language id, if no translation is found it returns an empty string - /// - /// - /// - /// - public static string? GetTranslatedValue(this IDictionaryItem d, int languageId) - { - var trans = d.Translations?.FirstOrDefault(x => x.LanguageId == languageId); - return trans == null ? string.Empty : trans.Value; - } +namespace Umbraco.Extensions; - /// - /// Returns the default translated value based on the default language - /// - /// - /// - public static string? GetDefaultValue(this IDictionaryItem d) - { - var defaultTranslation = d.Translations?.FirstOrDefault(x => x.Language?.Id == 1); - return defaultTranslation == null ? string.Empty : defaultTranslation.Value; - } +public static class DictionaryItemExtensions +{ + /// + /// Returns the translation value for the language id, if no translation is found it returns an empty string + /// + /// + /// + /// + public static string? GetTranslatedValue(this IDictionaryItem d, int languageId) + { + IDictionaryTranslation? trans = d.Translations.FirstOrDefault(x => x.LanguageId == languageId); + return trans == null ? string.Empty : trans.Value; + } + + /// + /// Returns the default translated value based on the default language + /// + /// + /// + public static string? GetDefaultValue(this IDictionaryItem d) + { + IDictionaryTranslation? defaultTranslation = d.Translations.FirstOrDefault(x => x.Language?.Id == 1); + return defaultTranslation == null ? string.Empty : defaultTranslation.Value; } } diff --git a/src/Umbraco.Core/Models/DictionaryPreviewImportModel.cs b/src/Umbraco.Core/Models/DictionaryPreviewImportModel.cs new file mode 100644 index 0000000000..530d49b013 --- /dev/null +++ b/src/Umbraco.Core/Models/DictionaryPreviewImportModel.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract(Name = "dictionaryPreviewImportModel")] + public class DictionaryPreviewImportModel + { + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "level")] + public int Level { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/DictionaryTranslation.cs b/src/Umbraco.Core/Models/DictionaryTranslation.cs index d0d98a64db..5d44768388 100644 --- a/src/Umbraco.Core/Models/DictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/DictionaryTranslation.cs @@ -1,107 +1,107 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a translation for a +/// +[Serializable] +[DataContract(IsReference = true)] +public class DictionaryTranslation : EntityBase, IDictionaryTranslation { - /// - /// Represents a translation for a - /// - [Serializable] - [DataContract(IsReference = true)] - public class DictionaryTranslation : EntityBase, IDictionaryTranslation + private ILanguage? _language; + + // note: this will be memberwise cloned + private string _value; + + public DictionaryTranslation(ILanguage language, string value) { - public Func? GetLanguage { get; set; } + _language = language ?? throw new ArgumentNullException("language"); + LanguageId = _language.Id; + _value = value; + } - private ILanguage? _language; - private string _value; - //note: this will be memberwise cloned - private int _languageId; + public DictionaryTranslation(ILanguage language, string value, Guid uniqueId) + { + _language = language ?? throw new ArgumentNullException("language"); + LanguageId = _language.Id; + _value = value; + Key = uniqueId; + } - public DictionaryTranslation(ILanguage language, string value) + public DictionaryTranslation(int languageId, string value) + { + LanguageId = languageId; + _value = value; + } + + public DictionaryTranslation(int languageId, string value, Guid uniqueId) + { + LanguageId = languageId; + _value = value; + Key = uniqueId; + } + + public Func? GetLanguage { get; set; } + + /// + /// Gets or sets the for the translation + /// + /// + /// Marked as DoNotClone - TODO: this member shouldn't really exist here in the first place, the DictionaryItem + /// class will have a deep hierarchy of objects which all get deep cloned which we don't want. This should have simply + /// just referenced a language ID not the actual language object. In v8 we need to fix this. + /// We're going to have to do the same hacky stuff we had to do with the Template/File contents so that this is + /// returned + /// on a callback. + /// + [DataMember] + [DoNotClone] + public ILanguage? Language + { + get { - if (language == null) throw new ArgumentNullException("language"); - _language = language; - _languageId = _language.Id; - _value = value; - } - - public DictionaryTranslation(ILanguage language, string value, Guid uniqueId) - { - if (language == null) throw new ArgumentNullException("language"); - _language = language; - _languageId = _language.Id; - _value = value; - Key = uniqueId; - } - - public DictionaryTranslation(int languageId, string value) - { - _languageId = languageId; - _value = value; - } - - public DictionaryTranslation(int languageId, string value, Guid uniqueId) - { - _languageId = languageId; - _value = value; - Key = uniqueId; - } - - /// - /// Gets or sets the for the translation - /// - /// - /// Marked as DoNotClone - TODO: this member shouldn't really exist here in the first place, the DictionaryItem - /// class will have a deep hierarchy of objects which all get deep cloned which we don't want. This should have simply - /// just referenced a language ID not the actual language object. In v8 we need to fix this. - /// We're going to have to do the same hacky stuff we had to do with the Template/File contents so that this is returned - /// on a callback. - /// - [DataMember] - [DoNotClone] - public ILanguage? Language - { - get + if (_language != null) { - if (_language != null) - return _language; - - // else, must lazy-load - if (GetLanguage != null && _languageId > 0) - _language = GetLanguage(_languageId); return _language; } - set + + // else, must lazy-load + if (GetLanguage != null && LanguageId > 0) { - SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); - _languageId = _language == null ? -1 : _language.Id; + _language = GetLanguage(LanguageId); } + + return _language; } - public int LanguageId + set { - get { return _languageId; } - } - - /// - /// Gets or sets the translated text - /// - [DataMember] - public string Value - { - get { return _value; } - set { SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); } - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (DictionaryTranslation)clone; - - // clear fields that were memberwise-cloned and that we don't want to clone - clonedEntity._language = null; + SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); + LanguageId = _language == null ? -1 : _language.Id; } } + + public int LanguageId { get; private set; } + + /// + /// Gets or sets the translated text + /// + [DataMember] + public string Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); + } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (DictionaryTranslation)clone; + + // clear fields that were memberwise-cloned and that we don't want to clone + clonedEntity._language = null; + } } diff --git a/src/Umbraco.Core/Models/DoNotCloneAttribute.cs b/src/Umbraco.Core/Models/DoNotCloneAttribute.cs index 39a7bcd900..1fb0b3cd4b 100644 --- a/src/Umbraco.Core/Models/DoNotCloneAttribute.cs +++ b/src/Umbraco.Core/Models/DoNotCloneAttribute.cs @@ -1,23 +1,16 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Used to attribute properties that have a setter and are a reference type +/// that should be ignored for cloning when using the DeepCloneHelper +/// +/// +/// This attribute must be used: +/// * when the property is backed by a field but the result of the property is the un-natural data stored in the field +/// This attribute should not be used: +/// * when the property is virtual +/// * when the setter performs additional required logic other than just setting the underlying field +/// +public class DoNotCloneAttribute : Attribute { - /// - /// Used to attribute properties that have a setter and are a reference type - /// that should be ignored for cloning when using the DeepCloneHelper - /// - /// - /// - /// This attribute must be used: - /// * when the property is backed by a field but the result of the property is the un-natural data stored in the field - /// - /// This attribute should not be used: - /// * when the property is virtual - /// * when the setter performs additional required logic other than just setting the underlying field - /// - /// - public class DoNotCloneAttribute : Attribute - { - - } } diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs index 0255cfd40e..ac19eef0c8 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs @@ -1,45 +1,42 @@ -using System; +namespace Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.Editors +/// +/// Represents data that has been submitted to be saved for a content property +/// +/// +/// This object exists because we may need to save additional data for each property, more than just +/// the string representation of the value being submitted. An example of this is uploaded files. +/// +public class ContentPropertyData { - /// - /// Represents data that has been submitted to be saved for a content property - /// - /// - /// This object exists because we may need to save additional data for each property, more than just - /// the string representation of the value being submitted. An example of this is uploaded files. - /// - public class ContentPropertyData + public ContentPropertyData(object? value, object? dataTypeConfiguration) { - public ContentPropertyData(object? value, object? dataTypeConfiguration) - { - Value = value; - DataTypeConfiguration = dataTypeConfiguration; - } - - /// - /// The value submitted for the property - /// - public object? Value { get; } - - /// - /// The data type configuration for the property. - /// - public object? DataTypeConfiguration { get; } - - /// - /// Gets or sets the unique identifier of the content owning the property. - /// - public Guid ContentKey { get; set; } - - /// - /// Gets or sets the unique identifier of the property type. - /// - public Guid PropertyTypeKey { get; set; } - - /// - /// Gets or sets the uploaded files. - /// - public ContentPropertyFile[]? Files { get; set; } + Value = value; + DataTypeConfiguration = dataTypeConfiguration; } + + /// + /// The value submitted for the property + /// + public object? Value { get; } + + /// + /// The data type configuration for the property. + /// + public object? DataTypeConfiguration { get; } + + /// + /// Gets or sets the unique identifier of the content owning the property. + /// + public Guid ContentKey { get; set; } + + /// + /// Gets or sets the unique identifier of the property type. + /// + public Guid PropertyTypeKey { get; set; } + + /// + /// Gets or sets the uploaded files. + /// + public ContentPropertyFile[]? Files { get; set; } } diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs index d1bc9127ce..9bb098697c 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs @@ -1,43 +1,42 @@ -namespace Umbraco.Cms.Core.Models.Editors +namespace Umbraco.Cms.Core.Models.Editors; + +/// +/// Represents an uploaded file for a property. +/// +public class ContentPropertyFile { + /// + /// Gets or sets the property alias. + /// + public string? PropertyAlias { get; set; } /// - /// Represents an uploaded file for a property. + /// When dealing with content variants, this is the culture for the variant /// - public class ContentPropertyFile - { - /// - /// Gets or sets the property alias. - /// - public string? PropertyAlias { get; set; } + public string? Culture { get; set; } - /// - /// When dealing with content variants, this is the culture for the variant - /// - public string? Culture { get; set; } + /// + /// When dealing with content variants, this is the segment for the variant + /// + public string? Segment { get; set; } - /// - /// When dealing with content variants, this is the segment for the variant - /// - public string? Segment { get; set; } + /// + /// An array of metadata that is parsed out from the file info posted to the server which is set on the client. + /// + /// + /// This can be used for property types like Nested Content that need to have special unique identifiers for each file + /// since there might be multiple files + /// per property. + /// + public string[]? Metadata { get; set; } - /// - /// An array of metadata that is parsed out from the file info posted to the server which is set on the client. - /// - /// - /// This can be used for property types like Nested Content that need to have special unique identifiers for each file since there might be multiple files - /// per property. - /// - public string[]? Metadata { get; set; } + /// + /// Gets or sets the name of the file. + /// + public string? FileName { get; set; } - /// - /// Gets or sets the name of the file. - /// - public string? FileName { get; set; } - - /// - /// Gets or sets the temporary path where the file has been uploaded. - /// - public string TempFilePath { get; set; } = string.Empty; - } + /// + /// Gets or sets the temporary path where the file has been uploaded. + /// + public string TempFilePath { get; set; } = string.Empty; } diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs index 4efc5017e1..c093962408 100644 --- a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs +++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs @@ -1,70 +1,56 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.Editors +/// +/// Used to track reference to other entities in a property value +/// +public struct UmbracoEntityReference : IEquatable { - /// - /// Used to track reference to other entities in a property value - /// - public struct UmbracoEntityReference : IEquatable + private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty); + + public UmbracoEntityReference(Udi udi, string relationTypeAlias) { - private static readonly UmbracoEntityReference _empty = new UmbracoEntityReference(UnknownTypeUdi.Instance, string.Empty); + Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); + } - public UmbracoEntityReference(Udi udi, string relationTypeAlias) + public UmbracoEntityReference(Udi udi) + { + Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + + switch (udi.EntityType) { - Udi = udi ?? throw new ArgumentNullException(nameof(udi)); - RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); - } - - public UmbracoEntityReference(Udi udi) - { - Udi = udi ?? throw new ArgumentNullException(nameof(udi)); - - switch (udi.EntityType) - { - case Constants.UdiEntityType.Media: - RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; - break; - default: - RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; - break; - } - } - - public static UmbracoEntityReference Empty() => _empty; - - public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); - - public Udi Udi { get; } - public string RelationTypeAlias { get; } - - public override bool Equals(object? obj) - { - return obj is UmbracoEntityReference reference && Equals(reference); - } - - public bool Equals(UmbracoEntityReference other) - { - return EqualityComparer.Default.Equals(Udi, other.Udi) && - RelationTypeAlias == other.RelationTypeAlias; - } - - public override int GetHashCode() - { - var hashCode = -487348478; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Udi); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelationTypeAlias); - return hashCode; - } - - public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) - { - return left.Equals(right); - } - - public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) - { - return !(left == right); + case Constants.UdiEntityType.Media: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; + break; + default: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + break; } } + + public Udi Udi { get; } + + public static UmbracoEntityReference Empty() => _empty; + + public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); + + public string RelationTypeAlias { get; } + + public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right); + + public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference); + + public bool Equals(UmbracoEntityReference other) => + EqualityComparer.Default.Equals(Udi, other.Udi) && + RelationTypeAlias == other.RelationTypeAlias; + + public override int GetHashCode() + { + var hashCode = -487348478; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Udi); + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(RelationTypeAlias); + return hashCode; + } + + public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right); } diff --git a/src/Umbraco.Core/Models/Email/EmailMessage.cs b/src/Umbraco.Core/Models/Email/EmailMessage.cs index b012bbfeb3..1419285417 100644 --- a/src/Umbraco.Core/Models/Email/EmailMessage.cs +++ b/src/Umbraco.Core/Models/Email/EmailMessage.cs @@ -1,82 +1,86 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Models.Email +public class EmailMessage { - public class EmailMessage + public EmailMessage(string? from, string? to, string? subject, string? body, bool isBodyHtml) + : this(from, new[] { to }, null, null, null, subject, body, isBodyHtml, null) { - public string? From { get; } + } - public string?[] To { get; } + public EmailMessage( + string? from, + string?[] to, + string[]? cc, + string[]? bcc, + string[]? replyTo, + string? subject, + string? body, + bool isBodyHtml, + IEnumerable? attachments) + { + ArgumentIsNotNullOrEmpty(to, nameof(to)); + ArgumentIsNotNullOrEmpty(subject, nameof(subject)); + ArgumentIsNotNullOrEmpty(body, nameof(body)); - public string[]? Cc { get; } + From = from; + To = to; + Cc = cc; + Bcc = bcc; + ReplyTo = replyTo; + Subject = subject; + Body = body; + IsBodyHtml = isBodyHtml; + Attachments = attachments?.ToList(); + } - public string[]? Bcc { get; } + public string? From { get; } - public string[]? ReplyTo { get; } + public string?[] To { get; } - public string? Subject { get; } + public string[]? Cc { get; } - public string? Body { get; } + public string[]? Bcc { get; } - public bool IsBodyHtml { get; } + public string[]? ReplyTo { get; } - public IList? Attachments { get; } + public string? Subject { get; } - public bool HasAttachments => Attachments != null && Attachments.Count > 0; + public string? Body { get; } - public EmailMessage(string? from, string? to, string? subject, string? body, bool isBodyHtml) - : this(from, new[] { to }, null, null, null, subject, body, isBodyHtml, null) + public bool IsBodyHtml { get; } + + public IList? Attachments { get; } + + public bool HasAttachments => Attachments != null && Attachments.Count > 0; + + private static void ArgumentIsNotNullOrEmpty(string? arg, string argName) + { + if (arg == null) { + throw new ArgumentNullException(argName); } - public EmailMessage(string? from, string?[] to, string[]? cc, string[]? bcc, string[]? replyTo, string? subject, string? body, bool isBodyHtml, IEnumerable? attachments) + if (arg.Length == 0) { - ArgumentIsNotNullOrEmpty(to, nameof(to)); - ArgumentIsNotNullOrEmpty(subject, nameof(subject)); - ArgumentIsNotNullOrEmpty(body, nameof(body)); + throw new ArgumentException("Value cannot be empty.", argName); + } + } - From = from; - To = to; - Cc = cc; - Bcc = bcc; - ReplyTo = replyTo; - Subject = subject; - Body = body; - IsBodyHtml = isBodyHtml; - Attachments = attachments?.ToList(); + private static void ArgumentIsNotNullOrEmpty(string?[]? arg, string argName) + { + if (arg == null) + { + throw new ArgumentNullException(argName); } - private static void ArgumentIsNotNullOrEmpty(string? arg, string argName) + if (arg.Length == 0) { - if (arg == null) - { - throw new ArgumentNullException(argName); - } - - if (arg.Length == 0) - { - throw new ArgumentException("Value cannot be empty.", argName); - } + throw new ArgumentException("Value cannot be an empty array.", argName); } - private static void ArgumentIsNotNullOrEmpty(string?[]? arg, string argName) + if (arg.Any(x => x is not null && x.Length > 0) == false) { - if (arg == null) - { - throw new ArgumentNullException(argName); - } - - if (arg.Length == 0) - { - throw new ArgumentException("Value cannot be an empty array.", argName); - } - - if (arg.Any(x => x is not null && x.Length > 0) == false) - { - throw new ArgumentException("Value cannot be an array containing only null or empty elements.", argName); - } + throw new ArgumentException("Value cannot be an array containing only null or empty elements.", argName); } } } diff --git a/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs b/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs index bbb24b69f7..96c52ef9e7 100644 --- a/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs +++ b/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs @@ -1,17 +1,14 @@ -using System.IO; +namespace Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Models.Email +public class EmailMessageAttachment { - public class EmailMessageAttachment + public EmailMessageAttachment(Stream stream, string fileName) { - public Stream Stream { get; } - - public string FileName { get; } - - public EmailMessageAttachment(Stream stream, string fileName) - { - Stream = stream; - FileName = fileName; - } + Stream = stream; + FileName = fileName; } + + public Stream Stream { get; } + + public string FileName { get; } } diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs b/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs index 755947c6a4..c9488f0798 100644 --- a/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs +++ b/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models.Email +namespace Umbraco.Cms.Core.Models.Email; + +/// +/// Represents an email address used for notifications. Contains both the address and its display name. +/// +public class NotificationEmailAddress { - /// - /// Represents an email address used for notifications. Contains both the address and its display name. - /// - public class NotificationEmailAddress + public NotificationEmailAddress(string address, string displayName) { - public string DisplayName { get; } - - public string Address { get; } - - public NotificationEmailAddress(string address, string displayName) - { - Address = address; - DisplayName = displayName; - } + Address = address; + DisplayName = displayName; } + + public string DisplayName { get; } + + public string Address { get; } } diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs b/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs index c71519d83f..abfea360d9 100644 --- a/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs +++ b/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs @@ -1,54 +1,49 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Models.Email +/// +/// Represents an email when sent with notifications. +/// +public class NotificationEmailModel { - /// - /// Represents an email when sent with notifications. - /// - public class NotificationEmailModel + public NotificationEmailModel( + NotificationEmailAddress? from, + IEnumerable? to, + IEnumerable? cc, + IEnumerable? bcc, + IEnumerable? replyTo, + string? subject, + string? body, + IEnumerable? attachments, + bool isBodyHtml) { - public NotificationEmailAddress? From { get; } - - public IEnumerable? To { get; } - - public IEnumerable? Cc { get; } - - public IEnumerable? Bcc { get; } - - public IEnumerable? ReplyTo { get; } - - public string? Subject { get; } - - public string? Body { get; } - - public bool IsBodyHtml { get; } - - public IList? Attachments { get; } - - public bool HasAttachments => Attachments != null && Attachments.Count > 0; - - public NotificationEmailModel( - NotificationEmailAddress? from, - IEnumerable? to, - IEnumerable? cc, - IEnumerable? bcc, - IEnumerable? replyTo, - string? subject, - string? body, - IEnumerable? attachments, - bool isBodyHtml) - { - From = from; - To = to; - Cc = cc; - Bcc = bcc; - ReplyTo = replyTo; - Subject = subject; - Body = body; - IsBodyHtml = isBodyHtml; - Attachments = attachments?.ToList(); - } - + From = from; + To = to; + Cc = cc; + Bcc = bcc; + ReplyTo = replyTo; + Subject = subject; + Body = body; + IsBodyHtml = isBodyHtml; + Attachments = attachments?.ToList(); } + + public NotificationEmailAddress? From { get; } + + public IEnumerable? To { get; } + + public IEnumerable? Cc { get; } + + public IEnumerable? Bcc { get; } + + public IEnumerable? ReplyTo { get; } + + public string? Subject { get; } + + public string? Body { get; } + + public bool IsBodyHtml { get; } + + public IList? Attachments { get; } + + public bool HasAttachments => Attachments != null && Attachments.Count > 0; } diff --git a/src/Umbraco.Core/Models/Entities/BeingDirty.cs b/src/Umbraco.Core/Models/Entities/BeingDirty.cs index 7b078b35b8..0ae2a142ed 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirty.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirty.cs @@ -1,36 +1,30 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Provides a concrete implementation of . +/// +/// +/// +/// This class is provided for classes that cannot inherit from +/// and therefore need to implement , by re-using some of +/// logic. +/// +/// +public sealed class BeingDirty : BeingDirtyBase { /// - /// Provides a concrete implementation of . + /// Sets a property value, detects changes and manages the dirty flag. /// - /// - /// This class is provided for classes that cannot inherit from - /// and therefore need to implement , by re-using some of - /// logic. - /// - public sealed class BeingDirty : BeingDirtyBase - { - /// - /// Sets a property value, detects changes and manages the dirty flag. - /// - /// The type of the value. - /// The new value. - /// A reference to the value to set. - /// The property name. - /// A comparer to compare property values. - public new void SetPropertyValueAndDetectChanges(T value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) - { - base.SetPropertyValueAndDetectChanges(value, ref valueRef, propertyName, comparer); - } + /// The type of the value. + /// The new value. + /// A reference to the value to set. + /// The property name. + /// A comparer to compare property values. + public new void SetPropertyValueAndDetectChanges(T value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) => + base.SetPropertyValueAndDetectChanges(value, ref valueRef, propertyName, comparer); - /// - /// Registers that a property has changed. - /// - public new void OnPropertyChanged(string propertyName) - { - base.OnPropertyChanged(propertyName); - } - } + /// + /// Registers that a property has changed. + /// + public new void OnPropertyChanged(string propertyName) => base.OnPropertyChanged(propertyName); } diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs index c63ee54a6d..887477c743 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs @@ -1,191 +1,176 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Provides a base implementation of and . +/// +[Serializable] +[DataContract(IsReference = true)] +public abstract class BeingDirtyBase : IRememberBeingDirty { - /// - /// Provides a base implementation of and . - /// - [Serializable] - [DataContract(IsReference = true)] - public abstract class BeingDirtyBase : IRememberBeingDirty + private Dictionary? _currentChanges; // which properties have changed? + private Dictionary? _savedChanges; // which properties had changed at last commit? + private bool _withChanges = true; // should we track changes? + + #region ICanBeDirty + + /// + public virtual bool IsDirty() => _currentChanges != null && _currentChanges.Any(); + + /// + public virtual bool IsPropertyDirty(string propertyName) => + _currentChanges != null && _currentChanges.ContainsKey(propertyName); + + /// + public virtual IEnumerable GetDirtyProperties() => + + // ReSharper disable once MergeConditionalExpression + _currentChanges == null + ? Enumerable.Empty() + : _currentChanges.Where(x => x.Value).Select(x => x.Key); + + /// + /// Saves dirty properties so they can be checked with WasDirty. + public virtual void ResetDirtyProperties() => ResetDirtyProperties(true); + + #endregion + + #region IRememberBeingDirty + + /// + public virtual bool WasDirty() => _savedChanges != null && _savedChanges.Any(); + + /// + public virtual bool WasPropertyDirty(string propertyName) => + _savedChanges != null && _savedChanges.ContainsKey(propertyName); + + /// + public virtual void ResetWereDirtyProperties() => + + // note: cannot .Clear() because when memberwise-cloning this will be the SAME + // instance as the one on the clone, so we need to create a new instance. + _savedChanges = null; + + /// + public virtual void ResetDirtyProperties(bool rememberDirty) { - private bool _withChanges = true; // should we track changes? - private Dictionary? _currentChanges; // which properties have changed? - private Dictionary? _savedChanges; // which properties had changed at last commit? + // capture changes if remembering + // clone the dictionary in case it's shared by an entity clone + _savedChanges = rememberDirty && _currentChanges != null + ? _currentChanges.ToDictionary(v => v.Key, v => v.Value) + : null; - #region ICanBeDirty + // note: cannot .Clear() because when memberwise-clone this will be the SAME + // instance as the one on the clone, so we need to create a new instance. + _currentChanges = null; + } - /// - public virtual bool IsDirty() + /// + public virtual IEnumerable GetWereDirtyProperties() => + + // ReSharper disable once MergeConditionalExpression + _savedChanges == null + ? Enumerable.Empty() + : _savedChanges.Where(x => x.Value).Select(x => x.Key); + + #endregion + + #region Change Tracking + + /// + /// Occurs when a property changes. + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Registers that a property has changed. + /// + protected virtual void OnPropertyChanged(string propertyName) + { + if (_withChanges == false) { - return _currentChanges != null && _currentChanges.Any(); + return; } - /// - public virtual bool IsPropertyDirty(string propertyName) + if (_currentChanges == null) { - return _currentChanges != null && _currentChanges.ContainsKey(propertyName); + _currentChanges = new Dictionary(); } - /// - public virtual IEnumerable GetDirtyProperties() + _currentChanges[propertyName] = true; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Disables change tracking. + /// + public void DisableChangeTracking() => _withChanges = false; + + /// + /// Enables change tracking. + /// + public void EnableChangeTracking() => _withChanges = true; + + /// + /// Sets a property value, detects changes and manages the dirty flag. + /// + /// The type of the value. + /// The new value. + /// A reference to the value to set. + /// The property name. + /// A comparer to compare property values. + protected void SetPropertyValueAndDetectChanges(T? value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) + { + if (comparer == null) { - // ReSharper disable once MergeConditionalExpression - return _currentChanges == null - ? Enumerable.Empty() - : _currentChanges.Where(x => x.Value).Select(x => x.Key); - } - - /// - /// Saves dirty properties so they can be checked with WasDirty. - public virtual void ResetDirtyProperties() - { - ResetDirtyProperties(true); - } - - #endregion - - #region IRememberBeingDirty - - /// - public virtual bool WasDirty() - { - return _savedChanges != null && _savedChanges.Any(); - } - - /// - public virtual bool WasPropertyDirty(string propertyName) - { - return _savedChanges != null && _savedChanges.ContainsKey(propertyName); - } - - /// - public virtual void ResetWereDirtyProperties() - { - // note: cannot .Clear() because when memberwise-cloning this will be the SAME - // instance as the one on the clone, so we need to create a new instance. - _savedChanges = null; - } - - /// - public virtual void ResetDirtyProperties(bool rememberDirty) - { - // capture changes if remembering - // clone the dictionary in case it's shared by an entity clone - _savedChanges = rememberDirty && _currentChanges != null - ? _currentChanges.ToDictionary(v => v.Key, v => v.Value) - : null; - - // note: cannot .Clear() because when memberwise-clone this will be the SAME - // instance as the one on the clone, so we need to create a new instance. - _currentChanges = null; - } - - /// - public virtual IEnumerable GetWereDirtyProperties() - { - // ReSharper disable once MergeConditionalExpression - return _savedChanges == null - ? Enumerable.Empty() - : _savedChanges.Where(x => x.Value).Select(x => x.Key); - } - - #endregion - - #region Change Tracking - - /// - /// Occurs when a property changes. - /// - public event PropertyChangedEventHandler? PropertyChanged; - - /// - /// Registers that a property has changed. - /// - protected virtual void OnPropertyChanged(string propertyName) - { - if (_withChanges == false) - return; - - if (_currentChanges == null) - _currentChanges = new Dictionary(); - - _currentChanges[propertyName] = true; - - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - /// - /// Disables change tracking. - /// - public void DisableChangeTracking() - { - _withChanges = false; - } - - /// - /// Enables change tracking. - /// - public void EnableChangeTracking() - { - _withChanges = true; - } - - /// - /// Sets a property value, detects changes and manages the dirty flag. - /// - /// The type of the value. - /// The new value. - /// A reference to the value to set. - /// The property name. - /// A comparer to compare property values. - protected void SetPropertyValueAndDetectChanges(T? value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) - { - if (comparer == null) + // if no comparer is provided, use the default provider, as long as the value is not + // an IEnumerable - exclude strings, which are IEnumerable but have a default comparer + Type typeofT = typeof(T); + if (!(typeofT == typeof(string)) && typeof(IEnumerable).IsAssignableFrom(typeofT)) { - // if no comparer is provided, use the default provider, as long as the value is not - // an IEnumerable - exclude strings, which are IEnumerable but have a default comparer - var typeofT = typeof(T); - if (!(typeofT == typeof(string)) && typeof(IEnumerable).IsAssignableFrom(typeofT)) - throw new ArgumentNullException(nameof(comparer), "A custom comparer must be supplied for IEnumerable values."); - comparer = EqualityComparer.Default; + throw new ArgumentNullException(nameof(comparer), "A custom comparer must be supplied for IEnumerable values."); } - // compare values - var changed = _withChanges && comparer.Equals(valueRef, value) == false; - - // assign the new value - valueRef = value; - - // handle change - if (changed) - OnPropertyChanged(propertyName); + comparer = EqualityComparer.Default; } - /// - /// Detects changes and manages the dirty flag. - /// - /// The type of the value. - /// The new value. - /// The original value. - /// The property name. - /// A comparer to compare property values. - /// A value indicating whether we know values have changed and no comparison is required. - protected void DetectChanges(T value, T orig, string propertyName, IEqualityComparer comparer, bool changed) + // compare values + var changed = _withChanges && comparer.Equals(valueRef, value) == false; + + // assign the new value + valueRef = value; + + // handle change + if (changed) { - // compare values - changed = _withChanges && (changed || !comparer.Equals(orig, value)); - - // handle change - if (changed) - OnPropertyChanged(propertyName); + OnPropertyChanged(propertyName); } - - #endregion } + + /// + /// Detects changes and manages the dirty flag. + /// + /// The type of the value. + /// The new value. + /// The original value. + /// The property name. + /// A comparer to compare property values. + /// A value indicating whether we know values have changed and no comparison is required. + protected void DetectChanges(T value, T orig, string propertyName, IEqualityComparer comparer, bool changed) + { + // compare values + changed = _withChanges && (changed || !comparer.Equals(orig, value)); + + // handle change + if (changed) + { + OnPropertyChanged(propertyName); + } + } + + #endregion } diff --git a/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs index 74bd4e4f44..3b9d139ba7 100644 --- a/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Implements . +/// +public class ContentEntitySlim : EntitySlim, IContentEntitySlim { - /// - /// Implements . - /// - public class ContentEntitySlim : EntitySlim, IContentEntitySlim - { - /// - public string ContentTypeAlias { get; set; } = string.Empty; + /// + public string ContentTypeAlias { get; set; } = string.Empty; - /// - public string? ContentTypeIcon { get; set; } + /// + public string? ContentTypeIcon { get; set; } - /// - public string? ContentTypeThumbnail { get; set; } - } + /// + public string? ContentTypeThumbnail { get; set; } } diff --git a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs index 3bc410fc9b..a5c0ca23c9 100644 --- a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs @@ -1,48 +1,42 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Implements . +/// +public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim { + private static readonly IReadOnlyDictionary Empty = new Dictionary(); - /// - /// Implements . - /// - public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim + private IReadOnlyDictionary? _cultureNames; + private IEnumerable? _editedCultures; + private IEnumerable? _publishedCultures; + + /// + public IReadOnlyDictionary CultureNames { - private static readonly IReadOnlyDictionary Empty = new Dictionary(); - - private IReadOnlyDictionary? _cultureNames; - private IEnumerable? _publishedCultures; - private IEnumerable? _editedCultures; - - /// - public IReadOnlyDictionary CultureNames - { - get => _cultureNames ?? Empty; - set => _cultureNames = value; - } - - /// - public IEnumerable PublishedCultures - { - get => _publishedCultures ?? Enumerable.Empty(); - set => _publishedCultures = value; - } - - /// - public IEnumerable EditedCultures - { - get => _editedCultures ?? Enumerable.Empty(); - set => _editedCultures = value; - } - - public ContentVariation Variations { get; set; } - - /// - public bool Published { get; set; } - - /// - public bool Edited { get; set; } - + get => _cultureNames ?? Empty; + set => _cultureNames = value; } + + /// + public IEnumerable PublishedCultures + { + get => _publishedCultures ?? Enumerable.Empty(); + set => _publishedCultures = value; + } + + /// + public IEnumerable EditedCultures + { + get => _editedCultures ?? Enumerable.Empty(); + set => _editedCultures = value; + } + + public ContentVariation Variations { get; set; } + + /// + public bool Published { get; set; } + + /// + public bool Edited { get; set; } } diff --git a/src/Umbraco.Core/Models/Entities/EntityBase.cs b/src/Umbraco.Core/Models/Entities/EntityBase.cs index 57b9eeae1f..df60d97a1e 100644 --- a/src/Umbraco.Core/Models/Entities/EntityBase.cs +++ b/src/Umbraco.Core/Models/Entities/EntityBase.cs @@ -1,155 +1,156 @@ -using System; using System.Diagnostics; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Provides a base class for entities. +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {" + nameof(Id) + "}")] +public abstract class EntityBase : BeingDirtyBase, IEntity { - /// - /// Provides a base class for entities. - /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {" + nameof(Id) + "}")] - public abstract class EntityBase : BeingDirtyBase, IEntity - { #if DEBUG_MODEL public Guid InstanceId = Guid.NewGuid(); #endif - private bool _hasIdentity; - private int _id; - private Guid _key; - private DateTime _createDate; - private DateTime _updateDate; + private bool _hasIdentity; + private int _id; + private Guid _key; + private DateTime _createDate; + private DateTime _updateDate; - /// - [DataMember] - public int Id + /// + [DataMember] + public int Id + { + get => _id; + set { - get => _id; - set + SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); + _hasIdentity = value != 0; + } + } + + /// + [DataMember] + public Guid Key + { + get + { + // if an entity does NOT have a key yet, assign one now + if (_key == Guid.Empty) { - SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); - _hasIdentity = value != 0; + _key = Guid.NewGuid(); } + + return _key; + } + set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); + } + + /// + [DataMember] + public DateTime CreateDate + { + get => _createDate; + set => SetPropertyValueAndDetectChanges(value, ref _createDate, nameof(CreateDate)); + } + + /// + [DataMember] + public DateTime UpdateDate + { + get => _updateDate; + set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); + } + + /// + [DataMember] + public DateTime? DeleteDate { get; set; } // no change tracking - not persisted + + /// + [DataMember] + public virtual bool HasIdentity => _hasIdentity; + + /// + /// Resets the entity identity. + /// + public virtual void ResetIdentity() + { + _id = default; + _key = Guid.Empty; + _hasIdentity = false; + } + + public virtual bool Equals(EntityBase? other) => + other != null && (ReferenceEquals(this, other) || SameIdentityAs(other)); + + public override bool Equals(object? obj) => + obj != null && (ReferenceEquals(this, obj) || SameIdentityAs(obj as EntityBase)); + + public override int GetHashCode() + { + unchecked + { + var hashCode = HasIdentity.GetHashCode(); + hashCode = (hashCode * 397) ^ Id; + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } + + private bool SameIdentityAs(EntityBase? other) + { + if (other == null) + { + return false; } - /// - [DataMember] - public Guid Key + // same identity if + // - same object (reference equals) + // - or same CLR type, both have identities, and they are identical + if (ReferenceEquals(this, other)) { - get - { - // if an entity does NOT have a key yet, assign one now - if (_key == Guid.Empty) - _key = Guid.NewGuid(); - return _key; - } - set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); + return true; } - /// - [DataMember] - public DateTime CreateDate - { - get => _createDate; - set => SetPropertyValueAndDetectChanges(value, ref _createDate, nameof(CreateDate)); - } + return GetType() == other.GetType() && HasIdentity && other.HasIdentity && Id == other.Id; + } - /// - [DataMember] - public DateTime UpdateDate - { - get => _updateDate; - set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); - } - - /// - [DataMember] - public DateTime? DeleteDate { get; set; } // no change tracking - not persisted - - /// - [DataMember] - public virtual bool HasIdentity => _hasIdentity; - - /// - /// Resets the entity identity. - /// - public virtual void ResetIdentity() - { - _id = default; - _key = Guid.Empty; - _hasIdentity = false; - } - - public virtual bool Equals(EntityBase? other) - { - return other != null && (ReferenceEquals(this, other) || SameIdentityAs(other)); - } - - public override bool Equals(object? obj) - { - return obj != null && (ReferenceEquals(this, obj) || SameIdentityAs(obj as EntityBase)); - } - - private bool SameIdentityAs(EntityBase? other) - { - if (other == null) return false; - - // same identity if - // - same object (reference equals) - // - or same CLR type, both have identities, and they are identical - - if (ReferenceEquals(this, other)) - return true; - - return GetType() == other.GetType() && HasIdentity && other.HasIdentity && Id == other.Id; - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = HasIdentity.GetHashCode(); - hashCode = (hashCode * 397) ^ Id; - hashCode = (hashCode * 397) ^ GetType().GetHashCode(); - return hashCode; - } - } - - public object DeepClone() - { - // memberwise-clone (ie shallow clone) the entity - var unused = Key; // ensure that 'this' has a key, before cloning - var clone = (EntityBase) MemberwiseClone(); + public object DeepClone() + { + // memberwise-clone (ie shallow clone) the entity + Guid unused = Key; // ensure that 'this' has a key, before cloning + var clone = (EntityBase)MemberwiseClone(); #if DEBUG_MODEL clone.InstanceId = Guid.NewGuid(); #endif - //disable change tracking while we deep clone IDeepCloneable properties - clone.DisableChangeTracking(); + // disable change tracking while we deep clone IDeepCloneable properties + clone.DisableChangeTracking(); - // deep clone ref properties that are IDeepCloneable - DeepCloneHelper.DeepCloneRefProperties(this, clone); + // deep clone ref properties that are IDeepCloneable + DeepCloneHelper.DeepCloneRefProperties(this, clone); - PerformDeepClone(clone); + PerformDeepClone(clone); - // clear changes (ensures the clone has its own dictionaries) - clone.ResetDirtyProperties(false); + // clear changes (ensures the clone has its own dictionaries) + clone.ResetDirtyProperties(false); - //re-enable change tracking - clone.EnableChangeTracking(); + // re-enable change tracking + clone.EnableChangeTracking(); - return clone; - } + return clone; + } - /// - /// Used by inheritors to modify the DeepCloning logic - /// - /// - protected virtual void PerformDeepClone(object clone) - { - } + /// + /// Used by inheritors to modify the DeepCloning logic + /// + /// + protected virtual void PerformDeepClone(object clone) + { } } diff --git a/src/Umbraco.Core/Models/Entities/EntityExtensions.cs b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs index ba3421349d..53801875ae 100644 --- a/src/Umbraco.Core/Models/Entities/EntityExtensions.cs +++ b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs @@ -1,49 +1,49 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class EntityExtensions { - public static class EntityExtensions + /// + /// Updates the entity when it is being saved. + /// + public static void UpdatingEntity(this IEntity entity) { - /// - /// Updates the entity when it is being saved. - /// - public static void UpdatingEntity(this IEntity entity) + DateTime now = DateTime.Now; + + if (entity.CreateDate == default) { - var now = DateTime.Now; - - if (entity.CreateDate == default) - { - entity.CreateDate = now; - } - - // set the update date if not already set - if (entity.UpdateDate == default || (entity is ICanBeDirty canBeDirty && canBeDirty.IsPropertyDirty("UpdateDate") == false)) - { - entity.UpdateDate = now; - } + entity.CreateDate = now; } - /// - /// Updates the entity when it is being saved for the first time. - /// - public static void AddingEntity(this IEntity entity) + // set the update date if not already set + if (entity.UpdateDate == default || + (entity is ICanBeDirty canBeDirty && canBeDirty.IsPropertyDirty("UpdateDate") == false)) { - var now = DateTime.Now; - var canBeDirty = entity as ICanBeDirty; + entity.UpdateDate = now; + } + } - // set the create and update dates, if not already set - if (entity.CreateDate == default || canBeDirty?.IsPropertyDirty("CreateDate") == false) - { - entity.CreateDate = now; - } - if (entity.UpdateDate == default || canBeDirty?.IsPropertyDirty("UpdateDate") == false) - { - entity.UpdateDate = now; - } + /// + /// Updates the entity when it is being saved for the first time. + /// + public static void AddingEntity(this IEntity entity) + { + DateTime now = DateTime.Now; + var canBeDirty = entity as ICanBeDirty; + + // set the create and update dates, if not already set + if (entity.CreateDate == default || canBeDirty?.IsPropertyDirty("CreateDate") == false) + { + entity.CreateDate = now; + } + + if (entity.UpdateDate == default || canBeDirty?.IsPropertyDirty("UpdateDate") == false) + { + entity.UpdateDate = now; } } } diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs index c4bc473661..91acaea3fd 100644 --- a/src/Umbraco.Core/Models/Entities/EntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/EntitySlim.cs @@ -1,181 +1,153 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Implementation of for internal use. +/// +/// +/// +/// Although it implements , this class does not +/// implement and everything this interface defines, throws. +/// +/// +/// Although it implements , this class does not +/// implement and deep-cloning throws. +/// +/// +public class EntitySlim : IEntitySlim { /// - /// Implementation of for internal use. + /// Gets an entity representing "root". /// - /// - /// Although it implements , this class does not - /// implement and everything this interface defines, throws. - /// Although it implements , this class does not - /// implement and deep-cloning throws. - /// - public class EntitySlim : IEntitySlim + public static readonly IEntitySlim Root = new EntitySlim { Path = "-1", Name = "root", HasChildren = true }; + + private IDictionary? _additionalData; + + // implement IEntity + + /// + [DataMember] + public int Id { get; set; } + + /// + [DataMember] + public Guid Key { get; set; } + + /// + [DataMember] + public DateTime CreateDate { get; set; } + + /// + [DataMember] + public DateTime UpdateDate { get; set; } + + /// + [DataMember] + public DateTime? DeleteDate { get; set; } + + /// + [DataMember] + public bool HasIdentity => Id != 0; + + // implement ITreeEntity + + /// + [DataMember] + public string? Name { get; set; } + + /// + [DataMember] + public int CreatorId { get; set; } + + /// + [DataMember] + public int ParentId { get; set; } + + /// + [DataMember] + public int Level { get; set; } + + /// + public void SetParent(ITreeEntity? parent) => + throw new InvalidOperationException("This property won't be implemented."); + + /// + [DataMember] + public string Path { get; set; } = string.Empty; + + /// + [DataMember] + public int SortOrder { get; set; } + + /// + [DataMember] + public bool Trashed { get; set; } + + // implement IUmbracoEntity + + /// + [DataMember] + public IDictionary? AdditionalData => +_additionalData ??= new Dictionary(); + + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; + + // implement IEntitySlim + + /// + [DataMember] + public Guid NodeObjectType { get; set; } + + /// + [DataMember] + public bool HasChildren { get; set; } + + /// + [DataMember] + public virtual bool IsContainer { get; set; } + + #region IDeepCloneable + + /// + public object DeepClone() => throw new InvalidOperationException("This method won't be implemented."); + + #endregion + + public void ResetIdentity() { - private IDictionary? _additionalData; - - /// - /// Gets an entity representing "root". - /// - public static readonly IEntitySlim Root = new EntitySlim { Path = "-1", Name = "root", HasChildren = true }; - - // implement IEntity - - /// - [DataMember] - public int Id { get; set; } - - /// - [DataMember] - public Guid Key { get; set; } - - /// - [DataMember] - public DateTime CreateDate { get; set; } - - /// - [DataMember] - public DateTime UpdateDate { get; set; } - - /// - [DataMember] - public DateTime? DeleteDate { get; set; } - - /// - [DataMember] - public bool HasIdentity => Id != 0; - - - // implement ITreeEntity - - /// - [DataMember] - public string? Name { get; set; } - - /// - [DataMember] - public int CreatorId { get; set; } - - /// - [DataMember] - public int ParentId { get; set; } - - /// - public void SetParent(ITreeEntity? parent) => throw new InvalidOperationException("This property won't be implemented."); - - /// - [DataMember] - public int Level { get; set; } - - /// - [DataMember] - public string Path { get; set; } = string.Empty; - - /// - [DataMember] - public int SortOrder { get; set; } - - /// - [DataMember] - public bool Trashed { get; set; } - - - // implement IUmbracoEntity - - /// - [DataMember] - public IDictionary? AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); - - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; - - - // implement IEntitySlim - - /// - [DataMember] - public Guid NodeObjectType { get; set; } - - /// - [DataMember] - public bool HasChildren { get; set; } - - /// - [DataMember] - public virtual bool IsContainer { get; set; } - - - #region IDeepCloneable - - /// - public object DeepClone() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion - - public void ResetIdentity() - { - Id = default; - Key = Guid.Empty; - } - - #region IRememberBeingDirty - - // IEntitySlim does *not* track changes, but since it indirectly implements IUmbracoEntity, - // and therefore IRememberBeingDirty, we have to have those methods - which all throw. - - public bool IsDirty() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public bool IsPropertyDirty(string propName) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public IEnumerable GetDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public void ResetDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public bool WasDirty() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public bool WasPropertyDirty(string propertyName) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public void ResetWereDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public void ResetDirtyProperties(bool rememberDirty) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public IEnumerable GetWereDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion - + Id = default; + Key = Guid.Empty; } + + #region IRememberBeingDirty + + // IEntitySlim does *not* track changes, but since it indirectly implements IUmbracoEntity, + // and therefore IRememberBeingDirty, we have to have those methods - which all throw. + public bool IsDirty() => throw new InvalidOperationException("This method won't be implemented."); + + public bool IsPropertyDirty(string propName) => + throw new InvalidOperationException("This method won't be implemented."); + + public IEnumerable GetDirtyProperties() => + throw new InvalidOperationException("This method won't be implemented."); + + public void ResetDirtyProperties() => throw new InvalidOperationException("This method won't be implemented."); + + public bool WasDirty() => throw new InvalidOperationException("This method won't be implemented."); + + public bool WasPropertyDirty(string propertyName) => + throw new InvalidOperationException("This method won't be implemented."); + + public void ResetWereDirtyProperties() => throw new InvalidOperationException("This method won't be implemented."); + + public void ResetDirtyProperties(bool rememberDirty) => + throw new InvalidOperationException("This method won't be implemented."); + + public IEnumerable GetWereDirtyProperties() => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } diff --git a/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs b/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs index d8644431d5..23d50d54d9 100644 --- a/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs +++ b/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs @@ -1,43 +1,41 @@ -using System.Collections.Generic; using System.ComponentModel; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Defines an entity that tracks property changes and can be dirty. +/// +public interface ICanBeDirty { + event PropertyChangedEventHandler PropertyChanged; + /// - /// Defines an entity that tracks property changes and can be dirty. + /// Determines whether the current entity is dirty. /// - public interface ICanBeDirty - { - /// - /// Determines whether the current entity is dirty. - /// - bool IsDirty(); + bool IsDirty(); - /// - /// Determines whether a specific property is dirty. - /// - bool IsPropertyDirty(string propName); + /// + /// Determines whether a specific property is dirty. + /// + bool IsPropertyDirty(string propName); - /// - /// Gets properties that are dirty. - /// - IEnumerable GetDirtyProperties(); + /// + /// Gets properties that are dirty. + /// + IEnumerable GetDirtyProperties(); - /// - /// Resets dirty properties. - /// - void ResetDirtyProperties(); + /// + /// Resets dirty properties. + /// + void ResetDirtyProperties(); - /// - /// Disables change tracking. - /// - void DisableChangeTracking(); + /// + /// Disables change tracking. + /// + void DisableChangeTracking(); - /// - /// Enables change tracking. - /// - void EnableChangeTracking(); - - event PropertyChangedEventHandler PropertyChanged; - } + /// + /// Enables change tracking. + /// + void EnableChangeTracking(); } diff --git a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs index 52ea701af3..78ddf9bd82 100644 --- a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Represents a lightweight content entity, managed by the entity service. +/// +public interface IContentEntitySlim : IEntitySlim { /// - /// Represents a lightweight content entity, managed by the entity service. + /// Gets the content type alias. /// - public interface IContentEntitySlim : IEntitySlim - { - /// - /// Gets the content type alias. - /// - string ContentTypeAlias { get; } + string ContentTypeAlias { get; } - /// - /// Gets the content type icon. - /// - string? ContentTypeIcon { get; } + /// + /// Gets the content type icon. + /// + string? ContentTypeIcon { get; } - /// - /// Gets the content type thumbnail. - /// - string? ContentTypeThumbnail { get; } - } + /// + /// Gets the content type thumbnail. + /// + string? ContentTypeThumbnail { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs index d160e144bb..75e16476c2 100644 --- a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs @@ -1,42 +1,37 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Represents a lightweight document entity, managed by the entity service. +/// +public interface IDocumentEntitySlim : IContentEntitySlim { + /// + /// Gets the variant name for each culture + /// + IReadOnlyDictionary CultureNames { get; } /// - /// Represents a lightweight document entity, managed by the entity service. + /// Gets the published cultures. /// - public interface IDocumentEntitySlim : IContentEntitySlim - { - /// - /// Gets the variant name for each culture - /// - IReadOnlyDictionary CultureNames { get; } + IEnumerable PublishedCultures { get; } - /// - /// Gets the published cultures. - /// - IEnumerable PublishedCultures { get; } + /// + /// Gets the edited cultures. + /// + IEnumerable EditedCultures { get; } - /// - /// Gets the edited cultures. - /// - IEnumerable EditedCultures { get; } + /// + /// Gets the content variation of the content type. + /// + ContentVariation Variations { get; } - /// - /// Gets the content variation of the content type. - /// - ContentVariation Variations { get; } + /// + /// Gets a value indicating whether the content is published. + /// + bool Published { get; } - /// - /// Gets a value indicating whether the content is published. - /// - bool Published { get; } - - /// - /// Gets a value indicating whether the content has been edited. - /// - bool Edited { get; } - - } + /// + /// Gets a value indicating whether the content has been edited. + /// + bool Edited { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IEntity.cs b/src/Umbraco.Core/Models/Entities/IEntity.cs index 6aeea58553..859975adfb 100644 --- a/src/Umbraco.Core/Models/Entities/IEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IEntity.cs @@ -1,47 +1,46 @@ -using System; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Defines an entity. +/// +public interface IEntity : IDeepCloneable { /// - /// Defines an entity. + /// Gets or sets the integer identifier of the entity. /// - public interface IEntity : IDeepCloneable - { - /// - /// Gets or sets the integer identifier of the entity. - /// - int Id { get; set; } + int Id { get; set; } - /// - /// Gets or sets the Guid unique identifier of the entity. - /// - Guid Key { get; set; } + /// + /// Gets or sets the Guid unique identifier of the entity. + /// + Guid Key { get; set; } - /// - /// Gets or sets the creation date. - /// - DateTime CreateDate { get; set; } + /// + /// Gets or sets the creation date. + /// + DateTime CreateDate { get; set; } - /// - /// Gets or sets the last update date. - /// - DateTime UpdateDate { get; set; } + /// + /// Gets or sets the last update date. + /// + DateTime UpdateDate { get; set; } - /// - /// Gets or sets the delete date. - /// - /// - /// The delete date is null when the entity has not been deleted. - /// The delete date has a value when the entity instance has been deleted, but this value - /// is transient and not persisted in database (since the entity does not exist anymore). - /// - DateTime? DeleteDate { get; set; } + /// + /// Gets or sets the delete date. + /// + /// + /// The delete date is null when the entity has not been deleted. + /// + /// The delete date has a value when the entity instance has been deleted, but this value + /// is transient and not persisted in database (since the entity does not exist anymore). + /// + /// + DateTime? DeleteDate { get; set; } - /// - /// Gets a value indicating whether the entity has an identity. - /// - bool HasIdentity { get; } + /// + /// Gets a value indicating whether the entity has an identity. + /// + bool HasIdentity { get; } - void ResetIdentity(); - } + void ResetIdentity(); } diff --git a/src/Umbraco.Core/Models/Entities/IEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IEntitySlim.cs index dfdb00edaa..120d417d1a 100644 --- a/src/Umbraco.Core/Models/Entities/IEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IEntitySlim.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Represents a lightweight entity, managed by the entity service. +/// +public interface IEntitySlim : IUmbracoEntity, IHaveAdditionalData { /// - /// Represents a lightweight entity, managed by the entity service. + /// Gets or sets the entity object type. /// - public interface IEntitySlim : IUmbracoEntity, IHaveAdditionalData - { - /// - /// Gets or sets the entity object type. - /// - Guid NodeObjectType { get; } + Guid NodeObjectType { get; } - /// - /// Gets or sets a value indicating whether the entity has children. - /// - bool HasChildren { get; } + /// + /// Gets or sets a value indicating whether the entity has children. + /// + bool HasChildren { get; } - /// - /// Gets a value indicating whether the entity is a container. - /// - bool IsContainer { get; } - } + /// + /// Gets a value indicating whether the entity is a container. + /// + bool IsContainer { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs b/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs index 651e6a5f7a..a2ac3a247a 100644 --- a/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs +++ b/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs @@ -1,42 +1,43 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Provides support for additional data. +/// +/// +/// Additional data are transient, not deep-cloned. +/// +public interface IHaveAdditionalData { /// - /// Provides support for additional data. + /// Gets additional data for this entity. /// /// - /// Additional data are transient, not deep-cloned. + /// Can be empty, but never null. To avoid allocating, do not + /// test for emptiness, but use instead. /// - public interface IHaveAdditionalData - { - /// - /// Gets additional data for this entity. - /// - /// Can be empty, but never null. To avoid allocating, do not - /// test for emptiness, but use instead. - IDictionary? AdditionalData { get; } + IDictionary? AdditionalData { get; } - /// - /// Determines whether this entity has additional data. - /// - /// Use this property to check for additional data without - /// getting , to avoid allocating. - bool HasAdditionalData { get; } + /// + /// Determines whether this entity has additional data. + /// + /// + /// Use this property to check for additional data without + /// getting , to avoid allocating. + /// + bool HasAdditionalData { get; } - // how to implement: + // how to implement: - /* - private IDictionary _additionalData; + /* + private IDictionary _additionalData; - /// - [DataMember] - [DoNotClone] - PublicAccessEntry IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); + /// + [DataMember] + [DoNotClone] + PublicAccessEntry IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); - /// - [IgnoreDataMember] - PublicAccessEntry bool HasAdditionalData => _additionalData != null; - */ - } + /// + [IgnoreDataMember] + PublicAccessEntry bool HasAdditionalData => _additionalData != null; + */ } diff --git a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs index 3a2996c6fe..019a6f1f7b 100644 --- a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs @@ -1,14 +1,12 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Represents a lightweight media entity, managed by the entity service. +/// +public interface IMediaEntitySlim : IContentEntitySlim { /// - /// Represents a lightweight media entity, managed by the entity service. + /// The media file's path/URL /// - public interface IMediaEntitySlim : IContentEntitySlim - { - - /// - /// The media file's path/URL - /// - string? MediaPath { get; } - } + string? MediaPath { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs index a43607fda7..0ded537035 100644 --- a/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs @@ -1,7 +1,5 @@ -namespace Umbraco.Cms.Core.Models.Entities -{ - public interface IMemberEntitySlim : IContentEntitySlim - { +namespace Umbraco.Cms.Core.Models.Entities; - } +public interface IMemberEntitySlim : IContentEntitySlim +{ } diff --git a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs index 618bab2698..85c1c472b5 100644 --- a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs +++ b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs @@ -1,40 +1,40 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Defines an entity that tracks property changes and can be dirty, and remembers +/// which properties were dirty when the changes were committed. +/// +public interface IRememberBeingDirty : ICanBeDirty { /// - /// Defines an entity that tracks property changes and can be dirty, and remembers - /// which properties were dirty when the changes were committed. + /// Determines whether the current entity is dirty. /// - public interface IRememberBeingDirty : ICanBeDirty - { - /// - /// Determines whether the current entity is dirty. - /// - /// A property was dirty if it had been changed and the changes were committed. - bool WasDirty(); + /// A property was dirty if it had been changed and the changes were committed. + bool WasDirty(); - /// - /// Determines whether a specific property was dirty. - /// - /// A property was dirty if it had been changed and the changes were committed. - bool WasPropertyDirty(string propertyName); + /// + /// Determines whether a specific property was dirty. + /// + /// A property was dirty if it had been changed and the changes were committed. + bool WasPropertyDirty(string propertyName); - /// - /// Resets properties that were dirty. - /// - void ResetWereDirtyProperties(); + /// + /// Resets properties that were dirty. + /// + void ResetWereDirtyProperties(); - /// - /// Resets dirty properties. - /// - /// A value indicating whether to remember dirty properties. - /// When is true, dirty properties are saved so they can be checked with WasDirty. - void ResetDirtyProperties(bool rememberDirty); + /// + /// Resets dirty properties. + /// + /// A value indicating whether to remember dirty properties. + /// + /// When is true, dirty properties are saved so they can be checked with + /// WasDirty. + /// + void ResetDirtyProperties(bool rememberDirty); - /// - /// Gets properties that were dirty. - /// - IEnumerable GetWereDirtyProperties(); - } + /// + /// Gets properties that were dirty. + /// + IEnumerable GetWereDirtyProperties(); } diff --git a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs index af105d63ff..b66368e425 100644 --- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs +++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs @@ -1,56 +1,57 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Defines an entity that belongs to a tree. +/// +public interface ITreeEntity : IEntity { /// - /// Defines an entity that belongs to a tree. + /// Gets or sets the name of the entity. /// - public interface ITreeEntity : IEntity - { - /// - /// Gets or sets the name of the entity. - /// - string? Name { get; set; } + string? Name { get; set; } - /// - /// Gets or sets the identifier of the user who created this entity. - /// - int CreatorId { get; set; } + /// + /// Gets or sets the identifier of the user who created this entity. + /// + int CreatorId { get; set; } - /// - /// Gets or sets the identifier of the parent entity. - /// - int ParentId { get; set; } + /// + /// Gets or sets the identifier of the parent entity. + /// + int ParentId { get; set; } - /// - /// Sets the parent entity. - /// - /// Use this method to set the parent entity when the parent entity is known, but has not - /// been persistent and does not yet have an identity. The parent identifier will be retrieved - /// from the parent entity when needed. If the parent entity still does not have an entity by that - /// time, an exception will be thrown by getter. - void SetParent(ITreeEntity? parent); + /// + /// Gets or sets the level of the entity. + /// + int Level { get; set; } - /// - /// Gets or sets the level of the entity. - /// - int Level { get; set; } + /// + /// Gets or sets the path to the entity. + /// + string Path { get; set; } - /// - /// Gets or sets the path to the entity. - /// - string Path { get; set; } + /// + /// Gets or sets the sort order of the entity. + /// + int SortOrder { get; set; } - /// - /// Gets or sets the sort order of the entity. - /// - int SortOrder { get; set; } + /// + /// Gets a value indicating whether this entity is trashed. + /// + /// + /// Trashed entities are located in the recycle bin. + /// Always false for entities that do not support being trashed. + /// + bool Trashed { get; } - /// - /// Gets a value indicating whether this entity is trashed. - /// - /// - /// Trashed entities are located in the recycle bin. - /// Always false for entities that do not support being trashed. - /// - bool Trashed { get; } - } + /// + /// Sets the parent entity. + /// + /// + /// Use this method to set the parent entity when the parent entity is known, but has not + /// been persistent and does not yet have an identity. The parent identifier will be retrieved + /// from the parent entity when needed. If the parent entity still does not have an entity by that + /// time, an exception will be thrown by getter. + /// + void SetParent(ITreeEntity? parent); } diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index d89e5d9312..3d8c89c7c4 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Models.Entities -{ +namespace Umbraco.Cms.Core.Models.Entities; - /// - /// Represents an entity that can be managed by the entity service. - /// - /// - /// An IUmbracoEntity can be related to another via the IRelationService. - /// IUmbracoEntities can be retrieved with the IEntityService. - /// An IUmbracoEntity can participate in notifications. - /// - public interface IUmbracoEntity : ITreeEntity - { } +/// +/// Represents an entity that can be managed by the entity service. +/// +/// +/// An IUmbracoEntity can be related to another via the IRelationService. +/// IUmbracoEntities can be retrieved with the IEntityService. +/// An IUmbracoEntity can participate in notifications. +/// +public interface IUmbracoEntity : ITreeEntity +{ } diff --git a/src/Umbraco.Core/Models/Entities/IValueObject.cs b/src/Umbraco.Core/Models/Entities/IValueObject.cs index e1b7ea01a6..f101f531fa 100644 --- a/src/Umbraco.Core/Models/Entities/IValueObject.cs +++ b/src/Umbraco.Core/Models/Entities/IValueObject.cs @@ -1,11 +1,9 @@ -namespace Umbraco.Cms.Core.Models.Entities -{ - /// - /// Marker interface for value object, eg. objects without - /// the same kind of identity as an Entity (with its Id). - /// - public interface IValueObject - { +namespace Umbraco.Cms.Core.Models.Entities; - } +/// +/// Marker interface for value object, eg. objects without +/// the same kind of identity as an Entity (with its Id). +/// +public interface IValueObject +{ } diff --git a/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs index fd3c01e15c..fb73e2332d 100644 --- a/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Implements . +/// +public class MediaEntitySlim : ContentEntitySlim, IMediaEntitySlim { - /// - /// Implements . - /// - public class MediaEntitySlim : ContentEntitySlim, IMediaEntitySlim - { - public string? MediaPath { get; set; } - } + public string? MediaPath { get; set; } } diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs index 66e3650fc5..923fef2477 100644 --- a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs @@ -1,6 +1,5 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +public class MemberEntitySlim : ContentEntitySlim, IMemberEntitySlim { - public class MemberEntitySlim : ContentEntitySlim, IMemberEntitySlim - { - } } diff --git a/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs b/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs index b5d6f40a4c..f10e49b957 100644 --- a/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs +++ b/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs @@ -1,106 +1,120 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Provides a base class for tree entities. +/// +public abstract class TreeEntityBase : EntityBase, ITreeEntity { - /// - /// Provides a base class for tree entities. - /// - public abstract class TreeEntityBase : EntityBase, ITreeEntity + private int _creatorId; + private bool _hasParentId; + private int _level; + private string _name = null!; + private ITreeEntity? _parent; + private int _parentId; + private string _path = string.Empty; + private int _sortOrder; + private bool _trashed; + + /// + [DataMember] + public string? Name { - private string _name = null!; - private int _creatorId; - private int _parentId; - private bool _hasParentId; - private ITreeEntity? _parent; - private int _level; - private string _path = String.Empty; - private int _sortOrder; - private bool _trashed; + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } + /// + [DataMember] + public int CreatorId + { + get => _creatorId; + set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); + } - /// - [DataMember] - public int CreatorId + /// + [DataMember] + public int ParentId + { + get { - get => _creatorId; - set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); - } - - /// - [DataMember] - public int ParentId - { - get + if (_hasParentId) { - if (_hasParentId) return _parentId; - - if (_parent == null) throw new InvalidOperationException("Content does not have a parent."); - if (!_parent.HasIdentity) throw new InvalidOperationException("Content's parent does not have an identity."); - - _parentId = _parent.Id; - if (_parentId == 0) - throw new Exception("Panic: parent has an identity but id is zero."); - - _hasParentId = true; - _parent = null; return _parentId; } - set + + if (_parent == null) { - if (value == 0) - throw new ArgumentException("Value cannot be zero.", nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); - _hasParentId = true; - _parent = null; + throw new InvalidOperationException("Content does not have a parent."); } + + if (!_parent.HasIdentity) + { + throw new InvalidOperationException("Content's parent does not have an identity."); + } + + _parentId = _parent.Id; + if (_parentId == 0) + { + throw new Exception("Panic: parent has an identity but id is zero."); + } + + _hasParentId = true; + _parent = null; + return _parentId; } - /// - public void SetParent(ITreeEntity? parent) + set { - _hasParentId = false; - _parent = parent; - OnPropertyChanged(nameof(ParentId)); - } + if (value == 0) + { + throw new ArgumentException("Value cannot be zero.", nameof(value)); + } - /// - [DataMember] - public int Level - { - get => _level; - set => SetPropertyValueAndDetectChanges(value, ref _level, nameof(Level)); - } - - /// - [DataMember] - public string Path - { - get => _path; - set => SetPropertyValueAndDetectChanges(value, ref _path!, nameof(Path)); - } - - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } - - /// - [DataMember] - public bool Trashed - { - get => _trashed; - set => SetPropertyValueAndDetectChanges(value, ref _trashed, nameof(Trashed)); + SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + _hasParentId = true; + _parent = null; } } + + /// + [DataMember] + public int Level + { + get => _level; + set => SetPropertyValueAndDetectChanges(value, ref _level, nameof(Level)); + } + + /// + public void SetParent(ITreeEntity? parent) + { + _hasParentId = false; + _parent = parent; + OnPropertyChanged(nameof(ParentId)); + } + + /// + [DataMember] + public string Path + { + get => _path; + set => SetPropertyValueAndDetectChanges(value, ref _path!, nameof(Path)); + } + + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } + + /// + [DataMember] + public bool Trashed + { + get => _trashed; + set => SetPropertyValueAndDetectChanges(value, ref _trashed, nameof(Trashed)); + } } diff --git a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs index 6fd147ace7..fe284a1e11 100644 --- a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs +++ b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Represents the path of a tree entity. +/// +public class TreeEntityPath { /// - /// Represents the path of a tree entity. + /// Gets or sets the identifier of the entity. /// - public class TreeEntityPath - { - /// - /// Gets or sets the identifier of the entity. - /// - public int Id { get; set; } + public int Id { get; set; } - /// - /// Gets or sets the path of the entity. - /// - public string Path { get; set; } = null!; - } + /// + /// Gets or sets the path of the entity. + /// + public string Path { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 114d78605c..762297af07 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -1,88 +1,91 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a folder for organizing entities such as content types and data types. +/// +public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity { - /// - /// Represents a folder for organizing entities such as content types and data types. - /// - public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity + private static readonly Dictionary ObjectTypeMap = new() { - private readonly Guid _containedObjectType; + { Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer }, + { Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer }, + { Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer }, + }; - private static readonly Dictionary ObjectTypeMap = new Dictionary + /// + /// Initializes a new instance of an class. + /// + public EntityContainer(Guid containedObjectType) + { + if (ObjectTypeMap.ContainsKey(containedObjectType) == false) { - { Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer }, - { Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer }, - { Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer } - }; - - /// - /// Initializes a new instance of an class. - /// - public EntityContainer(Guid containedObjectType) - { - if (ObjectTypeMap.ContainsKey(containedObjectType) == false) - throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); - _containedObjectType = containedObjectType; - - ParentId = -1; - Path = "-1"; - Level = 0; - SortOrder = 0; + throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); } - /// - /// Initializes a new instance of an class. - /// - public EntityContainer(int id, Guid uniqueId, int parentId, string path, int level, int sortOrder, Guid containedObjectType, string? name, int userId) - : this(containedObjectType) + ContainedObjectType = containedObjectType; + + ParentId = -1; + Path = "-1"; + Level = 0; + SortOrder = 0; + } + + /// + /// Initializes a new instance of an class. + /// + public EntityContainer(int id, Guid uniqueId, int parentId, string path, int level, int sortOrder, Guid containedObjectType, string? name, int userId) + : this(containedObjectType) + { + Id = id; + Key = uniqueId; + ParentId = parentId; + Name = name; + Path = path; + Level = level; + SortOrder = sortOrder; + CreatorId = userId; + } + + /// + /// Gets or sets the node object type of the contained objects. + /// + public Guid ContainedObjectType { get; } + + /// + /// Gets the node object type of the container objects. + /// + public Guid ContainerObjectType => ObjectTypeMap[ContainedObjectType]; + + /// + /// Gets the container object type corresponding to a contained object type. + /// + /// The contained object type. + /// The object type of containers containing objects of the contained object type. + public static Guid GetContainerObjectType(Guid containedObjectType) + { + if (ObjectTypeMap.ContainsKey(containedObjectType) == false) { - Id = id; - Key = uniqueId; - ParentId = parentId; - Name = name; - Path = path; - Level = level; - SortOrder = sortOrder; - CreatorId = userId; + throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); } - /// - /// Gets or sets the node object type of the contained objects. - /// - public Guid ContainedObjectType => _containedObjectType; + return ObjectTypeMap[containedObjectType]; + } - /// - /// Gets the node object type of the container objects. - /// - public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; - - /// - /// Gets the container object type corresponding to a contained object type. - /// - /// The contained object type. - /// The object type of containers containing objects of the contained object type. - public static Guid GetContainerObjectType(Guid containedObjectType) + /// + /// Gets the contained object type corresponding to a container object type. + /// + /// The container object type. + /// The object type of objects that containers of the container object type can contain. + public static Guid GetContainedObjectType(Guid containerObjectType) + { + Guid contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key; + if (contained == null) { - if (ObjectTypeMap.ContainsKey(containedObjectType) == false) - throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); - return ObjectTypeMap[containedObjectType]; + throw new ArgumentException("Not a container object type.", nameof(containerObjectType)); } - /// - /// Gets the contained object type corresponding to a container object type. - /// - /// The container object type. - /// The object type of objects that containers of the container object type can contain. - public static Guid GetContainedObjectType(Guid containerObjectType) - { - var contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key; - if (contained == null) - throw new ArgumentException("Not a container object type.", nameof(containerObjectType)); - return contained; - } + return contained; } } diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index 3865d4eee7..8abfdd1ef5 100644 --- a/src/Umbraco.Core/Models/File.cs +++ b/src/Umbraco.Core/Models/File.cs @@ -1,160 +1,154 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract file which provides basic functionality for a File with an Alias and Name +/// +[Serializable] +[DataContract(IsReference = true)] +public abstract class File : EntityBase, IFile { - /// - /// Represents an abstract file which provides basic functionality for a File with an Alias and Name - /// - [Serializable] - [DataContract(IsReference = true)] - public abstract class File : EntityBase, IFile + private string? _alias; + + // initialize to string.Empty so that it is possible to save a new file, + // should use the lazyContent ctor to set it to null when loading existing. + // cannot simply use HasIdentity as some classes (eg Script) override it + // in a weird way. + private string? _content; + private string? _name; + private string _path; + + protected File(string path, Func? getFileContent = null) { - private string _path; - private string _originalPath; + _path = SanitizePath(path); + OriginalPath = _path; + GetFileContent = getFileContent; + _content = getFileContent != null ? null : string.Empty; + } - // initialize to string.Empty so that it is possible to save a new file, - // should use the lazyContent ctor to set it to null when loading existing. - // cannot simply use HasIdentity as some classes (eg Script) override it - // in a weird way. - private string? _content; - public Func? GetFileContent { get; set; } + public Func? GetFileContent { get; set; } - protected File(string path, Func? getFileContent = null) + /// + /// Gets or sets the Name of the File including extension + /// + [DataMember] + public virtual string Name => _name ??= System.IO.Path.GetFileName(Path); + + /// + /// Gets or sets the Alias of the File, which is the name without the extension + /// + [DataMember] + public virtual string Alias + { + get { - _path = SanitizePath(path); - _originalPath = _path; - GetFileContent = getFileContent; - _content = getFileContent != null ? null : string.Empty; - } - - private string? _alias; - private string? _name; - - private static string SanitizePath(string path) - { - return path - .Replace('\\', System.IO.Path.DirectorySeparatorChar) - .Replace('/', System.IO.Path.DirectorySeparatorChar); - - //Don't strip the start - this was a bug fixed in 7.3, see ScriptRepositoryTests.PathTests - //.TrimStart(System.IO.Path.DirectorySeparatorChar) - //.TrimStart('/'); - } - - /// - /// Gets or sets the Name of the File including extension - /// - [DataMember] - public virtual string Name - { - get { return _name ?? (_name = System.IO.Path.GetFileName(Path)); } - } - - /// - /// Gets or sets the Alias of the File, which is the name without the extension - /// - [DataMember] - public virtual string Alias - { - get + if (_alias == null) { - if (_alias == null) + var name = System.IO.Path.GetFileName(Path); + if (name == null) { - var name = System.IO.Path.GetFileName(Path); - if (name == null) return string.Empty; - var lastIndexOf = name.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase); - _alias = name.Substring(0, lastIndexOf); + return string.Empty; } - return _alias; + + var lastIndexOf = name.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase); + _alias = name.Substring(0, lastIndexOf); } - } - /// - /// Gets or sets the Path to the File from the root of the file's associated IFileSystem - /// - [DataMember] - public virtual string Path - { - get { return _path; } - set - { - //reset - _alias = null; - _name = null; - - SetPropertyValueAndDetectChanges(SanitizePath(value), ref _path!, nameof(Path)); - } - } - - /// - /// Gets the original path of the file - /// - public string OriginalPath - { - get { return _originalPath; } - } - - /// - /// Called to re-set the OriginalPath to the Path - /// - public void ResetOriginalPath() - { - _originalPath = _path; - } - - /// - /// Gets or sets the Content of a File - /// - /// Marked as DoNotClone, because it should be lazy-reloaded from disk. - [DataMember] - [DoNotClone] - public virtual string? Content - { - get - { - if (_content != null) - return _content; - - // else, must lazy-load, and ensure it's not null - if (GetFileContent != null) - _content = GetFileContent(this); - return _content ?? (_content = string.Empty); - } - set - { - SetPropertyValueAndDetectChanges( - value ?? string.Empty, // cannot set to null - ref _content, nameof(Content)); - } - } - - /// - /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) - /// - public string? VirtualPath { get; set; } - - // this exists so that class that manage name and alias differently, eg Template, - // can implement their own cloning - (though really, not sure it's even needed) - protected virtual void DeepCloneNameAndAlias(File clone) - { - // set fields that have a lazy value, by forcing evaluation of the lazy - clone._name = Name; - clone._alias = Alias; - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedFile = (File)clone; - - // clear fields that were memberwise-cloned and that we don't want to clone - clonedFile._content = null; - - // ... - DeepCloneNameAndAlias(clonedFile); + return _alias; } } + + /// + /// Gets or sets the Path to the File from the root of the file's associated IFileSystem + /// + [DataMember] + public virtual string Path + { + get => _path; + set + { + // reset + _alias = null; + _name = null; + + SetPropertyValueAndDetectChanges(SanitizePath(value), ref _path!, nameof(Path)); + } + } + + /// + /// Gets the original path of the file + /// + public string OriginalPath { get; private set; } + + /// + /// Gets or sets the Content of a File + /// + /// Marked as DoNotClone, because it should be lazy-reloaded from disk. + [DataMember] + [DoNotClone] + public virtual string? Content + { + get + { + if (_content != null) + { + return _content; + } + + // else, must lazy-load, and ensure it's not null + if (GetFileContent != null) + { + _content = GetFileContent(this); + } + + return _content ??= string.Empty; + } + set => + SetPropertyValueAndDetectChanges( + value ?? string.Empty, // cannot set to null + ref _content, + nameof(Content)); + } + + /// + /// Called to re-set the OriginalPath to the Path + /// + public void ResetOriginalPath() => OriginalPath = _path; + + /// + /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) + /// + public string? VirtualPath { get; set; } + + // Don't strip the start - this was a bug fixed in 7.3, see ScriptRepositoryTests.PathTests + // .TrimStart(System.IO.Path.DirectorySeparatorChar) + // .TrimStart('/'); + // this exists so that class that manage name and alias differently, eg Template, + // can implement their own cloning - (though really, not sure it's even needed) + protected virtual void DeepCloneNameAndAlias(File clone) + { + // set fields that have a lazy value, by forcing evaluation of the lazy + clone._name = Name; + clone._alias = Alias; + } + + private static string SanitizePath(string path) => + path + .Replace('\\', System.IO.Path.DirectorySeparatorChar) + .Replace('/', System.IO.Path.DirectorySeparatorChar); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedFile = (File)clone; + + // clear fields that were memberwise-cloned and that we don't want to clone + clonedFile._content = null; + + // ... + DeepCloneNameAndAlias(clonedFile); + } } diff --git a/src/Umbraco.Core/Models/Folder.cs b/src/Umbraco.Core/Models/Folder.cs index 810bcaf3b3..60e636ca6e 100644 --- a/src/Umbraco.Core/Models/Folder.cs +++ b/src/Umbraco.Core/Models/Folder.cs @@ -1,14 +1,10 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public sealed class Folder : EntityBase { - public sealed class Folder : EntityBase - { - public Folder(string folderPath) - { - Path = folderPath; - } + public Folder(string folderPath) => Path = folderPath; - public string Path { get; set; } - } + public string Path { get; set; } } diff --git a/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs b/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs index 1c1c377403..79db47414a 100644 --- a/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs +++ b/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs @@ -1,20 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class HaveAdditionalDataExtensions { - public static class HaveAdditionalDataExtensions + /// + /// Gets additional data. + /// + public static object? GetAdditionalDataValueIgnoreCase(this IHaveAdditionalData entity, string key, object? defaultValue) { - /// - /// Gets additional data. - /// - public static object? GetAdditionalDataValueIgnoreCase(this IHaveAdditionalData entity, string key, object? defaultValue) + if (!entity.HasAdditionalData) { - if (!entity.HasAdditionalData) return defaultValue; - if (entity.AdditionalData?.ContainsKeyIgnoreCase(key) == false) return defaultValue; - return entity.AdditionalData?.GetValueIgnoreCase(key, defaultValue); + return defaultValue; } + + if (entity.AdditionalData?.ContainsKeyIgnoreCase(key) == false) + { + return defaultValue; + } + + return entity.AdditionalData?.GetValueIgnoreCase(key, defaultValue); } } diff --git a/src/Umbraco.Core/Models/IAuditEntry.cs b/src/Umbraco.Core/Models/IAuditEntry.cs index e12237f06d..3a1b412ce0 100644 --- a/src/Umbraco.Core/Models/IAuditEntry.cs +++ b/src/Umbraco.Core/Models/IAuditEntry.cs @@ -1,60 +1,62 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an audited event. +/// +/// +/// +/// The free-form details properties can be used to capture relevant infos (for example, +/// a user email and identifier) at the time of the audited event, even though they may change +/// later on - but we want to keep a track of their value at that time. +/// +/// +/// Depending on audit loggers, these properties can be purely free-form text, or +/// contain json serialized objects. +/// +/// +public interface IAuditEntry : IEntity, IRememberBeingDirty { /// - /// Represents an audited event. + /// Gets or sets the identifier of the user triggering the audited event. /// - /// - /// The free-form details properties can be used to capture relevant infos (for example, - /// a user email and identifier) at the time of the audited event, even though they may change - /// later on - but we want to keep a track of their value at that time. - /// Depending on audit loggers, these properties can be purely free-form text, or - /// contain json serialized objects. - /// - public interface IAuditEntry : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the identifier of the user triggering the audited event. - /// - int PerformingUserId { get; set; } + int PerformingUserId { get; set; } - /// - /// Gets or sets free-form details about the user triggering the audited event. - /// - string? PerformingDetails { get; set; } + /// + /// Gets or sets free-form details about the user triggering the audited event. + /// + string? PerformingDetails { get; set; } - /// - /// Gets or sets the IP address or the request triggering the audited event. - /// - string? PerformingIp { get; set; } + /// + /// Gets or sets the IP address or the request triggering the audited event. + /// + string? PerformingIp { get; set; } - /// - /// Gets or sets the date and time of the audited event. - /// - DateTime EventDateUtc { get; set; } + /// + /// Gets or sets the date and time of the audited event. + /// + DateTime EventDateUtc { get; set; } - /// - /// Gets or sets the identifier of the user affected by the audited event. - /// - /// Not used when no single user is affected by the event. - int AffectedUserId { get; set; } + /// + /// Gets or sets the identifier of the user affected by the audited event. + /// + /// Not used when no single user is affected by the event. + int AffectedUserId { get; set; } - /// - /// Gets or sets free-form details about the entity affected by the audited event. - /// - /// The entity affected by the event can be another user, a member... - string? AffectedDetails { get; set; } + /// + /// Gets or sets free-form details about the entity affected by the audited event. + /// + /// The entity affected by the event can be another user, a member... + string? AffectedDetails { get; set; } - /// - /// Gets or sets the type of the audited event. - /// - string? EventType { get; set; } + /// + /// Gets or sets the type of the audited event. + /// + string? EventType { get; set; } - /// - /// Gets or sets free-form details about the audited event. - /// - string? EventDetails { get; set; } - } + /// + /// Gets or sets free-form details about the audited event. + /// + string? EventDetails { get; set; } } diff --git a/src/Umbraco.Core/Models/IAuditItem.cs b/src/Umbraco.Core/Models/IAuditItem.cs index dbc7ad1fd4..dbf4fe01e8 100644 --- a/src/Umbraco.Core/Models/IAuditItem.cs +++ b/src/Umbraco.Core/Models/IAuditItem.cs @@ -1,35 +1,34 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an audit item. +/// +public interface IAuditItem : IEntity { /// - /// Represents an audit item. + /// Gets the audit type. /// - public interface IAuditItem : IEntity - { - /// - /// Gets the audit type. - /// - AuditType AuditType { get; } + AuditType AuditType { get; } - /// - /// Gets the audited entity type. - /// - string? EntityType { get; } + /// + /// Gets the audited entity type. + /// + string? EntityType { get; } - /// - /// Gets the audit user identifier. - /// - int UserId { get; } + /// + /// Gets the audit user identifier. + /// + int UserId { get; } - /// - /// Gets the audit comments. - /// - string? Comment { get; } + /// + /// Gets the audit comments. + /// + string? Comment { get; } - /// - /// Gets optional additional data parameters. - /// - string? Parameters { get; } - } + /// + /// Gets optional additional data parameters. + /// + string? Parameters { get; } } diff --git a/src/Umbraco.Core/Models/IConsent.cs b/src/Umbraco.Core/Models/IConsent.cs index 747e7a145c..bae0294283 100644 --- a/src/Umbraco.Core/Models/IConsent.cs +++ b/src/Umbraco.Core/Models/IConsent.cs @@ -1,55 +1,55 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a consent state. +/// +/// +/// +/// A consent is fully identified by a source (whoever is consenting), a context (for +/// example, an application), and an action (whatever is consented). +/// +/// A consent state registers the state of the consent (granted, revoked...). +/// +public interface IConsent : IEntity, IRememberBeingDirty { /// - /// Represents a consent state. + /// Determines whether the consent entity represents the current state. + /// + bool Current { get; } + + /// + /// Gets the unique identifier of whoever is consenting. + /// + string? Source { get; } + + /// + /// Gets the unique identifier of the context of the consent. /// /// - /// A consent is fully identified by a source (whoever is consenting), a context (for - /// example, an application), and an action (whatever is consented). - /// A consent state registers the state of the consent (granted, revoked...). + /// Represents the domain, application, scope... of the action. + /// When the action is a Udi, this should be the Udi type. /// - public interface IConsent : IEntity, IRememberBeingDirty - { - /// - /// Determines whether the consent entity represents the current state. - /// - bool Current { get; } + string? Context { get; } - /// - /// Gets the unique identifier of whoever is consenting. - /// - string? Source { get; } + /// + /// Gets the unique identifier of the consented action. + /// + string? Action { get; } - /// - /// Gets the unique identifier of the context of the consent. - /// - /// - /// Represents the domain, application, scope... of the action. - /// When the action is a Udi, this should be the Udi type. - /// - string? Context { get; } + /// + /// Gets the state of the consent. + /// + ConsentState State { get; } - /// - /// Gets the unique identifier of the consented action. - /// - string? Action { get; } + /// + /// Gets some additional free text. + /// + string? Comment { get; } - /// - /// Gets the state of the consent. - /// - ConsentState State { get; } - - /// - /// Gets some additional free text. - /// - string? Comment { get; } - - /// - /// Gets the previous states of this consent. - /// - IEnumerable? History { get; } - } + /// + /// Gets the previous states of this consent. + /// + IEnumerable? History { get; } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index e538b307e2..9e36306cfc 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -1,131 +1,138 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a document. +/// +/// +/// A document can be published, rendered by a template. +/// +public interface IContent : IContentBase { + /// + /// Gets or sets the template id used to render the content. + /// + int? TemplateId { get; set; } /// - /// Represents a document. + /// Gets a value indicating whether the content is published. + /// + /// The property tells you which version of the content is currently published. + bool Published { get; set; } + + PublishedState PublishedState { get; set; } + + /// + /// Gets a value indicating whether the content has been edited. /// /// - /// A document can be published, rendered by a template. + /// Will return `true` once unpublished edits have been made after the version with + /// has been published. /// - public interface IContent : IContentBase - { - /// - /// Gets or sets the template id used to render the content. - /// - int? TemplateId { get; set; } + bool Edited { get; set; } - /// - /// Gets a value indicating whether the content is published. - /// - /// The property tells you which version of the content is currently published. - bool Published { get; set; } + /// + /// Gets the version identifier for the currently published version of the content. + /// + int PublishedVersionId { get; set; } - PublishedState PublishedState { get; set; } + /// + /// Gets a value indicating whether the content item is a blueprint. + /// + bool Blueprint { get; set; } - /// - /// Gets a value indicating whether the content has been edited. - /// - /// Will return `true` once unpublished edits have been made after the version with has been published. - bool Edited { get; set; } + /// + /// Gets the template id used to render the published version of the content. + /// + /// When editing the content, the template can change, but this will not until the content is published. + int? PublishTemplateId { get; set; } - /// - /// Gets the version identifier for the currently published version of the content. - /// - int PublishedVersionId { get; set; } + /// + /// Gets the name of the published version of the content. + /// + /// When editing the content, the name can change, but this will not until the content is published. + string? PublishName { get; set; } - /// - /// Gets a value indicating whether the content item is a blueprint. - /// - bool Blueprint { get; set; } + /// + /// Gets the identifier of the user who published the content. + /// + int? PublisherId { get; set; } - /// - /// Gets the template id used to render the published version of the content. - /// - /// When editing the content, the template can change, but this will not until the content is published. - int? PublishTemplateId { get; set; } + /// + /// Gets the date and time the content was published. + /// + DateTime? PublishDate { get; set; } - /// - /// Gets the name of the published version of the content. - /// - /// When editing the content, the name can change, but this will not until the content is published. - string? PublishName { get; set; } + /// + /// Gets the published culture infos of the content. + /// + /// + /// + /// Because a dictionary key cannot be null this cannot get the invariant + /// name, which must be get via the property. + /// + /// + ContentCultureInfosCollection? PublishCultureInfos { get; set; } - /// - /// Gets the identifier of the user who published the content. - /// - int? PublisherId { get; set; } + /// + /// Gets the published cultures. + /// + IEnumerable PublishedCultures { get; } - /// - /// Gets the date and time the content was published. - /// - DateTime? PublishDate { get; set; } + /// + /// Gets the edited cultures. + /// + IEnumerable? EditedCultures { get; set; } - /// - /// Gets a value indicating whether a culture is published. - /// - /// - /// A culture becomes published whenever values for this culture are published, - /// and the content published name for this culture is non-null. It becomes non-published - /// whenever values for this culture are unpublished. - /// A culture becomes published as soon as PublishCulture has been invoked, - /// even though the document might not have been saved yet (and can have no identity). - /// Does not support the '*' wildcard (returns false). - /// - bool IsCulturePublished(string culture); + /// + /// Gets a value indicating whether a culture is published. + /// + /// + /// + /// A culture becomes published whenever values for this culture are published, + /// and the content published name for this culture is non-null. It becomes non-published + /// whenever values for this culture are unpublished. + /// + /// + /// A culture becomes published as soon as PublishCulture has been invoked, + /// even though the document might not have been saved yet (and can have no identity). + /// + /// Does not support the '*' wildcard (returns false). + /// + bool IsCulturePublished(string culture); - /// - /// Gets the date a culture was published. - /// - DateTime? GetPublishDate(string culture); + /// + /// Gets the date a culture was published. + /// + DateTime? GetPublishDate(string culture); - /// - /// Gets a value indicated whether a given culture is edited. - /// - /// - /// A culture is edited when it is available, and not published or published but - /// with changes. - /// A culture can be edited even though the document might now have been saved yet (and can have no identity). - /// Does not support the '*' wildcard (returns false). - /// - bool IsCultureEdited(string culture); + /// + /// Gets a value indicated whether a given culture is edited. + /// + /// + /// + /// A culture is edited when it is available, and not published or published but + /// with changes. + /// + /// A culture can be edited even though the document might now have been saved yet (and can have no identity). + /// Does not support the '*' wildcard (returns false). + /// + bool IsCultureEdited(string culture); - /// - /// Gets the name of the published version of the content for a given culture. - /// - /// - /// When editing the content, the name can change, but this will not until the content is published. - /// When is null, gets the invariant - /// language, which is the value of the property. - /// - string? GetPublishName(string? culture); + /// + /// Gets the name of the published version of the content for a given culture. + /// + /// + /// When editing the content, the name can change, but this will not until the content is published. + /// + /// When is null, gets the invariant + /// language, which is the value of the property. + /// + /// + string? GetPublishName(string? culture); - /// - /// Gets the published culture infos of the content. - /// - /// - /// Because a dictionary key cannot be null this cannot get the invariant - /// name, which must be get via the property. - /// - ContentCultureInfosCollection? PublishCultureInfos { get; set; } - - /// - /// Gets the published cultures. - /// - IEnumerable PublishedCultures { get; } - - /// - /// Gets the edited cultures. - /// - IEnumerable? EditedCultures { get; set; } - - /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset - /// - /// - IContent DeepCloneWithResetIdentities(); - - } + /// + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// + /// + IContent DeepCloneWithResetIdentities(); } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 20e78816ae..5f4ccf5244 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -1,130 +1,141 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Provides a base class for content items. +/// +/// +/// Content items are documents, medias and members. +/// Content items have a content type, and properties. +/// +public interface IContentBase : IUmbracoEntity, IRememberBeingDirty { + /// + /// Integer Id of the default ContentType + /// + int ContentTypeId { get; } /// - /// Provides a base class for content items. + /// Gets the content type of this content. + /// + ISimpleContentType ContentType { get; } + + /// + /// Gets the identifier of the writer. + /// + int WriterId { get; set; } + + /// + /// Gets the version identifier. + /// + int VersionId { get; set; } + + /// + /// Gets culture infos of the content item. /// /// - /// Content items are documents, medias and members. - /// Content items have a content type, and properties. + /// + /// Because a dictionary key cannot be null this cannot contain the invariant + /// culture name, which must be get or set via the property. + /// /// - public interface IContentBase : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Integer Id of the default ContentType - /// - int ContentTypeId { get; } + ContentCultureInfosCollection? CultureInfos { get; set; } - /// - /// Gets the content type of this content. - /// - ISimpleContentType ContentType { get; } + /// + /// Gets the available cultures. + /// + /// + /// Cannot contain the invariant culture, which is always available. + /// + IEnumerable AvailableCultures { get; } - /// - /// Gets the identifier of the writer. - /// - int WriterId { get; set; } + /// + /// List of properties, which make up all the data available for this Content object + /// + /// Properties are loaded as part of the Content object graph + IPropertyCollection Properties { get; set; } - /// - /// Gets the version identifier. - /// - int VersionId { get; set; } + /// + /// Sets the name of the content item for a specified culture. + /// + /// + /// + /// When is null, sets the invariant + /// culture name, which sets the property. + /// + /// + /// When is not null, throws if the content + /// type does not vary by culture. + /// + /// + void SetCultureName(string? value, string? culture); - /// - /// Sets the name of the content item for a specified culture. - /// - /// - /// When is null, sets the invariant - /// culture name, which sets the property. - /// When is not null, throws if the content - /// type does not vary by culture. - /// - void SetCultureName(string? value, string? culture); + /// + /// Gets the name of the content item for a specified language. + /// + /// + /// + /// When is null, gets the invariant + /// culture name, which is the value of the property. + /// + /// + /// When is not null, and the content type + /// does not vary by culture, returns null. + /// + /// + string? GetCultureName(string? culture); - /// - /// Gets the name of the content item for a specified language. - /// - /// - /// When is null, gets the invariant - /// culture name, which is the value of the property. - /// When is not null, and the content type - /// does not vary by culture, returns null. - /// - string? GetCultureName(string? culture); + /// + /// Gets a value indicating whether a given culture is available. + /// + /// + /// + /// A culture becomes available whenever the content name for this culture is + /// non-null, and it becomes unavailable whenever the content name is null. + /// + /// + /// Returns false for the invariant culture, in order to be consistent + /// with , even though the invariant culture is + /// always available. + /// + /// Does not support the '*' wildcard (returns false). + /// + bool IsCultureAvailable(string culture); - /// - /// Gets culture infos of the content item. - /// - /// - /// Because a dictionary key cannot be null this cannot contain the invariant - /// culture name, which must be get or set via the property. - /// - ContentCultureInfosCollection? CultureInfos { get; set; } + /// + /// Gets the date a culture was updated. + /// + /// + /// When is null, returns null. + /// If the specified culture is not available, returns null. + /// + DateTime? GetUpdateDate(string culture); - /// - /// Gets the available cultures. - /// - /// - /// Cannot contain the invariant culture, which is always available. - /// - IEnumerable AvailableCultures { get; } + /// + /// Gets a value indicating whether the content entity has a property with the supplied alias. + /// + /// + /// Indicates that the content entity has a property with the supplied alias, but + /// not necessarily that the content has a value for that property. Could be missing. + /// + bool HasProperty(string propertyTypeAlias); - /// - /// Gets a value indicating whether a given culture is available. - /// - /// - /// A culture becomes available whenever the content name for this culture is - /// non-null, and it becomes unavailable whenever the content name is null. - /// Returns false for the invariant culture, in order to be consistent - /// with , even though the invariant culture is - /// always available. - /// Does not support the '*' wildcard (returns false). - /// - bool IsCultureAvailable(string culture); + /// + /// Gets the value of a Property + /// + /// Values 'null' and 'empty' are equivalent for culture and segment. + object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - /// - /// Gets the date a culture was updated. - /// - /// - /// When is null, returns null. - /// If the specified culture is not available, returns null. - /// - DateTime? GetUpdateDate(string culture); + /// + /// Gets the typed value of a Property + /// + /// Values 'null' and 'empty' are equivalent for culture and segment. + TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - /// - /// List of properties, which make up all the data available for this Content object - /// - /// Properties are loaded as part of the Content object graph - IPropertyCollection Properties { get; set; } - - /// - /// Gets a value indicating whether the content entity has a property with the supplied alias. - /// - /// Indicates that the content entity has a property with the supplied alias, but - /// not necessarily that the content has a value for that property. Could be missing. - bool HasProperty(string propertyTypeAlias); - - /// - /// Gets the value of a Property - /// - /// Values 'null' and 'empty' are equivalent for culture and segment. - object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - - /// - /// Gets the typed value of a Property - /// - /// Values 'null' and 'empty' are equivalent for culture and segment. - TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - - /// - /// Sets the (edited) value of a Property - /// - /// Values 'null' and 'empty' are equivalent for culture and segment. - void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null); - - } + /// + /// Sets the (edited) value of a Property + /// + /// Values 'null' and 'empty' are equivalent for culture and segment. + void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null); } diff --git a/src/Umbraco.Core/Models/IContentModel.cs b/src/Umbraco.Core/Models/IContentModel.cs index 8aa8c18306..c7669dfbe4 100644 --- a/src/Umbraco.Core/Models/IContentModel.cs +++ b/src/Umbraco.Core/Models/IContentModel.cs @@ -1,29 +1,34 @@ using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The basic view model returned for front-end Umbraco controllers +/// +/// +/// +/// exists in order to unify all view models in Umbraco, whether it's a normal +/// template view or a partial view macro, or +/// a user's custom model that they have created when doing route hijacking or custom routes. +/// +/// +/// By default all front-end template views inherit from UmbracoViewPage which has a model of +/// but the model returned +/// from the controllers is which in normal circumstances would not work. This works +/// with UmbracoViewPage because it +/// performs model binding between IContentModel and IPublishedContent. This offers a lot of flexibility when +/// rendering views. In some cases if you +/// are route hijacking and returning a custom implementation of and your view is +/// strongly typed to this model, you can still +/// render partial views created in the back office that have the default model of IPublishedContent without having +/// to worry about explicitly passing +/// that model to the view. +/// +/// +public interface IContentModel { /// - /// The basic view model returned for front-end Umbraco controllers + /// Gets the /// - /// - /// - /// exists in order to unify all view models in Umbraco, whether it's a normal template view or a partial view macro, or - /// a user's custom model that they have created when doing route hijacking or custom routes. - /// - /// - /// By default all front-end template views inherit from UmbracoViewPage which has a model of but the model returned - /// from the controllers is which in normal circumstances would not work. This works with UmbracoViewPage because it - /// performs model binding between IContentModel and IPublishedContent. This offers a lot of flexibility when rendering views. In some cases if you - /// are route hijacking and returning a custom implementation of and your view is strongly typed to this model, you can still - /// render partial views created in the back office that have the default model of IPublishedContent without having to worry about explicitly passing - /// that model to the view. - /// - /// - public interface IContentModel - { - /// - /// Gets the - /// - IPublishedContent Content { get; } - } + IPublishedContent Content { get; } } diff --git a/src/Umbraco.Core/Models/IContentType.cs b/src/Umbraco.Core/Models/IContentType.cs index 4d693f9a50..f43764faeb 100644 --- a/src/Umbraco.Core/Models/IContentType.cs +++ b/src/Umbraco.Core/Models/IContentType.cs @@ -1,67 +1,64 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a ContentType, which Content is based on +/// +public interface IContentType : IContentTypeComposition { /// - /// Defines a ContentType, which Content is based on + /// Internal property to store the Id of the default template /// - public interface IContentType : IContentTypeComposition - { - /// - /// Internal property to store the Id of the default template - /// - int DefaultTemplateId { get; set; } + int DefaultTemplateId { get; set; } - /// - /// Gets the default Template of the ContentType - /// - ITemplate? DefaultTemplate { get; } + /// + /// Gets the default Template of the ContentType + /// + ITemplate? DefaultTemplate { get; } - /// - /// Gets or Sets a list of Templates which are allowed for the ContentType - /// - IEnumerable? AllowedTemplates { get; set; } + /// + /// Gets or Sets a list of Templates which are allowed for the ContentType + /// + IEnumerable? AllowedTemplates { get; set; } - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template id to check - /// True if AllowedTemplates contains the templateId else False - bool IsAllowedTemplate(int templateId); + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template id to check + /// True if AllowedTemplates contains the templateId else False + bool IsAllowedTemplate(int templateId); - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template alias to check - /// True if AllowedTemplates contains the templateAlias else False - bool IsAllowedTemplate(string templateAlias); + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template alias to check + /// True if AllowedTemplates contains the templateAlias else False + bool IsAllowedTemplate(string templateAlias); - /// - /// Sets the default template for the ContentType - /// - /// Default - void SetDefaultTemplate(ITemplate? template); + /// + /// Sets the default template for the ContentType + /// + /// Default + void SetDefaultTemplate(ITemplate? template); - /// - /// Removes a template from the list of allowed templates - /// - /// to remove - /// True if template was removed, otherwise False - bool RemoveTemplate(ITemplate template); + /// + /// Removes a template from the list of allowed templates + /// + /// to remove + /// True if template was removed, otherwise False + bool RemoveTemplate(ITemplate template); - /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset - /// - /// - /// - IContentType DeepCloneWithResetIdentities(string newAlias); + /// + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// + /// + /// + IContentType DeepCloneWithResetIdentities(string newAlias); - /// - /// Gets or sets the history cleanup configuration. - /// - /// The history cleanup configuration. - HistoryCleanup? HistoryCleanup { get; set; } - } + /// + /// Gets or sets the history cleanup configuration. + /// + /// The history cleanup configuration. + HistoryCleanup? HistoryCleanup { get; set; } } diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index eb3d4489d4..adcb4074f9 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -1,175 +1,182 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines the base for a ContentType with properties that +/// are shared between ContentTypes and MediaTypes. +/// +public interface IContentTypeBase : IUmbracoEntity, IRememberBeingDirty { /// - /// Defines the base for a ContentType with properties that - /// are shared between ContentTypes and MediaTypes. + /// Gets or Sets the Alias of the ContentType /// - public interface IContentTypeBase : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Gets or Sets the Alias of the ContentType - /// - string Alias { get; set; } + string Alias { get; set; } - /// - /// Gets or Sets the Description for the ContentType - /// - string? Description { get; set; } + /// + /// Gets or Sets the Description for the ContentType + /// + string? Description { get; set; } - /// - /// Gets or sets the icon for the content type. The value is a CSS class name representing - /// the icon (eg. icon-home) along with an optional CSS class name representing the - /// color (eg. icon-blue). Put together, the value for this scenario would be - /// icon-home color-blue. - /// - /// If a class name for the color isn't specified, the icon color will default to black. - /// - string? Icon { get; set; } + /// + /// Gets or sets the icon for the content type. The value is a CSS class name representing + /// the icon (eg. icon-home) along with an optional CSS class name representing the + /// color (eg. icon-blue). Put together, the value for this scenario would be + /// icon-home color-blue. + /// If a class name for the color isn't specified, the icon color will default to black. + /// + string? Icon { get; set; } - /// - /// Gets or Sets the Thumbnail for the ContentType - /// - string? Thumbnail { get; set; } + /// + /// Gets or Sets the Thumbnail for the ContentType + /// + string? Thumbnail { get; set; } - /// - /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root - /// - bool AllowedAsRoot { get; set; } + /// + /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root + /// + bool AllowedAsRoot { get; set; } - /// - /// Gets or Sets a boolean indicating whether this ContentType is a Container - /// - /// - /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. - /// - bool IsContainer { get; set; } + /// + /// Gets or Sets a boolean indicating whether this ContentType is a Container + /// + /// + /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. + /// + bool IsContainer { get; set; } - /// - /// Gets or sets a value indicating whether this content type is for an element. - /// - /// - /// By default a content type is for a true media, member or document, but - /// it can also be for an element, ie a subset that can for instance be used in - /// nested content. - /// - bool IsElement { get; set; } + /// + /// Gets or sets a value indicating whether this content type is for an element. + /// + /// + /// + /// By default a content type is for a true media, member or document, but + /// it can also be for an element, ie a subset that can for instance be used in + /// nested content. + /// + /// + bool IsElement { get; set; } - /// - /// Gets or sets the content variation of the content type. - /// - ContentVariation Variations { get; set; } + /// + /// Gets or sets the content variation of the content type. + /// + ContentVariation Variations { get; set; } - /// - /// Validates that a combination of culture and segment is valid for the content type. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcard are supported. - /// True if the combination is valid; otherwise false. - /// - /// The combination must match the content type variation exactly. For instance, if the content type varies by culture, - /// then an invariant culture would be invalid. - /// - bool SupportsVariation(string culture, string segment, bool wildcards = false); + /// + /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType + /// + IEnumerable? AllowedContentTypes { get; set; } - /// - /// Validates that a combination of culture and segment is valid for the content type properties. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcard are supported. - /// True if the combination is valid; otherwise false. - /// - /// The combination must be valid for properties of the content type. For instance, if the content type varies by culture, - /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content type is invariant, - /// then a variant culture is invalid, because no property could possibly vary by culture. - /// - bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false); + /// + /// Gets or sets the local property groups. + /// + PropertyGroupCollection PropertyGroups { get; set; } - /// - /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType - /// - IEnumerable? AllowedContentTypes { get; set; } + /// + /// Gets all local property types all local property groups or ungrouped. + /// + IEnumerable PropertyTypes { get; } - /// - /// Gets or sets the local property groups. - /// - PropertyGroupCollection PropertyGroups { get; set; } + /// + /// Gets or sets the local property types that do not belong to a group. + /// + IEnumerable NoGroupPropertyTypes { get; set; } - /// - /// Gets all local property types all local property groups or ungrouped. - /// - IEnumerable PropertyTypes { get; } + /// + /// Validates that a combination of culture and segment is valid for the content type. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcard are supported. + /// True if the combination is valid; otherwise false. + /// + /// + /// The combination must match the content type variation exactly. For instance, if the content type varies by + /// culture, + /// then an invariant culture would be invalid. + /// + /// + bool SupportsVariation(string culture, string segment, bool wildcards = false); - /// - /// Gets or sets the local property types that do not belong to a group. - /// - IEnumerable NoGroupPropertyTypes { get; set; } + /// + /// Validates that a combination of culture and segment is valid for the content type properties. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcard are supported. + /// True if the combination is valid; otherwise false. + /// + /// + /// The combination must be valid for properties of the content type. For instance, if the content type varies by + /// culture, + /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content + /// type is invariant, + /// then a variant culture is invalid, because no property could possibly vary by culture. + /// + /// + bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false); - /// - /// Removes a PropertyType from the current ContentType - /// - /// Alias of the to remove - void RemovePropertyType(string alias); + /// + /// Removes a PropertyType from the current ContentType + /// + /// Alias of the to remove + void RemovePropertyType(string alias); - /// - /// Removes a property group from the current content type. - /// - /// Alias of the to remove - void RemovePropertyGroup(string alias); + /// + /// Removes a property group from the current content type. + /// + /// Alias of the to remove + void RemovePropertyGroup(string alias); - /// - /// Checks whether a PropertyType with a given alias already exists - /// - /// Alias of the PropertyType - /// Returns True if a PropertyType with the passed in alias exists, otherwise False - bool PropertyTypeExists(string? alias); + /// + /// Checks whether a PropertyType with a given alias already exists + /// + /// Alias of the PropertyType + /// Returns True if a PropertyType with the passed in alias exists, otherwise False + bool PropertyTypeExists(string? alias); - /// - /// Adds the property type to the specified property group (creates a new group if not found and a name is specified). - /// - /// The property type to add. - /// The alias of the property group to add the property type to. - /// The name of the property group to create when not found. - /// - /// Returns true if the property type was added; otherwise, false. - /// - bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); + /// + /// Adds the property type to the specified property group (creates a new group if not found and a name is specified). + /// + /// The property type to add. + /// The alias of the property group to add the property type to. + /// The name of the property group to create when not found. + /// + /// Returns true if the property type was added; otherwise, false. + /// + bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); - /// - /// Adds a PropertyType, which does not belong to a PropertyGroup. - /// - /// to add - /// Returns True if PropertyType was added, otherwise False - bool AddPropertyType(IPropertyType propertyType); + /// + /// Adds a PropertyType, which does not belong to a PropertyGroup. + /// + /// to add + /// Returns True if PropertyType was added, otherwise False + bool AddPropertyType(IPropertyType propertyType); - /// - /// Adds a property group with the specified and . - /// - /// The alias. - /// Name of the group. - /// - /// Returns true if a property group with specified was added; otherwise, false. - /// - /// - /// This method will also check if a group already exists with the same alias. - /// - bool AddPropertyGroup(string alias, string name); + /// + /// Adds a property group with the specified and . + /// + /// The alias. + /// Name of the group. + /// + /// Returns true if a property group with specified was added; otherwise, false + /// . + /// + /// + /// This method will also check if a group already exists with the same alias. + /// + bool AddPropertyGroup(string alias, string name); - /// - /// Moves a PropertyType to a specified PropertyGroup - /// - /// Alias of the PropertyType to move - /// Alias of the PropertyGroup to move the PropertyType to - /// - bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias); + /// + /// Moves a PropertyType to a specified PropertyGroup + /// + /// Alias of the PropertyType to move + /// Alias of the PropertyGroup to move the PropertyType to + /// + bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias); - /// - /// Gets an corresponding to this content type. - /// - ISimpleContentType ToSimple(); - } + /// + /// Gets an corresponding to this content type. + /// + ISimpleContentType ToSimple(); } diff --git a/src/Umbraco.Core/Models/IContentTypeComposition.cs b/src/Umbraco.Core/Models/IContentTypeComposition.cs index de3e8fb416..650328548e 100644 --- a/src/Umbraco.Core/Models/IContentTypeComposition.cs +++ b/src/Umbraco.Core/Models/IContentTypeComposition.cs @@ -1,72 +1,69 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Defines the Composition of a ContentType +/// +public interface IContentTypeComposition : IContentTypeBase { /// - /// Defines the Composition of a ContentType + /// Gets or sets the content types that compose this content type. /// - public interface IContentTypeComposition : IContentTypeBase - { - /// - /// Gets or sets the content types that compose this content type. - /// - // TODO: we should be storing key references, not the object else we are caching way too much - IEnumerable ContentTypeComposition { get; set; } + // TODO: we should be storing key references, not the object else we are caching way too much + IEnumerable ContentTypeComposition { get; set; } - /// - /// Gets the property groups for the entire composition. - /// - IEnumerable CompositionPropertyGroups { get; } + /// + /// Gets the property groups for the entire composition. + /// + IEnumerable CompositionPropertyGroups { get; } - /// - /// Gets the property types for the entire composition. - /// - IEnumerable CompositionPropertyTypes { get; } + /// + /// Gets the property types for the entire composition. + /// + IEnumerable CompositionPropertyTypes { get; } - /// - /// Adds a new ContentType to the list of composite ContentTypes - /// - /// to add - /// True if ContentType was added, otherwise returns False - bool AddContentType(IContentTypeComposition? contentType); + /// + /// Returns a list of content type ids that have been removed from this instance's composition + /// + IEnumerable RemovedContentTypes { get; } - /// - /// Removes a ContentType with the supplied alias from the list of composite ContentTypes - /// - /// Alias of a - /// True if ContentType was removed, otherwise returns False - bool RemoveContentType(string alias); + /// + /// Adds a new ContentType to the list of composite ContentTypes + /// + /// to add + /// True if ContentType was added, otherwise returns False + bool AddContentType(IContentTypeComposition? contentType); - /// - /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes - /// - /// Alias of a - /// True if ContentType with alias exists, otherwise returns False - bool ContentTypeCompositionExists(string alias); + /// + /// Removes a ContentType with the supplied alias from the list of composite ContentTypes + /// + /// Alias of a + /// True if ContentType was removed, otherwise returns False + bool RemoveContentType(string alias); - /// - /// Gets a list of ContentType aliases from the current composition - /// - /// An enumerable list of string aliases - IEnumerable CompositionAliases(); + /// + /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes + /// + /// Alias of a + /// True if ContentType with alias exists, otherwise returns False + bool ContentTypeCompositionExists(string alias); - /// - /// Gets a list of ContentType Ids from the current composition - /// - /// An enumerable list of integer ids - IEnumerable CompositionIds(); + /// + /// Gets a list of ContentType aliases from the current composition + /// + /// An enumerable list of string aliases + IEnumerable CompositionAliases(); - /// - /// Returns a list of content type ids that have been removed from this instance's composition - /// - IEnumerable RemovedContentTypes { get; } + /// + /// Gets a list of ContentType Ids from the current composition + /// + /// An enumerable list of integer ids + IEnumerable CompositionIds(); - /// - /// Gets the property types obtained via composition. - /// - /// - /// Gets them raw, ie with their original variation. - /// - IEnumerable GetOriginalComposedPropertyTypes(); - } + /// + /// Gets the property types obtained via composition. + /// + /// + /// Gets them raw, ie with their original variation. + /// + IEnumerable GetOriginalComposedPropertyTypes(); } diff --git a/src/Umbraco.Core/Models/IDataType.cs b/src/Umbraco.Core/Models/IDataType.cs index 2fdc67dfcc..6f0002c779 100644 --- a/src/Umbraco.Core/Models/IDataType.cs +++ b/src/Umbraco.Core/Models/IDataType.cs @@ -1,38 +1,41 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a data type. +/// +public interface IDataType : IUmbracoEntity, IRememberBeingDirty { /// - /// Represents a data type. + /// Gets or sets the property editor. /// - public interface IDataType : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Gets or sets the property editor. - /// - IDataEditor? Editor { get; set; } + IDataEditor? Editor { get; set; } - /// - /// Gets the property editor alias. - /// - string EditorAlias { get; } + /// + /// Gets the property editor alias. + /// + string EditorAlias { get; } - /// - /// Gets or sets the database type for the data type values. - /// - /// In most cases this is imposed by the property editor, but some editors - /// may support storing different types. - ValueStorageType DatabaseType { get; set; } + /// + /// Gets or sets the database type for the data type values. + /// + /// + /// In most cases this is imposed by the property editor, but some editors + /// may support storing different types. + /// + ValueStorageType DatabaseType { get; set; } - /// - /// Gets or sets the configuration object. - /// - /// - /// The configuration object is serialized to Json and stored into the database. - /// The serialized Json is deserialized by the property editor, which by default should - /// return a Dictionary{string, object} but could return a typed object e.g. MyEditor.Configuration. - /// - object? Configuration { get; set; } - } + /// + /// Gets or sets the configuration object. + /// + /// + /// The configuration object is serialized to Json and stored into the database. + /// + /// The serialized Json is deserialized by the property editor, which by default should + /// return a Dictionary{string, object} but could return a typed object e.g. MyEditor.Configuration. + /// + /// + object? Configuration { get; set; } } diff --git a/src/Umbraco.Core/Models/IDataValueEditor.cs b/src/Umbraco.Core/Models/IDataValueEditor.cs index 8d4841a114..3e749b6d20 100644 --- a/src/Umbraco.Core/Models/IDataValueEditor.cs +++ b/src/Umbraco.Core/Models/IDataValueEditor.cs @@ -1,85 +1,88 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Xml.Linq; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an editor for editing data values. +/// +/// This is the base interface for parameter and property value editors. +public interface IDataValueEditor { + /// + /// Gets the editor view. + /// + string? View { get; } /// - /// Represents an editor for editing data values. + /// Gets the type of the value. /// - /// This is the base interface for parameter and property value editors. - public interface IDataValueEditor - { - /// - /// Gets the editor view. - /// - string? View { get; } + /// The value has to be a valid value. + string ValueType { get; set; } - /// - /// Gets the type of the value. - /// - /// The value has to be a valid value. - string ValueType { get; set; } + /// + /// Gets a value indicating whether the edited value is read-only. + /// + bool IsReadOnly { get; } - /// - /// Gets a value indicating whether the edited value is read-only. - /// - bool IsReadOnly { get; } + /// + /// Gets a value indicating whether to display the associated label. + /// + bool HideLabel { get; } - /// - /// Gets a value indicating whether to display the associated label. - /// - bool HideLabel { get; } + /// + /// Gets a value indicating whether the IDataValueEditor supports readonly mode + /// + bool SupportsReadOnly => false; - /// - /// Validates a property value. - /// - /// The property value. - /// A value indicating whether the property value is required. - /// A specific format (regex) that the property value must respect. - IEnumerable Validate(object? value, bool required, string? format); - /// - /// Gets the validators to use to validate the edited value. - /// - /// - /// Use this property to add validators, not to validate. Use instead. - /// TODO: replace with AddValidator? WithValidator? - /// - List Validators { get; } + /// + /// Gets the validators to use to validate the edited value. + /// + /// + /// Use this property to add validators, not to validate. Use instead. + /// TODO: replace with AddValidator? WithValidator? + /// + List Validators { get; } - /// - /// Converts a value posted by the editor to a property value. - /// - object? FromEditor(ContentPropertyData editorValue, object? currentValue); + /// + /// Validates a property value. + /// + /// The property value. + /// A value indicating whether the property value is required. + /// A specific format (regex) that the property value must respect. + IEnumerable Validate(object? value, bool required, string? format); - /// - /// Converts a property value to a value for the editor. - /// - object? ToEditor(IProperty property, string? culture = null, string? segment = null); + /// + /// Converts a value posted by the editor to a property value. + /// + object? FromEditor(ContentPropertyData editorValue, object? currentValue); - // TODO: / deal with this when unplugging the xml cache - // why property vs propertyType? services should be injected! etc... + /// + /// Converts a property value to a value for the editor. + /// + object? ToEditor(IProperty property, string? culture = null, string? segment = null); - /// - /// Used for serializing an item for packaging - /// - /// - /// - /// - IEnumerable ConvertDbToXml(IProperty property, bool published); + // TODO: / deal with this when unplugging the xml cache + // why property vs propertyType? services should be injected! etc... - /// - /// Used for serializing an item for packaging - /// - /// - /// - /// - XNode ConvertDbToXml(IPropertyType propertyType, object value); + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + IEnumerable ConvertDbToXml(IProperty property, bool published); - string ConvertDbToString(IPropertyType propertyType, object? value); - } + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + XNode ConvertDbToXml(IPropertyType propertyType, object value); + + string ConvertDbToString(IPropertyType propertyType, object? value); } diff --git a/src/Umbraco.Core/Models/IDeepCloneable.cs b/src/Umbraco.Core/Models/IDeepCloneable.cs index a7568b7e81..171c2a1f4e 100644 --- a/src/Umbraco.Core/Models/IDeepCloneable.cs +++ b/src/Umbraco.Core/Models/IDeepCloneable.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Provides a mean to deep-clone an object. +/// +public interface IDeepCloneable { - /// - /// Provides a mean to deep-clone an object. - /// - public interface IDeepCloneable - { - object DeepClone(); - } + object DeepClone(); } diff --git a/src/Umbraco.Core/Models/IDictionaryItem.cs b/src/Umbraco.Core/Models/IDictionaryItem.cs index f299ce2ac5..e47502199b 100644 --- a/src/Umbraco.Core/Models/IDictionaryItem.cs +++ b/src/Umbraco.Core/Models/IDictionaryItem.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IDictionaryItem : IEntity, IRememberBeingDirty { - public interface IDictionaryItem : IEntity, IRememberBeingDirty - { - /// - /// Gets or Sets the Parent Id of the Dictionary Item - /// - [DataMember] - Guid? ParentId { get; set; } + /// + /// Gets or Sets the Parent Id of the Dictionary Item + /// + [DataMember] + Guid? ParentId { get; set; } - /// - /// Gets or sets the Key for the Dictionary Item - /// - [DataMember] - string ItemKey { get; set; } + /// + /// Gets or sets the Key for the Dictionary Item + /// + [DataMember] + string ItemKey { get; set; } - /// - /// Gets or sets a list of translations for the Dictionary Item - /// - [DataMember] - IEnumerable Translations { get; set; } - } + /// + /// Gets or sets a list of translations for the Dictionary Item + /// + [DataMember] + IEnumerable Translations { get; set; } } diff --git a/src/Umbraco.Core/Models/IDictionaryTranslation.cs b/src/Umbraco.Core/Models/IDictionaryTranslation.cs index 445bafd4ba..37579151bc 100644 --- a/src/Umbraco.Core/Models/IDictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/IDictionaryTranslation.cs @@ -1,22 +1,21 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IDictionaryTranslation : IEntity, IRememberBeingDirty { - public interface IDictionaryTranslation : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the for the translation - /// - [DataMember] - ILanguage? Language { get; set; } + /// + /// Gets or sets the for the translation + /// + [DataMember] + ILanguage? Language { get; set; } - int LanguageId { get; } + int LanguageId { get; } - /// - /// Gets or sets the translated text - /// - [DataMember] - string Value { get; set; } - } + /// + /// Gets or sets the translated text + /// + [DataMember] + string Value { get; set; } } diff --git a/src/Umbraco.Core/Models/IDomain.cs b/src/Umbraco.Core/Models/IDomain.cs index f9d90dd9eb..2d4845c9a6 100644 --- a/src/Umbraco.Core/Models/IDomain.cs +++ b/src/Umbraco.Core/Models/IDomain.cs @@ -1,17 +1,19 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IDomain : IEntity, IRememberBeingDirty { - public interface IDomain : IEntity, IRememberBeingDirty - { - int? LanguageId { get; set; } - string DomainName { get; set; } - int? RootContentId { get; set; } - bool IsWildcard { get; } + int? LanguageId { get; set; } - /// - /// Readonly value of the language ISO code for the domain - /// - string? LanguageIsoCode { get; } - } + string DomainName { get; set; } + + int? RootContentId { get; set; } + + bool IsWildcard { get; } + + /// + /// Readonly value of the language ISO code for the domain + /// + string? LanguageIsoCode { get; } } diff --git a/src/Umbraco.Core/Models/IFile.cs b/src/Umbraco.Core/Models/IFile.cs index 216d45d277..ed52997c84 100644 --- a/src/Umbraco.Core/Models/IFile.cs +++ b/src/Umbraco.Core/Models/IFile.cs @@ -1,47 +1,45 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a File +/// +/// Used for Scripts, Stylesheets and Templates +public interface IFile : IEntity, IRememberBeingDirty { /// - /// Defines a File + /// Gets the Name of the File including extension /// - /// Used for Scripts, Stylesheets and Templates - public interface IFile : IEntity, IRememberBeingDirty - { - /// - /// Gets the Name of the File including extension - /// - string? Name { get; } + string? Name { get; } - /// - /// Gets the Alias of the File, which is the name without the extension - /// - string Alias { get; } + /// + /// Gets the Alias of the File, which is the name without the extension + /// + string Alias { get; } - /// - /// Gets or sets the Path to the File from the root of the file's associated IFileSystem - /// - string Path { get; set; } + /// + /// Gets or sets the Path to the File from the root of the file's associated IFileSystem + /// + string Path { get; set; } - /// - /// Gets the original path of the file - /// - string OriginalPath { get; } + /// + /// Gets the original path of the file + /// + string OriginalPath { get; } - /// - /// Called to re-set the OriginalPath to the Path - /// - void ResetOriginalPath(); + /// + /// Gets or sets the Content of a File + /// + string? Content { get; set; } - /// - /// Gets or sets the Content of a File - /// - string? Content { get; set; } + /// + /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) + /// + string? VirtualPath { get; set; } - /// - /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) - /// - string? VirtualPath { get; set; } - - } + /// + /// Called to re-set the OriginalPath to the Path + /// + void ResetOriginalPath(); } diff --git a/src/Umbraco.Core/Models/IKeyValue.cs b/src/Umbraco.Core/Models/IKeyValue.cs index 2a8c6528bf..b893aabf35 100644 --- a/src/Umbraco.Core/Models/IKeyValue.cs +++ b/src/Umbraco.Core/Models/IKeyValue.cs @@ -1,11 +1,10 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IKeyValue : IEntity { - public interface IKeyValue : IEntity - { - string Identifier { get; set; } + string Identifier { get; set; } - string? Value { get; set; } - } + string? Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index de5170cff6..5f48bc363e 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -2,56 +2,59 @@ using System.Globalization; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a language. +/// +public interface ILanguage : IEntity, IRememberBeingDirty { /// - /// Represents a language. + /// Gets or sets the ISO code of the language. /// - public interface ILanguage : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the ISO code of the language. - /// - [DataMember] - string IsoCode { get; set; } + [DataMember] + string IsoCode { get; set; } - /// - /// Gets or sets the culture name of the language. - /// - [DataMember] - string CultureName { get; set; } + /// + /// Gets or sets the culture name of the language. + /// + [DataMember] + string CultureName { get; set; } - /// - /// Gets the object for the language. - /// - [IgnoreDataMember] - CultureInfo? CultureInfo { get; } + /// + /// Gets the object for the language. + /// + [IgnoreDataMember] + CultureInfo? CultureInfo { get; } - /// - /// Gets or sets a value indicating whether the language is the default language. - /// - [DataMember] - bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether the language is the default language. + /// + [DataMember] + bool IsDefault { get; set; } - /// - /// Gets or sets a value indicating whether the language is mandatory. - /// - /// - /// When a language is mandatory, a multi-lingual document cannot be published - /// without that language being published, and unpublishing that language unpublishes - /// the entire document. - /// - [DataMember] - bool IsMandatory { get; set; } + /// + /// Gets or sets a value indicating whether the language is mandatory. + /// + /// + /// + /// When a language is mandatory, a multi-lingual document cannot be published + /// without that language being published, and unpublishing that language unpublishes + /// the entire document. + /// + /// + [DataMember] + bool IsMandatory { get; set; } - /// - /// Gets or sets the identifier of a fallback language. - /// - /// - /// The fallback language can be used in multi-lingual scenarios, to help - /// define fallback strategies when a value does not exist for a requested language. - /// - [DataMember] - int? FallbackLanguageId { get; set; } - } + /// + /// Gets or sets the identifier of a fallback language. + /// + /// + /// + /// The fallback language can be used in multi-lingual scenarios, to help + /// define fallback strategies when a value does not exist for a requested language. + /// + /// + [DataMember] + int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Core/Models/ILogViewerQuery.cs b/src/Umbraco.Core/Models/ILogViewerQuery.cs index 59a567a635..372fddc3d0 100644 --- a/src/Umbraco.Core/Models/ILogViewerQuery.cs +++ b/src/Umbraco.Core/Models/ILogViewerQuery.cs @@ -1,10 +1,10 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface ILogViewerQuery : IEntity { - public interface ILogViewerQuery : IEntity - { - string? Name { get; set; } - string? Query { get; set; } - } + string? Name { get; set; } + + string? Query { get; set; } } diff --git a/src/Umbraco.Core/Models/IMacro.cs b/src/Umbraco.Core/Models/IMacro.cs index e8102b7768..bc979804a7 100644 --- a/src/Umbraco.Core/Models/IMacro.cs +++ b/src/Umbraco.Core/Models/IMacro.cs @@ -1,65 +1,64 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a Macro +/// +public interface IMacro : IEntity, IRememberBeingDirty { /// - /// Defines a Macro + /// Gets or sets the alias of the Macro /// - public interface IMacro : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the alias of the Macro - /// - [DataMember] - string Alias { get; set; } + [DataMember] + string Alias { get; set; } - /// - /// Gets or sets the name of the Macro - /// - [DataMember] - string? Name { get; set; } + /// + /// Gets or sets the name of the Macro + /// + [DataMember] + string? Name { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro can be used in an Editor - /// - [DataMember] - bool UseInEditor { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro can be used in an Editor + /// + [DataMember] + bool UseInEditor { get; set; } - /// - /// Gets or sets the Cache Duration for the Macro - /// - [DataMember] - int CacheDuration { get; set; } + /// + /// Gets or sets the Cache Duration for the Macro + /// + [DataMember] + int CacheDuration { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached by Page - /// - [DataMember] - bool CacheByPage { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached by Page + /// + [DataMember] + bool CacheByPage { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached Personally - /// - [DataMember] - bool CacheByMember { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached Personally + /// + [DataMember] + bool CacheByMember { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor - /// - [DataMember] - bool DontRender { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor + /// + [DataMember] + bool DontRender { get; set; } - /// - /// Gets or set the path to the macro source to render - /// - [DataMember] - string MacroSource { get; set; } + /// + /// Gets or set the path to the macro source to render + /// + [DataMember] + string MacroSource { get; set; } - /// - /// Gets or sets a list of Macro Properties - /// - [DataMember] - MacroPropertyCollection Properties { get; } - } + /// + /// Gets or sets a list of Macro Properties + /// + [DataMember] + MacroPropertyCollection Properties { get; } } diff --git a/src/Umbraco.Core/Models/IMacroProperty.cs b/src/Umbraco.Core/Models/IMacroProperty.cs index d3d589a31e..e1b27b6483 100644 --- a/src/Umbraco.Core/Models/IMacroProperty.cs +++ b/src/Umbraco.Core/Models/IMacroProperty.cs @@ -1,42 +1,40 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a Property for a Macro +/// +public interface IMacroProperty : IValueObject, IDeepCloneable, IRememberBeingDirty { + [DataMember] + int Id { get; set; } + + [DataMember] + Guid Key { get; set; } + /// - /// Defines a Property for a Macro + /// Gets or sets the Alias of the Property /// - public interface IMacroProperty : IValueObject, IDeepCloneable, IRememberBeingDirty - { - [DataMember] - int Id { get; set; } + [DataMember] + string Alias { get; set; } - [DataMember] - Guid Key { get; set; } + /// + /// Gets or sets the Name of the Property + /// + [DataMember] + string? Name { get; set; } - /// - /// Gets or sets the Alias of the Property - /// - [DataMember] - string Alias { get; set; } + /// + /// Gets or sets the Sort Order of the Property + /// + [DataMember] + int SortOrder { get; set; } - /// - /// Gets or sets the Name of the Property - /// - [DataMember] - string? Name { get; set; } - - /// - /// Gets or sets the Sort Order of the Property - /// - [DataMember] - int SortOrder { get; set; } - - /// - /// Gets or sets the parameter editor alias - /// - [DataMember] - string EditorAlias { get; set; } - } + /// + /// Gets or sets the parameter editor alias + /// + [DataMember] + string EditorAlias { get; set; } } diff --git a/src/Umbraco.Core/Models/IMedia.cs b/src/Umbraco.Core/Models/IMedia.cs index cbb80fdd59..08f206f664 100644 --- a/src/Umbraco.Core/Models/IMedia.cs +++ b/src/Umbraco.Core/Models/IMedia.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IMedia : IContentBase { - public interface IMedia : IContentBase - { } } diff --git a/src/Umbraco.Core/Models/IMediaType.cs b/src/Umbraco.Core/Models/IMediaType.cs index 13655f0f55..0be980ae62 100644 --- a/src/Umbraco.Core/Models/IMediaType.cs +++ b/src/Umbraco.Core/Models/IMediaType.cs @@ -1,16 +1,14 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a ContentType, which Media is based on +/// +public interface IMediaType : IContentTypeComposition { /// - /// Defines a ContentType, which Media is based on + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset /// - public interface IMediaType : IContentTypeComposition - { - - /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset - /// - /// - /// - IMediaType DeepCloneWithResetIdentities(string newAlias); - } + /// + /// + IMediaType DeepCloneWithResetIdentities(string newAlias); } diff --git a/src/Umbraco.Core/Models/IMediaUrlGenerator.cs b/src/Umbraco.Core/Models/IMediaUrlGenerator.cs index 4565117dfd..a0af9dcc0e 100644 --- a/src/Umbraco.Core/Models/IMediaUrlGenerator.cs +++ b/src/Umbraco.Core/Models/IMediaUrlGenerator.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Used to generate paths to media items for a specified property editor alias +/// +public interface IMediaUrlGenerator { /// - /// Used to generate paths to media items for a specified property editor alias + /// Tries to get a media path for a given property editor alias /// - public interface IMediaUrlGenerator - { - /// - /// Tries to get a media path for a given property editor alias - /// - /// The property editor alias - /// The value of the property - /// - /// True if a media path was returned - /// - bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath); - } + /// The property editor alias + /// The value of the property + /// + /// True if a media path was returned + /// + bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath); } diff --git a/src/Umbraco.Core/Models/IMember.cs b/src/Umbraco.Core/Models/IMember.cs index 0dba1d8049..6085b84f01 100644 --- a/src/Umbraco.Core/Models/IMember.cs +++ b/src/Umbraco.Core/Models/IMember.cs @@ -1,64 +1,67 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models -{ - public interface IMember : IContentBase, IMembershipUser, IHaveAdditionalData - { - /// - /// String alias of the default ContentType - /// - string ContentTypeAlias { get; } +namespace Umbraco.Cms.Core.Models; - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - string? LongStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - string? ShortStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - int IntegerPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - bool BoolPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - DateTime DateTimePropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - string? PropertyTypeAlias { get; set; } - } +public interface IMember : IContentBase, IMembershipUser, IHaveAdditionalData +{ + /// + /// String alias of the default ContentType + /// + string ContentTypeAlias { get; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string? LongStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string? ShortStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + int IntegerPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + bool BoolPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + DateTime DateTimePropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string? PropertyTypeAlias { get; set; } } diff --git a/src/Umbraco.Core/Models/IMemberGroup.cs b/src/Umbraco.Core/Models/IMemberGroup.cs index 80d4a16ad6..904d60cf8c 100644 --- a/src/Umbraco.Core/Models/IMemberGroup.cs +++ b/src/Umbraco.Core/Models/IMemberGroup.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a member type +/// +public interface IMemberGroup : IEntity, IRememberBeingDirty, IHaveAdditionalData { /// - /// Represents a member type + /// The name of the member group /// - public interface IMemberGroup : IEntity, IRememberBeingDirty, IHaveAdditionalData - { - /// - /// The name of the member group - /// - string? Name { get; set; } + string? Name { get; set; } - /// - /// Profile of the user who created this Entity - /// - int CreatorId { get; set; } - } + /// + /// Profile of the user who created this Entity + /// + int CreatorId { get; set; } } diff --git a/src/Umbraco.Core/Models/IMemberType.cs b/src/Umbraco.Core/Models/IMemberType.cs index 324601efde..993e956df9 100644 --- a/src/Umbraco.Core/Models/IMemberType.cs +++ b/src/Umbraco.Core/Models/IMemberType.cs @@ -1,50 +1,49 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a MemberType, which Member is based on +/// +public interface IMemberType : IContentTypeComposition { /// - /// Defines a MemberType, which Member is based on + /// Gets a boolean indicating whether a Property is editable by the Member. /// - public interface IMemberType : IContentTypeComposition - { - /// - /// Gets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to check - /// - bool MemberCanEditProperty(string? propertyTypeAlias); + /// PropertyType Alias of the Property to check + /// + bool MemberCanEditProperty(string? propertyTypeAlias); - /// - /// Gets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - bool MemberCanViewProperty(string propertyTypeAlias); + /// + /// Gets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + bool MemberCanViewProperty(string propertyTypeAlias); - /// - /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - bool IsSensitiveProperty(string propertyTypeAlias); + /// + /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + bool IsSensitiveProperty(string propertyTypeAlias); - /// - /// Sets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - void SetMemberCanEditProperty(string propertyTypeAlias, bool value); + /// + /// Sets a boolean indicating whether a Property is editable by the Member. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + void SetMemberCanEditProperty(string propertyTypeAlias, bool value); - /// - /// Sets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - void SetMemberCanViewProperty(string propertyTypeAlias, bool value); + /// + /// Sets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + void SetMemberCanViewProperty(string propertyTypeAlias, bool value); - /// - /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - void SetIsSensitiveProperty(string propertyTypeAlias, bool value); - } + /// + /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + void SetIsSensitiveProperty(string propertyTypeAlias, bool value); } diff --git a/src/Umbraco.Core/Models/IMigrationEntry.cs b/src/Umbraco.Core/Models/IMigrationEntry.cs index a3d11e851a..392eb17097 100644 --- a/src/Umbraco.Core/Models/IMigrationEntry.cs +++ b/src/Umbraco.Core/Models/IMigrationEntry.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IMigrationEntry : IEntity, IRememberBeingDirty { - public interface IMigrationEntry : IEntity, IRememberBeingDirty - { - string? MigrationName { get; set; } - SemVersion? Version { get; set; } - } + string? MigrationName { get; set; } + + SemVersion? Version { get; set; } } diff --git a/src/Umbraco.Core/Models/IPartialView.cs b/src/Umbraco.Core/Models/IPartialView.cs index c45b76534d..a19cc65c7a 100644 --- a/src/Umbraco.Core/Models/IPartialView.cs +++ b/src/Umbraco.Core/Models/IPartialView.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPartialView : IFile { - public interface IPartialView : IFile - { - PartialViewType ViewType { get; } - } + PartialViewType ViewType { get; } } diff --git a/src/Umbraco.Core/Models/IProperty.cs b/src/Umbraco.Core/Models/IProperty.cs index 9ed37c34e1..54f1e8581f 100644 --- a/src/Umbraco.Core/Models/IProperty.cs +++ b/src/Umbraco.Core/Models/IProperty.cs @@ -1,40 +1,39 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IProperty : IEntity, IRememberBeingDirty { - public interface IProperty : IEntity, IRememberBeingDirty - { + ValueStorageType ValueStorageType { get; } - ValueStorageType ValueStorageType { get; } - /// - /// Returns the PropertyType, which this Property is based on - /// - IPropertyType PropertyType { get; } + /// + /// Returns the PropertyType, which this Property is based on + /// + IPropertyType PropertyType { get; } - /// - /// Gets the list of values. - /// - IReadOnlyCollection Values { get; set; } + /// + /// Gets the list of values. + /// + IReadOnlyCollection Values { get; set; } - /// - /// Returns the Alias of the PropertyType, which this Property is based on - /// - string Alias { get; } + /// + /// Returns the Alias of the PropertyType, which this Property is based on + /// + string Alias { get; } - /// - /// Gets the value. - /// - object? GetValue(string? culture = null, string? segment = null, bool published = false); + int PropertyTypeId { get; } - /// - /// Sets a value. - /// - void SetValue(object? value, string? culture = null, string? segment = null); + /// + /// Gets the value. + /// + object? GetValue(string? culture = null, string? segment = null, bool published = false); - int PropertyTypeId { get; } - void PublishValues(string? culture = "*", string segment = "*"); - void UnpublishValues(string? culture = "*", string segment = "*"); + /// + /// Sets a value. + /// + void SetValue(object? value, string? culture = null, string? segment = null); - } + void PublishValues(string? culture = "*", string segment = "*"); + + void UnpublishValues(string? culture = "*", string segment = "*"); } diff --git a/src/Umbraco.Core/Models/IPropertyCollection.cs b/src/Umbraco.Core/Models/IPropertyCollection.cs index d39a214fdd..535756fad8 100644 --- a/src/Umbraco.Core/Models/IPropertyCollection.cs +++ b/src/Umbraco.Core/Models/IPropertyCollection.cs @@ -1,40 +1,40 @@ -using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPropertyCollection : IEnumerable, IDeepCloneable, INotifyCollectionChanged { - public interface IPropertyCollection : IEnumerable, IDeepCloneable, INotifyCollectionChanged - { - bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property); - bool Contains(string key); + int Count { get; } - /// - /// Ensures that the collection contains properties for the specified property types. - /// - void EnsurePropertyTypes(IEnumerable propertyTypes); + /// + /// Gets the property with the specified alias. + /// + IProperty? this[string name] { get; } - /// - /// Ensures that the collection does not contain properties not in the specified property types. - /// - void EnsureCleanPropertyTypes(IEnumerable propertyTypes); + /// + /// Gets the property at the specified index. + /// + IProperty? this[int index] { get; } - /// - /// Gets the property with the specified alias. - /// - IProperty? this[string name] { get; } + bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property); - /// - /// Gets the property at the specified index. - /// - IProperty? this[int index] { get; } + bool Contains(string key); - /// - /// Adds or updates a property. - /// - void Add(IProperty property); + /// + /// Ensures that the collection contains properties for the specified property types. + /// + void EnsurePropertyTypes(IEnumerable propertyTypes); - int Count { get; } - void ClearCollectionChangedEvents(); - } + /// + /// Ensures that the collection does not contain properties not in the specified property types. + /// + void EnsureCleanPropertyTypes(IEnumerable propertyTypes); + + /// + /// Adds or updates a property. + /// + void Add(IProperty property); + + void ClearCollectionChangedEvents(); } diff --git a/src/Umbraco.Core/Models/IPropertyType.cs b/src/Umbraco.Core/Models/IPropertyType.cs index b820c1d7aa..a48f8e01ae 100644 --- a/src/Umbraco.Core/Models/IPropertyType.cs +++ b/src/Umbraco.Core/Models/IPropertyType.cs @@ -1,91 +1,89 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPropertyType : IEntity, IRememberBeingDirty { - public interface IPropertyType : IEntity, IRememberBeingDirty - { - /// - /// Gets of sets the name of the property type. - /// - string Name { get; set; } + /// + /// Gets of sets the name of the property type. + /// + string Name { get; set; } - /// - /// Gets of sets the alias of the property type. - /// - string Alias { get; set; } + /// + /// Gets of sets the alias of the property type. + /// + string Alias { get; set; } - /// - /// Gets of sets the description of the property type. - /// - string? Description { get; set; } + /// + /// Gets of sets the description of the property type. + /// + string? Description { get; set; } - /// - /// Gets or sets the identifier of the datatype for this property type. - /// - int DataTypeId { get; set; } + /// + /// Gets or sets the identifier of the datatype for this property type. + /// + int DataTypeId { get; set; } - Guid DataTypeKey { get; set; } + Guid DataTypeKey { get; set; } - /// - /// Gets or sets the alias of the property editor for this property type. - /// - string PropertyEditorAlias { get; set; } + /// + /// Gets or sets the alias of the property editor for this property type. + /// + string PropertyEditorAlias { get; set; } - /// - /// Gets or sets the database type for storing value for this property type. - /// - ValueStorageType ValueStorageType { get; set; } + /// + /// Gets or sets the database type for storing value for this property type. + /// + ValueStorageType ValueStorageType { get; set; } - /// - /// Gets or sets the identifier of the property group this property type belongs to. - /// - /// For generic properties, the value is null. - Lazy? PropertyGroupId { get; set; } + /// + /// Gets or sets the identifier of the property group this property type belongs to. + /// + /// For generic properties, the value is null. + Lazy? PropertyGroupId { get; set; } - /// - /// Gets of sets a value indicating whether a value for this property type is required. - /// - bool Mandatory { get; set; } + /// + /// Gets of sets a value indicating whether a value for this property type is required. + /// + bool Mandatory { get; set; } - /// - /// Gets or sets a value indicating whether the label of this property type should be displayed on top. - /// - bool LabelOnTop { get; set; } + /// + /// Gets or sets a value indicating whether the label of this property type should be displayed on top. + /// + bool LabelOnTop { get; set; } - /// - /// Gets of sets the sort order of the property type. - /// - int SortOrder { get; set; } + /// + /// Gets of sets the sort order of the property type. + /// + int SortOrder { get; set; } - /// - /// Gets or sets the regular expression validating the property values. - /// - string? ValidationRegExp { get; set; } + /// + /// Gets or sets the regular expression validating the property values. + /// + string? ValidationRegExp { get; set; } - bool SupportsPublishing { get; set; } + bool SupportsPublishing { get; set; } - /// - /// Gets or sets the content variation of the property type. - /// - ContentVariation Variations { get; set; } + /// + /// Gets or sets the content variation of the property type. + /// + ContentVariation Variations { get; set; } - /// - /// Determines whether the property type supports a combination of culture and segment. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcards are valid. - bool SupportsVariation(string? culture, string? segment, bool wildcards = false); + /// + /// Gets or sets the custom validation message used when a value for this PropertyType is required + /// + string? MandatoryMessage { get; set; } - /// - /// Gets or sets the custom validation message used when a value for this PropertyType is required - /// - string? MandatoryMessage { get; set; } + /// + /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched + /// + string? ValidationRegExpMessage { get; set; } - /// - /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched - /// - string? ValidationRegExpMessage { get; set; } - } + /// + /// Determines whether the property type supports a combination of culture and segment. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcards are valid. + bool SupportsVariation(string? culture, string? segment, bool wildcards = false); } diff --git a/src/Umbraco.Core/Models/IPropertyValue.cs b/src/Umbraco.Core/Models/IPropertyValue.cs index 77e9e1dc25..ef95cd2a01 100644 --- a/src/Umbraco.Core/Models/IPropertyValue.cs +++ b/src/Umbraco.Core/Models/IPropertyValue.cs @@ -1,34 +1,37 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPropertyValue { - public interface IPropertyValue - { - /// - /// Gets or sets the culture of the property. - /// - /// The culture is either null (invariant) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - string? Culture { get; set; } + /// + /// Gets or sets the culture of the property. + /// + /// + /// The culture is either null (invariant) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + string? Culture { get; set; } - /// - /// Gets or sets the segment of the property. - /// - /// The segment is either null (neutral) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - string? Segment { get; set; } + /// + /// Gets or sets the segment of the property. + /// + /// + /// The segment is either null (neutral) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + string? Segment { get; set; } - /// - /// Gets or sets the edited value of the property. - /// - object? EditedValue { get; set; } + /// + /// Gets or sets the edited value of the property. + /// + object? EditedValue { get; set; } - /// - /// Gets or sets the published value of the property. - /// - object? PublishedValue { get; set; } + /// + /// Gets or sets the published value of the property. + /// + object? PublishedValue { get; set; } - /// - /// Clones the property value. - /// - IPropertyValue Clone(); - } + /// + /// Clones the property value. + /// + IPropertyValue Clone(); } diff --git a/src/Umbraco.Core/Models/IReadOnlyContentBase.cs b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs index f7518140f5..37b5a5ddea 100644 --- a/src/Umbraco.Core/Models/IReadOnlyContentBase.cs +++ b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs @@ -1,72 +1,69 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public interface IReadOnlyContentBase { - public interface IReadOnlyContentBase - { - /// - /// Gets the integer identifier of the entity. - /// - int Id { get; } + /// + /// Gets the integer identifier of the entity. + /// + int Id { get; } - /// - /// Gets the Guid unique identifier of the entity. - /// - Guid Key { get; } + /// + /// Gets the Guid unique identifier of the entity. + /// + Guid Key { get; } - /// - /// Gets the creation date. - /// - DateTime CreateDate { get; } + /// + /// Gets the creation date. + /// + DateTime CreateDate { get; } - /// - /// Gets the last update date. - /// - DateTime UpdateDate { get; } + /// + /// Gets the last update date. + /// + DateTime UpdateDate { get; } - /// - /// Gets the name of the entity. - /// - string? Name { get; } + /// + /// Gets the name of the entity. + /// + string? Name { get; } - /// - /// Gets the identifier of the user who created this entity. - /// - int CreatorId { get; } + /// + /// Gets the identifier of the user who created this entity. + /// + int CreatorId { get; } - /// - /// Gets the identifier of the parent entity. - /// - int ParentId { get; } + /// + /// Gets the identifier of the parent entity. + /// + int ParentId { get; } - /// - /// Gets the level of the entity. - /// - int Level { get; } + /// + /// Gets the level of the entity. + /// + int Level { get; } - /// - /// Gets the path to the entity. - /// - string? Path { get; } + /// + /// Gets the path to the entity. + /// + string? Path { get; } - /// - /// Gets the sort order of the entity. - /// - int SortOrder { get; } + /// + /// Gets the sort order of the entity. + /// + int SortOrder { get; } - /// - /// Gets the content type id - /// - int ContentTypeId { get; } + /// + /// Gets the content type id + /// + int ContentTypeId { get; } - /// - /// Gets the identifier of the writer. - /// - int WriterId { get; } + /// + /// Gets the identifier of the writer. + /// + int WriterId { get; } - /// - /// Gets the version identifier. - /// - int VersionId { get; } - } + /// + /// Gets the version identifier. + /// + int VersionId { get; } } diff --git a/src/Umbraco.Core/Models/IRedirectUrl.cs b/src/Umbraco.Core/Models/IRedirectUrl.cs index 18498837b4..cbd12eb0b8 100644 --- a/src/Umbraco.Core/Models/IRedirectUrl.cs +++ b/src/Umbraco.Core/Models/IRedirectUrl.cs @@ -1,44 +1,41 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a redirect URL. +/// +public interface IRedirectUrl : IEntity, IRememberBeingDirty { /// - /// Represents a redirect URL. + /// Gets or sets the identifier of the content item. /// - public interface IRedirectUrl : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the identifier of the content item. - /// - [DataMember] - int ContentId { get; set; } + [DataMember] + int ContentId { get; set; } - /// - /// Gets or sets the unique key identifying the content item. - /// - [DataMember] - Guid ContentKey { get; set; } + /// + /// Gets or sets the unique key identifying the content item. + /// + [DataMember] + Guid ContentKey { get; set; } - /// - /// Gets or sets the redirect URL creation date. - /// - [DataMember] - DateTime CreateDateUtc { get; set; } + /// + /// Gets or sets the redirect URL creation date. + /// + [DataMember] + DateTime CreateDateUtc { get; set; } - /// - /// Gets or sets the culture. - /// - [DataMember] - string? Culture { get; set; } + /// + /// Gets or sets the culture. + /// + [DataMember] + string? Culture { get; set; } - /// - /// Gets or sets the redirect URL route. - /// - /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. - [DataMember] - string Url { get; set; } - - } + /// + /// Gets or sets the redirect URL route. + /// + /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. + [DataMember] + string Url { get; set; } } diff --git a/src/Umbraco.Core/Models/IRelation.cs b/src/Umbraco.Core/Models/IRelation.cs index 0370bbe61f..468a51b897 100644 --- a/src/Umbraco.Core/Models/IRelation.cs +++ b/src/Umbraco.Core/Models/IRelation.cs @@ -1,45 +1,43 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IRelation : IEntity, IRememberBeingDirty { - public interface IRelation : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the Parent Id of the Relation (Source) - /// - [DataMember] - int ParentId { get; set; } + /// + /// Gets or sets the Parent Id of the Relation (Source) + /// + [DataMember] + int ParentId { get; set; } - [DataMember] - Guid ParentObjectType { get; set; } + [DataMember] + Guid ParentObjectType { get; set; } - /// - /// Gets or sets the Child Id of the Relation (Destination) - /// - [DataMember] - int ChildId { get; set; } + /// + /// Gets or sets the Child Id of the Relation (Destination) + /// + [DataMember] + int ChildId { get; set; } - [DataMember] - Guid ChildObjectType { get; set; } + [DataMember] + Guid ChildObjectType { get; set; } - /// - /// Gets or sets the for the Relation - /// - [DataMember] - IRelationType RelationType { get; set; } + /// + /// Gets or sets the for the Relation + /// + [DataMember] + IRelationType RelationType { get; set; } - /// - /// Gets or sets a comment for the Relation - /// - [DataMember] - string? Comment { get; set; } + /// + /// Gets or sets a comment for the Relation + /// + [DataMember] + string? Comment { get; set; } - /// - /// Gets the Id of the that this Relation is based on. - /// - [IgnoreDataMember] - int RelationTypeId { get; } - } + /// + /// Gets the Id of the that this Relation is based on. + /// + [IgnoreDataMember] + int RelationTypeId { get; } } diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs index cbc485f64b..7675a1c49e 100644 --- a/src/Umbraco.Core/Models/IRelationType.cs +++ b/src/Umbraco.Core/Models/IRelationType.cs @@ -1,50 +1,48 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IRelationTypeWithIsDependency : IRelationType { - public interface IRelationTypeWithIsDependency : IRelationType - { - /// - /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. - /// - [DataMember] - bool IsDependency { get; set; } - } - - public interface IRelationType : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the Name of the RelationType - /// - [DataMember] - string? Name { get; set; } - - /// - /// Gets or sets the Alias of the RelationType - /// - [DataMember] - string Alias { get; set; } - - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember] - bool IsBidirectional { get; set; } - - /// - /// Gets or sets the Parents object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - Guid? ParentObjectType { get; set; } - - /// - /// Gets or sets the Childs object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - Guid? ChildObjectType { get; set; } - } + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember] + bool IsDependency { get; set; } +} + +public interface IRelationType : IEntity, IRememberBeingDirty +{ + /// + /// Gets or sets the Name of the RelationType + /// + [DataMember] + string? Name { get; set; } + + /// + /// Gets or sets the Alias of the RelationType + /// + [DataMember] + string Alias { get; set; } + + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember] + bool IsBidirectional { get; set; } + + /// + /// Gets or sets the Parents object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + Guid? ParentObjectType { get; set; } + + /// + /// Gets or sets the Childs object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + Guid? ChildObjectType { get; set; } } diff --git a/src/Umbraco.Core/Models/IScript.cs b/src/Umbraco.Core/Models/IScript.cs index 6a07d2aa25..f52bdc0286 100644 --- a/src/Umbraco.Core/Models/IScript.cs +++ b/src/Umbraco.Core/Models/IScript.cs @@ -1,7 +1,5 @@ -namespace Umbraco.Cms.Core.Models -{ - public interface IScript : IFile - { +namespace Umbraco.Cms.Core.Models; - } +public interface IScript : IFile +{ } diff --git a/src/Umbraco.Core/Models/IServerRegistration.cs b/src/Umbraco.Core/Models/IServerRegistration.cs index 7d8c0f58c1..525ed30163 100644 --- a/src/Umbraco.Core/Models/IServerRegistration.cs +++ b/src/Umbraco.Core/Models/IServerRegistration.cs @@ -1,36 +1,34 @@ -using System; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IServerRegistration : IServerAddress, IEntity, IRememberBeingDirty { - public interface IServerRegistration : IServerAddress, IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the server unique identity. - /// - string? ServerIdentity { get; set; } + /// + /// Gets or sets the server unique identity. + /// + string? ServerIdentity { get; set; } - new string? ServerAddress { get; set; } + new string? ServerAddress { get; set; } - /// - /// Gets or sets a value indicating whether the server is active. - /// - bool IsActive { get; set; } + /// + /// Gets or sets a value indicating whether the server is active. + /// + bool IsActive { get; set; } - /// - /// Gets or sets a value indicating whether the server is has the SchedulingPublisher role. - /// - bool IsSchedulingPublisher { get; set; } + /// + /// Gets or sets a value indicating whether the server is has the SchedulingPublisher role. + /// + bool IsSchedulingPublisher { get; set; } - /// - /// Gets the date and time the registration was created. - /// - DateTime Registered { get; set; } + /// + /// Gets the date and time the registration was created. + /// + DateTime Registered { get; set; } - /// - /// Gets the date and time the registration was last accessed. - /// - DateTime Accessed { get; set; } - } + /// + /// Gets the date and time the registration was last accessed. + /// + DateTime Accessed { get; set; } } diff --git a/src/Umbraco.Core/Models/ISimpleContentType.cs b/src/Umbraco.Core/Models/ISimpleContentType.cs index 503946ba96..8246b50ca0 100644 --- a/src/Umbraco.Core/Models/ISimpleContentType.cs +++ b/src/Umbraco.Core/Models/ISimpleContentType.cs @@ -1,63 +1,66 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a simplified view of a content type. +/// +public interface ISimpleContentType { + int Id { get; } + + Guid Key { get; } + + string? Name { get; } + /// - /// Represents a simplified view of a content type. + /// Gets the alias of the content type. /// - public interface ISimpleContentType - { - int Id { get; } - Guid Key { get; } - string? Name { get; } + string Alias { get; } - /// - /// Gets the alias of the content type. - /// - string Alias { get; } + /// + /// Gets the default template of the content type. + /// + ITemplate? DefaultTemplate { get; } - /// - /// Gets the default template of the content type. - /// - ITemplate? DefaultTemplate { get; } + /// + /// Gets the content variation of the content type. + /// + ContentVariation Variations { get; } - /// - /// Gets the content variation of the content type. - /// - ContentVariation Variations { get; } + /// + /// Gets the icon of the content type. + /// + string? Icon { get; } - /// - /// Gets the icon of the content type. - /// - string? Icon { get; } + /// + /// Gets a value indicating whether the content type is a container. + /// + bool IsContainer { get; } - /// - /// Gets a value indicating whether the content type is a container. - /// - bool IsContainer { get; } + /// + /// Gets a value indicating whether content of that type can be created at the root of the tree. + /// + bool AllowedAsRoot { get; } - /// - /// Gets a value indicating whether content of that type can be created at the root of the tree. - /// - bool AllowedAsRoot { get; } + /// + /// Gets a value indicating whether the content type is an element content type. + /// + bool IsElement { get; } - /// - /// Gets a value indicating whether the content type is an element content type. - /// - bool IsElement { get; } - - /// - /// Validates that a combination of culture and segment is valid for the content type properties. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcard are supported. - /// True if the combination is valid; otherwise false. - /// - /// The combination must be valid for properties of the content type. For instance, if the content type varies by culture, - /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content type is invariant, - /// then a variant culture is invalid, because no property could possibly vary by culture. - /// - bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false); - } + /// + /// Validates that a combination of culture and segment is valid for the content type properties. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcard are supported. + /// True if the combination is valid; otherwise false. + /// + /// + /// The combination must be valid for properties of the content type. For instance, if the content type varies by + /// culture, + /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content + /// type is invariant, + /// then a variant culture is invalid, because no property could possibly vary by culture. + /// + /// + bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false); } diff --git a/src/Umbraco.Core/Models/IStylesheet.cs b/src/Umbraco.Core/Models/IStylesheet.cs index e7710f26df..fbe9a1652b 100644 --- a/src/Umbraco.Core/Models/IStylesheet.cs +++ b/src/Umbraco.Core/Models/IStylesheet.cs @@ -1,29 +1,25 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public interface IStylesheet : IFile { - public interface IStylesheet : IFile - { - /// - /// Returns a list of umbraco back office enabled stylesheet properties - /// - /// - /// An umbraco back office enabled stylesheet property has a special prefix, for example: - /// - /// /** umb_name: MyPropertyName */ p { font-size: 1em; } - /// - IEnumerable? Properties { get; } + /// + /// Returns a list of umbraco back office enabled stylesheet properties + /// + /// + /// An umbraco back office enabled stylesheet property has a special prefix, for example: + /// /** umb_name: MyPropertyName */ p { font-size: 1em; } + /// + IEnumerable? Properties { get; } - /// - /// Adds an Umbraco stylesheet property for use in the back office - /// - /// - void AddProperty(IStylesheetProperty property); + /// + /// Adds an Umbraco stylesheet property for use in the back office + /// + /// + void AddProperty(IStylesheetProperty property); - /// - /// Removes an Umbraco stylesheet property - /// - /// - void RemoveProperty(string name); - } + /// + /// Removes an Umbraco stylesheet property + /// + /// + void RemoveProperty(string name); } diff --git a/src/Umbraco.Core/Models/IStylesheetProperty.cs b/src/Umbraco.Core/Models/IStylesheetProperty.cs index 781fb474b2..c2bb81060d 100644 --- a/src/Umbraco.Core/Models/IStylesheetProperty.cs +++ b/src/Umbraco.Core/Models/IStylesheetProperty.cs @@ -1,11 +1,12 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IStylesheetProperty : IRememberBeingDirty { - public interface IStylesheetProperty : IRememberBeingDirty - { - string Alias { get; set; } - string Name { get; } - string Value { get; set; } - } + string Alias { get; set; } + + string Name { get; } + + string Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ITag.cs b/src/Umbraco.Core/Models/ITag.cs index 79840481bb..9824ee5ed2 100644 --- a/src/Umbraco.Core/Models/ITag.cs +++ b/src/Umbraco.Core/Models/ITag.cs @@ -1,35 +1,34 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a tag entity. +/// +public interface ITag : IEntity, IRememberBeingDirty { /// - /// Represents a tag entity. + /// Gets or sets the tag group. /// - public interface ITag : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the tag group. - /// - [DataMember] - string Group { get; set; } + [DataMember] + string Group { get; set; } - /// - /// Gets or sets the tag text. - /// - [DataMember] - string Text { get; set; } + /// + /// Gets or sets the tag text. + /// + [DataMember] + string Text { get; set; } - /// - /// Gets or sets the tag language. - /// - [DataMember] - int? LanguageId { get; set; } + /// + /// Gets or sets the tag language. + /// + [DataMember] + int? LanguageId { get; set; } - /// - /// Gets the number of nodes tagged with this tag. - /// - /// Only when returning from queries. - int NodeCount { get; } - } + /// + /// Gets the number of nodes tagged with this tag. + /// + /// Only when returning from queries. + int NodeCount { get; } } diff --git a/src/Umbraco.Core/Models/ITemplate.cs b/src/Umbraco.Core/Models/ITemplate.cs index e20dcc55fa..321fff2831 100644 --- a/src/Umbraco.Core/Models/ITemplate.cs +++ b/src/Umbraco.Core/Models/ITemplate.cs @@ -1,36 +1,33 @@ -using Umbraco.Cms.Core.Models.Entities; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Defines a Template File (Mvc View) +/// +public interface ITemplate : IFile { /// - /// Defines a Template File (Mvc View) + /// Gets the Name of the File including extension /// - public interface ITemplate : IFile, IRememberBeingDirty, ICanBeDirty - { - /// - /// Gets the Name of the File including extension - /// - new string? Name { get; set; } + new string? Name { get; set; } - /// - /// Gets the Alias of the File, which is the name without the extension - /// - new string Alias { get; set; } + /// + /// Gets the Alias of the File, which is the name without the extension + /// + new string Alias { get; set; } - /// - /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') - /// - bool IsMasterTemplate { get; set; } + /// + /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') + /// + bool IsMasterTemplate { get; set; } - /// - /// returns the master template alias - /// - string? MasterTemplateAlias { get; } + /// + /// returns the master template alias + /// + string? MasterTemplateAlias { get; } - /// - /// Set the mastertemplate - /// - /// - void SetMasterTemplate(ITemplate? masterTemplate); - } + /// + /// Set the mastertemplate + /// + /// + void SetMasterTemplate(ITemplate? masterTemplate); } diff --git a/src/Umbraco.Core/Models/ITwoFactorLogin.cs b/src/Umbraco.Core/Models/ITwoFactorLogin.cs index ca005309b2..3840dcb174 100644 --- a/src/Umbraco.Core/Models/ITwoFactorLogin.cs +++ b/src/Umbraco.Core/Models/ITwoFactorLogin.cs @@ -1,12 +1,12 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface ITwoFactorLogin : IEntity, IRememberBeingDirty { - public interface ITwoFactorLogin: IEntity, IRememberBeingDirty - { - string ProviderName { get; } - string Secret { get; } - Guid UserOrMemberKey { get; } - } + string ProviderName { get; } + + string Secret { get; } + + Guid UserOrMemberKey { get; } } diff --git a/src/Umbraco.Core/Models/IconModel.cs b/src/Umbraco.Core/Models/IconModel.cs index 6b09c08602..8fd9005ac3 100644 --- a/src/Umbraco.Core/Models/IconModel.cs +++ b/src/Umbraco.Core/Models/IconModel.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class IconModel { - public class IconModel - { - public string Name { get; set; } = null!; - public string SvgString { get; set; } = null!; - } + public string Name { get; set; } = null!; + + public string SvgString { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/ImageCropAnchor.cs b/src/Umbraco.Core/Models/ImageCropAnchor.cs index 118f7348ae..68544289c6 100644 --- a/src/Umbraco.Core/Models/ImageCropAnchor.cs +++ b/src/Umbraco.Core/Models/ImageCropAnchor.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum ImageCropAnchor { - public enum ImageCropAnchor - { - Center, - Top, - Right, - Bottom, - Left, - TopLeft, - TopRight, - BottomLeft, - BottomRight - } + Center, + Top, + Right, + Bottom, + Left, + TopLeft, + TopRight, + BottomLeft, + BottomRight, } diff --git a/src/Umbraco.Core/Models/ImageCropMode.cs b/src/Umbraco.Core/Models/ImageCropMode.cs index 1cd7294a58..3ce2f4bfb9 100644 --- a/src/Umbraco.Core/Models/ImageCropMode.cs +++ b/src/Umbraco.Core/Models/ImageCropMode.cs @@ -1,35 +1,41 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum ImageCropMode { - public enum ImageCropMode - { - /// - /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original image then the output is cropped to match the new aspect ratio. - /// - Crop, + /// + /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original + /// image then the output is cropped to match the new aspect ratio. + /// + Crop, - /// - /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original image then the output is resized to the maximum possible value in each direction while maintaining the original aspect ratio. - /// - Max, + /// + /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original + /// image then the output is resized to the maximum possible value in each direction while maintaining the original + /// aspect ratio. + /// + Max, - /// - /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original image then the output is stretched to match the new aspect ratio. - /// - Stretch, + /// + /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original + /// image then the output is stretched to match the new aspect ratio. + /// + Stretch, - /// - /// Passing a single dimension will automatically preserve the aspect ratio of the original image. If the requested aspect ratio is different then the image will be padded to fit. - /// - Pad, + /// + /// Passing a single dimension will automatically preserve the aspect ratio of the original image. If the requested + /// aspect ratio is different then the image will be padded to fit. + /// + Pad, - /// - /// When upscaling an image the image pixels themselves are not resized, rather the image is padded to fit the given dimensions. - /// - BoxPad, + /// + /// When upscaling an image the image pixels themselves are not resized, rather the image is padded to fit the given + /// dimensions. + /// + BoxPad, - /// - /// Resizes the image until the shortest side reaches the set given dimension. This will maintain the aspect ratio of the original image. Upscaling is disabled in this mode and the original image will be returned if attempted. - /// - Min - } + /// + /// Resizes the image until the shortest side reaches the set given dimension. This will maintain the aspect ratio of + /// the original image. Upscaling is disabled in this mode and the original image will be returned if attempted. + /// + Min, } diff --git a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs index 876b2bfddb..9fd00ac2ab 100644 --- a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -1,124 +1,122 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated. +/// +public class ImageUrlGenerationOptions : IEquatable { - /// - /// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated. - /// - public class ImageUrlGenerationOptions : IEquatable + public ImageUrlGenerationOptions(string? imageUrl) => ImageUrl = imageUrl; + + public string? ImageUrl { get; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public int? Quality { get; set; } + + public ImageCropMode? ImageCropMode { get; set; } + + public ImageCropAnchor? ImageCropAnchor { get; set; } + + public FocalPointPosition? FocalPoint { get; set; } + + public CropCoordinates? Crop { get; set; } + + public string? CacheBusterValue { get; set; } + + public string? FurtherOptions { get; set; } + + public bool Equals(ImageUrlGenerationOptions? other) + => other != null && + ImageUrl == other.ImageUrl && + Width == other.Width && + Height == other.Height && + Quality == other.Quality && + ImageCropMode == other.ImageCropMode && + ImageCropAnchor == other.ImageCropAnchor && + EqualityComparer.Default.Equals(FocalPoint, other.FocalPoint) && + EqualityComparer.Default.Equals(Crop, other.Crop) && + CacheBusterValue == other.CacheBusterValue && + FurtherOptions == other.FurtherOptions; + + public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); + + public override int GetHashCode() { - public ImageUrlGenerationOptions(string? imageUrl) => ImageUrl = imageUrl; + var hash = default(HashCode); - public string? ImageUrl { get; } + hash.Add(ImageUrl); + hash.Add(Width); + hash.Add(Height); + hash.Add(Quality); + hash.Add(ImageCropMode); + hash.Add(ImageCropAnchor); + hash.Add(FocalPoint); + hash.Add(Crop); + hash.Add(CacheBusterValue); + hash.Add(FurtherOptions); - public int? Width { get; set; } + return hash.ToHashCode(); + } - public int? Height { get; set; } + /// + /// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the + /// total image from 0.0 to 1.0. + /// + public class FocalPointPosition : IEquatable + { + public FocalPointPosition(decimal left, decimal top) + { + Left = left; + Top = top; + } - public int? Quality { get; set; } + public decimal Left { get; } - public ImageCropMode? ImageCropMode { get; set; } + public decimal Top { get; } - public ImageCropAnchor? ImageCropAnchor { get; set; } - - public FocalPointPosition? FocalPoint { get; set; } - - public CropCoordinates? Crop { get; set; } - - public string? CacheBusterValue { get; set; } - - public string? FurtherOptions { get; set; } - - public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); - - public bool Equals(ImageUrlGenerationOptions? other) + public bool Equals(FocalPointPosition? other) => other != null && - ImageUrl == other.ImageUrl && - Width == other.Width && - Height == other.Height && - Quality == other.Quality && - ImageCropMode == other.ImageCropMode && - ImageCropAnchor == other.ImageCropAnchor && - EqualityComparer.Default.Equals(FocalPoint, other.FocalPoint) && - EqualityComparer.Default.Equals(Crop, other.Crop) && - CacheBusterValue == other.CacheBusterValue && - FurtherOptions == other.FurtherOptions; + Left == other.Left && + Top == other.Top; - public override int GetHashCode() + public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); + + public override int GetHashCode() => HashCode.Combine(Left, Top); + } + + /// + /// The bounds of the crop within the original image, in whatever units the registered IImageUrlGenerator uses, + /// typically a percentage between 0.0 and 1.0. + /// + public class CropCoordinates : IEquatable + { + public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) { - var hash = new HashCode(); - - hash.Add(ImageUrl); - hash.Add(Width); - hash.Add(Height); - hash.Add(Quality); - hash.Add(ImageCropMode); - hash.Add(ImageCropAnchor); - hash.Add(FocalPoint); - hash.Add(Crop); - hash.Add(CacheBusterValue); - hash.Add(FurtherOptions); - - return hash.ToHashCode(); + Left = left; + Top = top; + Right = right; + Bottom = bottom; } - /// - /// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the total image from 0.0 to 1.0. - /// - public class FocalPointPosition : IEquatable - { - public FocalPointPosition(decimal left, decimal top) - { - Left = left; - Top = top; - } + public decimal Left { get; } - public decimal Left { get; } + public decimal Top { get; } - public decimal Top { get; } + public decimal Right { get; } - public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); + public decimal Bottom { get; } - public bool Equals(FocalPointPosition? other) - => other != null && - Left == other.Left && - Top == other.Top; + public bool Equals(CropCoordinates? other) + => other != null && + Left == other.Left && + Top == other.Top && + Right == other.Right && + Bottom == other.Bottom; - public override int GetHashCode() => HashCode.Combine(Left, Top); - } + public override bool Equals(object? obj) => Equals(obj as CropCoordinates); - /// - /// The bounds of the crop within the original image, in whatever units the registered IImageUrlGenerator uses, typically a percentage between 0.0 and 1.0. - /// - public class CropCoordinates : IEquatable - { - public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) - { - Left = left; - Top = top; - Right = right; - Bottom = bottom; - } - - public decimal Left { get; } - - public decimal Top { get; } - - public decimal Right { get; } - - public decimal Bottom { get; } - - public override bool Equals(object? obj) => Equals(obj as CropCoordinates); - - public bool Equals(CropCoordinates? other) - => other != null && - Left == other.Left && - Top == other.Top && - Right == other.Right && - Bottom == other.Bottom; - - public override int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom); - } + public override int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom); } } diff --git a/src/Umbraco.Core/Models/KeyValue.cs b/src/Umbraco.Core/Models/KeyValue.cs index 4e38ee3390..bf5b26dbee 100644 --- a/src/Umbraco.Core/Models/KeyValue.cs +++ b/src/Umbraco.Core/Models/KeyValue.cs @@ -1,33 +1,31 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +[Serializable] +[DataContract(IsReference = true)] +public class KeyValue : EntityBase, IKeyValue { - /// - /// Implements . - /// - [Serializable] - [DataContract(IsReference = true)] - public class KeyValue : EntityBase, IKeyValue, IEntity + private string _identifier = null!; + private string? _value; + + /// + public string Identifier { - private string _identifier = null!; - private string? _value; - - /// - public string Identifier - { - get => _identifier; - set => SetPropertyValueAndDetectChanges(value, ref _identifier!, nameof(Identifier)); - } - - /// - public string? Value - { - get => _value; - set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); - } - - bool IEntity.HasIdentity => !string.IsNullOrEmpty(Identifier); + get => _identifier; + set => SetPropertyValueAndDetectChanges(value, ref _identifier!, nameof(Identifier)); } + + /// + public string? Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); + } + + bool IEntity.HasIdentity => !string.IsNullOrEmpty(Identifier); } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index cc526b1518..2072533917 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -3,31 +3,31 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models -{ - /// - /// Represents a Language. - /// - [Serializable] - [DataContract(IsReference = true)] - public class Language : EntityBase, ILanguage - { - private string _isoCode; - private string _cultureName; - private bool _isDefaultVariantLanguage; - private bool _mandatory; - private int? _fallbackLanguageId; +namespace Umbraco.Cms.Core.Models; - /// - /// Initializes a new instance of the class. - /// - /// The ISO code of the language. - /// The name of the language. - public Language(string isoCode, string cultureName) - { - _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); - _cultureName = cultureName ?? throw new ArgumentNullException(nameof(cultureName)); - } +/// +/// Represents a Language. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Language : EntityBase, ILanguage +{ + private string _cultureName; + private int? _fallbackLanguageId; + private bool _isDefaultVariantLanguage; + private string _isoCode; + private bool _mandatory; + + /// + /// Initializes a new instance of the class. + /// + /// The ISO code of the language. + /// The name of the language. + public Language(string isoCode, string cultureName) + { + _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); + _cultureName = cultureName ?? throw new ArgumentNullException(nameof(cultureName)); + } /// [DataMember] @@ -38,46 +38,45 @@ namespace Umbraco.Cms.Core.Models { ArgumentNullException.ThrowIfNull(value); - SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); - } - } - - /// - [DataMember] - public string CultureName - { - get => _cultureName; - set - { - ArgumentNullException.ThrowIfNull(value); - - SetPropertyValueAndDetectChanges(value, ref _cultureName!, nameof(CultureName)); - } - } - - /// - [IgnoreDataMember] - public CultureInfo? CultureInfo => IsoCode is not null ? CultureInfo.GetCultureInfo(IsoCode) : null; - - /// - public bool IsDefault - { - get => _isDefaultVariantLanguage; - set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, nameof(IsDefault)); - } - - /// - public bool IsMandatory - { - get => _mandatory; - set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(IsMandatory)); - } - - /// - public int? FallbackLanguageId - { - get => _fallbackLanguageId; - set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, nameof(FallbackLanguageId)); + SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); } } + + /// + [DataMember] + public string CultureName + { + get => _cultureName; + set + { + ArgumentNullException.ThrowIfNull(value); + + SetPropertyValueAndDetectChanges(value, ref _cultureName!, nameof(CultureName)); + } + } + + /// + [IgnoreDataMember] + public CultureInfo? CultureInfo => IsoCode is not null ? CultureInfo.GetCultureInfo(IsoCode) : null; + + /// + public bool IsDefault + { + get => _isDefaultVariantLanguage; + set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, nameof(IsDefault)); + } + + /// + public bool IsMandatory + { + get => _mandatory; + set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(IsMandatory)); + } + + /// + public int? FallbackLanguageId + { + get => _fallbackLanguageId; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, nameof(FallbackLanguageId)); + } } diff --git a/src/Umbraco.Core/Models/Link.cs b/src/Umbraco.Core/Models/Link.cs index 3bfc9c5a0d..7047b54555 100644 --- a/src/Umbraco.Core/Models/Link.cs +++ b/src/Umbraco.Core/Models/Link.cs @@ -1,11 +1,14 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class Link { - public class Link - { - public string? Name { get; set; } - public string? Target { get; set; } - public LinkType Type { get; set; } - public Udi? Udi { get; set; } - public string? Url { get; set; } - } + public string? Name { get; set; } + + public string? Target { get; set; } + + public LinkType Type { get; set; } + + public Udi? Udi { get; set; } + + public string? Url { get; set; } } diff --git a/src/Umbraco.Core/Models/LinkType.cs b/src/Umbraco.Core/Models/LinkType.cs index e4879249d8..5003805043 100644 --- a/src/Umbraco.Core/Models/LinkType.cs +++ b/src/Umbraco.Core/Models/LinkType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum LinkType { - public enum LinkType - { - Content, - Media, - External - } + Content, + Media, + External, } diff --git a/src/Umbraco.Core/Models/LogViewerQuery.cs b/src/Umbraco.Core/Models/LogViewerQuery.cs index e9c0dc3180..5941763e24 100644 --- a/src/Umbraco.Core/Models/LogViewerQuery.cs +++ b/src/Umbraco.Core/Models/LogViewerQuery.cs @@ -1,34 +1,32 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class LogViewerQuery : EntityBase, ILogViewerQuery { - [Serializable] - [DataContract(IsReference = true)] - public class LogViewerQuery : EntityBase, ILogViewerQuery + private string? _name; + private string? _query; + + public LogViewerQuery(string? name, string? query) { - private string? _name; - private string? _query; + Name = name; + _query = query; + } - public LogViewerQuery(string? name, string? query) - { - Name = name; - _query = query; - } + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - [DataMember] - public string? Query - { - get => _query; - set => SetPropertyValueAndDetectChanges(value, ref _query, nameof(Query)); - } + [DataMember] + public string? Query + { + get => _query; + set => SetPropertyValueAndDetectChanges(value, ref _query, nameof(Query)); } } diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index 1e395c2158..ea03750e32 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -1,275 +1,287 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Macro +/// +[Serializable] +[DataContract(IsReference = true)] +public class Macro : EntityBase, IMacro { - /// - /// Represents a Macro - /// - [Serializable] - [DataContract(IsReference = true)] - public class Macro : EntityBase, IMacro + private readonly IShortStringHelper _shortStringHelper; + private List _addedProperties; + + private string _alias; + private bool _cacheByMember; + private bool _cacheByPage; + private int _cacheDuration; + private bool _dontRender; + private string _macroSource; + private string? _name; + private List _removedProperties; + private bool _useInEditor; + + public Macro(IShortStringHelper shortStringHelper) { - private readonly IShortStringHelper _shortStringHelper; + _alias = string.Empty; + _shortStringHelper = shortStringHelper; + Properties = new MacroPropertyCollection(); + Properties.CollectionChanged += PropertiesChanged; + _addedProperties = new List(); + _removedProperties = new List(); + _macroSource = string.Empty; + } - public Macro(IShortStringHelper shortStringHelper) + /// + /// Creates an item with pre-filled properties + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Macro( + IShortStringHelper shortStringHelper, + int id, + Guid key, + bool useInEditor, + int cacheDuration, + string alias, + string? name, + bool cacheByPage, + bool cacheByMember, + bool dontRender, + string macroSource) + : this(shortStringHelper) + { + Id = id; + Key = key; + UseInEditor = useInEditor; + CacheDuration = cacheDuration; + Alias = alias.ToCleanString(shortStringHelper, CleanStringType.Alias); + Name = name; + CacheByPage = cacheByPage; + CacheByMember = cacheByMember; + DontRender = dontRender; + MacroSource = macroSource; + } + + /// + /// Creates an instance for persisting a new item + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Macro( + IShortStringHelper shortStringHelper, + string alias, + string? name, + string macroSource, + bool cacheByPage = false, + bool cacheByMember = false, + bool dontRender = true, + bool useInEditor = false, + int cacheDuration = 0) + : this(shortStringHelper) + { + UseInEditor = useInEditor; + CacheDuration = cacheDuration; + Alias = alias.ToCleanString(shortStringHelper, CleanStringType.Alias); + Name = name; + CacheByPage = cacheByPage; + CacheByMember = cacheByMember; + DontRender = dontRender; + MacroSource = macroSource; + } + + /// + /// Gets or sets the alias of the Macro + /// + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.Alias), + ref _alias!, + nameof(Alias)); + } + + /// + /// Used internally to check if we need to add a section in the repository to the db + /// + internal IEnumerable AddedProperties => _addedProperties; + + /// + /// Used internally to check if we need to remove a section in the repository to the db + /// + internal IEnumerable RemovedProperties => _removedProperties; + + public override void ResetDirtyProperties(bool rememberDirty) + { + base.ResetDirtyProperties(rememberDirty); + + _addedProperties.Clear(); + _removedProperties.Clear(); + + foreach (IMacroProperty prop in Properties) { - _alias = string.Empty; - _shortStringHelper = shortStringHelper; - _properties = new MacroPropertyCollection(); - _properties.CollectionChanged += PropertiesChanged; - _addedProperties = new List(); - _removedProperties = new List(); - _macroSource = string.Empty; - } - - /// - /// Creates an item with pre-filled properties - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public Macro(IShortStringHelper shortStringHelper, int id, Guid key, bool useInEditor, int cacheDuration, string @alias, string? name, bool cacheByPage, bool cacheByMember, bool dontRender, string macroSource) - : this(shortStringHelper) - { - Id = id; - Key = key; - UseInEditor = useInEditor; - CacheDuration = cacheDuration; - Alias = alias.ToCleanString(shortStringHelper,CleanStringType.Alias); - Name = name; - CacheByPage = cacheByPage; - CacheByMember = cacheByMember; - DontRender = dontRender; - MacroSource = macroSource; - } - - /// - /// Creates an instance for persisting a new item - /// - /// - /// - /// - /// - /// - /// - /// - /// - public Macro(IShortStringHelper shortStringHelper, string @alias, string? name, - string macroSource, - bool cacheByPage = false, - bool cacheByMember = false, - bool dontRender = true, - bool useInEditor = false, - int cacheDuration = 0) - : this(shortStringHelper) - { - UseInEditor = useInEditor; - CacheDuration = cacheDuration; - Alias = alias.ToCleanString(shortStringHelper, CleanStringType.Alias); - Name = name; - CacheByPage = cacheByPage; - CacheByMember = cacheByMember; - DontRender = dontRender; - MacroSource = macroSource; - } - - private string _alias; - private string? _name; - private bool _useInEditor; - private int _cacheDuration; - private bool _cacheByPage; - private bool _cacheByMember; - private bool _dontRender; - private string _macroSource; - private MacroPropertyCollection _properties; - private List _addedProperties; - private List _removedProperties; - - void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(Properties)); - - if (e.Action == NotifyCollectionChangedAction.Add) - { - //listen for changes - MacroProperty? prop = e.NewItems?.Cast().First(); - if (prop is not null) - { - prop.PropertyChanged += PropertyDataChanged; - - var alias = prop.Alias; - - if (_addedProperties.Contains(alias) == false) - { - //add to the added props - _addedProperties.Add(alias); - } - } - } - else if (e.Action == NotifyCollectionChangedAction.Remove) - { - //remove listening for changes - var prop = e.OldItems?.Cast().First(); - if (prop is not null) - { - prop.PropertyChanged -= PropertyDataChanged; - - var alias = prop.Alias; - - if (_removedProperties.Contains(alias) == false) - { - _removedProperties.Add(alias); - } - } - } - } - - /// - /// When some data of a property has changed ensure our Properties flag is dirty - /// - /// - /// - void PropertyDataChanged(object? sender, PropertyChangedEventArgs e) - { - OnPropertyChanged(nameof(Properties)); - } - - public override void ResetDirtyProperties(bool rememberDirty) - { - base.ResetDirtyProperties(rememberDirty); - - _addedProperties.Clear(); - _removedProperties.Clear(); - - foreach (var prop in Properties) - { - prop.ResetDirtyProperties(rememberDirty); - } - } - - /// - /// Used internally to check if we need to add a section in the repository to the db - /// - internal IEnumerable AddedProperties => _addedProperties; - - /// - /// Used internally to check if we need to remove a section in the repository to the db - /// - internal IEnumerable RemovedProperties => _removedProperties; - - /// - /// Gets or sets the alias of the Macro - /// - [DataMember] - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value.ToCleanString(_shortStringHelper, CleanStringType.Alias), ref _alias!, nameof(Alias)); - } - - /// - /// Gets or sets the name of the Macro - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro can be used in an Editor - /// - [DataMember] - public bool UseInEditor - { - get => _useInEditor; - set => SetPropertyValueAndDetectChanges(value, ref _useInEditor, nameof(UseInEditor)); - } - - /// - /// Gets or sets the Cache Duration for the Macro - /// - [DataMember] - public int CacheDuration - { - get => _cacheDuration; - set => SetPropertyValueAndDetectChanges(value, ref _cacheDuration, nameof(CacheDuration)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached by Page - /// - [DataMember] - public bool CacheByPage - { - get => _cacheByPage; - set => SetPropertyValueAndDetectChanges(value, ref _cacheByPage, nameof(CacheByPage)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached Personally - /// - [DataMember] - public bool CacheByMember - { - get => _cacheByMember; - set => SetPropertyValueAndDetectChanges(value, ref _cacheByMember, nameof(CacheByMember)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor - /// - [DataMember] - public bool DontRender - { - get => _dontRender; - set => SetPropertyValueAndDetectChanges(value, ref _dontRender, nameof(DontRender)); - } - - /// - /// Gets or set the path to the Partial View to render - /// - [DataMember] - public string MacroSource - { - get => _macroSource; - set => SetPropertyValueAndDetectChanges(value, ref _macroSource!, nameof(MacroSource)); - } - - /// - /// Gets or sets a list of Macro Properties - /// - [DataMember] - public MacroPropertyCollection Properties => _properties; - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (Macro)clone; - - clonedEntity._addedProperties = new List(); - clonedEntity._removedProperties = new List(); - clonedEntity._properties = (MacroPropertyCollection)Properties.DeepClone(); - //re-assign the event handler - clonedEntity._properties.CollectionChanged += clonedEntity.PropertiesChanged; - + prop.ResetDirtyProperties(rememberDirty); } } + + /// + /// Gets or sets the name of the Macro + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro can be used in an Editor + /// + [DataMember] + public bool UseInEditor + { + get => _useInEditor; + set => SetPropertyValueAndDetectChanges(value, ref _useInEditor, nameof(UseInEditor)); + } + + /// + /// Gets or sets the Cache Duration for the Macro + /// + [DataMember] + public int CacheDuration + { + get => _cacheDuration; + set => SetPropertyValueAndDetectChanges(value, ref _cacheDuration, nameof(CacheDuration)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached by Page + /// + [DataMember] + public bool CacheByPage + { + get => _cacheByPage; + set => SetPropertyValueAndDetectChanges(value, ref _cacheByPage, nameof(CacheByPage)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached Personally + /// + [DataMember] + public bool CacheByMember + { + get => _cacheByMember; + set => SetPropertyValueAndDetectChanges(value, ref _cacheByMember, nameof(CacheByMember)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor + /// + [DataMember] + public bool DontRender + { + get => _dontRender; + set => SetPropertyValueAndDetectChanges(value, ref _dontRender, nameof(DontRender)); + } + + /// + /// Gets or set the path to the Partial View to render + /// + [DataMember] + public string MacroSource + { + get => _macroSource; + set => SetPropertyValueAndDetectChanges(value, ref _macroSource!, nameof(MacroSource)); + } + + /// + /// Gets or sets a list of Macro Properties + /// + [DataMember] + public MacroPropertyCollection Properties { get; private set; } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (Macro)clone; + + clonedEntity._addedProperties = new List(); + clonedEntity._removedProperties = new List(); + clonedEntity.Properties = (MacroPropertyCollection)Properties.DeepClone(); + + // re-assign the event handler + clonedEntity.Properties.CollectionChanged += clonedEntity.PropertiesChanged; + } + + private void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(Properties)); + + if (e.Action == NotifyCollectionChangedAction.Add) + { + // listen for changes + MacroProperty? prop = e.NewItems?.Cast().First(); + if (prop is not null) + { + prop.PropertyChanged += PropertyDataChanged; + + var alias = prop.Alias; + + if (_addedProperties.Contains(alias) == false) + { + // add to the added props + _addedProperties.Add(alias); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + // remove listening for changes + MacroProperty? prop = e.OldItems?.Cast().First(); + if (prop is not null) + { + prop.PropertyChanged -= PropertyDataChanged; + + var alias = prop.Alias; + + if (_removedProperties.Contains(alias) == false) + { + _removedProperties.Add(alias); + } + } + } + } + + /// + /// When some data of a property has changed ensure our Properties flag is dirty + /// + /// + /// + private void PropertyDataChanged(object? sender, PropertyChangedEventArgs e) => + OnPropertyChanged(nameof(Properties)); } diff --git a/src/Umbraco.Core/Models/MacroProperty.cs b/src/Umbraco.Core/Models/MacroProperty.cs index 659334258e..2a6f041fc0 100644 --- a/src/Umbraco.Core/Models/MacroProperty.cs +++ b/src/Umbraco.Core/Models/MacroProperty.cs @@ -1,159 +1,168 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Macro Property +/// +[Serializable] +[DataContract(IsReference = true)] +public class MacroProperty : BeingDirtyBase, IMacroProperty { - /// - /// Represents a Macro Property - /// - [Serializable] - [DataContract(IsReference = true)] - public class MacroProperty : BeingDirtyBase, IMacroProperty + private string _alias; + private string _editorAlias; + private int _id; + + private Guid _key; + private string? _name; + private int _sortOrder; + + public MacroProperty() { - public MacroProperty() + _editorAlias = string.Empty; + _alias = string.Empty; + _key = Guid.NewGuid(); + } + + /// + /// Ctor for creating a new property + /// + /// + /// + /// + /// + public MacroProperty(string alias, string? name, int sortOrder, string editorAlias) + { + _alias = alias; + _name = name; + _sortOrder = sortOrder; + _key = Guid.NewGuid(); + _editorAlias = editorAlias; + } + + /// + /// Ctor for creating an existing property + /// + /// + /// + /// + /// + /// + /// + public MacroProperty(int id, Guid key, string alias, string? name, int sortOrder, string editorAlias) + { + _id = id; + _alias = alias; + _name = name; + _sortOrder = sortOrder; + _key = key; + _editorAlias = editorAlias; + } + + /// + /// Gets or sets the Key of the Property + /// + [DataMember] + public Guid Key + { + get => _key; + set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); + } + + /// + /// Gets or sets the Alias of the Property + /// + [DataMember] + public int Id + { + get => _id; + set => SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); + } + + /// + /// Gets or sets the Alias of the Property + /// + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + } + + /// + /// Gets or sets the Name of the Property + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the Sort Order of the Property + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } + + /// + /// Gets or sets the Type for this Property + /// + /// + /// The MacroPropertyTypes acts as a plugin for Macros. + /// All types was previously contained in the database, but has been ported to code. + /// + [DataMember] + public string EditorAlias + { + get => _editorAlias; + set => SetPropertyValueAndDetectChanges(value, ref _editorAlias!, nameof(EditorAlias)); + } + + public object DeepClone() + { + // Memberwise clone on MacroProperty will work since it doesn't have any deep elements + // for any sub class this will work for standard properties as well that aren't complex object's themselves. + var clone = (MacroProperty)MemberwiseClone(); + + // Automatically deep clone ref properties that are IDeepCloneable + DeepCloneHelper.DeepCloneRefProperties(this, clone); + clone.ResetDirtyProperties(false); + return clone; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - _editorAlias = string.Empty; - _alias = string.Empty; - _key = Guid.NewGuid(); + return false; } - /// - /// Ctor for creating a new property - /// - /// - /// - /// - /// - public MacroProperty(string @alias, string? name, int sortOrder, string editorAlias) + if (ReferenceEquals(this, obj)) { - _alias = alias; - _name = name; - _sortOrder = sortOrder; - _key = Guid.NewGuid(); - _editorAlias = editorAlias; + return true; } - /// - /// Ctor for creating an existing property - /// - /// - /// - /// - /// - /// - /// - public MacroProperty(int id, Guid key, string @alias, string? name, int sortOrder, string editorAlias) + if (obj.GetType() != GetType()) { - _id = id; - _alias = alias; - _name = name; - _sortOrder = sortOrder; - _key = key; - _editorAlias = editorAlias; + return false; } - private Guid _key; - private string _alias; - private string? _name; - private int _sortOrder; - private int _id; - private string _editorAlias; + return Equals((MacroProperty)obj); + } - /// - /// Gets or sets the Key of the Property - /// - [DataMember] - public Guid Key - { - get => _key; - set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); - } + protected bool Equals(MacroProperty other) => string.Equals(_alias, other._alias) && _id == other._id; - /// - /// Gets or sets the Alias of the Property - /// - [DataMember] - public int Id + public override int GetHashCode() + { + unchecked { - get => _id; - set => SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); - } - - /// - /// Gets or sets the Alias of the Property - /// - [DataMember] - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); - } - - /// - /// Gets or sets the Name of the Property - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Gets or sets the Sort Order of the Property - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } - - /// - /// Gets or sets the Type for this Property - /// - /// - /// The MacroPropertyTypes acts as a plugin for Macros. - /// All types was previously contained in the database, but has been ported to code. - /// - [DataMember] - public string EditorAlias - { - get => _editorAlias; - set => SetPropertyValueAndDetectChanges(value, ref _editorAlias!, nameof(EditorAlias)); - } - - public object DeepClone() - { - //Memberwise clone on MacroProperty will work since it doesn't have any deep elements - // for any sub class this will work for standard properties as well that aren't complex object's themselves. - var clone = (MacroProperty)MemberwiseClone(); - //Automatically deep clone ref properties that are IDeepCloneable - DeepCloneHelper.DeepCloneRefProperties(this, clone); - clone.ResetDirtyProperties(false); - return clone; - } - - protected bool Equals(MacroProperty other) - { - return string.Equals(_alias, other._alias) && _id == other._id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MacroProperty) obj); - } - - public override int GetHashCode() - { - unchecked - { - return ((_alias != null ? _alias.GetHashCode() : 0)*397) ^ _id; - } + return ((_alias != null ? _alias.GetHashCode() : 0) * 397) ^ _id; } } } diff --git a/src/Umbraco.Core/Models/MacroPropertyCollection.cs b/src/Umbraco.Core/Models/MacroPropertyCollection.cs index cda46d2af7..c31cc8cbc1 100644 --- a/src/Umbraco.Core/Models/MacroPropertyCollection.cs +++ b/src/Umbraco.Core/Models/MacroPropertyCollection.cs @@ -1,66 +1,66 @@ -using System; using Umbraco.Cms.Core.Collections; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A macro's property collection +/// +public class MacroPropertyCollection : ObservableDictionary, IDeepCloneable { - /// - /// A macro's property collection - /// - public class MacroPropertyCollection : ObservableDictionary, IDeepCloneable + public MacroPropertyCollection() + : base(property => property.Alias) { - public MacroPropertyCollection() - : base(property => property.Alias) - { - } - - public object DeepClone() - { - var clone = new MacroPropertyCollection(); - foreach (var item in this) - { - clone.Add((IMacroProperty)item.DeepClone()); - } - return clone; - } - - /// - /// Used to update an existing macro property - /// - /// - /// - /// - /// - /// The existing property alias - /// - /// - public void UpdateProperty(string currentAlias, string? name = null, int? sortOrder = null, string? editorAlias = null, string? newAlias = null) - { - var prop = this[currentAlias]; - if (prop == null) - { - throw new InvalidOperationException("No property exists with alias " + currentAlias); - } - - if (name.IsNullOrWhiteSpace() == false) - { - prop.Name = name; - } - if (sortOrder.HasValue) - { - prop.SortOrder = sortOrder.Value; - } - if (name.IsNullOrWhiteSpace() == false && editorAlias is not null) - { - prop.EditorAlias = editorAlias; - } - - if (newAlias.IsNullOrWhiteSpace() == false && currentAlias != newAlias && newAlias is not null) - { - prop.Alias = newAlias; - ChangeKey(currentAlias, newAlias); - } - } } + public object DeepClone() + { + var clone = new MacroPropertyCollection(); + foreach (IMacroProperty item in this) + { + clone.Add((IMacroProperty)item.DeepClone()); + } + + return clone; + } + + /// + /// Used to update an existing macro property + /// + /// + /// + /// + /// + /// The existing property alias + /// + /// + public void UpdateProperty(string currentAlias, string? name = null, int? sortOrder = null, string? editorAlias = null, string? newAlias = null) + { + IMacroProperty prop = this[currentAlias]; + if (prop == null) + { + throw new InvalidOperationException("No property exists with alias " + currentAlias); + } + + if (name.IsNullOrWhiteSpace() == false) + { + prop.Name = name; + } + + if (sortOrder.HasValue) + { + prop.SortOrder = sortOrder.Value; + } + + if (name.IsNullOrWhiteSpace() == false && editorAlias is not null) + { + prop.EditorAlias = editorAlias; + } + + if (newAlias.IsNullOrWhiteSpace() == false && currentAlias != newAlias && newAlias is not null) + { + prop.Alias = newAlias; + ChangeKey(currentAlias, newAlias); + } + } } diff --git a/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs index 072611da4c..02095596e7 100644 --- a/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs @@ -1,25 +1,22 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.Mapping -{ - public class AuditMapDefinition : IMapDefinition - { - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new AuditLog(), Map); - } +namespace Umbraco.Cms.Core.Models.Mapping; - // Umbraco.Code.MapAll -UserAvatars -UserName - private void Map(IAuditItem source, AuditLog target, MapperContext context) - { - target.UserId = source.UserId; - target.NodeId = source.Id; - target.Timestamp = source.CreateDate; - target.LogType = source.AuditType.ToString(); - target.EntityType = source.EntityType; - target.Comment = source.Comment; - target.Parameters = source.Parameters; - } +public class AuditMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) => + mapper.Define((source, context) => new AuditLog(), Map); + + // Umbraco.Code.MapAll -UserAvatars -UserName + private void Map(IAuditItem source, AuditLog target, MapperContext context) + { + target.UserId = source.UserId; + target.NodeId = source.Id; + target.Timestamp = source.CreateDate; + target.LogType = source.AuditType.ToString(); + target.EntityType = source.EntityType; + target.Comment = source.Comment; + target.Parameters = source.Parameters; } } diff --git a/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs index b185bb586e..e9ba018f9c 100644 --- a/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs @@ -1,100 +1,98 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class CodeFileMapDefinition : IMapDefinition { - public class CodeFileMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) { - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); - mapper.Define(Map); - mapper.Define(Map); + mapper.Define(Map); + mapper.Define(Map); + } - } + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IStylesheet source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } - // Umbraco.Code.MapAll -Trashed -Udi -Icon - private static void Map(IStylesheet source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - } + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IScript source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } - // Umbraco.Code.MapAll -Trashed -Udi -Icon - private static void Map(IScript source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - } + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IPartialView source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } - // Umbraco.Code.MapAll -Trashed -Udi -Icon - private static void Map(IPartialView source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - } + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(IPartialView source, CodeFileDisplay target, MapperContext context) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet - private static void Map(IPartialView source, CodeFileDisplay target, MapperContext context) - { - target.Content = source.Content; - target.Id = source.Id.ToString(); - target.Name = source.Name; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(IScript source, CodeFileDisplay target, MapperContext context) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet - private static void Map(IScript source, CodeFileDisplay target, MapperContext context) - { - target.Content = source.Content; - target.Id = source.Id.ToString(); - target.Name = source.Name; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(IStylesheet source, CodeFileDisplay target, MapperContext context) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet - private static void Map(IStylesheet source, CodeFileDisplay target, MapperContext context) - { - target.Content = source.Content; - target.Id = source.Id.ToString(); - target.Name = source.Name; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path + private static void Map(CodeFileDisplay source, IPartialView target, MapperContext context) + { + target.Content = source.Content; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path - private static void Map(CodeFileDisplay source, IPartialView target, MapperContext context) - { - target.Content = source.Content; - target.VirtualPath = source.VirtualPath; - } - - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -GetFileContent - // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path - private static void Map(CodeFileDisplay source, IScript target, MapperContext context) - { - target.Content = source.Content; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -GetFileContent + // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path + private static void Map(CodeFileDisplay source, IScript target, MapperContext context) + { + target.Content = source.Content; + target.VirtualPath = source.VirtualPath; } } diff --git a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs index 3832654f45..017ac1eb22 100644 --- a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.ContentApps; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; @@ -10,63 +7,62 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; using UserProfile = Umbraco.Cms.Core.Models.ContentEditing.UserProfile; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class CommonMapper { - public class CommonMapper + private readonly ContentAppFactoryCollection _contentAppDefinitions; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly ILocalizedTextService _localizedTextService; + private readonly IUserService _userService; + + public CommonMapper( + IUserService userService, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + ContentAppFactoryCollection contentAppDefinitions, + ILocalizedTextService localizedTextService) { - private readonly IUserService _userService; - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly ContentAppFactoryCollection _contentAppDefinitions; - private readonly ILocalizedTextService _localizedTextService; + _userService = userService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _contentAppDefinitions = contentAppDefinitions; + _localizedTextService = localizedTextService; + } - public CommonMapper(IUserService userService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - ContentAppFactoryCollection contentAppDefinitions, ILocalizedTextService localizedTextService) + public UserProfile? GetOwner(IContentBase source, MapperContext context) + { + IProfile? profile = source.GetCreatorProfile(_userService); + return profile == null ? null : context.Map(profile); + } + + public UserProfile? GetCreator(IContent source, MapperContext context) + { + IProfile? profile = source.GetWriterProfile(_userService); + return profile == null ? null : context.Map(profile); + } + + public ContentTypeBasic? GetContentType(IContentBase source, MapperContext context) + { + IContentTypeComposition? contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); + ContentTypeBasic? contentTypeBasic = context.Map(contentType); + return contentTypeBasic; + } + + public IEnumerable GetContentApps(IUmbracoEntity source) => GetContentAppsForEntity(source); + + public IEnumerable GetContentAppsForEntity(IEntity source) + { + ContentApp[] apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray(); + + // localize content app names + foreach (ContentApp app in apps) { - _userService = userService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _contentAppDefinitions = contentAppDefinitions; - _localizedTextService = localizedTextService; - } - - public UserProfile? GetOwner(IContentBase source, MapperContext context) - { - var profile = source.GetCreatorProfile(_userService); - return profile == null ? null : context.Map(profile); - } - - public UserProfile? GetCreator(IContent source, MapperContext context) - { - var profile = source.GetWriterProfile(_userService); - return profile == null ? null : context.Map(profile); - } - - public ContentTypeBasic? GetContentType(IContentBase source, MapperContext context) - { - var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); - var contentTypeBasic = context.Map(contentType); - return contentTypeBasic; - } - - public IEnumerable GetContentApps(IUmbracoEntity source) - { - return GetContentAppsForEntity(source); - } - - public IEnumerable GetContentAppsForEntity(IEntity source) - { - var apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray(); - - // localize content app names - foreach (var app in apps) + var localizedAppName = _localizedTextService.Localize("apps", app.Alias); + if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false) { - var localizedAppName = _localizedTextService.Localize("apps", app.Alias); - if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false) - { - app.Name = localizedAppName; - } + app.Name = localizedAppName; } - - return apps; } + + return apps; } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs index 4becc8f21a..9cda25fbbb 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; @@ -7,79 +5,92 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Creates a base generic ContentPropertyBasic from a Property +/// +internal class ContentPropertyBasicMapper + where TDestination : ContentPropertyBasic, new() { - /// - /// Creates a base generic ContentPropertyBasic from a Property - /// - internal class ContentPropertyBasicMapper - where TDestination : ContentPropertyBasic, new() + private readonly IEntityService _entityService; + private readonly ILogger> _logger; + private readonly PropertyEditorCollection _propertyEditors; + + public ContentPropertyBasicMapper( + IDataTypeService dataTypeService, + IEntityService entityService, + ILogger> logger, + PropertyEditorCollection propertyEditors) { - private readonly IEntityService _entityService; - private readonly ILogger> _logger; - private readonly PropertyEditorCollection _propertyEditors; - protected IDataTypeService DataTypeService { get; } + _logger = logger; + _propertyEditors = propertyEditors; + DataTypeService = dataTypeService; + _entityService = entityService; + } - public ContentPropertyBasicMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger> logger, PropertyEditorCollection propertyEditors) - { - _logger = logger; - _propertyEditors = propertyEditors; - DataTypeService = dataTypeService; - _entityService = entityService; - } + protected IDataTypeService DataTypeService { get; } - /// - /// Assigns the PropertyEditor, Id, Alias and Value to the property - /// - /// - public virtual void Map(IProperty property, TDestination dest, MapperContext context) + /// + /// Assigns the PropertyEditor, Id, Alias and Value to the property + /// + /// + public virtual void Map(IProperty property, TDestination dest, MapperContext context) + { + IDataEditor? editor = property.PropertyType is not null ? _propertyEditors[property.PropertyType.PropertyEditorAlias] : null; + if (editor == null) { - var editor = property.PropertyType is not null ? _propertyEditors[property.PropertyType.PropertyEditorAlias] : null; + _logger.LogError( + new NullReferenceException("The property editor with alias " + + property.PropertyType?.PropertyEditorAlias + " does not exist"), + "No property editor '{PropertyEditorAlias}' found, converting to a Label", + property.PropertyType?.PropertyEditorAlias); + + editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label]; + if (editor == null) { - _logger.LogError( - new NullReferenceException("The property editor with alias " + property.PropertyType?.PropertyEditorAlias + " does not exist"), - "No property editor '{PropertyEditorAlias}' found, converting to a Label", - property.PropertyType?.PropertyEditorAlias); - - editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label]; - - if (editor == null) - throw new InvalidOperationException($"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}"); + throw new InvalidOperationException( + $"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}"); } - - dest.Id = property.Id; - dest.Alias = property.Alias; - dest.PropertyEditor = editor; - dest.Editor = editor.Alias; - dest.DataTypeKey = property.PropertyType!.DataTypeKey; - - // if there's a set of property aliases specified, we will check if the current property's value should be mapped. - // if it isn't one of the ones specified in 'includeProperties', we will just return the result without mapping the Value. - var includedProperties = context.GetIncludedProperties(); - if (includedProperties != null && !includedProperties.Contains(property.Alias)) - return; - - //Get the culture from the context which will be set during the mapping operation for each property - var culture = context.GetCulture(); - - //a culture needs to be in the context for a property type that can vary - if (culture == null && property.PropertyType.VariesByCulture()) - throw new InvalidOperationException($"No culture found in mapping operation when one is required for the culture variant property type {property.PropertyType.Alias}"); - - //set the culture to null if it's an invariant property type - culture = !property.PropertyType.VariesByCulture() ? null : culture; - - dest.Culture = culture; - - // Get the segment, which is always allowed to be null even if the propertyType *can* be varied by segment. - // There is therefore no need to perform the null check like with culture above. - var segment = !property.PropertyType.VariesBySegment() ? null : context.GetSegment(); - dest.Segment = segment; - - // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. - dest.Value = editor.GetValueEditor().ToEditor(property, culture, segment); - } + + dest.Id = property.Id; + dest.Alias = property.Alias; + dest.PropertyEditor = editor; + dest.Editor = editor.Alias; + dest.SupportsReadOnly = editor.SupportsReadOnly; + dest.DataTypeKey = property.PropertyType!.DataTypeKey; + + // if there's a set of property aliases specified, we will check if the current property's value should be mapped. + // if it isn't one of the ones specified in 'includeProperties', we will just return the result without mapping the Value. + var includedProperties = context.GetIncludedProperties(); + if (includedProperties != null && !includedProperties.Contains(property.Alias)) + { + return; + } + + // Get the culture from the context which will be set during the mapping operation for each property + var culture = context.GetCulture(); + + // a culture needs to be in the context for a property type that can vary + if (culture == null && property.PropertyType.VariesByCulture()) + { + throw new InvalidOperationException( + $"No culture found in mapping operation when one is required for the culture variant property type {property.PropertyType.Alias}"); + } + + // set the culture to null if it's an invariant property type + culture = !property.PropertyType.VariesByCulture() ? null : culture; + + dest.Culture = culture; + + // Get the segment, which is always allowed to be null even if the propertyType *can* be varied by segment. + // There is therefore no need to perform the null check like with culture above. + var segment = !property.PropertyType.VariesBySegment() ? null : context.GetSegment(); + dest.Segment = segment; + + // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. + dest.Value = editor.GetValueEditor().ToEditor(property, culture, segment); } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs index a31d9e9c27..eb6c6d92e0 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Microsoft.Extensions.Logging; @@ -9,67 +9,75 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Creates a ContentPropertyDisplay from a Property +/// +internal class ContentPropertyDisplayMapper : ContentPropertyBasicMapper { - /// - /// Creates a ContentPropertyDisplay from a Property - /// - internal class ContentPropertyDisplayMapper : ContentPropertyBasicMapper + private readonly ICultureDictionary _cultureDictionary; + private readonly ILocalizedTextService _textService; + + public ContentPropertyDisplayMapper( + ICultureDictionary cultureDictionary, + IDataTypeService dataTypeService, + IEntityService entityService, + ILocalizedTextService textService, + ILogger logger, + PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { - private readonly ICultureDictionary _cultureDictionary; - private readonly ILocalizedTextService _textService; + _cultureDictionary = cultureDictionary; + _textService = textService; + } - public ContentPropertyDisplayMapper(ICultureDictionary cultureDictionary, IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, entityService, logger, propertyEditors) + public override void Map(IProperty originalProp, ContentPropertyDisplay dest, MapperContext context) + { + base.Map(originalProp, dest, context); + + var config = originalProp.PropertyType is null + ? null + : DataTypeService.GetDataType(originalProp.PropertyType.DataTypeId)?.Configuration; + + // TODO: IDataValueEditor configuration - general issue + // GetValueEditor() returns a non-configured IDataValueEditor + // - for richtext and nested, configuration determines HideLabel, so we need to configure the value editor + // - could configuration also determines ValueType, everywhere? + // - does it make any sense to use a IDataValueEditor without configuring it? + + // configure the editor for display with configuration + IDataValueEditor? valEditor = dest.PropertyEditor?.GetValueEditor(config); + + // set the display properties after mapping + dest.Alias = originalProp.Alias; + dest.Description = originalProp.PropertyType?.Description; + dest.Label = originalProp.PropertyType?.Name; + dest.HideLabel = valEditor?.HideLabel ?? false; + dest.LabelOnTop = originalProp.PropertyType?.LabelOnTop; + + // add the validation information + dest.Validation.Mandatory = originalProp.PropertyType?.Mandatory ?? false; + dest.Validation.MandatoryMessage = originalProp.PropertyType?.MandatoryMessage; + dest.Validation.Pattern = originalProp.PropertyType?.ValidationRegExp; + dest.Validation.PatternMessage = originalProp.PropertyType?.ValidationRegExpMessage; + + if (dest.PropertyEditor == null) { - _cultureDictionary = cultureDictionary; - _textService = textService; + // display.Config = PreValueCollection.AsDictionary(preVals); + // if there is no property editor it means that it is a legacy data type + // we cannot support editing with that so we'll just render the readonly value view. + dest.View = "views/propertyeditors/readonlyvalue/readonlyvalue.html"; } - public override void Map(IProperty originalProp, ContentPropertyDisplay dest, MapperContext context) + else { - base.Map(originalProp, dest, context); - - var config = originalProp.PropertyType is null ? null : DataTypeService.GetDataType(originalProp.PropertyType.DataTypeId)?.Configuration; - - // TODO: IDataValueEditor configuration - general issue - // GetValueEditor() returns a non-configured IDataValueEditor - // - for richtext and nested, configuration determines HideLabel, so we need to configure the value editor - // - could configuration also determines ValueType, everywhere? - // - does it make any sense to use a IDataValueEditor without configuring it? - - // configure the editor for display with configuration - var valEditor = dest.PropertyEditor?.GetValueEditor(config); - - //set the display properties after mapping - dest.Alias = originalProp.Alias; - dest.Description = originalProp.PropertyType?.Description; - dest.Label = originalProp.PropertyType?.Name; - dest.HideLabel = valEditor?.HideLabel ?? false; - dest.LabelOnTop = originalProp.PropertyType?.LabelOnTop; - - //add the validation information - dest.Validation.Mandatory = originalProp.PropertyType?.Mandatory ?? false; - dest.Validation.MandatoryMessage = originalProp.PropertyType?.MandatoryMessage; - dest.Validation.Pattern = originalProp.PropertyType?.ValidationRegExp; - dest.Validation.PatternMessage = originalProp.PropertyType?.ValidationRegExpMessage; - - if (dest.PropertyEditor == null) - { - //display.Config = PreValueCollection.AsDictionary(preVals); - //if there is no property editor it means that it is a legacy data type - // we cannot support editing with that so we'll just render the readonly value view. - dest.View = "views/propertyeditors/readonlyvalue/readonlyvalue.html"; - } - else - { - //let the property editor format the pre-values - dest.Config = dest.PropertyEditor.GetConfigurationEditor().ToValueEditor(config); - dest.View = valEditor?.View; - } - - //Translate - dest.Label = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Label); - dest.Description = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Description); + // let the property editor format the pre-values + dest.Config = dest.PropertyEditor.GetConfigurationEditor().ToValueEditor(config); + dest.View = valEditor?.View; } + + // Translate + dest.Label = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Label); + dest.Description = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Description); } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs index fe1eff99ca..5836317b5c 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs @@ -1,32 +1,32 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Creates a ContentPropertyDto from a Property +/// +internal class ContentPropertyDtoMapper : ContentPropertyBasicMapper { - /// - /// Creates a ContentPropertyDto from a Property - /// - internal class ContentPropertyDtoMapper : ContentPropertyBasicMapper + public ContentPropertyDtoMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { - public ContentPropertyDtoMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, entityService, logger, propertyEditors) - { } + } - public override void Map(IProperty property, ContentPropertyDto dest, MapperContext context) - { - base.Map(property, dest, context); + public override void Map(IProperty property, ContentPropertyDto dest, MapperContext context) + { + base.Map(property, dest, context); - dest.IsRequired = property.PropertyType?.Mandatory; - dest.IsRequiredMessage = property.PropertyType?.MandatoryMessage; - dest.ValidationRegExp = property.PropertyType?.ValidationRegExp; - dest.ValidationRegExpMessage = property.PropertyType?.ValidationRegExpMessage; - dest.Description = property.PropertyType?.Description; - dest.Label = property.PropertyType?.Name; - dest.DataType = property.PropertyType is null ? null : DataTypeService.GetDataType(property.PropertyType.DataTypeId); - dest.LabelOnTop = property.PropertyType?.LabelOnTop; - } + dest.IsRequired = property.PropertyType.Mandatory; + dest.IsRequiredMessage = property.PropertyType.MandatoryMessage; + dest.ValidationRegExp = property.PropertyType.ValidationRegExp; + dest.ValidationRegExpMessage = property.PropertyType.ValidationRegExpMessage; + dest.Description = property.PropertyType.Description; + dest.Label = property.PropertyType.Name; + dest.DataType = DataTypeService.GetDataType(property.PropertyType.DataTypeId); + dest.LabelOnTop = property.PropertyType.LabelOnTop; } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs index 270d821380..1e27389ebf 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs @@ -5,60 +5,78 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// A mapper which declares how to map content properties. These mappings are shared among media (and probably members) +/// which is +/// why they are in their own mapper +/// +public class ContentPropertyMapDefinition : IMapDefinition { - /// - /// A mapper which declares how to map content properties. These mappings are shared among media (and probably members) which is - /// why they are in their own mapper - /// - public class ContentPropertyMapDefinition : IMapDefinition + private readonly ContentPropertyBasicMapper _contentPropertyBasicConverter; + private readonly ContentPropertyDisplayMapper _contentPropertyDisplayMapper; + private readonly ContentPropertyDtoMapper _contentPropertyDtoConverter; + + public ContentPropertyMapDefinition( + ICultureDictionary cultureDictionary, + IDataTypeService dataTypeService, + IEntityService entityService, + ILocalizedTextService textService, + ILoggerFactory loggerFactory, + PropertyEditorCollection propertyEditors) { - private readonly ContentPropertyBasicMapper _contentPropertyBasicConverter; - private readonly ContentPropertyDtoMapper _contentPropertyDtoConverter; - private readonly ContentPropertyDisplayMapper _contentPropertyDisplayMapper; - - public ContentPropertyMapDefinition(ICultureDictionary cultureDictionary, IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILoggerFactory loggerFactory, PropertyEditorCollection propertyEditors) - { - _contentPropertyBasicConverter = new ContentPropertyBasicMapper(dataTypeService, entityService, loggerFactory.CreateLogger>(), propertyEditors); - _contentPropertyDtoConverter = new ContentPropertyDtoMapper(dataTypeService, entityService, loggerFactory.CreateLogger(), propertyEditors); - _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper(cultureDictionary, dataTypeService, entityService, textService, loggerFactory.CreateLogger(), propertyEditors); - } - - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define>((source, context) => new Tab(), Map); - mapper.Define((source, context) => new ContentPropertyBasic(), Map); - mapper.Define((source, context) => new ContentPropertyDto(), Map); - mapper.Define((source, context) => new ContentPropertyDisplay(), Map); - } - - // Umbraco.Code.MapAll -Properties -Alias -Expanded - private void Map(PropertyGroup source, Tab target, MapperContext mapper) - { - target.Id = source.Id; - target.Key = source.Key; - target.Type = source.Type.ToString(); - target.Label = source.Name; - target.Alias = source.Alias; - target.IsActive = true; - } - - private void Map(IProperty source, ContentPropertyBasic target, MapperContext context) - { - // assume this is mapping everything and no MapAll is required - _contentPropertyBasicConverter.Map(source, target, context); - } - - private void Map(IProperty source, ContentPropertyDto target, MapperContext context) - { - // assume this is mapping everything and no MapAll is required - _contentPropertyDtoConverter.Map(source, target, context); - } - - private void Map(IProperty source, ContentPropertyDisplay target, MapperContext context) - { - // assume this is mapping everything and no MapAll is required - _contentPropertyDisplayMapper.Map(source, target, context); - } + _contentPropertyBasicConverter = new ContentPropertyBasicMapper( + dataTypeService, + entityService, + loggerFactory.CreateLogger>(), + propertyEditors); + _contentPropertyDtoConverter = new ContentPropertyDtoMapper( + dataTypeService, + entityService, + loggerFactory.CreateLogger(), + propertyEditors); + _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper( + cultureDictionary, + dataTypeService, + entityService, + textService, + loggerFactory.CreateLogger(), + propertyEditors); } + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define>( + (source, context) => new Tab(), Map); + mapper.Define((source, context) => new ContentPropertyBasic(), Map); + mapper.Define((source, context) => new ContentPropertyDto(), Map); + mapper.Define((source, context) => new ContentPropertyDisplay(), Map); + } + + // Umbraco.Code.MapAll -Properties -Alias -Expanded + private void Map(PropertyGroup source, Tab target, MapperContext mapper) + { + target.Id = source.Id; + target.Key = source.Key; + target.Type = source.Type.ToString(); + target.Label = source.Name; + target.Alias = source.Alias; + target.IsActive = true; + } + + private void Map(IProperty source, ContentPropertyBasic target, MapperContext context) => + + // assume this is mapping everything and no MapAll is required + _contentPropertyBasicConverter.Map(source, target, context); + + private void Map(IProperty source, ContentPropertyDto target, MapperContext context) => + + // assume this is mapping everything and no MapAll is required + _contentPropertyDtoConverter.Map(source, target, context); + + private void Map(IProperty source, ContentPropertyDisplay target, MapperContext context) => + + // assume this is mapping everything and no MapAll is required + _contentPropertyDisplayMapper.Map(source, target, context); } diff --git a/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs index a087ce0d3e..ba06dae711 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs @@ -1,76 +1,83 @@ -using System; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Returns the for an item +/// +/// +public class ContentBasicSavedStateMapper + where T : ContentPropertyBasic { - /// - /// Returns the for an item - /// - /// - public class ContentBasicSavedStateMapper - where T : ContentPropertyBasic + private readonly ContentSavedStateMapper _inner = new(); + + public ContentSavedState? Map(IContent source, MapperContext context) => _inner.Map(source, context); +} + +/// +/// Returns the for an item +/// +/// +public class ContentSavedStateMapper + where T : ContentPropertyBasic +{ + public ContentSavedState Map(IContent source, MapperContext context) { - private readonly ContentSavedStateMapper _inner = new ContentSavedStateMapper(); + PublishedState publishedState; + bool isEdited; + bool isCreated; - public ContentSavedState? Map(IContent source, MapperContext context) + if (source.ContentType.VariesByCulture()) { - return _inner.Map(source, context); - } - } + // Get the culture from the context which will be set during the mapping operation for each variant + var culture = context.GetCulture(); - /// - /// Returns the for an item - /// - /// - public class ContentSavedStateMapper - where T : ContentPropertyBasic - { - public ContentSavedState Map(IContent source, MapperContext context) - { - PublishedState publishedState; - bool isEdited; - bool isCreated; - - if (source.ContentType.VariesByCulture()) + // a culture needs to be in the context for a variant content item + if (culture == null) { - //Get the culture from the context which will be set during the mapping operation for each variant - var culture = context.GetCulture(); + throw new InvalidOperationException( + "No culture found in mapping operation when one is required for a culture variant"); + } - //a culture needs to be in the context for a variant content item - if (culture == null) - throw new InvalidOperationException($"No culture found in mapping operation when one is required for a culture variant"); - - publishedState = source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished + publishedState = + source.PublishedState == + PublishedState + .Unpublished // if the entire document is unpublished, then flag every variant as unpublished ? PublishedState.Unpublished : source.IsCulturePublished(culture) ? PublishedState.Published : PublishedState.Unpublished; - isEdited = source.IsCultureEdited(culture); - isCreated = source.Id > 0 && source.IsCultureAvailable(culture); - } - else - { - publishedState = source.PublishedState == PublishedState.Unpublished - ? PublishedState.Unpublished - : PublishedState.Published; - - isEdited = source.Edited; - isCreated = source.Id > 0; - } - - if (!isCreated) - return ContentSavedState.NotCreated; - - if (publishedState == PublishedState.Unpublished) - return ContentSavedState.Draft; - - if (publishedState == PublishedState.Published) - return isEdited ? ContentSavedState.PublishedPendingChanges : ContentSavedState.Published; - - throw new NotSupportedException($"PublishedState {publishedState} is not supported."); + isEdited = source.IsCultureEdited(culture); + isCreated = source.Id > 0 && source.IsCultureAvailable(culture); } + else + { + publishedState = source.PublishedState == PublishedState.Unpublished + ? PublishedState.Unpublished + : PublishedState.Published; + + isEdited = source.Edited; + isCreated = source.Id > 0; + } + + if (!isCreated) + { + return ContentSavedState.NotCreated; + } + + if (publishedState == PublishedState.Unpublished) + { + return ContentSavedState.Draft; + } + + if (publishedState == PublishedState.Published) + { + return isEdited ? ContentSavedState.PublishedPendingChanges : ContentSavedState.Published; + } + + throw new NotSupportedException($"PublishedState {publishedState} is not supported."); } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs index 7d58a69616..e8df77aa68 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -13,888 +9,910 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Defines mappings for content/media/members type mappings +/// +public class ContentTypeMapDefinition : IMapDefinition { - /// - /// Defines mappings for content/media/members type mappings - /// - public class ContentTypeMapDefinition : IMapDefinition + private readonly CommonMapper _commonMapper; + private readonly IContentTypeService _contentTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly IFileService _fileService; + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IShortStringHelper _shortStringHelper; + private ContentSettings _contentSettings; + + public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, IFileService fileService, + IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, + IHostingEnvironment hostingEnvironment, IOptionsMonitor contentSettings) { - private readonly CommonMapper _commonMapper; - private readonly IContentTypeService _contentTypeService; - private readonly IDataTypeService _dataTypeService; - private readonly IFileService _fileService; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IMediaTypeService _mediaTypeService; - private readonly IMemberTypeService _memberTypeService; - private readonly PropertyEditorCollection _propertyEditors; - private readonly IShortStringHelper _shortStringHelper; - private ContentSettings _contentSettings; + _commonMapper = commonMapper; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _fileService = fileService; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); + _shortStringHelper = shortStringHelper; + _globalSettings = globalSettings.Value; + _hostingEnvironment = hostingEnvironment; - public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, IFileService fileService, - IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, - IMemberTypeService memberTypeService, - ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, - IHostingEnvironment hostingEnvironment, IOptionsMonitor contentSettings) - { - _commonMapper = commonMapper; - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _fileService = fileService; - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _memberTypeService = memberTypeService; - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); - _shortStringHelper = shortStringHelper; - _globalSettings = globalSettings.Value; - _hostingEnvironment = hostingEnvironment; + _contentSettings = contentSettings.CurrentValue; + contentSettings.OnChange(x => _contentSettings = x); + } - _contentSettings = contentSettings.CurrentValue; - contentSettings.OnChange(x => _contentSettings = x); - } + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define( + (source, context) => new ContentType(_shortStringHelper, source.ParentId), Map); + mapper.Define( + (source, context) => new MediaType(_shortStringHelper, source.ParentId), Map); + mapper.Define( + (source, context) => new MemberType(_shortStringHelper, source.ParentId), Map); - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define( - (source, context) => new ContentType(_shortStringHelper, source.ParentId), Map); - mapper.Define( - (source, context) => new MediaType(_shortStringHelper, source.ParentId), Map); - mapper.Define( - (source, context) => new MemberType(_shortStringHelper, source.ParentId), Map); + mapper.Define((source, context) => new DocumentTypeDisplay(), Map); + mapper.Define((source, context) => new MediaTypeDisplay(), Map); + mapper.Define((source, context) => new MemberTypeDisplay(), Map); - mapper.Define((source, context) => new DocumentTypeDisplay(), Map); - mapper.Define((source, context) => new MediaTypeDisplay(), Map); - mapper.Define((source, context) => new MemberTypeDisplay(), Map); - - mapper.Define( - (source, context) => - { - IDataType? dataType = _dataTypeService.GetDataType(source.DataTypeId); - if (dataType == null) - { - throw new NullReferenceException("No data type found with id " + source.DataTypeId); - } - - return new PropertyType(_shortStringHelper, dataType, source.Alias); - }, Map); - - // TODO: isPublishing in ctor? - mapper.Define, PropertyGroup>( - (source, context) => new PropertyGroup(false), Map); - mapper.Define, PropertyGroup>( - (source, context) => new PropertyGroup(false), Map); - - mapper.Define((source, context) => new ContentTypeBasic(), Map); - mapper.Define((source, context) => new ContentTypeBasic(), Map); - mapper.Define((source, context) => new ContentTypeBasic(), Map); - mapper.Define((source, context) => new ContentTypeBasic(), Map); - - mapper.Define((source, context) => new DocumentTypeDisplay(), Map); - mapper.Define((source, context) => new MediaTypeDisplay(), Map); - mapper.Define((source, context) => new MemberTypeDisplay(), Map); - - mapper.Define, PropertyGroupDisplay>( - (source, context) => new PropertyGroupDisplay(), Map); - mapper.Define, PropertyGroupDisplay>( - (source, context) => new PropertyGroupDisplay(), Map); - - mapper.Define((source, context) => new PropertyTypeDisplay(), Map); - mapper.Define( - (source, context) => new MemberPropertyTypeDisplay(), Map); - } - - // no MapAll - take care - private void Map(DocumentTypeSave source, IContentType target, MapperContext context) - { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _contentTypeService.Get(alias)); - - if (target is IContentType targetWithHistoryCleanup) + mapper.Define( + (source, context) => { - MapHistoryCleanup(source, targetWithHistoryCleanup); + IDataType? dataType = _dataTypeService.GetDataType(source.DataTypeId); + if (dataType == null) + { + throw new NullReferenceException("No data type found with id " + source.DataTypeId); + } + + return new PropertyType(_shortStringHelper, dataType, source.Alias); + }, + Map); + + // TODO: isPublishing in ctor? + mapper.Define, PropertyGroup>( + (source, context) => new PropertyGroup(false), Map); + mapper.Define, PropertyGroup>( + (source, context) => new PropertyGroup(false), Map); + + mapper.Define((source, context) => new ContentTypeBasic(), Map); + mapper.Define((source, context) => new ContentTypeBasic(), Map); + mapper.Define((source, context) => new ContentTypeBasic(), Map); + mapper.Define((source, context) => new ContentTypeBasic(), Map); + + mapper.Define((source, context) => new DocumentTypeDisplay(), Map); + mapper.Define((source, context) => new MediaTypeDisplay(), Map); + mapper.Define((source, context) => new MemberTypeDisplay(), Map); + + mapper.Define, PropertyGroupDisplay>( + (source, context) => new PropertyGroupDisplay(), Map); + mapper.Define, PropertyGroupDisplay>( + (source, context) => new PropertyGroupDisplay(), Map); + + mapper.Define((source, context) => new PropertyTypeDisplay(), Map); + mapper.Define( + (source, context) => new MemberPropertyTypeDisplay(), Map); + } + + public static Udi? MapContentTypeUdi(IContentTypeComposition source) + { + if (source == null) + { + return null; + } + + string udiType; + switch (source) + { + case IMemberType _: + udiType = Constants.UdiEntityType.MemberType; + break; + case IMediaType _: + udiType = Constants.UdiEntityType.MediaType; + break; + case IContentType _: + udiType = Constants.UdiEntityType.DocumentType; + break; + default: + throw new PanicException($"Source is of type {source.GetType()} which isn't supported here"); + } + + return Udi.Create(udiType, source.Key); + } + + // no MapAll - take care + private void Map(DocumentTypeSave source, IContentType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _contentTypeService.Get(alias)); + + MapHistoryCleanup(source, target); + + target.AllowedTemplates = source.AllowedTemplates? + .Where(x => x != null) + .Select(_fileService.GetTemplate) + .WhereNotNull() + .ToArray(); + + target.SetDefaultTemplate(source.DefaultTemplate == null + ? null + : _fileService.GetTemplate(source.DefaultTemplate)); + } + + private static void MapHistoryCleanup(DocumentTypeSave source, IContentType target) + { + // If source history cleanup is null we don't have to map all properties + if (source.HistoryCleanup is null) + { + target.HistoryCleanup = null; + return; + } + + // We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties + target.HistoryCleanup!.ResetDirtyProperties(false); + if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup) + { + target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup; + } + + if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays) + { + target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays; + } + + if (target.HistoryCleanup.KeepLatestVersionPerDayForDays != + source.HistoryCleanup.KeepLatestVersionPerDayForDays) + { + target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays; + } + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations + private static void Map(PropertyTypeBasic source, IPropertyType target, MapperContext context) + { + target.Name = source.Label; + target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; + target.Mandatory = source.Validation?.Mandatory ?? false; + target.MandatoryMessage = source.Validation?.MandatoryMessage; + target.ValidationRegExp = source.Validation?.Pattern; + target.ValidationRegExpMessage = source.Validation?.PatternMessage; + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); + + if (source.Id > 0) + { + target.Id = source.Id; + } + + if (source.GroupId > 0) + { + if (target.PropertyGroupId?.Value != source.GroupId) + { + target.PropertyGroupId = new Lazy(() => source.GroupId, false); + } + } + + target.Alias = source.Alias; + target.Description = source.Description; + target.SortOrder = source.SortOrder; + target.LabelOnTop = source.LabelOnTop; + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes + private static void Map(PropertyGroupBasic source, PropertyGroup target, MapperContext context) + { + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + } + + // no MapAll - take care + private void Map(MediaTypeSave source, IMediaType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _mediaTypeService.Get(alias)); + } + + // no MapAll - take care + private void Map(MemberTypeSave source, IMemberType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _memberTypeService.Get(alias)); + + foreach (MemberPropertyTypeBasic propertyType in source.Groups.SelectMany(x => x.Properties)) + { + MemberPropertyTypeBasic localCopy = propertyType; + IPropertyType? destProp = + target.PropertyTypes.SingleOrDefault(x => x.Alias?.InvariantEquals(localCopy.Alias) ?? false); + if (destProp == null) + { + continue; } - target.AllowedTemplates = source.AllowedTemplates? - .Where(x => x != null) - .Select(_fileService.GetTemplate) + target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); + target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); + target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); + } + } + + // no MapAll - take care + private void Map(IContentType source, DocumentTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); + + if (source is IContentType sourceWithHistoryCleanup) + { + target.HistoryCleanup = new HistoryCleanupViewModel + { + PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, + KeepAllVersionsNewerThanDays = + sourceWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = + sourceWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays, + GlobalKeepAllVersionsNewerThanDays = + _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays, + GlobalKeepLatestVersionPerDayForDays = + _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays, + GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup + }; + } + + target.AllowCultureVariant = source.VariesByCulture(); + target.AllowSegmentVariant = source.VariesBySegment(); + target.ContentApps = _commonMapper.GetContentAppsForEntity(source); + + // sync templates + if (source.AllowedTemplates is not null) + { + target.AllowedTemplates = + context.MapEnumerable(source.AllowedTemplates).WhereNotNull(); + } + + if (source.DefaultTemplate != null) + { + target.DefaultTemplate = context.Map(source.DefaultTemplate); + } + + // default listview + target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; + + if (string.IsNullOrEmpty(source.Alias)) + { + return; + } + + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; + if (_dataTypeService.GetDataType(name) != null) + { + target.ListViewEditorName = name; + } + } + + // no MapAll - take care + private void Map(IMediaType source, MediaTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); + + // default listview + target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; + target.IsSystemMediaType = source.IsSystemMediaType(); + + if (string.IsNullOrEmpty(source.Name)) + { + return; + } + + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; + if (_dataTypeService.GetDataType(name) != null) + { + target.ListViewEditorName = name; + } + } + + // no MapAll - take care + private void Map(IMemberType source, MemberTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); + + // map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData + foreach (IPropertyType propertyType in source.PropertyTypes) + { + IPropertyType localCopy = propertyType; + MemberPropertyTypeDisplay? displayProp = target.Groups.SelectMany(dest => dest.Properties) + .SingleOrDefault(dest => dest.Alias?.InvariantEquals(localCopy.Alias) ?? false); + if (displayProp == null) + { + continue; + } + + displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); + displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); + displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); + } + } + + // Umbraco.Code.MapAll -Blueprints + private void Map(IContentTypeBase source, ContentTypeBasic target, string entityType) + { + target.Udi = Udi.Create(entityType, source.Key); + target.Alias = source.Alias; + target.CreateDate = source.CreateDate; + target.Description = source.Description; + target.Icon = source.Icon; + target.IconFilePath = target.IconIsClass + ? string.Empty + : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; + + target.Trashed = source.Trashed; + target.Id = source.Id; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + target.ThumbnailFilePath = target.ThumbnailIsClass + ? string.Empty + : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); + target.UpdateDate = source.UpdateDate; + } + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.MemberType); + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IContentType source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.DocumentType); + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IMediaType source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.MediaType); + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IMemberType source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.MemberType); + + // no MapAll - take care + private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase( + source, + target, + context); + + // sync templates + IEnumerable destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias); + + // if the dest is set and it's the same as the source, then don't change + if (source.AllowedTemplates is not null && + destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) + { + IEnumerable? templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray()); + target.AllowedTemplates = source.AllowedTemplates + .Select(x => + { + ITemplate? template = templates?.SingleOrDefault(t => t.Alias == x); + return template != null + ? context.Map(template) + : null; + }) + .WhereNotNull() + .ToArray(); + } + + if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) + { + // if the dest is set and it's the same as the source, then don't change + if (target.DefaultTemplate == null || source.DefaultTemplate != target.DefaultTemplate.Alias) + { + ITemplate? template = _fileService.GetTemplate(source.DefaultTemplate); + target.DefaultTemplate = template == null ? null : context.Map(template); + } + } + else + { + target.DefaultTemplate = null; + } + } + + // no MapAll - take care + private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase( + source, + target, + context); + + // no MapAll - take care + private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase( + source, target, context); + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes + private static void Map(PropertyGroupBasic source, PropertyGroup target, + MapperContext context) + { + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + } + + // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames + private static void Map( + PropertyGroupBasic source, + PropertyGroupDisplay target, + MapperContext context) + { + target.Inherited = source.Inherited; + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + target.Properties = context.MapEnumerable(source.Properties) + .WhereNotNull(); + } + + // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames + private static void Map( + PropertyGroupBasic source, + PropertyGroupDisplay target, + MapperContext context) + { + target.Inherited = source.Inherited; + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + target.Properties = + context.MapEnumerable(source.Properties).WhereNotNull(); + } + + // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName + private static void Map(PropertyTypeBasic source, PropertyTypeDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; + target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; + target.Description = source.Description; + target.GroupId = source.GroupId; + target.Id = source.Id; + target.Inherited = source.Inherited; + target.Label = source.Label; + target.SortOrder = source.SortOrder; + target.Validation = source.Validation; + target.LabelOnTop = source.LabelOnTop; + } + + // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName + private static void Map(MemberPropertyTypeBasic source, MemberPropertyTypeDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; + target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; + target.Description = source.Description; + target.GroupId = source.GroupId; + target.Id = source.Id; + target.Inherited = source.Inherited; + target.IsSensitiveData = source.IsSensitiveData; + target.Label = source.Label; + target.MemberCanEditProperty = source.MemberCanEditProperty; + target.MemberCanViewProperty = source.MemberCanViewProperty; + target.SortOrder = source.SortOrder; + target.Validation = source.Validation; + target.LabelOnTop = source.LabelOnTop; + } + + // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) + private static void MapSaveToTypeBase( + TSource source, + IContentTypeComposition target, + MapperContext context) + where TSource : ContentTypeSave + where TSourcePropertyType : PropertyTypeBasic + { + // TODO: not so clean really + var isPublishing = target is IContentType; + + var id = Convert.ToInt32(source.Id); + if (id > 0) + { + target.Id = id; + } + + target.Alias = source.Alias; + target.Description = source.Description; + target.Icon = source.Icon; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + + target.AllowedAsRoot = source.AllowAsRoot; + + var allowedContentTypesUnchanged = target.AllowedContentTypes?.Select(x => x.Id.Value) + .SequenceEqual(source.AllowedContentTypes) ?? false; + + if (allowedContentTypesUnchanged is false) + { + target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); + } + + if (!(target is IMemberType)) + { + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); + } + + // handle property groups and property types + // note that ContentTypeSave has + // - all groups, inherited and local; only *one* occurrence per group *name* + // - potentially including the generic properties group + // - all properties, inherited and local + // + // also, see PropertyTypeGroupResolver.ResolveCore: + // - if a group is local *and* inherited, then Inherited is true + // and the identifier is the identifier of the *local* group + // + // IContentTypeComposition AddPropertyGroup, AddPropertyType methods do some + // unique-alias-checking, etc that is *not* compatible with re-mapping everything + // the way we do it here, so we should exclusively do it by + // - managing a property group's PropertyTypes collection + // - managing the content type's PropertyTypes collection (for generic properties) + + // handle actual groups (non-generic-properties) + PropertyGroup[] destOrigGroups = target.PropertyGroups.ToArray(); // local groups + IPropertyType[] destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not + var destGroups = new List(); + PropertyGroupBasic[] sourceGroups = + source.Groups.Where(x => x.IsGenericProperties == false).ToArray(); + var sourceGroupParentAliases = sourceGroups.Select(x => x.GetParentAlias()).Distinct().ToArray(); + foreach (PropertyGroupBasic sourceGroup in sourceGroups) + { + // get the dest group + PropertyGroup? destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context); + + // handle local properties + IPropertyType[] destProperties = sourceGroup.Properties + .Where(x => x.Inherited == false) + .Select(x => MapSaveProperty(x, destOrigProperties, context)) .WhereNotNull() .ToArray(); - target.SetDefaultTemplate(source.DefaultTemplate == null - ? null - : _fileService.GetTemplate(source.DefaultTemplate)); - } - - private static void MapHistoryCleanup(DocumentTypeSave source, IContentType target) - { - // If source history cleanup is null we don't have to map all properties - if (source.HistoryCleanup is null) + // if the group has no local properties and is not used as parent, skip it, ie sort-of garbage-collect + // local groups which would not have local properties anymore + if (destProperties.Length == 0 && !sourceGroupParentAliases.Contains(sourceGroup.Alias)) { - target.HistoryCleanup = null; - return; + continue; } - // We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties - target.HistoryCleanup!.ResetDirtyProperties(false); - if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup) + // ensure no duplicate alias, then assign the group properties collection + EnsureUniqueAliases(destProperties); + + if (destGroup is not null) { - target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup; - } - - if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays) - { - target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays; - } - - if (target.HistoryCleanup.KeepLatestVersionPerDayForDays != - source.HistoryCleanup.KeepLatestVersionPerDayForDays) - { - target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays; - } - } - - // no MapAll - take care - private void Map(MediaTypeSave source, IMediaType target, MapperContext context) - { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _mediaTypeService.Get(alias)); - } - - // no MapAll - take care - private void Map(MemberTypeSave source, IMemberType target, MapperContext context) - { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _memberTypeService.Get(alias)); - - foreach (MemberPropertyTypeBasic propertyType in source.Groups.SelectMany(x => x.Properties)) - { - MemberPropertyTypeBasic localCopy = propertyType; - IPropertyType? destProp = - target.PropertyTypes.SingleOrDefault(x => x.Alias?.InvariantEquals(localCopy.Alias) ?? false); - if (destProp == null) + if (destGroup.PropertyTypes?.SupportsPublishing != isPublishing || + destGroup.PropertyTypes.SequenceEqual(destProperties) is false) { - continue; + destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); } - target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); - target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); - target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); + destGroups.Add(destGroup); } } - // no MapAll - take care - private void Map(IContentType source, DocumentTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target); + // ensure no duplicate name, then assign the groups collection + EnsureUniqueAliases(destGroups); - if (source is IContentType sourceWithHistoryCleanup) + if (target.PropertyGroups.SequenceEqual(destGroups) is false) + { + target.PropertyGroups = new PropertyGroupCollection(destGroups); + } + + // because the property groups collection was rebuilt, there is no need to remove + // the old groups - they are just gone and will be cleared by the repository + + // handle non-grouped (ie generic) properties + PropertyGroupBasic? genericPropertiesGroup = + source.Groups.FirstOrDefault(x => x.IsGenericProperties); + if (genericPropertiesGroup != null) + { + // handle local properties + IPropertyType[] destProperties = genericPropertiesGroup.Properties + .Where(x => x.Inherited == false) + .Select(x => MapSaveProperty(x, destOrigProperties, context)) + .WhereNotNull() + .ToArray(); + + // ensure no duplicate alias, then assign the generic properties collection + EnsureUniqueAliases(destProperties); + target.NoGroupPropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); + } + + // because all property collections were rebuilt, there is no need to remove + // some old properties, they are just gone and will be cleared by the repository + } + + // Umbraco.Code.MapAll -Blueprints -Errors -ListViewEditorName -Trashed + private void MapTypeToDisplayBase(IContentTypeComposition source, ContentTypeCompositionDisplay target) + { + target.Alias = source.Alias; + target.AllowAsRoot = source.AllowedAsRoot; + target.CreateDate = source.CreateDate; + target.Description = source.Description; + target.Icon = source.Icon; + target.IconFilePath = target.IconIsClass + ? string.Empty + : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; + target.Id = source.Id; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + target.ThumbnailFilePath = target.ThumbnailIsClass + ? string.Empty + : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); + target.Udi = MapContentTypeUdi(source); + target.UpdateDate = source.UpdateDate; + + target.AllowedContentTypes = source.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); + target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); + target.LockedCompositeContentTypes = MapLockedCompositions(source); + } + + // no MapAll - relies on the non-generic method + private void MapTypeToDisplayBase(IContentTypeComposition source, TTarget target) + where TTarget : ContentTypeCompositionDisplay + where TTargetPropertyType : PropertyTypeDisplay, new() + { + MapTypeToDisplayBase(source, target); + + var groupsMapper = new PropertyTypeGroupMapper( + _propertyEditors, + _dataTypeService, + _shortStringHelper, + _loggerFactory.CreateLogger>()); + target.Groups = groupsMapper.Map(source); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -ListViewEditorName -Errors -LockedCompositeContentTypes + private void MapTypeToDisplayBase(ContentTypeSave source, ContentTypeCompositionDisplay target) + { + target.Alias = source.Alias; + target.AllowAsRoot = source.AllowAsRoot; + target.AllowedContentTypes = source.AllowedContentTypes; + target.Blueprints = source.Blueprints; + target.CompositeContentTypes = source.CompositeContentTypes; + target.Description = source.Description; + target.Icon = source.Icon; + target.IconFilePath = target.IconIsClass + ? string.Empty + : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; + target.Id = source.Id; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + target.ThumbnailFilePath = target.ThumbnailIsClass + ? string.Empty + : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); + target.Trashed = source.Trashed; + target.Udi = source.Udi; + } + + // no MapAll - relies on the non-generic method + private void MapTypeToDisplayBase( + TSource source, + TTarget target, + MapperContext context) + where TSource : ContentTypeSave + where TSourcePropertyType : PropertyTypeBasic + where TTarget : ContentTypeCompositionDisplay + where TTargetPropertyType : PropertyTypeDisplay + { + MapTypeToDisplayBase(source, target); + + target.Groups = + context + .MapEnumerable, PropertyGroupDisplay>( + source.Groups).WhereNotNull(); + } + + private IEnumerable MapLockedCompositions(IContentTypeComposition source) + { + // get ancestor ids from path of parent if not root + if (source.ParentId == Constants.System.Root) + { + return Enumerable.Empty(); + } + + IContentType? parent = _contentTypeService.Get(source.ParentId); + if (parent == null) + { + return Enumerable.Empty(); + } + + var aliases = new List(); + IEnumerable? ancestorIds = parent.Path?.Split(Constants.CharArrays.Comma) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); + + // loop through all content types and return ordered aliases of ancestors + IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray(); + if (ancestorIds is not null) + { + foreach (var ancestorId in ancestorIds) { - target.HistoryCleanup = new HistoryCleanupViewModel + IContentType? ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor is not null && ancestor.Alias is not null) { - PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, - KeepAllVersionsNewerThanDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, - KeepLatestVersionPerDayForDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays, - GlobalKeepAllVersionsNewerThanDays = _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays, - GlobalKeepLatestVersionPerDayForDays = _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays, - GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup - }; - } - - target.AllowCultureVariant = source.VariesByCulture(); - target.AllowSegmentVariant = source.VariesBySegment(); - target.ContentApps = _commonMapper.GetContentAppsForEntity(source); - - //sync templates - if (source.AllowedTemplates is not null) - { - target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates).WhereNotNull(); - } - - if (source.DefaultTemplate != null) - { - target.DefaultTemplate = context.Map(source.DefaultTemplate); - } - - //default listview - target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; - - if (string.IsNullOrEmpty(source.Alias)) - { - return; - } - - var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; - if (_dataTypeService.GetDataType(name) != null) - { - target.ListViewEditorName = name; - } - } - - // no MapAll - take care - private void Map(IMediaType source, MediaTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target); - - //default listview - target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; - target.IsSystemMediaType = source.IsSystemMediaType(); - - if (string.IsNullOrEmpty(source.Name)) - { - return; - } - - var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; - if (_dataTypeService.GetDataType(name) != null) - { - target.ListViewEditorName = name; - } - } - - // no MapAll - take care - private void Map(IMemberType source, MemberTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target); - - //map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData - foreach (IPropertyType propertyType in source.PropertyTypes) - { - IPropertyType localCopy = propertyType; - MemberPropertyTypeDisplay? displayProp = target.Groups.SelectMany(dest => dest.Properties) - .SingleOrDefault(dest => dest.Alias?.InvariantEquals(localCopy.Alias) ?? false); - if (displayProp == null) - { - continue; - } - - displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); - displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); - displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); - } - } - - // Umbraco.Code.MapAll -Blueprints - private void Map(IContentTypeBase source, ContentTypeBasic target, string entityType) - { - target.Udi = Udi.Create(entityType, source.Key); - target.Alias = source.Alias; - target.CreateDate = source.CreateDate; - target.Description = source.Description; - target.Icon = source.Icon; - target.IconFilePath = target.IconIsClass - ? string.Empty - : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; - - target.Trashed = source.Trashed; - target.Id = source.Id; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - target.ThumbnailFilePath = target.ThumbnailIsClass - ? string.Empty - : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); - target.UpdateDate = source.UpdateDate; - } - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.MemberType); - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IContentType source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.DocumentType); - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IMediaType source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.MediaType); - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IMemberType source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.MemberType); - - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations - private static void Map(PropertyTypeBasic source, IPropertyType target, MapperContext context) - { - target.Name = source.Label; - target.DataTypeId = source.DataTypeId; - target.DataTypeKey = source.DataTypeKey; - target.Mandatory = source.Validation?.Mandatory ?? false; - target.MandatoryMessage = source.Validation?.MandatoryMessage; - target.ValidationRegExp = source.Validation?.Pattern; - target.ValidationRegExpMessage = source.Validation?.PatternMessage; - target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); - target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); - - if (source.Id > 0) - { - target.Id = source.Id; - } - - if (source.GroupId > 0) - { - if (target.PropertyGroupId?.Value != source.GroupId) - { - target.PropertyGroupId = new Lazy(() => source.GroupId, false); + aliases.Add(ancestor.Alias); } } - - target.Alias = source.Alias; - target.Description = source.Description; - target.SortOrder = source.SortOrder; - target.LabelOnTop = source.LabelOnTop; } - // no MapAll - take care - private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context) + return aliases.OrderBy(x => x); + } + + private static PropertyGroup? MapSaveGroup( + PropertyGroupBasic sourceGroup, + IEnumerable destOrigGroups, + MapperContext context) + where TPropertyType : PropertyTypeBasic + { + PropertyGroup? destGroup; + if (sourceGroup.Id > 0) { - MapTypeToDisplayBase(source, - target, context); - - //sync templates - IEnumerable destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias); - //if the dest is set and it's the same as the source, then don't change - if (source.AllowedTemplates is not null && destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) + // update an existing group + // ensure it is still there, then map/update + destGroup = destOrigGroups.FirstOrDefault(x => x.Id == sourceGroup.Id); + if (destGroup != null) { - IEnumerable? templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray()); - target.AllowedTemplates = source.AllowedTemplates - .Select(x => - { - ITemplate? template = templates?.SingleOrDefault(t => t.Alias == x); - return template != null - ? context.Map(template) - : null; - }) - .WhereNotNull() - .ToArray(); + context.Map(sourceGroup, destGroup); + return destGroup; } - if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) - { - //if the dest is set and it's the same as the source, then don't change - if (target.DefaultTemplate == null || source.DefaultTemplate != target.DefaultTemplate.Alias) - { - ITemplate? template = _fileService.GetTemplate(source.DefaultTemplate); - target.DefaultTemplate = template == null ? null : context.Map(template); - } - } - else - { - target.DefaultTemplate = null; - } + // force-clear the ID as it does not match anything + sourceGroup.Id = 0; } - // no MapAll - take care - private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) => - MapTypeToDisplayBase(source, - target, context); + // insert a new group, or update an existing group that has + // been deleted in the meantime and we need to re-create + // map/create + destGroup = context.Map(sourceGroup); + return destGroup; + } - // no MapAll - take care - private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) => - MapTypeToDisplayBase( - source, target, context); - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, - MapperContext context) + private static IPropertyType? MapSaveProperty( + PropertyTypeBasic sourceProperty, + IEnumerable destOrigProperties, + MapperContext context) + { + IPropertyType? destProperty; + if (sourceProperty.Id > 0) { - if (source.Id > 0) + // updating an existing property + // ensure it is still there, then map/update + destProperty = destOrigProperties.FirstOrDefault(x => x.Id == sourceProperty.Id); + if (destProperty != null) { - target.Id = source.Id; + context.Map(sourceProperty, destProperty); + return destProperty; } - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; + // force-clear the ID as it does not match anything + sourceProperty.Id = 0; } - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, - MapperContext context) - { - if (source.Id > 0) - { - target.Id = source.Id; - } + // insert a new property, or update an existing property that has + // been deleted in the meantime and we need to re-create + // map/create + destProperty = context.Map(sourceProperty); + return destProperty; + } - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; + private static void EnsureUniqueAliases(IEnumerable properties) + { + IPropertyType[] propertiesA = properties.ToArray(); + var distinctProperties = propertiesA + .Select(x => x.Alias?.ToUpperInvariant()) + .Distinct() + .Count(); + if (distinctProperties != propertiesA.Length) + { + throw new InvalidOperationException("Cannot map properties due to alias conflict."); + } + } + + private static void EnsureUniqueAliases(IEnumerable groups) + { + PropertyGroup[] groupsA = groups.ToArray(); + var distinctProperties = groupsA + .Select(x => x.Alias) + .Distinct() + .Count(); + if (distinctProperties != groupsA.Length) + { + throw new InvalidOperationException("Cannot map groups due to alias conflict."); + } + } + + private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, + Func getContentType) + { + var current = target.CompositionAliases().ToArray(); + IEnumerable proposed = source.CompositeContentTypes; + + IEnumerable remove = current.Where(x => !proposed.Contains(x)); + IEnumerable add = proposed.Where(x => !current.Contains(x)); + + foreach (var alias in remove) + { + target.RemoveContentType(alias); } - // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames - private static void Map(PropertyGroupBasic source, - PropertyGroupDisplay target, MapperContext context) + foreach (var alias in add) { - target.Inherited = source.Inherited; - if (source.Id > 0) + // TODO: Remove N+1 lookup + IContentTypeComposition? contentType = getContentType(alias); + if (contentType != null) { - target.Id = source.Id; - } - - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; - target.Properties = context.MapEnumerable(source.Properties).WhereNotNull(); - } - - // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames - private static void Map(PropertyGroupBasic source, - PropertyGroupDisplay target, MapperContext context) - { - target.Inherited = source.Inherited; - if (source.Id > 0) - { - target.Id = source.Id; - } - - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; - target.Properties = - context.MapEnumerable(source.Properties).WhereNotNull(); - } - - // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName - private static void Map(PropertyTypeBasic source, PropertyTypeDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.AllowCultureVariant = source.AllowCultureVariant; - target.AllowSegmentVariant = source.AllowSegmentVariant; - target.DataTypeId = source.DataTypeId; - target.DataTypeKey = source.DataTypeKey; - target.Description = source.Description; - target.GroupId = source.GroupId; - target.Id = source.Id; - target.Inherited = source.Inherited; - target.Label = source.Label; - target.SortOrder = source.SortOrder; - target.Validation = source.Validation; - target.LabelOnTop = source.LabelOnTop; - } - - // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName - private static void Map(MemberPropertyTypeBasic source, MemberPropertyTypeDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.AllowCultureVariant = source.AllowCultureVariant; - target.AllowSegmentVariant = source.AllowSegmentVariant; - target.DataTypeId = source.DataTypeId; - target.DataTypeKey = source.DataTypeKey; - target.Description = source.Description; - target.GroupId = source.GroupId; - target.Id = source.Id; - target.Inherited = source.Inherited; - target.IsSensitiveData = source.IsSensitiveData; - target.Label = source.Label; - target.MemberCanEditProperty = source.MemberCanEditProperty; - target.MemberCanViewProperty = source.MemberCanViewProperty; - target.SortOrder = source.SortOrder; - target.Validation = source.Validation; - target.LabelOnTop = source.LabelOnTop; - } - - // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) - private static void MapSaveToTypeBase(TSource source, - IContentTypeComposition target, MapperContext context) - where TSource : ContentTypeSave - where TSourcePropertyType : PropertyTypeBasic - { - // TODO: not so clean really - var isPublishing = target is IContentType; - - var id = Convert.ToInt32(source.Id); - if (id > 0) - { - target.Id = id; - } - - target.Alias = source.Alias; - target.Description = source.Description; - target.Icon = source.Icon; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - - target.AllowedAsRoot = source.AllowAsRoot; - - bool allowedContentTypesUnchanged = target.AllowedContentTypes?.Select(x => x.Id.Value) - .SequenceEqual(source.AllowedContentTypes) ?? false; - - if (allowedContentTypesUnchanged is false) - { - target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); - } - - - if (!(target is IMemberType)) - { - target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); - target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); - } - - // handle property groups and property types - // note that ContentTypeSave has - // - all groups, inherited and local; only *one* occurrence per group *name* - // - potentially including the generic properties group - // - all properties, inherited and local - // - // also, see PropertyTypeGroupResolver.ResolveCore: - // - if a group is local *and* inherited, then Inherited is true - // and the identifier is the identifier of the *local* group - // - // IContentTypeComposition AddPropertyGroup, AddPropertyType methods do some - // unique-alias-checking, etc that is *not* compatible with re-mapping everything - // the way we do it here, so we should exclusively do it by - // - managing a property group's PropertyTypes collection - // - managing the content type's PropertyTypes collection (for generic properties) - - // handle actual groups (non-generic-properties) - PropertyGroup[] destOrigGroups = target.PropertyGroups.ToArray(); // local groups - IPropertyType[] destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not - var destGroups = new List(); - PropertyGroupBasic[] sourceGroups = - source.Groups.Where(x => x.IsGenericProperties == false).ToArray(); - var sourceGroupParentAliases = sourceGroups.Select(x => x.GetParentAlias()).Distinct().ToArray(); - foreach (PropertyGroupBasic sourceGroup in sourceGroups) - { - // get the dest group - PropertyGroup? destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context); - - // handle local properties - IPropertyType[] destProperties = sourceGroup.Properties - .Where(x => x.Inherited == false) - .Select(x => MapSaveProperty(x, destOrigProperties, context)) - .WhereNotNull() - .ToArray(); - - // if the group has no local properties and is not used as parent, skip it, ie sort-of garbage-collect - // local groups which would not have local properties anymore - if (destProperties.Length == 0 && !sourceGroupParentAliases.Contains(sourceGroup.Alias)) - { - continue; - } - - // ensure no duplicate alias, then assign the group properties collection - EnsureUniqueAliases(destProperties); - - if (destGroup is not null) - { - if (destGroup.PropertyTypes?.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false) - { - destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); - } - - destGroups.Add(destGroup); - } - } - - // ensure no duplicate name, then assign the groups collection - EnsureUniqueAliases(destGroups); - - if (target.PropertyGroups.SequenceEqual(destGroups) is false) - { - target.PropertyGroups = new PropertyGroupCollection(destGroups); - } - - // because the property groups collection was rebuilt, there is no need to remove - // the old groups - they are just gone and will be cleared by the repository - - // handle non-grouped (ie generic) properties - PropertyGroupBasic? genericPropertiesGroup = - source.Groups.FirstOrDefault(x => x.IsGenericProperties); - if (genericPropertiesGroup != null) - { - // handle local properties - IPropertyType[] destProperties = genericPropertiesGroup.Properties - .Where(x => x.Inherited == false) - .Select(x => MapSaveProperty(x, destOrigProperties, context)) - .WhereNotNull() - .ToArray(); - - // ensure no duplicate alias, then assign the generic properties collection - EnsureUniqueAliases(destProperties); - target.NoGroupPropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); - } - - // because all property collections were rebuilt, there is no need to remove - // some old properties, they are just gone and will be cleared by the repository - } - - // Umbraco.Code.MapAll -Blueprints -Errors -ListViewEditorName -Trashed - private void MapTypeToDisplayBase(IContentTypeComposition source, ContentTypeCompositionDisplay target) - { - target.Alias = source.Alias; - target.AllowAsRoot = source.AllowedAsRoot; - target.CreateDate = source.CreateDate; - target.Description = source.Description; - target.Icon = source.Icon; - target.IconFilePath = target.IconIsClass - ? string.Empty - : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; - target.Id = source.Id; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - target.ThumbnailFilePath = target.ThumbnailIsClass - ? string.Empty - : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); - target.Udi = MapContentTypeUdi(source); - target.UpdateDate = source.UpdateDate; - - target.AllowedContentTypes = source.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); - target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); - target.LockedCompositeContentTypes = MapLockedCompositions(source); - } - - // no MapAll - relies on the non-generic method - private void MapTypeToDisplayBase(IContentTypeComposition source, TTarget target) - where TTarget : ContentTypeCompositionDisplay - where TTargetPropertyType : PropertyTypeDisplay, new() - { - MapTypeToDisplayBase(source, target); - - var groupsMapper = new PropertyTypeGroupMapper(_propertyEditors, _dataTypeService, - _shortStringHelper, _loggerFactory.CreateLogger>()); - target.Groups = groupsMapper.Map(source); - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -ListViewEditorName -Errors -LockedCompositeContentTypes - private void MapTypeToDisplayBase(ContentTypeSave source, ContentTypeCompositionDisplay target) - { - target.Alias = source.Alias; - target.AllowAsRoot = source.AllowAsRoot; - target.AllowedContentTypes = source.AllowedContentTypes; - target.Blueprints = source.Blueprints; - target.CompositeContentTypes = source.CompositeContentTypes; - target.Description = source.Description; - target.Icon = source.Icon; - target.IconFilePath = target.IconIsClass - ? string.Empty - : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; - target.Id = source.Id; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - target.ThumbnailFilePath = target.ThumbnailIsClass - ? string.Empty - : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); - target.Trashed = source.Trashed; - target.Udi = source.Udi; - } - - // no MapAll - relies on the non-generic method - private void MapTypeToDisplayBase(TSource source, - TTarget target, MapperContext context) - where TSource : ContentTypeSave - where TSourcePropertyType : PropertyTypeBasic - where TTarget : ContentTypeCompositionDisplay - where TTargetPropertyType : PropertyTypeDisplay - { - MapTypeToDisplayBase(source, target); - - target.Groups = - context - .MapEnumerable, PropertyGroupDisplay>( - source.Groups).WhereNotNull(); - } - - private IEnumerable MapLockedCompositions(IContentTypeComposition source) - { - // get ancestor ids from path of parent if not root - if (source.ParentId == Constants.System.Root) - { - return Enumerable.Empty(); - } - - IContentType? parent = _contentTypeService.Get(source.ParentId); - if (parent == null) - { - return Enumerable.Empty(); - } - - var aliases = new List(); - IEnumerable? ancestorIds = parent.Path?.Split(Constants.CharArrays.Comma) - .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); - // loop through all content types and return ordered aliases of ancestors - IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray(); - if (ancestorIds is not null) - { - foreach (var ancestorId in ancestorIds) - { - IContentType? ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); - if (ancestor is not null && ancestor.Alias is not null) - { - aliases.Add(ancestor.Alias); - } - } - } - - - return aliases.OrderBy(x => x); - } - - public static Udi? MapContentTypeUdi(IContentTypeComposition source) - { - if (source == null) - { - return null; - } - - string udiType; - switch (source) - { - case IMemberType _: - udiType = Constants.UdiEntityType.MemberType; - break; - case IMediaType _: - udiType = Constants.UdiEntityType.MediaType; - break; - case IContentType _: - udiType = Constants.UdiEntityType.DocumentType; - break; - default: - throw new PanicException($"Source is of type {source.GetType()} which isn't supported here"); - } - - return Udi.Create(udiType, source.Key); - } - - private static PropertyGroup? MapSaveGroup(PropertyGroupBasic sourceGroup, - IEnumerable destOrigGroups, MapperContext context) - where TPropertyType : PropertyTypeBasic - { - PropertyGroup? destGroup; - if (sourceGroup.Id > 0) - { - // update an existing group - // ensure it is still there, then map/update - destGroup = destOrigGroups.FirstOrDefault(x => x.Id == sourceGroup.Id); - if (destGroup != null) - { - context.Map(sourceGroup, destGroup); - return destGroup; - } - - // force-clear the ID as it does not match anything - sourceGroup.Id = 0; - } - - // insert a new group, or update an existing group that has - // been deleted in the meantime and we need to re-create - // map/create - destGroup = context.Map(sourceGroup); - return destGroup; - } - - private static IPropertyType? MapSaveProperty(PropertyTypeBasic sourceProperty, - IEnumerable destOrigProperties, MapperContext context) - { - IPropertyType? destProperty; - if (sourceProperty.Id > 0) - { - // updating an existing property - // ensure it is still there, then map/update - destProperty = destOrigProperties.FirstOrDefault(x => x.Id == sourceProperty.Id); - if (destProperty != null) - { - context.Map(sourceProperty, destProperty); - return destProperty; - } - - // force-clear the ID as it does not match anything - sourceProperty.Id = 0; - } - - // insert a new property, or update an existing property that has - // been deleted in the meantime and we need to re-create - // map/create - destProperty = context.Map(sourceProperty); - return destProperty; - } - - private static void EnsureUniqueAliases(IEnumerable properties) - { - IPropertyType[] propertiesA = properties.ToArray(); - var distinctProperties = propertiesA - .Select(x => x.Alias?.ToUpperInvariant()) - .Distinct() - .Count(); - if (distinctProperties != propertiesA.Length) - { - throw new InvalidOperationException("Cannot map properties due to alias conflict."); - } - } - - private static void EnsureUniqueAliases(IEnumerable groups) - { - PropertyGroup[] groupsA = groups.ToArray(); - var distinctProperties = groupsA - .Select(x => x.Alias) - .Distinct() - .Count(); - if (distinctProperties != groupsA.Length) - { - throw new InvalidOperationException("Cannot map groups due to alias conflict."); - } - } - - private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, - Func getContentType) - { - var current = target.CompositionAliases().ToArray(); - IEnumerable proposed = source.CompositeContentTypes; - - IEnumerable remove = current.Where(x => !proposed.Contains(x)); - IEnumerable add = proposed.Where(x => !current.Contains(x)); - - foreach (var alias in remove) - { - target.RemoveContentType(alias); - } - - foreach (var alias in add) - { - // TODO: Remove N+1 lookup - IContentTypeComposition? contentType = getContentType(alias); - if (contentType != null) - { - target.AddContentType(contentType); - } + target.AddContentType(contentType); } } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs index 2f330b581f..5441320b0f 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs @@ -1,178 +1,305 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class ContentVariantMapper { - public class ContentVariantMapper + private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService _localizedTextService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IContentService _contentService; + private readonly IUserService _userService; + private SecuritySettings _securitySettings; + + public ContentVariantMapper( + ILocalizationService localizationService, + ILocalizedTextService localizedTextService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentService contentService, + IUserService userService, + IOptionsMonitor securitySettings) { - private readonly ILocalizationService _localizationService; - private readonly ILocalizedTextService _localizedTextService; + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _contentService = contentService; + _userService = userService; + _securitySettings = securitySettings.CurrentValue; + securitySettings.OnChange(settings => _securitySettings = settings); + } - public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) + public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) + : this( + localizationService, + localizedTextService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public IEnumerable Map(IContent source, MapperContext context) + where TVariant : ContentVariantDisplay + { + var variesByCulture = source.ContentType.VariesByCulture(); + var variesBySegment = source.ContentType.VariesBySegment(); + + List variants = new(); + + if (!variesByCulture && !variesBySegment) { - _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + // this is invariant so just map the IContent instance to ContentVariationDisplay + TVariant? variantDisplay = context.Map(source); + if (variantDisplay is not null) + { + // Map allowed actions per language + variantDisplay.AllowedActions = GetLanguagePermissions(source, context, variantDisplay); + variants.Add(variantDisplay); + } } - - public IEnumerable Map(IContent source, MapperContext context) where TVariant : ContentVariantDisplay + else if (variesByCulture && !variesBySegment) { - var variesByCulture = source.ContentType.VariesByCulture(); - var variesBySegment = source.ContentType.VariesBySegment(); - - List variants = new (); - - if (!variesByCulture && !variesBySegment) - { - // this is invariant so just map the IContent instance to ContentVariationDisplay - var variantDisplay = context.Map(source); - if (variantDisplay is not null) - { - variants.Add(variantDisplay); - } - } - else if (variesByCulture && !variesBySegment) - { - var languages = GetLanguages(context); - variants = languages - .Select(language => CreateVariantDisplay(context, source, language, null)) - .WhereNotNull() - .ToList(); - } - else if (variesBySegment && !variesByCulture) - { - // Segment only - var segments = GetSegments(source); - variants = segments - .Select(segment => CreateVariantDisplay(context, source, null, segment)) - .WhereNotNull() - .ToList(); - } - else - { - // Culture and segment - var languages = GetLanguages(context).ToList(); - var segments = GetSegments(source).ToList(); - - if (languages.Count == 0 || segments.Count == 0) - { - // This should not happen - throw new InvalidOperationException("No languages or segments available"); - } - - variants = languages - .SelectMany(language => segments - .Select(segment => CreateVariantDisplay(context, source, language, segment))) - .WhereNotNull() - .ToList(); - } - - return SortVariants(variants); + IEnumerable languages = GetLanguages(context); + variants = languages + .Select(language => CreateVariantDisplay(context, source, language, null)) + .WhereNotNull() + .ToList(); } - - private IList SortVariants(IList variants) where TVariant : ContentVariantDisplay + else if (variesBySegment && !variesByCulture) { - if (variants.Count <= 1) + // Segment only + IEnumerable segments = GetSegments(source); + variants = segments + .Select(segment => CreateVariantDisplay(context, source, null, segment)) + .WhereNotNull() + .ToList(); + } + else + { + // Culture and segment + var languages = GetLanguages(context).ToList(); + var segments = GetSegments(source).ToList(); + + if (languages.Count == 0 || segments.Count == 0) { - return variants; + // This should not happen + throw new InvalidOperationException("No languages or segments available"); } - // Default variant first, then order by language, segment. - return variants - .OrderBy(v => IsDefaultLanguage(v) ? 0 : 1) - .ThenBy(v => IsDefaultSegment(v) ? 0 : 1) - .ThenBy(v => v?.Language?.Name) - .ThenBy(v => v.Segment) + variants = languages + .SelectMany(language => segments + .Select(segment => CreateVariantDisplay(context, source, language, segment))) + .WhereNotNull() .ToList(); } - private static bool IsDefaultSegment(ContentVariantDisplay variant) + return SortVariants(variants); + } + + private static bool IsDefaultSegment(ContentVariantDisplay variant) => variant.Segment == null; + + private IList SortVariants(IList variants) + where TVariant : ContentVariantDisplay + { + if (variants.Count <= 1) { - return variant.Segment == null; + return variants; } - private static bool IsDefaultLanguage(ContentVariantDisplay variant) + // Default variant first, then order by language, segment. + return variants + .OrderBy(v => IsDefaultLanguage(v) ? 0 : 1) + .ThenBy(v => IsDefaultSegment(v) ? 0 : 1) + .ThenBy(v => v?.Language?.Name) + .ThenBy(v => v.Segment) + .ToList(); + } + + private static bool IsDefaultLanguage(ContentVariantDisplay variant) => + variant.Language == null || variant.Language.IsDefault; + + private IEnumerable GetLanguages(MapperContext context) + { + var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); + if (allLanguages.Count == 0) { - return variant.Language == null || variant.Language.IsDefault; + // This should never happen + return Enumerable.Empty(); } - private IEnumerable GetLanguages(MapperContext context) + return context.MapEnumerable(allLanguages).WhereNotNull().ToList(); + } + + /// + /// Returns all segments assigned to the content + /// + /// + /// + /// Returns all segments assigned to the content including the default `null` segment. + /// + private IEnumerable GetSegments(IContent content) + { + // The default segment (null) is always there, + // even when there is no property data at all yet + var segments = new List {null}; + + // Add actual segments based on the property values + segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); + + // Do not return a segment more than once + return segments.Distinct(); + } + + private TVariant? CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language? language, string? segment) + where TVariant : ContentVariantDisplay + { + context.SetCulture(language?.IsoCode); + context.SetSegment(segment); + + TVariant? variantDisplay = context.Map(content); + + if (variantDisplay is null) { - var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (allLanguages.Count == 0) + return null; + } + + variantDisplay.Segment = segment; + variantDisplay.Language = language; + + // Map allowed actions + variantDisplay.AllowedActions = GetLanguagePermissions(content, context, variantDisplay); + variantDisplay.Name = content.GetCultureName(language?.IsoCode); + variantDisplay.DisplayName = GetDisplayName(language, segment); + + return variantDisplay; + } + + private string GetDisplayName(ContentEditing.Language? language, string? segment) + { + var isCultureVariant = language is not null; + var isSegmentVariant = !segment.IsNullOrWhiteSpace(); + + if (!isCultureVariant && !isSegmentVariant) + { + return _localizedTextService.Localize("general", "default"); + } + + var parts = new List(); + + if (isSegmentVariant) + { + parts.Add(segment!); + } + + if (isCultureVariant) + { + parts.Add(language?.Name!); + } + + return string.Join(" — ", parts); + } + + // This is a bit ugly, but when languages get granular permissions this will be really useful + // For now we just return the exact same permissions as you had on the node, if you have access via language + private IEnumerable GetLanguagePermissions(IContent content, MapperContext context, TVariant variantDisplay) + where TVariant : ContentVariantDisplay + { + context.Items.TryGetValue("CurrentUser", out var currentBackofficeUser); + + IUser? currentUser = null; + + if (currentBackofficeUser is IUser currentIUserBackofficeUser) + { + currentUser = currentIUserBackofficeUser; + } + else if(_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser is not null) + { + currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + } + + if (currentUser is null) + { + return Enumerable.Empty(); + } + + IEnumerable userGroups = currentUser.Groups; + + // Map allowed actions + var hasAccess = false; + foreach (IReadOnlyUserGroup group in userGroups) + { + // Handle invariant + if (variantDisplay.Language is null) { - // This should never happen - return Enumerable.Empty(); + var defaultLanguageId = _localizationService.GetDefaultLanguageId(); + if (_securitySettings.AllowEditInvariantFromNonDefault || (defaultLanguageId.HasValue && group.HasAccessToLanguage(defaultLanguageId.Value))) + { + hasAccess = true; + } } - else + + if (variantDisplay.Language is not null && group.HasAccessToLanguage(variantDisplay.Language.Id)) { - return context.MapEnumerable(allLanguages).WhereNotNull().ToList(); + hasAccess = true; + break; } } - /// - /// Returns all segments assigned to the content - /// - /// - /// - /// Returns all segments assigned to the content including the default `null` segment. - /// - private IEnumerable GetSegments(IContent content) + // If user does not have access, return only browse permission + if (!hasAccess) { - // The default segment (null) is always there, - // even when there is no property data at all yet - var segments = new List { null }; - - // Add actual segments based on the property values - segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); - - // Do not return a segment more than once - return segments.Distinct(); + return new[] { ActionBrowse.ActionLetter.ToString() }; } - private TVariant? CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language? language, string? segment) where TVariant : ContentVariantDisplay + IContent? parent; + if (context.Items.TryGetValue("Parent", out var parentObj) && + parentObj is IContent typedParent) { - context.SetCulture(language?.IsoCode); - context.SetSegment(segment); + parent = typedParent; + } + else + { + parent = _contentService.GetParent(content); + } - var variantDisplay = context.Map(content); + string path; + if (content.HasIdentity) + { + path = content.Path; + } + else + { + path = parent == null ? "-1" : parent.Path; + } - if (variantDisplay is null) + // A bit of a mess, but we need to ensure that all the required values are here AND that they're the right type. + if (context.Items.TryGetValue("Permissions", out var permissionsObject) && permissionsObject is Dictionary permissionsDict) + { + // If we already have permissions for a given path, + // and the current user is the same as was used to generate the permissions, return the stored permissions. + if (_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id == currentUser.Id && + permissionsDict.TryGetValue(path, out EntityPermissionSet? permissions)) { - return null; + return permissions.GetAllPermissions(); } - variantDisplay.Segment = segment; - variantDisplay.Language = language; - variantDisplay.Name = content.GetCultureName(language?.IsoCode); - variantDisplay.DisplayName = GetDisplayName(language, segment); - - return variantDisplay; } - private string GetDisplayName(ContentEditing.Language? language, string? segment) - { - var isCultureVariant = language is not null; - var isSegmentVariant = !segment.IsNullOrWhiteSpace(); + // TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is + // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null + // reference exception :( - if(!isCultureVariant && !isSegmentVariant) - { - return _localizedTextService.Localize("general", "default"); - } - - var parts = new List(); - - if (isSegmentVariant) - parts.Add(segment!); - - if (isCultureVariant) - parts.Add(language?.Name!); - - return string.Join(" — ", parts); - - } + return _userService.GetPermissionsForPath(currentUser, path).GetAllPermissions(); } } diff --git a/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs index f1fc81cd24..de2a773257 100644 --- a/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -10,205 +7,230 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class DataTypeMapDefinition : IMapDefinition { - public class DataTypeMapDefinition : IMapDefinition + private static readonly int[] SystemIds = { - private readonly PropertyEditorCollection _propertyEditors; - private readonly ILogger _logger; - private readonly ContentSettings _contentSettings; - private readonly IConfigurationEditorJsonSerializer _serializer; + Constants.DataTypes.DefaultContentListView, Constants.DataTypes.DefaultMediaListView, + Constants.DataTypes.DefaultMembersListView, + }; - public DataTypeMapDefinition(PropertyEditorCollection propertyEditors, ILogger logger, IOptions contentSettings, IConfigurationEditorJsonSerializer serializer) + private readonly ContentSettings _contentSettings; + private readonly ILogger _logger; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IConfigurationEditorJsonSerializer _serializer; + + public DataTypeMapDefinition(PropertyEditorCollection propertyEditors, ILogger logger, IOptions contentSettings, IConfigurationEditorJsonSerializer serializer) + { + _propertyEditors = propertyEditors; + _logger = logger; + _contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings)); + _serializer = serializer; + } + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new PropertyEditorBasic(), Map); + mapper.Define( + (source, context) => new DataTypeConfigurationFieldDisplay(), Map); + mapper.Define((source, context) => new DataTypeBasic(), Map); + mapper.Define((source, context) => new DataTypeBasic(), Map); + mapper.Define((source, context) => new DataTypeDisplay(), Map); + mapper.Define>(MapPreValues); + mapper.Define( + (source, context) => + new DataType(_propertyEditors[source.EditorAlias], _serializer) { CreateDate = DateTime.Now }, + Map); + mapper.Define>(MapPreValues); + } + + // Umbraco.Code.MapAll + private static void Map(IDataEditor source, PropertyEditorBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Name = source.Name; + } + + // Umbraco.Code.MapAll -Value + private static void Map(ConfigurationField source, DataTypeConfigurationFieldDisplay target, MapperContext context) + { + target.Config = source.Config; + target.Description = source.Description; + target.HideLabel = source.HideLabel; + target.Key = source.Key; + target.Name = source.Name; + target.View = source.View; + } + + // Umbraco.Code.MapAll -Udi -HasPrevalues -IsSystemDataType -Id -Trashed -Key + // Umbraco.Code.MapAll -ParentId -Path + private static void Map(IDataEditor source, DataTypeBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Group = source.Group; + target.Icon = source.Icon; + target.Name = source.Name; + } + + // Umbraco.Code.MapAll -HasPrevalues + private void Map(IDataType source, DataTypeBasic target, MapperContext context) + { + target.Id = source.Id; + target.IsSystemDataType = SystemIds.Contains(source.Id); + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Trashed = source.Trashed; + target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); + + if (!_propertyEditors.TryGet(source.EditorAlias, out IDataEditor? editor)) { - _propertyEditors = propertyEditors; - _logger = logger; - _contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings)); - _serializer = serializer; + return; } - private static readonly int[] SystemIds = - { - Constants.DataTypes.DefaultContentListView, - Constants.DataTypes.DefaultMediaListView, - Constants.DataTypes.DefaultMembersListView - }; + target.Alias = editor.Alias; + target.Group = editor.Group; + target.Icon = editor.Icon; + } - public void DefineMaps(IUmbracoMapper mapper) + // Umbraco.Code.MapAll -HasPrevalues + private void Map(IDataType source, DataTypeDisplay target, MapperContext context) + { + target.AvailableEditors = MapAvailableEditors(source, context); + target.Id = source.Id; + target.IsSystemDataType = SystemIds.Contains(source.Id); + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.PreValues = MapPreValues(source, context); + target.SelectedEditor = source.EditorAlias.IsNullOrWhiteSpace() ? null : source.EditorAlias; + target.Trashed = source.Trashed; + target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); + + if (!_propertyEditors.TryGet(source.EditorAlias, out IDataEditor? editor)) { - mapper.Define((source, context) => new PropertyEditorBasic(), Map); - mapper.Define((source, context) => new DataTypeConfigurationFieldDisplay(), Map); - mapper.Define((source, context) => new DataTypeBasic(), Map); - mapper.Define((source, context) => new DataTypeBasic(), Map); - mapper.Define((source, context) => new DataTypeDisplay(), Map); - mapper.Define>(MapPreValues); - mapper.Define((source, context) => new DataType(_propertyEditors[source.EditorAlias], _serializer) { CreateDate = DateTime.Now }, Map); - mapper.Define>(MapPreValues); + return; } - // Umbraco.Code.MapAll - private static void Map(IDataEditor source, PropertyEditorBasic target, MapperContext context) + target.Alias = editor.Alias; + target.Group = editor.Group; + target.Icon = editor.Icon; + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -Key -Path -CreatorId -Level -SortOrder -Configuration + private void Map(DataTypeSave source, IDataType target, MapperContext context) + { + target.DatabaseType = MapDatabaseType(source); + target.Editor = _propertyEditors[source.EditorAlias]; + target.Id = Convert.ToInt32(source.Id); + target.Name = source.Name; + target.ParentId = source.ParentId; + } + + private IEnumerable MapAvailableEditors(IDataType source, MapperContext context) + { + IOrderedEnumerable properties = _propertyEditors + .Where(x => !x.IsDeprecated || _contentSettings.ShowDeprecatedPropertyEditors || + source.EditorAlias == x.Alias) + .OrderBy(x => x.Name); + return context.MapEnumerable(properties).WhereNotNull(); + } + + private IEnumerable MapPreValues(IDataType dataType, MapperContext context) + { + // in v7 it was apparently fine to have an empty .EditorAlias here, in which case we would map onto + // an empty fields list, which made no sense since there would be nothing to map to - and besides, + // a datatype without an editor alias is a serious issue - v8 wants an editor here + if (string.IsNullOrWhiteSpace(dataType.EditorAlias) || + !_propertyEditors.TryGet(dataType.EditorAlias, out IDataEditor? editor)) { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Name = source.Name; + throw new InvalidOperationException( + $"Could not find a property editor with alias \"{dataType.EditorAlias}\"."); } - // Umbraco.Code.MapAll -Value - private static void Map(ConfigurationField source, DataTypeConfigurationFieldDisplay target, MapperContext context) + IConfigurationEditor configurationEditor = editor.GetConfigurationEditor(); + var fields = context + .MapEnumerable(configurationEditor.Fields) + .WhereNotNull().ToList(); + IDictionary configurationDictionary = + configurationEditor.ToConfigurationEditor(dataType.Configuration); + + MapConfigurationFields(dataType, fields, configurationDictionary); + + return fields; + } + + private void MapConfigurationFields(IDataType? dataType, List fields, IDictionary? configuration) + { + if (fields == null) { - target.Config = source.Config; - target.Description = source.Description; - target.HideLabel = source.HideLabel; - target.Key = source.Key; - target.Name = source.Name; - target.View = source.View; + throw new ArgumentNullException(nameof(fields)); } - // Umbraco.Code.MapAll -Udi -HasPrevalues -IsSystemDataType -Id -Trashed -Key - // Umbraco.Code.MapAll -ParentId -Path - private static void Map(IDataEditor source, DataTypeBasic target, MapperContext context) + if (configuration == null) { - target.Alias = source.Alias; - target.Group = source.Group; - target.Icon = source.Icon; - target.Name = source.Name; + throw new ArgumentNullException(nameof(configuration)); } - // Umbraco.Code.MapAll -HasPrevalues - private void Map(IDataType source, DataTypeBasic target, MapperContext context) + // now we need to wire up the pre-values values with the actual fields defined + foreach (DataTypeConfigurationFieldDisplay field in fields.ToList()) { - target.Id = source.Id; - target.IsSystemDataType = SystemIds.Contains(source.Id); - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Trashed = source.Trashed; - target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); - - if (!_propertyEditors.TryGet(source.EditorAlias, out var editor)) - return; - - target.Alias = editor!.Alias; - target.Group = editor.Group; - target.Icon = editor.Icon; - } - - // Umbraco.Code.MapAll -HasPrevalues - private void Map(IDataType source, DataTypeDisplay target, MapperContext context) - { - target.AvailableEditors = MapAvailableEditors(source, context); - target.Id = source.Id; - target.IsSystemDataType = SystemIds.Contains(source.Id); - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.PreValues = MapPreValues(source, context); - target.SelectedEditor = source.EditorAlias.IsNullOrWhiteSpace() ? null : source.EditorAlias; - target.Trashed = source.Trashed; - target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); - - if (!_propertyEditors.TryGet(source.EditorAlias, out var editor)) - return; - - target.Alias = editor!.Alias; - target.Group = editor.Group; - target.Icon = editor.Icon; - } - - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -Key -Path -CreatorId -Level -SortOrder -Configuration - private void Map(DataTypeSave source, IDataType target, MapperContext context) - { - target.DatabaseType = MapDatabaseType(source); - target.Editor = _propertyEditors[source.EditorAlias]; - target.Id = Convert.ToInt32(source.Id); - target.Name = source.Name; - target.ParentId = source.ParentId; - } - - private IEnumerable MapAvailableEditors(IDataType source, MapperContext context) - { - var properties = _propertyEditors - .Where(x => !x.IsDeprecated || _contentSettings.ShowDeprecatedPropertyEditors || source.EditorAlias == x.Alias) - .OrderBy(x => x.Name); - return context.MapEnumerable(properties).WhereNotNull(); - } - - private IEnumerable MapPreValues(IDataType dataType, MapperContext context) - { - // in v7 it was apparently fine to have an empty .EditorAlias here, in which case we would map onto - // an empty fields list, which made no sense since there would be nothing to map to - and besides, - // a datatype without an editor alias is a serious issue - v8 wants an editor here - - if (string.IsNullOrWhiteSpace(dataType.EditorAlias) || !_propertyEditors.TryGet(dataType.EditorAlias, out var editor)) - throw new InvalidOperationException($"Could not find a property editor with alias \"{dataType.EditorAlias}\"."); - - var configurationEditor = editor!.GetConfigurationEditor(); - var fields = context.MapEnumerable(configurationEditor.Fields).WhereNotNull().ToList(); - var configurationDictionary = configurationEditor.ToConfigurationEditor(dataType.Configuration); - - MapConfigurationFields(dataType, fields, configurationDictionary); - - return fields; - } - - private void MapConfigurationFields(IDataType? dataType, List fields, IDictionary? configuration) - { - if (fields == null) throw new ArgumentNullException(nameof(fields)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - // now we need to wire up the pre-values values with the actual fields defined - foreach (var field in fields.ToList()) + // filter out the not-supported pre-values for built-in data types + if (dataType != null && dataType.IsBuildInDataType() && + (field.Key?.InvariantEquals(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes) ?? false)) { - //filter out the not-supported pre-values for built-in data types - if (dataType != null && dataType.IsBuildInDataType() && (field.Key?.InvariantEquals(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes) ?? false)) - { - fields.Remove(field); - continue; - } + fields.Remove(field); + continue; + } - if (field.Key is not null && configuration.TryGetValue(field.Key, out var value)) - { - field.Value = value; - } - else - { - // weird - just leave the field without a value - but warn - _logger.LogWarning("Could not find a value for configuration field '{ConfigField}'", field.Key); - } + if (field.Key is not null && configuration.TryGetValue(field.Key, out var value)) + { + field.Value = value; + } + else + { + // weird - just leave the field without a value - but warn + _logger.LogWarning("Could not find a value for configuration field '{ConfigField}'", field.Key); } } + } - private ValueStorageType MapDatabaseType(DataTypeSave source) + private ValueStorageType MapDatabaseType(DataTypeSave source) + { + if (!_propertyEditors.TryGet(source.EditorAlias, out IDataEditor? editor)) { - if (!_propertyEditors.TryGet(source.EditorAlias, out var editor)) - throw new InvalidOperationException($"Could not find property editor \"{source.EditorAlias}\"."); - - // TODO: what about source.PropertyEditor? can we get the configuration here? 'cos it may change the storage type?! - var valueType = editor!.GetValueEditor().ValueType; - return ValueTypes.ToStorageType(valueType); + throw new InvalidOperationException($"Could not find property editor \"{source.EditorAlias}\"."); } - private IEnumerable MapPreValues(IDataEditor source, MapperContext context) + // TODO: what about source.PropertyEditor? can we get the configuration here? 'cos it may change the storage type?! + var valueType = editor.GetValueEditor().ValueType; + return ValueTypes.ToStorageType(valueType); + } + + private IEnumerable MapPreValues(IDataEditor source, MapperContext context) + { + // this is a new data type, initialize default configuration + // get the configuration editor, + // get the configuration fields and map to UI, + // get the configuration default values and map to UI + IConfigurationEditor configurationEditor = source.GetConfigurationEditor(); + + var fields = + context.MapEnumerable(configurationEditor.Fields) + .WhereNotNull().ToList(); + + IDictionary defaultConfiguration = configurationEditor.DefaultConfiguration; + if (defaultConfiguration != null) { - // this is a new data type, initialize default configuration - // get the configuration editor, - // get the configuration fields and map to UI, - // get the configuration default values and map to UI - - var configurationEditor = source.GetConfigurationEditor(); - - var fields = - context.MapEnumerable(configurationEditor.Fields).WhereNotNull().ToList(); - - var defaultConfiguration = configurationEditor.DefaultConfiguration; - if (defaultConfiguration != null) - MapConfigurationFields(null, fields, defaultConfiguration); - - return fields; + MapConfigurationFields(null, fields, defaultConfiguration); } + + return fields; } } diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs index 2a776fd2fd..5e92783d14 100644 --- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs @@ -1,121 +1,120 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// +/// The dictionary model mapper. +/// +public class DictionaryMapDefinition : IMapDefinition { - /// - /// - /// The dictionary model mapper. - /// - public class DictionaryMapDefinition : IMapDefinition + private readonly CommonMapper? _commonMapper; + private readonly ILocalizationService _localizationService; + + public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper) { - private readonly ILocalizationService _localizationService; - private readonly CommonMapper? _commonMapper; + _localizationService = localizationService; + _commonMapper = commonMapper; + } - public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper) + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new DictionaryDisplay(), Map); + mapper.Define((source, context) => new DictionaryOverviewDisplay(), Map); + } + + // Umbraco.Code.MapAll -ParentId -Path -Trashed -Udi -Icon + private static void Map(IDictionaryItem source, EntityBasic target, MapperContext context) + { + target.Alias = source.ItemKey; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.ItemKey; + } + + private static void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) + { + IDictionaryItem? dictionary = localizationService.GetDictionaryItemById(parentId); + if (dictionary == null) { - _localizationService = localizationService; - _commonMapper = commonMapper; + return; } - public void DefineMaps(IUmbracoMapper mapper) + ids.Add(dictionary.Id); + + if (dictionary.ParentId.HasValue) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new DictionaryDisplay(), Map); - mapper.Define((source, context) => new DictionaryOverviewDisplay(), Map); + GetParentId(dictionary.ParentId.Value, localizationService, ids); + } + } + + // Umbraco.Code.MapAll -Icon -Trashed -Alias + private void Map(IDictionaryItem source, DictionaryDisplay target, MapperContext context) + { + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.ItemKey; + target.ParentId = source.ParentId ?? Guid.Empty; + target.Udi = Udi.Create(Constants.UdiEntityType.DictionaryItem, source.Key); + if (_commonMapper != null) + { + target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source)); } - // Umbraco.Code.MapAll -ParentId -Path -Trashed -Udi -Icon - private static void Map(IDictionaryItem source, EntityBasic target, MapperContext context) + // build up the path to make it possible to set active item in tree + // TODO: check if there is a better way + if (source.ParentId.HasValue) { - target.Alias = source.ItemKey; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.ItemKey; + var ids = new List { -1 }; + var parentIds = new List(); + GetParentId(source.ParentId.Value, _localizationService, parentIds); + parentIds.Reverse(); + ids.AddRange(parentIds); + ids.Add(source.Id); + target.Path = string.Join(",", ids); + } + else + { + target.Path = "-1," + source.Id; } - // Umbraco.Code.MapAll -Icon -Trashed -Alias - private void Map(IDictionaryItem source, DictionaryDisplay target, MapperContext context) + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) { - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.ItemKey; - target.ParentId = source.ParentId ?? Guid.Empty; - target.Udi = Udi.Create(Constants.UdiEntityType.DictionaryItem, source.Key); - if (_commonMapper != null) - { - target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source)); - } + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - // build up the path to make it possible to set active item in tree - // TODO: check if there is a better way - if (source.ParentId.HasValue) + target.Translations.Add(new DictionaryTranslationDisplay { - var ids = new List { -1 }; - var parentIds = new List(); - GetParentId(source.ParentId.Value, _localizationService, parentIds); - parentIds.Reverse(); - ids.AddRange(parentIds); - ids.Add(source.Id); - target.Path = string.Join(",", ids); - } - else - { - target.Path = "-1," + source.Id; - } + IsoCode = lang.IsoCode, + DisplayName = lang.CultureName, + Translation = translation?.Value ?? string.Empty, + LanguageId = lang.Id, + }); + } + } - // add all languages and the translations - foreach (var lang in _localizationService.GetAllLanguages()) - { - var langId = lang.Id; - var translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); + // Umbraco.Code.MapAll -Level -Translations + private void Map(IDictionaryItem source, DictionaryOverviewDisplay target, MapperContext context) + { + target.Id = source.Id; + target.Name = source.ItemKey; - target.Translations.Add(new DictionaryTranslationDisplay + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) + { + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); + + target.Translations.Add( + new DictionaryOverviewTranslationDisplay { - IsoCode = lang.IsoCode, DisplayName = lang.CultureName, - Translation = translation?.Value ?? string.Empty, - LanguageId = lang.Id + HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false, }); - } - } - - // Umbraco.Code.MapAll -Level -Translations - private void Map(IDictionaryItem source, DictionaryOverviewDisplay target, MapperContext context) - { - target.Id = source.Id; - target.Name = source.ItemKey; - - // add all languages and the translations - foreach (var lang in _localizationService.GetAllLanguages()) - { - var langId = lang.Id; - var translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - - target.Translations.Add( - new DictionaryOverviewTranslationDisplay - { - DisplayName = lang.CultureName, - HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false - }); - } - } - - private static void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) - { - var dictionary = localizationService.GetDictionaryItemById(parentId); - if (dictionary == null) - return; - - ids.Add(dictionary.Id); - - if (dictionary.ParentId.HasValue) - GetParentId(dictionary.ParentId.Value, localizationService, ids); } } } diff --git a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs index 7450ec62b4..81096889c8 100644 --- a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs @@ -1,60 +1,61 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class LanguageMapDefinition : IMapDefinition { - public class LanguageMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) { - public void DefineMaps(IUmbracoMapper mapper) + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new ContentEditing.Language(), Map); + mapper.Define, IEnumerable>( + (source, context) => new List(), Map); + } + + // Umbraco.Code.MapAll -Udi -Path -Trashed -AdditionalData -Icon + private static void Map(ILanguage source, EntityBasic target, MapperContext context) + { + target.Name = source.CultureName; + target.Key = source.Key; + target.ParentId = -1; + target.Alias = source.IsoCode; + target.Id = source.Id; + } + + // Umbraco.Code.MapAll + private static void Map(ILanguage source, ContentEditing.Language target, MapperContext context) + { + target.Id = source.Id; + target.IsoCode = source.IsoCode; + target.Name = source.CultureName; + target.IsDefault = source.IsDefault; + target.IsMandatory = source.IsMandatory; + target.FallbackLanguageId = source.FallbackLanguageId; + } + + private static void Map(IEnumerable source, IEnumerable target, MapperContext context) + { + if (target == null) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new ContentEditing.Language(), Map); - mapper.Define, IEnumerable>((source, context) => new List(), Map); + throw new ArgumentNullException(nameof(target)); } - // Umbraco.Code.MapAll -Udi -Path -Trashed -AdditionalData -Icon - private static void Map(ILanguage source, EntityBasic target, MapperContext context) + if (!(target is List list)) { - target.Name = source.CultureName; - target.Key = source.Key; - target.ParentId = -1; - target.Alias = source.IsoCode; - target.Id = source.Id; + throw new NotSupportedException($"{nameof(target)} must be a List."); } - // Umbraco.Code.MapAll - private static void Map(ILanguage source, ContentEditing.Language target, MapperContext context) + List temp = context.MapEnumerable(source); + + // Put the default language first in the list & then sort rest by a-z + ContentEditing.Language? defaultLang = temp.SingleOrDefault(x => x.IsDefault); + + // insert default lang first, then remaining language a-z + if (defaultLang is not null) { - target.Id = source.Id; - target.IsoCode = source.IsoCode; - target.Name = source.CultureName; - target.IsDefault = source.IsDefault; - target.IsMandatory = source.IsMandatory; - target.FallbackLanguageId = source.FallbackLanguageId; - } - - private static void Map(IEnumerable source, IEnumerable target, MapperContext context) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - if (!(target is List list)) - throw new NotSupportedException($"{nameof(target)} must be a List."); - - var temp = context.MapEnumerable(source); - - //Put the default language first in the list & then sort rest by a-z - var defaultLang = temp.SingleOrDefault(x => x!.IsDefault); - - // insert default lang first, then remaining language a-z - if (defaultLang is not null) - { - list.Add(defaultLang); - list.AddRange(temp.Where(x => x != defaultLang).OrderBy(x => x!.Name).WhereNotNull()); - } + list.Add(defaultLang); + list.AddRange(temp.Where(x => x != defaultLang).OrderBy(x => x.Name)); } } } diff --git a/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs index 13fe7f7c33..a042497013 100644 --- a/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs @@ -1,88 +1,89 @@ -using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class MacroMapDefinition : IMapDefinition { - public class MacroMapDefinition : IMapDefinition + private readonly ILogger _logger; + private readonly ParameterEditorCollection _parameterEditors; + + public MacroMapDefinition(ParameterEditorCollection parameterEditors, ILogger logger) { - private readonly ParameterEditorCollection _parameterEditors; - private readonly ILogger _logger; + _parameterEditors = parameterEditors; + _logger = logger; + } - public MacroMapDefinition(ParameterEditorCollection parameterEditors, ILogger logger) + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new MacroDisplay(), Map); + mapper.Define>((source, context) => + context.MapEnumerable(source.Properties.Values).WhereNotNull()); + mapper.Define((source, context) => new MacroParameter(), Map); + } + + // Umbraco.Code.MapAll -Trashed -AdditionalData + private static void Map(IMacro source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Macro; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); + } + + private void Map(IMacro source, MacroDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Macro; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); + target.CacheByPage = source.CacheByPage; + target.CacheByUser = source.CacheByMember; + target.CachePeriod = source.CacheDuration; + target.UseInEditor = source.UseInEditor; + target.RenderInEditor = !source.DontRender; + target.View = source.MacroSource; + } + + // Umbraco.Code.MapAll -Value + private void Map(IMacroProperty source, MacroParameter target, MapperContext context) + { + target.Alias = source.Alias; + target.Name = source.Name; + target.SortOrder = source.SortOrder; + + // map the view and the config + // we need to show the deprecated ones for backwards compatibility + IDataEditor? paramEditor = _parameterEditors[source.EditorAlias]; // TODO: include/filter deprecated?! + if (paramEditor == null) { - _parameterEditors = parameterEditors; - _logger = logger; + // we'll just map this to a text box + paramEditor = _parameterEditors[Constants.PropertyEditors.Aliases.TextBox]; + _logger.LogWarning( + "Could not resolve a parameter editor with alias {PropertyEditorAlias}, a textbox will be rendered in it's place", + source.EditorAlias); } - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new MacroDisplay(), Map); - mapper.Define>((source, context) => context.MapEnumerable(source.Properties.Values).WhereNotNull()); - mapper.Define((source, context) => new MacroParameter(), Map); - } + target.View = paramEditor?.GetValueEditor().View; - // Umbraco.Code.MapAll -Trashed -AdditionalData - private static void Map(IMacro source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = Constants.Icons.Macro; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); - } - - private void Map(IMacro source, MacroDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = Constants.Icons.Macro; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); - target.CacheByPage = source.CacheByPage; - target.CacheByUser = source.CacheByMember; - target.CachePeriod = source.CacheDuration; - target.UseInEditor = source.UseInEditor; - target.RenderInEditor = !source.DontRender; - target.View = source.MacroSource; - } - // Umbraco.Code.MapAll -Value - private void Map(IMacroProperty source, MacroParameter target, MapperContext context) - { - target.Alias = source.Alias; - target.Name = source.Name; - target.SortOrder = source.SortOrder; - - //map the view and the config - // we need to show the deprecated ones for backwards compatibility - var paramEditor = _parameterEditors[source.EditorAlias]; // TODO: include/filter deprecated?! - if (paramEditor == null) - { - //we'll just map this to a text box - paramEditor = _parameterEditors[Constants.PropertyEditors.Aliases.TextBox]; - _logger.LogWarning("Could not resolve a parameter editor with alias {PropertyEditorAlias}, a textbox will be rendered in it's place", source.EditorAlias); - } - - target.View = paramEditor?.GetValueEditor().View; - - // sets the parameter configuration to be the default configuration editor's configuration, - // ie configurationEditor.DefaultConfigurationObject, prepared for the value editor, ie - // after ToValueEditor - important to use DefaultConfigurationObject here, because depending - // on editors, ToValueEditor expects the actual strongly typed configuration - not the - // dictionary thing returned by DefaultConfiguration - - var configurationEditor = paramEditor?.GetConfigurationEditor(); - target.Configuration = configurationEditor?.ToValueEditor(configurationEditor.DefaultConfigurationObject); - } + // sets the parameter configuration to be the default configuration editor's configuration, + // ie configurationEditor.DefaultConfigurationObject, prepared for the value editor, ie + // after ToValueEditor - important to use DefaultConfigurationObject here, because depending + // on editors, ToValueEditor expects the actual strongly typed configuration - not the + // dictionary thing returned by DefaultConfiguration + IConfigurationEditor? configurationEditor = paramEditor?.GetConfigurationEditor(); + target.Configuration = configurationEditor?.ToValueEditor(configurationEditor.DefaultConfigurationObject); } } diff --git a/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs b/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs index 89cdc22106..70d4826ab6 100644 --- a/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs +++ b/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs @@ -1,68 +1,61 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the class. +/// +public static class MapperContextExtensions { + private const string CultureKey = "Map.Culture"; + private const string SegmentKey = "Map.Segment"; + private const string IncludedPropertiesKey = "Map.IncludedProperties"; + /// - /// Provides extension methods for the class. + /// Gets the context culture. /// - public static class MapperContextExtensions + public static string? GetCulture(this MapperContext context) => + context.HasItems && context.Items.TryGetValue(CultureKey, out var obj) && obj is string s ? s : null; + + /// + /// Gets the context segment. + /// + public static string? GetSegment(this MapperContext context) => + context.HasItems && context.Items.TryGetValue(SegmentKey, out var obj) && obj is string s ? s : null; + + /// + /// Sets a context culture. + /// + public static void SetCulture(this MapperContext context, string? culture) { - private const string CultureKey = "Map.Culture"; - private const string SegmentKey = "Map.Segment"; - private const string IncludedPropertiesKey = "Map.IncludedProperties"; - - /// - /// Gets the context culture. - /// - public static string? GetCulture(this MapperContext context) + if (culture is not null) { - return context.HasItems && context.Items.TryGetValue(CultureKey, out var obj) && obj is string s ? s : null; - } - - /// - /// Gets the context segment. - /// - public static string? GetSegment(this MapperContext context) - { - return context.HasItems && context.Items.TryGetValue(SegmentKey, out var obj) && obj is string s ? s : null; - } - - /// - /// Sets a context culture. - /// - public static void SetCulture(this MapperContext context, string? culture) - { - if (culture is not null) - { - context.Items[CultureKey] = culture; - } - } - - /// - /// Sets a context segment. - /// - public static void SetSegment(this MapperContext context, string? segment) - { - if (segment is not null) - { - context.Items[SegmentKey] = segment; - } - } - - /// - /// Get included properties. - /// - public static string[]? GetIncludedProperties(this MapperContext context) - { - return context.HasItems && context.Items.TryGetValue(IncludedPropertiesKey, out var obj) && obj is string[] s ? s : null; - } - - /// - /// Sets included properties. - /// - public static void SetIncludedProperties(this MapperContext context, string[] properties) - { - context.Items[IncludedPropertiesKey] = properties; + context.Items[CultureKey] = culture; } } + + /// + /// Sets a context segment. + /// + public static void SetSegment(this MapperContext context, string? segment) + { + if (segment is not null) + { + context.Items[SegmentKey] = segment; + } + } + + /// + /// Get included properties. + /// + public static string[]? GetIncludedProperties(this MapperContext context) => context.HasItems && + context.Items.TryGetValue(IncludedPropertiesKey, out var obj) && obj is string[] s + ? s + : null; + + /// + /// Sets included properties. + /// + public static void SetIncludedProperties(this MapperContext context, string[] properties) => + context.Items[IncludedPropertiesKey] = properties; } diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs index c2c3e14f5d..8444d5bd0a 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -1,32 +1,31 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +public class MemberMapDefinition : IMapDefinition { /// - public class MemberMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) => mapper.Define(Map); + + private static void Map(MemberSave source, IMember target, MapperContext context) { - /// - public void DefineMaps(IUmbracoMapper mapper) => mapper.Define(Map); + target.IsApproved = source.IsApproved; + target.Name = source.Name; + target.Email = source.Email; + target.Key = source.Key; + target.Username = source.Username; + target.Comments = source.Comments; + target.CreateDate = source.CreateDate; + target.UpdateDate = source.UpdateDate; + target.Email = source.Email; - private static void Map(MemberSave source, IMember target, MapperContext context) - { - target.IsApproved = source.IsApproved; - target.Name = source.Name; - target.Email = source.Email; - target.Key = source.Key; - target.Username = source.Username; - target.Comments = source.Comments; - target.CreateDate = source.CreateDate; - target.UpdateDate = source.UpdateDate; - target.Email = source.Email; + // TODO: ensure all properties are mapped as required + // target.Id = source.Id; + // target.ParentId = -1; + // target.Path = "-1," + source.Id; - // TODO: ensure all properties are mapped as required - //target.Id = source.Id; - //target.ParentId = -1; - //target.Path = "-1," + source.Id; - - //TODO: add groups as required - } + // TODO: add groups as required } } diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 1b32bca397..65db6181dd 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Schema; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dictionary; @@ -12,287 +8,286 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// A custom tab/property resolver for members which will ensure that the built-in membership properties are or aren't displayed +/// depending on if the member type has these properties +/// +/// +/// This also ensures that the IsLocked out property is readonly when the member is not locked out - this is because +/// an admin cannot actually set isLockedOut = true, they can only unlock. +/// +public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper { - /// - /// A custom tab/property resolver for members which will ensure that the built-in membership properties are or aren't displayed - /// depending on if the member type has these properties - /// - /// - /// This also ensures that the IsLocked out property is readonly when the member is not locked out - this is because - /// an admin cannot actually set isLockedOut = true, they can only unlock. - /// - public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly ILocalizedTextService _localizedTextService; + private readonly IMemberTypeService _memberTypeService; + private readonly IMemberService _memberService; + private readonly IMemberGroupService _memberGroupService; + private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; + private readonly PropertyEditorCollection _propertyEditorCollection; + + public MemberTabsAndPropertiesMapper( + ICultureDictionary cultureDictionary, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ILocalizedTextService localizedTextService, + IMemberTypeService memberTypeService, + IMemberService memberService, + IMemberGroupService memberGroupService, + IOptions memberPasswordConfiguration, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + PropertyEditorCollection propertyEditorCollection) + : base(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider) { - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly ILocalizedTextService _localizedTextService; - private readonly IMemberTypeService _memberTypeService; - private readonly IMemberService _memberService; - private readonly IMemberGroupService _memberGroupService; - private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; - private readonly PropertyEditorCollection _propertyEditorCollection; + _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); + _memberPasswordConfiguration = memberPasswordConfiguration.Value; + _propertyEditorCollection = propertyEditorCollection; + } - public MemberTabsAndPropertiesMapper(ICultureDictionary cultureDictionary, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - ILocalizedTextService localizedTextService, - IMemberTypeService memberTypeService, - IMemberService memberService, - IMemberGroupService memberGroupService, - IOptions memberPasswordConfiguration, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - PropertyEditorCollection propertyEditorCollection) - : base(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider) - { - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); - _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); - _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); - _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); - _memberPasswordConfiguration = memberPasswordConfiguration.Value; - _propertyEditorCollection = propertyEditorCollection; - } + /// + /// Overridden to deal with custom member properties and permissions. + public override IEnumerable> Map(IMember source, MapperContext context) + { - /// - /// Overridden to deal with custom member properties and permissions. - public override IEnumerable> Map(IMember source, MapperContext context) + IMemberType? memberType = _memberTypeService.Get(source.ContentTypeId); + + if (memberType is not null) { - var memberType = _memberTypeService.Get(source.ContentTypeId); - - if (memberType is not null) - { - - IgnoreProperties = memberType.CompositionPropertyTypes - .Where(x => x.HasIdentity == false) - .Select(x => x.Alias) - .ToArray(); - } - - var resolved = base.Map(source, context); - - return resolved; + IgnoreProperties = memberType.CompositionPropertyTypes + .Where(x => x.HasIdentity == false) + .Select(x => x.Alias) + .ToArray(); } - // We need this because we call GetCustomGenericProperties from TabsAndPropertiesMapper + IEnumerable> resolved = base.Map(source, context); + + return resolved; + } + + // We need this because we call GetCustomGenericProperties from TabsAndPropertiesMapper // and we have no access to MapMembershipProperties from the base class without casting - protected override IEnumerable GetCustomGenericProperties(IContentBase content) + protected override IEnumerable GetCustomGenericProperties(IContentBase content) + { + var member = (IMember)content; + return MapMembershipProperties(member, null); + } + + private Dictionary GetPasswordConfig(IMember member) + { + var result = new Dictionary(_memberPasswordConfiguration.GetConfiguration(true)) { - var member = (IMember)content; - return MapMembershipProperties(member, null); + // the password change toggle will only be displayed if there is already a password assigned. + {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} + }; + + // This will always be true for members since we always want to allow admins to change a password - so long as that + // user has access to edit members (but that security is taken care of separately) + result["allowManuallyChangingPassword"] = true; + + return result; + } + + /// + /// Overridden to assign the IsSensitive property values + /// + /// + /// + /// + /// + protected override List MapProperties(IContentBase content, List properties, MapperContext context) + { + List result = base.MapProperties(content, properties, context); + var member = (IMember)content; + IMemberType? memberType = _memberTypeService.Get(member.ContentTypeId); + + // now update the IsSensitive value + foreach (ContentPropertyDisplay prop in result) + { + // check if this property is flagged as sensitive + var isSensitiveProperty = memberType?.IsSensitiveProperty(prop.Alias) ?? false; + // check permissions for viewing sensitive data + if (isSensitiveProperty && _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() == false) + { + // mark this property as sensitive + prop.IsSensitive = true; + // mark this property as readonly so that it does not post any data + prop.Readonly = true; + // replace this editor with a sensitive value + prop.View = "sensitivevalue"; + // clear the value + prop.Value = null; + } } + return result; + } - private Dictionary GetPasswordConfig(IMember member) + /// + /// Returns the login property display field + /// + /// + /// + /// + /// + /// If the membership provider installed is the umbraco membership provider, then we will allow changing the username, however if + /// the membership provider is a custom one, we cannot allow changing the username because MembershipProvider's do not actually natively + /// allow that. + /// + internal static ContentPropertyDisplay GetLoginProperty(IMember member, ILocalizedTextService localizedText) + { + var prop = new ContentPropertyDisplay { - var result = new Dictionary(_memberPasswordConfiguration.GetConfiguration(true)) - { - // the password change toggle will only be displayed if there is already a password assigned. - {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} - }; + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", + Label = localizedText.Localize(null,"login"), + Value = member.Username + }; - // This will always be true for members since we always want to allow admins to change a password - so long as that - // user has access to edit members (but that security is taken care of separately) - result["allowManuallyChangingPassword"] = true; + prop.View = "textbox"; + prop.Validation.Mandatory = true; + return prop; + } + internal IDictionary GetMemberGroupValue(string username) + { + IEnumerable userRoles = _memberService.GetAllRoles(username); + + // create a dictionary of all roles (except internal roles) + "false" + var result = _memberGroupService.GetAll() + .Select(x => x.Name!) + // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access + .Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToDictionary(x => x, x => false); + + // if user has no roles, just return the dictionary + if (userRoles == null) + { return result; } - /// - /// Overridden to assign the IsSensitive property values - /// - /// - /// - /// - /// - protected override List MapProperties(IContentBase content, List properties, MapperContext context) + // else update the dictionary to "true" for the user roles (except internal roles) + foreach (var userRole in userRoles.Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false)) { - var result = base.MapProperties(content, properties, context); - var member = (IMember)content; - var memberType = _memberTypeService.Get(member.ContentTypeId); + result[userRole] = true; + } - // now update the IsSensitive value - foreach (var prop in result) + return result; + } + + public IEnumerable MapMembershipProperties(IMember member, MapperContext? context) + { + var properties = new List + { + GetLoginProperty(member, _localizedTextService), + new() { - // check if this property is flagged as sensitive - var isSensitiveProperty = memberType?.IsSensitiveProperty(prop.Alias) ?? false; - // check permissions for viewing sensitive data - if (isSensitiveProperty && (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() == false)) + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", + Label = _localizedTextService.Localize("general","email"), + Value = member.Email, + View = "email", + Validation = { Mandatory = true } + }, + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", + Label = _localizedTextService.Localize(null,"password"), + Value = new Dictionary { - // mark this property as sensitive - prop.IsSensitive = true; - // mark this property as readonly so that it does not post any data - prop.Readonly = true; - // replace this editor with a sensitive value - prop.View = "sensitivevalue"; - // clear the value - prop.Value = null; + // TODO: why ignoreCase, what are we doing here?! + { "newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) } + }, + View = "changepassword", + Config = GetPasswordConfig(member) // Initialize the dictionary with the configuration from the default membership provider + }, + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", + Label = _localizedTextService.Localize("content","membergroup"), + Value = GetMemberGroupValue(member.Username), + View = "membergroups", + Config = new Dictionary + { + { "IsRequired", true } + }, + }, + + // These properties used to live on the member as property data, defaulting to sensitive, so we set them to sensitive here too + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}failedPasswordAttempts", + Label = _localizedTextService.Localize("user", "failedPasswordAttempts"), + Value = member.FailedPasswordAttempts, + View = "readonlyvalue", + IsSensitive = true, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}approved", + Label = _localizedTextService.Localize("user", "stateApproved"), + Value = member.IsApproved, + View = "boolean", + IsSensitive = true, + Readonly = false, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lockedOut", + Label = _localizedTextService.Localize("user", "stateLockedOut"), + Value = member.IsLockedOut, + View = "boolean", + IsSensitive = true, + Readonly = !member.IsLockedOut, // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate", + Label = _localizedTextService.Localize("user", "lastLockoutDate"), + Value = member.LastLockoutDate?.ToString(), + View = "readonlyvalue", + IsSensitive = true, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLoginDate", + Label = _localizedTextService.Localize("user", "lastLogin"), + Value = member.LastLoginDate?.ToString(), + View = "readonlyvalue", + IsSensitive = true, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastPasswordChangeDate", + Label = _localizedTextService.Localize("user", "lastPasswordChangeDate"), + Value = member.LastPasswordChangeDate?.ToString(), + View = "readonlyvalue", + IsSensitive = true, + }, + }; + + if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() is false) + { + // Current user doesn't have access to sensitive data so explicitly set the views and remove the value from sensitive data + foreach (ContentPropertyDisplay property in properties) + { + if (property.IsSensitive) + { + property.Value = null; + property.View = "sensitivevalue"; + property.Readonly = true; } } - return result; } - /// - /// Returns the login property display field - /// - /// - /// - /// - /// - /// - /// If the membership provider installed is the umbraco membership provider, then we will allow changing the username, however if - /// the membership provider is a custom one, we cannot allow changing the username because MembershipProvider's do not actually natively - /// allow that. - /// - internal static ContentPropertyDisplay GetLoginProperty(IMember member, ILocalizedTextService localizedText) - { - var prop = new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", - Label = localizedText.Localize(null,"login"), - Value = member.Username - }; - - prop.View = "textbox"; - prop.Validation.Mandatory = true; - return prop; - } - - internal IDictionary GetMemberGroupValue(string username) - { - IEnumerable userRoles = _memberService.GetAllRoles(username); - - // create a dictionary of all roles (except internal roles) + "false" - var result = _memberGroupService.GetAll() - .Select(x => x.Name!) - // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access - .Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToDictionary(x => x, x => false); - - // if user has no roles, just return the dictionary - if (userRoles == null) - { - return result; - } - - // else update the dictionary to "true" for the user roles (except internal roles) - foreach (var userRole in userRoles.Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false)) - { - result[userRole] = true; - } - - return result; - } - - public IEnumerable MapMembershipProperties(IMember member, MapperContext? context) - { - var properties = new List - { - GetLoginProperty(member, _localizedTextService), - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", - Label = _localizedTextService.Localize("general","email"), - Value = member.Email, - View = "email", - Validation = { Mandatory = true } - }, - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", - Label = _localizedTextService.Localize(null,"password"), - Value = new Dictionary - { - // TODO: why ignoreCase, what are we doing here?! - { "newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) } - }, - View = "changepassword", - Config = GetPasswordConfig(member) // Initialize the dictionary with the configuration from the default membership provider - }, - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", - Label = _localizedTextService.Localize("content","membergroup"), - Value = GetMemberGroupValue(member.Username), - View = "membergroups", - Config = new Dictionary - { - { "IsRequired", true } - }, - }, - - // These properties used to live on the member as property data, defaulting to sensitive, so we set them to sensitive here too - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}failedPasswordAttempts", - Label = _localizedTextService.Localize("user", "failedPasswordAttempts"), - Value = member.FailedPasswordAttempts, - View = "readonlyvalue", - IsSensitive = true, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}approved", - Label = _localizedTextService.Localize("user", "stateApproved"), - Value = member.IsApproved, - View = "boolean", - IsSensitive = true, - Readonly = false, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lockedOut", - Label = _localizedTextService.Localize("user", "stateLockedOut"), - Value = member.IsLockedOut, - View = "boolean", - IsSensitive = true, - Readonly = !member.IsLockedOut, // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate", - Label = _localizedTextService.Localize("user", "lastLockoutDate"), - Value = member.LastLockoutDate?.ToString(), - View = "readonlyvalue", - IsSensitive = true, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLoginDate", - Label = _localizedTextService.Localize("user", "lastLogin"), - Value = member.LastLoginDate?.ToString(), - View = "readonlyvalue", - IsSensitive = true, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastPasswordChangeDate", - Label = _localizedTextService.Localize("user", "lastPasswordChangeDate"), - Value = member.LastPasswordChangeDate?.ToString(), - View = "readonlyvalue", - IsSensitive = true, - }, - }; - - if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() is false) - { - // Current user doesn't have access to sensitive data so explicitly set the views and remove the value from sensitive data - foreach (var property in properties) - { - if (property.IsSensitive) - { - property.Value = null; - property.View = "sensitivevalue"; - property.Readonly = true; - } - } - } - - return properties; - } + return properties; } } diff --git a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs index 5d4d9ba485..cb77d790cd 100644 --- a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; @@ -8,258 +5,291 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class PropertyTypeGroupMapper + where TPropertyType : PropertyTypeDisplay, new() { - public class PropertyTypeGroupMapper - where TPropertyType : PropertyTypeDisplay, new() + private readonly IDataTypeService _dataTypeService; + private readonly ILogger> _logger; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IShortStringHelper _shortStringHelper; + + public PropertyTypeGroupMapper(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ILogger> logger) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly IShortStringHelper _shortStringHelper; - private readonly ILogger> _logger; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _shortStringHelper = shortStringHelper; + _logger = logger; + } - public PropertyTypeGroupMapper(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ILogger> logger) + public IEnumerable> Map(IContentTypeComposition source) + { + // deal with groups + var groups = new List>(); + + // add groups local to this content type + foreach (PropertyGroup propertyGroup in source.PropertyGroups) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _shortStringHelper = shortStringHelper; - _logger = logger; + var group = new PropertyGroupDisplay + { + Id = propertyGroup.Id, + Key = propertyGroup.Key, + Type = propertyGroup.Type, + Name = propertyGroup.Name, + Alias = propertyGroup.Alias, + SortOrder = propertyGroup.SortOrder, + Properties = MapProperties(propertyGroup.PropertyTypes, source, propertyGroup.Id, false), + ContentTypeId = source.Id, + }; + + groups.Add(group); } - /// - /// Gets the content type that defines a property group, within a composition. - /// - /// The composition. - /// The identifier of the property group. - /// The composition content type that defines the specified property group. - private static IContentTypeComposition? GetContentTypeForPropertyGroup(IContentTypeComposition contentType, int propertyGroupId) + // add groups inherited through composition + var localGroupIds = groups.Select(x => x.Id).ToArray(); + foreach (PropertyGroup propertyGroup in source.CompositionPropertyGroups) { - // test local groups - if (contentType.PropertyGroups.Any(x => x.Id == propertyGroupId)) - return contentType; + // skip those that are local to this content type + if (localGroupIds.Contains(propertyGroup.Id)) + { + continue; + } - // test composition types groups - // .ContentTypeComposition is just the local ones, not recursive, - // so we have to recurse here - return contentType.ContentTypeComposition - .Select(x => GetContentTypeForPropertyGroup(x, propertyGroupId)) - .FirstOrDefault(x => x != null); + // get the content type that defines this group + IContentTypeComposition? definingContentType = GetContentTypeForPropertyGroup(source, propertyGroup.Id); + if (definingContentType == null) + { + throw new Exception("PropertyGroup with id=" + propertyGroup.Id + + " was not found on any of the content type's compositions."); + } + + var group = new PropertyGroupDisplay + { + Inherited = true, + Id = propertyGroup.Id, + Key = propertyGroup.Key, + Type = propertyGroup.Type, + Name = propertyGroup.Name, + Alias = propertyGroup.Alias, + SortOrder = propertyGroup.SortOrder, + Properties = + MapProperties(propertyGroup.PropertyTypes, definingContentType, propertyGroup.Id, true), + ContentTypeId = definingContentType.Id, + ParentTabContentTypes = new[] { definingContentType.Id }, + ParentTabContentTypeNames = new[] { definingContentType.Name }, + }; + + groups.Add(group); } - /// - /// Gets the content type that defines a property group, within a composition. - /// - /// The composition. - /// The identifier of the property type. - /// The composition content type that defines the specified property group. - private static IContentTypeComposition? GetContentTypeForPropertyType(IContentTypeComposition contentType, int propertyTypeId) - { - // test local property types - if (contentType.PropertyTypes.Any(x => x.Id == propertyTypeId)) - return contentType; + // deal with generic properties + var genericProperties = new List(); - // test composition property types - // .ContentTypeComposition is just the local ones, not recursive, - // so we have to recurse here - return contentType.ContentTypeComposition - .Select(x => GetContentTypeForPropertyType(x, propertyTypeId)) - .FirstOrDefault(x => x != null); + // add generic properties local to this content type + IEnumerable entityGenericProperties = source.PropertyTypes.Where(x => x.PropertyGroupId == null); + genericProperties.AddRange(MapProperties(entityGenericProperties, source, PropertyGroupBasic.GenericPropertiesGroupId, false)); + + // add generic properties inherited through compositions + var localGenericPropertyIds = genericProperties.Select(x => x.Id).ToArray(); + IEnumerable compositionGenericProperties = source.CompositionPropertyTypes + .Where(x => x.PropertyGroupId == null // generic + && localGenericPropertyIds.Contains(x.Id) == false); // skip those that are local + foreach (IPropertyType compositionGenericProperty in compositionGenericProperties) + { + IContentTypeComposition? definingContentType = + GetContentTypeForPropertyType(source, compositionGenericProperty.Id); + if (definingContentType == null) + { + throw new Exception("PropertyType with id=" + compositionGenericProperty.Id + + " was not found on any of the content type's compositions."); + } + + genericProperties.AddRange(MapProperties(new[] { compositionGenericProperty }, definingContentType, PropertyGroupBasic.GenericPropertiesGroupId, true)); } - public IEnumerable> Map(IContentTypeComposition source) + // if there are any generic properties, add the corresponding tab + if (genericProperties.Any()) { - // deal with groups - var groups = new List>(); - - // add groups local to this content type - foreach (var propertyGroup in source.PropertyGroups) + var genericGroup = new PropertyGroupDisplay { - var group = new PropertyGroupDisplay - { - Id = propertyGroup.Id, - Key = propertyGroup.Key, - Type = propertyGroup.Type, - Name = propertyGroup.Name, - Alias = propertyGroup.Alias, - SortOrder = propertyGroup.SortOrder, - Properties = MapProperties(propertyGroup.PropertyTypes, source, propertyGroup.Id, false), - ContentTypeId = source.Id - }; + Id = PropertyGroupBasic.GenericPropertiesGroupId, + Name = "Generic properties", + Alias = "genericProperties", + SortOrder = 999, + Properties = genericProperties, + ContentTypeId = source.Id, + }; - groups.Add(group); - } - - // add groups inherited through composition - var localGroupIds = groups.Select(x => x.Id).ToArray(); - foreach (var propertyGroup in source.CompositionPropertyGroups) - { - // skip those that are local to this content type - if (localGroupIds.Contains(propertyGroup.Id)) continue; - - // get the content type that defines this group - var definingContentType = GetContentTypeForPropertyGroup(source, propertyGroup.Id); - if (definingContentType == null) - throw new Exception("PropertyGroup with id=" + propertyGroup.Id + " was not found on any of the content type's compositions."); - - var group = new PropertyGroupDisplay - { - Inherited = true, - Id = propertyGroup.Id, - Key = propertyGroup.Key, - Type = propertyGroup.Type, - Name = propertyGroup.Name, - Alias = propertyGroup.Alias, - SortOrder = propertyGroup.SortOrder, - Properties = MapProperties(propertyGroup.PropertyTypes, definingContentType, propertyGroup.Id, true), - ContentTypeId = definingContentType.Id, - ParentTabContentTypes = new[] { definingContentType.Id }, - ParentTabContentTypeNames = new[] { definingContentType.Name } - }; - - groups.Add(group); - } - - // deal with generic properties - var genericProperties = new List(); - - // add generic properties local to this content type - var entityGenericProperties = source.PropertyTypes.Where(x => x.PropertyGroupId == null); - genericProperties.AddRange(MapProperties(entityGenericProperties, source, PropertyGroupBasic.GenericPropertiesGroupId, false)); - - // add generic properties inherited through compositions - var localGenericPropertyIds = genericProperties.Select(x => x.Id).ToArray(); - var compositionGenericProperties = source.CompositionPropertyTypes - .Where(x => x.PropertyGroupId == null // generic - && localGenericPropertyIds.Contains(x.Id) == false); // skip those that are local - foreach (var compositionGenericProperty in compositionGenericProperties) - { - var definingContentType = GetContentTypeForPropertyType(source, compositionGenericProperty.Id); - if (definingContentType == null) - throw new Exception("PropertyType with id=" + compositionGenericProperty.Id + " was not found on any of the content type's compositions."); - genericProperties.AddRange(MapProperties(new[] { compositionGenericProperty }, definingContentType, PropertyGroupBasic.GenericPropertiesGroupId, true)); - } - - // if there are any generic properties, add the corresponding tab - if (genericProperties.Any()) - { - var genericGroup = new PropertyGroupDisplay - { - Id = PropertyGroupBasic.GenericPropertiesGroupId, - Name = "Generic properties", - Alias = "genericProperties", - SortOrder = 999, - Properties = genericProperties, - ContentTypeId = source.Id - }; - - groups.Add(genericGroup); - } - - // handle locked properties - var lockedPropertyAliases = new List(); - // add built-in member property aliases to list of aliases to be locked - foreach (var propertyAlias in ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Keys) - { - lockedPropertyAliases.Add(propertyAlias); - } - // lock properties by aliases - foreach (var property in groups.SelectMany(x => x.Properties)) - { - if (property.Alias is not null) - { - property.Locked = lockedPropertyAliases.Contains(property.Alias); - } - } - - // now merge tabs based on alias - // as for one name, we might have one local tab, plus some inherited tabs - var groupsGroupsByAlias = groups.GroupBy(x => x.Alias).ToArray(); - groups = new List>(); // start with a fresh list - foreach (var groupsByAlias in groupsGroupsByAlias) - { - // single group, just use it - if (groupsByAlias.Count() == 1) - { - groups.Add(groupsByAlias.First()); - continue; - } - - // multiple groups, merge - var group = groupsByAlias.FirstOrDefault(x => x.Inherited == false) // try local - ?? groupsByAlias.First(); // else pick one randomly - groups.Add(group); - - // in case we use the local one, flag as inherited - group.Inherited = true; // TODO Remove to allow changing sort order of the local one (and use the inherited group order below) - - // merge (and sort) properties - var properties = groupsByAlias.SelectMany(x => x.Properties).OrderBy(x => x.SortOrder).ToArray(); - group.Properties = properties; - - // collect parent group info - var parentGroups = groupsByAlias.Where(x => x.ContentTypeId != source.Id).ToArray(); - group.ParentTabContentTypes = parentGroups.SelectMany(x => x.ParentTabContentTypes).ToArray(); - group.ParentTabContentTypeNames = parentGroups.SelectMany(x => x.ParentTabContentTypeNames).ToArray(); - } - - return groups.OrderBy(x => x.SortOrder); + groups.Add(genericGroup); } - private IEnumerable MapProperties(IEnumerable? properties, IContentTypeBase contentType, int groupId, bool inherited) + // handle locked properties + var lockedPropertyAliases = new List(); + + // add built-in member property aliases to list of aliases to be locked + foreach (var propertyAlias in ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Keys) { - var mappedProperties = new List(); + lockedPropertyAliases.Add(propertyAlias); + } - foreach (var p in properties?.Where(x => x.DataTypeId != 0).OrderBy(x => x.SortOrder) ?? Enumerable.Empty()) + // lock properties by aliases + foreach (TPropertyType property in groups.SelectMany(x => x.Properties)) + { + if (property.Alias is not null) { - var propertyEditorAlias = p.PropertyEditorAlias; - var propertyEditor = _propertyEditors[propertyEditorAlias]; - var dataType = _dataTypeService.GetDataType(p.DataTypeId); + property.Locked = lockedPropertyAliases.Contains(property.Alias); + } + } - //fixme: Don't explode if we can't find this, log an error and change this to a label - if (propertyEditor == null) - { - _logger.LogError("No property editor could be resolved with the alias: {PropertyEditorAlias}, defaulting to label", p.PropertyEditorAlias); - propertyEditorAlias = Constants.PropertyEditors.Aliases.Label; - propertyEditor = _propertyEditors[propertyEditorAlias]; - } - - var config = propertyEditor is null || dataType is null - ? new Dictionary() - : dataType.Editor?.GetConfigurationEditor().ToConfigurationEditor(dataType.Configuration); - - mappedProperties.Add(new TPropertyType - { - Id = p.Id, - Alias = p.Alias, - Description = p.Description, - LabelOnTop = p.LabelOnTop, - Editor = p.PropertyEditorAlias, - Validation = new PropertyTypeValidation - { - Mandatory = p.Mandatory, - MandatoryMessage = p.MandatoryMessage, - Pattern = p.ValidationRegExp, - PatternMessage = p.ValidationRegExpMessage, - }, - Label = p.Name, - View = propertyEditor?.GetValueEditor().View, - Config = config, - //Value = "", - GroupId = groupId, - Inherited = inherited, - DataTypeId = p.DataTypeId, - DataTypeKey = p.DataTypeKey, - DataTypeName = dataType?.Name, - DataTypeIcon = propertyEditor?.Icon, - SortOrder = p.SortOrder, - ContentTypeId = contentType.Id, - ContentTypeName = contentType.Name, - AllowCultureVariant = p.VariesByCulture(), - AllowSegmentVariant = p.VariesBySegment() - }); + // now merge tabs based on alias + // as for one name, we might have one local tab, plus some inherited tabs + IGrouping>[] groupsGroupsByAlias = + groups.GroupBy(x => x.Alias).ToArray(); + groups = new List>(); // start with a fresh list + foreach (IGrouping> groupsByAlias in groupsGroupsByAlias) + { + // single group, just use it + if (groupsByAlias.Count() == 1) + { + groups.Add(groupsByAlias.First()); + continue; } - return mappedProperties; + // multiple groups, merge + PropertyGroupDisplay group = + groupsByAlias.FirstOrDefault(x => x.Inherited == false) // try local + ?? groupsByAlias.First(); // else pick one randomly + groups.Add(group); + + // in case we use the local one, flag as inherited + group.Inherited = + true; // TODO Remove to allow changing sort order of the local one (and use the inherited group order below) + + // merge (and sort) properties + TPropertyType[] properties = + groupsByAlias.SelectMany(x => x.Properties).OrderBy(x => x.SortOrder).ToArray(); + group.Properties = properties; + + // collect parent group info + PropertyGroupDisplay[] parentGroups = + groupsByAlias.Where(x => x.ContentTypeId != source.Id).ToArray(); + group.ParentTabContentTypes = parentGroups.SelectMany(x => x.ParentTabContentTypes).ToArray(); + group.ParentTabContentTypeNames = parentGroups.SelectMany(x => x.ParentTabContentTypeNames).ToArray(); } + + return groups.OrderBy(x => x.SortOrder); + } + + /// + /// Gets the content type that defines a property group, within a composition. + /// + /// The composition. + /// The identifier of the property group. + /// The composition content type that defines the specified property group. + private static IContentTypeComposition? GetContentTypeForPropertyGroup( + IContentTypeComposition contentType, + int propertyGroupId) + { + // test local groups + if (contentType.PropertyGroups.Any(x => x.Id == propertyGroupId)) + { + return contentType; + } + + // test composition types groups + // .ContentTypeComposition is just the local ones, not recursive, + // so we have to recurse here + return contentType.ContentTypeComposition + .Select(x => GetContentTypeForPropertyGroup(x, propertyGroupId)) + .FirstOrDefault(x => x != null); + } + + /// + /// Gets the content type that defines a property group, within a composition. + /// + /// The composition. + /// The identifier of the property type. + /// The composition content type that defines the specified property group. + private static IContentTypeComposition? GetContentTypeForPropertyType( + IContentTypeComposition contentType, + int propertyTypeId) + { + // test local property types + if (contentType.PropertyTypes.Any(x => x.Id == propertyTypeId)) + { + return contentType; + } + + // test composition property types + // .ContentTypeComposition is just the local ones, not recursive, + // so we have to recurse here + return contentType.ContentTypeComposition + .Select(x => GetContentTypeForPropertyType(x, propertyTypeId)) + .FirstOrDefault(x => x != null); + } + + private IEnumerable MapProperties( + IEnumerable? properties, + IContentTypeBase contentType, + int groupId, + bool inherited) + { + var mappedProperties = new List(); + + foreach (IPropertyType p in properties?.Where(x => x.DataTypeId != 0).OrderBy(x => x.SortOrder) ?? + Enumerable.Empty()) + { + var propertyEditorAlias = p.PropertyEditorAlias; + IDataEditor? propertyEditor = _propertyEditors[propertyEditorAlias]; + IDataType? dataType = _dataTypeService.GetDataType(p.DataTypeId); + + // fixme: Don't explode if we can't find this, log an error and change this to a label + if (propertyEditor == null) + { + _logger.LogError( + "No property editor could be resolved with the alias: {PropertyEditorAlias}, defaulting to label", + p.PropertyEditorAlias); + propertyEditorAlias = Constants.PropertyEditors.Aliases.Label; + propertyEditor = _propertyEditors[propertyEditorAlias]; + } + + IDictionary? config = propertyEditor is null || dataType is null ? new Dictionary() + : dataType.Editor?.GetConfigurationEditor().ToConfigurationEditor(dataType.Configuration); + + mappedProperties.Add(new TPropertyType + { + Id = p.Id, + Alias = p.Alias, + Description = p.Description, + LabelOnTop = p.LabelOnTop, + Editor = p.PropertyEditorAlias, + Validation = new PropertyTypeValidation + { + Mandatory = p.Mandatory, + MandatoryMessage = p.MandatoryMessage, + Pattern = p.ValidationRegExp, + PatternMessage = p.ValidationRegExpMessage, + }, + Label = p.Name, + View = propertyEditor?.GetValueEditor().View, + Config = config, + + // Value = "", + GroupId = groupId, + Inherited = inherited, + DataTypeId = p.DataTypeId, + DataTypeKey = p.DataTypeKey, + DataTypeName = dataType?.Name, + DataTypeIcon = propertyEditor?.Icon, + SortOrder = p.SortOrder, + ContentTypeId = contentType.Id, + ContentTypeName = contentType.Name, + AllowCultureVariant = p.VariesByCulture(), + AllowSegmentVariant = p.VariesBySegment(), + }); + } + + return mappedProperties; } } diff --git a/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs index f4715b3a6b..148470c706 100644 --- a/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs @@ -1,32 +1,29 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class RedirectUrlMapDefinition : IMapDefinition { - public class RedirectUrlMapDefinition : IMapDefinition + private readonly IPublishedUrlProvider _publishedUrlProvider; + + public RedirectUrlMapDefinition(IPublishedUrlProvider publishedUrlProvider) => + _publishedUrlProvider = publishedUrlProvider; + + public void DefineMaps(IUmbracoMapper mapper) => + mapper.Define((source, context) => new ContentRedirectUrl(), Map); + + // Umbraco.Code.MapAll + private void Map(IRedirectUrl source, ContentRedirectUrl target, MapperContext context) { - private readonly IPublishedUrlProvider _publishedUrlProvider; - - public RedirectUrlMapDefinition(IPublishedUrlProvider publishedUrlProvider) - { - _publishedUrlProvider = publishedUrlProvider; - } - - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new ContentRedirectUrl(), Map); - } - - // Umbraco.Code.MapAll - private void Map(IRedirectUrl source, ContentRedirectUrl target, MapperContext context) - { - target.ContentId = source.ContentId; - target.CreateDateUtc = source.CreateDateUtc; - target.Culture = source.Culture; - target.DestinationUrl = source.ContentId > 0 ? _publishedUrlProvider?.GetUrl(source.ContentId, culture: source.Culture) : "#"; - target.OriginalUrl = _publishedUrlProvider?.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); - target.RedirectId = source.Key; - } + target.ContentId = source.ContentId; + target.CreateDateUtc = source.CreateDateUtc; + target.Culture = source.Culture; + target.DestinationUrl = source.ContentId > 0 + ? _publishedUrlProvider?.GetUrl(source.ContentId, culture: source.Culture) + : "#"; + target.OriginalUrl = _publishedUrlProvider?.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); + target.RedirectId = source.Key; } } diff --git a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs index b0aaab9537..d565836847 100644 --- a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs @@ -1,95 +1,96 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class RelationMapDefinition : IMapDefinition { - public class RelationMapDefinition : IMapDefinition + private readonly IEntityService _entityService; + private readonly IRelationService _relationService; + + public RelationMapDefinition(IEntityService entityService, IRelationService relationService) { - private readonly IEntityService _entityService; - private readonly IRelationService _relationService; + _entityService = entityService; + _relationService = relationService; + } - public RelationMapDefinition(IEntityService entityService, IRelationService relationService) + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new RelationTypeDisplay(), Map); + mapper.Define((source, context) => new RelationDisplay(), Map); + mapper.Define(Map); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + private static void Map(RelationTypeSave source, IRelationType target, MapperContext context) + { + target.Alias = source.Alias; + target.ChildObjectType = source.ChildObjectType; + target.Id = source.Id.TryConvertTo().Result; + target.IsBidirectional = source.IsBidirectional; + if (target is IRelationTypeWithIsDependency targetWithIsDependency) { - _entityService = entityService; - _relationService = relationService; + targetWithIsDependency.IsDependency = source.IsDependency; } - public void DefineMaps(IUmbracoMapper mapper) + target.Key = source.Key; + target.Name = source.Name; + target.ParentObjectType = source.ParentObjectType; + } + + // Umbraco.Code.MapAll -Icon -Trashed -AdditionalData + // Umbraco.Code.MapAll -ParentId -Notifications + private void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) + { + target.ChildObjectType = source.ChildObjectType; + target.Id = source.Id; + target.IsBidirectional = source.IsBidirectional; + + if (source is IRelationTypeWithIsDependency sourceWithIsDependency) { - mapper.Define((source, context) => new RelationTypeDisplay(), Map); - mapper.Define((source, context) => new RelationDisplay(), Map); - mapper.Define(Map); + target.IsDependency = sourceWithIsDependency.IsDependency; } - // Umbraco.Code.MapAll -Icon -Trashed -AdditionalData - // Umbraco.Code.MapAll -ParentId -Notifications - private void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) + target.Key = source.Key; + target.Name = source.Name; + target.Alias = source.Alias; + target.ParentObjectType = source.ParentObjectType; + target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); + target.Path = "-1," + source.Id; + + target.IsSystemRelationType = source.IsSystemRelationType(); + + // Set the "friendly" and entity names for the parent and child object types + if (source.ParentObjectType.HasValue) { - target.ChildObjectType = source.ChildObjectType; - target.Id = source.Id; - target.IsBidirectional = source.IsBidirectional; - - if (source is IRelationTypeWithIsDependency sourceWithIsDependency) - { - target.IsDependency = sourceWithIsDependency.IsDependency; - } - target.Key = source.Key; - target.Name = source.Name; - target.Alias = source.Alias; - target.ParentObjectType = source.ParentObjectType; - target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); - target.Path = "-1," + source.Id; - - target.IsSystemRelationType = source.IsSystemRelationType(); - - // Set the "friendly" and entity names for the parent and child object types - if (source.ParentObjectType.HasValue) - { - var objType = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value); - target.ParentObjectTypeName = objType.GetFriendlyName(); - } - - if (source.ChildObjectType.HasValue) - { - var objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); - target.ChildObjectTypeName = objType.GetFriendlyName(); - } + UmbracoObjectTypes objType = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value); + target.ParentObjectTypeName = objType.GetFriendlyName(); } - // Umbraco.Code.MapAll -ParentName -ChildName - private void Map(IRelation source, RelationDisplay target, MapperContext context) + if (source.ChildObjectType.HasValue) { - target.ChildId = source.ChildId; - target.Comment = source.Comment; - target.CreateDate = source.CreateDate; - target.ParentId = source.ParentId; - - var entities = _relationService.GetEntitiesFromRelation(source); - - if (entities is not null) - { - target.ParentName = entities.Item1.Name; - target.ChildName = entities.Item2.Name; - } + UmbracoObjectTypes objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); + target.ChildObjectTypeName = objType.GetFriendlyName(); } + } - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - private static void Map(RelationTypeSave source, IRelationType target, MapperContext context) + // Umbraco.Code.MapAll -ParentName -ChildName + private void Map(IRelation source, RelationDisplay target, MapperContext context) + { + target.ChildId = source.ChildId; + target.Comment = source.Comment; + target.CreateDate = source.CreateDate; + target.ParentId = source.ParentId; + + Tuple? entities = _relationService.GetEntitiesFromRelation(source); + + if (entities is not null) { - target.Alias = source.Alias; - target.ChildObjectType = source.ChildObjectType; - target.Id = source.Id.TryConvertTo().Result; - target.IsBidirectional = source.IsBidirectional; - if (target is IRelationTypeWithIsDependency targetWithIsDependency) - { - targetWithIsDependency.IsDependency = source.IsDependency; - } - - target.Key = source.Key; - target.Name = source.Name; - target.ParentObjectType = source.ParentObjectType; + target.ParentName = entities.Item1.Name; + target.ChildName = entities.Item2.Name; } } } diff --git a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs index b7bdbccd26..c64af5ac0a 100644 --- a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs @@ -1,48 +1,45 @@ -using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Sections; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class SectionMapDefinition : IMapDefinition { - public class SectionMapDefinition : IMapDefinition + private readonly ILocalizedTextService _textService; + + public SectionMapDefinition(ILocalizedTextService textService) => _textService = textService; + + public void DefineMaps(IUmbracoMapper mapper) { - private readonly ILocalizedTextService _textService; - public SectionMapDefinition(ILocalizedTextService textService) - { - _textService = textService; - } + mapper.Define((source, context) => new Section(), Map); - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new Section(), Map); + // this is for AutoMapper ReverseMap - but really? + mapper.Define(); + mapper.Define(); + mapper.Define(Map); + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + } - // this is for AutoMapper ReverseMap - but really? - mapper.Define(); - mapper.Define(); - mapper.Define(Map); - mapper.Define(); - mapper.Define(); - mapper.Define(); - mapper.Define(); - mapper.Define(); - mapper.Define(); - } + // Umbraco.Code.MapAll + private static void Map(Section source, ManifestSection target, MapperContext context) + { + target.Alias = source.Alias; + target.Name = source.Name; + } - // Umbraco.Code.MapAll -RoutePath - private void Map(ISection source, Section target, MapperContext context) - { - target.Alias = source.Alias; - target.Name = _textService.Localize("sections", source.Alias); - } - - // Umbraco.Code.MapAll - private static void Map(Section source, ManifestSection target, MapperContext context) - { - target.Alias = source.Alias; - target.Name = source.Name; - } + // Umbraco.Code.MapAll -RoutePath + private void Map(ISection source, Section target, MapperContext context) + { + target.Alias = source.Alias; + target.Name = _textService.Localize("sections", source.Alias); } } diff --git a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs index be4b6bae61..42ea05e8f9 100644 --- a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs @@ -1,159 +1,156 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public abstract class TabsAndPropertiesMapper { - public abstract class TabsAndPropertiesMapper + protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService) + : this(cultureDictionary, localizedTextService, new List()) { - protected ICultureDictionary CultureDictionary { get; } - protected ILocalizedTextService LocalizedTextService { get; } - protected IEnumerable IgnoreProperties { get; set; } + } - protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService) - : this(cultureDictionary, localizedTextService, new List()) - { } + protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) + { + CultureDictionary = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary)); + LocalizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + IgnoreProperties = ignoreProperties ?? throw new ArgumentNullException(nameof(ignoreProperties)); + } - protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) + protected ICultureDictionary CultureDictionary { get; } + + protected ILocalizedTextService LocalizedTextService { get; } + + protected IEnumerable IgnoreProperties { get; set; } + + /// + /// Returns a collection of custom generic properties that exist on the generic properties tab + /// + /// + protected virtual IEnumerable GetCustomGenericProperties(IContentBase content) => + Enumerable.Empty(); + + /// + /// Maps properties on to the generic properties tab + /// + /// + /// + /// + /// + /// The generic properties tab is responsible for + /// setting up the properties such as Created date, updated date, template selected, etc... + /// + protected virtual void MapGenericProperties(IContentBase content, List> tabs, MapperContext context) + { + // add the generic properties tab, for properties that don't belong to a tab + // get the properties, map and translate them, then add the tab + var noGroupProperties = content.GetNonGroupedProperties() + .Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored + .ToList(); + List genericProperties = MapProperties(content, noGroupProperties, context); + + IEnumerable customProperties = GetCustomGenericProperties(content); + if (customProperties != null) { - CultureDictionary = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary)); - LocalizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); - IgnoreProperties = ignoreProperties ?? throw new ArgumentNullException(nameof(ignoreProperties)); + genericProperties.AddRange(customProperties); } - /// - /// Returns a collection of custom generic properties that exist on the generic properties tab - /// - /// - protected virtual IEnumerable GetCustomGenericProperties(IContentBase content) + if (genericProperties.Count > 0) { - return Enumerable.Empty(); - } - - /// - /// Maps properties on to the generic properties tab - /// - /// - /// - /// - /// - /// The generic properties tab is responsible for - /// setting up the properties such as Created date, updated date, template selected, etc... - /// - protected virtual void MapGenericProperties(IContentBase content, List> tabs, MapperContext context) - { - // add the generic properties tab, for properties that don't belong to a tab - // get the properties, map and translate them, then add the tab - var noGroupProperties = content.GetNonGroupedProperties() - .Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored - .ToList(); - var genericProperties = MapProperties(content, noGroupProperties, context); - - - - var customProperties = GetCustomGenericProperties(content); - if (customProperties != null) + tabs.Add(new Tab { - genericProperties.AddRange(customProperties); - } - - if (genericProperties.Count > 0) - { - tabs.Add(new Tab - { - Id = 0, - Label = LocalizedTextService.Localize("general", "properties"), - Alias = "Generic properties", - Properties = genericProperties - }); - } - } - - /// - /// Maps a list of to a list of - /// - /// - /// - /// - /// - protected virtual List MapProperties(IContentBase content, List properties, MapperContext context) - { - return context.MapEnumerable(properties.OrderBy(x => x.PropertyType?.SortOrder)).WhereNotNull().ToList(); + Id = 0, + Label = LocalizedTextService.Localize("general", "properties"), + Alias = "Generic properties", + Properties = genericProperties, + }); } } /// - /// Creates the tabs collection with properties assigned for display models + /// Maps a list of to a list of /// - public class TabsAndPropertiesMapper : TabsAndPropertiesMapper - where TSource : IContentBase + /// + /// + /// + /// + protected virtual List MapProperties(IContentBase content, List properties, MapperContext context) => + context.MapEnumerable(properties.OrderBy(x => x.PropertyType?.SortOrder)) + .WhereNotNull().ToList(); +} + +/// +/// Creates the tabs collection with properties assigned for display models +/// +public class TabsAndPropertiesMapper : TabsAndPropertiesMapper + where TSource : IContentBase +{ + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + + public TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + : base(cultureDictionary, localizedTextService) => + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? + throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); + + public virtual IEnumerable> Map(TSource source, MapperContext context) { - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + var tabs = new List>(); - public TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) - : base(cultureDictionary, localizedTextService) + // Property groups only exist on the content type (as it's only used for display purposes) + IContentTypeComposition? contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); + + // Merge the groups, as compositions can introduce duplicate aliases + PropertyGroup[]? groups = contentType?.CompositionPropertyGroups.OrderBy(x => x.SortOrder).ToArray(); + var parentAliases = groups?.Select(x => x.GetParentAlias()).Distinct().ToArray(); + if (groups is not null) { - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); - } - - public virtual IEnumerable> Map(TSource source, MapperContext context) - { - var tabs = new List>(); - - // Property groups only exist on the content type (as it's only used for display purposes) - var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); - - // Merge the groups, as compositions can introduce duplicate aliases - var groups = contentType?.CompositionPropertyGroups.OrderBy(x => x.SortOrder).ToArray(); - var parentAliases = groups?.Select(x => x.GetParentAlias()).Distinct().ToArray(); - if (groups is not null) + foreach (IGrouping groupsByAlias in groups.GroupBy(x => x.Alias)) { - foreach (var groupsByAlias in groups.GroupBy(x => x.Alias)) + var properties = new List(); + + // Merge properties for groups with the same alias + foreach (PropertyGroup group in groupsByAlias) { - var properties = new List(); + IEnumerable groupProperties = source.GetPropertiesForGroup(group) + .Where(x => IgnoreProperties.Contains(x.Alias) == false); // Skip ignored properties - // Merge properties for groups with the same alias - foreach (var group in groupsByAlias) - { - var groupProperties = source.GetPropertiesForGroup(group) - .Where(x => IgnoreProperties.Contains(x.Alias) == false); // Skip ignored properties - - properties.AddRange(groupProperties); - } - - if (properties.Count == 0 && (!parentAliases?.Contains(groupsByAlias.Key) ?? false)) - continue; - - // Map the properties - var mappedProperties = MapProperties(source, properties, context); - - // Add the tab (the first is closest to the content type, e.g. local, then direct composition) - var g = groupsByAlias.First(); - - tabs.Add(new Tab - { - Id = g.Id, - Key = g.Key, - Type = g.Type.ToString(), - Alias = g.Alias, - Label = LocalizedTextService.UmbracoDictionaryTranslate(CultureDictionary, g.Name), - Properties = mappedProperties - }); + properties.AddRange(groupProperties); } + + if (properties.Count == 0 && (!parentAliases?.Contains(groupsByAlias.Key) ?? false)) + { + continue; + } + + // Map the properties + List mappedProperties = MapProperties(source, properties, context); + + // Add the tab (the first is closest to the content type, e.g. local, then direct composition) + PropertyGroup g = groupsByAlias.First(); + + tabs.Add(new Tab + { + Id = g.Id, + Key = g.Key, + Type = g.Type.ToString(), + Alias = g.Alias, + Label = LocalizedTextService.UmbracoDictionaryTranslate(CultureDictionary, g.Name), + Properties = mappedProperties, + }); } - - MapGenericProperties(source, tabs, context); - - // Activate the first tab, if any - if (tabs.Count > 0) - tabs[0].IsActive = true; - - return tabs; } + + MapGenericProperties(source, tabs, context); + + // Activate the first tab, if any + if (tabs.Count > 0) + { + tabs[0].IsActive = true; + } + + return tabs; } } diff --git a/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs index 7bd436fa54..f9c1690c6a 100644 --- a/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs @@ -1,21 +1,18 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class TagMapDefinition : IMapDefinition { - public class TagMapDefinition : IMapDefinition - { - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new TagModel(), Map); - } + public void DefineMaps(IUmbracoMapper mapper) => + mapper.Define((source, context) => new TagModel(), Map); - // Umbraco.Code.MapAll - private static void Map(ITag source, TagModel target, MapperContext context) - { - target.Id = source.Id; - target.Text = source.Text; - target.Group = source.Group; - target.NodeCount = source.NodeCount; - } + // Umbraco.Code.MapAll + private static void Map(ITag source, TagModel target, MapperContext context) + { + target.Id = source.Id; + target.Text = source.Text; + target.Group = source.Group; + target.NodeCount = source.NodeCount; } } diff --git a/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs index 624868f3f4..5afc8bc8d7 100644 --- a/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs @@ -1,50 +1,47 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class TemplateMapDefinition : IMapDefinition { - public class TemplateMapDefinition : IMapDefinition + private readonly IShortStringHelper _shortStringHelper; + + public TemplateMapDefinition(IShortStringHelper shortStringHelper) => _shortStringHelper = shortStringHelper; + + public void DefineMaps(IUmbracoMapper mapper) { - private readonly IShortStringHelper _shortStringHelper; + mapper.Define((source, context) => new TemplateDisplay(), Map); + mapper.Define( + (source, context) => new Template(_shortStringHelper, source.Name, source.Alias), Map); + } - public TemplateMapDefinition(IShortStringHelper shortStringHelper) - { - _shortStringHelper = shortStringHelper; - } + // Umbraco.Code.MapAll + private static void Map(ITemplate source, TemplateDisplay target, MapperContext context) + { + target.Id = source.Id; + target.Name = source.Name; + target.Alias = source.Alias; + target.Key = source.Key; + target.Content = source.Content; + target.Path = source.Path; + target.VirtualPath = source.VirtualPath; + target.MasterTemplateAlias = source.MasterTemplateAlias; + target.IsMasterTemplate = source.IsMasterTemplate; + } - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new TemplateDisplay(), Map); - mapper.Define((source, context) => new Template(_shortStringHelper, source.Name, source.Alias), Map); - } - - // Umbraco.Code.MapAll - private static void Map(ITemplate source, TemplateDisplay target, MapperContext context) - { - target.Id = source.Id; - target.Name = source.Name; - target.Alias = source.Alias; - target.Key = source.Key; - target.Content = source.Content; - target.Path = source.Path; - target.VirtualPath = source.VirtualPath; - target.MasterTemplateAlias = source.MasterTemplateAlias; - target.IsMasterTemplate = source.IsMasterTemplate; - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -Path -VirtualPath -MasterTemplateId -IsMasterTemplate - // Umbraco.Code.MapAll -GetFileContent - private static void Map(TemplateDisplay source, ITemplate target, MapperContext context) - { - // don't need to worry about mapping MasterTemplateAlias here; - // the template controller handles any changes made to the master template - target.Name = source.Name; - target.Alias = source.Alias; - target.Content = source.Content; - target.Id = source.Id; - target.Key = source.Key; - } + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -Path -VirtualPath -MasterTemplateId -IsMasterTemplate + // Umbraco.Code.MapAll -GetFileContent + private static void Map(TemplateDisplay source, ITemplate target, MapperContext context) + { + // don't need to worry about mapping MasterTemplateAlias here; + // the template controller handles any changes made to the master template + target.Name = source.Name; + target.Alias = source.Alias; + target.Content = source.Content; + target.Id = source.Id; + target.Key = source.Key; } } diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs index a2c3fa7f28..9174ee7004 100644 --- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; @@ -15,450 +14,557 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Sections; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; +using UserProfile = Umbraco.Cms.Core.Models.ContentEditing.UserProfile; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class UserMapDefinition : IMapDefinition { - public class UserMapDefinition : IMapDefinition + private readonly ActionCollection _actions; + private readonly AppCaches _appCaches; + private readonly IEntityService _entityService; + private readonly GlobalSettings _globalSettings; + private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly MediaFileManager _mediaFileManager; + private readonly ISectionService _sectionService; + private readonly IShortStringHelper _shortStringHelper; + private readonly ILocalizedTextService _textService; + private readonly IUserService _userService; + private readonly ILocalizationService _localizationService; + + public UserMapDefinition( + ILocalizedTextService textService, + IUserService userService, + IEntityService entityService, + ISectionService sectionService, + AppCaches appCaches, + ActionCollection actions, + IOptions globalSettings, + MediaFileManager mediaFileManager, + IShortStringHelper shortStringHelper, + IImageUrlGenerator imageUrlGenerator, + ILocalizationService localizationService) { - private readonly ISectionService _sectionService; - private readonly IEntityService _entityService; - private readonly IUserService _userService; - private readonly ILocalizedTextService _textService; - private readonly ActionCollection _actions; - private readonly AppCaches _appCaches; - private readonly GlobalSettings _globalSettings; - private readonly MediaFileManager _mediaFileManager; - private readonly IShortStringHelper _shortStringHelper; - private readonly IImageUrlGenerator _imageUrlGenerator; + _sectionService = sectionService; + _entityService = entityService; + _userService = userService; + _textService = textService; + _actions = actions; + _appCaches = appCaches; + _globalSettings = globalSettings.Value; + _mediaFileManager = mediaFileManager; + _shortStringHelper = shortStringHelper; + _imageUrlGenerator = imageUrlGenerator; + _localizationService = localizationService; + } - public UserMapDefinition(ILocalizedTextService textService, IUserService userService, IEntityService entityService, ISectionService sectionService, - AppCaches appCaches, ActionCollection actions, IOptions globalSettings, MediaFileManager mediaFileManager, IShortStringHelper shortStringHelper, - IImageUrlGenerator imageUrlGenerator) + [Obsolete("Please use constructor that takes an ILocalizationService instead")] + public UserMapDefinition( + ILocalizedTextService textService, + IUserService userService, + IEntityService entityService, + ISectionService sectionService, + AppCaches appCaches, + ActionCollection actions, + IOptions globalSettings, + MediaFileManager mediaFileManager, + IShortStringHelper shortStringHelper, + IImageUrlGenerator imageUrlGenerator) + : this( + textService, + userService, + entityService, + sectionService, + appCaches, + actions, + globalSettings, + mediaFileManager, + shortStringHelper, + imageUrlGenerator, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define( + (source, context) => new UserGroup(_shortStringHelper) { CreateDate = DateTime.UtcNow }, Map); + mapper.Define(Map); + mapper.Define((source, context) => new UserProfile(), Map); + mapper.Define((source, context) => new UserGroupBasic(), Map); + mapper.Define((source, context) => new UserGroupBasic(), Map); + mapper.Define( + (source, context) => new AssignedUserGroupPermissions(), + Map); + mapper.Define( + (source, context) => new AssignedContentPermissions(), + Map); + mapper.Define((source, context) => new UserGroupDisplay(), Map); + mapper.Define((source, context) => new UserBasic(), Map); + mapper.Define((source, context) => new UserDetail(), Map); + + // used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! + mapper.Define(Map); + + // important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. + mapper.Define((source, context) => new UserDisplay(), Map); + } + + // mappers + private static void Map(UserGroupSave source, IUserGroup target, MapperContext context) + { + if (!(target is UserGroup ttarget)) { - _sectionService = sectionService; - _entityService = entityService; - _userService = userService; - _textService = textService; - _actions = actions; - _appCaches = appCaches; - _globalSettings = globalSettings.Value; - _mediaFileManager = mediaFileManager; - _shortStringHelper = shortStringHelper; - _imageUrlGenerator = imageUrlGenerator; + throw new NotSupportedException($"{nameof(target)} must be a UserGroup."); } - public void DefineMaps(IUmbracoMapper mapper) + Map(source, ttarget); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + private static void Map(UserGroupSave source, UserGroup target) + { + target.StartMediaId = source.StartMediaId; + target.StartContentId = source.StartContentId; + target.Icon = source.Icon; + target.Alias = source.Alias; + target.Name = source.Name; + target.Permissions = source.DefaultPermissions; + target.Key = source.Key; + target.HasAccessToAllLanguages = source.HasAccessToAllLanguages; + + var id = GetIntId(source.Id); + if (id > 0) { - mapper.Define((source, context) => new UserGroup(_shortStringHelper) { CreateDate = DateTime.UtcNow }, Map); - mapper.Define(Map); - mapper.Define((source, context) => new ContentEditing.UserProfile(), Map); - mapper.Define((source, context) => new UserGroupBasic(), Map); - mapper.Define((source, context) => new UserGroupBasic(), Map); - mapper.Define((source, context) => new AssignedUserGroupPermissions(), Map); - mapper.Define((source, context) => new AssignedContentPermissions(), Map); - mapper.Define((source, context) => new UserGroupDisplay(), Map); - mapper.Define((source, context) => new UserBasic(), Map); - mapper.Define((source, context) => new UserDetail(), Map); - - // used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! - mapper.Define(Map); - - // important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - mapper.Define((source, context) => new UserDisplay(), Map); + target.Id = id; } - // mappers - - private static void Map(UserGroupSave source, IUserGroup target, MapperContext context) + target.ClearAllowedSections(); + if (source.Sections is not null) { - if (!(target is UserGroup ttarget)) - throw new NotSupportedException($"{nameof(target)} must be a UserGroup."); - Map(source, ttarget); - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - private static void Map(UserGroupSave source, UserGroup target) - { - target.StartMediaId = source.StartMediaId; - target.StartContentId = source.StartContentId; - target.Icon = source.Icon; - target.Alias = source.Alias; - target.Name = source.Name; - target.Permissions = source.DefaultPermissions; - target.Key = source.Key; - - var id = GetIntId(source.Id); - if (id > 0) - target.Id = id; - - target.ClearAllowedSections(); - if (source.Sections is not null) + foreach (var section in source.Sections) { - foreach (var section in source.Sections) - { - target.AddAllowedSection(section); - } + target.AddAllowedSection(section); } - } - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -Id -TourData -StartContentIds -StartMediaIds -Language -Username - // Umbraco.Code.MapAll -PasswordQuestion -SessionTimeout -EmailConfirmedDate -InvitedDate - // Umbraco.Code.MapAll -SecurityStamp -Avatar -ProviderUserKey -RawPasswordValue - // Umbraco.Code.MapAll -RawPasswordAnswerValue -Comments -IsApproved -IsLockedOut -LastLoginDate - // Umbraco.Code.MapAll -LastPasswordChangeDate -LastLockoutDate -FailedPasswordAttempts - // Umbraco.Code.MapAll -PasswordConfiguration - private void Map(UserInvite source, IUser target, MapperContext context) + target.ClearAllowedLanguages(); + if (source.AllowedLanguages is not null) { - target.Email = source.Email; - target.Key = source.Key; - target.Name = source.Name; - target.IsApproved = false; - - target.ClearGroups(); - var groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); - foreach (var group in groups) - target.AddGroup(group.ToReadOnlyGroup()); - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -TourData -SessionTimeout -EmailConfirmedDate -InvitedDate -SecurityStamp -Avatar - // Umbraco.Code.MapAll -ProviderUserKey -RawPasswordValue -RawPasswordAnswerValue -PasswordQuestion -Comments - // Umbraco.Code.MapAll -IsApproved -IsLockedOut -LastLoginDate -LastPasswordChangeDate -LastLockoutDate - // Umbraco.Code.MapAll -FailedPasswordAttempts - // Umbraco.Code.MapAll -PasswordConfiguration - private void Map(UserSave source, IUser target, MapperContext context) - { - target.Name = source.Name; - target.StartContentIds = source.StartContentIds ?? Array.Empty(); - target.StartMediaIds = source.StartMediaIds ?? Array.Empty(); - target.Language = source.Culture; - target.Email = source.Email; - target.Key = source.Key; - target.Username = source.Username; - target.Id = source.Id; - - target.ClearGroups(); - var groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); - foreach (var group in groups) - target.AddGroup(group.ToReadOnlyGroup()); - } - - // Umbraco.Code.MapAll - private static void Map(IProfile source, ContentEditing.UserProfile target, MapperContext context) - { - target.Name = source.Name; - target.UserId = source.Id; - } - - // Umbraco.Code.MapAll -ContentStartNode -UserCount -MediaStartNode -Key -Sections - // Umbraco.Code.MapAll -Notifications -Udi -Trashed -AdditionalData -IsSystemUserGroup - private void Map(IReadOnlyUserGroup source, UserGroupBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Id = source.Id; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.IsSystemUserGroup = source.IsSystemUserGroup(); - - MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); - } - - // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications - // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -IsSystemUserGroup - private void Map(IUserGroup source, UserGroupBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.UserCount = source.UserCount; - target.IsSystemUserGroup = source.IsSystemUserGroup(); - - MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); - } - - // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -AssignedPermissions - private void Map(IUserGroup source, AssignedUserGroupPermissions target, MapperContext context) - { - target.Id = source.Id; - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - - target.DefaultPermissions = MapUserGroupDefaultPermissions(source); - - if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = Constants.Icons.UserGroup; - } - - // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions - private static void Map(EntitySlim source, AssignedContentPermissions target, MapperContext context) - { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - - if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) - target.Icon = Constants.Icons.Member; - } - - // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications -Udi - // Umbraco.Code.MapAll -Trashed -AdditionalData -Users -AssignedPermissions - private void Map(IUserGroup source, UserGroupDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.DefaultPermissions = MapUserGroupDefaultPermissions(source); - target.Icon = source.Icon; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.UserCount = source.UserCount; - target.IsSystemUserGroup = source.IsSystemUserGroup(); - - MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); - - //Important! Currently we are never mapping to multiple UserGroupDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - var users = _userService.GetAllInGroup(source.Id); - target.Users = context.MapEnumerable(users).WhereNotNull(); - - //Deal with assigned permissions: - - var allContentPermissions = _userService.GetPermissions(source, true) - .ToDictionary(x => x.EntityId, x => x); - - IEntitySlim[] contentEntities; - if (allContentPermissions.Keys.Count == 0) + foreach (var language in source.AllowedLanguages) { - contentEntities = Array.Empty(); + target.AddAllowedLanguage(language); } - else - { - // a group can end up with way more than 2000 assigned permissions, - // so we need to break them into groups in order to avoid breaking - // the entity service due to too many Sql parameters. - - var list = new List(); - foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(Constants.Sql.MaxParameterCount)) - list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); - contentEntities = list.ToArray(); - } - - var allAssignedPermissions = new List(); - foreach (var entity in contentEntities) - { - var contentPermissions = allContentPermissions[entity.Id]; - - var assignedContentPermissions = context.Map(entity); - if (assignedContentPermissions is null) - { - continue; - } - assignedContentPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(target.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in assignedContentPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = contentPermissions.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - allAssignedPermissions.Add(assignedContentPermissions); - } - - target.AssignedPermissions = allAssignedPermissions; - } - - // Umbraco.Code.MapAll -Notifications -Udi -Icon -IsCurrentUser -Trashed -ResetPasswordValue - // Umbraco.Code.MapAll -Alias -AdditionalData - private void Map(IUser source, UserDisplay target, MapperContext context) - { - target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); - target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); - target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService,_appCaches), UmbracoObjectTypes.Document, "content","contentRoot", context); - target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media","mediaRoot", context); - target.CreateDate = source.CreateDate; - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); - target.Email = source.Email; - target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); - target.FailedPasswordAttempts = source.FailedPasswordAttempts; - target.Id = source.Id; - target.Key = source.Key; - target.LastLockoutDate = source.LastLockoutDate; - target.LastLoginDate = source.LastLoginDate == default(DateTime) ? null : (DateTime?)source.LastLoginDate; - target.LastPasswordChangeDate = source.LastPasswordChangeDate; - target.Name = source.Name; - target.Navigation = CreateUserEditorNavigation(); - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.StartContentIds = GetStartNodes(source.StartContentIds?.ToArray(), UmbracoObjectTypes.Document, "content","contentRoot", context); - target.StartMediaIds = GetStartNodes(source.StartMediaIds?.ToArray(), UmbracoObjectTypes.Media, "media","mediaRoot", context); - target.UpdateDate = source.UpdateDate; - target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); - target.Username = source.Username; - target.UserState = source.UserState; - } - - // Umbraco.Code.MapAll -Notifications -IsCurrentUser -Udi -Icon -Trashed -Alias -AdditionalData - private void Map(IUser source, UserBasic target, MapperContext context) - { - //Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost - //Alternatively, if this is annoying the back office UI would need to be updated to request the avatars for the list of users separately so it doesn't look - //like the load time is waiting. - target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); - target.Email = source.Email; - target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); - target.Id = source.Id; - target.Key = source.Key; - target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?)source.LastLoginDate; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); - target.Username = source.Username; - target.UserState = source.UserState; - } - - // Umbraco.Code.MapAll -SecondsUntilTimeout - private void Map(IUser source, UserDetail target, MapperContext context) - { - target.AllowedSections = source.AllowedSections; - target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); - target.Email = source.Email; - target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); - target.Name = source.Name; - target.StartContentIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); - target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); - target.UserId = source.Id; - - //we need to map the legacy UserType - //the best we can do here is to return the user's first user group as a IUserType object - //but we should attempt to return any group that is the built in ones first - target.UserGroups = source.Groups.Select(x => x.Alias).ToArray(); - } - - // helpers - - private void MapUserGroupBasic(UserGroupBasic target, IEnumerable sourceAllowedSections, int? sourceStartContentId, int? sourceStartMediaId, MapperContext context) - { - var allSections = _sectionService.GetSections(); - target.Sections = context.MapEnumerable(allSections.Where(x => sourceAllowedSections.Contains(x.Alias))).WhereNotNull(); - - if (sourceStartMediaId > 0) - target.MediaStartNode = context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); - else if (sourceStartMediaId == -1) - target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot")); - - if (sourceStartContentId > 0) - target.ContentStartNode = context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); - else if (sourceStartContentId == -1) - target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot")); - - if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = Constants.Icons.UserGroup; - } - - private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) - { - Permission GetPermission(IAction action) - => new Permission - { - Category = action.Category.IsNullOrWhiteSpace() - ? _textService.Localize("actionCategories",Constants.Conventions.PermissionCategories.OtherCategory) - : _textService.Localize("actionCategories", action.Category), - Name = _textService.Localize("actions", action.Alias), - Description = _textService.Localize("actionDescriptions", action.Alias), - Icon = action.Icon, - Checked = source.Permissions != null && source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), - PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture) - }; - - return _actions - .Where(x => x.CanBePermissionAssigned) - .Select(GetPermission) - .GroupBy(x => x.Category) - .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); - } - - private static string? MapContentTypeIcon(IEntitySlim entity) - => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; - - private IEnumerable GetStartNodes(int[]? startNodeIds, UmbracoObjectTypes objectType, string localizedArea,string localizedAlias, MapperContext context) - { - if (startNodeIds is null || startNodeIds.Length <= 0) - return Enumerable.Empty(); - - var startNodes = new List(); - if (startNodeIds.Contains(-1)) - startNodes.Add(CreateRootNode(_textService.Localize(localizedArea, localizedAlias))); - - var mediaItems = _entityService.GetAll(objectType, startNodeIds); - startNodes.AddRange(context.MapEnumerable(mediaItems).WhereNotNull()); - return startNodes; - } - - private IEnumerable CreateUserEditorNavigation() - { - return new[] - { - new EditorNavigation - { - Active = true, - Alias = "details", - Icon = "icon-umb-users", - Name = _textService.Localize("general","user"), - View = "views/users/views/user/details.html" - } - }; - } - - private static int GetIntId(object? id) - { - if (id is string strId && int.TryParse(strId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt)) - { - return asInt; - } - var result = id.TryConvertTo(); - if (result.Success == false) - { - throw new InvalidOperationException( - "Cannot convert the profile to a " + typeof(UserDetail).Name + " object since the id is not an integer"); - } - return result.Result; - } - - private EntityBasic CreateRootNode(string name) - { - return new EntityBasic - { - Name = name, - Path = "-1", - Icon = "icon-folder", - Id = -1, - Trashed = false, - ParentId = -1 - }; } } + + // Umbraco.Code.MapAll + private static void Map(IProfile source, UserProfile target, MapperContext context) + { + target.Name = source.Name; + target.UserId = source.Id; + } + + // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions + private static void Map(EntitySlim source, AssignedContentPermissions target, MapperContext context) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) + { + target.Icon = Constants.Icons.Member; + } + } + + private static string? MapContentTypeIcon(IEntitySlim entity) + => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + + private static int GetIntId(object? id) + { + if (id is string strId && + int.TryParse(strId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt)) + { + return asInt; + } + + Attempt result = id.TryConvertTo(); + if (result.Success == false) + { + throw new InvalidOperationException( + "Cannot convert the profile to a " + typeof(UserDetail).Name + + " object since the id is not an integer"); + } + + return result.Result; + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -Id -TourData -StartContentIds -StartMediaIds -Language -Username + // Umbraco.Code.MapAll -PasswordQuestion -SessionTimeout -EmailConfirmedDate -InvitedDate + // Umbraco.Code.MapAll -SecurityStamp -Avatar -ProviderUserKey -RawPasswordValue + // Umbraco.Code.MapAll -RawPasswordAnswerValue -Comments -IsApproved -IsLockedOut -LastLoginDate + // Umbraco.Code.MapAll -LastPasswordChangeDate -LastLockoutDate -FailedPasswordAttempts + // Umbraco.Code.MapAll -PasswordConfiguration + private void Map(UserInvite source, IUser target, MapperContext context) + { + target.Email = source.Email; + target.Key = source.Key; + target.Name = source.Name; + target.IsApproved = false; + + target.ClearGroups(); + IEnumerable groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); + foreach (IUserGroup group in groups) + { + target.AddGroup(group.ToReadOnlyGroup()); + } + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -TourData -SessionTimeout -EmailConfirmedDate -InvitedDate -SecurityStamp -Avatar + // Umbraco.Code.MapAll -ProviderUserKey -RawPasswordValue -RawPasswordAnswerValue -PasswordQuestion -Comments + // Umbraco.Code.MapAll -IsApproved -IsLockedOut -LastLoginDate -LastPasswordChangeDate -LastLockoutDate + // Umbraco.Code.MapAll -FailedPasswordAttempts + // Umbraco.Code.MapAll -PasswordConfiguration + private void Map(UserSave source, IUser target, MapperContext context) + { + target.Name = source.Name; + target.StartContentIds = source.StartContentIds ?? Array.Empty(); + target.StartMediaIds = source.StartMediaIds ?? Array.Empty(); + target.Language = source.Culture; + target.Email = source.Email; + target.Key = source.Key; + target.Username = source.Username; + target.Id = source.Id; + + target.ClearGroups(); + IEnumerable groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); + foreach (IUserGroup group in groups) + { + target.AddGroup(group.ToReadOnlyGroup()); + } + } + + // Umbraco.Code.MapAll -ContentStartNode -UserCount -MediaStartNode -Key -Languages -Sections + // Umbraco.Code.MapAll -Notifications -Udi -Trashed -AdditionalData -IsSystemUserGroup + private void Map(IReadOnlyUserGroup source, UserGroupBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + target.HasAccessToAllLanguages = source.HasAccessToAllLanguages; + + MapUserGroupBasic(target, source.AllowedLanguages, source.AllowedSections, source.StartContentId, source.StartMediaId, context); + } + + // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Languages -Sections -Notifications + // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -IsSystemUserGroup + private void Map(IUserGroup source, UserGroupBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserCount = source.UserCount; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + target.HasAccessToAllLanguages = source.HasAccessToAllLanguages; + + MapUserGroupBasic(target, source.AllowedLanguages, source.AllowedSections, source.StartContentId, source.StartMediaId, context); + } + + // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -AssignedPermissions + private void Map(IUserGroup source, AssignedUserGroupPermissions target, MapperContext context) + { + target.Id = source.Id; + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + + target.DefaultPermissions = MapUserGroupDefaultPermissions(source); + + if (target.Icon.IsNullOrWhiteSpace()) + { + target.Icon = Constants.Icons.UserGroup; + } + } + + // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Languages -Sections -Notifications -Udi + // Umbraco.Code.MapAll -Trashed -AdditionalData -Users -AssignedPermissions + private void Map(IUserGroup source, UserGroupDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.DefaultPermissions = MapUserGroupDefaultPermissions(source); + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserCount = source.UserCount; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + target.HasAccessToAllLanguages = source.HasAccessToAllLanguages; + + MapUserGroupBasic(target, source.AllowedLanguages, source.AllowedSections, source.StartContentId, source.StartMediaId, context); + + // Important! Currently we are never mapping to multiple UserGroupDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. + IEnumerable users = _userService.GetAllInGroup(source.Id); + target.Users = context.MapEnumerable(users).WhereNotNull(); + + // Deal with assigned permissions: + var allContentPermissions = _userService.GetPermissions(source, true) + .ToDictionary(x => x.EntityId, x => x); + + IEntitySlim[] contentEntities; + if (allContentPermissions.Keys.Count == 0) + { + contentEntities = Array.Empty(); + } + else + { + // a group can end up with way more than 2000 assigned permissions, + // so we need to break them into groups in order to avoid breaking + // the entity service due to too many Sql parameters. + var list = new List(); + foreach (IEnumerable idGroup in allContentPermissions.Keys.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); + } + + contentEntities = list.ToArray(); + } + + var allAssignedPermissions = new List(); + foreach (IEntitySlim entity in contentEntities) + { + EntityPermission contentPermissions = allContentPermissions[entity.Id]; + + AssignedContentPermissions? assignedContentPermissions = context.Map(entity); + if (assignedContentPermissions is null) + { + continue; + } + + assignedContentPermissions.AssignedPermissions = + AssignedUserGroupPermissions.ClonePermissions(target.DefaultPermissions); + + // since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + // and we'll re-check it if it's one of the explicitly assigned ones + foreach (Permission permission in assignedContentPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = + contentPermissions.AssignedPermissions.Contains( + permission.PermissionCode, + StringComparer.InvariantCulture); + } + + allAssignedPermissions.Add(assignedContentPermissions); + } + + target.AssignedPermissions = allAssignedPermissions; + } + + // Umbraco.Code.MapAll -Notifications -Udi -Icon -IsCurrentUser -Trashed -ResetPasswordValue + // Umbraco.Code.MapAll -Alias -AdditionalData + private void Map(IUser source, UserDisplay target, MapperContext context) + { + target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); + target.CalculatedStartContentIds = + GetStartNodes(source.CalculateContentStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Document, "content", "contentRoot", context); + target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media", "mediaRoot", context); + target.CreateDate = source.CreateDate; + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); + target.FailedPasswordAttempts = source.FailedPasswordAttempts; + target.Id = source.Id; + target.Key = source.Key; + target.LastLockoutDate = source.LastLockoutDate; + target.LastLoginDate = source.LastLoginDate == default(DateTime) ? null : source.LastLoginDate; + target.LastPasswordChangeDate = source.LastPasswordChangeDate; + target.Name = source.Name; + target.Navigation = CreateUserEditorNavigation(); + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.StartContentIds = GetStartNodes(source.StartContentIds?.ToArray(), UmbracoObjectTypes.Document, "content", "contentRoot", context); + target.StartMediaIds = GetStartNodes(source.StartMediaIds?.ToArray(), UmbracoObjectTypes.Media, "media", "mediaRoot", context); + target.UpdateDate = source.UpdateDate; + target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); + target.Username = source.Username; + target.UserState = source.UserState; + } + + // Umbraco.Code.MapAll -Notifications -IsCurrentUser -Udi -Icon -Trashed -Alias -AdditionalData + private void Map(IUser source, UserBasic target, MapperContext context) + { + // Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost + // Alternatively, if this is annoying the back office UI would need to be updated to request the avatars for the list of users separately so it doesn't look + // like the load time is waiting. + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); + target.Id = source.Id; + target.Key = source.Key; + target.LastLoginDate = source.LastLoginDate == default ? null : source.LastLoginDate; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); + target.Username = source.Username; + target.UserState = source.UserState; + } + + // Umbraco.Code.MapAll -SecondsUntilTimeout + private void Map(IUser source, UserDetail target, MapperContext context) + { + target.AllowedSections = source.AllowedSections; + target.AllowedLanguageIds = source.CalculateAllowedLanguageIds(_localizationService); + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); + target.Name = source.Name; + target.StartContentIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); + target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); + target.UserId = source.Id; + + // we need to map the legacy UserType + // the best we can do here is to return the user's first user group as a IUserType object + // but we should attempt to return any group that is the built in ones first + target.UserGroups = source.Groups.Select(x => x.Alias).ToArray(); + } + + // helpers + private void MapUserGroupBasic(UserGroupBasic target, IEnumerable sourceAllowedLanguages, IEnumerable sourceAllowedSections, int? sourceStartContentId, int? sourceStartMediaId, MapperContext context) + { + var allLanguages = _localizationService.GetAllLanguages(); + var applicableLanguages = Enumerable.Empty(); + + + if (sourceAllowedLanguages.Any()) + { + applicableLanguages = allLanguages.Where(x => sourceAllowedLanguages.Contains(x.Id)); + } + + target.Languages = context.MapEnumerable(applicableLanguages).WhereNotNull(); + + var allSections = _sectionService.GetSections(); + target.Sections = context.MapEnumerable(allSections.Where(x => sourceAllowedSections.Contains(x.Alias))).WhereNotNull(); + + if (sourceStartMediaId > 0) + { + target.MediaStartNode = + context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); + } + else if (sourceStartMediaId == -1) + { + target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot")); + } + + if (sourceStartContentId > 0) + { + target.ContentStartNode = + context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); + } + else if (sourceStartContentId == -1) + { + target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot")); + } + + if (target.Icon.IsNullOrWhiteSpace()) + { + target.Icon = Constants.Icons.UserGroup; + } + } + + private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) + { + Permission GetPermission(IAction action) + { + return new() + { + Category = action.Category.IsNullOrWhiteSpace() + ? _textService.Localize( + "actionCategories", + Constants.Conventions.PermissionCategories.OtherCategory) + : _textService.Localize("actionCategories", action.Category), + Name = _textService.Localize("actions", action.Alias), + Description = _textService.Localize("actionDescriptions", action.Alias), + Icon = action.Icon, + Checked = source.Permissions != null && + source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), + PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture), + }; + } + + return _actions + .Where(x => x.CanBePermissionAssigned) + .Select(GetPermission) + .GroupBy(x => x.Category) + .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); + } + + private IEnumerable GetStartNodes(int[]? startNodeIds, UmbracoObjectTypes objectType, string localizedArea, string localizedAlias, MapperContext context) + { + if (startNodeIds is null || startNodeIds.Length <= 0) + { + return Enumerable.Empty(); + } + + var startNodes = new List(); + if (startNodeIds.Contains(-1)) + { + startNodes.Add(CreateRootNode(_textService.Localize(localizedArea, localizedAlias))); + } + + IEnumerable mediaItems = _entityService.GetAll(objectType, startNodeIds); + startNodes.AddRange(context.MapEnumerable(mediaItems).WhereNotNull()); + return startNodes; + } + + private IEnumerable CreateUserEditorNavigation() => + new[] + { + new EditorNavigation + { + Active = true, + Alias = "details", + Icon = "icon-umb-users", + Name = _textService.Localize("general", "user"), + View = "views/users/views/user/details.html", + }, + }; + + private EntityBasic CreateRootNode(string name) => + new EntityBasic + { + Name = name, + Path = "-1", + Icon = "icon-folder", + Id = -1, + Trashed = false, + ParentId = -1, + }; } diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs index 926fe2ef09..d0cf05b8b9 100644 --- a/src/Umbraco.Core/Models/Media.cs +++ b/src/Umbraco.Core/Models/Media.cs @@ -1,84 +1,87 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Media object +/// +[Serializable] +[DataContract(IsReference = true)] +public class Media : ContentBase, IMedia { /// - /// Represents a Media object + /// Constructor for creating a Media object /// - [Serializable] - [DataContract(IsReference = true)] - public class Media : ContentBase, IMedia + /// name of the Media object + /// Parent object + /// MediaType for the current Media object + public Media(string? name, IMedia? parent, IMediaType mediaType) + : this(name, parent, mediaType, new PropertyCollection()) { - /// - /// Constructor for creating a Media object - /// - /// name of the Media object - /// Parent object - /// MediaType for the current Media object - public Media(string? name, IMedia? parent, IMediaType mediaType) - : this(name, parent, mediaType, new PropertyCollection()) - { } + } - /// - /// Constructor for creating a Media object - /// - /// name of the Media object - /// Parent object - /// MediaType for the current Media object - /// Collection of properties - public Media(string? name, IMedia? parent, IMediaType mediaType, IPropertyCollection properties) - : base(name, parent, mediaType, properties) - { } + /// + /// Constructor for creating a Media object + /// + /// name of the Media object + /// Parent object + /// MediaType for the current Media object + /// Collection of properties + public Media(string? name, IMedia? parent, IMediaType mediaType, IPropertyCollection properties) + : base(name, parent, mediaType, properties) + { + } - /// - /// Constructor for creating a Media object - /// - /// name of the Media object - /// Id of the Parent IMedia - /// MediaType for the current Media object - public Media(string? name, int parentId, IMediaType? mediaType) - : this(name, parentId, mediaType, new PropertyCollection()) - { } + /// + /// Constructor for creating a Media object + /// + /// name of the Media object + /// Id of the Parent IMedia + /// MediaType for the current Media object + public Media(string? name, int parentId, IMediaType? mediaType) + : this(name, parentId, mediaType, new PropertyCollection()) + { + } - /// - /// Constructor for creating a Media object - /// - /// Name of the Media object - /// Id of the Parent IMedia - /// MediaType for the current Media object - /// Collection of properties - public Media(string? name, int parentId, IMediaType? mediaType, IPropertyCollection properties) - : base(name, parentId, mediaType, properties) - { } + /// + /// Constructor for creating a Media object + /// + /// Name of the Media object + /// Id of the Parent IMedia + /// MediaType for the current Media object + /// Collection of properties + public Media(string? name, int parentId, IMediaType? mediaType, IPropertyCollection properties) + : base(name, parentId, mediaType, properties) + { + } - /// - /// Changes the for the current Media object - /// - /// New MediaType for this Media - /// Leaves PropertyTypes intact after change - internal void ChangeContentType(IMediaType mediaType) + /// + /// Changes the for the current Media object + /// + /// New MediaType for this Media + /// Leaves PropertyTypes intact after change + internal void ChangeContentType(IMediaType mediaType) => ChangeContentType(mediaType, false); + + /// + /// Changes the for the current Media object and removes PropertyTypes, + /// which are not part of the new MediaType. + /// + /// New MediaType for this Media + /// Boolean indicating whether to clear PropertyTypes upon change + internal void ChangeContentType(IMediaType mediaType, bool clearProperties) + { + ChangeContentType(new SimpleContentType(mediaType)); + + if (clearProperties) { - ChangeContentType(mediaType, false); + Properties.EnsureCleanPropertyTypes(mediaType.CompositionPropertyTypes); + } + else + { + Properties.EnsurePropertyTypes(mediaType.CompositionPropertyTypes); } - /// - /// Changes the for the current Media object and removes PropertyTypes, - /// which are not part of the new MediaType. - /// - /// New MediaType for this Media - /// Boolean indicating whether to clear PropertyTypes upon change - internal void ChangeContentType(IMediaType mediaType, bool clearProperties) - { - ChangeContentType(new SimpleContentType(mediaType)); - - if (clearProperties) - Properties.EnsureCleanPropertyTypes(mediaType.CompositionPropertyTypes); - else - Properties.EnsurePropertyTypes(mediaType.CompositionPropertyTypes); - - Properties.ClearCollectionChangedEvents(); // be sure not to double add - Properties.CollectionChanged += PropertiesChanged; - } + Properties.ClearCollectionChangedEvents(); // be sure not to double add + Properties.CollectionChanged += PropertiesChanged; } } diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs index 236ec9deb7..ee69c25de4 100644 --- a/src/Umbraco.Core/Models/MediaExtensions.cs +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -1,32 +1,30 @@ -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Extensions -{ - public static class MediaExtensions - { - /// - /// Gets the URL of a media item. - /// - public static string? GetUrl(this IMedia media, string propertyAlias, MediaUrlGeneratorCollection mediaUrlGenerators) - { - if (media.TryGetMediaPath(propertyAlias, mediaUrlGenerators, out var mediaPath)) - { - return mediaPath; - } +namespace Umbraco.Extensions; - return string.Empty; +public static class MediaExtensions +{ + /// + /// Gets the URL of a media item. + /// + public static string? GetUrl(this IMedia media, string propertyAlias, MediaUrlGeneratorCollection mediaUrlGenerators) + { + if (media.TryGetMediaPath(propertyAlias, mediaUrlGenerators, out var mediaPath)) + { + return mediaPath; } - /// - /// Gets the URLs of a media item. - /// - public static string?[] GetUrls(this IMedia media, ContentSettings contentSettings, MediaUrlGeneratorCollection mediaUrlGenerators) - => contentSettings.Imaging.AutoFillImageProperties - .Select(field => media.GetUrl(field.Alias, mediaUrlGenerators)) - .Where(link => string.IsNullOrWhiteSpace(link) == false) - .ToArray(); + return string.Empty; } + + /// + /// Gets the URLs of a media item. + /// + public static string?[] GetUrls(this IMedia media, ContentSettings contentSettings, MediaUrlGeneratorCollection mediaUrlGenerators) + => contentSettings.Imaging.AutoFillImageProperties + .Select(field => media.GetUrl(field.Alias, mediaUrlGenerators)) + .Where(link => string.IsNullOrWhiteSpace(link) == false) + .ToArray(); } diff --git a/src/Umbraco.Core/Models/MediaType.cs b/src/Umbraco.Core/Models/MediaType.cs index a529dc3189..64683ae462 100644 --- a/src/Umbraco.Core/Models/MediaType.cs +++ b/src/Umbraco.Core/Models/MediaType.cs @@ -1,54 +1,51 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the content type that a object is based on +/// +[Serializable] +[DataContract(IsReference = true)] +public class MediaType : ContentTypeCompositionBase, IMediaType { + public const bool SupportsPublishingConst = false; + /// - /// Represents the content type that a object is based on + /// Constuctor for creating a MediaType with the parent's id. /// - [Serializable] - [DataContract(IsReference = true)] - public class MediaType : ContentTypeCompositionBase, IMediaType + /// Only use this for creating MediaTypes at the root (with ParentId -1). + public MediaType(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - public const bool SupportsPublishingConst = false; - - /// - /// Constuctor for creating a MediaType with the parent's id. - /// - /// Only use this for creating MediaTypes at the root (with ParentId -1). - /// - public MediaType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId) - { - } - - /// - /// Constuctor for creating a MediaType with the parent as an inherited type. - /// - /// Use this to ensure inheritance from parent. - /// - public MediaType(IShortStringHelper shortStringHelper,IMediaType parent) : this(shortStringHelper, parent, string.Empty) - { - } - - /// - /// Constuctor for creating a MediaType with the parent as an inherited type. - /// - /// Use this to ensure inheritance from parent. - /// - /// - public MediaType(IShortStringHelper shortStringHelper, IMediaType parent, string alias) - : base(shortStringHelper, parent, alias) - { - } - - /// - public override ISimpleContentType ToSimple() => new SimpleContentType(this); - - /// - public override bool SupportsPublishing => SupportsPublishingConst; - - /// - IMediaType IMediaType.DeepCloneWithResetIdentities(string newAlias) => (IMediaType)DeepCloneWithResetIdentities(newAlias); } + + /// + /// Constuctor for creating a MediaType with the parent as an inherited type. + /// + /// Use this to ensure inheritance from parent. + public MediaType(IShortStringHelper shortStringHelper, IMediaType parent) + : this(shortStringHelper, parent, string.Empty) + { + } + + /// + /// Constuctor for creating a MediaType with the parent as an inherited type. + /// + /// Use this to ensure inheritance from parent. + public MediaType(IShortStringHelper shortStringHelper, IMediaType parent, string alias) + : base(shortStringHelper, parent, alias) + { + } + + /// + public override bool SupportsPublishing => SupportsPublishingConst; + + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); + + /// + IMediaType IMediaType.DeepCloneWithResetIdentities(string newAlias) => + (IMediaType)DeepCloneWithResetIdentities(newAlias); } diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 4244e1ba44..cddf04b4fe 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -1,500 +1,576 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Member object +/// +[Serializable] +[DataContract(IsReference = true)] +public class Member : ContentBase, IMember { + private IDictionary? _additionalData; + private string _email; + private DateTime? _emailConfirmedDate; + private int _failedPasswordAttempts; + private bool _isApproved; + private bool _isLockedOut; + private DateTime? _lastLockoutDate; + private DateTime? _lastLoginDate; + private DateTime? _lastPasswordChangeDate; + private string? _passwordConfig; + private string? _rawPasswordValue; + private string? _securityStamp; + private string _username; + /// - /// Represents a Member object + /// Initializes a new instance of the class. + /// Constructor for creating an empty Member object /// - [Serializable] - [DataContract(IsReference = true)] - public class Member : ContentBase, IMember + /// ContentType for the current Content object + public Member(IMemberType contentType) + : base(string.Empty, -1, contentType, new PropertyCollection()) { - private IDictionary? _additionalData; - private string _username; - private string _email; - private string? _rawPasswordValue; - private string? _passwordConfig; - private DateTime? _emailConfirmedDate; - private string? _securityStamp; - private int _failedPasswordAttempts; - private bool _isApproved; - private bool _isLockedOut; - private DateTime? _lastLockoutDate; - private DateTime? _lastLoginDate; - private DateTime? _lastPasswordChangeDate; + IsApproved = true; - /// - /// Initializes a new instance of the class. - /// Constructor for creating an empty Member object - /// - /// ContentType for the current Content object - public Member(IMemberType contentType) - : base("", -1, contentType, new PropertyCollection()) + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + _email = string.Empty; + _username = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// Name of the content + /// ContentType for the current Content object + public Member(string name, IMemberType contentType) + : base(name, -1, contentType, new PropertyCollection()) + { + if (name == null) { - IsApproved = true; - - // this cannot be null but can be empty - _rawPasswordValue = ""; - _email = ""; - _username = ""; + throw new ArgumentNullException(nameof(name)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// Name of the content - /// ContentType for the current Content object - public Member(string name, IMemberType contentType) - : base(name, -1, contentType, new PropertyCollection()) + if (string.IsNullOrWhiteSpace(name)) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - IsApproved = true; - - // this cannot be null but can be empty - _rawPasswordValue = ""; - _email = ""; - _username = ""; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true) - : base(name, -1, contentType, new PropertyCollection()) + IsApproved = true; + + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + _email = string.Empty; + _username = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// + public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true) + : base(name, -1, contentType, new PropertyCollection()) + { + if (name == null) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (email == null) - throw new ArgumentNullException(nameof(email)); - if (string.IsNullOrWhiteSpace(email)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); - - _email = email; - _username = username; - IsApproved = isApproved; - - // this cannot be null but can be empty - _rawPasswordValue = ""; + throw new ArgumentNullException(nameof(name)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// - /// - public Member(string name, string email, string username, IMemberType contentType, int userId, bool isApproved = true) - : base(name, -1, contentType, new PropertyCollection()) + if (string.IsNullOrWhiteSpace(name)) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (email == null) throw new ArgumentNullException(nameof(email)); - if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); - if (username == null) throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); - - _email = email; - _username = username; - CreatorId = userId; - IsApproved = isApproved; - - //this cannot be null but can be empty - _rawPasswordValue = ""; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password - /// - /// - public Member(string? name, string email, string username, string? rawPasswordValue, IMemberType? contentType) - : base(name, -1, contentType, new PropertyCollection()) + if (email == null) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = true; + throw new ArgumentNullException(nameof(email)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password - /// - /// - /// - public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved) - : base(name, -1, contentType, new PropertyCollection()) + if (string.IsNullOrWhiteSpace(email)) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = isApproved; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(email)); } - /// - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password - /// - /// - /// - /// - public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved, int userId) - : base(name, -1, contentType, new PropertyCollection()) + if (username == null) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = isApproved; - CreatorId = userId; + throw new ArgumentNullException(nameof(username)); } - /// - /// Gets or sets the Username - /// - [DataMember] - public string Username + if (string.IsNullOrWhiteSpace(username)) { - get => _username; - set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); } - /// - /// Gets or sets the Email - /// - [DataMember] - public string Email + _email = email; + _username = username; + IsApproved = isApproved; + + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// + /// + public Member(string name, string email, string username, IMemberType contentType, int userId, bool isApproved = true) + : base(name, -1, contentType, new PropertyCollection()) + { + if (name == null) { - get => _email; - set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + throw new ArgumentNullException(nameof(name)); } - [DataMember] - public DateTime? EmailConfirmedDate + if (string.IsNullOrWhiteSpace(name)) { - get => _emailConfirmedDate; - set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Gets or sets the raw password value - /// - [IgnoreDataMember] - public string? RawPasswordValue + if (email == null) { - get => _rawPasswordValue; - set + throw new ArgumentNullException(nameof(email)); + } + + if (string.IsNullOrWhiteSpace(email)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(email)); + } + + if (username == null) + { + throw new ArgumentNullException(nameof(username)); + } + + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); + } + + _email = email; + _username = username; + CreatorId = userId; + IsApproved = isApproved; + + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + } + + /// + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's + /// password + /// + /// + public Member(string? name, string email, string username, string? rawPasswordValue, IMemberType? contentType) + : base(name, -1, contentType, new PropertyCollection()) + { + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + IsApproved = true; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's + /// password + /// + /// + /// + public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved) + : base(name, -1, contentType, new PropertyCollection()) + { + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + IsApproved = isApproved; + } + + /// + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's + /// password + /// + /// + /// + /// + public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved, int userId) + : base(name, -1, contentType, new PropertyCollection()) + { + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + IsApproved = isApproved; + CreatorId = userId; + } + + /// + /// Gets or sets the Groups that Member is part of + /// + [DataMember] + public IEnumerable? Groups { get; set; } + + /// + /// Gets or sets the Username + /// + [DataMember] + public string Username + { + get => _username; + set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + } + + /// + /// Gets or sets the Email + /// + [DataMember] + public string Email + { + get => _email; + set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + } + + [DataMember] + public DateTime? EmailConfirmedDate + { + get => _emailConfirmedDate; + set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + } + + /// + /// Gets or sets the raw password value + /// + [IgnoreDataMember] + public string? RawPasswordValue + { + get => _rawPasswordValue; + set + { + if (value == null) { - if (value == null) - { - //special case, this is used to ensure that the password is not updated when persisting, in this case - //we don't want to track changes either - _rawPasswordValue = null; - } - else - { - SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); - } + // special case, this is used to ensure that the password is not updated when persisting, in this case + // we don't want to track changes either + _rawPasswordValue = null; + } + else + { + SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); } } + } - [IgnoreDataMember] - public string? PasswordConfiguration + [IgnoreDataMember] + public string? PasswordConfiguration + { + get => _passwordConfig; + set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + } + + // TODO: When get/setting all of these properties we MUST: + // * Check if we are using the umbraco membership provider, if so then we need to use the configured fields - not the explicit fields below + // * If any of the fields don't exist, what should we do? Currently it will throw an exception! + + /// + /// Gets or set the comments for the member + /// + /// + /// Alias: umbracoMemberComments + /// Part of the standard properties collection. + /// + [DataMember] + public string? Comments + { + get { - get => _passwordConfig; - set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + Attempt a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); + if (a.Success == false) + { + return a.Result; + } + + return Properties[Constants.Conventions.Member.Comments]?.GetValue() == null + ? string.Empty + : Properties[Constants.Conventions.Member.Comments]?.GetValue()?.ToString(); } - /// - /// Gets or sets the Groups that Member is part of - /// - [DataMember] - public IEnumerable? Groups { get; set; } - - // TODO: When get/setting all of these properties we MUST: - // * Check if we are using the umbraco membership provider, if so then we need to use the configured fields - not the explicit fields below - // * If any of the fields don't exist, what should we do? Currently it will throw an exception! - - /// - /// Gets or set the comments for the member - /// - /// - /// Alias: umbracoMemberComments - /// Part of the standard properties collection. - /// - [DataMember] - public string? Comments + set { - get - { - var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); - if (a.Success == false) - return a.Result; - - return Properties[Constants.Conventions.Member.Comments]?.GetValue() == null - ? string.Empty - : Properties[Constants.Conventions.Member.Comments]?.GetValue()?.ToString(); - } - set - { - if (WarnIfPropertyTypeNotFoundOnSet( + if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.Comments, nameof(Comments)) == false) - return; - - Properties[Constants.Conventions.Member.Comments]?.SetValue(value); - } - } - - /// - /// Gets or sets a value indicating whether the Member is approved - /// - [DataMember] - public bool IsApproved - { - get => _isApproved; - set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); - } - - /// - /// Gets or sets a boolean indicating whether the Member is locked out - /// - /// - /// Alias: umbracoMemberLockedOut - /// Part of the standard properties collection. - /// - [DataMember] - public bool IsLockedOut - { - get => _isLockedOut; - set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); - } - - /// - /// Gets or sets the date for last login - /// - /// - /// Alias: umbracoMemberLastLogin - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastLoginDate - { - get => _lastLoginDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); - } - - /// - /// Gest or sets the date for last password change - /// - /// - /// Alias: umbracoMemberLastPasswordChangeDate - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastPasswordChangeDate - { - get => _lastPasswordChangeDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDate, nameof(LastPasswordChangeDate)); - } - - /// - /// Gets or sets the date for when Member was locked out - /// - /// - /// Alias: umbracoMemberLastLockoutDate - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastLockoutDate - { - get => _lastLockoutDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); - } - - /// - /// Gets or sets the number of failed password attempts. - /// This is the number of times the password was entered incorrectly upon login. - /// - /// - /// Alias: umbracoMemberFailedPasswordAttempts - /// Part of the standard properties collection. - /// - [DataMember] - public int FailedPasswordAttempts - { - get => _failedPasswordAttempts; - set => SetPropertyValueAndDetectChanges(value, ref _failedPasswordAttempts, nameof(FailedPasswordAttempts)); - } - - /// - /// String alias of the default ContentType - /// - [DataMember] - public virtual string ContentTypeAlias => ContentType.Alias; - - /// - /// The security stamp used by ASP.Net identity - /// - [IgnoreDataMember] - public string? SecurityStamp - { - get => _securityStamp; - set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); - } - - - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public string? LongStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public string? ShortStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public int IntegerPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public bool BoolPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public DateTime DateTimePropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public string? PropertyTypeAlias { get; set; } - - private Attempt WarnIfPropertyTypeNotFoundOnGet(string propertyAlias, string propertyName, T defaultVal) - { - void DoLog(string logPropertyAlias, string logPropertyName) { - StaticApplicationLogging.Logger.LogWarning("Trying to access the '{PropertyName}' property on '{MemberType}' " + - "but the {PropertyAlias} property does not exist on the member type so a default value is returned. " + - "Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.", - logPropertyName, - typeof(Member), - logPropertyAlias); + return; } - // if the property doesn't exist, - if (Properties.Contains(propertyAlias) == false) - { - // put a warn in the log if this entity has been persisted - // then return a failure - if (HasIdentity) - DoLog(propertyAlias, propertyName); - return Attempt.Fail(defaultVal); - } - - return Attempt.Succeed(); + Properties[Constants.Conventions.Member.Comments]?.SetValue(value); } + } - private bool WarnIfPropertyTypeNotFoundOnSet(string propertyAlias, string propertyName) + /// + /// Gets or sets a value indicating whether the Member is approved + /// + [DataMember] + public bool IsApproved + { + get => _isApproved; + set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); + } + + /// + /// Gets or sets a boolean indicating whether the Member is locked out + /// + /// + /// Alias: umbracoMemberLockedOut + /// Part of the standard properties collection. + /// + [DataMember] + public bool IsLockedOut + { + get => _isLockedOut; + set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); + } + + /// + /// Gets or sets the date for last login + /// + /// + /// Alias: umbracoMemberLastLogin + /// Part of the standard properties collection. + /// + [DataMember] + public DateTime? LastLoginDate + { + get => _lastLoginDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); + } + + /// + /// Gest or sets the date for last password change + /// + /// + /// Alias: umbracoMemberLastPasswordChangeDate + /// Part of the standard properties collection. + /// + [DataMember] + public DateTime? LastPasswordChangeDate + { + get => _lastPasswordChangeDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDate, nameof(LastPasswordChangeDate)); + } + + /// + /// Gets or sets the date for when Member was locked out + /// + /// + /// Alias: umbracoMemberLastLockoutDate + /// Part of the standard properties collection. + /// + [DataMember] + public DateTime? LastLockoutDate + { + get => _lastLockoutDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); + } + + /// + /// Gets or sets the number of failed password attempts. + /// This is the number of times the password was entered incorrectly upon login. + /// + /// + /// Alias: umbracoMemberFailedPasswordAttempts + /// Part of the standard properties collection. + /// + [DataMember] + public int FailedPasswordAttempts + { + get => _failedPasswordAttempts; + set => SetPropertyValueAndDetectChanges(value, ref _failedPasswordAttempts, nameof(FailedPasswordAttempts)); + } + + /// + /// String alias of the default ContentType + /// + [DataMember] + public virtual string ContentTypeAlias => ContentType.Alias; + + /// + /// The security stamp used by ASP.Net identity + /// + [IgnoreDataMember] + public string? SecurityStamp + { + get => _securityStamp; + set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); + } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public string? LongStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public string? ShortStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public int IntegerPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public bool BoolPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public DateTime DateTimePropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public string? PropertyTypeAlias { get; set; } + + /// + [DataMember] + [DoNotClone] + public IDictionary AdditionalData => _additionalData ??= new Dictionary(); + + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; + + private Attempt WarnIfPropertyTypeNotFoundOnGet(string propertyAlias, string propertyName, T defaultVal) + { + static void DoLog(string logPropertyAlias, string logPropertyName) { - void DoLog(string logPropertyAlias, string logPropertyName) - { - StaticApplicationLogging.Logger.LogWarning("An attempt was made to set a value on the property '{PropertyName}' on type '{MemberType}' but the " + - "property type {PropertyAlias} does not exist on the member type, ensure that this property type exists so that setting this property works correctly.", - logPropertyName, - typeof(Member), - logPropertyAlias); - } - - // if the property doesn't exist, - if (Properties.Contains(propertyAlias) == false) - { - // put a warn in the log if this entity has been persisted - // then return a failure - if (HasIdentity) - DoLog(propertyAlias, propertyName); - return false; - } - - return true; + StaticApplicationLogging.Logger.LogWarning( + "Trying to access the '{PropertyName}' property on '{MemberType}' " + + "but the {PropertyAlias} property does not exist on the member type so a default value is returned. " + + "Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.", + logPropertyName, + typeof(Member), + logPropertyAlias); } - /// - [DataMember] - [DoNotClone] - public IDictionary? AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); + // if the property doesn't exist, + if (Properties.Contains(propertyAlias) == false) + { + // put a warn in the log if this entity has been persisted + // then return a failure + if (HasIdentity) + { + DoLog(propertyAlias, propertyName); + } - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; + return Attempt.Fail(defaultVal); + } + + return Attempt.Succeed(); + } + + private bool WarnIfPropertyTypeNotFoundOnSet(string propertyAlias, string propertyName) + { + static void DoLog(string logPropertyAlias, string logPropertyName) + { + StaticApplicationLogging.Logger.LogWarning( + "An attempt was made to set a value on the property '{PropertyName}' on type '{MemberType}' but the " + + "property type {PropertyAlias} does not exist on the member type, ensure that this property type exists so that setting this property works correctly.", + logPropertyName, + typeof(Member), + logPropertyAlias); + } + + // if the property doesn't exist, + if (Properties.Contains(propertyAlias) == false) + { + // put a warn in the log if this entity has been persisted + // then return a failure + if (HasIdentity) + { + DoLog(propertyAlias, propertyName); + } + + return false; + } + + return true; } } diff --git a/src/Umbraco.Core/Models/MemberGroup.cs b/src/Umbraco.Core/Models/MemberGroup.cs index 7a35b78875..5ae7a7edd2 100644 --- a/src/Umbraco.Core/Models/MemberGroup.cs +++ b/src/Umbraco.Core/Models/MemberGroup.cs @@ -1,53 +1,51 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a member type +/// +[Serializable] +[DataContract(IsReference = true)] +public class MemberGroup : EntityBase, IMemberGroup { - /// - /// Represents a member type - /// - [Serializable] - [DataContract(IsReference = true)] - public class MemberGroup : EntityBase, IMemberGroup + private IDictionary? _additionalData; + private int _creatorId; + private string? _name; + + /// + [DataMember] + [DoNotClone] + public IDictionary AdditionalData => +_additionalData ??= new Dictionary(); + + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; + + [DataMember] + public string? Name { - private IDictionary? _additionalData; - private string? _name; - private int _creatorId; - - /// - [DataMember] - [DoNotClone] - public IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); - - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; - - [DataMember] - public string? Name + get => _name; + set { - get => _name; - set + if (_name != value) { - if (_name != value) - { - //if the name has changed, add the value to the additional data, - //this is required purely for event handlers to know the previous name of the group - //so we can keep the public access up to date. - AdditionalData["previousName"] = _name; - } - - SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + // if the name has changed, add the value to the additional data, + // this is required purely for event handlers to know the previous name of the group + // so we can keep the public access up to date. + AdditionalData["previousName"] = _name; } - } - [DataMember] - public int CreatorId - { - get => _creatorId; - set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); + SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); } } + + [DataMember] + public int CreatorId + { + get => _creatorId; + set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); + } } diff --git a/src/Umbraco.Core/Models/MemberPropertyModel.cs b/src/Umbraco.Core/Models/MemberPropertyModel.cs index f6d06956e5..96466af397 100644 --- a/src/Umbraco.Core/Models/MemberPropertyModel.cs +++ b/src/Umbraco.Core/Models/MemberPropertyModel.cs @@ -1,37 +1,34 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A simple representation of an Umbraco member property +/// +public class MemberPropertyModel { - /// - /// A simple representation of an Umbraco member property - /// - public class MemberPropertyModel - { - [Required] - public string Alias { get; set; } = null!; + [Required] + public string Alias { get; set; } = null!; - //NOTE: This has to be a string currently, if it is an object it will bind as an array which we don't want. - // If we want to have this as an 'object' with a true type on it, we have to create a custom model binder - // for an UmbracoProperty and then bind with the correct type based on the property type for this alias. This - // would be a bit long winded and perhaps unnecessary. The reason is because it is always posted as a string anyways - // and when we set this value on the property object that gets sent to the database we do a TryConvertTo to the - // real type anyways. + // NOTE: This has to be a string currently, if it is an object it will bind as an array which we don't want. + // If we want to have this as an 'object' with a true type on it, we have to create a custom model binder + // for an UmbracoProperty and then bind with the correct type based on the property type for this alias. This + // would be a bit long winded and perhaps unnecessary. The reason is because it is always posted as a string anyways + // and when we set this value on the property object that gets sent to the database we do a TryConvertTo to the + // real type anyways. + [DataType(System.ComponentModel.DataAnnotations.DataType.Text)] + public string? Value { get; set; } - [DataType(System.ComponentModel.DataAnnotations.DataType.Text)] - public string? Value { get; set; } + [ReadOnly(true)] + public string? Name { get; set; } - [ReadOnly(true)] - public string? Name { get; set; } + // TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers can just render their own inside the view - // TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers can just render their own inside the view - - ///// - ///// This can dynamically be set to a custom template name to change - ///// the editor type for this property - ///// - //[ReadOnly(true)] - //public string EditorTemplate { get; set; } - - } + ///// + ///// This can dynamically be set to a custom template name to change + ///// the editor type for this property + ///// + // [ReadOnly(true)] + // public string EditorTemplate { get; set; } } diff --git a/src/Umbraco.Core/Models/MemberType.cs b/src/Umbraco.Core/Models/MemberType.cs index 4db8388b94..502a61df9f 100644 --- a/src/Umbraco.Core/Models/MemberType.cs +++ b/src/Umbraco.Core/Models/MemberType.cs @@ -1,169 +1,172 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the content type that a object is based on +/// +[Serializable] +[DataContract(IsReference = true)] +public class MemberType : ContentTypeCompositionBase, IMemberType { + public const bool SupportsPublishingConst = false; + private readonly IShortStringHelper _shortStringHelper; + /// - /// Represents the content type that a object is based on + /// Gets or Sets a Dictionary of Tuples (MemberCanEdit, VisibleOnProfile, IsSensitive) by the PropertyTypes' alias. /// - [Serializable] - [DataContract(IsReference = true)] - public class MemberType : ContentTypeCompositionBase, IMemberType + private readonly IDictionary _memberTypePropertyTypes; + + // Dictionary is divided into string: PropertyTypeAlias, Tuple: MemberCanEdit, VisibleOnProfile, PropertyTypeId + private string _alias = string.Empty; + + public MemberType(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - private readonly IShortStringHelper _shortStringHelper; - public const bool SupportsPublishingConst = false; + _shortStringHelper = shortStringHelper; + _memberTypePropertyTypes = new Dictionary(); + } - //Dictionary is divided into string: PropertyTypeAlias, Tuple: MemberCanEdit, VisibleOnProfile, PropertyTypeId - private string _alias = string.Empty; + public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent) + : this( + shortStringHelper, + parent, + string.Empty) + { + } - public MemberType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId) + public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) + : base(shortStringHelper, parent, alias) + { + _shortStringHelper = shortStringHelper; + _memberTypePropertyTypes = new Dictionary(); + } + + /// + public override bool SupportsPublishing => SupportsPublishingConst; + + public override ContentVariation Variations + { + // note: although technically possible, variations on members don't make much sense + // and therefore are disabled - they are fully supported at service level, though, + // but not at published snapshot level. + get => base.Variations; + set => throw new NotSupportedException("Variations are not supported on members."); + } + + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); + + /// + /// The Alias of the ContentType + /// + [DataMember] + public override string Alias + { + get => _alias; + set { - _shortStringHelper = shortStringHelper; - _memberTypePropertyTypes = new Dictionary(); + // NOTE: WE are overriding this because we don't want to do a ToSafeAlias when the alias is the special case of + // "_umbracoSystemDefaultProtectType" which is used internally, currently there is an issue with the safe alias as it strips + // leading underscores which we don't want in this case. + // see : http://issues.umbraco.org/issue/U4-3968 + + // TODO: BUT, I'm pretty sure we could do this with regards to underscores now: + // .ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase) + // Need to ask Stephen + var newVal = value == "_umbracoSystemDefaultProtectType" + ? value + : value == null + ? string.Empty + : value.ToSafeAlias(_shortStringHelper); + + SetPropertyValueAndDetectChanges(newVal, ref _alias!, nameof(Alias)); } + } - public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent) : this(shortStringHelper, parent, string.Empty) + /// + /// Gets a boolean indicating whether a Property is editable by the Member. + /// + /// PropertyType Alias of the Property to check + /// + public bool MemberCanEditProperty(string? propertyTypeAlias) => propertyTypeAlias is not null && + _memberTypePropertyTypes.TryGetValue( + propertyTypeAlias, + out MemberTypePropertyProfileAccess? propertyProfile) && + propertyProfile.IsEditable; + + /// + /// Gets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + public bool MemberCanViewProperty(string propertyTypeAlias) => + _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile) && + propertyProfile.IsVisible; + + /// + /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + public bool IsSensitiveProperty(string propertyTypeAlias) => + _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile) && + propertyProfile.IsSensitive; + + /// + /// Sets a boolean indicating whether a Property is editable by the Member. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + public void SetMemberCanEditProperty(string propertyTypeAlias, bool value) + { + if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile)) { + propertyProfile.IsEditable = value; } - - public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) - : base(shortStringHelper, parent, alias) + else { - _shortStringHelper = shortStringHelper; - _memberTypePropertyTypes = new Dictionary(); + var tuple = new MemberTypePropertyProfileAccess(false, value, false); + _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); } + } - /// - public override ISimpleContentType ToSimple() => new SimpleContentType(this); - - /// - public override bool SupportsPublishing => SupportsPublishingConst; - - public override ContentVariation Variations + /// + /// Sets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + public void SetMemberCanViewProperty(string propertyTypeAlias, bool value) + { + if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile)) { - // note: although technically possible, variations on members don't make much sense - // and therefore are disabled - they are fully supported at service level, though, - // but not at published snapshot level. - - get => base.Variations; - set => throw new NotSupportedException("Variations are not supported on members."); + propertyProfile.IsVisible = value; } - - /// - /// The Alias of the ContentType - /// - [DataMember] - public override string Alias + else { - get => _alias; - set - { - //NOTE: WE are overriding this because we don't want to do a ToSafeAlias when the alias is the special case of - // "_umbracoSystemDefaultProtectType" which is used internally, currently there is an issue with the safe alias as it strips - // leading underscores which we don't want in this case. - // see : http://issues.umbraco.org/issue/U4-3968 - - // TODO: BUT, I'm pretty sure we could do this with regards to underscores now: - // .ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase) - // Need to ask Stephen - - var newVal = value == "_umbracoSystemDefaultProtectType" - ? value - : (value == null ? string.Empty : value.ToSafeAlias(_shortStringHelper)); - - SetPropertyValueAndDetectChanges(newVal, ref _alias!, nameof(Alias)); - } + var tuple = new MemberTypePropertyProfileAccess(value, false, false); + _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); } + } - /// - /// Gets or Sets a Dictionary of Tuples (MemberCanEdit, VisibleOnProfile, IsSensitive) by the PropertyTypes' alias. - /// - private IDictionary _memberTypePropertyTypes; - - /// - /// Gets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to check - /// - public bool MemberCanEditProperty(string? propertyTypeAlias) + /// + /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + public void SetIsSensitiveProperty(string propertyTypeAlias, bool value) + { + if (_memberTypePropertyTypes.TryGetValue( + propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile)) { - return propertyTypeAlias is not null && _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsEditable; + propertyProfile.IsSensitive = value; } - - /// - /// Gets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - public bool MemberCanViewProperty(string propertyTypeAlias) + else { - return _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsVisible; - } - /// - /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - public bool IsSensitiveProperty(string propertyTypeAlias) - { - return _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsSensitive; - } - - /// - /// Sets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - public void SetMemberCanEditProperty(string propertyTypeAlias, bool value) - { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsEditable = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(false, value, false); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } - } - - /// - /// Sets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - public void SetMemberCanViewProperty(string propertyTypeAlias, bool value) - { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsVisible = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(value, false, false); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } - } - - /// - /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - public void SetIsSensitiveProperty(string propertyTypeAlias, bool value) - { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsSensitive = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(false, false, value); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } + var tuple = new MemberTypePropertyProfileAccess(false, false, value); + _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); } } } diff --git a/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs b/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs index 89bf2f283d..e6e619354b 100644 --- a/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs +++ b/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs @@ -1,19 +1,20 @@ -namespace Umbraco.Cms.Core.Models -{ - /// - /// Used to track the property types that are visible/editable on member profiles - /// - public class MemberTypePropertyProfileAccess - { - public MemberTypePropertyProfileAccess(bool isVisible, bool isEditable, bool isSenstive) - { - IsVisible = isVisible; - IsEditable = isEditable; - IsSensitive = isSenstive; - } +namespace Umbraco.Cms.Core.Models; - public bool IsVisible { get; set; } - public bool IsEditable { get; set; } - public bool IsSensitive { get; set; } +/// +/// Used to track the property types that are visible/editable on member profiles +/// +public class MemberTypePropertyProfileAccess +{ + public MemberTypePropertyProfileAccess(bool isVisible, bool isEditable, bool isSenstive) + { + IsVisible = isVisible; + IsEditable = isEditable; + IsSensitive = isSenstive; } + + public bool IsVisible { get; set; } + + public bool IsEditable { get; set; } + + public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs index 9c585589fa..613a873d7a 100644 --- a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs @@ -1,55 +1,42 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Represents an -> user group & permission key value pair collection +/// +/// +/// This implements purely so it can be used with the repository layer which is why it's +/// explicitly implemented. +/// +public class ContentPermissionSet : EntityPermissionSet, IEntity { - /// - /// Represents an -> user group & permission key value pair collection - /// - /// - /// This implements purely so it can be used with the repository layer which is why it's explicitly implemented. - /// - public class ContentPermissionSet : EntityPermissionSet, IEntity + private readonly IContent _content; + + public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet) + : base(content.Id, permissionsSet) => + _content = content; + + public override int EntityId => _content.Id; + + int IEntity.Id { - private readonly IContent _content; - - public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet) - : base(content.Id, permissionsSet) - { - _content = content; - } - - public override int EntityId - { - get { return _content.Id; } - } - - #region Explicit implementation of IAggregateRoot - int IEntity.Id - { - get { return EntityId; } - set { throw new NotImplementedException(); } - } - - bool IEntity.HasIdentity - { - get { return EntityId > 0; } - } - - void IEntity.ResetIdentity() => throw new InvalidOperationException($"Resetting identity on {nameof(ContentPermissionSet)} is invalid"); - - Guid IEntity.Key { get; set; } - - DateTime IEntity.CreateDate { get; set; } - - DateTime IEntity.UpdateDate { get; set; } - - DateTime? IEntity.DeleteDate { get; set; } - - object IDeepCloneable.DeepClone() - { - throw new NotImplementedException(); - } - #endregion + get => EntityId; + set => throw new NotImplementedException(); } + + bool IEntity.HasIdentity => EntityId > 0; + + Guid IEntity.Key { get; set; } + + void IEntity.ResetIdentity() => + throw new InvalidOperationException($"Resetting identity on {nameof(ContentPermissionSet)} is invalid"); + + DateTime IEntity.CreateDate { get; set; } + + DateTime IEntity.UpdateDate { get; set; } + + DateTime? IEntity.DeleteDate { get; set; } + + object IDeepCloneable.DeepClone() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index a86c844622..58e84f27f9 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -1,66 +1,84 @@ -using System; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user) +/// +public class EntityPermission : IEquatable { - /// - /// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user) - /// - public class EntityPermission : IEquatable + public EntityPermission(int groupId, int entityId, string[] assignedPermissions) { - public EntityPermission(int groupId, int entityId, string[] assignedPermissions) - { - UserGroupId = groupId; - EntityId = entityId; - AssignedPermissions = assignedPermissions; - IsDefaultPermissions = false; - } - - public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions) - { - UserGroupId = groupId; - EntityId = entityId; - AssignedPermissions = assignedPermissions; - IsDefaultPermissions = isDefaultPermissions; - } - - public int EntityId { get; private set; } - public int UserGroupId { get; private set; } - - /// - /// The assigned permissions for the user/entity combo - /// - public string[] AssignedPermissions { get; private set; } - - /// - /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined permissions - /// - /// - /// This will be the case when looking up entity permissions and falling back to the default permissions - /// - public bool IsDefaultPermissions { get; private set; } - - public bool Equals(EntityPermission? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return EntityId == other.EntityId && UserGroupId == other.UserGroupId; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((EntityPermission) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (EntityId * 397) ^ UserGroupId; - } - } + UserGroupId = groupId; + EntityId = entityId; + AssignedPermissions = assignedPermissions; + IsDefaultPermissions = false; } + public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions) + { + UserGroupId = groupId; + EntityId = entityId; + AssignedPermissions = assignedPermissions; + IsDefaultPermissions = isDefaultPermissions; + } + + public int EntityId { get; } + + public int UserGroupId { get; } + + /// + /// The assigned permissions for the user/entity combo + /// + public string[] AssignedPermissions { get; } + + /// + /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined + /// permissions + /// + /// + /// This will be the case when looking up entity permissions and falling back to the default permissions + /// + public bool IsDefaultPermissions { get; } + + public bool Equals(EntityPermission? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return EntityId == other.EntityId && UserGroupId == other.UserGroupId; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((EntityPermission)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (EntityId * 397) ^ UserGroupId; + } + } } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs index ac03ef75d8..727f7964f7 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs @@ -1,57 +1,55 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// A of +/// +public class EntityPermissionCollection : HashSet { - /// - /// A of - /// - public class EntityPermissionCollection : HashSet + private Dictionary? _aggregateNodePermissions; + + private string[]? _aggregatePermissions; + + public EntityPermissionCollection() { - public EntityPermissionCollection() - { - } - - public EntityPermissionCollection(IEnumerable collection) : base(collection) - { - } - - /// - /// Returns the aggregate permissions in the permission set for a single node - /// - /// - /// - /// This value is only calculated once per node - /// - public IEnumerable GetAllPermissions(int entityId) - { - if (_aggregateNodePermissions == null) - _aggregateNodePermissions = new Dictionary(); - - string[]? entityPermissions; - if (_aggregateNodePermissions.TryGetValue(entityId, out entityPermissions) == false) - { - entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions).Distinct().ToArray(); - _aggregateNodePermissions[entityId] = entityPermissions; - } - return entityPermissions; - } - - private Dictionary? _aggregateNodePermissions; - - /// - /// Returns the aggregate permissions in the permission set for all nodes - /// - /// - /// - /// This value is only calculated once - /// - public IEnumerable GetAllPermissions() - { - return _aggregatePermissions ?? (_aggregatePermissions = - this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray()); - } - - private string[]? _aggregatePermissions; } + + public EntityPermissionCollection(IEnumerable collection) + : base(collection) + { + } + + /// + /// Returns the aggregate permissions in the permission set for a single node + /// + /// + /// + /// This value is only calculated once per node + /// + public IEnumerable GetAllPermissions(int entityId) + { + if (_aggregateNodePermissions == null) + { + _aggregateNodePermissions = new Dictionary(); + } + + if (_aggregateNodePermissions.TryGetValue(entityId, out string[]? entityPermissions) == false) + { + entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions) + .Distinct().ToArray(); + _aggregateNodePermissions[entityId] = entityPermissions; + } + + return entityPermissions; + } + + /// + /// Returns the aggregate permissions in the permission set for all nodes + /// + /// + /// + /// This value is only calculated once + /// + public IEnumerable GetAllPermissions() => +_aggregatePermissions ??= + this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray(); } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs index 68e97a5d9f..0ae0dbf335 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs @@ -1,54 +1,41 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// Represents an entity -> user group & permission key value pair collection +/// +public class EntityPermissionSet { - /// - /// Represents an entity -> user group & permission key value pair collection - /// - public class EntityPermissionSet + private static readonly Lazy EmptyInstance = + new(() => new EntityPermissionSet(-1, new EntityPermissionCollection())); + + public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet) { - private static readonly Lazy EmptyInstance = new Lazy(() => new EntityPermissionSet(-1, new EntityPermissionCollection())); - /// - /// Returns an empty permission set - /// - /// - public static EntityPermissionSet Empty() - { - return EmptyInstance.Value; - } - - public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet) - { - EntityId = entityId; - PermissionsSet = permissionsSet; - } - - /// - /// The entity id with permissions assigned - /// - public virtual int EntityId { get; private set; } - - /// - /// The key/value pairs of user group id & single permission - /// - public EntityPermissionCollection PermissionsSet { get; private set; } - - - /// - /// Returns the aggregate permissions in the permission set - /// - /// - /// - /// This value is only calculated once - /// - public IEnumerable GetAllPermissions() - { - return PermissionsSet.GetAllPermissions(); - } - - - - + EntityId = entityId; + PermissionsSet = permissionsSet; } + + /// + /// The entity id with permissions assigned + /// + public virtual int EntityId { get; } + + /// + /// The key/value pairs of user group id & single permission + /// + public EntityPermissionCollection PermissionsSet { get; } + + /// + /// Returns an empty permission set + /// + /// + public static EntityPermissionSet Empty() => EmptyInstance.Value; + + /// + /// Returns the aggregate permissions in the permission set + /// + /// + /// + /// This value is only calculated once + /// + public IEnumerable GetAllPermissions() => PermissionsSet.GetAllPermissions(); } diff --git a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs index f8efe55885..704158a1af 100644 --- a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs +++ b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs @@ -1,50 +1,55 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Defines the base contract for and +/// +public interface IMembershipUser : IEntity { + string Username { get; set; } + + string Email { get; set; } + + DateTime? EmailConfirmedDate { get; set; } + /// - /// Defines the base contract for and + /// Gets or sets the raw password value /// - public interface IMembershipUser : IEntity - { - string Username { get; set; } - string Email { get; set; } - DateTime? EmailConfirmedDate { get; set; } + string? RawPasswordValue { get; set; } - /// - /// Gets or sets the raw password value - /// - string? RawPasswordValue { get; set; } + /// + /// The user's specific password config (i.e. algorithm type, etc...) + /// + string? PasswordConfiguration { get; set; } - /// - /// The user's specific password config (i.e. algorithm type, etc...) - /// - string? PasswordConfiguration { get; set; } + string? Comments { get; set; } - string? Comments { get; set; } - bool IsApproved { get; set; } - bool IsLockedOut { get; set; } - DateTime? LastLoginDate { get; set; } - DateTime? LastPasswordChangeDate { get; set; } - DateTime? LastLockoutDate { get; set; } + bool IsApproved { get; set; } - /// - /// Gets or sets the number of failed password attempts. - /// This is the number of times the password was entered incorrectly upon login. - /// - /// - /// Alias: umbracoMemberFailedPasswordAttempts - /// Part of the standard properties collection. - /// - int FailedPasswordAttempts { get; set; } + bool IsLockedOut { get; set; } - /// - /// Gets or sets the security stamp used by ASP.NET Identity - /// - string? SecurityStamp { get; set; } + DateTime? LastLoginDate { get; set; } - //object ProfileId { get; set; } - //IEnumerable Groups { get; set; } - } + DateTime? LastPasswordChangeDate { get; set; } + + DateTime? LastLockoutDate { get; set; } + + /// + /// Gets or sets the number of failed password attempts. + /// This is the number of times the password was entered incorrectly upon login. + /// + /// + /// Alias: umbracoMemberFailedPasswordAttempts + /// Part of the standard properties collection. + /// + int FailedPasswordAttempts { get; set; } + + /// + /// Gets or sets the security stamp used by ASP.NET Identity + /// + string? SecurityStamp { get; set; } + + // object ProfileId { get; set; } + // IEnumerable Groups { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IProfile.cs b/src/Umbraco.Core/Models/Membership/IProfile.cs index 395ebe0de8..f30bfd1225 100644 --- a/src/Umbraco.Core/Models/Membership/IProfile.cs +++ b/src/Umbraco.Core/Models/Membership/IProfile.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Defines the User Profile interface +/// +public interface IProfile { - /// - /// Defines the User Profile interface - /// - public interface IProfile - { - int Id { get; } - string? Name { get; } - } + int Id { get; } + + string? Name { get; } } diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index be84b4bca6..df34964954 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -1,31 +1,40 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// A readonly user group providing basic information +/// +public interface IReadOnlyUserGroup { + string? Name { get; } + + string? Icon { get; } + + int Id { get; } + + int? StartContentId { get; } + + int? StartMediaId { get; } + /// - /// A readonly user group providing basic information + /// The alias /// - public interface IReadOnlyUserGroup - { - string? Name { get; } - string? Icon { get; } - int Id { get; } - int? StartContentId { get; } - int? StartMediaId { get; } + string Alias { get; } - /// - /// The alias - /// - string Alias { get; } + // This is set to return true as default to avoid breaking changes. + bool HasAccessToAllLanguages => true; - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - IEnumerable? Permissions { get; set; } + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + IEnumerable? Permissions { get; set; } - IEnumerable AllowedSections { get; } - } + IEnumerable AllowedSections { get; } + + IEnumerable AllowedLanguages => Enumerable.Empty(); + + public bool HasAccessToLanguage( int languageId) => HasAccessToAllLanguages || AllowedLanguages.Contains(languageId); } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index c7c68dabda..6fc409a0c0 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,50 +1,52 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Defines the interface for a +/// +/// Will be left internal until a proper Membership implementation is part of the roadmap +public interface IUser : IMembershipUser, IRememberBeingDirty { + UserState UserState { get; } + + string? Name { get; set; } + + int SessionTimeout { get; set; } + + int[]? StartContentIds { get; set; } + + int[]? StartMediaIds { get; set; } + + string? Language { get; set; } + + DateTime? InvitedDate { get; set; } /// - /// Defines the interface for a + /// Gets the groups that user is part of /// - /// Will be left internal until a proper Membership implementation is part of the roadmap - public interface IUser : IMembershipUser, IRememberBeingDirty, ICanBeDirty - { - UserState UserState { get; } + IEnumerable Groups { get; } - string? Name { get; set; } - int SessionTimeout { get; set; } - int[]? StartContentIds { get; set; } - int[]? StartMediaIds { get; set; } - string? Language { get; set; } + IEnumerable AllowedSections { get; } - DateTime? InvitedDate { get; set; } + /// + /// Exposes the basic profile data + /// + IProfile ProfileData { get; } - /// - /// Gets the groups that user is part of - /// - IEnumerable Groups { get; } + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + string? Avatar { get; set; } - void RemoveGroup(string group); - void ClearGroups(); - void AddGroup(IReadOnlyUserGroup group); + /// + /// A Json blob stored for recording tour data for a user + /// + string? TourData { get; set; } - IEnumerable AllowedSections { get; } + void RemoveGroup(string group); - /// - /// Exposes the basic profile data - /// - IProfile ProfileData { get; } + void ClearGroups(); - /// - /// Will hold the media file system relative path of the users custom avatar if they uploaded one - /// - string? Avatar { get; set; } - - /// - /// A Json blob stored for recording tour data for a user - /// - string? TourData { get; set; } - } + void AddGroup(IReadOnlyUserGroup group); } diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index 96ae3c6dfb..11b97a9996 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -1,44 +1,64 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +public interface IUserGroup : IEntity, IRememberBeingDirty { - public interface IUserGroup : IEntity, IRememberBeingDirty + string Alias { get; set; } + + int? StartContentId { get; set; } + + int? StartMediaId { get; set; } + + /// + /// The icon + /// + string? Icon { get; set; } + + /// + /// The name + /// + string? Name { get; set; } + + /// + /// If this property is true it will give the group access to all languages + /// + /// This is set to return true as default to avoid breaking changes + public bool HasAccessToAllLanguages => true; + + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + IEnumerable? Permissions { get; set; } + + IEnumerable AllowedSections { get; } + + void RemoveAllowedSection(string sectionAlias); + + void AddAllowedSection(string sectionAlias); + + void ClearAllowedSections(); + + IEnumerable AllowedLanguages => Enumerable.Empty(); + + void RemoveAllowedLanguage(int languageId) { - string Alias { get; set; } - - int? StartContentId { get; set; } - int? StartMediaId { get; set; } - - /// - /// The icon - /// - string? Icon { get; set; } - - /// - /// The name - /// - string? Name { get; set; } - - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - IEnumerable? Permissions { get; set; } - - IEnumerable AllowedSections { get; } - - void RemoveAllowedSection(string sectionAlias); - - void AddAllowedSection(string sectionAlias); - - void ClearAllowedSections(); - - /// - /// Specifies the number of users assigned to this group - /// - int UserCount { get; } } + + void AddAllowedLanguage(int languageId) + { + } + + void ClearAllowedLanguages() + { + } + + /// + /// Specifies the number of users assigned to this group + /// + int UserCount { get; } } diff --git a/src/Umbraco.Core/Models/Membership/MemberCountType.cs b/src/Umbraco.Core/Models/Membership/MemberCountType.cs index 89990994e8..6ff29bdee2 100644 --- a/src/Umbraco.Core/Models/Membership/MemberCountType.cs +++ b/src/Umbraco.Core/Models/Membership/MemberCountType.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// The types of members to count +/// +public enum MemberCountType { - /// - /// The types of members to count - /// - public enum MemberCountType - { - All, - LockedOut, - Approved - } + All, + LockedOut, + Approved, } diff --git a/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs index fec933190c..a34f1a8d1d 100644 --- a/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs +++ b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs @@ -1,14 +1,16 @@ -using System; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +public class MemberExportProperty { - public class MemberExportProperty - { - public int Id { get; set; } - public string? Alias { get; set; } - public string? Name { get; set; } - public object? Value { get; set; } - public DateTime? CreateDate { get; set; } - public DateTime? UpdateDate { get; set; } - } + public int Id { get; set; } + + public string? Alias { get; set; } + + public string? Name { get; set; } + + public object? Value { get; set; } + + public DateTime? CreateDate { get; set; } + + public DateTime? UpdateDate { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs b/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs index 3e4831d9c3..f1c0463bdd 100644 --- a/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs +++ b/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs @@ -1,22 +1,22 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// The data stored against the user for their password configuration +/// +[DataContract(Name = "userPasswordSettings", Namespace = "")] +public class PersistedPasswordSettings { /// - /// The data stored against the user for their password configuration + /// The algorithm name /// - [DataContract(Name = "userPasswordSettings", Namespace = "")] - public class PersistedPasswordSettings - { - /// - /// The algorithm name - /// - /// - /// This doesn't explicitly need to map to a 'true' algorithm name, this may match an algorithm name alias that - /// uses many different options such as PBKDF2.ASPNETCORE.V3 which would map to the aspnetcore's v3 implementation of PBKDF2 - /// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. - /// - [DataMember(Name = "hashAlgorithm")] - public string? HashAlgorithm { get; set; } - } + /// + /// This doesn't explicitly need to map to a 'true' algorithm name, this may match an algorithm name alias that + /// uses many different options such as PBKDF2.ASPNETCORE.V3 which would map to the aspnetcore's v3 implementation of + /// PBKDF2 + /// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. + /// + [DataMember(Name = "hashAlgorithm")] + public string? HashAlgorithm { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs index 24543337ba..09865a61bb 100644 --- a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs @@ -1,70 +1,111 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable { - public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable + public ReadOnlyUserGroup( + int id, + string? name, + string? icon, + int? startContentId, + int? startMediaId, + string? alias, + IEnumerable allowedLanguages, + IEnumerable allowedSections, + IEnumerable? permissions, + bool hasAccessToAllLanguages) { - public ReadOnlyUserGroup(int id, string? name, string? icon, int? startContentId, int? startMediaId, string? @alias, - IEnumerable allowedSections, IEnumerable? permissions) - { - Name = name ?? string.Empty; - Icon = icon; - Id = id; - Alias = alias ?? string.Empty; - AllowedSections = allowedSections.ToArray(); - Permissions = permissions?.ToArray(); + Name = name ?? string.Empty; + Icon = icon; + Id = id; + Alias = alias ?? string.Empty; + AllowedLanguages = allowedLanguages.ToArray(); + AllowedSections = allowedSections.ToArray(); + Permissions = permissions?.ToArray(); - //Zero is invalid and will be treated as Null - StartContentId = startContentId == 0 ? null : startContentId; - StartMediaId = startMediaId == 0 ? null : startMediaId; - } - - public int Id { get; private set; } - public string Name { get; private set; } - public string? Icon { get; private set; } - public int? StartContentId { get; private set; } - public int? StartMediaId { get; private set; } - public string Alias { get; private set; } - - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - public IEnumerable? Permissions { get; set; } - public IEnumerable AllowedSections { get; private set; } - - public bool Equals(ReadOnlyUserGroup? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Alias, other.Alias); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((ReadOnlyUserGroup) obj); - } - - public override int GetHashCode() - { - return Alias?.GetHashCode() ?? base.GetHashCode(); - } - - public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right) - { - return Equals(left, right); - } - - public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) - { - return !Equals(left, right); - } + // Zero is invalid and will be treated as Null + StartContentId = startContentId == 0 ? null : startContentId; + StartMediaId = startMediaId == 0 ? null : startMediaId; + HasAccessToAllLanguages = hasAccessToAllLanguages; } + + [Obsolete("please use ctor that takes allowedActions & hasAccessToAllLanguages instead, scheduled for removal in v12")] + public ReadOnlyUserGroup( + int id, + string? name, + string? icon, + int? startContentId, + int? startMediaId, + string? alias, + IEnumerable allowedSections, + IEnumerable? permissions) + : this(id, name, icon, startContentId, startMediaId, alias, Enumerable.Empty(), allowedSections, permissions, true) + { + } + + public int Id { get; } + + public bool Equals(ReadOnlyUserGroup? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return string.Equals(Alias, other.Alias); + } + + public string Name { get; } + + public string? Icon { get; } + + public int? StartContentId { get; } + + public int? StartMediaId { get; } + + public string Alias { get; } + + public bool HasAccessToAllLanguages { get; set; } + + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + public IEnumerable? Permissions { get; set; } + + public IEnumerable AllowedLanguages { get; private set; } + public IEnumerable AllowedSections { get; private set; } + + public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => Equals(left, right); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((ReadOnlyUserGroup)obj); + } + + public override int GetHashCode() => Alias?.GetHashCode() ?? base.GetHashCode(); + + public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 463b44c73e..4607b7c811 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -1,421 +1,466 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Represents a backoffice user +/// +[Serializable] +[DataContract(IsReference = true)] +public class User : EntityBase, IUser, IProfile { + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> IntegerEnumerableComparer = + new( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); + + private IEnumerable? _allowedSections; + private string? _avatar; + private string _email; + private DateTime? _emailConfirmedDate; + private int _failedLoginAttempts; + private DateTime? _invitedDate; + private bool _isApproved; + private bool _isLockedOut; + private string? _language; + private DateTime? _lastLockoutDate; + private DateTime? _lastLoginDate; + private DateTime? _lastPasswordChangedDate; + + private string _name; + private string? _passwordConfig; + private string? _rawPasswordValue; + private string? _securityStamp; + private int _sessionTimeout; + private int[]? _startContentIds; + private int[]? _startMediaIds; + private string? _tourData; + private HashSet _userGroups; + + private string _username; + /// - /// Represents a backoffice user + /// Constructor for creating a new/empty user /// - [Serializable] - [DataContract(IsReference = true)] - public class User : EntityBase, IUser, IProfile + public User(GlobalSettings globalSettings) { - /// - /// Constructor for creating a new/empty user - /// - public User(GlobalSettings globalSettings) + SessionTimeout = 60; + _userGroups = new HashSet(); + _language = globalSettings.DefaultUILanguage; + _isApproved = true; + _isLockedOut = false; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; + + // cannot be null + _rawPasswordValue = string.Empty; + _username = string.Empty; + _email = string.Empty; + _name = string.Empty; + } + + /// + /// Constructor for creating a new/empty user + /// + /// + /// + /// + /// + /// + public User(GlobalSettings globalSettings, string? name, string email, string username, string rawPasswordValue) + : this(globalSettings) + { + if (string.IsNullOrWhiteSpace(name)) { - SessionTimeout = 60; - _userGroups = new HashSet(); - _language = globalSettings.DefaultUILanguage; - _isApproved = true; - _isLockedOut = false; - _startContentIds = new int[] { }; - _startMediaIds = new int[] { }; - //cannot be null - _rawPasswordValue = ""; - _username = string.Empty; - _email = string.Empty; - _name = string.Empty; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); } - /// - /// Constructor for creating a new/empty user - /// - /// - /// - /// - /// - public User(GlobalSettings globalSettings, string? name, string email, string username, string rawPasswordValue) - : this(globalSettings) + if (string.IsNullOrWhiteSpace(email)) { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(email)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrEmpty(rawPasswordValue)) throw new ArgumentException("Value cannot be null or empty.", nameof(rawPasswordValue)); - - _name = name; - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - _userGroups = new HashSet(); - _isApproved = true; - _isLockedOut = false; - _startContentIds = new int[] { }; - _startMediaIds = new int[] { }; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(email)); } - /// - /// Constructor for creating a new User instance for an existing user - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public User(GlobalSettings globalSettings, int id, string? name, string email, string? username, - string? rawPasswordValue, string? passwordConfig, - IEnumerable userGroups, int[] startContentIds, int[] startMediaIds) - : this(globalSettings) + if (string.IsNullOrWhiteSpace(username)) { - //we allow whitespace for this value so just check null - if (rawPasswordValue == null) throw new ArgumentNullException(nameof(rawPasswordValue)); - if (userGroups == null) throw new ArgumentNullException(nameof(userGroups)); - if (startContentIds == null) throw new ArgumentNullException(nameof(startContentIds)); - if (startMediaIds == null) throw new ArgumentNullException(nameof(startMediaIds)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - - Id = id; - _name = name; - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - _passwordConfig = passwordConfig; - _userGroups = new HashSet(userGroups); - _isApproved = true; - _isLockedOut = false; - _startContentIds = startContentIds; - _startMediaIds = startMediaIds; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); } - private string _name; - private string? _securityStamp; - private string? _avatar; - private string? _tourData; - private int _sessionTimeout; - private int[]? _startContentIds; - private int[]? _startMediaIds; - private int _failedLoginAttempts; - - private string _username; - private DateTime? _emailConfirmedDate; - private DateTime? _invitedDate; - private string _email; - private string? _rawPasswordValue; - private string? _passwordConfig; - private IEnumerable? _allowedSections; - private HashSet _userGroups; - private bool _isApproved; - private bool _isLockedOut; - private string? _language; - private DateTime? _lastPasswordChangedDate; - private DateTime? _lastLoginDate; - private DateTime? _lastLockoutDate; - - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> IntegerEnumerableComparer = - new DelegateEqualityComparer>( - (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), - enum1 => enum1.GetHashCode()); - - - [DataMember] - public DateTime? EmailConfirmedDate + if (string.IsNullOrEmpty(rawPasswordValue)) { - get => _emailConfirmedDate; - set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + throw new ArgumentException("Value cannot be null or empty.", nameof(rawPasswordValue)); } - [DataMember] - public DateTime? InvitedDate + _name = name; + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + _userGroups = new HashSet(); + _isApproved = true; + _isLockedOut = false; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; + } + + /// + /// Constructor for creating a new User instance for an existing user + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public User( + GlobalSettings globalSettings, + int id, + string? name, + string email, + string? username, + string? rawPasswordValue, + string? passwordConfig, + IEnumerable userGroups, + int[] startContentIds, + int[] startMediaIds) + : this(globalSettings) + { + // we allow whitespace for this value so just check null + if (rawPasswordValue == null) { - get => _invitedDate; - set => SetPropertyValueAndDetectChanges(value, ref _invitedDate, nameof(InvitedDate)); + throw new ArgumentNullException(nameof(rawPasswordValue)); } - [DataMember] - public string Username + if (userGroups == null) { - get => _username; - set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + throw new ArgumentNullException(nameof(userGroups)); } - [DataMember] - public string Email + if (string.IsNullOrWhiteSpace(name)) { - get => _email; - set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); } - [IgnoreDataMember] - public string? RawPasswordValue + if (string.IsNullOrWhiteSpace(username)) { - get => _rawPasswordValue; - set => SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); } - [IgnoreDataMember] - public string? PasswordConfiguration - { - get => _passwordConfig; - set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); - } + Id = id; + _name = name; + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + _passwordConfig = passwordConfig; + _userGroups = new HashSet(userGroups); + _isApproved = true; + _isLockedOut = false; + _startContentIds = startContentIds ?? throw new ArgumentNullException(nameof(startContentIds)); + _startMediaIds = startMediaIds ?? throw new ArgumentNullException(nameof(startMediaIds)); + } - [DataMember] - public bool IsApproved - { - get => _isApproved; - set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); - } + [DataMember] + public DateTime? EmailConfirmedDate + { + get => _emailConfirmedDate; + set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + } - [IgnoreDataMember] - public bool IsLockedOut - { - get => _isLockedOut; - set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); - } + [DataMember] + public DateTime? InvitedDate + { + get => _invitedDate; + set => SetPropertyValueAndDetectChanges(value, ref _invitedDate, nameof(InvitedDate)); + } - [IgnoreDataMember] - public DateTime? LastLoginDate - { - get => _lastLoginDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); - } + [DataMember] + public string Username + { + get => _username; + set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + } - [IgnoreDataMember] - public DateTime? LastPasswordChangeDate - { - get => _lastPasswordChangedDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangedDate, nameof(LastPasswordChangeDate)); - } + [DataMember] + public string Email + { + get => _email; + set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + } - [IgnoreDataMember] - public DateTime? LastLockoutDate - { - get => _lastLockoutDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); - } + [IgnoreDataMember] + public string? RawPasswordValue + { + get => _rawPasswordValue; + set => SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); + } - [IgnoreDataMember] - public int FailedPasswordAttempts - { - get => _failedLoginAttempts; - set => SetPropertyValueAndDetectChanges(value, ref _failedLoginAttempts, nameof(FailedPasswordAttempts)); - } + [IgnoreDataMember] + public string? PasswordConfiguration + { + get => _passwordConfig; + set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + } - [IgnoreDataMember] - public string? Comments { get; set; } + [DataMember] + public bool IsApproved + { + get => _isApproved; + set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); + } - public UserState UserState + [IgnoreDataMember] + public bool IsLockedOut + { + get => _isLockedOut; + set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); + } + + [IgnoreDataMember] + public DateTime? LastLoginDate + { + get => _lastLoginDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); + } + + [IgnoreDataMember] + public DateTime? LastPasswordChangeDate + { + get => _lastPasswordChangedDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangedDate, nameof(LastPasswordChangeDate)); + } + + [IgnoreDataMember] + public DateTime? LastLockoutDate + { + get => _lastLockoutDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); + } + + [IgnoreDataMember] + public int FailedPasswordAttempts + { + get => _failedLoginAttempts; + set => SetPropertyValueAndDetectChanges(value, ref _failedLoginAttempts, nameof(FailedPasswordAttempts)); + } + + [IgnoreDataMember] + public string? Comments { get; set; } + + public UserState UserState + { + get { - get + if (LastLoginDate == default && IsApproved == false && InvitedDate != null) { - if (LastLoginDate == default && IsApproved == false && InvitedDate != null) - return UserState.Invited; - - if (IsLockedOut) - return UserState.LockedOut; - if (IsApproved == false) - return UserState.Disabled; - - // User is not disabled or locked and has never logged in before - if (LastLoginDate == default && IsApproved && IsLockedOut == false) - return UserState.Inactive; - - return UserState.Active; + return UserState.Invited; } - } - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } - - public IEnumerable AllowedSections - { - get { return _allowedSections ?? (_allowedSections = new List(_userGroups.SelectMany(x => x.AllowedSections).Distinct())); } - } - - public IProfile ProfileData => new WrappedUserProfile(this); - - /// - /// The security stamp used by ASP.Net identity - /// - [IgnoreDataMember] - public string? SecurityStamp - { - get => _securityStamp; - set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); - } - - [DataMember] - public string? Avatar - { - get => _avatar; - set => SetPropertyValueAndDetectChanges(value, ref _avatar, nameof(Avatar)); - } - - /// - /// A Json blob stored for recording tour data for a user - /// - [DataMember] - public string? TourData - { - get => _tourData; - set => SetPropertyValueAndDetectChanges(value, ref _tourData, nameof(TourData)); - } - - /// - /// Gets or sets the session timeout. - /// - /// - /// The session timeout. - /// - [DataMember] - public int SessionTimeout - { - get => _sessionTimeout; - set => SetPropertyValueAndDetectChanges(value, ref _sessionTimeout, nameof(SessionTimeout)); - } - - /// - /// Gets or sets the start content id. - /// - /// - /// The start content id. - /// - [DataMember] - [DoNotClone] - public int[]? StartContentIds - { - get => _startContentIds; - set => SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), IntegerEnumerableComparer); - } - - /// - /// Gets or sets the start media id. - /// - /// - /// The start media id. - /// - [DataMember] - [DoNotClone] - public int[]? StartMediaIds - { - get => _startMediaIds; - set => SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), IntegerEnumerableComparer); - } - - [DataMember] - public string? Language - { - get => _language; - set => SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); - } - - /// - /// Gets the groups that user is part of - /// - [DataMember] - public IEnumerable Groups => _userGroups; - - public void RemoveGroup(string group) - { - foreach (var userGroup in _userGroups.ToArray()) + if (IsLockedOut) { - if (userGroup.Alias == group) - { - _userGroups.Remove(userGroup); - //reset this flag so it's rebuilt with the assigned groups - _allowedSections = null; - OnPropertyChanged(nameof(Groups)); - } + return UserState.LockedOut; } - } - public void ClearGroups() - { - if (_userGroups.Count > 0) + if (IsApproved == false) { - _userGroups.Clear(); - //reset this flag so it's rebuilt with the assigned groups + return UserState.Disabled; + } + + // User is not disabled or locked and has never logged in before + if (LastLoginDate == default && IsApproved && IsLockedOut == false) + { + return UserState.Inactive; + } + + return UserState.Active; + } + } + + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } + + public IEnumerable AllowedSections => _allowedSections ??= new List(_userGroups + .SelectMany(x => x.AllowedSections).Distinct()); + + public IProfile ProfileData => new WrappedUserProfile(this); + + /// + /// The security stamp used by ASP.Net identity + /// + [IgnoreDataMember] + public string? SecurityStamp + { + get => _securityStamp; + set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); + } + + [DataMember] + public string? Avatar + { + get => _avatar; + set => SetPropertyValueAndDetectChanges(value, ref _avatar, nameof(Avatar)); + } + + /// + /// A Json blob stored for recording tour data for a user + /// + [DataMember] + public string? TourData + { + get => _tourData; + set => SetPropertyValueAndDetectChanges(value, ref _tourData, nameof(TourData)); + } + + /// + /// Gets or sets the session timeout. + /// + /// + /// The session timeout. + /// + [DataMember] + public int SessionTimeout + { + get => _sessionTimeout; + set => SetPropertyValueAndDetectChanges(value, ref _sessionTimeout, nameof(SessionTimeout)); + } + + /// + /// Gets or sets the start content id. + /// + /// + /// The start content id. + /// + [DataMember] + [DoNotClone] + public int[]? StartContentIds + { + get => _startContentIds; + set => SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), IntegerEnumerableComparer); + } + + /// + /// Gets or sets the start media id. + /// + /// + /// The start media id. + /// + [DataMember] + [DoNotClone] + public int[]? StartMediaIds + { + get => _startMediaIds; + set => SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), IntegerEnumerableComparer); + } + + [DataMember] + public string? Language + { + get => _language; + set => SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); + } + + /// + /// Gets the groups that user is part of + /// + [DataMember] + public IEnumerable Groups => _userGroups; + + public void RemoveGroup(string group) + { + foreach (IReadOnlyUserGroup userGroup in _userGroups.ToArray()) + { + if (userGroup.Alias == group) + { + _userGroups.Remove(userGroup); + + // reset this flag so it's rebuilt with the assigned groups _allowedSections = null; OnPropertyChanged(nameof(Groups)); } } + } - public void AddGroup(IReadOnlyUserGroup group) + public void ClearGroups() + { + if (_userGroups.Count > 0) { - if (_userGroups.Add(group)) + _userGroups.Clear(); + + // reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; + OnPropertyChanged(nameof(Groups)); + } + } + + public void AddGroup(IReadOnlyUserGroup group) + { + if (_userGroups.Add(group)) + { + // reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; + OnPropertyChanged(nameof(Groups)); + } + } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (User)clone; + + // manually clone the start node props + clonedEntity._startContentIds = _startContentIds?.ToArray(); + clonedEntity._startMediaIds = _startMediaIds?.ToArray(); + + // need to create new collections otherwise they'll get copied by ref + clonedEntity._userGroups = new HashSet(_userGroups); + clonedEntity._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; + } + + /// + /// Internal class used to wrap the user in a profile + /// + private class WrappedUserProfile : IProfile + { + private readonly IUser _user; + + public WrappedUserProfile(IUser user) => _user = user; + + public int Id => _user.Id; + + public string? Name => _user.Name; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - //reset this flag so it's rebuilt with the assigned groups - _allowedSections = null; - OnPropertyChanged(nameof(Groups)); + return false; } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((WrappedUserProfile)obj); } - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (User)clone; - - //manually clone the start node props - clonedEntity._startContentIds = _startContentIds?.ToArray(); - clonedEntity._startMediaIds = _startMediaIds?.ToArray(); - //need to create new collections otherwise they'll get copied by ref - clonedEntity._userGroups = new HashSet(_userGroups); - clonedEntity._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; - - } - - /// - /// Internal class used to wrap the user in a profile - /// - private class WrappedUserProfile : IProfile - { - private readonly IUser _user; - - public WrappedUserProfile(IUser user) - { - _user = user; - } - - public int Id => _user.Id; - - public string? Name => _user.Name; - - private bool Equals(WrappedUserProfile other) - { - return _user.Equals(other._user); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((WrappedUserProfile) obj); - } - - public override int GetHashCode() - { - return _user.GetHashCode(); - } - } + private bool Equals(WrappedUserProfile other) => _user.Equals(other._user); + public override int GetHashCode() => _user.GetHashCode(); } } diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 5807a83abe..7369771cbd 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -1,144 +1,181 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Represents a Group for a Backoffice User +/// +[Serializable] +[DataContract(IsReference = true)] +public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup { + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> _stringEnumerableComparer = + new( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); + + private readonly IShortStringHelper _shortStringHelper; + private string _alias; + private string? _icon; + private string _name; + private bool _hasAccessToAllLanguages; + private IEnumerable? _permissions; + private List _sectionCollection; + private List _languageCollection; + private int? _startContentId; + private int? _startMediaId; + /// - /// Represents a Group for a Backoffice User + /// Constructor to create a new user group /// - [Serializable] - [DataContract(IsReference = true)] - public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup + public UserGroup(IShortStringHelper shortStringHelper) { - private readonly IShortStringHelper _shortStringHelper; - private int? _startContentId; - private int? _startMediaId; - private string _alias; - private string? _icon; - private string _name; - private IEnumerable? _permissions; - private List _sectionCollection; + _alias = string.Empty; + _name = string.Empty; + _shortStringHelper = shortStringHelper; + _sectionCollection = new List(); + _languageCollection = new List(); + } - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> StringEnumerableComparer = - new DelegateEqualityComparer>( - (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), - enum1 => enum1.GetHashCode()); + /// + /// Constructor to create an existing user group + /// + /// + /// + /// + /// + /// + /// + public UserGroup( + IShortStringHelper shortStringHelper, + int userCount, + string? alias, + string? name, + IEnumerable permissions, + string? icon) + : this(shortStringHelper) + { + UserCount = userCount; + _alias = alias ?? string.Empty; + _name = name ?? string.Empty; + _permissions = permissions; + _icon = icon; + } - /// - /// Constructor to create a new user group - /// - public UserGroup(IShortStringHelper shortStringHelper) + [DataMember] + public int? StartMediaId + { + get => _startMediaId; + set => SetPropertyValueAndDetectChanges(value, ref _startMediaId, nameof(StartMediaId)); + } + + [DataMember] + public int? StartContentId + { + get => _startContentId; + set => SetPropertyValueAndDetectChanges(value, ref _startContentId, nameof(StartContentId)); + } + + [DataMember] + public string? Icon + { + get => _icon; + set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); + } + + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), ref _alias!, + nameof(Alias)); + } + + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } + + [DataMember] + public bool HasAccessToAllLanguages + { + get => _hasAccessToAllLanguages; + set => SetPropertyValueAndDetectChanges(value, ref _hasAccessToAllLanguages, nameof(HasAccessToAllLanguages)); + } + + /// + /// The set of default permissions for the user group + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + [DataMember] + public IEnumerable? Permissions + { + get => _permissions; + set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), _stringEnumerableComparer); + } + + public IEnumerable AllowedSections => _sectionCollection; + + public int UserCount { get; } + + public void RemoveAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias)) { - _alias = string.Empty; - _name = string.Empty; - _shortStringHelper = shortStringHelper; - _sectionCollection = new List(); - } - - /// - /// Constructor to create an existing user group - /// - /// - /// - /// - /// - /// - public UserGroup(IShortStringHelper shortStringHelper, int userCount, string? alias, string? name, IEnumerable permissions, string? icon) - : this(shortStringHelper) - { - UserCount = userCount; - _alias = alias ?? string.Empty; - _name = name ?? string.Empty; - _permissions = permissions; - _icon = icon; - } - - [DataMember] - public int? StartMediaId - { - get => _startMediaId; - set => SetPropertyValueAndDetectChanges(value, ref _startMediaId, nameof(StartMediaId)); - } - - [DataMember] - public int? StartContentId - { - get => _startContentId; - set => SetPropertyValueAndDetectChanges(value, ref _startContentId, nameof(StartContentId)); - } - - [DataMember] - public string? Icon - { - get => _icon; - set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); - } - - [DataMember] - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), ref _alias!, nameof(Alias)); - } - - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } - - /// - /// The set of default permissions for the user group - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - [DataMember] - public IEnumerable? Permissions - { - get => _permissions; - set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), StringEnumerableComparer); - } - - public IEnumerable AllowedSections - { - get => _sectionCollection; - } - - public void RemoveAllowedSection(string sectionAlias) - { - if (_sectionCollection.Contains(sectionAlias)) - _sectionCollection.Remove(sectionAlias); - } - - public void AddAllowedSection(string sectionAlias) - { - if (_sectionCollection.Contains(sectionAlias) == false) - _sectionCollection.Add(sectionAlias); - } - - public void ClearAllowedSections() - { - _sectionCollection.Clear(); - } - - public int UserCount { get; } - - protected override void PerformDeepClone(object clone) - { - - base.PerformDeepClone(clone); - - var clonedEntity = (UserGroup)clone; - - //manually clone the start node props - clonedEntity._sectionCollection = new List(_sectionCollection); + _sectionCollection.Remove(sectionAlias); } } + + public void AddAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias) == false) + { + _sectionCollection.Add(sectionAlias); + } + } + + public IEnumerable AllowedLanguages + { + get => _languageCollection; + } + + public void RemoveAllowedLanguage(int languageId) + { + if (_languageCollection.Contains(languageId)) + { + _languageCollection.Remove(languageId); + } + } + + public void AddAllowedLanguage(int languageId) + { + if (_languageCollection.Contains(languageId) == false) + { + _languageCollection.Add(languageId); + } + } + + public void ClearAllowedLanguages() => _languageCollection.Clear(); + + public void ClearAllowedSections() => _sectionCollection.Clear(); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (UserGroup)clone; + + // manually clone the start node props + clonedEntity._sectionCollection = new List(_sectionCollection); + } } diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs index 84b165b81e..dbfbb2f935 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -1,31 +1,30 @@ -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UserGroupExtensions { - public static class UserGroupExtensions + public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group) { - public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group) + // this will generally always be the case + if (group is IReadOnlyUserGroup readonlyGroup) { - //this will generally always be the case - var readonlyGroup = group as IReadOnlyUserGroup; - if (readonlyGroup != null) return readonlyGroup; - - //otherwise create one - return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections, group.Permissions); + return readonlyGroup; } - public static bool IsSystemUserGroup(this IUserGroup group) => - IsSystemUserGroup(group.Alias); - - public static bool IsSystemUserGroup(this IReadOnlyUserGroup group) => - IsSystemUserGroup(group.Alias); - - private static bool IsSystemUserGroup(this string? groupAlias) - { - return groupAlias == Constants.Security.AdminGroupAlias - || groupAlias == Constants.Security.SensitiveDataGroupAlias - || groupAlias == Constants.Security.TranslatorGroupAlias; - } + // otherwise create one + return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedLanguages, group.AllowedSections, group.Permissions, group.HasAccessToAllLanguages); } + + public static bool IsSystemUserGroup(this IUserGroup group) => + IsSystemUserGroup(group.Alias); + + public static bool IsSystemUserGroup(this IReadOnlyUserGroup group) => + IsSystemUserGroup(group.Alias); + + private static bool IsSystemUserGroup(this string? groupAlias) => + groupAlias == Constants.Security.AdminGroupAlias + || groupAlias == Constants.Security.SensitiveDataGroupAlias + || groupAlias == Constants.Security.TranslatorGroupAlias; } diff --git a/src/Umbraco.Core/Models/Membership/UserProfile.cs b/src/Umbraco.Core/Models/Membership/UserProfile.cs index aca757b317..51eb882a6b 100644 --- a/src/Umbraco.Core/Models/Membership/UserProfile.cs +++ b/src/Umbraco.Core/Models/Membership/UserProfile.cs @@ -1,46 +1,55 @@ -using System; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +public class UserProfile : IProfile, IEquatable { - public class UserProfile : IProfile, IEquatable + public UserProfile(int id, string? name) { - public UserProfile(int id, string? name) - { - Id = id; - Name = name; - } - - public int Id { get; private set; } - public string? Name { get; private set; } - - public bool Equals(UserProfile? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id == other.Id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((UserProfile) obj); - } - - public override int GetHashCode() - { - return Id; - } - - public static bool operator ==(UserProfile left, UserProfile right) - { - return Equals(left, right); - } - - public static bool operator !=(UserProfile left, UserProfile right) - { - return Equals(left, right) == false; - } + Id = id; + Name = name; } + + public int Id { get; } + + public string? Name { get; } + + public static bool operator ==(UserProfile left, UserProfile right) => Equals(left, right); + + public static bool operator !=(UserProfile left, UserProfile right) => Equals(left, right) == false; + + public bool Equals(UserProfile? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((UserProfile)obj); + } + + public override int GetHashCode() => Id; } diff --git a/src/Umbraco.Core/Models/Membership/UserState.cs b/src/Umbraco.Core/Models/Membership/UserState.cs index 13d2077105..e59e4d25c8 100644 --- a/src/Umbraco.Core/Models/Membership/UserState.cs +++ b/src/Umbraco.Core/Models/Membership/UserState.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// The state of a user +/// +public enum UserState { - /// - /// The state of a user - /// - public enum UserState - { - All = -1, - Active = 0, - Disabled = 1, - LockedOut = 2, - Invited = 3, - Inactive = 4 - } + All = -1, + Active = 0, + Disabled = 1, + LockedOut = 2, + Invited = 3, + Inactive = 4, } diff --git a/src/Umbraco.Core/Models/MigrationEntry.cs b/src/Umbraco.Core/Models/MigrationEntry.cs index f62dc7eb60..ab1294b13e 100644 --- a/src/Umbraco.Core/Models/MigrationEntry.cs +++ b/src/Umbraco.Core/Models/MigrationEntry.cs @@ -1,36 +1,34 @@ -using System; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class MigrationEntry : EntityBase, IMigrationEntry { - public class MigrationEntry : EntityBase, IMigrationEntry + private string? _migrationName; + private SemVersion? _version; + + public MigrationEntry() { - public MigrationEntry() - { - } + } - public MigrationEntry(int id, DateTime createDate, string migrationName, SemVersion version) - { - Id = id; - CreateDate = createDate; - _migrationName = migrationName; - _version = version; - } + public MigrationEntry(int id, DateTime createDate, string migrationName, SemVersion version) + { + Id = id; + CreateDate = createDate; + _migrationName = migrationName; + _version = version; + } - private string? _migrationName; - private SemVersion? _version; + public string? MigrationName + { + get => _migrationName; + set => SetPropertyValueAndDetectChanges(value, ref _migrationName, nameof(MigrationName)); + } - public string? MigrationName - { - get => _migrationName; - set => SetPropertyValueAndDetectChanges(value, ref _migrationName, nameof(MigrationName)); - } - - public SemVersion? Version - { - get => _version; - set => SetPropertyValueAndDetectChanges(value, ref _version, nameof(Version)); - } + public SemVersion? Version + { + get => _version; + set => SetPropertyValueAndDetectChanges(value, ref _version, nameof(Version)); } } diff --git a/src/Umbraco.Core/Models/Notification.cs b/src/Umbraco.Core/Models/Notification.cs index 95091efe1f..31d17513a6 100644 --- a/src/Umbraco.Core/Models/Notification.cs +++ b/src/Umbraco.Core/Models/Notification.cs @@ -1,20 +1,20 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class Notification { - public class Notification + public Notification(int entityId, int userId, string action, Guid? entityType) { - public Notification(int entityId, int userId, string action, Guid? entityType) - { - EntityId = entityId; - UserId = userId; - Action = action; - EntityType = entityType; - } - - public int EntityId { get; private set; } - public int UserId { get; private set; } - public string Action { get; private set; } - public Guid? EntityType { get; private set; } + EntityId = entityId; + UserId = userId; + Action = action; + EntityType = entityType; } + + public int EntityId { get; } + + public int UserId { get; } + + public string Action { get; } + + public Guid? EntityType { get; } } diff --git a/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs b/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs index 5174ee636b..85e2cfdcd6 100644 --- a/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs +++ b/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs @@ -1,32 +1,35 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class NotificationEmailBodyParams { - public class NotificationEmailBodyParams + public NotificationEmailBodyParams(string? recipientName, string? action, string? itemName, string itemId, string itemUrl, string? editedUser, string siteUrl, string summary) { - public NotificationEmailBodyParams(string? recipientName, string? action, string? itemName, string itemId, string itemUrl, string? editedUser, string siteUrl, string summary) - { - RecipientName = recipientName ?? throw new ArgumentNullException(nameof(recipientName)); - Action = action ?? throw new ArgumentNullException(nameof(action)); - ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); - ItemId = itemId ?? throw new ArgumentNullException(nameof(itemId)); - ItemUrl = itemUrl ?? throw new ArgumentNullException(nameof(itemUrl)); - Summary = summary ?? throw new ArgumentNullException(nameof(summary)); - EditedUser = editedUser ?? throw new ArgumentNullException(nameof(editedUser)); - SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); - } - - public string RecipientName { get; } - public string Action { get; } - public string ItemName { get; } - public string ItemId { get; } - public string ItemUrl { get; } - - /// - /// This will either be an HTML or text based summary depending on the email type being sent - /// - public string Summary { get; } - public string EditedUser { get; } - public string SiteUrl { get; } + RecipientName = recipientName ?? throw new ArgumentNullException(nameof(recipientName)); + Action = action ?? throw new ArgumentNullException(nameof(action)); + ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); + ItemId = itemId ?? throw new ArgumentNullException(nameof(itemId)); + ItemUrl = itemUrl ?? throw new ArgumentNullException(nameof(itemUrl)); + Summary = summary ?? throw new ArgumentNullException(nameof(summary)); + EditedUser = editedUser ?? throw new ArgumentNullException(nameof(editedUser)); + SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); } + + public string RecipientName { get; } + + public string Action { get; } + + public string ItemName { get; } + + public string ItemId { get; } + + public string ItemUrl { get; } + + /// + /// This will either be an HTML or text based summary depending on the email type being sent + /// + public string Summary { get; } + + public string EditedUser { get; } + + public string SiteUrl { get; } } diff --git a/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs b/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs index c644f7c1a6..51b1e4031e 100644 --- a/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs +++ b/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs @@ -1,19 +1,17 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class NotificationEmailSubjectParams { - - public class NotificationEmailSubjectParams + public NotificationEmailSubjectParams(string siteUrl, string? action, string? itemName) { - public NotificationEmailSubjectParams(string siteUrl, string? action, string? itemName) - { - SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); - Action = action ?? throw new ArgumentNullException(nameof(action)); - ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); - } - - public string SiteUrl { get; } - public string Action { get; } - public string ItemName { get; } + SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); + Action = action ?? throw new ArgumentNullException(nameof(action)); + ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); } + + public string SiteUrl { get; } + + public string Action { get; } + + public string ItemName { get; } } diff --git a/src/Umbraco.Core/Models/ObjectTypes.cs b/src/Umbraco.Core/Models/ObjectTypes.cs index 8e4eef3246..0f44a269cc 100644 --- a/src/Umbraco.Core/Models/ObjectTypes.cs +++ b/src/Umbraco.Core/Models/ObjectTypes.cs @@ -1,163 +1,153 @@ -using System; using System.Collections.Concurrent; using System.Reflection; using Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.Models -{ - /// - /// Provides utilities and extension methods to handle object types. - /// - public static class ObjectTypes - { - // must be concurrent to avoid thread collisions! - private static readonly ConcurrentDictionary UmbracoGuids = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary UmbracoUdiTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary UmbracoFriendlyNames = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary UmbracoTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary GuidUdiTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary GuidObjectTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary GuidTypes = new ConcurrentDictionary(); +namespace Umbraco.Cms.Core.Models; - private static FieldInfo? GetEnumField(string name) +/// +/// Provides utilities and extension methods to handle object types. +/// +public static class ObjectTypes +{ + // must be concurrent to avoid thread collisions! + private static readonly ConcurrentDictionary UmbracoGuids = new(); + private static readonly ConcurrentDictionary UmbracoUdiTypes = new(); + private static readonly ConcurrentDictionary UmbracoFriendlyNames = new(); + private static readonly ConcurrentDictionary UmbracoTypes = new(); + private static readonly ConcurrentDictionary GuidUdiTypes = new(); + private static readonly ConcurrentDictionary GuidObjectTypes = new(); + private static readonly ConcurrentDictionary GuidTypes = new(); + + /// + /// Gets the Umbraco object type corresponding to a name. + /// + public static UmbracoObjectTypes GetUmbracoObjectType(string name) => + (UmbracoObjectTypes)Enum.Parse(typeof(UmbracoObjectTypes), name, true); + + private static FieldInfo? GetEnumField(string name) => + typeof(UmbracoObjectTypes).GetField(name, BindingFlags.Public | BindingFlags.Static); + + private static FieldInfo? GetEnumField(Guid guid) + { + FieldInfo[] fields = typeof(UmbracoObjectTypes).GetFields(BindingFlags.Public | BindingFlags.Static); + foreach (FieldInfo field in fields) { - return typeof (UmbracoObjectTypes).GetField(name, BindingFlags.Public | BindingFlags.Static); + UmbracoObjectTypeAttribute? attribute = field.GetCustomAttribute(false); + if (attribute != null && attribute.ObjectId == guid) + { + return field; + } } - private static FieldInfo? GetEnumField(Guid guid) + return null; + } + + #region Guid object type utilities + + /// + /// Gets the Umbraco object type corresponding to an object type Guid. + /// + public static UmbracoObjectTypes GetUmbracoObjectType(Guid objectType) => + GuidObjectTypes.GetOrAdd(objectType, t => { - var fields = typeof (UmbracoObjectTypes).GetFields(BindingFlags.Public | BindingFlags.Static); - foreach (var field in fields) + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var attribute = field.GetCustomAttribute(false); - if (attribute != null && attribute.ObjectId == guid) return field; + return UmbracoObjectTypes.Unknown; } - return null; - } + return (UmbracoObjectTypes?)field.GetValue(null) ?? UmbracoObjectTypes.Unknown; + }); - /// - /// Gets the Umbraco object type corresponding to a name. - /// - public static UmbracoObjectTypes GetUmbracoObjectType(string name) + /// + /// Gets the Udi type corresponding to an object type Guid. + /// + public static string GetUdiType(Guid objectType) => + GuidUdiTypes.GetOrAdd(objectType, t => { - return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, true); - } - - #region Guid object type utilities - - /// - /// Gets the Umbraco object type corresponding to an object type Guid. - /// - public static UmbracoObjectTypes GetUmbracoObjectType(Guid objectType) - { - return GuidObjectTypes.GetOrAdd(objectType, t => + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var field = GetEnumField(objectType); - if (field == null) return UmbracoObjectTypes.Unknown; + return Constants.UdiEntityType.Unknown; + } - return (UmbracoObjectTypes?)field.GetValue(null) ?? UmbracoObjectTypes.Unknown; - }); - } + UmbracoUdiTypeAttribute? attribute = field.GetCustomAttribute(false); + return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; + }); - /// - /// Gets the Udi type corresponding to an object type Guid. - /// - public static string GetUdiType(Guid objectType) + /// + /// Gets the CLR type corresponding to an object type Guid. + /// + public static Type? GetClrType(Guid objectType) => + GuidTypes.GetOrAdd(objectType, t => { - return GuidUdiTypes.GetOrAdd(objectType, t => + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var field = GetEnumField(objectType); - if (field == null) return Constants.UdiEntityType.Unknown; + return null; + } - var attribute = field.GetCustomAttribute(false); - return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; - }); - } + UmbracoObjectTypeAttribute? attribute = field.GetCustomAttribute(false); + return attribute?.ModelType; + }); - /// - /// Gets the CLR type corresponding to an object type Guid. - /// - public static Type? GetClrType(Guid objectType) + #endregion + + #region UmbracoObjectTypes extension methods + + /// + /// Gets the object type Guid corresponding to this Umbraco object type. + /// + public static Guid GetGuid(this UmbracoObjectTypes objectType) => + UmbracoGuids.GetOrAdd(objectType, t => { - return GuidTypes.GetOrAdd(objectType, t => - { - var field = GetEnumField(objectType); - if (field == null) return null; + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoObjectTypeAttribute? attribute = field?.GetCustomAttribute(false); - var attribute = field.GetCustomAttribute(false); - return attribute?.ModelType; - }); - } + return attribute?.ObjectId ?? Guid.Empty; + }); - #endregion - - #region UmbracoObjectTypes extension methods - - /// - /// Gets the object type Guid corresponding to this Umbraco object type. - /// - public static Guid GetGuid(this UmbracoObjectTypes objectType) + /// + /// Gets the Udi type corresponding to this Umbraco object type. + /// + public static string GetUdiType(this UmbracoObjectTypes objectType) => + UmbracoUdiTypes.GetOrAdd(objectType, t => { - return UmbracoGuids.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoUdiTypeAttribute? attribute = field?.GetCustomAttribute(false); - return attribute?.ObjectId ?? Guid.Empty; - }); - } + return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; + }); - /// - /// Gets the Udi type corresponding to this Umbraco object type. - /// - public static string GetUdiType(this UmbracoObjectTypes objectType) + /// + /// Gets the name corresponding to this Umbraco object type. + /// + public static string? GetName(this UmbracoObjectTypes objectType) => + Enum.GetName(typeof(UmbracoObjectTypes), objectType); + + /// + /// Gets the friendly name corresponding to this Umbraco object type. + /// + public static string GetFriendlyName(this UmbracoObjectTypes objectType) => + UmbracoFriendlyNames.GetOrAdd(objectType, t => { - return UmbracoUdiTypes.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + FieldInfo? field = GetEnumField(t.ToString()); + FriendlyNameAttribute? attribute = field?.GetCustomAttribute(false); - return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; - }); - } + return attribute?.ToString() ?? string.Empty; + }); - /// - /// Gets the name corresponding to this Umbraco object type. - /// - public static string? GetName(this UmbracoObjectTypes objectType) + /// + /// Gets the CLR type corresponding to this Umbraco object type. + /// + public static Type? GetClrType(this UmbracoObjectTypes objectType) => + UmbracoTypes.GetOrAdd(objectType, t => { - return Enum.GetName(typeof (UmbracoObjectTypes), objectType); - } + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoObjectTypeAttribute? attribute = field?.GetCustomAttribute(false); - /// - /// Gets the friendly name corresponding to this Umbraco object type. - /// - public static string GetFriendlyName(this UmbracoObjectTypes objectType) - { - return UmbracoFriendlyNames.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + return attribute?.ModelType; + }); - return attribute?.ToString() ?? string.Empty; - }); - } - - /// - /// Gets the CLR type corresponding to this Umbraco object type. - /// - public static Type? GetClrType(this UmbracoObjectTypes objectType) - { - return UmbracoTypes.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); - - return attribute?.ModelType; - }); - } - - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs b/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs index e6c430627c..6119d2cea1 100644 --- a/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs +++ b/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs @@ -1,30 +1,41 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Xml.Linq; -namespace Umbraco.Cms.Core.Models.Packaging +namespace Umbraco.Cms.Core.Models.Packaging; + +/// +/// The model of the umbraco package data manifest (xml file) +/// +public class CompiledPackage { - /// - /// The model of the umbraco package data manifest (xml file) - /// - public class CompiledPackage - { - public FileInfo? PackageFile { get; set; } - public string Name { get; set; } = null!; - public InstallWarnings Warnings { get; set; } = new InstallWarnings(); - public IEnumerable Macros { get; set; } = null!; // TODO: make strongly typed - public IEnumerable MacroPartialViews { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Templates { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Stylesheets { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Scripts { get; set; } = null!; // TODO: make strongly typed - public IEnumerable PartialViews { get; set; } = null!; // TODO: make strongly typed - public IEnumerable DataTypes { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Languages { get; set; } = null!; // TODO: make strongly typed - public IEnumerable DictionaryItems { get; set; } = null!; // TODO: make strongly typed - public IEnumerable DocumentTypes { get; set; } = null!; // TODO: make strongly typed - public IEnumerable MediaTypes { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Documents { get; set; } = null!; - public IEnumerable Media { get; set; } = null!; - } + public FileInfo? PackageFile { get; set; } + + public string Name { get; set; } = null!; + + public InstallWarnings Warnings { get; set; } = new(); + + public IEnumerable Macros { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable MacroPartialViews { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Templates { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Stylesheets { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Scripts { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable PartialViews { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable DataTypes { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Languages { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable DictionaryItems { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable DocumentTypes { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable MediaTypes { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Documents { get; set; } = null!; + + public IEnumerable Media { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs b/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs index 0fb1c60908..794262406a 100644 --- a/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs +++ b/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs @@ -1,25 +1,20 @@ using System.Xml.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Packaging +namespace Umbraco.Cms.Core.Models.Packaging; + +/// +/// Compiled representation of a content base (Document or Media) +/// +public class CompiledPackageContentBase { + public string? ImportMode { get; set; } // this is never used + /// - /// Compiled representation of a content base (Document or Media) + /// The serialized version of the content /// - public class CompiledPackageContentBase - { - public static CompiledPackageContentBase Create(XElement xml) => - new CompiledPackageContentBase - { - XmlData = xml, - ImportMode = xml.AttributeValue("importMode") - }; + public XElement XmlData { get; set; } = null!; - public string? ImportMode { get; set; } //this is never used - - /// - /// The serialized version of the content - /// - public XElement XmlData { get; set; } = null!; - } + public static CompiledPackageContentBase Create(XElement xml) => + new() { XmlData = xml, ImportMode = xml.AttributeValue("importMode") }; } diff --git a/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs b/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs index 7cad9b5b9a..d1154f1b30 100644 --- a/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs +++ b/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Models.Packaging +public class InstallWarnings { + // TODO: Shouldn't we detect other conflicting entities too ? + public IEnumerable? ConflictingMacros { get; set; } = Enumerable.Empty(); - public class InstallWarnings - { - // TODO: Shouldn't we detect other conflicting entities too ? - public IEnumerable? ConflictingMacros { get; set; } = Enumerable.Empty(); - public IEnumerable? ConflictingTemplates { get; set; } = Enumerable.Empty(); - public IEnumerable? ConflictingStylesheets { get; set; } = Enumerable.Empty(); - } + public IEnumerable? ConflictingTemplates { get; set; } = Enumerable.Empty(); + + public IEnumerable? ConflictingStylesheets { get; set; } = Enumerable.Empty(); } diff --git a/src/Umbraco.Core/Models/PagedResult.cs b/src/Umbraco.Core/Models/PagedResult.cs index f15768cc2d..6dbe6dd703 100644 --- a/src/Umbraco.Core/Models/PagedResult.cs +++ b/src/Umbraco.Core/Models/PagedResult.cs @@ -1,56 +1,55 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a paged result for a model collection +/// +/// +[DataContract(Name = "pagedCollection", Namespace = "")] +public abstract class PagedResult { - /// - /// Represents a paged result for a model collection - /// - /// - [DataContract(Name = "pagedCollection", Namespace = "")] - public abstract class PagedResult + public PagedResult(long totalItems, long pageNumber, long pageSize) { - public PagedResult(long totalItems, long pageNumber, long pageSize) - { - TotalItems = totalItems; - PageNumber = pageNumber; - PageSize = pageSize; + TotalItems = totalItems; + PageNumber = pageNumber; + PageSize = pageSize; - if (pageSize > 0) - { - TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); - } - else - { - TotalPages = 1; - } + if (pageSize > 0) + { + TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); } - - [DataMember(Name = "pageNumber")] - public long PageNumber { get; private set; } - - [DataMember(Name = "pageSize")] - public long PageSize { get; private set; } - - [DataMember(Name = "totalPages")] - public long TotalPages { get; private set; } - - [DataMember(Name = "totalItems")] - public long TotalItems { get; private set; } - - /// - /// Calculates the skip size based on the paged parameters specified - /// - /// - /// Returns 0 if the page number or page size is zero - /// - public int GetSkipSize() + else { - if (PageNumber > 0 && PageSize > 0) - { - return Convert.ToInt32((PageNumber - 1) * PageSize); - } - return 0; + TotalPages = 1; } } + + [DataMember(Name = "pageNumber")] + public long PageNumber { get; private set; } + + [DataMember(Name = "pageSize")] + public long PageSize { get; private set; } + + [DataMember(Name = "totalPages")] + public long TotalPages { get; private set; } + + [DataMember(Name = "totalItems")] + public long TotalItems { get; private set; } + + /// + /// Calculates the skip size based on the paged parameters specified + /// + /// + /// Returns 0 if the page number or page size is zero + /// + public int GetSkipSize() + { + if (PageNumber > 0 && PageSize > 0) + { + return Convert.ToInt32((PageNumber - 1) * PageSize); + } + + return 0; + } } diff --git a/src/Umbraco.Core/Models/PagedResultOfT.cs b/src/Umbraco.Core/Models/PagedResultOfT.cs index 125256ec3b..c2d11a4f82 100644 --- a/src/Umbraco.Core/Models/PagedResultOfT.cs +++ b/src/Umbraco.Core/Models/PagedResultOfT.cs @@ -1,20 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ - /// - /// Represents a paged result for a model collection - /// - /// - [DataContract(Name = "pagedCollection", Namespace = "")] - public class PagedResult : PagedResult - { - public PagedResult(long totalItems, long pageNumber, long pageSize) - : base(totalItems, pageNumber, pageSize) - { } +namespace Umbraco.Cms.Core.Models; - [DataMember(Name = "items")] - public IEnumerable? Items { get; set; } +/// +/// Represents a paged result for a model collection +/// +/// +[DataContract(Name = "pagedCollection", Namespace = "")] +public class PagedResult : PagedResult +{ + public PagedResult(long totalItems, long pageNumber, long pageSize) + : base(totalItems, pageNumber, pageSize) + { } + + [DataMember(Name = "items")] + public IEnumerable? Items { get; set; } } diff --git a/src/Umbraco.Core/Models/PartialView.cs b/src/Umbraco.Core/Models/PartialView.cs index ffa9412c51..2900674570 100644 --- a/src/Umbraco.Core/Models/PartialView.cs +++ b/src/Umbraco.Core/Models/PartialView.cs @@ -1,25 +1,22 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Partial View file +/// +[Serializable] +[DataContract(IsReference = true)] +public class PartialView : File, IPartialView { - /// - /// Represents a Partial View file - /// - [Serializable] - [DataContract(IsReference = true)] - public class PartialView : File, IPartialView + public PartialView(PartialViewType viewType, string path) + : this(viewType, path, null) { - public PartialView(PartialViewType viewType, string path) - : this(viewType, path, null) - { } - - public PartialView(PartialViewType viewType, string path, Func? getFileContent) - : base(path, getFileContent) - { - ViewType = viewType; - } - - public PartialViewType ViewType { get; set; } } + + public PartialView(PartialViewType viewType, string path, Func? getFileContent) + : base(path, getFileContent) => + ViewType = viewType; + + public PartialViewType ViewType { get; set; } } diff --git a/src/Umbraco.Core/Models/PartialViewMacroModel.cs b/src/Umbraco.Core/Models/PartialViewMacroModel.cs index 662894b39f..0d999d5dd6 100644 --- a/src/Umbraco.Core/Models/PartialViewMacroModel.cs +++ b/src/Umbraco.Core/Models/PartialViewMacroModel.cs @@ -1,31 +1,33 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The model used when rendering Partial View Macros +/// +public class PartialViewMacroModel : IContentModel { - /// - /// The model used when rendering Partial View Macros - /// - public class PartialViewMacroModel : IContentModel + public PartialViewMacroModel( + IPublishedContent page, + int macroId, + string? macroAlias, + string? macroName, + IDictionary macroParams) { - - public PartialViewMacroModel(IPublishedContent page, - int macroId, - string? macroAlias, - string? macroName, - IDictionary macroParams) - { - Content = page; - MacroParameters = macroParams; - MacroName = macroName; - MacroAlias = macroAlias; - MacroId = macroId; - } - - public IPublishedContent Content { get; } - public string? MacroName { get; } - public string? MacroAlias { get; } - public int MacroId { get; } - public IDictionary MacroParameters { get; } + Content = page; + MacroParameters = macroParams; + MacroName = macroName; + MacroAlias = macroAlias; + MacroId = macroId; } + + public string? MacroName { get; } + + public string? MacroAlias { get; } + + public int MacroId { get; } + + public IDictionary MacroParameters { get; } + + public IPublishedContent Content { get; } } diff --git a/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs b/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs index aea801719f..ecbf22323b 100644 --- a/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs +++ b/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs @@ -1,38 +1,39 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for the PartialViewMacroModel object +/// +public static class PartialViewMacroModelExtensions { /// - /// Extension methods for the PartialViewMacroModel object + /// Attempt to get a Macro parameter from a PartialViewMacroModel and return a default value otherwise /// - public static class PartialViewMacroModelExtensions + /// + /// + /// + /// Parameter value if available, the default value that was passed otherwise. + public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias, T defaultValue) { - /// - /// Attempt to get a Macro parameter from a PartialViewMacroModel and return a default value otherwise - /// - /// - /// - /// - /// Parameter value if available, the default value that was passed otherwise. - public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias, T defaultValue) + if (partialViewMacroModel.MacroParameters.ContainsKey(parameterAlias) == false || + string.IsNullOrEmpty(partialViewMacroModel.MacroParameters[parameterAlias]?.ToString())) { - if (partialViewMacroModel.MacroParameters.ContainsKey(parameterAlias) == false || string.IsNullOrEmpty(partialViewMacroModel.MacroParameters[parameterAlias]?.ToString())) - return defaultValue; - - var attempt = partialViewMacroModel.MacroParameters[parameterAlias].TryConvertTo(typeof(T)); - - return attempt.Success ? (T?) attempt.Result : defaultValue; + return defaultValue; } - /// - /// Attempt to get a Macro parameter from a PartialViewMacroModel - /// - /// - /// - /// Parameter value if available, the default value for the type otherwise. - public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias) - { - return partialViewMacroModel.GetParameterValue(parameterAlias, default(T)); - } + Attempt attempt = partialViewMacroModel.MacroParameters[parameterAlias].TryConvertTo(typeof(T)); + + return attempt.Success ? (T?)attempt.Result : defaultValue; } + + /// + /// Attempt to get a Macro parameter from a PartialViewMacroModel + /// + /// + /// + /// Parameter value if available, the default value for the type otherwise. + public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias) => + partialViewMacroModel.GetParameterValue(parameterAlias, default(T)); } diff --git a/src/Umbraco.Core/Models/PartialViewType.cs b/src/Umbraco.Core/Models/PartialViewType.cs index 5dc6dbc59c..65499be9a2 100644 --- a/src/Umbraco.Core/Models/PartialViewType.cs +++ b/src/Umbraco.Core/Models/PartialViewType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum PartialViewType : byte { - public enum PartialViewType : byte - { - Unknown = 0, // default - PartialView = 1, - PartialViewMacro = 2 - } + Unknown = 0, // default + PartialView = 1, + PartialViewMacro = 2, } diff --git a/src/Umbraco.Core/Models/PasswordChangedModel.cs b/src/Umbraco.Core/Models/PasswordChangedModel.cs index 231940f105..0cd405e604 100644 --- a/src/Umbraco.Core/Models/PasswordChangedModel.cs +++ b/src/Umbraco.Core/Models/PasswordChangedModel.cs @@ -1,20 +1,19 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing an attempt at changing a password +/// +public class PasswordChangedModel { /// - /// A model representing an attempt at changing a password + /// The error affiliated with the failing password changes, null if changing was successful /// - public class PasswordChangedModel - { - /// - /// The error affiliated with the failing password changes, null if changing was successful - /// - public ValidationResult? ChangeError { get; set; } + public ValidationResult? ChangeError { get; set; } - /// - /// If the password was reset, this is the value it has been changed to - /// - public string? ResetPassword { get; set; } - } + /// + /// If the password was reset, this is the value it has been changed to + /// + public string? ResetPassword { get; set; } } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index f4bba10c2c..e49edc9217 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -1,557 +1,658 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; +using System.Collections; using System.Runtime.Serialization; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a property. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Property : EntityBase, IProperty { + private static readonly DelegateEqualityComparer PropertyValueComparer = new( + (o, o1) => + { + if (o == null && o1 == null) + { + return true; + } + + // custom comparer for strings. + // if one is null and another is empty then they are the same + if (o is string || o1 is string) + { + return ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) || + (o != null && o1 != null && o.Equals(o1)); + } + + if (o == null || o1 == null) + { + return false; + } + + // custom comparer for enumerable + // ReSharper disable once MergeCastWithTypeCheck + if (o is IEnumerable && o1 is IEnumerable enumerable) + { + return ((IEnumerable)o).Cast().UnsortedSequenceEqual(enumerable.Cast()); + } + + return o.Equals(o1); + }, + o => o!.GetHashCode()); + + // _pvalue contains the invariant-neutral property value + private IPropertyValue? _pvalue; + + // _values contains all property values, including the invariant-neutral value + private List _values = new(); + + // _vvalues contains the (indexed) variant property values + private Dictionary? _vvalues; + /// - /// Represents a property. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class Property : EntityBase, IProperty + public Property(IPropertyType propertyType) => PropertyType = propertyType; + + /// + /// Initializes a new instance of the class. + /// + public Property(int id, IPropertyType propertyType) { - // _values contains all property values, including the invariant-neutral value - private List _values = new List(); + Id = id; + PropertyType = propertyType; + } - // _pvalue contains the invariant-neutral property value - private IPropertyValue? _pvalue; + /// + /// Returns the PropertyType, which this Property is based on + /// + [IgnoreDataMember] + public IPropertyType PropertyType { get; private set; } - // _vvalues contains the (indexed) variant property values - private Dictionary? _vvalues; - - /// - /// Initializes a new instance of the class. - /// - public Property(IPropertyType propertyType) + /// + /// Gets the list of values. + /// + [DataMember] + public IReadOnlyCollection Values + { + get => _values; + set { - PropertyType = propertyType; - } - - /// - /// Initializes a new instance of the class. - /// - public Property(int id, IPropertyType propertyType) - { - Id = id; - PropertyType = propertyType; - } - - /// - /// Creates a new instance for existing - /// - /// - /// - /// - /// Generally will contain a published and an unpublished property values - /// - /// - public static Property CreateWithValues(int id, IPropertyType propertyType, params InitialPropertyValue[] values) - { - var property = new Property(propertyType); - try - { - property.DisableChangeTracking(); - property.Id = id; - foreach(var value in values) - { - property.FactorySetValue(value.Culture, value.Segment, value.Published, value.Value); - } - property.ResetDirtyProperties(false); - return property; - } - finally - { - property.EnableChangeTracking(); - } - } - - /// - /// Used for constructing a new instance - /// - public class InitialPropertyValue - { - public InitialPropertyValue(string? culture, string? segment, bool published, object? value) - { - Culture = culture; - Segment = segment; - Published = published; - Value = value; - } - - public string? Culture { get; } - public string? Segment { get; } - public bool Published { get; } - public object? Value { get; } - } - - /// - /// Represents a property value. - /// - public class PropertyValue : IPropertyValue, IDeepCloneable, IEquatable - { - // TODO: Either we allow change tracking at this class level, or we add some special change tracking collections to the Property - // class to deal with change tracking which variants have changed - - private string? _culture; - private string? _segment; - - /// - /// Gets or sets the culture of the property. - /// - /// The culture is either null (invariant) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - public string? Culture - { - get => _culture; - set => _culture = value.IsNullOrWhiteSpace() ? null : value!.ToLowerInvariant(); - } - - /// - /// Gets or sets the segment of the property. - /// - /// The segment is either null (neutral) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - public string? Segment - { - get => _segment; - set => _segment = value?.ToLowerInvariant(); - } - - /// - /// Gets or sets the edited value of the property. - /// - public object? EditedValue { get; set; } - - /// - /// Gets or sets the published value of the property. - /// - public object? PublishedValue { get; set; } - - /// - /// Clones the property value. - /// - public IPropertyValue Clone() - => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; - - public object DeepClone() => Clone(); - - public override bool Equals(object? obj) - { - return Equals(obj as PropertyValue); - } - - public bool Equals(PropertyValue? other) - { - return other != null && - _culture == other._culture && - _segment == other._segment && - EqualityComparer.Default.Equals(EditedValue, other.EditedValue) && - EqualityComparer.Default.Equals(PublishedValue, other.PublishedValue); - } - - public override int GetHashCode() - { - var hashCode = 1885328050; - if (_culture is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_culture); - } - - if (_segment is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_segment); - } - - if (EditedValue is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(EditedValue); - } - - if (PublishedValue is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(PublishedValue); - } - return hashCode; - } - } - - private static readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( - (o, o1) => - { - if (o == null && o1 == null) return true; - - // custom comparer for strings. - // if one is null and another is empty then they are the same - if (o is string || o1 is string) - return ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) || (o != null && o1 != null && o.Equals(o1)); - - if (o == null || o1 == null) return false; - - // custom comparer for enumerable - // ReSharper disable once MergeCastWithTypeCheck - if (o is IEnumerable && o1 is IEnumerable enumerable) - return ((IEnumerable)o).Cast().UnsortedSequenceEqual(enumerable.Cast()); - - return o.Equals(o1); - }, o => o!.GetHashCode()); - - /// - /// Returns the PropertyType, which this Property is based on - /// - [IgnoreDataMember] - public IPropertyType PropertyType { get; private set; } - - /// - /// Gets the list of values. - /// - [DataMember] - public IReadOnlyCollection Values - { - get => _values; - set - { - // make sure we filter out invalid variations - // make sure we leave _vvalues null if possible - _values = value.Where(x => PropertyType?.SupportsVariation(x.Culture, x.Segment) ?? false).ToList(); - _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); - _vvalues = _values.Count > (_pvalue == null ? 0 : 1) - ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) - : null; - } - } - - /// - /// Returns the Alias of the PropertyType, which this Property is based on - /// - [DataMember] - public string Alias => PropertyType.Alias; - - /// - /// Returns the Id of the PropertyType, which this Property is based on - /// - [IgnoreDataMember] - public int PropertyTypeId => PropertyType.Id; - - /// - /// Returns the DatabaseType that the underlaying DataType is using to store its values - /// - /// - /// Only used internally when saving the property value. - /// - [IgnoreDataMember] - public ValueStorageType ValueStorageType => PropertyType.ValueStorageType; - - /// - /// Gets the value. - /// - public object? GetValue(string? culture = null, string? segment = null, bool published = false) - { - // ensure null or whitespace are nulls - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - if (!PropertyType.SupportsVariation(culture, segment)) return null; - if (culture == null && segment == null) return GetPropertyValue(_pvalue, published); - if (_vvalues == null) return null; - return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out var pvalue) - ? GetPropertyValue(pvalue, published) + // make sure we filter out invalid variations + // make sure we leave _vvalues null if possible + _values = value.Where(x => PropertyType?.SupportsVariation(x.Culture, x.Segment) ?? false).ToList(); + _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); + _vvalues = _values.Count > (_pvalue == null ? 0 : 1) + ? _values.Where(x => x != _pvalue) + .ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) : null; } + } - private object? GetPropertyValue(IPropertyValue? pvalue, bool published) + /// + /// Returns the Alias of the PropertyType, which this Property is based on + /// + [DataMember] + public string Alias => PropertyType.Alias; + + /// + /// Returns the Id of the PropertyType, which this Property is based on + /// + [IgnoreDataMember] + public int PropertyTypeId => PropertyType.Id; + + /// + /// Returns the DatabaseType that the underlaying DataType is using to store its values + /// + /// + /// Only used internally when saving the property value. + /// + [IgnoreDataMember] + public ValueStorageType ValueStorageType => PropertyType.ValueStorageType; + + /// + /// Creates a new instance for existing + /// + /// + /// + /// + /// Generally will contain a published and an unpublished property values + /// + /// + public static Property CreateWithValues(int id, IPropertyType propertyType, params InitialPropertyValue[] values) + { + var property = new Property(propertyType); + try { - if (pvalue == null) return null; - - return PropertyType.SupportsPublishing - ? (published ? pvalue.PublishedValue : pvalue.EditedValue) - : pvalue.EditedValue; - } - - // internal - must be invoked by the content item - // does *not* validate the value - content item must validate first - public void PublishValues(string? culture = "*", string? segment = "*") - { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - // if invariant or all, and invariant-neutral is supported, publish invariant-neutral - if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) - PublishValue(_pvalue); - - // then deal with everything that varies - if (_vvalues == null) return; - - // get the property values that are still relevant (wrt the property type variation), - // and match the specified culture and segment (or anything when '*'). - var pvalues = _vvalues.Where(x => - PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok - (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches - .Select(x => x.Value); - - foreach (var pvalue in pvalues) - PublishValue(pvalue); - } - - // internal - must be invoked by the content item - public void UnpublishValues(string? culture = "*", string? segment = "*") - { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - // if invariant or all, and invariant-neutral is supported, publish invariant-neutral - if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) - UnpublishValue(_pvalue); - - // then deal with everything that varies - if (_vvalues == null) return; - - // get the property values that are still relevant (wrt the property type variation), - // and match the specified culture and segment (or anything when '*'). - var pvalues = _vvalues.Where(x => - PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok - (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches - .Select(x => x.Value); - - foreach (var pvalue in pvalues) - UnpublishValue(pvalue); - } - - private void PublishValue(IPropertyValue? pvalue) - { - if (pvalue == null) return; - - if (!PropertyType.SupportsPublishing) - throw new NotSupportedException("Property type does not support publishing."); - var origValue = pvalue.PublishedValue; - pvalue.PublishedValue = ConvertAssignedValue(pvalue.EditedValue); - DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); - } - - private void UnpublishValue(IPropertyValue? pvalue) - { - if (pvalue == null) return; - - if (!PropertyType.SupportsPublishing) - throw new NotSupportedException("Property type does not support publishing."); - var origValue = pvalue.PublishedValue; - pvalue.PublishedValue = ConvertAssignedValue(null); - DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); - } - - /// - /// Sets a value. - /// - public void SetValue(object? value, string? culture = null, string? segment = null) - { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - if (!PropertyType.SupportsVariation(culture, segment)) - throw new NotSupportedException($"Variation \"{culture??""},{segment??""}\" is not supported by the property type."); - - var (pvalue, change) = GetPValue(culture, segment, true); - - if (pvalue is not null) + property.DisableChangeTracking(); + property.Id = id; + foreach (InitialPropertyValue value in values) { - var origValue = pvalue.EditedValue; - var setValue = ConvertAssignedValue(value); + property.FactorySetValue(value.Culture, value.Segment, value.Published, value.Value); + } - pvalue.EditedValue = setValue; + property.ResetDirtyProperties(false); + return property; + } + finally + { + property.EnableChangeTracking(); + } + } - DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); + /// + /// Gets the value. + /// + public object? GetValue(string? culture = null, string? segment = null, bool published = false) + { + // ensure null or whitespace are nulls + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) + { + return null; + } + + if (culture == null && segment == null) + { + return GetPropertyValue(_pvalue, published); + } + + if (_vvalues == null) + { + return null; + } + + return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out IPropertyValue? pvalue) + ? GetPropertyValue(pvalue, published) + : null; + } + + // internal - must be invoked by the content item + // does *not* validate the value - content item must validate first + public void PublishValues(string? culture = "*", string? segment = "*") + { + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + PropertyType.SupportsVariation(null, null)) + { + PublishValue(_pvalue); + } + + // then deal with everything that varies + if (_vvalues == null) + { + return; + } + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + IEnumerable pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches + .Select(x => x.Value); + + foreach (IPropertyValue pvalue in pvalues) + { + PublishValue(pvalue); + } + } + + // internal - must be invoked by the content item + public void UnpublishValues(string? culture = "*", string? segment = "*") + { + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + PropertyType.SupportsVariation(null, null)) + { + UnpublishValue(_pvalue); + } + + // then deal with everything that varies + if (_vvalues == null) + { + return; + } + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + IEnumerable pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok + (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches + (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches + .Select(x => x.Value); + + foreach (IPropertyValue pvalue in pvalues) + { + UnpublishValue(pvalue); + } + } + + /// + /// Sets a value. + /// + public void SetValue(object? value, string? culture = null, string? segment = null) + { + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) + { + throw new NotSupportedException( + $"Variation \"{culture ?? ""},{segment ?? ""}\" is not supported by the property type."); + } + + (IPropertyValue? pvalue, var change) = GetPValue(culture, segment, true); + + if (pvalue is not null) + { + var origValue = pvalue.EditedValue; + var setValue = ConvertAssignedValue(value); + + pvalue.EditedValue = setValue; + + DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); + } + } + + public object? ConvertAssignedValue(object? value) => + TryConvertAssignedValue(value, true, out var converted) ? converted : null; + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (Property)clone; + + // need to manually assign since this is a readonly property + clonedEntity.PropertyType = (PropertyType)PropertyType.DeepClone(); + } + + private object? GetPropertyValue(IPropertyValue? pvalue, bool published) + { + if (pvalue == null) + { + return null; + } + + return PropertyType.SupportsPublishing + ? published ? pvalue.PublishedValue : pvalue.EditedValue + : pvalue.EditedValue; + } + + private void PublishValue(IPropertyValue? pvalue) + { + if (pvalue == null) + { + return; + } + + if (!PropertyType.SupportsPublishing) + { + throw new NotSupportedException("Property type does not support publishing."); + } + + var origValue = pvalue.PublishedValue; + pvalue.PublishedValue = ConvertAssignedValue(pvalue.EditedValue); + DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); + } + + private void UnpublishValue(IPropertyValue? pvalue) + { + if (pvalue == null) + { + return; + } + + if (!PropertyType.SupportsPublishing) + { + throw new NotSupportedException("Property type does not support publishing."); + } + + var origValue = pvalue.PublishedValue; + pvalue.PublishedValue = ConvertAssignedValue(null); + DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); + } + + // bypasses all changes detection and is the *only* way to set the published value + private void FactorySetValue(string? culture, string? segment, bool published, object? value) + { + (IPropertyValue? pvalue, _) = GetPValue(culture, segment, true); + + if (pvalue is not null) + { + if (published && PropertyType.SupportsPublishing) + { + pvalue.PublishedValue = value; + } + else + { + pvalue.EditedValue = value; } } + } - // bypasses all changes detection and is the *only* way to set the published value - private void FactorySetValue(string? culture, string? segment, bool published, object? value) + private (IPropertyValue?, bool) GetPValue(bool create) + { + var change = false; + if (_pvalue == null) { - var (pvalue, _) = GetPValue(culture, segment, true); - - if (pvalue is not null) + if (!create) { - if (published && PropertyType.SupportsPublishing) - pvalue.PublishedValue = value; - else - pvalue.EditedValue = value; + return (null, false); } + + _pvalue = new PropertyValue(); + _values.Add(_pvalue); + change = true; } - private (IPropertyValue?, bool) GetPValue(bool create) + return (_pvalue, change); + } + + private (IPropertyValue?, bool) GetPValue(string? culture, string? segment, bool create) + { + if (culture == null && segment == null) { - var change = false; - if (_pvalue == null) - { - if (!create) return (null, false); - _pvalue = new PropertyValue(); - _values.Add(_pvalue); - change = true; - } - return (_pvalue, change); + return GetPValue(create); } - private (IPropertyValue?, bool) GetPValue(string? culture, string? segment, bool create) + var change = false; + if (_vvalues == null) { - if (culture == null && segment == null) - return GetPValue(create); + if (!create) + { + return (null, false); + } - var change = false; - if (_vvalues == null) - { - if (!create) return (null, false); - _vvalues = new Dictionary(); - change = true; - } - var k = new CompositeNStringNStringKey(culture, segment); - if (!_vvalues.TryGetValue(k, out var pvalue)) - { - if (!create) return (null, false); - pvalue = _vvalues[k] = new PropertyValue(); - pvalue.Culture = culture; - pvalue.Segment = segment; - _values.Add(pvalue); - change = true; - } - return (pvalue, change); + _vvalues = new Dictionary(); + change = true; } - /// - public object? ConvertAssignedValue(object? value) => TryConvertAssignedValue(value, true, out var converted) ? converted : null; - - /// - /// Tries to convert a value assigned to a property. - /// - /// - /// - /// - private bool TryConvertAssignedValue(object? value, bool throwOnError, out object? converted) + var k = new CompositeNStringNStringKey(culture, segment); + if (!_vvalues.TryGetValue(k, out IPropertyValue? pvalue)) { - var isOfExpectedType = IsOfExpectedPropertyType(value); - if (isOfExpectedType) + if (!create) { - converted = value; + return (null, false); + } + + pvalue = _vvalues[k] = new PropertyValue(); + pvalue.Culture = culture; + pvalue.Segment = segment; + _values.Add(pvalue); + change = true; + } + + return (pvalue, change); + } + + private static void ThrowTypeException(object? value, Type expected, string alias) => + throw new InvalidOperationException( + $"Cannot assign value \"{value}\" of type \"{value?.GetType()}\" to property \"{alias}\" expecting type \"{expected}\"."); + + /// + /// Tries to convert a value assigned to a property. + /// + /// + /// + /// + private bool TryConvertAssignedValue(object? value, bool throwOnError, out object? converted) + { + var isOfExpectedType = IsOfExpectedPropertyType(value); + if (isOfExpectedType) + { + converted = value; + return true; + } + + // isOfExpectedType is true if value is null - so if false, value is *not* null + // "garbage-in", accept what we can & convert + // throw only if conversion is not possible + var s = value?.ToString(); + converted = null; + + switch (ValueStorageType) + { + case ValueStorageType.Nvarchar: + case ValueStorageType.Ntext: + { + converted = s; return true; } - // isOfExpectedType is true if value is null - so if false, value is *not* null - // "garbage-in", accept what we can & convert - // throw only if conversion is not possible + case ValueStorageType.Integer: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } - var s = value?.ToString(); - converted = null; + Attempt convInt = value.TryConvertTo(); + if (convInt.Success) + { + converted = convInt.Result; + return true; + } - switch (ValueStorageType) - { - case ValueStorageType.Nvarchar: - case ValueStorageType.Ntext: - { - converted = s; - return true; - } + if (throwOnError) + { + ThrowTypeException(value, typeof(int), Alias); + } - case ValueStorageType.Integer: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convInt = value.TryConvertTo(); - if (convInt.Success) - { - converted = convInt.Result; - return true; - } + return false; - if (throwOnError) - ThrowTypeException(value, typeof(int), Alias ?? string.Empty); - return false; + case ValueStorageType.Decimal: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } - case ValueStorageType.Decimal: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convDecimal = value.TryConvertTo(); - if (convDecimal.Success) - { - // need to normalize the value (change the scaling factor and remove trailing zeros) - // because the underlying database is going to mess with the scaling factor anyways. - converted = convDecimal.Result.Normalize(); - return true; - } + Attempt convDecimal = value.TryConvertTo(); + if (convDecimal.Success) + { + // need to normalize the value (change the scaling factor and remove trailing zeros) + // because the underlying database is going to mess with the scaling factor anyways. + converted = convDecimal.Result.Normalize(); + return true; + } - if (throwOnError) - ThrowTypeException(value, typeof(decimal), Alias ?? string.Empty); - return false; + if (throwOnError) + { + ThrowTypeException(value, typeof(decimal), Alias); + } - case ValueStorageType.Date: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convDateTime = value.TryConvertTo(); - if (convDateTime.Success) - { - converted = convDateTime.Result; - return true; - } + return false; - if (throwOnError) - ThrowTypeException(value, typeof(DateTime), Alias ?? string.Empty); - return false; + case ValueStorageType.Date: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } - default: - throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); - } + Attempt convDateTime = value.TryConvertTo(); + if (convDateTime.Success) + { + converted = convDateTime.Result; + return true; + } + + if (throwOnError) + { + ThrowTypeException(value, typeof(DateTime), Alias); + } + + return false; + + default: + throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); + } + } + + /// + /// Determines whether a value is of the expected type for this property type. + /// + /// + /// + /// If the value is of the expected type, it can be directly assigned to the property. + /// Otherwise, some conversion is required. + /// + /// + private bool IsOfExpectedPropertyType(object? value) + { + // null values are assumed to be ok + if (value == null) + { + return true; } - private static void ThrowTypeException(object? value, Type expected, string alias) + // check if the type of the value matches the type from the DataType/PropertyEditor + // then it can be directly assigned, anything else requires conversion + Type valueType = value.GetType(); + switch (ValueStorageType) { - throw new InvalidOperationException($"Cannot assign value \"{value}\" of type \"{value?.GetType()}\" to property \"{alias}\" expecting type \"{expected}\"."); + case ValueStorageType.Integer: + return valueType == typeof(int); + case ValueStorageType.Decimal: + return valueType == typeof(decimal); + case ValueStorageType.Date: + return valueType == typeof(DateTime); + case ValueStorageType.Nvarchar: + return valueType == typeof(string); + case ValueStorageType.Ntext: + return valueType == typeof(string); + default: + throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); + } + } + + /// + /// Used for constructing a new instance + /// + public class InitialPropertyValue + { + public InitialPropertyValue(string? culture, string? segment, bool published, object? value) + { + Culture = culture; + Segment = segment; + Published = published; + Value = value; + } + + public string? Culture { get; } + + public string? Segment { get; } + + public bool Published { get; } + + public object? Value { get; } + } + + /// + /// Represents a property value. + /// + public class PropertyValue : IPropertyValue, IDeepCloneable, IEquatable + { + // TODO: Either we allow change tracking at this class level, or we add some special change tracking collections to the Property + // class to deal with change tracking which variants have changed + private string? _culture; + private string? _segment; + + /// + /// Gets or sets the culture of the property. + /// + /// + /// The culture is either null (invariant) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + public string? Culture + { + get => _culture; + set => _culture = value.IsNullOrWhiteSpace() ? null : value!.ToLowerInvariant(); + } + + public object DeepClone() => Clone(); + + public bool Equals(PropertyValue? other) => + other != null && + _culture == other._culture && + _segment == other._segment && + EqualityComparer.Default.Equals(EditedValue, other.EditedValue) && + EqualityComparer.Default.Equals(PublishedValue, other.PublishedValue); + + /// + /// Gets or sets the segment of the property. + /// + /// + /// The segment is either null (neutral) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + public string? Segment + { + get => _segment; + set => _segment = value?.ToLowerInvariant(); } /// - /// Determines whether a value is of the expected type for this property type. + /// Gets or sets the edited value of the property. /// - /// - /// If the value is of the expected type, it can be directly assigned to the property. - /// Otherwise, some conversion is required. - /// - private bool IsOfExpectedPropertyType(object? value) - { - // null values are assumed to be ok - if (value == null) - return true; + public object? EditedValue { get; set; } - // check if the type of the value matches the type from the DataType/PropertyEditor - // then it can be directly assigned, anything else requires conversion - var valueType = value.GetType(); - switch (ValueStorageType) + /// + /// Gets or sets the published value of the property. + /// + public object? PublishedValue { get; set; } + + /// + /// Clones the property value. + /// + public IPropertyValue Clone() + => new PropertyValue { - case ValueStorageType.Integer: - return valueType == typeof(int); - case ValueStorageType.Decimal: - return valueType == typeof(decimal); - case ValueStorageType.Date: - return valueType == typeof(DateTime); - case ValueStorageType.Nvarchar: - return valueType == typeof(string); - case ValueStorageType.Ntext: - return valueType == typeof(string); - default: - throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); - } - } + _culture = _culture, + _segment = _segment, + PublishedValue = PublishedValue, + EditedValue = EditedValue, + }; + public override bool Equals(object? obj) => Equals(obj as PropertyValue); - protected override void PerformDeepClone(object clone) + public override int GetHashCode() { - base.PerformDeepClone(clone); + var hashCode = 1885328050; + if (_culture is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(_culture); + } - var clonedEntity = (Property)clone; + if (_segment is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(_segment); + } - //need to manually assign since this is a readonly property - clonedEntity.PropertyType = (PropertyType) PropertyType.DeepClone(); + if (EditedValue is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(EditedValue); + } + + if (PublishedValue is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(PublishedValue); + } + + return hashCode; } } } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index 49b392ba67..dbb648df29 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -1,215 +1,217 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a collection of property values. +/// +[Serializable] +[DataContract(IsReference = true)] +public class PropertyCollection : KeyedCollection, IPropertyCollection { + private readonly object _addLocker = new(); /// - /// Represents a collection of property values. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class PropertyCollection : KeyedCollection, IPropertyCollection + public PropertyCollection() + : base(StringComparer.InvariantCultureIgnoreCase) { - private readonly object _addLocker = new object(); + } - /// - /// Initializes a new instance of the class. - /// - public PropertyCollection() - : base(StringComparer.InvariantCultureIgnoreCase) - { } + /// + /// Initializes a new instance of the class. + /// + public PropertyCollection(IEnumerable properties) + : this() => + Reset(properties); - /// - /// Initializes a new instance of the class. - /// - public PropertyCollection(IEnumerable properties) - : this() + /// + /// Occurs when the collection changes. + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + /// + /// Gets the property with the specified PropertyType. + /// + internal IProperty? this[IPropertyType propertyType] => + this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyType.Alias)); + + /// + public new void Add(IProperty property) + { + // TODO: why are we locking here and not everywhere else?! + lock (_addLocker) { - Reset(properties); - } - - /// - /// Replaces all properties, whilst maintaining validation delegates. - /// - private void Reset(IEnumerable properties) - { - //collection events will be raised in each of these calls - Clear(); - - //collection events will be raised in each of these calls - foreach (var property in properties) - Add(property); - } - - /// - /// Replaces the property at the specified index with the specified property. - /// - protected override void SetItem(int index, IProperty property) - { - var oldItem = index >= 0 ? this[index] : property; - base.SetItem(index, property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem)); - } - - /// - /// Removes the property at the specified index. - /// - protected override void RemoveItem(int index) - { - var removed = this[index]; - base.RemoveItem(index); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } - - /// - /// Inserts the specified property at the specified index. - /// - protected override void InsertItem(int index, IProperty property) - { - base.InsertItem(index, property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property)); - } - - /// - /// Removes all properties. - /// - protected override void ClearItems() - { - base.ClearItems(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - public new void Add(IProperty property) - { - lock (_addLocker) // TODO: why are we locking here and not everywhere else?! + var key = GetKeyForItem(property); + if (key != null) { - var key = GetKeyForItem(property); - if (key != null) + if (Contains(key)) { - if (Contains(key)) + // transfer id and values if ... + IProperty existing = this[key]; + + if (property.Id == 0 && existing.Id != 0) { - // transfer id and values if ... - var existing = this[key]; - - if (property.Id == 0 && existing.Id != 0) - property.Id = existing.Id; - - if (property.Values.Count == 0 && existing.Values.Count > 0) - property.Values = existing.Values.Select(x => x.Clone()).ToList(); - - // replace existing with property and return, - // SetItem invokes OnCollectionChanged (but not OnAdd) - SetItem(IndexOfKey(key), property); - return; + property.Id = existing.Id; } + + if (property.Values.Count == 0 && existing.Values.Count > 0) + { + property.Values = existing.Values.Select(x => x.Clone()).ToList(); + } + + // replace existing with property and return, + // SetItem invokes OnCollectionChanged (but not OnAdd) + SetItem(IndexOfKey(key), property); + return; } - - //collection events will be raised in InsertItem with Add - base.Add(property); - } - } - - /// - /// Gets the index for a specified property alias. - /// - private int IndexOfKey(string key) - { - for (var i = 0; i < Count; i++) - { - if (this[i].Alias?.InvariantEquals(key) ?? false) - return i; - } - return -1; - } - - protected override string GetKeyForItem(IProperty item) - { - return item.Alias!; - } - - /// - /// Gets the property with the specified PropertyType. - /// - internal IProperty? this[IPropertyType propertyType] - { - get - { - return this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyType.Alias)); - } - } - - public bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property) - { - property = this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - return property != null; - } - - /// - /// Occurs when the collection changes. - /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - public void ClearCollectionChangedEvents() => CollectionChanged = null; - - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) - { - CollectionChanged?.Invoke(this, args); - } - - - /// - public void EnsurePropertyTypes(IEnumerable propertyTypes) - { - if (propertyTypes == null) - return; - - foreach (var propertyType in propertyTypes) - Add(new Property(propertyType)); - } - - - /// - public void EnsureCleanPropertyTypes(IEnumerable propertyTypes) - { - if (propertyTypes == null) - return; - - var propertyTypesA = propertyTypes.ToArray(); - - var thisAliases = this.Select(x => x.Alias); - var typeAliases = propertyTypesA.Select(x => x.Alias); - var remove = thisAliases.Except(typeAliases).ToArray(); - foreach (var alias in remove) - { - if (alias is not null) - { - Remove(alias); - } - } - - foreach (var propertyType in propertyTypesA) - Add(new Property(propertyType)); - } - - /// - /// Deep clones. - /// - public object DeepClone() - { - var clone = new PropertyCollection(); - foreach (var property in this) - clone.Add((Property) property.DeepClone()); - return clone; + // collection events will be raised in InsertItem with Add + base.Add(property); } } + + public new bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property) + { + property = this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + return property != null; + } + + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + /// + public void EnsurePropertyTypes(IEnumerable propertyTypes) + { + if (propertyTypes == null) + { + return; + } + + foreach (IPropertyType propertyType in propertyTypes) + { + Add(new Property(propertyType)); + } + } + + /// + public void EnsureCleanPropertyTypes(IEnumerable propertyTypes) + { + if (propertyTypes == null) + { + return; + } + + IPropertyType[] propertyTypesA = propertyTypes.ToArray(); + + IEnumerable thisAliases = this.Select(x => x.Alias); + IEnumerable typeAliases = propertyTypesA.Select(x => x.Alias); + var remove = thisAliases.Except(typeAliases).ToArray(); + foreach (var alias in remove) + { + if (alias is not null) + { + Remove(alias); + } + } + + foreach (IPropertyType propertyType in propertyTypesA) + { + Add(new Property(propertyType)); + } + } + + /// + /// Deep clones. + /// + public object DeepClone() + { + var clone = new PropertyCollection(); + foreach (IProperty property in this) + { + clone.Add((Property)property.DeepClone()); + } + + return clone; + } + + /// + /// Replaces the property at the specified index with the specified property. + /// + protected override void SetItem(int index, IProperty property) + { + IProperty oldItem = index >= 0 ? this[index] : property; + base.SetItem(index, property); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem)); + } + + /// + /// Replaces all properties, whilst maintaining validation delegates. + /// + private void Reset(IEnumerable properties) + { + // collection events will be raised in each of these calls + Clear(); + + // collection events will be raised in each of these calls + foreach (IProperty property in properties) + { + Add(property); + } + } + + /// + /// Removes the property at the specified index. + /// + protected override void RemoveItem(int index) + { + IProperty removed = this[index]; + base.RemoveItem(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } + + /// + /// Inserts the specified property at the specified index. + /// + protected override void InsertItem(int index, IProperty property) + { + base.InsertItem(index, property); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property)); + } + + /// + /// Removes all properties. + /// + protected override void ClearItems() + { + base.ClearItems(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + protected override string GetKeyForItem(IProperty item) => item.Alias; + + /// + /// Gets the index for a specified property alias. + /// + private int IndexOfKey(string key) + { + for (var i = 0; i < Count; i++) + { + if (this[i].Alias?.InvariantEquals(key) ?? false) + { + return i; + } + } + + return -1; + } + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => + CollectionChanged?.Invoke(this, args); } diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs index 17e6603284..034770cdfc 100644 --- a/src/Umbraco.Core/Models/PropertyGroup.cs +++ b/src/Umbraco.Core/Models/PropertyGroup.cs @@ -1,159 +1,157 @@ -using System; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a group of property types. +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] +public class PropertyGroup : EntityBase, IEquatable { - /// - /// Represents a group of property types. - /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] - public class PropertyGroup : EntityBase, IEquatable + [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "This field is for internal use only (to allow changing item keys).")] + internal PropertyGroupCollection? Collection; + + private string _alias; + private string? _name; + private PropertyTypeCollection? _propertyTypes; + private int _sortOrder; + + private PropertyGroupType _type; + + public PropertyGroup(bool isPublishing) + : this(new PropertyTypeCollection(isPublishing)) { - [SuppressMessage("Style", "IDE1006:Naming Styles", - Justification = "This field is for internal use only (to allow changing item keys).")] - internal PropertyGroupCollection? Collection; + } - private PropertyGroupType _type; - private string? _name; - private string _alias; - private int _sortOrder; - private PropertyTypeCollection? _propertyTypes; + public PropertyGroup(PropertyTypeCollection propertyTypeCollection) + { + PropertyTypes = propertyTypeCollection; + _alias = string.Empty; + } - public PropertyGroup(bool isPublishing) - : this(new PropertyTypeCollection(isPublishing)) + /// + /// Gets or sets the type of the group. + /// + /// + /// The type. + /// + [DataMember] + public PropertyGroupType Type + { + get => _type; + set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); + } + + /// + /// Gets or sets the name of the group. + /// + /// + /// The name. + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the alias of the group. + /// + /// + /// The alias. + /// + [DataMember] + public string Alias + { + get => _alias; + set { + // If added to a collection, ensure the key is changed before setting it (this ensures the internal lookup dictionary is updated) + Collection?.ChangeKey(this, value); + + SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); } + } - public PropertyGroup(PropertyTypeCollection propertyTypeCollection) - { - PropertyTypes = propertyTypeCollection; - _alias = string.Empty; - } + /// + /// Gets or sets the sort order of the group. + /// + /// + /// The sort order. + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } - private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) + /// + /// Gets or sets a collection of property types for the group. + /// + /// + /// The property types. + /// + /// + /// Marked with DoNotClone, because we will manually deal with cloning and the event handlers. + /// + [DataMember] + [DoNotClone] + public PropertyTypeCollection? PropertyTypes + { + get => _propertyTypes; + set { - OnPropertyChanged(nameof(PropertyTypes)); - } - - /// - /// Gets or sets the type of the group. - /// - /// - /// The type. - /// - [DataMember] - public PropertyGroupType Type - { - get => _type; - set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); - } - - /// - /// Gets or sets the name of the group. - /// - /// - /// The name. - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Gets or sets the alias of the group. - /// - /// - /// The alias. - /// - [DataMember] - public string Alias - { - get => _alias; - set + if (_propertyTypes != null) { - // If added to a collection, ensure the key is changed before setting it (this ensures the internal lookup dictionary is updated) - Collection?.ChangeKey(this, value); - - SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + _propertyTypes.ClearCollectionChangedEvents(); } - } - /// - /// Gets or sets the sort order of the group. - /// - /// - /// The sort order. - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } + _propertyTypes = value; - /// - /// Gets or sets a collection of property types for the group. - /// - /// - /// The property types. - /// - /// - /// Marked with DoNotClone, because we will manually deal with cloning and the event handlers. - /// - [DataMember] - [DoNotClone] - public PropertyTypeCollection? PropertyTypes - { - get => _propertyTypes; - set + if (_propertyTypes is not null) { - if (_propertyTypes != null) + // since we're adding this collection to this group, + // we need to ensure that all the lazy values are set. + foreach (IPropertyType propertyType in _propertyTypes) { - _propertyTypes.ClearCollectionChangedEvents(); + propertyType.PropertyGroupId = new Lazy(() => Id); } - _propertyTypes = value; - - if (_propertyTypes is not null) - { - // since we're adding this collection to this group, - // we need to ensure that all the lazy values are set. - foreach (var propertyType in _propertyTypes) - propertyType.PropertyGroupId = new Lazy(() => Id); - - OnPropertyChanged(nameof(PropertyTypes)); - _propertyTypes.CollectionChanged += PropertyTypesChanged; - } - } - } - - public bool Equals(PropertyGroup? other) => - base.Equals(other) || (other != null && Type == other.Type && Alias == other.Alias); - - public override int GetHashCode() => (base.GetHashCode(), Type, Alias).GetHashCode(); - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (PropertyGroup)clone; - clonedEntity.Collection = null; - - if (clonedEntity._propertyTypes != null) - { - clonedEntity._propertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any - clonedEntity._propertyTypes = (PropertyTypeCollection)_propertyTypes!.DeepClone(); //manually deep clone - clonedEntity._propertyTypes.CollectionChanged += - clonedEntity.PropertyTypesChanged; //re-assign correct event handler + OnPropertyChanged(nameof(PropertyTypes)); + _propertyTypes.CollectionChanged += PropertyTypesChanged; } } } + + public bool Equals(PropertyGroup? other) => + base.Equals(other) || (other != null && Type == other.Type && Alias == other.Alias); + + public override int GetHashCode() => (base.GetHashCode(), Type, Alias).GetHashCode(); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (PropertyGroup)clone; + clonedEntity.Collection = null; + + if (clonedEntity._propertyTypes != null) + { + clonedEntity._propertyTypes.ClearCollectionChangedEvents(); // clear this event handler if any + clonedEntity._propertyTypes = (PropertyTypeCollection)_propertyTypes!.DeepClone(); // manually deep clone + clonedEntity._propertyTypes.CollectionChanged += + clonedEntity.PropertyTypesChanged; // re-assign correct event handler + } + } + + private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + OnPropertyChanged(nameof(PropertyTypes)); } diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index f248b12811..5e4479ec37 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -1,156 +1,162 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a collection of objects +/// +[Serializable] +[DataContract] + +// TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details +public class PropertyGroupCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { /// - /// Represents a collection of objects + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract] - // TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details - public class PropertyGroupCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable + public PropertyGroupCollection() { - /// - /// Initializes a new instance of the class. - /// - public PropertyGroupCollection() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The groups. - public PropertyGroupCollection(IEnumerable groups) => Reset(groups); + /// + /// Initializes a new instance of the class. + /// + /// The groups. + public PropertyGroupCollection(IEnumerable groups) => Reset(groups); - /// - /// Resets the collection to only contain the instances referenced in the parameter. - /// - /// The property groups. - /// - internal void Reset(IEnumerable groups) + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public object DeepClone() + { + var clone = new PropertyGroupCollection(); + foreach (PropertyGroup group in this) { - // Collection events will be raised in each of these calls - Clear(); - - // Collection events will be raised in each of these calls - foreach (var group in groups) - Add(group); + clone.Add((PropertyGroup)group.DeepClone()); } - protected override void SetItem(int index, PropertyGroup item) + return clone; + } + + public new void Add(PropertyGroup item) + { + // Ensure alias is set + if (string.IsNullOrEmpty(item.Alias)) { - var oldItem = index >= 0 ? this[index] : item; - - base.SetItem(index, item); - - oldItem.Collection = null; - item.Collection = this; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + throw new InvalidOperationException("Set an alias before adding the property group."); } - protected override void RemoveItem(int index) + // Note this is done to ensure existing groups can be renamed + if (item.HasIdentity && item.Id > 0) { - var removed = this[index]; - - base.RemoveItem(index); - - removed.Collection = null; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } - - protected override void InsertItem(int index, PropertyGroup item) - { - base.InsertItem(index, item); - - item.Collection = this; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); - } - - protected override void ClearItems() - { - foreach (var item in this) + var index = IndexOfKey(item.Id); + if (index != -1) { - item.Collection = null; - } - - base.ClearItems(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - public new void Add(PropertyGroup item) - { - // Ensure alias is set - if (string.IsNullOrEmpty(item.Alias)) - { - throw new InvalidOperationException("Set an alias before adding the property group."); - } - - // Note this is done to ensure existing groups can be renamed - if (item.HasIdentity && item.Id > 0) - { - var index = IndexOfKey(item.Id); - if (index != -1) + var keyExists = Contains(item.Alias); + if (keyExists) { - var keyExists = Contains(item.Alias); - if (keyExists) - throw new ArgumentException($"Naming conflict: changing the alias of property group '{item.Name}' would result in duplicates."); - - // Collection events will be raised in SetItem - SetItem(index, item); - return; + throw new ArgumentException( + $"Naming conflict: changing the alias of property group '{item.Name}' would result in duplicates."); } + + // Collection events will be raised in SetItem + SetItem(index, item); + return; } - else + } + else + { + var index = IndexOfKey(item.Alias); + if (index != -1) { - var index = IndexOfKey(item.Alias); - if (index != -1) - { - // Collection events will be raised in SetItem - SetItem(index, item); - return; - } + // Collection events will be raised in SetItem + SetItem(index, item); + return; } - - // Collection events will be raised in InsertItem - base.Add(item); } - internal void ChangeKey(PropertyGroup item, string newKey) => ChangeItemKey(item, newKey); + // Collection events will be raised in InsertItem + base.Add(item); + } - public bool Contains(int id) => this.IndexOfKey(id) != -1; + public bool Contains(int id) => IndexOfKey(id) != -1; - public int IndexOfKey(string key) => this.FindIndex(x => x.Alias == key); + /// + /// Resets the collection to only contain the instances referenced in the + /// parameter. + /// + /// The property groups. + /// + internal void Reset(IEnumerable groups) + { + // Collection events will be raised in each of these calls + Clear(); - public int IndexOfKey(int id) => this.FindIndex(x => x.Id == id); - - protected override string GetKeyForItem(PropertyGroup item) => item.Alias; - - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; - - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); - - public object DeepClone() + // Collection events will be raised in each of these calls + foreach (PropertyGroup group in groups) { - var clone = new PropertyGroupCollection(); - foreach (var group in this) - { - clone.Add((PropertyGroup)group.DeepClone()); - } - - return clone; + Add(group); } } + + protected override void SetItem(int index, PropertyGroup item) + { + PropertyGroup oldItem = index >= 0 ? this[index] : item; + + base.SetItem(index, item); + + oldItem.Collection = null; + item.Collection = this; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + } + + protected override void RemoveItem(int index) + { + PropertyGroup removed = this[index]; + + base.RemoveItem(index); + + removed.Collection = null; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } + + protected override void InsertItem(int index, PropertyGroup item) + { + base.InsertItem(index, item); + + item.Collection = this; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + } + + protected override void ClearItems() + { + foreach (PropertyGroup item in this) + { + item.Collection = null; + } + + base.ClearItems(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + internal void ChangeKey(PropertyGroup item, string newKey) => ChangeItemKey(item, newKey); + + public int IndexOfKey(string key) => this.FindIndex(x => x.Alias == key); + + public int IndexOfKey(int id) => this.FindIndex(x => x.Id == id); + + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + protected override string GetKeyForItem(PropertyGroup item) => item.Alias; + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => + CollectionChanged?.Invoke(this, args); } diff --git a/src/Umbraco.Core/Models/PropertyGroupExtensions.cs b/src/Umbraco.Core/Models/PropertyGroupExtensions.cs index bb12e1bc1b..95f3bce75b 100644 --- a/src/Umbraco.Core/Models/PropertyGroupExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyGroupExtensions.cs @@ -1,83 +1,82 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public static class PropertyGroupExtensions { - public static class PropertyGroupExtensions + private const char AliasSeparator = '/'; + + /// + /// Gets the local alias. + /// + /// The property group. + /// + /// The local alias. + /// + public static string? GetLocalAlias(this PropertyGroup propertyGroup) => GetLocalAlias(propertyGroup.Alias); + + internal static string? GetLocalAlias(string alias) { - private const char AliasSeparator = '/'; - - internal static string? GetLocalAlias(string alias) + var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; + if (lastIndex != -1) { - var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; - if (lastIndex != -1) - { - return alias?.Substring(lastIndex + 1); - } - - return alias; + return alias?.Substring(lastIndex + 1); } - internal static string? GetParentAlias(string? alias) - { - var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; - if (lastIndex == -1) - { - return null; - } + return alias; + } - return alias?.Substring(0, lastIndex); + internal static string? GetParentAlias(string? alias) + { + var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; + if (lastIndex == -1) + { + return null; } - /// - /// Gets the local alias. - /// - /// The property group. - /// - /// The local alias. - /// - public static string? GetLocalAlias(this PropertyGroup propertyGroup) => GetLocalAlias(propertyGroup.Alias); + return alias?.Substring(0, lastIndex); + } - /// - /// Updates the local alias. - /// - /// The property group. - /// The local alias. - public static void UpdateLocalAlias(this PropertyGroup propertyGroup, string localAlias) + /// + /// Updates the local alias. + /// + /// The property group. + /// The local alias. + public static void UpdateLocalAlias(this PropertyGroup propertyGroup, string localAlias) + { + var parentAlias = propertyGroup.GetParentAlias(); + if (string.IsNullOrEmpty(parentAlias)) { - var parentAlias = propertyGroup.GetParentAlias(); - if (string.IsNullOrEmpty(parentAlias)) - { - propertyGroup.Alias = localAlias; - } - else - { - propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; - } + propertyGroup.Alias = localAlias; } - - /// - /// Gets the parent alias. - /// - /// The property group. - /// - /// The parent alias. - /// - public static string? GetParentAlias(this PropertyGroup propertyGroup) => GetParentAlias(propertyGroup.Alias); - - /// - /// Updates the parent alias. - /// - /// The property group. - /// The parent alias. - public static void UpdateParentAlias(this PropertyGroup propertyGroup, string parentAlias) + else { - var localAlias = propertyGroup.GetLocalAlias(); - if (string.IsNullOrEmpty(parentAlias)) - { - propertyGroup.Alias = localAlias!; - } - else - { - propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; - } + propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; + } + } + + /// + /// Gets the parent alias. + /// + /// The property group. + /// + /// The parent alias. + /// + public static string? GetParentAlias(this PropertyGroup propertyGroup) => GetParentAlias(propertyGroup.Alias); + + /// + /// Updates the parent alias. + /// + /// The property group. + /// The parent alias. + public static void UpdateParentAlias(this PropertyGroup propertyGroup, string parentAlias) + { + var localAlias = propertyGroup.GetLocalAlias(); + if (string.IsNullOrEmpty(parentAlias)) + { + propertyGroup.Alias = localAlias!; + } + else + { + propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; } } } diff --git a/src/Umbraco.Core/Models/PropertyGroupType.cs b/src/Umbraco.Core/Models/PropertyGroupType.cs index 03bcbc08f0..9111bf9bb4 100644 --- a/src/Umbraco.Core/Models/PropertyGroupType.cs +++ b/src/Umbraco.Core/Models/PropertyGroupType.cs @@ -1,17 +1,17 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the type of a property group. +/// +public enum PropertyGroupType : short { /// - /// Represents the type of a property group. + /// Display property types in a group. /// - public enum PropertyGroupType : short - { - /// - /// Display property types in a group. - /// - Group = 0, - /// - /// Display property types in a tab. - /// - Tab = 1 - } + Group = 0, + + /// + /// Display property types in a tab. + /// + Tab = 1, } diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 7bd3a49baf..9ad98d66c0 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -8,235 +5,291 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the class to manage tags. +/// +public static class PropertyTagsExtensions { - /// - /// Provides extension methods for the class to manage tags. - /// - public static class PropertyTagsExtensions + // gets the tag configuration for a property + // from the datatype configuration, and the editor tag configuration attribute + public static TagConfiguration? GetTagConfiguration(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) { - // gets the tag configuration for a property - // from the datatype configuration, and the editor tag configuration attribute - public static TagConfiguration? GetTagConfiguration(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) + if (property == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var editor = propertyEditors[property.PropertyType?.PropertyEditorAlias]; - var tagAttribute = editor?.GetTagAttribute(); - if (tagAttribute == null) return null; - - var configurationObject = property.PropertyType is null ? null : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; - var configuration = ConfigurationEditor.ConfigurationAs(configurationObject); - - if (configuration?.Delimiter == default && configuration?.Delimiter is not null) - configuration.Delimiter = tagAttribute.Delimiter; - - return configuration; + throw new ArgumentNullException(nameof(property)); } - /// - /// Assign tags. - /// - /// The property. - /// - /// The tags. - /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// A culture, for multi-lingual properties. - /// - /// - public static void AssignTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, bool merge = false, string? culture = null) + IDataEditor? editor = propertyEditors[property.PropertyType?.PropertyEditorAlias]; + TagsPropertyEditorAttribute? tagAttribute = editor?.GetTagAttribute(); + if (tagAttribute == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); - if (configuration == null) - throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - - property.AssignTags(tags, merge, configuration.StorageType, serializer, configuration.Delimiter, culture); + return null; } - // assumes that parameters are consistent with the datatype configuration - private static void AssignTags(this IProperty property, IEnumerable tags, bool merge, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + var configurationObject = property.PropertyType is null + ? null + : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; + TagConfiguration? configuration = ConfigurationEditor.ConfigurationAs(configurationObject); + + if (configuration?.Delimiter == default && configuration?.Delimiter is not null) { - // set the property value - var trimmedTags = tags.Select(x => x.Trim()).ToArray(); - - if (merge) - { - var currentTags = property.GetTagsValue(storageType, serializer, delimiter); - - switch (storageType) - { - case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string - break; - - case TagsStorageType.Json: - var updatedTags = currentTags.Union(trimmedTags).ToArray(); - var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); - property.SetValue(updatedValue, culture); // json array - break; - } - } - else - { - switch (storageType) - { - case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), culture); // csv string - break; - - case TagsStorageType.Json: - var updatedValue = trimmedTags.Length == 0 ? null : serializer.Serialize(trimmedTags); - property.SetValue(updatedValue, culture); // json array - break; - } - } + configuration.Delimiter = tagAttribute.Delimiter; } - /// - /// Removes tags. - /// - /// The property. - /// - /// The tags. - /// A culture, for multi-lingual properties. - /// - /// - public static void RemoveTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, string? culture = null) + return configuration; + } + + /// + /// Assign tags. + /// + /// The property. + /// + /// The tags. + /// A value indicating whether to merge the tags with existing tags instead of replacing them. + /// A culture, for multi-lingual properties. + /// + /// + public static void AssignTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, bool merge = false, string? culture = null) + { + if (property == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); - if (configuration == null) - throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - - property.RemoveTags(tags, configuration.StorageType, serializer, configuration.Delimiter, culture); + throw new ArgumentNullException(nameof(property)); } - // assumes that parameters are consistent with the datatype configuration - private static void RemoveTags(this IProperty property, IEnumerable tags, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) { - // already empty = nothing to do - var value = property.GetValue(culture)?.ToString(); - if (string.IsNullOrWhiteSpace(value)) return; + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } + + property.AssignTags(tags, merge, configuration.StorageType, serializer, configuration.Delimiter, culture); + } + + /// + /// Removes tags. + /// + /// The property. + /// + /// The tags. + /// A culture, for multi-lingual properties. + /// + /// + public static void RemoveTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, string? culture = null) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) + { + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } + + property.RemoveTags(tags, configuration.StorageType, serializer, configuration.Delimiter, culture); + } + + // used by ContentRepositoryBase + public static IEnumerable GetTagsValue(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string? culture = null) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) + { + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } + + return property.GetTagsValue(configuration.StorageType, serializer, configuration.Delimiter, culture); + } + + /// + /// Sets tags on a content property, based on the property editor tags configuration. + /// + /// The property. + /// + /// The property value. + /// The datatype configuration. + /// A culture, for multi-lingual properties. + /// + /// The value is either a string (delimited string) or an enumeration of strings (tag list). + /// + /// This is used both by the content repositories to initialize a property with some tag values, and by the + /// content controllers to update a property with values received from the property editor. + /// + /// + public static void SetTagsValue(this IProperty property, IJsonSerializer serializer, object? value, TagConfiguration? tagConfiguration, string? culture) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (tagConfiguration == null) + { + throw new ArgumentNullException(nameof(tagConfiguration)); + } + + TagsStorageType storageType = tagConfiguration.StorageType; + var delimiter = tagConfiguration.Delimiter; + + SetTagsValue(property, value, storageType, serializer, delimiter, culture); + } + + // assumes that parameters are consistent with the datatype configuration + private static void AssignTags(this IProperty property, IEnumerable tags, bool merge, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + { + // set the property value + var trimmedTags = tags.Select(x => x.Trim()).ToArray(); + + if (merge) + { + IEnumerable currentTags = property.GetTagsValue(storageType, serializer, delimiter); - // set the property value - var trimmedTags = tags.Select(x => x.Trim()).ToArray(); - var currentTags = property.GetTagsValue(storageType, serializer, delimiter, culture); switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string + property.SetValue( + string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)).NullOrWhiteSpaceAsNull(), + culture); // csv string break; case TagsStorageType.Json: - var updatedTags = currentTags.Except(trimmedTags).ToArray(); + var updatedTags = currentTags.Union(trimmedTags).ToArray(); var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); property.SetValue(updatedValue, culture); // json array break; } } - - // used by ContentRepositoryBase - public static IEnumerable GetTagsValue(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string? culture = null) + else { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); - if (configuration == null) - throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - - return property.GetTagsValue(configuration.StorageType, serializer, configuration.Delimiter, culture); - } - - private static IEnumerable GetTagsValue(this IProperty property, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture = null) - { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var value = property.GetValue(culture)?.ToString(); - if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty(); - switch (storageType) { case TagsStorageType.Csv: - return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); - - case TagsStorageType.Json: - try - { - return serializer.Deserialize(value)?.Select(x => x.Trim()) ?? Enumerable.Empty(); - } - catch (Exception) - { - //cannot parse, malformed - return Enumerable.Empty(); - } - - default: - throw new NotSupportedException($"Value \"{storageType}\" is not a valid TagsStorageType."); - } - } - - /// - /// Sets tags on a content property, based on the property editor tags configuration. - /// - /// The property. - /// The property value. - /// The datatype configuration. - /// A culture, for multi-lingual properties. - /// - /// The value is either a string (delimited string) or an enumeration of strings (tag list). - /// This is used both by the content repositories to initialize a property with some tag values, and by the - /// content controllers to update a property with values received from the property editor. - /// - public static void SetTagsValue(this IProperty property, IJsonSerializer serializer, object? value, TagConfiguration? tagConfiguration, string? culture) - { - if (property == null) throw new ArgumentNullException(nameof(property)); - if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration)); - - var storageType = tagConfiguration.StorageType; - var delimiter = tagConfiguration.Delimiter; - - SetTagsValue(property, value, storageType, serializer, delimiter, culture); - } - - // assumes that parameters are consistent with the datatype configuration - // value can be an enumeration of string, or a serialized value using storageType format - private static void SetTagsValue(IProperty property, object? value, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) - { - if (value == null) value = Enumerable.Empty(); - - // if value is already an enumeration of strings, just use it - if (value is IEnumerable tags1) - { - property.AssignTags(tags1, false, storageType, serializer, delimiter, culture); - return; - } - - // otherwise, deserialize value based upon storage type - switch (storageType) - { - case TagsStorageType.Csv: - var tags2 = value.ToString()!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - property.AssignTags(tags2, false, storageType, serializer, delimiter, culture); + property.SetValue( + string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), + culture); // csv string break; case TagsStorageType.Json: - try - { - var tags3 = serializer.Deserialize>(value.ToString()!); - property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, serializer, delimiter, culture); - } - catch (Exception ex) - { - StaticApplicationLogging.Logger.LogWarning(ex, "Could not automatically convert stored json value to an enumerable string '{Json}'", value.ToString()); - } + var updatedValue = trimmedTags.Length == 0 ? null : serializer.Serialize(trimmedTags); + property.SetValue(updatedValue, culture); // json array break; - - default: - throw new ArgumentOutOfRangeException(nameof(storageType)); } } } + + // assumes that parameters are consistent with the datatype configuration + private static void RemoveTags(this IProperty property, IEnumerable tags, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + { + // already empty = nothing to do + var value = property.GetValue(culture)?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + // set the property value + var trimmedTags = tags.Select(x => x.Trim()).ToArray(); + IEnumerable currentTags = property.GetTagsValue(storageType, serializer, delimiter, culture); + switch (storageType) + { + case TagsStorageType.Csv: + property.SetValue( + string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)).NullOrWhiteSpaceAsNull(), + culture); // csv string + break; + + case TagsStorageType.Json: + var updatedTags = currentTags.Except(trimmedTags).ToArray(); + var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); + property.SetValue(updatedValue, culture); // json array + break; + } + } + + private static IEnumerable GetTagsValue(this IProperty property, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture = null) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + var value = property.GetValue(culture)?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Enumerable.Empty(); + } + + switch (storageType) + { + case TagsStorageType.Csv: + return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); + + case TagsStorageType.Json: + try + { + return serializer.Deserialize(value)?.Select(x => x.Trim()) ?? Enumerable.Empty(); + } + catch (Exception) + { + // cannot parse, malformed + return Enumerable.Empty(); + } + + default: + throw new NotSupportedException($"Value \"{storageType}\" is not a valid TagsStorageType."); + } + } + + // assumes that parameters are consistent with the datatype configuration + // value can be an enumeration of string, or a serialized value using storageType format + private static void SetTagsValue(IProperty property, object? value, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + { + if (value == null) + { + value = Enumerable.Empty(); + } + + // if value is already an enumeration of strings, just use it + if (value is IEnumerable tags1) + { + property.AssignTags(tags1, false, storageType, serializer, delimiter, culture); + return; + } + + // otherwise, deserialize value based upon storage type + switch (storageType) + { + case TagsStorageType.Csv: + var tags2 = value.ToString()!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); + property.AssignTags(tags2, false, storageType, serializer, delimiter, culture); + break; + + case TagsStorageType.Json: + try + { + IEnumerable? tags3 = serializer.Deserialize>(value.ToString()!); + property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, serializer, delimiter, culture); + } + catch (Exception ex) + { + StaticApplicationLogging.Logger.LogWarning( + ex, + "Could not automatically convert stored json value to an enumerable string '{Json}'", + value.ToString()); + } + + break; + + default: + throw new ArgumentOutOfRangeException(nameof(storageType)); + } + } } diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 3acbad2720..0699ecbc0d 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -1,295 +1,305 @@ -using System; using System.Diagnostics; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a property type. +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] +public class PropertyType : EntityBase, IPropertyType, IEquatable { + private readonly bool _forceValueStorageType; + private readonly IShortStringHelper _shortStringHelper; + private string _alias; + private int _dataTypeId; + private Guid _dataTypeKey; + private string? _description; + private bool _labelOnTop; + private bool _mandatory; + private string? _mandatoryMessage; + private string _name; + private string _propertyEditorAlias; + private Lazy? _propertyGroupId; + private int _sortOrder; + private string? _validationRegExp; + private string? _validationRegExpMessage; + private ValueStorageType _valueStorageType; + private ContentVariation _variations; + /// - /// Represents a property type. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] - public class PropertyType : EntityBase, IPropertyType, IEquatable + public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType) { - private readonly IShortStringHelper _shortStringHelper; - private readonly bool _forceValueStorageType; - private string _name; - private string _alias; - private string? _description; - private int _dataTypeId; - private Guid _dataTypeKey; - private Lazy? _propertyGroupId; - private string _propertyEditorAlias; - private ValueStorageType _valueStorageType; - private bool _mandatory; - private string? _mandatoryMessage; - private int _sortOrder; - private string? _validationRegExp; - private string? _validationRegExpMessage; - private ContentVariation _variations; - private bool _labelOnTop; - - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType) + if (dataType == null) { - if (dataType == null) throw new ArgumentNullException(nameof(dataType)); - _shortStringHelper = shortStringHelper; - - if (dataType.HasIdentity) - _dataTypeId = dataType.Id; - - _propertyEditorAlias = dataType.EditorAlias; - _valueStorageType = dataType.DatabaseType; - _variations = ContentVariation.Nothing; - _alias = string.Empty; - _name = string.Empty; + throw new ArgumentNullException(nameof(dataType)); } - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType, string propertyTypeAlias) - : this(shortStringHelper, dataType) + _shortStringHelper = shortStringHelper; + + if (dataType.HasIdentity) { - _alias = SanitizeAlias(propertyTypeAlias); + _dataTypeId = dataType.Id; } - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper,string propertyEditorAlias, ValueStorageType valueStorageType) - : this(shortStringHelper, propertyEditorAlias, valueStorageType, false) - { - } + _propertyEditorAlias = dataType.EditorAlias; + _valueStorageType = dataType.DatabaseType; + _variations = ContentVariation.Nothing; + _alias = string.Empty; + _name = string.Empty; + } - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper,string propertyEditorAlias, ValueStorageType valueStorageType, string propertyTypeAlias) - : this(shortStringHelper, propertyEditorAlias, valueStorageType, false, propertyTypeAlias) - { - } + /// + /// Initializes a new instance of the class. + /// + public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType, string propertyTypeAlias) + : this(shortStringHelper, dataType) => + _alias = SanitizeAlias(propertyTypeAlias); - /// - /// Initializes a new instance of the class. - /// - /// Set to true to force the value storage type. Values assigned to - /// the property, eg from the underlying datatype, will be ignored. - public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType, bool forceValueStorageType, string? propertyTypeAlias = null) - { - _shortStringHelper = shortStringHelper; - _propertyEditorAlias = propertyEditorAlias; - _valueStorageType = valueStorageType; - _forceValueStorageType = forceValueStorageType; - _alias = propertyTypeAlias == null ? string.Empty : SanitizeAlias(propertyTypeAlias); - _variations = ContentVariation.Nothing; - _name = string.Empty; - } + /// + /// Initializes a new instance of the class. + /// + public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType) + : this(shortStringHelper, propertyEditorAlias, valueStorageType, false) + { + } - /// - /// Gets a value indicating whether the content type owning this property type is publishing. - /// - /// - /// A publishing content type supports draft and published values for properties. - /// It is possible to retrieve either the draft (default) or published value of a property. - /// Setting the value always sets the draft value, which then needs to be published. - /// A non-publishing content type only supports one value for properties. Getting - /// the draft or published value of a property returns the same thing, and publishing - /// a value property has no effect. - /// When true, getting the property value returns the edited value by default, but - /// it is possible to get the published value using the appropriate 'published' method - /// parameter. - /// When false, getting the property value always return the edited value, - /// regardless of the 'published' method parameter. - /// - public bool SupportsPublishing { get; set; } + /// + /// Initializes a new instance of the class. + /// + public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType, string propertyTypeAlias) + : this(shortStringHelper, propertyEditorAlias, valueStorageType, false, propertyTypeAlias) + { + } - /// - [DataMember] - public string Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } + /// + /// Initializes a new instance of the class. + /// + /// + /// Set to true to force the value storage type. Values assigned to + /// the property, eg from the underlying datatype, will be ignored. + /// + public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType, bool forceValueStorageType, string? propertyTypeAlias = null) + { + _shortStringHelper = shortStringHelper; + _propertyEditorAlias = propertyEditorAlias; + _valueStorageType = valueStorageType; + _forceValueStorageType = forceValueStorageType; + _alias = propertyTypeAlias == null ? string.Empty : SanitizeAlias(propertyTypeAlias); + _variations = ContentVariation.Nothing; + _name = string.Empty; + } - /// - [DataMember] - public virtual string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(SanitizeAlias(value), ref _alias!, nameof(Alias)); - } + /// + /// Gets a value indicating whether the content type owning this property type is publishing. + /// + /// + /// + /// A publishing content type supports draft and published values for properties. + /// It is possible to retrieve either the draft (default) or published value of a property. + /// Setting the value always sets the draft value, which then needs to be published. + /// + /// + /// A non-publishing content type only supports one value for properties. Getting + /// the draft or published value of a property returns the same thing, and publishing + /// a value property has no effect. + /// + /// + /// When true, getting the property value returns the edited value by default, but + /// it is possible to get the published value using the appropriate 'published' method + /// parameter. + /// + /// + /// When false, getting the property value always return the edited value, + /// regardless of the 'published' method parameter. + /// + /// + public bool SupportsPublishing { get; set; } - /// - [DataMember] - public string? Description - { - get => _description; - set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); - } + /// + public bool Equals(PropertyType? other) => + other != null && (base.Equals(other) || (Alias?.InvariantEquals(other.Alias) ?? false)); - /// - [DataMember] - public int DataTypeId - { - get => _dataTypeId; - set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId)); - } + /// + [DataMember] + public string Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - [DataMember] - public Guid DataTypeKey - { - get => _dataTypeKey; - set => SetPropertyValueAndDetectChanges(value, ref _dataTypeKey, nameof(DataTypeKey)); - } + /// + [DataMember] + public virtual string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(SanitizeAlias(value), ref _alias!, nameof(Alias)); + } - /// - [DataMember] - public string PropertyEditorAlias - { - get => _propertyEditorAlias; - set => SetPropertyValueAndDetectChanges(value, ref _propertyEditorAlias!, nameof(PropertyEditorAlias)); - } + /// + [DataMember] + public string? Description + { + get => _description; + set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); + } - /// - [DataMember] - public ValueStorageType ValueStorageType + /// + [DataMember] + public int DataTypeId + { + get => _dataTypeId; + set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId)); + } + + [DataMember] + public Guid DataTypeKey + { + get => _dataTypeKey; + set => SetPropertyValueAndDetectChanges(value, ref _dataTypeKey, nameof(DataTypeKey)); + } + + /// + [DataMember] + public string PropertyEditorAlias + { + get => _propertyEditorAlias; + set => SetPropertyValueAndDetectChanges(value, ref _propertyEditorAlias!, nameof(PropertyEditorAlias)); + } + + /// + [DataMember] + public ValueStorageType ValueStorageType + { + get => _valueStorageType; + set { - get => _valueStorageType; - set + if (_forceValueStorageType) { - if (_forceValueStorageType) return; // ignore changes - SetPropertyValueAndDetectChanges(value, ref _valueStorageType, nameof(ValueStorageType)); + return; // ignore changes } - } - /// - [DataMember] - [DoNotClone] - public Lazy? PropertyGroupId - { - get => _propertyGroupId; - set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); - } - - /// - [DataMember] - public bool Mandatory - { - get => _mandatory; - set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(Mandatory)); - } - - - /// - [DataMember] - public string? MandatoryMessage - { - get => _mandatoryMessage; - set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage)); - } - - /// - [DataMember] - public bool LabelOnTop - { - get => _labelOnTop; - set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop)); - } - - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } - - /// - [DataMember] - public string? ValidationRegExp - { - get => _validationRegExp; - set => SetPropertyValueAndDetectChanges(value, ref _validationRegExp, nameof(ValidationRegExp)); - } - - - /// - /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched - /// - [DataMember] - public string? ValidationRegExpMessage - { - get => _validationRegExpMessage; - set => SetPropertyValueAndDetectChanges(value, ref _validationRegExpMessage, nameof(ValidationRegExpMessage)); - } - - /// - public ContentVariation Variations - { - get => _variations; - set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); - } - - /// - public bool SupportsVariation(string? culture, string? segment, bool wildcards = false) - { - // exact validation: cannot accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, true, wildcards, false); - } - - /// - /// Sanitizes a property type alias. - /// - private string SanitizeAlias(string value) - { - //NOTE: WE are doing this because we don't want to do a ToSafeAlias when the alias is the special case of - // being prefixed with Constants.PropertyEditors.InternalGenericPropertiesPrefix - // which is used internally - - return value.StartsWith(Constants.PropertyEditors.InternalGenericPropertiesPrefix) - ? value - : value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase); - } - - /// - public bool Equals(PropertyType? other) - { - return other != null && (base.Equals(other) || (Alias?.InvariantEquals(other.Alias) ?? false)); - } - - /// - public override int GetHashCode() - { - //Get hash code for the Name field if it is not null. - int baseHash = base.GetHashCode(); - - //Get hash code for the Alias field. - int? hashAlias = Alias?.ToLowerInvariant().GetHashCode(); - - //Calculate the hash code for the product. - return baseHash ^ hashAlias ?? baseHash; - } - - /// - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (PropertyType) clone; - //need to manually assign the Lazy value as it will not be automatically mapped - if (PropertyGroupId != null) - { - clonedEntity._propertyGroupId = new Lazy(() => PropertyGroupId.Value); - } + SetPropertyValueAndDetectChanges(value, ref _valueStorageType, nameof(ValueStorageType)); } } + + /// + [DataMember] + [DoNotClone] + public Lazy? PropertyGroupId + { + get => _propertyGroupId; + set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); + } + + /// + [DataMember] + public bool Mandatory + { + get => _mandatory; + set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(Mandatory)); + } + + /// + [DataMember] + public string? MandatoryMessage + { + get => _mandatoryMessage; + set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage)); + } + + /// + [DataMember] + public bool LabelOnTop + { + get => _labelOnTop; + set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop)); + } + + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } + + /// + [DataMember] + public string? ValidationRegExp + { + get => _validationRegExp; + set => SetPropertyValueAndDetectChanges(value, ref _validationRegExp, nameof(ValidationRegExp)); + } + + /// + /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched + /// + [DataMember] + public string? ValidationRegExpMessage + { + get => _validationRegExpMessage; + set => SetPropertyValueAndDetectChanges(value, ref _validationRegExpMessage, nameof(ValidationRegExpMessage)); + } + + /// + public ContentVariation Variations + { + get => _variations; + set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); + } + + /// + public bool SupportsVariation(string? culture, string? segment, bool wildcards = false) => + + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, true, wildcards, false); + + /// + public override int GetHashCode() + { + // Get hash code for the Name field if it is not null. + var baseHash = base.GetHashCode(); + + // Get hash code for the Alias field. + var hashAlias = Alias?.ToLowerInvariant().GetHashCode(); + + // Calculate the hash code for the product. + return baseHash ^ hashAlias ?? baseHash; + } + + /// + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (PropertyType)clone; + + // need to manually assign the Lazy value as it will not be automatically mapped + if (PropertyGroupId != null) + { + clonedEntity._propertyGroupId = new Lazy(() => PropertyGroupId.Value); + } + } + + /// + /// Sanitizes a property type alias. + /// + private string SanitizeAlias(string value) => + + // NOTE: WE are doing this because we don't want to do a ToSafeAlias when the alias is the special case of + // being prefixed with Constants.PropertyEditors.InternalGenericPropertiesPrefix + // which is used internally + value.StartsWith(Constants.PropertyEditors.InternalGenericPropertiesPrefix) + ? value + : value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase); } diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 96133f6677..49c83b4c9d 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -1,179 +1,184 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Linq; +using System.ComponentModel; using System.Runtime.Serialization; -using System.Threading; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +// public interface IPropertyTypeCollection: IEnumerable + +/// +/// Represents a collection of objects. +/// +[Serializable] +[DataContract] + +// TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details +public class PropertyTypeCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable, + ICollection { + public PropertyTypeCollection(bool supportsPublishing) => SupportsPublishing = supportsPublishing; - //public interface IPropertyTypeCollection: IEnumerable - /// - /// Represents a collection of objects. - /// - [Serializable] - [DataContract] - // TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details - public class PropertyTypeCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable, ICollection + public PropertyTypeCollection(bool supportsPublishing, IEnumerable properties) + : this(supportsPublishing) => + Reset(properties); + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public bool SupportsPublishing { get; } + + // This baseclass calling is needed, else compiler will complain about nullability + + /// + public bool IsReadOnly => ((ICollection)this).IsReadOnly; + + // 'new' keyword is required! we can explicitly implement ICollection.Add BUT since normally a concrete PropertyType type + // is passed in, the explicit implementation doesn't get called, this ensures it does get called. + public new void Add(IPropertyType item) { - public PropertyTypeCollection(bool supportsPublishing) + item.SupportsPublishing = SupportsPublishing; + + // TODO: this is not pretty and should be refactored + var key = GetKeyForItem(item); + if (key != null) { - SupportsPublishing = supportsPublishing; - } - - public PropertyTypeCollection(bool supportsPublishing, IEnumerable properties) - : this(supportsPublishing) - { - Reset(properties); - } - - public bool SupportsPublishing { get; } - - // This baseclass calling is needed, else compiler will complain about nullability - - /// - public bool IsReadOnly => ((ICollection)this).IsReadOnly; - - /// - /// Resets the collection to only contain the instances referenced in the parameter. - /// - /// The properties. - /// - internal void Reset(IEnumerable properties) - { - //collection events will be raised in each of these calls - Clear(); - - //collection events will be raised in each of these calls - foreach (var property in properties) - Add(property); - } - - protected override void SetItem(int index, IPropertyType item) - { - item.SupportsPublishing = SupportsPublishing; - var oldItem = index >= 0 ? this[index] : item; - base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); - item.PropertyChanged += Item_PropertyChanged; - } - - protected override void RemoveItem(int index) - { - var removed = this[index]; - base.RemoveItem(index); - removed.PropertyChanged -= Item_PropertyChanged; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } - - protected override void InsertItem(int index, IPropertyType item) - { - item.SupportsPublishing = SupportsPublishing; - base.InsertItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); - item.PropertyChanged += Item_PropertyChanged; - } - - protected override void ClearItems() - { - base.ClearItems(); - foreach (var item in this) - item.PropertyChanged -= Item_PropertyChanged; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - // 'new' keyword is required! we can explicitly implement ICollection.Add BUT since normally a concrete PropertyType type - // is passed in, the explicit implementation doesn't get called, this ensures it does get called. - public new void Add(IPropertyType item) - { - item.SupportsPublishing = SupportsPublishing; - - // TODO: this is not pretty and should be refactored - - var key = GetKeyForItem(item); - if (key != null) + var exists = Contains(key); + if (exists) { - var exists = Contains(key); - if (exists) - { - //collection events will be raised in SetItem - SetItem(IndexOfKey(key), item); - return; - } + // collection events will be raised in SetItem + SetItem(IndexOfKey(key), item); + return; } - - //check if the item's sort order is already in use - if (this.Any(x => x.SortOrder == item.SortOrder)) - { - //make it the next iteration - item.SortOrder = this.Max(x => x.SortOrder) + 1; - } - - //collection events will be raised in InsertItem - base.Add(item); } - /// - /// Occurs when a property changes on a IPropertyType that exists in this collection - /// - /// - /// - private void Item_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + // check if the item's sort order is already in use + if (this.Any(x => x.SortOrder == item.SortOrder)) { - var propType = (IPropertyType?)sender; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + // make it the next iteration + item.SortOrder = this.Max(x => x.SortOrder) + 1; } - /// - /// Determines whether this collection contains a whose alias matches the specified PropertyType. - /// - /// Alias of the PropertyType. - /// true if the collection contains the specified alias; otherwise, false. - /// - public new bool Contains(string propertyAlias) + // collection events will be raised in InsertItem + base.Add(item); + } + + public object DeepClone() + { + var clone = new PropertyTypeCollection(SupportsPublishing); + foreach (IPropertyType propertyType in this) { - return this.Any(x => x.Alias == propertyAlias); + clone.Add((IPropertyType)propertyType.DeepClone()); } - public bool RemoveItem(string propertyTypeAlias) - { - var key = IndexOfKey(propertyTypeAlias); - if (key != -1) RemoveItem(key); - return key != -1; - } + return clone; + } - public int IndexOfKey(string key) - { - for (var i = 0; i < Count; i++) - if (this[i].Alias == key) - return i; - return -1; - } + /// + /// Determines whether this collection contains a whose alias matches the specified + /// PropertyType. + /// + /// Alias of the PropertyType. + /// true if the collection contains the specified alias; otherwise, false. + /// + public new bool Contains(string propertyAlias) => this.Any(x => x.Alias == propertyAlias); - protected override string GetKeyForItem(IPropertyType item) - { - return item.Alias!; - } + /// + /// Resets the collection to only contain the instances referenced in the + /// parameter. + /// + /// The properties. + /// + internal void Reset(IEnumerable properties) + { + // collection events will be raised in each of these calls + Clear(); - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + // collection events will be raised in each of these calls + foreach (IPropertyType property in properties) { - CollectionChanged?.Invoke(this, args); - } - - public object DeepClone() - { - var clone = new PropertyTypeCollection(SupportsPublishing); - foreach (var propertyType in this) - clone.Add((IPropertyType) propertyType.DeepClone()); - return clone; + Add(property); } } + + protected override void SetItem(int index, IPropertyType item) + { + item.SupportsPublishing = SupportsPublishing; + IPropertyType oldItem = index >= 0 ? this[index] : item; + base.SetItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + item.PropertyChanged += Item_PropertyChanged; + } + + protected override void RemoveItem(int index) + { + IPropertyType removed = this[index]; + base.RemoveItem(index); + removed.PropertyChanged -= Item_PropertyChanged; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } + + protected override void InsertItem(int index, IPropertyType item) + { + item.SupportsPublishing = SupportsPublishing; + base.InsertItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + item.PropertyChanged += Item_PropertyChanged; + } + + protected override void ClearItems() + { + base.ClearItems(); + foreach (IPropertyType item in this) + { + item.PropertyChanged -= Item_PropertyChanged; + } + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + /// Occurs when a property changes on a IPropertyType that exists in this collection + /// + /// + /// + private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + var propType = (IPropertyType?)sender; + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + } + + public bool RemoveItem(string propertyTypeAlias) + { + var key = IndexOfKey(propertyTypeAlias); + if (key != -1) + { + RemoveItem(key); + } + + return key != -1; + } + + public int IndexOfKey(string key) + { + for (var i = 0; i < Count; i++) + { + if (this[i].Alias == key) + { + return i; + } + } + + return -1; + } + + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + protected override string GetKeyForItem(IPropertyType item) => item.Alias; + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => + CollectionChanged?.Invoke(this, args); } diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 00e05442d8..8789ef5052 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -1,158 +1,154 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class PublicAccessEntry : EntityBase { - [Serializable] - [DataContract(IsReference = true)] - public class PublicAccessEntry : EntityBase + private readonly List _removedRules = new(); + private readonly EventClearingObservableCollection _ruleCollection; + private int _loginNodeId; + private int _noAccessNodeId; + private int _protectedNodeId; + + public PublicAccessEntry(IContent protectedNode, IContent loginNode, IContent noAccessNode, IEnumerable ruleCollection) { - private readonly EventClearingObservableCollection _ruleCollection; - private int _protectedNodeId; - private int _noAccessNodeId; - private int _loginNodeId; - private readonly List _removedRules = new List(); - - public PublicAccessEntry(IContent protectedNode, IContent loginNode, IContent noAccessNode, IEnumerable ruleCollection) + if (protectedNode == null) { - if (protectedNode == null) throw new ArgumentNullException(nameof(protectedNode)); - if (loginNode == null) throw new ArgumentNullException(nameof(loginNode)); - if (noAccessNode == null) throw new ArgumentNullException(nameof(noAccessNode)); - - LoginNodeId = loginNode.Id; - NoAccessNodeId = noAccessNode.Id; - _protectedNodeId = protectedNode.Id; - - _ruleCollection = new EventClearingObservableCollection(ruleCollection); - _ruleCollection.CollectionChanged += _ruleCollection_CollectionChanged; - - foreach (var rule in _ruleCollection) - rule.AccessEntryId = Key; + throw new ArgumentNullException(nameof(protectedNode)); } - public PublicAccessEntry(Guid id, int protectedNodeId, int loginNodeId, int noAccessNodeId, IEnumerable ruleCollection) + if (loginNode == null) { - Key = id; - Id = Key.GetHashCode(); - - LoginNodeId = loginNodeId; - NoAccessNodeId = noAccessNodeId; - _protectedNodeId = protectedNodeId; - - _ruleCollection = new EventClearingObservableCollection(ruleCollection); - _ruleCollection.CollectionChanged += _ruleCollection_CollectionChanged; - - foreach (var rule in _ruleCollection) - rule.AccessEntryId = Key; + throw new ArgumentNullException(nameof(loginNode)); } - void _ruleCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + if (noAccessNode == null) { - OnPropertyChanged(nameof(Rules)); + throw new ArgumentNullException(nameof(noAccessNode)); + } - //if (e.Action == NotifyCollectionChangedAction.Add) - //{ - // var item = e.NewItems.Cast().First(); + LoginNodeId = loginNode.Id; + NoAccessNodeId = noAccessNode.Id; + _protectedNodeId = protectedNode.Id; - // if (_addedSections.Contains(item) == false) - // { - // _addedSections.Add(item); - // } - //} + _ruleCollection = new EventClearingObservableCollection(ruleCollection); + _ruleCollection.CollectionChanged += RuleCollection_CollectionChanged; - if (e.Action == NotifyCollectionChangedAction.Remove) + foreach (PublicAccessRule rule in _ruleCollection) + { + rule.AccessEntryId = Key; + } + } + + public PublicAccessEntry(Guid id, int protectedNodeId, int loginNodeId, int noAccessNodeId, IEnumerable ruleCollection) + { + Key = id; + Id = Key.GetHashCode(); + + LoginNodeId = loginNodeId; + NoAccessNodeId = noAccessNodeId; + _protectedNodeId = protectedNodeId; + + _ruleCollection = new EventClearingObservableCollection(ruleCollection); + _ruleCollection.CollectionChanged += RuleCollection_CollectionChanged; + + foreach (PublicAccessRule rule in _ruleCollection) + { + rule.AccessEntryId = Key; + } + } + + public IEnumerable RemovedRules => _removedRules; + + public IEnumerable Rules => _ruleCollection; + + [DataMember] + public int LoginNodeId + { + get => _loginNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _loginNodeId, nameof(LoginNodeId)); + } + + [DataMember] + public int NoAccessNodeId + { + get => _noAccessNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _noAccessNodeId, nameof(NoAccessNodeId)); + } + + [DataMember] + public int ProtectedNodeId + { + get => _protectedNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _protectedNodeId, nameof(ProtectedNodeId)); + } + + public PublicAccessRule AddRule(string ruleValue, string ruleType) + { + var rule = new PublicAccessRule { AccessEntryId = Key, RuleValue = ruleValue, RuleType = ruleType }; + _ruleCollection.Add(rule); + return rule; + } + + private void RuleCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(Rules)); + + // if (e.Action == NotifyCollectionChangedAction.Add) + // { + // var item = e.NewItems.Cast().First(); + + // if (_addedSections.Contains(item) == false) + // { + // _addedSections.Add(item); + // } + // } + if (e.Action == NotifyCollectionChangedAction.Remove) + { + PublicAccessRule? item = e.OldItems?.Cast().First(); + + if (item is not null) { - var item = e.OldItems?.Cast().First(); - - if (item is not null) + if (_removedRules.Contains(item.Key) == false) { - if (_removedRules.Contains(item.Key) == false) - { - _removedRules.Add(item.Key); - } + _removedRules.Add(item.Key); } } } + } - public IEnumerable RemovedRules => _removedRules; + public void RemoveRule(PublicAccessRule rule) => _ruleCollection.Remove(rule); - public IEnumerable Rules => _ruleCollection; + public void ClearRules() => _ruleCollection.Clear(); - public PublicAccessRule AddRule(string ruleValue, string ruleType) + public override void ResetDirtyProperties(bool rememberDirty) + { + _removedRules.Clear(); + base.ResetDirtyProperties(rememberDirty); + foreach (PublicAccessRule publicAccessRule in _ruleCollection) { - var rule = new PublicAccessRule - { - AccessEntryId = Key, - RuleValue = ruleValue, - RuleType = ruleType - }; - _ruleCollection.Add(rule); - return rule; + publicAccessRule.ResetDirtyProperties(rememberDirty); } + } - public void RemoveRule(PublicAccessRule rule) + internal void ClearRemovedRules() => _removedRules.Clear(); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var cloneEntity = (PublicAccessEntry)clone; + + if (cloneEntity._ruleCollection != null) { - _ruleCollection.Remove(rule); - } - - public void ClearRules() - { - _ruleCollection.Clear(); - } - - - internal void ClearRemovedRules() - { - _removedRules.Clear(); - } - - [DataMember] - public int LoginNodeId - { - get => _loginNodeId; - set => SetPropertyValueAndDetectChanges(value, ref _loginNodeId, nameof(LoginNodeId)); - } - - [DataMember] - public int NoAccessNodeId - { - get => _noAccessNodeId; - set => SetPropertyValueAndDetectChanges(value, ref _noAccessNodeId, nameof(NoAccessNodeId)); - } - - [DataMember] - public int ProtectedNodeId - { - get => _protectedNodeId; - set => SetPropertyValueAndDetectChanges(value, ref _protectedNodeId, nameof(ProtectedNodeId)); - } - - public override void ResetDirtyProperties(bool rememberDirty) - { - _removedRules.Clear(); - base.ResetDirtyProperties(rememberDirty); - foreach (var publicAccessRule in _ruleCollection) - { - publicAccessRule.ResetDirtyProperties(rememberDirty); - } - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var cloneEntity = (PublicAccessEntry)clone; - - if (cloneEntity._ruleCollection != null) - { - cloneEntity._ruleCollection.ClearCollectionChangedEvents(); //clear this event handler if any - cloneEntity._ruleCollection.CollectionChanged += cloneEntity._ruleCollection_CollectionChanged; //re-assign correct event handler - } + cloneEntity._ruleCollection.ClearCollectionChangedEvents(); // clear this event handler if any + cloneEntity._ruleCollection.CollectionChanged += + cloneEntity.RuleCollection_CollectionChanged; // re-assign correct event handler } } } diff --git a/src/Umbraco.Core/Models/PublicAccessRule.cs b/src/Umbraco.Core/Models/PublicAccessRule.cs index 790d8b6a1b..f8af1a6d98 100644 --- a/src/Umbraco.Core/Models/PublicAccessRule.cs +++ b/src/Umbraco.Core/Models/PublicAccessRule.cs @@ -1,41 +1,37 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class PublicAccessRule : EntityBase { - [Serializable] - [DataContract(IsReference = true)] - public class PublicAccessRule : EntityBase + private string? _ruleType; + private string? _ruleValue; + + public PublicAccessRule(Guid id, Guid accessEntryId) { - private string? _ruleValue; - private string? _ruleType; + AccessEntryId = accessEntryId; + Key = id; + Id = Key.GetHashCode(); + } - public PublicAccessRule(Guid id, Guid accessEntryId) - { - AccessEntryId = accessEntryId; - Key = id; - Id = Key.GetHashCode(); - } + public PublicAccessRule() + { + } - public PublicAccessRule() - { - } - - public Guid AccessEntryId { get; set; } - - public string? RuleValue - { - get => _ruleValue; - set => SetPropertyValueAndDetectChanges(value, ref _ruleValue, nameof(RuleValue)); - } - - public string? RuleType - { - get => _ruleType; - set => SetPropertyValueAndDetectChanges(value, ref _ruleType, nameof(RuleType)); - } + public Guid AccessEntryId { get; set; } + public string? RuleValue + { + get => _ruleValue; + set => SetPropertyValueAndDetectChanges(value, ref _ruleValue, nameof(RuleValue)); + } + public string? RuleType + { + get => _ruleType; + set => SetPropertyValueAndDetectChanges(value, ref _ruleType, nameof(RuleType)); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs index 1aaa0d9814..2c665f1710 100644 --- a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs @@ -1,75 +1,63 @@ -using System; using System.Collections; -using System.Collections.Generic; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Manages the built-in fallback policies. +/// +public struct Fallback : IEnumerable { /// - /// Manages the built-in fallback policies. + /// Do not fallback. /// - public struct Fallback : IEnumerable - { - private readonly int[] _values; + public const int None = 0; - /// - /// Initializes a new instance of the struct with values. - /// - private Fallback(int[] values) - { - _values = values; - } + private readonly int[] _values; - /// - /// Gets an ordered set of fallback policies. - /// - /// - public static Fallback To(params int[] values) => new Fallback(values); + /// + /// Initializes a new instance of the struct with values. + /// + private Fallback(int[] values) => _values = values; - /// - /// Do not fallback. - /// - public const int None = 0; + /// + /// Gets an ordered set of fallback policies. + /// + /// + public static Fallback To(params int[] values) => new(values); - /// - /// Fallback to default value. - /// - public const int DefaultValue = 1; + /// + /// Fallback to default value. + /// + public const int DefaultValue = 1; - /// - /// Gets the fallback to default value policy. - /// - public static Fallback ToDefaultValue => new Fallback(new[] { DefaultValue }); + /// + /// Fallback to other languages. + /// + public const int Language = 2; - /// - /// Fallback to other languages. - /// - public const int Language = 2; + /// + /// Fallback to tree ancestors. + /// + public const int Ancestors = 3; - /// - /// Gets the fallback to language policy. - /// - public static Fallback ToLanguage => new Fallback(new[] { Language }); + /// + /// Gets the fallback to default value policy. + /// + public static Fallback ToDefaultValue => new(new[] { DefaultValue }); - /// - /// Fallback to tree ancestors. - /// - public const int Ancestors = 3; + /// + /// Gets the fallback to language policy. + /// + public static Fallback ToLanguage => new(new[] { Language }); - /// - /// Gets the fallback to tree ancestors policy. - /// - public static Fallback ToAncestors => new Fallback(new[] { Ancestors }); + /// + /// Gets the fallback to tree ancestors policy. + /// + public static Fallback ToAncestors => new(new[] { Ancestors }); - /// - public IEnumerator GetEnumerator() - { - return ((IEnumerable)_values ?? Array.Empty()).GetEnumerator(); - } + /// + public IEnumerator GetEnumerator() => ((IEnumerable)_values ?? Array.Empty()).GetEnumerator(); - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs index 3fb18fad2d..6d8fe9e547 100644 --- a/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs @@ -1,25 +1,24 @@ using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Implements on top of . +/// +public class HttpContextVariationContextAccessor : IVariationContextAccessor { + private const string ContextKey = "Umbraco.Web.Models.PublishedContent.DefaultVariationContextAccessor"; + private readonly IRequestCache _requestCache; + /// - /// Implements on top of . + /// Initializes a new instance of the class. /// - public class HttpContextVariationContextAccessor : IVariationContextAccessor + public HttpContextVariationContextAccessor(IRequestCache requestCache) => _requestCache = requestCache; + + /// + public VariationContext? VariationContext { - private readonly IRequestCache _requestCache; - private const string ContextKey = "Umbraco.Web.Models.PublishedContent.DefaultVariationContextAccessor"; - - /// - /// Initializes a new instance of the class. - /// - public HttpContextVariationContextAccessor(IRequestCache requestCache) => _requestCache = requestCache; - - /// - public VariationContext? VariationContext - { - get => (VariationContext?) _requestCache.Get(ContextKey); - set => _requestCache.Set(ContextKey, value); - } + get => (VariationContext?)_requestCache.Get(ContextKey); + set => _requestCache.Set(ContextKey, value); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs index d974041d3b..2be9638438 100644 --- a/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs @@ -1,23 +1,23 @@ using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Models.PublishedContent -{ - /// - /// Implements a hybrid . - /// - public class HybridVariationContextAccessor : HybridAccessorBase, IVariationContextAccessor - { - public HybridVariationContextAccessor(IRequestCache requestCache) - : base(requestCache) - { } +namespace Umbraco.Cms.Core.Models.PublishedContent; - /// - /// Gets or sets the object. - /// - public VariationContext? VariationContext - { - get => Value; - set => Value = value; - } +/// +/// Implements a hybrid . +/// +public class HybridVariationContextAccessor : HybridAccessorBase, IVariationContextAccessor +{ + public HybridVariationContextAccessor(IRequestCache requestCache) + : base(requestCache) + { + } + + /// + /// Gets or sets the object. + /// + public VariationContext? VariationContext + { + get => Value; + set => Value = value; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs index 2838297a8e..37ca5b3733 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs @@ -1,24 +1,22 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a live published model creation service. +/// +public interface IAutoPublishedModelFactory : IPublishedModelFactory { + /// + /// Gets an object that can be used to synchronize access to the factory. + /// + object SyncRoot { get; } /// - /// Provides a live published model creation service. + /// If the live model factory /// - public interface IAutoPublishedModelFactory : IPublishedModelFactory - { - /// - /// Gets an object that can be used to synchronize access to the factory. - /// - object SyncRoot { get; } + bool Enabled { get; } - /// - /// Tells the factory that it should build a new generation of models - /// - void Reset(); - - /// - /// If the live model factory - /// - bool Enabled { get; } - } + /// + /// Tells the factory that it should build a new generation of models + /// + void Reset(); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index eb52339936..01b57f38f8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -1,150 +1,150 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// +/// Represents a published content item. +/// +/// +/// Can be a published document, media or member. +/// +public interface IPublishedContent : IPublishedElement { + // TODO: IPublishedContent properties colliding with models + // we need to find a way to remove as much clutter as possible from IPublishedContent, + // since this is preventing someone from creating a property named 'Path' and have it + // in a model, for instance. we could move them all under one unique property eg + // Infos, so we would do .Infos.SortOrder - just an idea - not going to do it in v8 - /// /// - /// Represents a published content item. + /// Gets the unique identifier of the content item. + /// + int Id { get; } + + /// + /// Gets the name of the content item for the current culture. + /// + string? Name { get; } + + /// + /// Gets the URL segment of the content item for the current culture. + /// + string? UrlSegment { get; } + + /// + /// Gets the sort order of the content item. + /// + int SortOrder { get; } + + /// + /// Gets the tree level of the content item. + /// + int Level { get; } + + /// + /// Gets the tree path of the content item. + /// + string Path { get; } + + /// + /// Gets the identifier of the template to use to render the content item. + /// + int? TemplateId { get; } + + /// + /// Gets the identifier of the user who created the content item. + /// + int CreatorId { get; } + + /// + /// Gets the date the content item was created. + /// + DateTime CreateDate { get; } + + /// + /// Gets the identifier of the user who last updated the content item. + /// + int WriterId { get; } + + /// + /// Gets the date the content item was last updated. /// /// - /// Can be a published document, media or member. + /// For published content items, this is also the date the item was published. + /// + /// This date is always global to the content item, see CultureDate() for the + /// date each culture was published. + /// /// - public interface IPublishedContent : IPublishedElement - { - #region Content + DateTime UpdateDate { get; } - // TODO: IPublishedContent properties colliding with models - // we need to find a way to remove as much clutter as possible from IPublishedContent, - // since this is preventing someone from creating a property named 'Path' and have it - // in a model, for instance. we could move them all under one unique property eg - // Infos, so we would do .Infos.SortOrder - just an idea - not going to do it in v8 + /// + /// Gets available culture infos. + /// + /// + /// + /// Contains only those culture that are available. For a published content, these are + /// the cultures that are published. For a draft content, those that are 'available' ie + /// have a non-empty content name. + /// + /// Does not contain the invariant culture. + /// // fixme? + /// + IReadOnlyDictionary Cultures { get; } - /// - /// Gets the unique identifier of the content item. - /// - int Id { get; } + /// + /// Gets the type of the content item (document, media...). + /// + PublishedItemType ItemType { get; } - /// - /// Gets the name of the content item for the current culture. - /// - string? Name { get; } + /// + /// Gets the parent of the content item. + /// + /// The parent of root content is null. + IPublishedContent? Parent { get; } - /// - /// Gets the URL segment of the content item for the current culture. - /// - string? UrlSegment { get; } + /// + /// Gets a value indicating whether the content is draft. + /// + /// + /// + /// A content is draft when it is the unpublished version of a content, which may + /// have a published version, or not. + /// + /// + /// When retrieving documents from cache in non-preview mode, IsDraft is always false, + /// as only published documents are returned. When retrieving in preview mode, IsDraft can + /// either be true (document is not published, or has been edited, and what is returned + /// is the edited version) or false (document is published, and has not been edited, and + /// what is returned is the published version). + /// + /// + bool IsDraft(string? culture = null); - /// - /// Gets the sort order of the content item. - /// - int SortOrder { get; } + /// + /// Gets a value indicating whether the content is published. + /// + /// + /// A content is published when it has a published version. + /// + /// When retrieving documents from cache in non-preview mode, IsPublished is always + /// true, as only published documents are returned. When retrieving in draft mode, IsPublished + /// can either be true (document has a published version) or false (document has no + /// published version). + /// + /// + /// It is therefore possible for both IsDraft and IsPublished to be true at the same + /// time, meaning that the content is the draft version, and a published version exists. + /// + /// + bool IsPublished(string? culture = null); - /// - /// Gets the tree level of the content item. - /// - int Level { get; } + /// + /// Gets the children of the content item that are available for the current culture. + /// + IEnumerable? Children { get; } - /// - /// Gets the tree path of the content item. - /// - string Path { get; } - - /// - /// Gets the identifier of the template to use to render the content item. - /// - int? TemplateId { get; } - - /// - /// Gets the identifier of the user who created the content item. - /// - int CreatorId { get; } - - /// - /// Gets the date the content item was created. - /// - DateTime CreateDate { get; } - - /// - /// Gets the identifier of the user who last updated the content item. - /// - int WriterId { get; } - - /// - /// Gets the date the content item was last updated. - /// - /// - /// For published content items, this is also the date the item was published. - /// This date is always global to the content item, see CultureDate() for the - /// date each culture was published. - /// - DateTime UpdateDate { get; } - - /// - /// Gets available culture infos. - /// - /// - /// Contains only those culture that are available. For a published content, these are - /// the cultures that are published. For a draft content, those that are 'available' ie - /// have a non-empty content name. - /// Does not contain the invariant culture. // fixme? - /// - IReadOnlyDictionary Cultures { get; } - - /// - /// Gets the type of the content item (document, media...). - /// - PublishedItemType ItemType { get; } - - /// - /// Gets a value indicating whether the content is draft. - /// - /// - /// A content is draft when it is the unpublished version of a content, which may - /// have a published version, or not. - /// When retrieving documents from cache in non-preview mode, IsDraft is always false, - /// as only published documents are returned. When retrieving in preview mode, IsDraft can - /// either be true (document is not published, or has been edited, and what is returned - /// is the edited version) or false (document is published, and has not been edited, and - /// what is returned is the published version). - /// - bool IsDraft(string? culture = null); - - /// - /// Gets a value indicating whether the content is published. - /// - /// - /// A content is published when it has a published version. - /// When retrieving documents from cache in non-preview mode, IsPublished is always - /// true, as only published documents are returned. When retrieving in draft mode, IsPublished - /// can either be true (document has a published version) or false (document has no - /// published version). - /// It is therefore possible for both IsDraft and IsPublished to be true at the same - /// time, meaning that the content is the draft version, and a published version exists. - /// - bool IsPublished(string? culture = null); - - #endregion - - #region Tree - - /// - /// Gets the parent of the content item. - /// - /// The parent of root content is null. - IPublishedContent? Parent { get; } - - /// - /// Gets the children of the content item that are available for the current culture. - /// - IEnumerable? Children { get; } - - /// - /// Gets all the children of the content item, regardless of whether they are available for the current culture. - /// - IEnumerable? ChildrenForAllCultures { get; } - - #endregion - } + /// + /// Gets all the children of the content item, regardless of whether they are available for the current culture. + /// + IEnumerable? ChildrenForAllCultures { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs index bd3f77152d..5ce8bef875 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs @@ -1,69 +1,67 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// Represents an type. +/// +/// +/// Instances implementing the interface should be +/// immutable, ie if the content type changes, then a new instance needs to be created. +/// +public interface IPublishedContentType { /// - /// Represents an type. + /// Gets the unique key for the content type. /// - /// Instances implementing the interface should be - /// immutable, ie if the content type changes, then a new instance needs to be created. - public interface IPublishedContentType - { - /// - /// Gets the unique key for the content type. - /// - Guid Key { get; } + Guid Key { get; } - /// - /// Gets the content type identifier. - /// - int Id { get; } + /// + /// Gets the content type identifier. + /// + int Id { get; } - /// - /// Gets the content type alias. - /// - string Alias { get; } + /// + /// Gets the content type alias. + /// + string Alias { get; } - /// - /// Gets the content item type. - /// - PublishedItemType ItemType { get; } + /// + /// Gets the content item type. + /// + PublishedItemType ItemType { get; } - /// - /// Gets the aliases of the content types participating in the composition. - /// - HashSet CompositionAliases { get; } + /// + /// Gets the aliases of the content types participating in the composition. + /// + HashSet CompositionAliases { get; } - /// - /// Gets the content variations of the content type. - /// - ContentVariation Variations { get; } + /// + /// Gets the content variations of the content type. + /// + ContentVariation Variations { get; } - /// - /// Gets a value indicating whether this content type is for an element. - /// - bool IsElement { get; } + /// + /// Gets a value indicating whether this content type is for an element. + /// + bool IsElement { get; } - /// - /// Gets the content type properties. - /// - IEnumerable PropertyTypes { get; } + /// + /// Gets the content type properties. + /// + IEnumerable PropertyTypes { get; } - /// - /// Gets a property type index. - /// - /// The alias is case-insensitive. This is the only place where alias strings are compared. - int GetPropertyIndex(string alias); + /// + /// Gets a property type index. + /// + /// The alias is case-insensitive. This is the only place where alias strings are compared. + int GetPropertyIndex(string alias); - /// - /// Gets a property type. - /// - IPublishedPropertyType? GetPropertyType(string alias); + /// + /// Gets a property type. + /// + IPublishedPropertyType? GetPropertyType(string alias); - /// - /// Gets a property type. - /// - IPublishedPropertyType? GetPropertyType(int index); - } + /// + /// Gets a property type. + /// + IPublishedPropertyType? GetPropertyType(int index); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index b1a1740b31..09e9a00389 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -1,58 +1,64 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Creates published content types. +/// +public interface IPublishedContentTypeFactory { + /// + /// Creates a published content type. + /// + /// An content type. + /// A published content type corresponding to the item type and content type. + IPublishedContentType CreateContentType(IContentTypeComposition contentType); /// - /// Creates published content types. + /// Creates a published property type. /// - public interface IPublishedContentTypeFactory - { - /// - /// Creates a published content type. - /// - /// An content type. - /// A published content type corresponding to the item type and content type. - IPublishedContentType CreateContentType(IContentTypeComposition contentType); + /// The published content type owning the property. + /// A property type. + /// Is used by constructor to create property types. + IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType); - /// - /// Creates a published property type. - /// - /// The published content type owning the property. - /// A property type. - /// Is used by constructor to create property types. - IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType); + /// + /// Creates a published property type. + /// + /// The published content type owning the property. + /// The property type alias. + /// The datatype identifier. + /// The variations. + /// Is used by constructor to create special property types. + IPublishedPropertyType CreatePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations); - /// - /// Creates a published property type. - /// - /// The published content type owning the property. - /// The property type alias. - /// The datatype identifier. - /// The variations. - /// Is used by constructor to create special property types. - IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// + /// Creates a core (non-user) published property type. + /// + /// The published content type owning the property. + /// The property type alias. + /// The datatype identifier. + /// The variations. + /// Is used by constructor to create special property types. + IPublishedPropertyType CreateCorePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations); - /// - /// Creates a core (non-user) published property type. - /// - /// The published content type owning the property. - /// The property type alias. - /// The datatype identifier. - /// The variations. - /// Is used by constructor to create special property types. - IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// + /// Gets a published datatype. + /// + PublishedDataType GetDataType(int id); - /// - /// Gets a published datatype. - /// - PublishedDataType GetDataType(int id); - - /// - /// Notifies the factory of datatype changes. - /// - /// - /// This is so the factory can flush its caches. - /// Invoked by the IPublishedSnapshotService. - /// - void NotifyDataTypeChanges(int[] ids); - } + /// + /// Notifies the factory of datatype changes. + /// + /// + /// This is so the factory can flush its caches. + /// Invoked by the IPublishedSnapshotService. + /// + void NotifyDataTypeChanges(int[] ids); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs index 767d3eadc0..a198064137 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs @@ -1,52 +1,50 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// Represents a published element. +/// +public interface IPublishedElement { + #region ContentType + /// - /// Represents a published element. + /// Gets the content type. /// - public interface IPublishedElement - { - #region ContentType + IPublishedContentType ContentType { get; } - /// - /// Gets the content type. - /// - IPublishedContentType ContentType { get; } + #endregion - #endregion + #region PublishedElement - #region PublishedElement + /// + /// Gets the unique key of the published element. + /// + Guid Key { get; } - /// - /// Gets the unique key of the published element. - /// - Guid Key { get; } + #endregion - #endregion + #region Properties - #region Properties + /// + /// Gets the properties of the element. + /// + /// + /// Contains one IPublishedProperty for each property defined for the content type, including + /// inherited properties. Some properties may have no value. + /// + IEnumerable Properties { get; } - /// - /// Gets the properties of the element. - /// - /// Contains one IPublishedProperty for each property defined for the content type, including - /// inherited properties. Some properties may have no value. - IEnumerable Properties { get; } + /// + /// Gets a property identified by its alias. + /// + /// The property alias. + /// The property identified by the alias. + /// + /// If the content type has no property with that alias, including inherited properties, returns null, + /// otherwise return a property -- that may have no value (ie HasValue is false). + /// The alias is case-insensitive. + /// + IPublishedProperty? GetProperty(string alias); - /// - /// Gets a property identified by its alias. - /// - /// The property alias. - /// The property identified by the alias. - /// - /// If the content type has no property with that alias, including inherited properties, returns null, - /// otherwise return a property -- that may have no value (ie HasValue is false). - /// The alias is case-insensitive. - /// - IPublishedProperty? GetProperty(string alias); - - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs index ba8bdc43d4..cefb51241e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs @@ -1,31 +1,29 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IPublishedMemberCache { - public interface IPublishedMemberCache - { - /// - /// Get an from an - /// - /// - /// - IPublishedContent? Get(IMember member); + /// + /// Get an from an + /// + /// + /// + IPublishedContent? Get(IMember member); - /// - /// Gets a content type identified by its unique identifier. - /// - /// The content type unique identifier. - /// The content type, or null. - IPublishedContentType GetContentType(int id); + /// + /// Gets a content type identified by its unique identifier. + /// + /// The content type unique identifier. + /// The content type, or null. + IPublishedContentType GetContentType(int id); - /// - /// Gets a content type identified by its alias. - /// - /// The content type alias. - /// The content type, or null. - /// The alias is case-insensitive. - IPublishedContentType GetContentType(string alias); - } + /// + /// Gets a content type identified by its alias. + /// + /// The content type alias. + /// The content type, or null. + /// The alias is case-insensitive. + IPublishedContentType GetContentType(string alias); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs index c34a4a6ba4..03485f0b6c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs @@ -1,50 +1,49 @@ using System.Collections; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides the published model creation service. +/// +public interface IPublishedModelFactory { /// - /// Provides the published model creation service. + /// Creates a strongly-typed model representing a published element. /// - public interface IPublishedModelFactory - { - /// - /// Creates a strongly-typed model representing a published element. - /// - /// The original published element. - /// - /// The strongly-typed model representing the published element, - /// or the published element itself it the factory has no model for the corresponding element type. - /// - IPublishedElement CreateModel(IPublishedElement element); + /// The original published element. + /// + /// The strongly-typed model representing the published element, + /// or the published element itself it the factory has no model for the corresponding element type. + /// + IPublishedElement CreateModel(IPublishedElement element); - /// - /// Creates a List{T} of a strongly-typed model for a model type alias. - /// - /// The model type alias. - /// - /// A List{T} of the strongly-typed model, exposed as an IList. - /// - IList? CreateModelList(string? alias); + /// + /// Creates a List{T} of a strongly-typed model for a model type alias. + /// + /// The model type alias. + /// + /// A List{T} of the strongly-typed model, exposed as an IList. + /// + IList? CreateModelList(string? alias); - /// - /// Gets the Type of a strongly-typed model for a model type alias. - /// - /// The model type alias. - /// - /// The type of the strongly-typed model. - /// - Type GetModelType(string? alias); + /// + /// Gets the Type of a strongly-typed model for a model type alias. + /// + /// The model type alias. + /// + /// The type of the strongly-typed model. + /// + Type GetModelType(string? alias); - /// - /// Maps a CLR type that may contain model types, to an actual CLR type. - /// - /// The CLR type. - /// - /// The actual CLR type. - /// - /// - /// See for more details. - /// - Type MapModelType(Type type); - } + /// + /// Maps a CLR type that may contain model types, to an actual CLR type. + /// + /// The CLR type. + /// + /// The actual CLR type. + /// + /// + /// See for more details. + /// + Type MapModelType(Type type); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs index 804d0972da..b030f145fd 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs @@ -1,62 +1,73 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a property of an IPublishedElement. +/// +public interface IPublishedProperty { + IPublishedPropertyType PropertyType { get; } + /// - /// Represents a property of an IPublishedElement. + /// Gets the alias of the property. /// - public interface IPublishedProperty - { - IPublishedPropertyType PropertyType { get; } + string Alias { get; } - /// - /// Gets the alias of the property. - /// - string Alias { get; } + /// + /// Gets a value indicating whether the property has a value. + /// + /// + /// + /// This is somewhat implementation-dependent -- depending on whatever IPublishedCache considers + /// a missing value. + /// + /// + /// The XmlPublishedCache raw values are strings, and it will consider missing, null or empty (and + /// that includes whitespace-only) strings as "no value". + /// + /// + /// Other caches that get their raw value from the database would consider that a property has "no + /// value" if it is missing, null, or an empty string (including whitespace-only). + /// + /// + bool HasValue(string? culture = null, string? segment = null); - /// - /// Gets a value indicating whether the property has a value. - /// - /// - /// This is somewhat implementation-dependent -- depending on whatever IPublishedCache considers - /// a missing value. - /// The XmlPublishedCache raw values are strings, and it will consider missing, null or empty (and - /// that includes whitespace-only) strings as "no value". - /// Other caches that get their raw value from the database would consider that a property has "no - /// value" if it is missing, null, or an empty string (including whitespace-only). - /// - bool HasValue(string? culture = null, string? segment = null); + /// + /// Gets the source value of the property. + /// + /// + /// + /// The source value is whatever was passed to the property when it was instantiated, and it is + /// somewhat implementation-dependent -- depending on how the IPublishedCache is implemented. + /// + /// The XmlPublishedCache source values are strings exclusively since they come from the Xml cache. + /// + /// For other caches that get their source value from the database, it would be either a string, + /// an integer (Int32), a date and time (DateTime) or a decimal (double). + /// + /// + /// If you're using that value, you're probably wrong, unless you're doing some internal + /// Umbraco stuff. + /// + /// + object? GetSourceValue(string? culture = null, string? segment = null); - /// - /// Gets the source value of the property. - /// - /// - /// The source value is whatever was passed to the property when it was instantiated, and it is - /// somewhat implementation-dependent -- depending on how the IPublishedCache is implemented. - /// The XmlPublishedCache source values are strings exclusively since they come from the Xml cache. - /// For other caches that get their source value from the database, it would be either a string, - /// an integer (Int32), a date and time (DateTime) or a decimal (double). - /// If you're using that value, you're probably wrong, unless you're doing some internal - /// Umbraco stuff. - /// - object? GetSourceValue(string? culture = null, string? segment = null); + /// + /// Gets the object value of the property. + /// + /// + /// The value is what you want to use when rendering content in an MVC view ie in C#. + /// It can be null, or any type of CLR object. + /// It has been fully prepared and processed by the appropriate converter. + /// + object? GetValue(string? culture = null, string? segment = null); - /// - /// Gets the object value of the property. - /// - /// - /// The value is what you want to use when rendering content in an MVC view ie in C#. - /// It can be null, or any type of CLR object. - /// It has been fully prepared and processed by the appropriate converter. - /// - object? GetValue(string? culture = null, string? segment = null); - - /// - /// Gets the XPath value of the property. - /// - /// - /// The XPath value is what you want to use when navigating content via XPath eg in the XSLT engine. - /// It must be either null, or a string, or an XPathNavigator. - /// It has been fully prepared and processed by the appropriate converter. - /// - object? GetXPathValue(string? culture = null, string? segment = null); - } + /// + /// Gets the XPath value of the property. + /// + /// + /// The XPath value is what you want to use when navigating content via XPath eg in the XSLT engine. + /// It must be either null, or a string, or an XPathNavigator. + /// It has been fully prepared and processed by the appropriate converter. + /// + object? GetXPathValue(string? culture = null, string? segment = null); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs index 3ab21d15f6..3caaee9a37 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs @@ -1,108 +1,112 @@ -using System; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a published property type. +/// +/// +/// Instances implementing the interface should be +/// immutable, ie if the property type changes, then a new instance needs to be created. +/// +public interface IPublishedPropertyType { /// - /// Represents a published property type. + /// Gets the published content type containing the property type. /// - /// Instances implementing the interface should be - /// immutable, ie if the property type changes, then a new instance needs to be created. - public interface IPublishedPropertyType - { - /// - /// Gets the published content type containing the property type. - /// - IPublishedContentType? ContentType { get; } + IPublishedContentType? ContentType { get; } - /// - /// Gets the data type. - /// - PublishedDataType DataType { get; } + /// + /// Gets the data type. + /// + PublishedDataType DataType { get; } - /// - /// Gets property type alias. - /// - string Alias { get; } + /// + /// Gets property type alias. + /// + string Alias { get; } - /// - /// Gets the property editor alias. - /// - string EditorAlias { get; } + /// + /// Gets the property editor alias. + /// + string EditorAlias { get; } - /// - /// Gets a value indicating whether the property is a user content property. - /// - /// A non-user content property is a property that has been added to a - /// published content type by Umbraco but does not corresponds to a user-defined - /// published property. - bool IsUserProperty { get; } + /// + /// Gets a value indicating whether the property is a user content property. + /// + /// + /// A non-user content property is a property that has been added to a + /// published content type by Umbraco but does not corresponds to a user-defined + /// published property. + /// + bool IsUserProperty { get; } - /// - /// Gets the content variations of the property type. - /// - ContentVariation Variations { get; } + /// + /// Gets the content variations of the property type. + /// + ContentVariation Variations { get; } - /// - /// Determines whether a value is an actual value, or not a value. - /// - /// Used by property.HasValue and, for instance, in fallback scenarios. - bool? IsValue(object? value, PropertyValueLevel level); + /// + /// Gets the property cache level. + /// + PropertyCacheLevel CacheLevel { get; } - /// - /// Gets the property cache level. - /// - PropertyCacheLevel CacheLevel { get; } + /// + /// Gets the property model CLR type. + /// + /// + /// The model CLR type may be a type, or may contain types. + /// For the actual CLR type, see . + /// + Type ModelClrType { get; } - /// - /// Converts the source value into the intermediate value. - /// - /// The published element owning the property. - /// The source value. - /// A value indicating whether content should be considered draft. - /// The intermediate value. - object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview); + /// + /// Gets the property CLR type. + /// + /// + /// Returns the actual CLR type which does not contain types. + /// + /// Mapping from may throw if some instances + /// could not be mapped to actual CLR types. + /// + /// + Type? ClrType { get; } - /// - /// Converts the intermediate value into the object value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The object value. - object? ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + /// + /// Determines whether a value is an actual value, or not a value. + /// + /// Used by property.HasValue and, for instance, in fallback scenarios. + bool? IsValue(object? value, PropertyValueLevel level); - /// - /// Converts the intermediate value into the XPath value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The XPath value. - /// - /// The XPath value can be either a string or an XPathNavigator. - /// - object? ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + /// + /// Converts the source value into the intermediate value. + /// + /// The published element owning the property. + /// The source value. + /// A value indicating whether content should be considered draft. + /// The intermediate value. + object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview); - /// - /// Gets the property model CLR type. - /// - /// - /// The model CLR type may be a type, or may contain types. - /// For the actual CLR type, see . - /// - Type ModelClrType { get; } + /// + /// Converts the intermediate value into the object value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The object value. + object? ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); - /// - /// Gets the property CLR type. - /// - /// - /// Returns the actual CLR type which does not contain types. - /// Mapping from may throw if some instances - /// could not be mapped to actual CLR types. - /// - Type? ClrType { get; } - } + /// + /// Converts the intermediate value into the XPath value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The XPath value. + /// + /// The XPath value can be either a string or an XPathNavigator. + /// + object? ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index c1ecf1909a..729f7dd6bc 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -1,132 +1,173 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a fallback strategy for getting values. +/// +public interface IPublishedValueFallback { /// - /// Provides a fallback strategy for getting values. + /// Tries to get a fallback value for a property. /// - public interface IPublishedValueFallback - { - /// - /// Tries to get a fallback value for a property. - /// - /// The property. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and - /// property.HasValue(culture, segment) is false. - /// It can only fallback at property level (no recurse). - /// At property level, property.GetValue() does *not* implement fallback, and one has to - /// get property.Value() or property.Value{T}() to trigger fallback. - /// Note that and may not be contextualized, - /// so the variant context should be used to contextualize them (see our default implementation in - /// the web project. - /// - bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); + /// The property. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// + /// It can only fallback at property level (no recurse). + /// + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// + /// + /// Note that and may not be contextualized, + /// so the variant context should be used to contextualize them (see our default implementation in + /// the web project. + /// + /// + bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); - /// - /// Tries to get a fallback value for a property. - /// - /// The type of the value. - /// The property. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and - /// property.HasValue(culture, segment) is false. - /// It can only fallback at property level (no recurse). - /// At property level, property.GetValue() does *not* implement fallback, and one has to - /// get property.Value() or property.Value{T}() to trigger fallback. - /// - bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); + /// + /// Tries to get a fallback value for a property. + /// + /// The type of the value. + /// The property. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// + /// It can only fallback at property level (no recurse). + /// + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// + /// + bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); - /// - /// Tries to get a fallback value for a published element property. - /// - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// It can only fallback at element level (no recurse). - /// - bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); + /// + /// Tries to get a fallback value for a published element property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// It can only fallback at element level (no recurse). + /// + bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); - /// - /// Tries to get a fallback value for a published element property. - /// - /// The type of the value. - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// It can only fallback at element level (no recurse). - /// - bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); + /// + /// Tries to get a fallback value for a published element property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// It can only fallback at element level (no recurse). + /// + bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); - /// - /// Tries to get a fallback value for a published content property. - /// - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// The property that does not have a value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// In an , because walking up the tree is possible, the content itself may not even - /// have a property with the specified alias, but such a property may exist up in the tree. The - /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the - /// converter's interpretation of "no value". - /// - bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty); + /// + /// Tries to get a fallback value for a published content property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// The property that does not have a value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// + /// In an , because walking up the tree is possible, the content itself may not + /// even + /// have a property with the specified alias, but such a property may exist up in the tree. The + /// + /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the + /// converter's interpretation of "no value". + /// + /// + bool TryGetValue( + IPublishedContent content, + string alias, + string? culture, + string? segment, + Fallback fallback, + object? defaultValue, + out object? value, + out IPublishedProperty? noValueProperty); - /// - /// Tries to get a fallback value for a published content property. - /// - /// The type of the value. - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// The property that does not have a value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// In an , because walking up the tree is possible, the content itself may not even - /// have a property with the specified alias, but such a property may exist up in the tree. The - /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the - /// converter's interpretation of "no value". - /// - bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T defaultValue, out T? value, out IPublishedProperty? noValueProperty); - } + /// + /// Tries to get a fallback value for a published content property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// The property that does not have a value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// + /// In an , because walking up the tree is possible, the content itself may not + /// even + /// have a property with the specified alias, but such a property may exist up in the tree. The + /// + /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the + /// converter's interpretation of "no value". + /// + /// + bool TryGetValue( + IPublishedContent content, + string alias, + string? culture, + string? segment, + Fallback fallback, + T defaultValue, + out T? value, + out IPublishedProperty? noValueProperty); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs index 83c5f19c9e..a20820d954 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Gives access to the current . +/// +public interface IVariationContextAccessor { /// - /// Gives access to the current . + /// Gets or sets the current . /// - public interface IVariationContextAccessor - { - /// - /// Gets or sets the current . - /// - VariationContext? VariationContext { get; set; } - } + VariationContext? VariationContext { get; set; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs b/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs index 7c7049c026..fe7fe2a474 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs @@ -1,444 +1,386 @@ -using System.Net; +using System.Net; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents an item in an array that stores its own index and the total count. +/// +/// The type of the content. +public class IndexedArrayItem { /// - /// Represents an item in an array that stores its own index and the total count. + /// Initializes a new instance of the class. /// - /// The type of the content. - public class IndexedArrayItem + /// The content. + /// The index. + public IndexedArrayItem(TContent content, int index) { - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The index. - public IndexedArrayItem(TContent content, int index) - { - Content = content; - Index = index; - } - - /// - /// Gets the content. - /// - /// - /// The content. - /// - public TContent Content { get; } - - /// - /// Gets the index. - /// - /// - /// The index. - /// - public int Index { get; } - - /// - /// Gets the total count. - /// - /// - /// The total count. - /// - public int TotalCount { get; set; } - - /// - /// Determines whether this item is the first. - /// - /// - /// true if this item is the first; otherwise, false. - /// - public bool IsFirst() - { - return Index == 0; - } - - /// - /// If this item is the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsFirst(string valueIfTrue) - { - return IsFirst(valueIfTrue, string.Empty); - } - - /// - /// If this item is the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsFirst(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsFirst() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not the first. - /// - /// - /// true if this item is not the first; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotFirst() - { - return IsFirst() == false; - } - - - /// - /// If this item is not the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotFirst(string valueIfTrue) - { - return IsNotFirst(valueIfTrue, string.Empty); - } - - /// - /// If this item is not the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotFirst(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotFirst() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at the specified . - /// - /// The index. - /// - /// true if this item is at the specified ; otherwise, false. - /// - public bool IsIndex(int index) - { - return Index == index; - } - - /// - /// If this item is at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsIndex(int index, string valueIfTrue) - { - return IsIndex(index, valueIfTrue, string.Empty); - } - - /// - /// If this item is at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsIndex(int index, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsIndex(index) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at an index that can be divided by the specified . - /// - /// The modulus. - /// - /// true if this item is at an index that can be divided by the specified ; otherwise, false. - /// - public bool IsModZero(int modulus) - { - return Index % modulus == 0; - } - - /// - /// If this item is at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue) - { - return IsModZero(modulus, valueIfTrue, string.Empty); - } - - /// - /// If this item is at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsModZero(modulus) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not at an index that can be divided by the specified . - /// - /// The modulus. - /// - /// true if this item is not at an index that can be divided by the specified ; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotModZero(int modulus) - { - return IsModZero(modulus) == false; - } - - /// - /// If this item is not at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue) - { - return IsNotModZero(modulus, valueIfTrue, string.Empty); - } - - /// - /// If this item is not at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotModZero(modulus) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not at the specified . - /// - /// The index. - /// - /// true if this item is not at the specified ; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotIndex(int index) - { - return IsIndex(index) == false; - } - - /// - /// If this item is not at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue) - { - return IsNotIndex(index, valueIfTrue, string.Empty); - } - - /// - /// If this item is at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotIndex(index) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is the last. - /// - /// - /// true if this item is the last; otherwise, false. - /// - public bool IsLast() - { - return Index == TotalCount - 1; - } - - /// - /// If this item is the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsLast(string valueIfTrue) - { - return IsLast(valueIfTrue, string.Empty); - } - - /// - /// If this item is the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsLast(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsLast() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not the last. - /// - /// - /// true if this item is not the last; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotLast() - { - return IsLast() == false; - } - - /// - /// If this item is not the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotLast(string valueIfTrue) - { - return IsNotLast(valueIfTrue, string.Empty); - } - - /// - /// If this item is not the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotLast(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotLast() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at an even index. - /// - /// - /// true if this item is at an even index; otherwise, false. - /// - public bool IsEven() - { - return Index % 2 == 0; - } - - /// - /// If this item is at an even index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsEven(string valueIfTrue) - { - return IsEven(valueIfTrue, string.Empty); - } - - /// - /// If this item is at an even index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsEven(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsEven() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at an odd index. - /// - /// - /// true if this item is at an odd index; otherwise, false. - /// - public bool IsOdd() - { - return Index % 2 == 1; - } - - /// - /// If this item is at an odd index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsOdd(string valueIfTrue) - { - return IsOdd(valueIfTrue, string.Empty); - } - - /// - /// If this item is at an odd index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsOdd(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsOdd() ? valueIfTrue : valueIfFalse)); - } + Content = content; + Index = index; } + + /// + /// Gets the content. + /// + /// + /// The content. + /// + public TContent Content { get; } + + /// + /// Gets the index. + /// + /// + /// The index. + /// + public int Index { get; } + + /// + /// Gets the total count. + /// + /// + /// The total count. + /// + public int TotalCount { get; set; } + + /// + /// Determines whether this item is the first. + /// + /// + /// true if this item is the first; otherwise, false. + /// + public bool IsFirst() => Index == 0; + + /// + /// If this item is the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsFirst(string valueIfTrue) => IsFirst(valueIfTrue, string.Empty); + + /// + /// If this item is the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsFirst(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsFirst() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not the first. + /// + /// + /// true if this item is not the first; otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotFirst() => IsFirst() == false; + + /// + /// If this item is not the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotFirst(string valueIfTrue) => IsNotFirst(valueIfTrue, string.Empty); + + /// + /// If this item is not the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotFirst(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotFirst() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at the specified . + /// + /// The index. + /// + /// true if this item is at the specified ; otherwise, false. + /// + public bool IsIndex(int index) => Index == index; + + /// + /// If this item is at the specified , the HTML encoded will + /// be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsIndex(int index, string valueIfTrue) => IsIndex(index, valueIfTrue, string.Empty); + + /// + /// If this item is at the specified , the HTML encoded will + /// be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsIndex(int index, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsIndex(index) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at an index that can be divided by the specified . + /// + /// The modulus. + /// + /// true if this item is at an index that can be divided by the specified ; + /// otherwise, false. + /// + public bool IsModZero(int modulus) => Index % modulus == 0; + + /// + /// If this item is at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue) => + IsModZero(modulus, valueIfTrue, string.Empty); + + /// + /// If this item is at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsModZero(modulus) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not at an index that can be divided by the specified . + /// + /// The modulus. + /// + /// true if this item is not at an index that can be divided by the specified ; + /// otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotModZero(int modulus) => IsModZero(modulus) == false; + + /// + /// If this item is not at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue) => + IsNotModZero(modulus, valueIfTrue, string.Empty); + + /// + /// If this item is not at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotModZero(modulus) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not at the specified . + /// + /// The index. + /// + /// true if this item is not at the specified ; otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotIndex(int index) => IsIndex(index) == false; + + /// + /// If this item is not at the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue) => IsNotIndex(index, valueIfTrue, string.Empty); + + /// + /// If this item is at the specified , the HTML encoded will + /// be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotIndex(index) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is the last. + /// + /// + /// true if this item is the last; otherwise, false. + /// + public bool IsLast() => Index == TotalCount - 1; + + /// + /// If this item is the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsLast(string valueIfTrue) => IsLast(valueIfTrue, string.Empty); + + /// + /// If this item is the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsLast(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsLast() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not the last. + /// + /// + /// true if this item is not the last; otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotLast() => IsLast() == false; + + /// + /// If this item is not the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotLast(string valueIfTrue) => IsNotLast(valueIfTrue, string.Empty); + + /// + /// If this item is not the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotLast(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotLast() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at an even index. + /// + /// + /// true if this item is at an even index; otherwise, false. + /// + public bool IsEven() => Index % 2 == 0; + + /// + /// If this item is at an even index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsEven(string valueIfTrue) => IsEven(valueIfTrue, string.Empty); + + /// + /// If this item is at an even index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsEven(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsEven() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at an odd index. + /// + /// + /// true if this item is at an odd index; otherwise, false. + /// + public bool IsOdd() => Index % 2 == 1; + + /// + /// If this item is at an odd index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsOdd(string valueIfTrue) => IsOdd(valueIfTrue, string.Empty); + + /// + /// If this item is at an odd index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsOdd(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsOdd() ? valueIfTrue : valueIfFalse)); } diff --git a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs index 0de838fa0e..4588d47967 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs @@ -1,412 +1,518 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Reflection; using Umbraco.Cms.Core.Exceptions; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// +/// Represents the CLR type of a model. +/// +/// +/// ModelType.For("alias") +/// typeof (IEnumerable{}).MakeGenericType(ModelType.For("alias")) +/// Model.For("alias").MakeArrayType() +/// +public class ModelType : Type { - /// - /// - /// Represents the CLR type of a model. - /// - /// - /// ModelType.For("alias") - /// typeof (IEnumerable{}).MakeGenericType(ModelType.For("alias")) - /// Model.For("alias").MakeArrayType() - /// - public class ModelType : Type + private ModelType(string? contentTypeAlias) { - private ModelType(string? contentTypeAlias) + if (contentTypeAlias == null) { - if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias)); - - ContentTypeAlias = contentTypeAlias; - Name = "{" + ContentTypeAlias + "}"; + throw new ArgumentNullException(nameof(contentTypeAlias)); } - /// - /// Gets the content type alias. - /// - public string ContentTypeAlias { get; } - - /// - public override string ToString() - => Name; - - /// - /// Gets the model type for a published element type. - /// - /// The published element type alias. - /// The model type for the published element type. - public static ModelType For(string? alias) - => new ModelType(alias); - - /// - /// Gets the actual CLR type by replacing model types, if any. - /// - /// The type. - /// The model types map. - /// The actual CLR type. - public static Type Map(Type type, Dictionary? modelTypes) - => Map(type, modelTypes, false); - - public static Type Map(Type type, Dictionary? modelTypes, bool dictionaryIsInvariant) + if (string.IsNullOrWhiteSpace(contentTypeAlias)) { - // it may be that senders forgot to send an invariant dictionary (garbage-in) - if (modelTypes is not null && !dictionaryIsInvariant) - modelTypes = new Dictionary(modelTypes, StringComparer.InvariantCultureIgnoreCase); - - if (type is ModelType modelType) - { - if (modelTypes?.TryGetValue(modelType.ContentTypeAlias, out var actualType) ?? false) - return actualType; - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); - } - - if (type is ModelTypeArrayType arrayType) - { - if (modelTypes?.TryGetValue(arrayType.ContentTypeAlias, out var actualType) ?? false) - return actualType.MakeArrayType(); - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); - } - - if (type.IsGenericType == false) - return type; - var def = type.GetGenericTypeDefinition(); - if (def == null) - throw new PanicException($"The type {type} has not generic type definition"); - - var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); - return def.MakeGenericType(args); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(contentTypeAlias)); } - /// - /// Gets the actual CLR type name by replacing model types, if any. - /// - /// The type. - /// The model types map. - /// The actual CLR type name. - public static string MapToName(Type type, Dictionary map) - => MapToName(type, map, false); + ContentTypeAlias = contentTypeAlias; + Name = "{" + ContentTypeAlias + "}"; + } - private static string MapToName(Type type, Dictionary map, bool dictionaryIsInvariant) + /// + /// Gets the content type alias. + /// + public string ContentTypeAlias { get; } + + /// + public override Type UnderlyingSystemType => this; + + /// + public override Type? BaseType => null; + + /// + public override string Name { get; } + + /// + public override Guid GUID { get; } = Guid.NewGuid(); + + /// + public override Module Module => GetType().Module; // hackish but FullName requires something + + /// + public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something + + /// + public override string FullName => Name; + + /// + public override string Namespace => string.Empty; + + /// + public override string AssemblyQualifiedName => Name; + + /// + /// Gets the model type for a published element type. + /// + /// The published element type alias. + /// The model type for the published element type. + public static ModelType For(string? alias) + => new(alias); + + /// + public override string ToString() + => Name; + + /// + /// Gets the actual CLR type by replacing model types, if any. + /// + /// The type. + /// The model types map. + /// The actual CLR type. + public static Type Map(Type type, Dictionary? modelTypes) + => Map(type, modelTypes, false); + + public static Type Map(Type type, Dictionary? modelTypes, bool dictionaryIsInvariant) + { + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (modelTypes is not null && !dictionaryIsInvariant) { - // it may be that senders forgot to send an invariant dictionary (garbage-in) - if (!dictionaryIsInvariant) - map = new Dictionary(map, StringComparer.InvariantCultureIgnoreCase); - - if (type is ModelType modelType) - { - if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName)) - return actualTypeName; - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); - } - - if (type is ModelTypeArrayType arrayType) - { - if (map.TryGetValue(arrayType.ContentTypeAlias, out var actualTypeName)) - return actualTypeName + "[]"; - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); - } - - if (type.IsGenericType == false) - return type.FullName!; - var def = type.GetGenericTypeDefinition(); - if (def == null) - throw new PanicException($"The type {type} has not generic type definition"); - - var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); - var defFullName = def.FullName?.Substring(0, def.FullName.IndexOf('`')); - return defFullName + "<" + string.Join(", ", args) + ">"; + modelTypes = new Dictionary(modelTypes, StringComparer.InvariantCultureIgnoreCase); } - /// - /// Gets a value indicating whether two instances are equal. - /// - /// The first instance. - /// The second instance. - /// A value indicating whether the two instances are equal. - /// Knows how to compare instances. - public static bool Equals(Type t1, Type t2) + if (type is ModelType modelType) { - if (t1 == t2) - return true; - - if (t1 is ModelType m1 && t2 is ModelType m2) - return m1.ContentTypeAlias == m2.ContentTypeAlias; - - if (t1 is ModelTypeArrayType a1 && t2 is ModelTypeArrayType a2) - return a1.ContentTypeAlias == a2.ContentTypeAlias; - - if (t1.IsGenericType == false || t2.IsGenericType == false) - return false; - - var args1 = t1.GetGenericArguments(); - var args2 = t2.GetGenericArguments(); - if (args1.Length != args2.Length) return false; - - for (var i = 0; i < args1.Length; i++) + if (modelTypes?.TryGetValue(modelType.ContentTypeAlias, out Type? actualType) ?? false) { - // ReSharper disable once CheckForReferenceEqualityInstead.2 - if (Equals(args1[i], args2[i]) == false) return false; + return actualType; } + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); + } + + if (type is ModelTypeArrayType arrayType) + { + if (modelTypes?.TryGetValue(arrayType.ContentTypeAlias, out Type? actualType) ?? false) + { + return actualType.MakeArrayType(); + } + + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); + } + + if (type.IsGenericType == false) + { + return type; + } + + Type def = type.GetGenericTypeDefinition(); + if (def == null) + { + throw new PanicException($"The type {type} has not generic type definition"); + } + + Type[] args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); + return def.MakeGenericType(args); + } + + /// + /// Gets the actual CLR type name by replacing model types, if any. + /// + /// The type. + /// The model types map. + /// The actual CLR type name. + public static string MapToName(Type type, Dictionary map) + => MapToName(type, map, false); + + /// + /// Gets a value indicating whether two instances are equal. + /// + /// The first instance. + /// The second instance. + /// A value indicating whether the two instances are equal. + /// Knows how to compare instances. + public static bool Equals(Type t1, Type t2) + { + if (t1 == t2) + { return true; } - /// - protected override TypeAttributes GetAttributeFlagsImpl() - => TypeAttributes.Class; + if (t1 is ModelType m1 && t2 is ModelType m2) + { + return m1.ContentTypeAlias == m2.ContentTypeAlias; + } - /// - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => Array.Empty(); + if (t1 is ModelTypeArrayType a1 && t2 is ModelTypeArrayType a2) + { + return a1.ContentTypeAlias == a2.ContentTypeAlias; + } - /// - protected override ConstructorInfo? GetConstructorImpl(BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[] types, ParameterModifier[]? modifiers) - => null; + if (t1.IsGenericType == false || t2.IsGenericType == false) + { + return false; + } - /// - public override Type[] GetInterfaces() - => Array.Empty(); + Type[] args1 = t1.GetGenericArguments(); + Type[] args2 = t2.GetGenericArguments(); + if (args1.Length != args2.Length) + { + return false; + } - /// - public override Type? GetInterface(string name, bool ignoreCase) - => null; + for (var i = 0; i < args1.Length; i++) + { + // ReSharper disable once CheckForReferenceEqualityInstead.2 + if (Equals(args1[i], args2[i]) == false) + { + return false; + } + } - /// - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) - => null; - - /// - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override Type? GetNestedType(string name, BindingFlags bindingAttr) - => null; - - /// - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => Array.Empty(); - - /// - protected override PropertyInfo? GetPropertyImpl(string name, BindingFlags bindingAttr, Binder? binder, Type? returnType, Type[]? types, ParameterModifier[]? modifiers) - => null; - - /// - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => Array.Empty(); - - /// - protected override MethodInfo? GetMethodImpl(string name, BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[]? types, ParameterModifier[]? modifiers) - => null; - - /// - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override FieldInfo? GetField(string name, BindingFlags bindingAttr) - => null; - - /// - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); - - /// - public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); - - /// - public override bool IsDefined(Type attributeType, bool inherit) - => false; - - /// - public override Type? GetElementType() - => null; - - /// - protected override bool HasElementTypeImpl() - => false; - - /// - protected override bool IsArrayImpl() - => false; - - /// - protected override bool IsByRefImpl() - => false; - - /// - protected override bool IsPointerImpl() - => false; - - /// - protected override bool IsPrimitiveImpl() - => false; - - /// - protected override bool IsCOMObjectImpl() - => false; - - /// - public override object InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, object?[]? args, ParameterModifier[]? modifiers, CultureInfo? culture, string[]? namedParameters) - => throw new NotSupportedException(); - - /// - public override Type UnderlyingSystemType => this; - - /// - public override Type? BaseType => null; - - /// - public override string Name { get; } - - /// - public override Guid GUID { get; } = Guid.NewGuid(); - - /// - public override Module Module => GetType().Module; // hackish but FullName requires something - - /// - public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something - - /// - public override string FullName => Name; - - /// - public override string Namespace => string.Empty; - - /// - public override string AssemblyQualifiedName => Name; - - /// - public override Type MakeArrayType() - => new ModelTypeArrayType(this); + return true; } - internal class ModelTypeArrayType : Type + /// + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override Type[] GetInterfaces() + => Array.Empty(); + + private static string MapToName(Type type, Dictionary map, bool dictionaryIsInvariant) { - private readonly Type _elementType; - - public ModelTypeArrayType(ModelType type) + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (!dictionaryIsInvariant) { - _elementType = type; - ContentTypeAlias = type.ContentTypeAlias; - Name = "{" + type.ContentTypeAlias + "}[*]"; + map = new Dictionary(map, StringComparer.InvariantCultureIgnoreCase); } - public string ContentTypeAlias { get; } - - public override string ToString() - => Name; - - protected override TypeAttributes GetAttributeFlagsImpl() - => TypeAttributes.Class; - - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => Array.Empty(); - - protected override ConstructorInfo? GetConstructorImpl(BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[] types, ParameterModifier[]? modifiers) - => null; - - public override Type[] GetInterfaces() - => Array.Empty(); - - public override Type? GetInterface(string name, bool ignoreCase) - => null; - - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => Array.Empty(); - - public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) - => null; - - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => Array.Empty(); - - public override Type? GetNestedType(string name, BindingFlags bindingAttr) - => null; - - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => Array.Empty(); - - protected override PropertyInfo? GetPropertyImpl(string name, BindingFlags bindingAttr, Binder? binder, Type? returnType, Type[]? types, ParameterModifier[]? modifiers) - => null; - - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => Array.Empty(); - - protected override MethodInfo? GetMethodImpl(string name, BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[]? types, ParameterModifier[]? modifiers) - => null; - - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => Array.Empty(); - - public override FieldInfo? GetField(string name, BindingFlags bindingAttr) - => null; - - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => Array.Empty(); - - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); - - public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); - - public override bool IsDefined(Type attributeType, bool inherit) - => false; - - public override Type GetElementType() - => _elementType; - - protected override bool HasElementTypeImpl() - => true; - - protected override bool IsArrayImpl() - => true; - - protected override bool IsByRefImpl() - => false; - - protected override bool IsPointerImpl() - => false; - - protected override bool IsPrimitiveImpl() - => false; - - protected override bool IsCOMObjectImpl() - => false; - public override object InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, object?[]? args, ParameterModifier[]? modifiers, CultureInfo? culture, string[]? namedParameters) + if (type is ModelType modelType) { - throw new NotSupportedException(); + if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName)) + { + return actualTypeName; + } + + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); } - public override Type UnderlyingSystemType => this; - public override Type? BaseType => null; + if (type is ModelTypeArrayType arrayType) + { + if (map.TryGetValue(arrayType.ContentTypeAlias, out var actualTypeName)) + { + return actualTypeName + "[]"; + } - public override string Name { get; } - public override Guid GUID { get; } = Guid.NewGuid(); - public override Module Module =>GetType().Module; // hackish but FullName requires something - public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something - public override string FullName => Name; - public override string Namespace => string.Empty; - public override string AssemblyQualifiedName => Name; + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); + } - public override int GetArrayRank() - => 1; + if (type.IsGenericType == false) + { + return type.FullName!; + } + + Type def = type.GetGenericTypeDefinition(); + if (def == null) + { + throw new PanicException($"The type {type} has not generic type definition"); + } + + var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); + var defFullName = def.FullName?[..def.FullName.IndexOf('`')]; + return defFullName + "<" + string.Join(", ", args) + ">"; } + + /// + protected override TypeAttributes GetAttributeFlagsImpl() + => TypeAttributes.Class; + + /// + protected override ConstructorInfo? GetConstructorImpl( + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[] types, + ParameterModifier[]? modifiers) + => null; + + /// + public override Type? GetInterface(string name, bool ignoreCase) + => null; + + /// + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) + => null; + + /// + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override Type? GetNestedType(string name, BindingFlags bindingAttr) + => null; + + /// + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + => Array.Empty(); + + /// + protected override PropertyInfo? GetPropertyImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + Type? returnType, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + /// + protected override MethodInfo? GetMethodImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + /// + public override FieldInfo? GetField(string name, BindingFlags bindingAttr) + => null; + + /// + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + => Array.Empty(); + + /// + public override object[] GetCustomAttributes(bool inherit) + => Array.Empty(); + + /// + public override bool IsDefined(Type attributeType, bool inherit) + => false; + + /// + public override Type? GetElementType() + => null; + + /// + public override object InvokeMember( + string name, + BindingFlags invokeAttr, + Binder? binder, + object? target, + object?[]? args, + ParameterModifier[]? modifiers, + CultureInfo? culture, + string[]? namedParameters) + => throw new NotSupportedException(); + + /// + protected override bool HasElementTypeImpl() + => false; + + /// + protected override bool IsArrayImpl() + => false; + + /// + protected override bool IsByRefImpl() + => false; + + /// + protected override bool IsPointerImpl() + => false; + + /// + protected override bool IsPrimitiveImpl() + => false; + + /// + protected override bool IsCOMObjectImpl() + => false; + + /// + public override Type MakeArrayType() + => new ModelTypeArrayType(this); +} + +/// +internal class ModelTypeArrayType : Type +{ + private readonly Type _elementType; + + public ModelTypeArrayType(ModelType type) + { + _elementType = type; + ContentTypeAlias = type.ContentTypeAlias; + Name = "{" + type.ContentTypeAlias + "}[*]"; + } + + public string ContentTypeAlias { get; } + + public override Type UnderlyingSystemType => this; + + public override Type? BaseType => null; + + public override string Name { get; } + + public override Guid GUID { get; } = Guid.NewGuid(); + + public override Module Module => GetType().Module; // hackish but FullName requires something + + public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something + + public override string FullName => Name; + + public override string Namespace => string.Empty; + + public override string AssemblyQualifiedName => Name; + + public override string ToString() + => Name; + + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + => Array.Empty(); + + public override Type[] GetInterfaces() + => Array.Empty(); + + protected override TypeAttributes GetAttributeFlagsImpl() + => TypeAttributes.Class; + + protected override ConstructorInfo? GetConstructorImpl( + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[] types, + ParameterModifier[]? modifiers) + => null; + + public override Type? GetInterface(string name, bool ignoreCase) + => null; + + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + => Array.Empty(); + + public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) + => null; + + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + => Array.Empty(); + + public override Type? GetNestedType(string name, BindingFlags bindingAttr) + => null; + + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + => Array.Empty(); + + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + => Array.Empty(); + + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + => Array.Empty(); + + protected override PropertyInfo? GetPropertyImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + Type? returnType, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + protected override MethodInfo? GetMethodImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + public override FieldInfo? GetField(string name, BindingFlags bindingAttr) + => null; + + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + => Array.Empty(); + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + => Array.Empty(); + + public override object[] GetCustomAttributes(bool inherit) + => Array.Empty(); + + public override bool IsDefined(Type attributeType, bool inherit) + => false; + + public override Type GetElementType() + => _elementType; + + public override object InvokeMember( + string name, + BindingFlags invokeAttr, + Binder? binder, + object? target, + object?[]? args, + ParameterModifier[]? modifiers, + CultureInfo? culture, + string[]? namedParameters) => + throw new NotSupportedException(); + + protected override bool HasElementTypeImpl() + => true; + + protected override bool IsArrayImpl() + => true; + + protected override bool IsByRefImpl() + => false; + + protected override bool IsPointerImpl() + => false; + + protected override bool IsPrimitiveImpl() + => false; + + protected override bool IsCOMObjectImpl() + => false; + + public override int GetArrayRank() + => 1; } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs index 93b6948edc..5eefd1e12b 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs @@ -1,21 +1,20 @@ using System.Collections; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a no-operation factory. +public class NoopPublishedModelFactory : IPublishedModelFactory { /// - /// Represents a no-operation factory. - public class NoopPublishedModelFactory : IPublishedModelFactory - { - /// - public IPublishedElement CreateModel(IPublishedElement element) => element; + public IPublishedElement CreateModel(IPublishedElement element) => element; - /// - public IList CreateModelList(string? alias) => new List(); + /// + public IList CreateModelList(string? alias) => new List(); - /// - public Type GetModelType(string? alias) => typeof(IPublishedElement); + /// + public Type GetModelType(string? alias) => typeof(IPublishedElement); - /// - public Type MapModelType(Type type) => typeof(IPublishedElement); - } + /// + public Type MapModelType(Type type) => typeof(IPublishedElement); } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index a08a20658d..1dd2fef124 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -1,55 +1,54 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a noop implementation for . +/// +/// +/// This is for tests etc - does not implement fallback at all. +/// +public class NoopPublishedValueFallback : IPublishedValueFallback { - /// - /// Provides a noop implementation for . - /// - /// - /// This is for tests etc - does not implement fallback at all. - /// - public class NoopPublishedValueFallback : IPublishedValueFallback + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) { - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) - { - value = default; - return false; - } + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) - { - value = default; - return false; - } + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) - { - value = default; - return false; - } + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) + { + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) - { - value = default; - return false; - } + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) - { - value = default; - noValueProperty = default; - return false; - } + /// + public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) + { + value = default; + noValueProperty = default; + return false; + } - /// - public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T defaultValue, out T? value, out IPublishedProperty? noValueProperty) - { - value = default; - noValueProperty = default; - return false; - } + /// + public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T defaultValue, out T? value, out IPublishedProperty? noValueProperty) + { + value = default; + noValueProperty = default; + return false; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs index 60d3cd4a02..077b420735 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent @@ -15,26 +13,13 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { private readonly IVariationContextAccessor? _variationContextAccessor; - protected PublishedContentBase(IVariationContextAccessor? variationContextAccessor) - { - _variationContextAccessor = variationContextAccessor; - } - - #region ContentType + protected PublishedContentBase(IVariationContextAccessor? variationContextAccessor) => _variationContextAccessor = variationContextAccessor; public abstract IPublishedContentType ContentType { get; } - #endregion - - #region PublishedElement - /// public abstract Guid Key { get; } - #endregion - - #region PublishedContent - /// public abstract int Id { get; } @@ -80,10 +65,6 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public abstract bool IsPublished(string? culture = null); - #endregion - - #region Tree - /// public abstract IPublishedContent? Parent { get; } @@ -93,16 +74,10 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public abstract IEnumerable ChildrenForAllCultures { get; } - #endregion - - #region Properties - /// public abstract IEnumerable Properties { get; } /// public abstract IPublishedProperty? GetProperty(string alias); - - #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs index f1d348d2ff..2b123a33a9 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs @@ -1,35 +1,47 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides strongly typed published content models services. +/// +public static class PublishedContentExtensionsForModels { /// - /// Provides strongly typed published content models services. + /// Creates a strongly typed published content model for an internal published content. /// - public static class PublishedContentExtensionsForModels + /// The internal published content. + /// The published model factory + /// The strongly typed published content model. + public static IPublishedContent? CreateModel( + this IPublishedContent? content, + IPublishedModelFactory? publishedModelFactory) { - /// - /// Creates a strongly typed published content model for an internal published content. - /// - /// The internal published content. - /// The strongly typed published content model. - public static IPublishedContent? CreateModel(this IPublishedContent content, IPublishedModelFactory? publishedModelFactory) + if (publishedModelFactory == null) { - if (publishedModelFactory == null) throw new ArgumentNullException(nameof(publishedModelFactory)); - if (content == null) - return null; - - // get model - // if factory returns nothing, throw - var model = publishedModelFactory.CreateModel(content); - if (model == null) - throw new InvalidOperationException("Factory returned null."); - - // if factory returns a different type, throw - if (!(model is IPublishedContent publishedContent)) - throw new InvalidOperationException($"Factory returned model of type {model.GetType().FullName} which does not implement IPublishedContent."); - - return publishedContent; + throw new ArgumentNullException(nameof(publishedModelFactory)); } + + if (content == null) + { + return null; + } + + // get model + // if factory returns nothing, throw + IPublishedElement model = publishedModelFactory.CreateModel(content); + if (model == null) + { + throw new InvalidOperationException("Factory returned null."); + } + + // if factory returns a different type, throw + if (!(model is IPublishedContent publishedContent)) + { + throw new InvalidOperationException( + $"Factory returned model of type {model.GetType().FullName} which does not implement IPublishedContent."); + } + + return publishedContent; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs index 249c2cb465..cfa648594e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs @@ -1,21 +1,22 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a strongly-typed published content. +/// +/// +/// Every strongly-typed published content class should inherit from PublishedContentModel +/// (or inherit from a class that inherits from... etc.) so they are picked by the factory. +/// +public abstract class PublishedContentModel : PublishedContentWrapped { /// - /// Represents a strongly-typed published content. + /// Initializes a new instance of the class with + /// an original instance. /// - /// Every strongly-typed published content class should inherit from PublishedContentModel - /// (or inherit from a class that inherits from... etc.) so they are picked by the factory. - public abstract class PublishedContentModel : PublishedContentWrapped + /// The original content. + /// the PublishedValueFallback + protected PublishedContentModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback) + : base(content, publishedValueFallback) { - /// - /// Initializes a new instance of the class with - /// an original instance. - /// - /// The original content. - protected PublishedContentModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback) - : base(content, publishedValueFallback) - { } - - } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index e7a113ed09..aeee722ed2 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent @@ -30,7 +27,9 @@ namespace Umbraco.Cms.Core.Models.PublishedContent .ToList(); if (ItemType == PublishedItemType.Member) + { EnsureMemberProperties(propertyTypes, factory); + } _propertyTypes = propertyTypes.ToArray(); @@ -46,9 +45,12 @@ namespace Umbraco.Cms.Core.Models.PublishedContent public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false) : this(key, id, alias, itemType, compositionAliases, variations, isElement) { - var propertyTypesA = propertyTypes.ToArray(); - foreach (var propertyType in propertyTypesA) + PublishedPropertyType[] propertyTypesA = propertyTypes.ToArray(); + foreach (PublishedPropertyType propertyType in propertyTypesA) + { propertyType.ContentType = this; + } + _propertyTypes = propertyTypesA; InitializeIndexes(); @@ -102,15 +104,19 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { var aliases = new HashSet(propertyTypes.Select(x => x.Alias), StringComparer.OrdinalIgnoreCase); - foreach (var (alias, dataTypeId) in BuiltinMemberProperties) + foreach (var (alias, dataTypeId) in _builtinMemberProperties) { - if (aliases.Contains(alias)) continue; + if (aliases.Contains(alias)) + { + continue; + } + propertyTypes.Add(factory.CreateCorePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); } } // TODO: this list somehow also exists in constants, see memberTypeRepository => remove duplicate! - private static readonly Dictionary BuiltinMemberProperties = new Dictionary + private static readonly Dictionary _builtinMemberProperties = new Dictionary { { nameof(IMember.Email), Constants.DataTypes.Textbox }, { nameof(IMember.Username), Constants.DataTypes.Textbox }, @@ -153,8 +159,16 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public int GetPropertyIndex(string alias) { - if (_indexes.TryGetValue(alias, out var index)) return index; // fastest - if (_indexes.TryGetValue(alias.ToLowerInvariant(), out index)) return index; // slower + if (_indexes.TryGetValue(alias, out var index)) + { + return index; // fastest + } + + if (_indexes.TryGetValue(alias.ToLowerInvariant(), out index)) + { + return index; // slower + } + return -1; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs index 23adf358ca..957246ccfe 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs @@ -1,28 +1,25 @@ -using System; using System.ComponentModel; using System.Globalization; -using System.Linq; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +internal class PublishedContentTypeConverter : TypeConverter { - internal class PublishedContentTypeConverter : TypeConverter + private static readonly Type[] ConvertingTypes = { typeof(int) }; + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => + ConvertingTypes.Any(x => x.IsAssignableFrom(destinationType)) + || (destinationType is not null && CanConvertFrom(context, destinationType)); + + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { - private static readonly Type[] ConvertingTypes = { typeof(int) }; - - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + if (!(value is IPublishedContent publishedContent)) { - return ConvertingTypes.Any(x => x.IsAssignableFrom(destinationType)) - || (destinationType is not null && CanConvertFrom(context, destinationType)); + return null; } - public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) - { - if (!(value is IPublishedContent publishedContent)) - return null; - - return typeof(int).IsAssignableFrom(destinationType) - ? publishedContent.Id - : base.ConvertTo(context, culture, value, destinationType); - } + return typeof(int).IsAssignableFrom(destinationType) + ? publishedContent.Id + : base.ConvertTo(context, culture, value, destinationType); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 5a43295981..f2b1b9bbca 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -1,124 +1,141 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a default implementation for . +/// +public class PublishedContentTypeFactory : IPublishedContentTypeFactory { - /// - /// Provides a default implementation for . - /// - public class PublishedContentTypeFactory : IPublishedContentTypeFactory + private readonly IDataTypeService _dataTypeService; + private readonly PropertyValueConverterCollection _propertyValueConverters; + private readonly object _publishedDataTypesLocker = new(); + private readonly IPublishedModelFactory _publishedModelFactory; + private Dictionary? _publishedDataTypes; + + public PublishedContentTypeFactory( + IPublishedModelFactory publishedModelFactory, + PropertyValueConverterCollection propertyValueConverters, + IDataTypeService dataTypeService) { - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly PropertyValueConverterCollection _propertyValueConverters; - private readonly IDataTypeService _dataTypeService; - private readonly object _publishedDataTypesLocker = new object(); - private Dictionary? _publishedDataTypes; - - public PublishedContentTypeFactory(IPublishedModelFactory publishedModelFactory, PropertyValueConverterCollection propertyValueConverters, IDataTypeService dataTypeService) - { - _publishedModelFactory = publishedModelFactory; - _propertyValueConverters = propertyValueConverters; - _dataTypeService = dataTypeService; - } - - /// - public IPublishedContentType CreateContentType(IContentTypeComposition contentType) - { - return new PublishedContentType(contentType, this); - } - - /// - /// This method is for tests and is not intended to be used directly from application code. - /// - /// Values are assumed to be consisted and are not checked. - internal IPublishedContentType CreateContentType(Guid key, int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) - { - return new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); - } - - /// - /// This method is for tests and is not intended to be used directly from application code. - /// - /// Values are assumed to be consisted and are not checked. - internal IPublishedContentType CreateContentType(Guid key, int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) - { - return new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); - } - - /// - public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType) - { - return new PublishedPropertyType(contentType, propertyType, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) - { - return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - public IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) - { - return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - /// This method is for tests and is not intended to be used directly from application code. - /// - /// Values are assumed to be consisted and are not checked. - internal IPublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) - { - return new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - public PublishedDataType GetDataType(int id) - { - Dictionary? publishedDataTypes; - lock (_publishedDataTypesLocker) - { - if (_publishedDataTypes == null) - { - var dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); - } - - publishedDataTypes = _publishedDataTypes; - } - - if (publishedDataTypes is null || !publishedDataTypes.TryGetValue(id, out var dataType)) - throw new ArgumentException($"Could not find a datatype with identifier {id}.", nameof(id)); - - return dataType; - } - - /// - public void NotifyDataTypeChanges(int[] ids) - { - lock (_publishedDataTypesLocker) - { - if (_publishedDataTypes == null) - { - var dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); - } - else - { - foreach (var id in ids) - _publishedDataTypes.Remove(id); - - var dataTypes = _dataTypeService.GetAll(ids); - foreach (var dataType in dataTypes) - _publishedDataTypes[dataType.Id] = CreatePublishedDataType(dataType); - } - } - } - - private PublishedDataType CreatePublishedDataType(IDataType dataType) - => new PublishedDataType(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfiguration() : new Lazy(() => dataType.Configuration)); + _publishedModelFactory = publishedModelFactory; + _propertyValueConverters = propertyValueConverters; + _dataTypeService = dataTypeService; } + + /// + public IPublishedContentType CreateContentType(IContentTypeComposition contentType) => + new PublishedContentType(contentType, this); + + /// + public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType) => + new PublishedPropertyType(contentType, propertyType, _propertyValueConverters, _publishedModelFactory, this); + + /// + public IPublishedPropertyType CreatePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations = ContentVariation.Nothing) => + new PublishedPropertyType( + contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); + + /// + public IPublishedPropertyType CreateCorePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations = ContentVariation.Nothing) => + new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this); + + /// + public PublishedDataType GetDataType(int id) + { + Dictionary? publishedDataTypes; + lock (_publishedDataTypesLocker) + { + if (_publishedDataTypes == null) + { + IEnumerable dataTypes = _dataTypeService.GetAll(); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); + } + + publishedDataTypes = _publishedDataTypes; + } + + if (publishedDataTypes is null || !publishedDataTypes.TryGetValue(id, out PublishedDataType? dataType)) + { + throw new ArgumentException($"Could not find a datatype with identifier {id}.", nameof(id)); + } + + return dataType; + } + + /// + public void NotifyDataTypeChanges(int[] ids) + { + lock (_publishedDataTypesLocker) + { + if (_publishedDataTypes == null) + { + IEnumerable dataTypes = _dataTypeService.GetAll(); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); + } + else + { + foreach (var id in ids) + { + _publishedDataTypes.Remove(id); + } + + IEnumerable dataTypes = _dataTypeService.GetAll(ids); + foreach (IDataType dataType in dataTypes) + { + _publishedDataTypes[dataType.Id] = CreatePublishedDataType(dataType); + } + } + } + } + + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType( + Guid key, + int id, + string alias, + Func> propertyTypes, + ContentVariation variations = ContentVariation.Nothing, + bool isElement = false) => + new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); + + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType( + Guid key, + int id, + string alias, + IEnumerable compositionAliases, + Func> propertyTypes, + ContentVariation variations = ContentVariation.Nothing, + bool isElement = false) => + new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); + + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedPropertyType CreatePropertyType( + string propertyTypeAlias, + int dataTypeId, + bool umbraco = false, + ContentVariation variations = ContentVariation.Nothing) => + new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); + + private PublishedDataType CreatePublishedDataType(IDataType dataType) + => new(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfiguration() : new Lazy(() => dataType.Configuration)); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 9d16de743d..b5e9a94e13 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -1,135 +1,112 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +// we cannot implement strongly-typed content by inheriting from some sort +// of "master content" because that master content depends on the actual content cache +// that is being used. It can be an XmlPublishedContent with the XmlPublishedCache, +// or just anything else. +// +// So we implement strongly-typed content by encapsulating whatever content is +// returned by the content cache, and providing extra properties (mostly) or +// methods or whatever. This class provides the base for such encapsulation. +// + +/// +/// Provides an abstract base class for IPublishedContent implementations that +/// wrap and extend another IPublishedContent. +/// +[DebuggerDisplay("{Id}: {Name} ({ContentType?.Alias})")] +public abstract class PublishedContentWrapped : IPublishedContent { - // - // we cannot implement strongly-typed content by inheriting from some sort - // of "master content" because that master content depends on the actual content cache - // that is being used. It can be an XmlPublishedContent with the XmlPublishedCache, - // or just anything else. - // - // So we implement strongly-typed content by encapsulating whatever content is - // returned by the content cache, and providing extra properties (mostly) or - // methods or whatever. This class provides the base for such encapsulation. - // + private readonly IPublishedContent _content; + private readonly IPublishedValueFallback _publishedValueFallback; /// - /// Provides an abstract base class for IPublishedContent implementations that - /// wrap and extend another IPublishedContent. + /// Initialize a new instance of the class + /// with an IPublishedContent instance to wrap. /// - [DebuggerDisplay("{Id}: {Name} ({ContentType?.Alias})")] - public abstract class PublishedContentWrapped : IPublishedContent + /// The content to wrap. + /// The published value fallback. + protected PublishedContentWrapped(IPublishedContent content, IPublishedValueFallback publishedValueFallback) { - private readonly IPublishedContent _content; - private readonly IPublishedValueFallback _publishedValueFallback; - - /// - /// Initialize a new instance of the class - /// with an IPublishedContent instance to wrap. - /// - /// The content to wrap. - /// The published value fallback. - protected PublishedContentWrapped(IPublishedContent content, IPublishedValueFallback publishedValueFallback) - { - _content = content; - _publishedValueFallback = publishedValueFallback; - } - - /// - /// Gets the wrapped content. - /// - /// The wrapped content, that was passed as an argument to the constructor. - public IPublishedContent Unwrap() => _content; - - #region ContentType - - /// - public virtual IPublishedContentType ContentType => _content.ContentType; - - #endregion - - #region PublishedElement - - /// - public Guid Key => _content.Key; - - #endregion - - #region PublishedContent - - /// - public virtual int Id => _content.Id; - - /// - public virtual string? Name => _content.Name; - - /// - public virtual string? UrlSegment => _content.UrlSegment; - - /// - public virtual int SortOrder => _content.SortOrder; - - /// - public virtual int Level => _content.Level; - - /// - public virtual string Path => _content.Path; - - /// - public virtual int? TemplateId => _content.TemplateId; - - /// - public virtual int CreatorId => _content.CreatorId; - - /// - public virtual DateTime CreateDate => _content.CreateDate; - - /// - public virtual int WriterId => _content.WriterId; - - /// - public virtual DateTime UpdateDate => _content.UpdateDate; - - /// - public IReadOnlyDictionary Cultures => _content.Cultures; - - /// - public virtual PublishedItemType ItemType => _content.ItemType; - - /// - public virtual bool IsDraft(string? culture = null) => _content.IsDraft(culture); - - /// - public virtual bool IsPublished(string? culture = null) => _content.IsPublished(culture); - - #endregion - - #region Tree - - /// - public virtual IPublishedContent? Parent => _content.Parent; - - /// - public virtual IEnumerable? Children => _content.Children; - - /// - public virtual IEnumerable? ChildrenForAllCultures => _content.ChildrenForAllCultures; - - #endregion - - #region Properties - - /// - public virtual IEnumerable Properties => _content.Properties; - - /// - public virtual IPublishedProperty? GetProperty(string alias) - { - return _content.GetProperty(alias); - } - - #endregion + _content = content; + _publishedValueFallback = publishedValueFallback; } + + /// + public virtual IPublishedContentType ContentType => _content.ContentType; + + /// + public Guid Key => _content.Key; + + #region PublishedContent + + /// + public virtual int Id => _content.Id; + + #endregion + + /// + /// Gets the wrapped content. + /// + /// The wrapped content, that was passed as an argument to the constructor. + public IPublishedContent Unwrap() => _content; + + /// + public virtual string? Name => _content.Name; + + /// + public virtual string? UrlSegment => _content.UrlSegment; + + /// + public virtual int SortOrder => _content.SortOrder; + + /// + public virtual int Level => _content.Level; + + /// + public virtual string Path => _content.Path; + + /// + public virtual int? TemplateId => _content.TemplateId; + + /// + public virtual int CreatorId => _content.CreatorId; + + /// + public virtual DateTime CreateDate => _content.CreateDate; + + /// + public virtual int WriterId => _content.WriterId; + + /// + public virtual DateTime UpdateDate => _content.UpdateDate; + + /// + public IReadOnlyDictionary Cultures => _content.Cultures; + + /// + public virtual PublishedItemType ItemType => _content.ItemType; + + /// + public virtual IPublishedContent? Parent => _content.Parent; + + /// + public virtual bool IsDraft(string? culture = null) => _content.IsDraft(culture); + + /// + public virtual bool IsPublished(string? culture = null) => _content.IsPublished(culture); + + /// + public virtual IEnumerable? Children => _content.Children; + + /// + public virtual IEnumerable? ChildrenForAllCultures => _content.ChildrenForAllCultures; + + /// + public virtual IEnumerable Properties => _content.Properties; + + /// + public virtual IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs index 9525a9d7ac..1101301f36 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs @@ -1,51 +1,60 @@ -using System; using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Contains culture specific values for . +/// +[DebuggerDisplay("{Culture}")] +public class PublishedCultureInfo { /// - /// Contains culture specific values for . + /// Initializes a new instance of the class. /// - [DebuggerDisplay("{Culture}")] - public class PublishedCultureInfo + public PublishedCultureInfo(string culture, string? name, string? urlSegment, DateTime date) { - /// - /// Initializes a new instance of the class. - /// - public PublishedCultureInfo(string culture, string? name, string? urlSegment, DateTime date) + if (name == null) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - Culture = culture ?? throw new ArgumentNullException(nameof(culture)); - Name = name; - UrlSegment = urlSegment; - Date = date; + throw new ArgumentNullException(nameof(name)); } - /// - /// Gets the culture. - /// - public string Culture { get; } + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } - /// - /// Gets the name of the item. - /// - public string Name { get; } - - /// - /// Gets the URL segment of the item. - /// - public string? UrlSegment { get; } - - /// - /// Gets the date associated with the culture. - /// - /// - /// For published culture, this is the date the culture was published. For draft - /// cultures, this is the date the culture was made available, ie the last time its - /// name changed. - /// - public DateTime Date { get; } + Culture = culture ?? throw new ArgumentNullException(nameof(culture)); + Name = name; + UrlSegment = urlSegment; + Date = date; } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the name of the item. + /// + public string Name { get; } + + /// + /// Gets the URL segment of the item. + /// + public string? UrlSegment { get; } + + /// + /// Gets the date associated with the culture. + /// + /// + /// + /// For published culture, this is the date the culture was published. For draft + /// cultures, this is the date the culture was made available, ie the last time its + /// name changed. + /// + /// + public DateTime Date { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs index de590c2531..8f77f404ae 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs @@ -1,64 +1,65 @@ -using System; using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a published data type. +/// +/// +/// +/// Instances of the class are immutable, ie +/// if the data type changes, then a new class needs to be created. +/// +/// These instances should be created by an . +/// +[DebuggerDisplay("{EditorAlias}")] +public class PublishedDataType { + private readonly Lazy _lazyConfiguration; + /// - /// Represents a published data type. + /// Initializes a new instance of the class. /// - /// - /// Instances of the class are immutable, ie - /// if the data type changes, then a new class needs to be created. - /// These instances should be created by an . - /// - [DebuggerDisplay("{EditorAlias}")] - public class PublishedDataType + public PublishedDataType(int id, string editorAlias, Lazy lazyConfiguration) { - private readonly Lazy _lazyConfiguration; + _lazyConfiguration = lazyConfiguration; - /// - /// Initializes a new instance of the class. - /// - public PublishedDataType(int id, string editorAlias, Lazy lazyConfiguration) + Id = id; + EditorAlias = editorAlias; + } + + /// + /// Gets the datatype identifier. + /// + public int Id { get; } + + /// + /// Gets the data type editor alias. + /// + public string EditorAlias { get; } + + /// + /// Gets the data type configuration. + /// + public object? Configuration => _lazyConfiguration?.Value; + + /// + /// Gets the configuration object. + /// + /// The expected type of the configuration object. + /// When the datatype configuration is not of the expected type. + public T? ConfigurationAs() + where T : class + { + switch (Configuration) { - _lazyConfiguration = lazyConfiguration; - - Id = id; - EditorAlias = editorAlias; + case null: + return null; + case T configurationAsT: + return configurationAsT; } - /// - /// Gets the datatype identifier. - /// - public int Id { get; } - - /// - /// Gets the data type editor alias. - /// - public string EditorAlias { get; } - - /// - /// Gets the data type configuration. - /// - public object? Configuration => _lazyConfiguration?.Value; - - /// - /// Gets the configuration object. - /// - /// The expected type of the configuration object. - /// When the datatype configuration is not of the expected type. - public T? ConfigurationAs() - where T : class - { - switch (Configuration) - { - case null: - return null; - case T configurationAsT: - return configurationAsT; - } - - throw new InvalidCastException($"Cannot cast dataType configuration, of type {Configuration.GetType().Name}, to {typeof(T).Name}."); - } + throw new InvalidCastException( + $"Cannot cast dataType configuration, of type {Configuration.GetType().Name}, to {typeof(T).Name}."); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs index f093e7b20c..b91171012c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs @@ -1,22 +1,24 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// +/// Represents a strongly-typed published element. +/// +/// +/// Every strongly-typed property set class should inherit from PublishedElementModel +/// (or inherit from a class that inherits from... etc.) so they are picked by the factory. +/// +public abstract class PublishedElementModel : PublishedElementWrapped { /// /// - /// Represents a strongly-typed published element. + /// Initializes a new instance of the class with + /// an original instance. /// - /// Every strongly-typed property set class should inherit from PublishedElementModel - /// (or inherit from a class that inherits from... etc.) so they are picked by the factory. - public abstract class PublishedElementModel : PublishedElementWrapped + /// The original content. + /// The published value fallback. + protected PublishedElementModel(IPublishedElement content, IPublishedValueFallback publishedValueFallback) + : base(content, publishedValueFallback) { - /// - /// - /// Initializes a new instance of the class with - /// an original instance. - /// - /// The original content. - /// The published value fallback. - protected PublishedElementModel(IPublishedElement content, IPublishedValueFallback publishedValueFallback) - : base(content, publishedValueFallback) - { } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs index cc0c6b963a..d56230cbfa 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs @@ -1,45 +1,41 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// Provides an abstract base class for IPublishedElement implementations that +/// wrap and extend another IPublishedElement. +/// +public abstract class PublishedElementWrapped : IPublishedElement { + private readonly IPublishedElement _content; + private readonly IPublishedValueFallback _publishedValueFallback; + /// - /// Provides an abstract base class for IPublishedElement implementations that - /// wrap and extend another IPublishedElement. + /// Initializes a new instance of the class + /// with an IPublishedElement instance to wrap. /// - public abstract class PublishedElementWrapped : IPublishedElement + /// The content to wrap. + /// The published value fallback. + protected PublishedElementWrapped(IPublishedElement content, IPublishedValueFallback publishedValueFallback) { - private readonly IPublishedElement _content; - private readonly IPublishedValueFallback _publishedValueFallback; - - /// - /// Initializes a new instance of the class - /// with an IPublishedElement instance to wrap. - /// - /// The content to wrap. - /// The published value fallback. - protected PublishedElementWrapped(IPublishedElement content, IPublishedValueFallback publishedValueFallback) - { - _content = content; - _publishedValueFallback = publishedValueFallback; - } - - /// - /// Gets the wrapped content. - /// - /// The wrapped content, that was passed as an argument to the constructor. - public IPublishedElement Unwrap() => _content; - - /// - public IPublishedContentType ContentType => _content.ContentType; - - /// - public Guid Key => _content.Key; - - /// - public IEnumerable Properties => _content.Properties; - - /// - public IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias); + _content = content; + _publishedValueFallback = publishedValueFallback; } + + /// + public IPublishedContentType ContentType => _content.ContentType; + + /// + public Guid Key => _content.Key; + + /// + public IEnumerable Properties => _content.Properties; + + /// + public IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias); + + /// + /// Gets the wrapped content. + /// + /// The wrapped content, that was passed as an argument to the constructor. + public IPublishedElement Unwrap() => _content; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs index 7d16152b6e..2204cc5107 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs @@ -1,34 +1,33 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// The type of published element. +/// +/// Can be a simple element, or a document, a media, a member. +public enum PublishedItemType { /// - /// The type of published element. + /// Unknown. /// - /// Can be a simple element, or a document, a media, a member. - public enum PublishedItemType - { - /// - /// Unknown. - /// - Unknown = 0, + Unknown = 0, - /// - /// An element. - /// - Element, + /// + /// An element. + /// + Element, - /// - /// A document. - /// - Content, + /// + /// A document. + /// + Content, - /// - /// A media. - /// - Media, + /// + /// A media. + /// + Media, - /// - /// A member. - /// - Member - } + /// + /// A member. + /// + Member, } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs index 035c8a213a..5048f61908 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs @@ -1,32 +1,40 @@ -using System; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// +/// Indicates that the class is a published content model for a specified content type. +/// +/// +/// By default, the name of the class is assumed to be the content type alias. The +/// PublishedContentModelAttribute can be used to indicate a different alias. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class PublishedModelAttribute : Attribute { /// /// - /// Indicates that the class is a published content model for a specified content type. + /// Initializes a new instance of the class with a content type alias. /// - /// By default, the name of the class is assumed to be the content type alias. The - /// PublishedContentModelAttribute can be used to indicate a different alias. - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class PublishedModelAttribute : Attribute + /// The content type alias. + public PublishedModelAttribute(string contentTypeAlias) { - /// - /// - /// Initializes a new instance of the class with a content type alias. - /// - /// The content type alias. - public PublishedModelAttribute(string contentTypeAlias) + if (contentTypeAlias == null) { - if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias)); - - ContentTypeAlias = contentTypeAlias; + throw new ArgumentNullException(nameof(contentTypeAlias)); } - /// - /// Gets or sets the content type alias. - /// - public string ContentTypeAlias { get; } + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(contentTypeAlias)); + } + + ContentTypeAlias = contentTypeAlias; } + + /// + /// Gets or sets the content type alias. + /// + public string ContentTypeAlias { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs index 7053a238e6..b2d5da7876 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs @@ -1,156 +1,164 @@ using System.Collections; using System.Reflection; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Implements a strongly typed content model factory +/// +public class PublishedModelFactory : IPublishedModelFactory { + private readonly Dictionary? _modelInfos; + private readonly Dictionary _modelTypeMap; + private readonly IPublishedValueFallback _publishedValueFallback; + /// - /// Implements a strongly typed content model factory + /// Initializes a new instance of the class with types. /// - public class PublishedModelFactory : IPublishedModelFactory + /// The model types. + /// + /// + /// + /// Types must implement IPublishedContent and have a unique constructor that + /// accepts one IPublishedContent as a parameter. + /// + /// To activate, + /// + /// var types = TypeLoader.Current.GetTypes{PublishedContentModel}(); + /// var factory = new PublishedContentModelFactoryImpl(types); + /// PublishedContentModelFactoryResolver.Current.SetFactory(factory); + /// + /// + public PublishedModelFactory(IEnumerable types, IPublishedValueFallback publishedValueFallback) { - private readonly Dictionary? _modelInfos; - private readonly Dictionary _modelTypeMap; - private readonly IPublishedValueFallback _publishedValueFallback; + var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private class ModelInfo + foreach (Type type in types) { - public Type? ParameterType { get; set; } + // so... the model type has to implement a ctor with one parameter being, or inheriting from, + // IPublishedElement - but it can be IPublishedContent - so we cannot get one precise ctor, + // we have to iterate over all ctors and try to find the right one + ConstructorInfo? constructor = null; + Type? parameterType = null; - public Func? Ctor { get; set; } - - public Type? ModelType { get; set; } - - public Func? ListCtor { get; set; } - } - - /// - /// Initializes a new instance of the class with types. - /// - /// The model types. - /// - /// Types must implement IPublishedContent and have a unique constructor that - /// accepts one IPublishedContent as a parameter. - /// To activate, - /// - /// var types = TypeLoader.Current.GetTypes{PublishedContentModel}(); - /// var factory = new PublishedContentModelFactoryImpl(types); - /// PublishedContentModelFactoryResolver.Current.SetFactory(factory); - /// - /// - public PublishedModelFactory(IEnumerable types, IPublishedValueFallback publishedValueFallback) - { - var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - foreach (var type in types) + foreach (ConstructorInfo ctor in type.GetConstructors()) { - // so... the model type has to implement a ctor with one parameter being, or inheriting from, - // IPublishedElement - but it can be IPublishedContent - so we cannot get one precise ctor, - // we have to iterate over all ctors and try to find the right one - - ConstructorInfo? constructor = null; - Type? parameterType = null; - - foreach (var ctor in type.GetConstructors()) + ParameterInfo[] parms = ctor.GetParameters(); + if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && + typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) { - var parms = ctor.GetParameters(); - if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) + if (constructor != null) { - if (constructor != null) - { - throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPublishedElement."); - } - - constructor = ctor; - parameterType = parms[0].ParameterType; + throw new InvalidOperationException( + $"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPublishedElement."); } + + constructor = ctor; + parameterType = parms[0].ParameterType; } - - if (constructor == null) - { - throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPublishedElement."); - } - - var attribute = type.GetCustomAttribute(false); - var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; - - if (modelInfos.TryGetValue(typeName, out var modelInfo)) - { - throw new InvalidOperationException($"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType?.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); - } - - // have to use an unsafe ctor because we don't know the types, really - var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); - modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = modelCtor }; - modelTypeMap[typeName] = type; } - _modelInfos = modelInfos.Count > 0 ? modelInfos : null; - _modelTypeMap = modelTypeMap; - _publishedValueFallback = publishedValueFallback; + if (constructor == null) + { + throw new InvalidOperationException( + $"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPublishedElement."); + } + + PublishedModelAttribute? attribute = type.GetCustomAttribute(false); + var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; + + if (modelInfos.TryGetValue(typeName, out ModelInfo? modelInfo)) + { + throw new InvalidOperationException( + $"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType?.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); + } + + // have to use an unsafe ctor because we don't know the types, really + Func modelCtor = + ReflectionUtilities.EmitConstructorUnsafe>(constructor); + modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = modelCtor }; + modelTypeMap[typeName] = type; } - /// - public IPublishedElement CreateModel(IPublishedElement element) + _modelInfos = modelInfos.Count > 0 ? modelInfos : null; + _modelTypeMap = modelTypeMap; + _publishedValueFallback = publishedValueFallback; + } + + /// + public IPublishedElement CreateModel(IPublishedElement element) + { + // fail fast + if (_modelInfos is null || element.ContentType.Alias is null || + !_modelInfos.TryGetValue(element.ContentType.Alias, out ModelInfo? modelInfo)) { - // fail fast - if (_modelInfos is null || element.ContentType.Alias is null || !_modelInfos.TryGetValue(element.ContentType.Alias, out var modelInfo)) - { - return element; - } - - // ReSharper disable once UseMethodIsInstanceOfType - if (modelInfo.ParameterType?.IsAssignableFrom(element.GetType()) == false) - { - throw new InvalidOperationException($"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); - } - - // can cast, because we checked when creating the ctor - return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); + return element; } - /// - public IList? CreateModelList(string? alias) + // ReSharper disable once UseMethodIsInstanceOfType + if (modelInfo.ParameterType?.IsAssignableFrom(element.GetType()) == false) { - // fail fast - if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out var modelInfo) || modelInfo.ModelType is null) - { - return new List(); - } - - var ctor = modelInfo.ListCtor; - if (ctor != null) - { - return ctor(); - } - - var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); - if (ctor is not null) - { - return ctor(); - } - - return null; + throw new InvalidOperationException( + $"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); } - /// - public Type GetModelType(string? alias) + // can cast, because we checked when creating the ctor + return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); + } + + /// + public IList? CreateModelList(string? alias) + { + // fail fast + if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out ModelInfo? modelInfo) || + modelInfo.ModelType is null) { - // fail fast - if (_modelInfos is null || - alias is null || - !_modelInfos.TryGetValue(alias, out var modelInfo) || - modelInfo.ModelType is null) - { - return typeof(IPublishedElement); - } - - return modelInfo.ModelType; + return new List(); } - /// - public Type MapModelType(Type type) - => ModelType.Map(type, _modelTypeMap); + Func? ctor = modelInfo.ListCtor; + if (ctor != null) + { + return ctor(); + } + + Type listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); + if (ctor is not null) + { + return ctor(); + } + + return null; + } + + /// + public Type GetModelType(string? alias) + { + // fail fast + if (_modelInfos is null || + alias is null || + !_modelInfos.TryGetValue(alias, out ModelInfo? modelInfo) || modelInfo.ModelType is null) + { + return typeof(IPublishedElement); + } + + return modelInfo.ModelType; + } + + /// + public Type MapModelType(Type type) + => ModelType.Map(type, _modelTypeMap); + + private class ModelInfo + { + public Type? ParameterType { get; set; } + + public Func? Ctor { get; set; } + + public Type? ModelType { get; set; } + + public Func? ListCtor { get; set; } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs index 6cdbd85c74..25cf64899b 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs @@ -1,69 +1,71 @@ -using System; using System.Diagnostics; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a base class for IPublishedProperty implementations which converts and caches +/// the value source to the actual value to use when rendering content. +/// +[DebuggerDisplay("{Alias} ({PropertyType?.EditorAlias})")] +public abstract class PublishedPropertyBase : IPublishedProperty { /// - /// Provides a base class for IPublishedProperty implementations which converts and caches - /// the value source to the actual value to use when rendering content. + /// Initializes a new instance of the class. /// - [DebuggerDisplay("{Alias} ({PropertyType?.EditorAlias})")] - public abstract class PublishedPropertyBase : IPublishedProperty + protected PublishedPropertyBase(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) { - /// - /// Initializes a new instance of the class. - /// - protected PublishedPropertyBase(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); + ReferenceCacheLevel = referenceCacheLevel; + + ValidateCacheLevel(ReferenceCacheLevel, true); + ValidateCacheLevel(PropertyType.CacheLevel, false); + } + + /// + /// Gets the property reference cache level. + /// + public PropertyCacheLevel ReferenceCacheLevel { get; } + + /// + /// Gets the property type. + /// + public IPublishedPropertyType PropertyType { get; } + + /// + public string Alias => PropertyType.Alias; + + /// + public abstract bool HasValue(string? culture = null, string? segment = null); + + /// + public abstract object? GetSourceValue(string? culture = null, string? segment = null); + + /// + public abstract object? GetValue(string? culture = null, string? segment = null); + + /// + public abstract object? GetXPathValue(string? culture = null, string? segment = null); + + // validates the cache level + private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel, bool validateUnknown) + { + switch (cacheLevel) { - PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - ReferenceCacheLevel = referenceCacheLevel; + case PropertyCacheLevel.Element: + case PropertyCacheLevel.Elements: + case PropertyCacheLevel.Snapshot: + case PropertyCacheLevel.None: + break; + case PropertyCacheLevel.Unknown: + if (!validateUnknown) + { + goto default; + } - ValidateCacheLevel(ReferenceCacheLevel, true); - ValidateCacheLevel(PropertyType.CacheLevel, false); + break; + default: + throw new Exception($"Invalid cache level \"{cacheLevel}\"."); } - - // validates the cache level - private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel, bool validateUnknown) - { - switch (cacheLevel) - { - case PropertyCacheLevel.Element: - case PropertyCacheLevel.Elements: - case PropertyCacheLevel.Snapshot: - case PropertyCacheLevel.None: - break; - case PropertyCacheLevel.Unknown: - if (!validateUnknown) goto default; - break; - default: - throw new Exception($"Invalid cache level \"{cacheLevel}\"."); - } - } - - /// - /// Gets the property type. - /// - public IPublishedPropertyType PropertyType { get; } - - /// - /// Gets the property reference cache level. - /// - public PropertyCacheLevel ReferenceCacheLevel { get; } - - /// - public string Alias => PropertyType.Alias; - - /// - public abstract bool HasValue(string? culture = null, string? segment = null); - - /// - public abstract object? GetSourceValue(string? culture = null, string? segment = null); - - /// - public abstract object? GetValue(string? culture = null, string? segment = null); - - /// - public abstract object? GetXPathValue(string? culture = null, string? segment = null); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 9420811f24..4bc4b02f68 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Xml.Linq; using System.Xml.XPath; using Umbraco.Cms.Core.PropertyEditors; @@ -99,10 +98,18 @@ namespace Umbraco.Cms.Core.Models.PublishedContent private void Initialize() { - if (_initialized) return; + if (_initialized) + { + return; + } + lock (_locker) { - if (_initialized) return; + if (_initialized) + { + return; + } + InitializeLocked(); _initialized = true; } @@ -113,10 +120,12 @@ namespace Umbraco.Cms.Core.Models.PublishedContent _converter = null; var isdefault = false; - foreach (var converter in _propertyValueConverters) + foreach (IPropertyValueConverter converter in _propertyValueConverters) { if (!converter.IsConverter(this)) + { continue; + } if (_converter == null) { @@ -142,11 +151,14 @@ namespace Umbraco.Cms.Core.Models.PublishedContent else { // no shadow - bad - throw new InvalidOperationException(string.Format("Type '{2}' cannot be an IPropertyValueConverter" - + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" - + " for that property, and only one converter can exist for a property.", - ContentType?.Alias, Alias, - converter.GetType().FullName, _converter.GetType().FullName)); + throw new InvalidOperationException(string.Format( + "Type '{2}' cannot be an IPropertyValueConverter" + + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" + + " for that property, and only one converter can exist for a property.", + ContentType?.Alias, + Alias, + converter.GetType().FullName, + _converter.GetType().FullName)); } } else @@ -165,11 +177,14 @@ namespace Umbraco.Cms.Core.Models.PublishedContent else { // previous was non-default, and got another non-default - bad - throw new InvalidOperationException(string.Format("Type '{2}' cannot be an IPropertyValueConverter" - + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" - + " for that property, and only one converter can exist for a property.", - ContentType?.Alias, Alias, - converter.GetType().FullName, _converter.GetType().FullName)); + throw new InvalidOperationException(string.Format( + "Type '{2}' cannot be an IPropertyValueConverter" + + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" + + " for that property, and only one converter can exist for a property.", + ContentType?.Alias, + Alias, + converter.GetType().FullName, + _converter.GetType().FullName)); } } } @@ -181,11 +196,16 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public bool? IsValue(object? value, PropertyValueLevel level) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // if we have a converter, use the converter if (_converter != null) + { return _converter.IsValue(value, level); + } // otherwise use the old magic null & string comparisons return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); @@ -196,7 +216,11 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _cacheLevel; } } @@ -204,7 +228,10 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // use the converter if any, else just return the source value return _converter != null @@ -215,7 +242,10 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public object? ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // use the converter if any, else just return the inter value return _converter != null @@ -226,16 +256,28 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public object? ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // use the converter if any if (_converter != null) + { return _converter.ConvertIntermediateToXPath(owner, this, referenceCacheLevel, inter, preview); + } // else just return the inter value as a string or an XPathNavigator - if (inter == null) return null; + if (inter == null) + { + return null; + } + if (inter is XElement xElement) + { return xElement.CreateNavigator(); + } + return inter.ToString()?.Trim(); } @@ -244,7 +286,11 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _modelClrType!; } } @@ -254,7 +300,11 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _clrType ?? (_modelClrType is not null ? _clrType = _publishedModelFactory.MapModelType(_modelClrType) : null); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs index edc6cd9150..f0c2626f90 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs @@ -1,17 +1,17 @@ -using System.Diagnostics; +using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +[DebuggerDisplay("{Content?.Name} ({Score})")] +public class PublishedSearchResult { - [DebuggerDisplay("{Content?.Name} ({Score})")] - public class PublishedSearchResult + public PublishedSearchResult(IPublishedContent content, float score) { - public PublishedSearchResult(IPublishedContent content, float score) - { - Content = content; - Score = score; - } - - public IPublishedContent Content { get; } - public float Score { get; } + Content = content; + Score = score; } + + public IPublishedContent Content { get; } + + public float Score { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index ed8acf2736..64f0160383 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -1,296 +1,350 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a default implementation for . +/// +public class PublishedValueFallback : IPublishedValueFallback { + private readonly ILocalizationService? _localizationService; + private readonly IVariationContextAccessor _variationContextAccessor; + /// - /// Provides a default implementation for . + /// Initializes a new instance of the class. /// - public class PublishedValueFallback : IPublishedValueFallback + public PublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor) { - private readonly ILocalizationService? _localizationService; - private readonly IVariationContextAccessor _variationContextAccessor; + _localizationService = serviceContext.LocalizationService; + _variationContextAccessor = variationContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public PublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor) + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) => + TryGetValue(property, culture, segment, fallback, defaultValue, out value); + + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + _variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment); + + foreach (var f in fallback) { - _localizationService = serviceContext.LocalizationService; - _variationContextAccessor = variationContextAccessor; - } - - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) - { - return TryGetValue(property, culture, segment, fallback, defaultValue, out value); - } - - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) - { - _variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment); - - foreach (var f in fallback) + switch (f) { - switch (f) - { - case Fallback.None: - continue; - case Fallback.DefaultValue: - value = defaultValue; + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (TryGetValueWithLanguageFallback(property, culture, segment, out value)) + { return true; - case Fallback.Language: - if (TryGetValueWithLanguageFallback(property, culture, segment, out value)) - return true; - break; - default: - throw NotSupportedFallbackMethod(f, "property"); - } - } + } + break; + default: + throw NotSupportedFallbackMethod(f, "property"); + } + } + + value = default; + return false; + } + + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) => + TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); + + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + IPublishedPropertyType? propertyType = content.ContentType.GetPropertyType(alias); + if (propertyType == null) + { value = default; return false; } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); + + foreach (var f in fallback) { - return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); + switch (f) + { + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) + { + return true; + } + + break; + default: + throw NotSupportedFallbackMethod(f, "element"); + } } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + value = default; + return false; + } + + /// + public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) => + TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty); + + /// + public virtual bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value, out IPublishedProperty? noValueProperty) + { + noValueProperty = default; + + IPublishedPropertyType? propertyType = content.ContentType.GetPropertyType(alias); + if (propertyType != null) { - var propertyType = content.ContentType.GetPropertyType(alias); - if (propertyType == null) + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); + noValueProperty = content.GetProperty(alias); + } + + // note: we don't support "recurse & language" which would walk up the tree, + // looking at languages at each level - should someone need it... they'll have + // to implement it. + foreach (var f in fallback) + { + switch (f) + { + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (propertyType == null) + { + continue; + } + + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) + { + return true; + } + + break; + case Fallback.Ancestors: + if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty)) + { + return true; + } + + break; + default: + throw NotSupportedFallbackMethod(f, "content"); + } + } + + value = default; + return false; + } + + private NotSupportedException NotSupportedFallbackMethod(int fallback, string level) => + new NotSupportedException( + $"Fallback {GetType().Name} does not support fallback code '{fallback}' at {level} level."); + + // tries to get a value, recursing the tree + // because we recurse, content may not even have the a property with the specified alias (but only some ancestor) + // in case no value was found, noValueProperty contains the first property that was found (which does not have a value) + private bool TryGetValueWithAncestorsFallback(IPublishedContent? content, string alias, string? culture, string? segment, out T? value, ref IPublishedProperty? noValueProperty) + { + IPublishedProperty? property; // if we are here, content's property has no value + do + { + content = content?.Parent; + + IPublishedPropertyType? propertyType = content?.ContentType.GetPropertyType(alias); + + if (propertyType != null && content is not null) + { + culture = null; + segment = null; + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); + } + + property = content?.GetProperty(alias); + if (property != null && noValueProperty == null) + { + noValueProperty = property; + } + } + while (content != null && (property == null || property.HasValue(culture, segment) == false)); + + // if we found a content with the property having a value, return that property value + if (property != null && property.HasValue(culture, segment)) + { + value = property.Value(this, culture, segment); + return true; + } + + value = default; + return false; + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { + return false; + } + + var visited = new HashSet(); + + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) + { + return false; + } + + while (true) + { + if (language.FallbackLanguageId == null) { - value = default; return false; } - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); - - foreach (var f in fallback) + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) { - switch (f) - { - case Fallback.None: - continue; - case Fallback.DefaultValue: - value = defaultValue; - return true; - case Fallback.Language: - if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) - return true; - break; - default: - throw NotSupportedFallbackMethod(f, "element"); - } + return false; } - value = default; - return false; - } + visited.Add(language2Id); - /// - public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) - { - return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty); - } - - /// - public virtual bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value, out IPublishedProperty? noValueProperty) - { - noValueProperty = default; - - var propertyType = content.ContentType.GetPropertyType(alias); - if (propertyType != null) + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) { - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); - noValueProperty = content.GetProperty(alias); + return false; } - // note: we don't support "recurse & language" which would walk up the tree, - // looking at languages at each level - should someone need it... they'll have - // to implement it. + var culture2 = language2.IsoCode; - foreach (var f in fallback) + if (property.HasValue(culture2, segment)) { - switch (f) - { - case Fallback.None: - continue; - case Fallback.DefaultValue: - value = defaultValue; - return true; - case Fallback.Language: - if (propertyType == null) - continue; - if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) - return true; - break; - case Fallback.Ancestors: - if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty)) - return true; - break; - default: - throw NotSupportedFallbackMethod(f, "content"); - } - } - - value = default; - return false; - } - - private NotSupportedException NotSupportedFallbackMethod(int fallback, string level) - { - return new NotSupportedException($"Fallback {GetType().Name} does not support fallback code '{fallback}' at {level} level."); - } - - // tries to get a value, recursing the tree - // because we recurse, content may not even have the a property with the specified alias (but only some ancestor) - // in case no value was found, noValueProperty contains the first property that was found (which does not have a value) - private bool TryGetValueWithAncestorsFallback(IPublishedContent? content, string alias, string? culture, string? segment, out T? value, ref IPublishedProperty? noValueProperty) - { - IPublishedProperty? property; // if we are here, content's property has no value - do - { - content = content?.Parent; - - var propertyType = content?.ContentType.GetPropertyType(alias); - - if (propertyType != null && content is not null) - { - culture = null; - segment = null; - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); - } - - property = content?.GetProperty(alias); - if (property != null && noValueProperty == null) - { - noValueProperty = property; - } - } - while (content != null && (property == null || property.HasValue(culture, segment) == false)); - - // if we found a content with the property having a value, return that property value - if (property != null && property.HasValue(culture, segment)) - { - value = property.Value(this, culture, segment); + value = property.Value(this, culture2, segment); return true; } - value = default; + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { return false; } - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) + var visited = new HashSet(); + + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) { - value = default; - - if (culture.IsNullOrWhiteSpace()) return false; - - var visited = new HashSet(); - - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; - - while (true) - { - if (language.FallbackLanguageId == null) return false; - - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); - - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; - - if (property.HasValue(culture2, segment)) - { - value = property.Value(this, culture2, segment); - return true; - } - - language = language2; - } + return false; } - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string? culture, string? segment, out T? value) + while (true) { - value = default; - - if (culture.IsNullOrWhiteSpace()) return false; - - var visited = new HashSet(); - - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; - - while (true) + if (language.FallbackLanguageId == null) { - if (language.FallbackLanguageId == null) return false; - - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); - - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; - - if (content.HasValue(alias, culture2, segment)) - { - value = content.Value(this, alias, culture2, segment); - return true; - } - - language = language2; + return false; } + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) + { + return false; + } + + visited.Add(language2Id); + + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) + { + return false; + } + + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(this, alias, culture2, segment); + return true; + } + + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { + return false; } - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string? culture, string? segment, out T? value) + var visited = new HashSet(); + + // TODO: _localizationService.GetXxx() is expensive, it deep clones objects + // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) { - value = default; + return false; + } - if (culture.IsNullOrWhiteSpace()) return false; - - var visited = new HashSet(); - - // TODO: _localizationService.GetXxx() is expensive, it deep clones objects - // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone - - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; - - while (true) + while (true) + { + if (language.FallbackLanguageId == null) { - if (language.FallbackLanguageId == null) return false; - - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); - - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; - - if (content.HasValue(alias, culture2, segment)) - { - value = content.Value(this, alias, culture2, segment); - return true; - } - - language = language2; + return false; } + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) + { + return false; + } + + visited.Add(language2Id); + + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) + { + return false; + } + + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(this, alias, culture2, segment); + return true; + } + + language = language2; } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index 2ae0ce6c1d..763006f8f1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -1,54 +1,60 @@ -using System; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// +/// Represents a published property that has a unique invariant-neutral value +/// and caches conversion results locally. +/// +/// +/// +/// Conversions results are stored within the property and will not +/// be refreshed, so this class is not suitable for cached properties. +/// +/// +/// Does not support variations: the ctor throws if the property type +/// supports variations. +/// +/// +public class RawValueProperty : PublishedPropertyBase { - /// - /// - /// Represents a published property that has a unique invariant-neutral value - /// and caches conversion results locally. - /// - /// - /// Conversions results are stored within the property and will not - /// be refreshed, so this class is not suitable for cached properties. - /// Does not support variations: the ctor throws if the property type - /// supports variations. - /// - public class RawValueProperty : PublishedPropertyBase + private readonly Lazy _objectValue; + private readonly object _sourceValue; // the value in the db + private readonly Lazy _xpathValue; + + public RawValueProperty(IPublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) + : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { - private readonly object _sourceValue; //the value in the db - private readonly Lazy _objectValue; - private readonly Lazy _xpathValue; - - // RawValueProperty does not (yet?) support variants, - // only manages the current "default" value - - public override object? GetSourceValue(string? culture = null, string? segment = null) - => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _sourceValue : null; - - public override bool HasValue(string? culture = null, string? segment = null) + if (propertyType.Variations != ContentVariation.Nothing) { - var sourceValue = GetSourceValue(culture, segment); - return sourceValue is string s ? !string.IsNullOrWhiteSpace(s) : sourceValue != null; + throw new ArgumentException("Property types with variations are not supported here.", nameof(propertyType)); } - public override object? GetValue(string? culture = null, string? segment = null) - => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _objectValue.Value : null; + _sourceValue = sourceValue; - public override object? GetXPathValue(string? culture = null, string? segment = null) - => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; - - public RawValueProperty(IPublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) - : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored - { - if (propertyType.Variations != ContentVariation.Nothing) - throw new ArgumentException("Property types with variations are not supported here.", nameof(propertyType)); - - _sourceValue = sourceValue; - - var interValue = new Lazy(() => PropertyType.ConvertSourceToInter(content, _sourceValue, isPreviewing)); - _objectValue = new Lazy(() => PropertyType.ConvertInterToObject(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); - _xpathValue = new Lazy(() => PropertyType.ConvertInterToXPath(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); - } + var interValue = + new Lazy(() => PropertyType.ConvertSourceToInter(content, _sourceValue, isPreviewing)); + _objectValue = new Lazy(() => + PropertyType.ConvertInterToObject(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); + _xpathValue = new Lazy(() => + PropertyType.ConvertInterToXPath(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); } + + // RawValueProperty does not (yet?) support variants, + // only manages the current "default" value + public override object? GetSourceValue(string? culture = null, string? segment = null) + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _sourceValue : null; + + public override bool HasValue(string? culture = null, string? segment = null) + { + var sourceValue = GetSourceValue(culture, segment); + return sourceValue is string s ? !string.IsNullOrWhiteSpace(s) : sourceValue != null; + } + + public override object? GetValue(string? culture = null, string? segment = null) + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _objectValue.Value : null; + + public override object? GetXPathValue(string? culture = null, string? segment = null) + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; } diff --git a/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs index a9d06e521f..5919370792 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs @@ -1,23 +1,20 @@ -using System; using System.Collections.Concurrent; -using System.Threading; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a CurrentUICulture-based implementation of . +/// +/// +/// This accessor does not support segments. There is no need to set the current context. +/// +public class ThreadCultureVariationContextAccessor : IVariationContextAccessor { - /// - /// Provides a CurrentUICulture-based implementation of . - /// - /// - /// This accessor does not support segments. There is no need to set the current context. - /// - public class ThreadCultureVariationContextAccessor : IVariationContextAccessor - { - private readonly ConcurrentDictionary _contexts = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _contexts = new(); - public VariationContext? VariationContext - { - get => _contexts.GetOrAdd(Thread.CurrentThread.CurrentUICulture.Name, culture => new VariationContext(culture)); - set => throw new NotSupportedException(); - } + public VariationContext? VariationContext + { + get => _contexts.GetOrAdd(Thread.CurrentThread.CurrentUICulture.Name, culture => new VariationContext(culture)); + set => throw new NotSupportedException(); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs index 8e24f25332..ff13964fb3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs +++ b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Specifies the type of URLs that the URL provider should produce, Auto is the default. +/// +public enum UrlMode { /// - /// Specifies the type of URLs that the URL provider should produce, Auto is the default. + /// Indicates that the URL provider should do what it has been configured to do. /// - public enum UrlMode - { - /// - /// Indicates that the URL provider should do what it has been configured to do. - /// - Default = 0, + Default = 0, - /// - /// Indicates that the URL provider should produce relative URLs exclusively. - /// - Relative, + /// + /// Indicates that the URL provider should produce relative URLs exclusively. + /// + Relative, - /// - /// Indicates that the URL provider should produce absolute URLs exclusively. - /// - Absolute, + /// + /// Indicates that the URL provider should produce absolute URLs exclusively. + /// + Absolute, - /// - /// Indicates that the URL provider should determine automatically whether to return relative or absolute URLs. - /// - Auto - } + /// + /// Indicates that the URL provider should determine automatically whether to return relative or absolute URLs. + /// + Auto, } diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs index 9b8ae30245..92326ae359 100644 --- a/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs @@ -1,34 +1,33 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents the variation context. +/// +public class VariationContext { /// - /// Represents the variation context. + /// Initializes a new instance of the class. /// - public class VariationContext + public VariationContext(string? culture = null, string? segment = null) { - /// - /// Initializes a new instance of the class. - /// - public VariationContext(string? culture = null, string? segment = null) - { - Culture = culture ?? ""; // cannot be null, default to invariant - Segment = segment ?? ""; // cannot be null, default to neutral - } - - /// - /// Gets the culture. - /// - public string Culture { get; } - - /// - /// Gets the segment. - /// - public string Segment { get; } - - /// - /// Gets the segment for the content item - /// - /// - /// - public virtual string GetSegment(int contentId) => Segment; + Culture = culture ?? string.Empty; // cannot be null, default to invariant + Segment = segment ?? string.Empty; // cannot be null, default to neutral } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the segment. + /// + public string Segment { get; } + + /// + /// Gets the segment for the content item + /// + /// + /// + public virtual string GetSegment(int contentId) => Segment; } diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs index 4a986597bd..e8f6e3bdc1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs @@ -1,42 +1,58 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class VariationContextAccessorExtensions { - public static class VariationContextAccessorExtensions + public static void ContextualizeVariation( + this IVariationContextAccessor variationContextAccessor, + ContentVariation variations, + ref string? culture, + ref string? segment) + => variationContextAccessor.ContextualizeVariation(variations, null, ref culture, ref segment); + + public static void ContextualizeVariation( + this IVariationContextAccessor variationContextAccessor, + ContentVariation variations, + int contentId, + ref string? culture, + ref string? segment) + => variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, ref culture, ref segment); + + private static void ContextualizeVariation( + this IVariationContextAccessor variationContextAccessor, + ContentVariation variations, + int? contentId, + ref string? culture, + ref string? segment) { - public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, ref string? culture, ref string? segment) - => variationContextAccessor.ContextualizeVariation(variations, null, ref culture, ref segment); - - public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, int contentId, ref string? culture, ref string? segment) - => variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, ref culture, ref segment); - - private static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, int? contentId, ref string? culture, ref string? segment) + if (culture != null && segment != null) { - if (culture != null && segment != null) return; + return; + } - // use context values - var publishedVariationContext = variationContextAccessor?.VariationContext; - if (culture == null) + // use context values + VariationContext? publishedVariationContext = variationContextAccessor?.VariationContext; + if (culture == null) + { + culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : string.Empty; + } + + if (segment == null) + { + if (variations.VariesBySegment()) { - culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; + segment = contentId == null + ? publishedVariationContext?.Segment + : publishedVariationContext?.GetSegment(contentId.Value); } - - if (segment == null) + else { - if (variations.VariesBySegment()) - { - segment = contentId == null - ? publishedVariationContext?.Segment - : publishedVariationContext?.GetSegment(contentId.Value); - } - else - { - segment = ""; - } + segment = string.Empty; } } } diff --git a/src/Umbraco.Core/Models/PublishedState.cs b/src/Umbraco.Core/Models/PublishedState.cs index 87c106e11e..39d68ea273 100644 --- a/src/Umbraco.Core/Models/PublishedState.cs +++ b/src/Umbraco.Core/Models/PublishedState.cs @@ -1,60 +1,71 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The states of a content item. +/// +public enum PublishedState { + // versions management in repo: + // + // - published = the content is published + // repo: saving draft values + // update current version (draft) values + // + // - unpublished = the content is not published + // repo: saving draft values + // update current version (draft) values + // + // - publishing = the content is being published (transitory) + // if currently published: + // delete all draft values from current version, not current anymore + // create new version with published+draft values + // + // - unpublishing = the content is being unpublished (transitory) + // if currently published (just in case): + // delete all draft values from current version, not current anymore + // create new version with published+draft values (should be managed by service) + + // when a content item is loaded, its state is one of those two: /// - /// The states of a content item. + /// The content item is published. /// - public enum PublishedState - { - // versions management in repo: - // - // - published = the content is published - // repo: saving draft values - // update current version (draft) values - // - // - unpublished = the content is not published - // repo: saving draft values - // update current version (draft) values - // - // - publishing = the content is being published (transitory) - // if currently published: - // delete all draft values from current version, not current anymore - // create new version with published+draft values - // - // - unpublishing = the content is being unpublished (transitory) - // if currently published (just in case): - // delete all draft values from current version, not current anymore - // create new version with published+draft values (should be managed by service) + Published, - // when a content item is loaded, its state is one of those two: + // also: handled over to repo to save draft values for a published content - /// - /// The content item is published. - /// - Published, - // also: handled over to repo to save draft values for a published content + /// + /// The content item is not published. + /// + Unpublished, - /// - /// The content item is not published. - /// - Unpublished, - // also: handled over to repo to save draft values for an unpublished content + // also: handled over to repo to save draft values for an unpublished content - // when it is handled over to the repository, its state can also be one of those: + // when it is handled over to the repository, its state can also be one of those: - /// - /// The version is being saved, in order to publish the content. - /// - /// The Publishing state is transitional. Once the version - /// is saved, its state changes to Published. - Publishing, + /// + /// The version is being saved, in order to publish the content. + /// + /// + /// The + /// Publishing + /// state is transitional. Once the version + /// is saved, its state changes to + /// Published + /// . + /// + Publishing, - /// - /// The version is being saved, in order to unpublish the content. - /// - /// The Unpublishing state is transitional. Once the version - /// is saved, its state changes to Unpublished. - Unpublishing - - } + /// + /// The version is being saved, in order to unpublish the content. + /// + /// + /// The + /// Unpublishing + /// state is transitional. Once the version + /// is saved, its state changes to + /// Unpublished + /// . + /// + Unpublishing, } diff --git a/src/Umbraco.Core/Models/Range.cs b/src/Umbraco.Core/Models/Range.cs index 9c5da2087e..78d49ad851 100644 --- a/src/Umbraco.Core/Models/Range.cs +++ b/src/Umbraco.Core/Models/Range.cs @@ -1,130 +1,144 @@ -using System; using System.Globalization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a range with a minimum and maximum value. +/// +/// The type of the minimum and maximum values. +/// +public class Range : IEquatable> + where T : IComparable { /// - /// Represents a range with a minimum and maximum value. + /// Gets or sets the minimum value. /// - /// The type of the minimum and maximum values. - /// - public class Range : IEquatable> - where T : IComparable - { - /// - /// Gets or sets the minimum value. - /// - /// - /// The minimum value. - /// - public T? Minimum { get; set; } + /// + /// The minimum value. + /// + public T? Minimum { get; set; } - /// - /// Gets or sets the maximum value. - /// - /// - /// The maximum value. - /// - public T? Maximum { get; set; } + /// + /// Gets or sets the maximum value. + /// + /// + /// The maximum value. + /// + public T? Maximum { get; set; } - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() => this.ToString("{0},{1}", CultureInfo.InvariantCulture); + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// + /// if the current object is equal to the parameter; otherwise, + /// . + /// + public bool Equals(Range? other) => other != null && Equals(other.Minimum, other.Maximum); - /// - /// Returns a that represents this instance. - /// - /// A composite format string for a single value (minimum and maximum are equal). Use {0} for the minimum and {1} for the maximum value. - /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum value. - /// An object that supplies culture-specific formatting information. - /// - /// A that represents this instance. - /// - public string ToString(string format, string formatRange, IFormatProvider? provider = null) => this.ToString(this.Minimum?.CompareTo(this.Maximum) == 0 ? format : formatRange, provider); + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => ToString("{0},{1}", CultureInfo.InvariantCulture); - /// - /// Returns a that represents this instance. - /// - /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum value. - /// An object that supplies culture-specific formatting information. - /// - /// A that represents this instance. - /// - public string ToString(string format, IFormatProvider? provider = null) => string.Format(provider, format, this.Minimum, this.Maximum); + /// + /// Returns a that represents this instance. + /// + /// + /// A composite format string for a single value (minimum and maximum are equal). Use {0} for the + /// minimum and {1} for the maximum value. + /// + /// + /// A composite format string for the range values. Use {0} for the minimum and {1} for the + /// maximum value. + /// + /// An object that supplies culture-specific formatting information. + /// + /// A that represents this instance. + /// + public string ToString(string format, string formatRange, IFormatProvider? provider = null) => + ToString(Minimum?.CompareTo(Maximum) == 0 ? format : formatRange, provider); - /// - /// Determines whether this range is valid (the minimum value is lower than or equal to the maximum value). - /// - /// - /// true if this range is valid; otherwise, false. - /// - public bool IsValid() => this.Minimum?.CompareTo(this.Maximum) <= 0; + /// + /// Returns a that represents this instance. + /// + /// + /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum + /// value. + /// + /// An object that supplies culture-specific formatting information. + /// + /// A that represents this instance. + /// + public string ToString(string format, IFormatProvider? provider = null) => + string.Format(provider, format, Minimum, Maximum); - /// - /// Determines whether this range contains the specified value. - /// - /// The value. - /// - /// true if this range contains the specified value; otherwise, false. - /// - public bool ContainsValue(T? value) => this.Minimum?.CompareTo(value) <= 0 && value?.CompareTo(this.Maximum) <= 0; + /// + /// Determines whether this range is valid (the minimum value is lower than or equal to the maximum value). + /// + /// + /// true if this range is valid; otherwise, false. + /// + public bool IsValid() => Minimum?.CompareTo(Maximum) <= 0; - /// - /// Determines whether this range is inside the specified range. - /// - /// The range. - /// - /// true if this range is inside the specified range; otherwise, false. - /// - public bool IsInsideRange(Range range) => this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum); + /// + /// Determines whether this range contains the specified value. + /// + /// The value. + /// + /// true if this range contains the specified value; otherwise, false. + /// + public bool ContainsValue(T? value) => Minimum?.CompareTo(value) <= 0 && value?.CompareTo(Maximum) <= 0; - /// - /// Determines whether this range contains the specified range. - /// - /// The range. - /// - /// true if this range contains the specified range; otherwise, false. - /// - public bool ContainsRange(Range range) => this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum); + /// + /// Determines whether this range is inside the specified range. + /// + /// The range. + /// + /// true if this range is inside the specified range; otherwise, false. + /// + public bool IsInsideRange(Range range) => + IsValid() && range.IsValid() && range.ContainsValue(Minimum) && range.ContainsValue(Maximum); - /// - /// Determines whether the specified , is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object? obj) => obj is Range other && this.Equals(other); + /// + /// Determines whether this range contains the specified range. + /// + /// The range. + /// + /// true if this range contains the specified range; otherwise, false. + /// + public bool ContainsRange(Range range) => + IsValid() && range.IsValid() && ContainsValue(range.Minimum) && ContainsValue(range.Maximum); - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// - /// if the current object is equal to the parameter; otherwise, . - /// - public bool Equals(Range? other) => other != null && this.Equals(other.Minimum, other.Maximum); + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object? obj) => obj is Range other && Equals(other); - /// - /// Determines whether the specified and values are equal to this instance values. - /// - /// The minimum value. - /// The maximum value. - /// - /// true if the specified and values are equal to this instance values; otherwise, false. - /// - public bool Equals(T? minimum, T? maximum) => this.Minimum?.CompareTo(minimum) == 0 && this.Maximum?.CompareTo(maximum) == 0; + /// + /// Determines whether the specified and values are equal to + /// this instance values. + /// + /// The minimum value. + /// The maximum value. + /// + /// true if the specified and values are equal to this + /// instance values; otherwise, false. + /// + public bool Equals(T? minimum, T? maximum) => Minimum?.CompareTo(minimum) == 0 && Maximum?.CompareTo(maximum) == 0; - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() => (this.Minimum, this.Maximum).GetHashCode(); - } + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() => (Minimum, Maximum).GetHashCode(); } diff --git a/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs index cbb1e51a3e..77b6253178 100644 --- a/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs +++ b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs @@ -1,42 +1,37 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public struct ReadOnlyContentBaseAdapter : IReadOnlyContentBase { - public struct ReadOnlyContentBaseAdapter : IReadOnlyContentBase - { - private readonly IContentBase _content; + private readonly IContentBase _content; - private ReadOnlyContentBaseAdapter(IContentBase content) - { - _content = content ?? throw new ArgumentNullException(nameof(content)); - } + private ReadOnlyContentBaseAdapter(IContentBase content) => + _content = content ?? throw new ArgumentNullException(nameof(content)); - public static ReadOnlyContentBaseAdapter Create(IContentBase content) => new ReadOnlyContentBaseAdapter(content); + public int Id => _content.Id; - public int Id => _content.Id; + public static ReadOnlyContentBaseAdapter Create(IContentBase content) => new(content); - public Guid Key => _content.Key; + public Guid Key => _content.Key; - public DateTime CreateDate => _content.CreateDate; + public DateTime CreateDate => _content.CreateDate; - public DateTime UpdateDate => _content.UpdateDate; + public DateTime UpdateDate => _content.UpdateDate; - public string? Name => _content.Name; + public string? Name => _content.Name; - public int CreatorId => _content.CreatorId; + public int CreatorId => _content.CreatorId; - public int ParentId => _content.ParentId; + public int ParentId => _content.ParentId; - public int Level => _content.Level; + public int Level => _content.Level; - public string? Path => _content.Path; + public string? Path => _content.Path; - public int SortOrder => _content.SortOrder; + public int SortOrder => _content.SortOrder; - public int ContentTypeId => _content.ContentTypeId; + public int ContentTypeId => _content.ContentTypeId; - public int WriterId => _content.WriterId; + public int WriterId => _content.WriterId; - public int VersionId => _content.VersionId; - } + public int VersionId => _content.VersionId; } diff --git a/src/Umbraco.Core/Models/ReadOnlyRelation.cs b/src/Umbraco.Core/Models/ReadOnlyRelation.cs index a57a5ba7e1..4388499e98 100644 --- a/src/Umbraco.Core/Models/ReadOnlyRelation.cs +++ b/src/Umbraco.Core/Models/ReadOnlyRelation.cs @@ -1,35 +1,37 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// A read only relation. Can be used to bulk save witch performs better than the normal save operation, +/// but do not populate Ids back to the model +/// +public class ReadOnlyRelation { - /// - /// A read only relation. Can be used to bulk save witch performs better than the normal save operation, - /// but do not populate Ids back to the model - /// - public class ReadOnlyRelation + public ReadOnlyRelation(int id, int parentId, int childId, int relationTypeId, DateTime createDate, string comment) { - public ReadOnlyRelation(int id, int parentId, int childId, int relationTypeId, DateTime createDate, string comment) - { - Id = id; - ParentId = parentId; - ChildId = childId; - RelationTypeId = relationTypeId; - CreateDate = createDate; - Comment = comment; - } - - public ReadOnlyRelation(int parentId, int childId, int relationTypeId): this(0, parentId, childId, relationTypeId, DateTime.Now, string.Empty) - { - - } - - public int Id { get; } - public int ParentId { get; } - public int ChildId { get; } - public int RelationTypeId { get; } - public DateTime CreateDate { get; } - public string Comment { get; } - - public bool HasIdentity => Id != 0; + Id = id; + ParentId = parentId; + ChildId = childId; + RelationTypeId = relationTypeId; + CreateDate = createDate; + Comment = comment; } + + public ReadOnlyRelation(int parentId, int childId, int relationTypeId) + : this(0, parentId, childId, relationTypeId, DateTime.Now, string.Empty) + { + } + + public int Id { get; } + + public int ParentId { get; } + + public int ChildId { get; } + + public int RelationTypeId { get; } + + public DateTime CreateDate { get; } + + public string Comment { get; } + + public bool HasIdentity => Id != 0; } diff --git a/src/Umbraco.Core/Models/RedirectUrl.cs b/src/Umbraco.Core/Models/RedirectUrl.cs index d4acc0b66d..ed0cde70bd 100644 --- a/src/Umbraco.Core/Models/RedirectUrl.cs +++ b/src/Umbraco.Core/Models/RedirectUrl.cs @@ -1,64 +1,62 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +[Serializable] +[DataContract(IsReference = true)] +public class RedirectUrl : EntityBase, IRedirectUrl { + private int _contentId; + private Guid _contentKey; + private DateTime _createDateUtc; + private string? _culture; + private string _url; + /// - /// Implements . + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class RedirectUrl : EntityBase, IRedirectUrl + public RedirectUrl() { - /// - /// Initializes a new instance of the class. - /// - public RedirectUrl() - { - CreateDateUtc = DateTime.UtcNow; - _url = string.Empty; - } + CreateDateUtc = DateTime.UtcNow; + _url = string.Empty; + } - private int _contentId; - private Guid _contentKey; - private DateTime _createDateUtc; - private string? _culture; - private string _url; + /// + public int ContentId + { + get => _contentId; + set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(ContentId)); + } - /// - public int ContentId - { - get => _contentId; - set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(ContentId)); - } + /// + public Guid ContentKey + { + get => _contentKey; + set => SetPropertyValueAndDetectChanges(value, ref _contentKey, nameof(ContentKey)); + } - /// - public Guid ContentKey - { - get => _contentKey; - set => SetPropertyValueAndDetectChanges(value, ref _contentKey, nameof(ContentKey)); - } + /// + public DateTime CreateDateUtc + { + get => _createDateUtc; + set => SetPropertyValueAndDetectChanges(value, ref _createDateUtc, nameof(CreateDateUtc)); + } - /// - public DateTime CreateDateUtc - { - get => _createDateUtc; - set => SetPropertyValueAndDetectChanges(value, ref _createDateUtc, nameof(CreateDateUtc)); - } + /// + public string? Culture + { + get => _culture; + set => SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); + } - /// - public string? Culture - { - get => _culture; - set => SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); - } - - /// - public string Url - { - get => _url; - set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); - } + /// + public string Url + { + get => _url; + set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); } } diff --git a/src/Umbraco.Core/Models/Relation.cs b/src/Umbraco.Core/Models/Relation.cs index 54227db910..c495ed39fb 100644 --- a/src/Umbraco.Core/Models/Relation.cs +++ b/src/Umbraco.Core/Models/Relation.cs @@ -1,103 +1,102 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Relation between two items +/// +[Serializable] +[DataContract(IsReference = true)] +public class Relation : EntityBase, IRelation { + private int _childId; + + private string? _comment; + + // NOTE: The datetime column from umbracoRelation is set on CreateDate on the Entity + private int _parentId; + private IRelationType _relationType; + /// - /// Represents a Relation between two items + /// Constructor for constructing the entity to be created /// - [Serializable] - [DataContract(IsReference = true)] - public class Relation : EntityBase, IRelation + /// + /// + /// + public Relation(int parentId, int childId, IRelationType relationType) { - //NOTE: The datetime column from umbracoRelation is set on CreateDate on the Entity - private int _parentId; - private int _childId; - private IRelationType _relationType; - private string? _comment; - - /// - /// Constructor for constructing the entity to be created - /// - /// - /// - /// - public Relation(int parentId, int childId, IRelationType relationType) - { - _parentId = parentId; - _childId = childId; - _relationType = relationType; - } - - /// - /// Constructor for reconstructing the entity from the data source - /// - /// - /// - /// - /// - /// - public Relation(int parentId, int childId, Guid parentObjectType, Guid childObjectType, IRelationType relationType) - { - _parentId = parentId; - _childId = childId; - _relationType = relationType; - ParentObjectType = parentObjectType; - ChildObjectType = childObjectType; - } - - - /// - /// Gets or sets the Parent Id of the Relation (Source) - /// - [DataMember] - public int ParentId - { - get => _parentId; - set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); - } - - [DataMember] - public Guid ParentObjectType { get; set; } - - /// - /// Gets or sets the Child Id of the Relation (Destination) - /// - [DataMember] - public int ChildId - { - get => _childId; - set => SetPropertyValueAndDetectChanges(value, ref _childId, nameof(ChildId)); - } - - [DataMember] - public Guid ChildObjectType { get; set; } - - /// - /// Gets or sets the for the Relation - /// - [DataMember] - public IRelationType RelationType - { - get => _relationType; - set => SetPropertyValueAndDetectChanges(value, ref _relationType!, nameof(RelationType)); - } - - /// - /// Gets or sets a comment for the Relation - /// - [DataMember] - public string? Comment - { - get => _comment; - set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); - } - - /// - /// Gets the Id of the that this Relation is based on. - /// - [IgnoreDataMember] - public int RelationTypeId => _relationType.Id; + _parentId = parentId; + _childId = childId; + _relationType = relationType; } + + /// + /// Constructor for reconstructing the entity from the data source + /// + /// + /// + /// + /// + /// + public Relation(int parentId, int childId, Guid parentObjectType, Guid childObjectType, IRelationType relationType) + { + _parentId = parentId; + _childId = childId; + _relationType = relationType; + ParentObjectType = parentObjectType; + ChildObjectType = childObjectType; + } + + /// + /// Gets or sets the Parent Id of the Relation (Source) + /// + [DataMember] + public int ParentId + { + get => _parentId; + set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + } + + [DataMember] + public Guid ParentObjectType { get; set; } + + /// + /// Gets or sets the Child Id of the Relation (Destination) + /// + [DataMember] + public int ChildId + { + get => _childId; + set => SetPropertyValueAndDetectChanges(value, ref _childId, nameof(ChildId)); + } + + [DataMember] + public Guid ChildObjectType { get; set; } + + /// + /// Gets or sets the for the Relation + /// + [DataMember] + public IRelationType RelationType + { + get => _relationType; + set => SetPropertyValueAndDetectChanges(value, ref _relationType!, nameof(RelationType)); + } + + /// + /// Gets or sets a comment for the Relation + /// + [DataMember] + public string? Comment + { + get => _comment; + set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); + } + + /// + /// Gets the Id of the that this Relation is based on. + /// + [IgnoreDataMember] + public int RelationTypeId => _relationType.Id; } diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs index 75344914f0..111d7e6dfa 100644 --- a/src/Umbraco.Core/Models/RelationItem.cs +++ b/src/Umbraco.Core/Models/RelationItem.cs @@ -1,44 +1,40 @@ -using System; -using System.Runtime.Serialization; -using Umbraco.Cms.Core.Models.Entities; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "relationItem", Namespace = "")] +public class RelationItem { - [DataContract(Name = "relationItem", Namespace = "")] - public class RelationItem - { - [DataMember(Name = "id")] - public int NodeId { get; set; } + [DataMember(Name = "id")] + public int NodeId { get; set; } - [DataMember(Name = "key")] - public Guid NodeKey { get; set; } + [DataMember(Name = "key")] + public Guid NodeKey { get; set; } - [DataMember(Name = "name")] - public string? NodeName { get; set; } + [DataMember(Name = "name")] + public string? NodeName { get; set; } - [DataMember(Name = "type")] - public string? NodeType { get; set; } + [DataMember(Name = "type")] + public string? NodeType { get; set; } - [DataMember(Name = "udi")] - public Udi NodeUdi => Udi.Create(NodeType, NodeKey); + [DataMember(Name = "udi")] + public Udi? NodeUdi => NodeType == Constants.UdiEntityType.Unknown ? null : Udi.Create(NodeType, NodeKey); - [DataMember(Name = "icon")] - public string? ContentTypeIcon { get; set; } + [DataMember(Name = "icon")] + public string? ContentTypeIcon { get; set; } - [DataMember(Name = "alias")] - public string? ContentTypeAlias { get; set; } + [DataMember(Name = "alias")] + public string? ContentTypeAlias { get; set; } - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } + [DataMember(Name = "contentTypeName")] + public string? ContentTypeName { get; set; } - [DataMember(Name = "relationTypeName")] - public string? RelationTypeName { get; set; } + [DataMember(Name = "relationTypeName")] + public string? RelationTypeName { get; set; } - [DataMember(Name = "relationTypeIsBidirectional")] - public bool RelationTypeIsBidirectional { get; set; } + [DataMember(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } - [DataMember(Name = "relationTypeIsDependency")] - public bool RelationTypeIsDependency { get; set; } - - } + [DataMember(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } } diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index f6ee00fc72..519885b7fa 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -1,100 +1,115 @@ -using System; -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a RelationType +/// +[Serializable] +[DataContract(IsReference = true)] +public class RelationType : EntityBase, IRelationTypeWithIsDependency { - /// - /// Represents a RelationType - /// - [Serializable] - [DataContract(IsReference = true)] - public class RelationType : EntityBase, IRelationType, IRelationTypeWithIsDependency + private string _alias; + private Guid? _childObjectType; + private bool _isBidirectional; + private bool _isDependency; + private string _name; + private Guid? _parentObjectType; + + public RelationType(string alias, string name) + : this(name, alias, false, null, null, false) { - private string _name; - private string _alias; - private bool _isBidirectional; - private bool _isDependency; - private Guid? _parentObjectType; - private Guid? _childObjectType; + } - public RelationType(string alias, string name) - : this(name: name, alias: alias, false, null, null, false) + public RelationType(string? name, string? alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency){ + if (name == null) { + throw new ArgumentNullException(nameof(name)); } - public RelationType(string? name, string? alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency) + if (string.IsNullOrWhiteSpace(name)) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (alias == null) throw new ArgumentNullException(nameof(alias)); - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); - - _name = name; - _alias = alias; - _isBidirectional = isBidrectional; - _isDependency = isDependency; - _parentObjectType = parentObjectType; - _childObjectType = childObjectType; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Gets or sets the Name of the RelationType - /// - [DataMember] - public string? Name + if (alias == null) { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + throw new ArgumentNullException(nameof(alias)); } - /// - /// Gets or sets the Alias of the RelationType - /// - [DataMember] - public string Alias + if (string.IsNullOrWhiteSpace(alias)) { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(alias)); } - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember] - public bool IsBidirectional - { - get => _isBidirectional; - set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional)); - } + _name = name; + _alias = alias; + _isBidirectional = isBidrectional; + _isDependency = isDependency; + _parentObjectType = parentObjectType; + _childObjectType = childObjectType; + } - /// - /// Gets or sets the Parents object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - public Guid? ParentObjectType - { - get => _parentObjectType; - set => SetPropertyValueAndDetectChanges(value, ref _parentObjectType, nameof(ParentObjectType)); - } + /// + /// Gets or sets the Name of the RelationType + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - /// - /// Gets or sets the Childs object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - public Guid? ChildObjectType - { - get => _childObjectType; - set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType)); - } + /// + /// Gets or sets the Alias of the RelationType + /// + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + } + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember] + public bool IsBidirectional + { + get => _isBidirectional; + set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional)); + } - public bool IsDependency - { - get => _isDependency; - set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency)); - } + /// + /// Gets or sets the Parents object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + public Guid? ParentObjectType + { + get => _parentObjectType; + set => SetPropertyValueAndDetectChanges(value, ref _parentObjectType, nameof(ParentObjectType)); + } + + /// + /// Gets or sets the Childs object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + public Guid? ChildObjectType + { + get => _childObjectType; + set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType)); + } + + public bool IsDependency + { + get => _isDependency; + set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency)); } } diff --git a/src/Umbraco.Core/Models/RelationTypeExtensions.cs b/src/Umbraco.Core/Models/RelationTypeExtensions.cs index 1e7282b66b..b5803d3fb3 100644 --- a/src/Umbraco.Core/Models/RelationTypeExtensions.cs +++ b/src/Umbraco.Core/Models/RelationTypeExtensions.cs @@ -1,18 +1,17 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class RelationTypeExtensions { - public static class RelationTypeExtensions - { - public static bool IsSystemRelationType(this IRelationType relationType) => - relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - } + public static bool IsSystemRelationType(this IRelationType relationType) => + relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; } diff --git a/src/Umbraco.Core/Models/RequestPasswordResetModel.cs b/src/Umbraco.Core/Models/RequestPasswordResetModel.cs index 438e97fb30..9b4932f88a 100644 --- a/src/Umbraco.Core/Models/RequestPasswordResetModel.cs +++ b/src/Umbraco.Core/Models/RequestPasswordResetModel.cs @@ -1,14 +1,12 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ +namespace Umbraco.Cms.Core.Models; - [DataContract(Name = "requestPasswordReset", Namespace = "")] - public class RequestPasswordResetModel - { - [Required] - [DataMember(Name = "email", IsRequired = true)] - public string Email { get; set; } = null!; - } +[DataContract(Name = "requestPasswordReset", Namespace = "")] +public class RequestPasswordResetModel +{ + [Required] + [DataMember(Name = "email", IsRequired = true)] + public string Email { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/Script.cs b/src/Umbraco.Core/Models/Script.cs index 0d121368f8..03888bd27a 100644 --- a/src/Umbraco.Core/Models/Script.cs +++ b/src/Umbraco.Core/Models/Script.cs @@ -1,29 +1,29 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Script file +/// +[Serializable] +[DataContract(IsReference = true)] +public class Script : File, IScript { - /// - /// Represents a Script file - /// - [Serializable] - [DataContract(IsReference = true)] - public class Script : File, IScript + public Script(string path) + : this(path, null) { - public Script(string path) - : this(path, (Func?) null) - { } - - public Script(string path, Func? getFileContent) - : base(path, getFileContent) - { } - - /// - /// Indicates whether the current entity has an identity, which in this case is a path/name. - /// - /// - /// Overrides the default Entity identity check. - /// - public override bool HasIdentity => string.IsNullOrEmpty(Path) == false; } + + public Script(string path, Func? getFileContent) + : base(path, getFileContent) + { + } + + /// + /// Indicates whether the current entity has an identity, which in this case is a path/name. + /// + /// + /// Overrides the default Entity identity check. + /// + public override bool HasIdentity => string.IsNullOrEmpty(Path) == false; } diff --git a/src/Umbraco.Core/Models/SendCodeViewModel.cs b/src/Umbraco.Core/Models/SendCodeViewModel.cs index 783bcdeec2..c73fd73eb3 100644 --- a/src/Umbraco.Core/Models/SendCodeViewModel.cs +++ b/src/Umbraco.Core/Models/SendCodeViewModel.cs @@ -1,32 +1,32 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Used for 2FA verification +/// +[DataContract(Name = "code", Namespace = "")] +public class Verify2FACodeModel { + [Required] + [DataMember(Name = "code", IsRequired = true)] + public string? Code { get; set; } + + [Required] + [DataMember(Name = "provider", IsRequired = true)] + public string? Provider { get; set; } + /// - /// Used for 2FA verification + /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// - [DataContract(Name = "code", Namespace = "")] - public class Verify2FACodeModel - { - [Required] - [DataMember(Name = "code", IsRequired = true)] - public string? Code { get; set; } + [DataMember(Name = "isPersistent", IsRequired = true)] + public bool IsPersistent { get; set; } - [Required] - [DataMember(Name = "provider", IsRequired = true)] - public string? Provider { get; set; } - - /// - /// Flag indicating whether the sign-in cookie should persist after the browser is closed. - /// - [DataMember(Name = "isPersistent", IsRequired = true)] - public bool IsPersistent { get; set; } - - /// - /// Flag indicating whether the current browser should be remember, suppressing all further two factor authentication prompts. - /// - [DataMember(Name = "rememberClient", IsRequired = true)] - public bool RememberClient { get; set; } - } + /// + /// Flag indicating whether the current browser should be remember, suppressing all further two factor authentication + /// prompts. + /// + [DataMember(Name = "rememberClient", IsRequired = true)] + public bool RememberClient { get; set; } } diff --git a/src/Umbraco.Core/Models/ServerRegistration.cs b/src/Umbraco.Core/Models/ServerRegistration.cs index 553460eb5b..6507d5d64c 100644 --- a/src/Umbraco.Core/Models/ServerRegistration.cs +++ b/src/Umbraco.Core/Models/ServerRegistration.cs @@ -1,124 +1,120 @@ -using System; using System.Globalization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a registered server in a multiple-servers environment. +/// +public class ServerRegistration : EntityBase, IServerRegistration { + private bool _isActive; + private bool _isSchedulingPublisher; + private string? _serverAddress; + private string? _serverIdentity; + /// - /// Represents a registered server in a multiple-servers environment. + /// Initializes a new instance of the class. /// - public class ServerRegistration : EntityBase, IServerRegistration + public ServerRegistration() { - private string? _serverAddress; - private string? _serverIdentity; - private bool _isActive; - private bool _isSchedulingPublisher; - - /// - /// Initializes a new instance of the class. - /// - public ServerRegistration() - { } - - /// - /// Initializes a new instance of the class. - /// - /// The unique id of the server registration. - /// The server URL. - /// The unique server identity. - /// The date and time the registration was created. - /// The date and time the registration was last accessed. - /// A value indicating whether the registration is active. - /// A value indicating whether the registration is master. - public ServerRegistration(int id, string? serverAddress, string? serverIdentity, DateTime registered, DateTime accessed, bool isActive, bool isSchedulingPublisher) - { - UpdateDate = accessed; - CreateDate = registered; - Key = id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); - Id = id; - ServerAddress = serverAddress; - ServerIdentity = serverIdentity; - IsActive = isActive; - IsSchedulingPublisher = isSchedulingPublisher; - } - - /// - /// Initializes a new instance of the class. - /// - /// The server URL. - /// The unique server identity. - /// The date and time the registration was created. - public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered) - { - CreateDate = registered; - UpdateDate = registered; - Key = 0.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); - ServerAddress = serverAddress; - ServerIdentity = serverIdentity; - } - - /// - /// Gets or sets the server URL. - /// - public string? ServerAddress - { - get => _serverAddress; - set => SetPropertyValueAndDetectChanges(value, ref _serverAddress, nameof(ServerAddress)); - } - - /// - /// Gets or sets the server unique identity. - /// - public string? ServerIdentity - { - get => _serverIdentity; - set => SetPropertyValueAndDetectChanges(value, ref _serverIdentity, nameof(ServerIdentity)); - } - - /// - /// Gets or sets a value indicating whether the server is active. - /// - public bool IsActive - { - get => _isActive; - set => SetPropertyValueAndDetectChanges(value, ref _isActive, nameof(IsActive)); - } - - /// - /// Gets or sets a value indicating whether the server has the SchedulingPublisher role - /// - public bool IsSchedulingPublisher - { - get => _isSchedulingPublisher; - set => SetPropertyValueAndDetectChanges(value, ref _isSchedulingPublisher, nameof(IsSchedulingPublisher)); - } - - /// - /// Gets the date and time the registration was created. - /// - public DateTime Registered - { - get => CreateDate; - set => CreateDate = value; - } - - /// - /// Gets the date and time the registration was last accessed. - /// - public DateTime Accessed - { - get => UpdateDate; - set => UpdateDate = value; - } - - /// - /// Converts the value of this instance to its equivalent string representation. - /// - /// - public override string ToString() - { - return string.Format("{{\"{0}\", \"{1}\", {2}active, {3}master}}", ServerAddress, ServerIdentity, IsActive ? "" : "!", IsSchedulingPublisher ? "" : "!"); - } } + + /// + /// Initializes a new instance of the class. + /// + /// The unique id of the server registration. + /// The server URL. + /// The unique server identity. + /// The date and time the registration was created. + /// The date and time the registration was last accessed. + /// A value indicating whether the registration is active. + /// A value indicating whether the registration is scheduling publisher. + public ServerRegistration(int id, string? serverAddress, string? serverIdentity, DateTime registered, DateTime accessed, bool isActive, bool isSchedulingPublisher) + { + UpdateDate = accessed; + CreateDate = registered; + Key = id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); + Id = id; + ServerAddress = serverAddress; + ServerIdentity = serverIdentity; + IsActive = isActive; + IsSchedulingPublisher = isSchedulingPublisher; + } + + /// + /// Initializes a new instance of the class. + /// + /// The server URL. + /// The unique server identity. + /// The date and time the registration was created. + public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered) + { + CreateDate = registered; + UpdateDate = registered; + Key = 0.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); + ServerAddress = serverAddress; + ServerIdentity = serverIdentity; + } + + /// + /// Gets or sets the server URL. + /// + public string? ServerAddress + { + get => _serverAddress; + set => SetPropertyValueAndDetectChanges(value, ref _serverAddress, nameof(ServerAddress)); + } + + /// + /// Gets or sets the server unique identity. + /// + public string? ServerIdentity + { + get => _serverIdentity; + set => SetPropertyValueAndDetectChanges(value, ref _serverIdentity, nameof(ServerIdentity)); + } + + /// + /// Gets or sets a value indicating whether the server is active. + /// + public bool IsActive + { + get => _isActive; + set => SetPropertyValueAndDetectChanges(value, ref _isActive, nameof(IsActive)); + } + + /// + /// Gets or sets a value indicating whether the server has the SchedulingPublisher role + /// + public bool IsSchedulingPublisher + { + get => _isSchedulingPublisher; + set => SetPropertyValueAndDetectChanges(value, ref _isSchedulingPublisher, nameof(IsSchedulingPublisher)); + } + + /// + /// Gets the date and time the registration was created. + /// + public DateTime Registered + { + get => CreateDate; + set => CreateDate = value; + } + + /// + /// Gets the date and time the registration was last accessed. + /// + public DateTime Accessed + { + get => UpdateDate; + set => UpdateDate = value; + } + + /// + /// Converts the value of this instance to its equivalent string representation. + /// + /// + public override string ToString() => string.Format("{{\"{0}\", \"{1}\", {2}active, {3}master}}", ServerAddress, ServerIdentity, IsActive ? string.Empty : "!", IsSchedulingPublisher ? string.Empty : "!"); } diff --git a/src/Umbraco.Core/Models/SetPasswordModel.cs b/src/Umbraco.Core/Models/SetPasswordModel.cs index c904f98694..57d1abc38f 100644 --- a/src/Umbraco.Core/Models/SetPasswordModel.cs +++ b/src/Umbraco.Core/Models/SetPasswordModel.cs @@ -1,21 +1,20 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "setPassword", Namespace = "")] +public class SetPasswordModel { - [DataContract(Name = "setPassword", Namespace = "")] - public class SetPasswordModel - { - [Required] - [DataMember(Name = "userId", IsRequired = true)] - public int UserId { get; set; } + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } - [Required] - [DataMember(Name = "password", IsRequired = true)] - public string? Password { get; set; } + [Required] + [DataMember(Name = "password", IsRequired = true)] + public string? Password { get; set; } - [Required] - [DataMember(Name = "resetCode", IsRequired = true)] - public string? ResetCode { get; set; } - } + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string? ResetCode { get; set; } } diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index 31e061362c..7fe88a8a8a 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -1,99 +1,108 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +public class SimpleContentType : ISimpleContentType { /// - /// Implements . + /// Initializes a new instance of the class. /// - public class SimpleContentType : ISimpleContentType + public SimpleContentType(IContentType contentType) + : this((IContentTypeBase)contentType) => + DefaultTemplate = contentType.DefaultTemplate; + + /// + /// Initializes a new instance of the class. + /// + public SimpleContentType(IMediaType mediaType) + : this((IContentTypeBase)mediaType) { - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IContentType contentType) - : this((IContentTypeBase)contentType) + } + + /// + /// Initializes a new instance of the class. + /// + public SimpleContentType(IMemberType memberType) + : this((IContentTypeBase)memberType) + { + } + + private SimpleContentType(IContentTypeBase contentType) + { + if (contentType == null) { - DefaultTemplate = contentType.DefaultTemplate; + throw new ArgumentNullException(nameof(contentType)); } - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IMediaType mediaType) - : this((IContentTypeBase)mediaType) - { } + Id = contentType.Id; + Key = contentType.Key; + Alias = contentType.Alias; + Variations = contentType.Variations; + Icon = contentType.Icon; + IsContainer = contentType.IsContainer; + Name = contentType.Name; + AllowedAsRoot = contentType.AllowedAsRoot; + IsElement = contentType.IsElement; + } - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IMemberType memberType) - : this((IContentTypeBase)memberType) - { } + public string Alias { get; } - private SimpleContentType(IContentTypeBase contentType) + public int Id { get; } + + public Guid Key { get; } + + /// + public ITemplate? DefaultTemplate { get; } + + public ContentVariation Variations { get; } + + public string? Icon { get; } + + public bool IsContainer { get; } + + public string? Name { get; } + + public bool AllowedAsRoot { get; } + + public bool IsElement { get; } + + public bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false) => + + // non-exact validation: can accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, false, wildcards, false); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (contentType == null) throw new ArgumentNullException(nameof(contentType)); - - Id = contentType.Id; - Key = contentType.Key; - Alias = contentType.Alias; - Variations = contentType.Variations; - Icon = contentType.Icon; - IsContainer = contentType.IsContainer; - Name = contentType.Name; - AllowedAsRoot = contentType.AllowedAsRoot; - IsElement = contentType.IsElement; + return false; } - public string Alias { get; } - - public int Id { get; } - - public Guid Key { get; } - - /// - public ITemplate? DefaultTemplate { get; } - - public ContentVariation Variations { get; } - - public string? Icon { get; } - - public bool IsContainer { get; } - - public string? Name { get; } - - public bool AllowedAsRoot { get; } - - public bool IsElement { get; } - - public bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false) + if (ReferenceEquals(this, obj)) { - // non-exact validation: can accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, false, wildcards, false); + return true; } - protected bool Equals(SimpleContentType other) + if (obj.GetType() != GetType()) { - return string.Equals(Alias, other.Alias) && Id == other.Id; + return false; } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((SimpleContentType) obj); - } + return Equals((SimpleContentType)obj); + } - public override int GetHashCode() + protected bool Equals(SimpleContentType other) => string.Equals(Alias, other.Alias) && Id == other.Id; + + public override int GetHashCode() + { + unchecked { - unchecked - { - return ((Alias != null ? Alias.GetHashCode() : 0) * 397) ^ Id; - } + return ((Alias != null ? Alias.GetHashCode() : 0) * 397) ^ Id; } - } + } } diff --git a/src/Umbraco.Core/Models/SimpleValidationModel.cs b/src/Umbraco.Core/Models/SimpleValidationModel.cs index 30efec7dfe..390fe5a31c 100644 --- a/src/Umbraco.Core/Models/SimpleValidationModel.cs +++ b/src/Umbraco.Core/Models/SimpleValidationModel.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class SimpleValidationModel { - public class SimpleValidationModel + public SimpleValidationModel(IDictionary modelState, string message = "The request is invalid.") { - public SimpleValidationModel(IDictionary modelState, string message = "The request is invalid.") - { - Message = message; - ModelState = modelState; - } - - public string Message { get; } - public IDictionary ModelState { get; } + Message = message; + ModelState = modelState; } + + public string Message { get; } + + public IDictionary ModelState { get; } } diff --git a/src/Umbraco.Core/Models/Stylesheet.cs b/src/Umbraco.Core/Models/Stylesheet.cs index 7b1d971434..07f35c88e3 100644 --- a/src/Umbraco.Core/Models/Stylesheet.cs +++ b/src/Umbraco.Core/Models/Stylesheet.cs @@ -1,178 +1,166 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Data; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings.Css; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Stylesheet file +/// +[Serializable] +[DataContract(IsReference = true)] +public class Stylesheet : File, IStylesheet { - /// - /// Represents a Stylesheet file - /// - [Serializable] - [DataContract(IsReference = true)] - public class Stylesheet : File, IStylesheet + private Lazy>? _properties; + + public Stylesheet(string path) + : this(path, null) { - public Stylesheet(string path) - : this(path, null) - { } + } - public Stylesheet(string path, Func? getFileContent) - : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) + public Stylesheet(string path, Func? getFileContent) + : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) => + InitializeProperties(); + + /// + /// Gets or sets the Content of a File + /// + public override string? Content + { + get => base.Content; + set { + base.Content = value; + + // re-set the properties so they are re-read from the content InitializeProperties(); } + } - private Lazy>? _properties; + /// + /// Returns a list of umbraco back office enabled stylesheet properties + /// + /// + /// An umbraco back office enabled stylesheet property has a special prefix, for example: + /// /** umb_name: MyPropertyName */ p { font-size: 1em; } + /// + [IgnoreDataMember] + public IEnumerable? Properties => _properties?.Value; - private void InitializeProperties() + /// + /// Indicates whether the current entity has an identity, which in this case is a path/name. + /// + /// + /// Overrides the default Entity identity check. + /// + public override bool HasIdentity => string.IsNullOrEmpty(Path) == false; + + /// + /// Adds an Umbraco stylesheet property for use in the back office + /// + /// + public void AddProperty(IStylesheetProperty property) + { + if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(property.Name))) { - //if the value is already created, we need to be created and update the collection according to - //what is now in the content - if (_properties != null && _properties.IsValueCreated) - { - //re-parse it so we can check what properties are different and adjust the event handlers - var parsed = StylesheetHelper.ParseRules(Content).ToArray(); - var names = parsed.Select(x => x.Name).ToArray(); - var existing = _properties.Value.Where(x => names.InvariantContains(x.Name)).ToArray(); - //update existing - foreach (var stylesheetProperty in existing) - { - var updateFrom = parsed.Single(x => x.Name.InvariantEquals(stylesheetProperty.Name)); - //remove current event handler while we update, we'll reset it after - stylesheetProperty.PropertyChanged -= Property_PropertyChanged; - stylesheetProperty.Alias = updateFrom.Selector; - stylesheetProperty.Value = updateFrom.Styles; - //re-add - stylesheetProperty.PropertyChanged += Property_PropertyChanged; - } - //remove no longer existing - var nonExisting = _properties.Value.Where(x => names.InvariantContains(x.Name) == false).ToArray(); - foreach (var stylesheetProperty in nonExisting) - { - stylesheetProperty.PropertyChanged -= Property_PropertyChanged; - _properties.Value.Remove(stylesheetProperty); - } - //add new ones - var newItems = parsed.Where(x => _properties.Value.Select(p => p.Name).InvariantContains(x.Name) == false); - foreach (var stylesheetRule in newItems) - { - var prop = new StylesheetProperty(stylesheetRule.Name, stylesheetRule.Selector, stylesheetRule.Styles); - prop.PropertyChanged += Property_PropertyChanged; - _properties.Value.Add(prop); - } - } - - //we haven't read the properties yet so create the lazy delegate - _properties = new Lazy>(() => - { - var parsed = StylesheetHelper.ParseRules(Content); - return parsed.Select(statement => - { - var property = new StylesheetProperty(statement.Name, statement.Selector, statement.Styles); - property.PropertyChanged += Property_PropertyChanged; - return property; - - }).ToList(); - }); + throw new DuplicateNameException("The property with the name " + property.Name + + " already exists in the collection"); } - /// - /// If the property has changed then we need to update the content - /// - /// - /// - void Property_PropertyChanged(object? sender, PropertyChangedEventArgs e) - { - var prop = (StylesheetProperty?) sender; + // now we need to serialize out the new property collection over-top of the string Content. + Content = StylesheetHelper.AppendRule( + Content, + new StylesheetRule { Name = property.Name, Selector = property.Alias, Styles = property.Value }); - if (prop is not null) + // re-set lazy collection + InitializeProperties(); + } + + /// + /// Removes an Umbraco stylesheet property + /// + /// + public void RemoveProperty(string name) + { + if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(name))) + { + Content = StylesheetHelper.ReplaceRule(Content, name, null); + } + } + + private void InitializeProperties() + { + // if the value is already created, we need to be created and update the collection according to + // what is now in the content + if (_properties != null && _properties.IsValueCreated) + { + // re-parse it so we can check what properties are different and adjust the event handlers + StylesheetRule[] parsed = StylesheetHelper.ParseRules(Content).ToArray(); + var names = parsed.Select(x => x.Name).ToArray(); + StylesheetProperty[] existing = _properties.Value.Where(x => names.InvariantContains(x.Name)).ToArray(); + + // update existing + foreach (StylesheetProperty stylesheetProperty in existing) { - //Ensure we are setting base.Content here so that the properties don't get reset and thus any event handlers would get reset too - base.Content = StylesheetHelper.ReplaceRule(Content, prop.Name, new StylesheetRule - { - Name = prop.Name, - Selector = prop.Alias, - Styles = prop.Value - }); + StylesheetRule updateFrom = parsed.Single(x => x.Name.InvariantEquals(stylesheetProperty.Name)); + + // remove current event handler while we update, we'll reset it after + stylesheetProperty.PropertyChanged -= Property_PropertyChanged; + stylesheetProperty.Alias = updateFrom.Selector; + stylesheetProperty.Value = updateFrom.Styles; + + // re-add + stylesheetProperty.PropertyChanged += Property_PropertyChanged; + } + + // remove no longer existing + StylesheetProperty[] nonExisting = + _properties.Value.Where(x => names.InvariantContains(x.Name) == false).ToArray(); + foreach (StylesheetProperty stylesheetProperty in nonExisting) + { + stylesheetProperty.PropertyChanged -= Property_PropertyChanged; + _properties.Value.Remove(stylesheetProperty); + } + + // add new ones + IEnumerable newItems = parsed.Where(x => + _properties.Value.Select(p => p.Name).InvariantContains(x.Name) == false); + foreach (StylesheetRule stylesheetRule in newItems) + { + var prop = new StylesheetProperty(stylesheetRule.Name, stylesheetRule.Selector, stylesheetRule.Styles); + prop.PropertyChanged += Property_PropertyChanged; + _properties.Value.Add(prop); } } - /// - /// Gets or sets the Content of a File - /// - public override string? Content + // we haven't read the properties yet so create the lazy delegate + _properties = new Lazy>(() => { - get { return base.Content; } - set + IEnumerable parsed = StylesheetHelper.ParseRules(Content); + return parsed.Select(statement => { - base.Content = value; - //re-set the properties so they are re-read from the content - InitializeProperties(); - } - } + var property = new StylesheetProperty(statement.Name, statement.Selector, statement.Styles); + property.PropertyChanged += Property_PropertyChanged; + return property; + }).ToList(); + }); + } - /// - /// Returns a list of umbraco back office enabled stylesheet properties - /// - /// - /// An umbraco back office enabled stylesheet property has a special prefix, for example: - /// - /// /** umb_name: MyPropertyName */ p { font-size: 1em; } - /// - [IgnoreDataMember] - public IEnumerable? Properties + /// + /// If the property has changed then we need to update the content + /// + /// + /// + private void Property_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + var prop = (StylesheetProperty?)sender; + + if (prop is not null) { - get { return _properties?.Value; } - } - - /// - /// Adds an Umbraco stylesheet property for use in the back office - /// - /// - public void AddProperty(IStylesheetProperty property) - { - if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(property.Name))) - { - throw new DuplicateNameException("The property with the name " + property.Name + " already exists in the collection"); - } - - //now we need to serialize out the new property collection over-top of the string Content. - Content = StylesheetHelper.AppendRule(Content, new StylesheetRule - { - Name = property.Name, - Selector = property.Alias, - Styles = property.Value - }); - - //re-set lazy collection - InitializeProperties(); - } - - /// - /// Removes an Umbraco stylesheet property - /// - /// - public void RemoveProperty(string name) - { - if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(name))) - { - Content = StylesheetHelper.ReplaceRule(Content, name, null); - } - } - - /// - /// Indicates whether the current entity has an identity, which in this case is a path/name. - /// - /// - /// Overrides the default Entity identity check. - /// - public override bool HasIdentity - { - get { return string.IsNullOrEmpty(Path) == false; } + // Ensure we are setting base.Content here so that the properties don't get reset and thus any event handlers would get reset too + base.Content = StylesheetHelper.ReplaceRule(Content, prop.Name, new StylesheetRule { Name = prop.Name, Selector = prop.Alias, Styles = prop.Value }); } } } diff --git a/src/Umbraco.Core/Models/StylesheetProperty.cs b/src/Umbraco.Core/Models/StylesheetProperty.cs index af6f347a63..730ff8ff3e 100644 --- a/src/Umbraco.Core/Models/StylesheetProperty.cs +++ b/src/Umbraco.Core/Models/StylesheetProperty.cs @@ -1,51 +1,48 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Stylesheet Property +/// +/// +/// Properties are always formatted to have a single selector, so it can be used in the backoffice +/// +[Serializable] +[DataContract(IsReference = true)] +public class StylesheetProperty : BeingDirtyBase, IValueObject, IStylesheetProperty { - /// - /// Represents a Stylesheet Property - /// - /// - /// Properties are always formatted to have a single selector, so it can be used in the backoffice - /// - [Serializable] - [DataContract(IsReference = true)] - public class StylesheetProperty : BeingDirtyBase, IValueObject, IStylesheetProperty + private string _alias; + private string _value; + + public StylesheetProperty(string name, string alias, string value) { - private string _alias; - private string _value; + Name = name; + _alias = alias; + _value = value; + } - public StylesheetProperty(string name, string @alias, string value) - { - Name = name; - _alias = alias; - _value = value; - } + /// + /// The CSS rule name that can be used by Umbraco in the back office + /// + public string Name { get; private set; } - /// - /// The CSS rule name that can be used by Umbraco in the back office - /// - public string Name { get; private set; } - - /// - /// This is the CSS Selector - /// - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); - } - - /// - /// The CSS value for the selector - /// - public string Value - { - get => _value; - set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); - } + /// + /// This is the CSS Selector + /// + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + } + /// + /// The CSS value for the selector + /// + public string Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); } } diff --git a/src/Umbraco.Core/Models/Tag.cs b/src/Umbraco.Core/Models/Tag.cs index 92436d068b..1c4bf4b88c 100644 --- a/src/Umbraco.Core/Models/Tag.cs +++ b/src/Umbraco.Core/Models/Tag.cs @@ -1,59 +1,58 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a tag entity. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Tag : EntityBase, ITag { + private string _group = string.Empty; + private int? _languageId; + private string _text = string.Empty; + /// - /// Represents a tag entity. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class Tag : EntityBase, ITag + public Tag() { - private string _group = string.Empty; - private string _text = string.Empty; - private int? _languageId; - - /// - /// Initializes a new instance of the class. - /// - public Tag() - { } - - /// - /// Initializes a new instance of the class. - /// - public Tag(int id, string group, string text, int? languageId = null) - { - Id = id; - Text = text; - Group = group; - LanguageId = languageId; - } - - /// - public string Group - { - get => _group; - set => SetPropertyValueAndDetectChanges(value, ref _group!, nameof(Group)); - } - - /// - public string Text - { - get => _text; - set => SetPropertyValueAndDetectChanges(value, ref _text!, nameof(Text)); - } - - /// - public int? LanguageId - { - get => _languageId; - set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); - } - - /// - public int NodeCount { get; set; } } + + /// + /// Initializes a new instance of the class. + /// + public Tag(int id, string group, string text, int? languageId = null) + { + Id = id; + Text = text; + Group = group; + LanguageId = languageId; + } + + /// + public string Group + { + get => _group; + set => SetPropertyValueAndDetectChanges(value, ref _group!, nameof(Group)); + } + + /// + public string Text + { + get => _text; + set => SetPropertyValueAndDetectChanges(value, ref _text!, nameof(Text)); + } + + /// + public int? LanguageId + { + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); + } + + /// + public int NodeCount { get; set; } } diff --git a/src/Umbraco.Core/Models/TagModel.cs b/src/Umbraco.Core/Models/TagModel.cs index 6a0430a492..2646b216e3 100644 --- a/src/Umbraco.Core/Models/TagModel.cs +++ b/src/Umbraco.Core/Models/TagModel.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "tag", Namespace = "")] +public class TagModel { - [DataContract(Name = "tag", Namespace = "")] - public class TagModel - { - [DataMember(Name = "id", IsRequired = true)] - public int Id { get; set; } + [DataMember(Name = "id", IsRequired = true)] + public int Id { get; set; } - [DataMember(Name = "text", IsRequired = true)] - public string? Text { get; set; } + [DataMember(Name = "text", IsRequired = true)] + public string? Text { get; set; } - [DataMember(Name = "group")] - public string? Group { get; set; } + [DataMember(Name = "group")] + public string? Group { get; set; } - [DataMember(Name = "nodeCount")] - public int NodeCount { get; set; } - } + [DataMember(Name = "nodeCount")] + public int NodeCount { get; set; } } diff --git a/src/Umbraco.Core/Models/TaggableObjectTypes.cs b/src/Umbraco.Core/Models/TaggableObjectTypes.cs index 8a9384ec74..03be2273a2 100644 --- a/src/Umbraco.Core/Models/TaggableObjectTypes.cs +++ b/src/Umbraco.Core/Models/TaggableObjectTypes.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Enum representing the taggable object types +/// +public enum TaggableObjectTypes { - /// - /// Enum representing the taggable object types - /// - public enum TaggableObjectTypes - { - All, - Content, - Media, - Member - } + All, + Content, + Media, + Member, } diff --git a/src/Umbraco.Core/Models/TaggedEntity.cs b/src/Umbraco.Core/Models/TaggedEntity.cs index 9bc05eae15..821f592343 100644 --- a/src/Umbraco.Core/Models/TaggedEntity.cs +++ b/src/Umbraco.Core/Models/TaggedEntity.cs @@ -1,31 +1,30 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a tagged entity. +/// +/// +/// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged, +/// which is why this class is composed of a list of tagged properties and the identifier the actual entity. +/// +public class TaggedEntity { /// - /// Represents a tagged entity. + /// Initializes a new instance of the class. /// - /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged, - /// which is why this class is composed of a list of tagged properties and the identifier the actual entity. - public class TaggedEntity + public TaggedEntity(int entityId, IEnumerable taggedProperties) { - /// - /// Initializes a new instance of the class. - /// - public TaggedEntity(int entityId, IEnumerable taggedProperties) - { - EntityId = entityId; - TaggedProperties = taggedProperties; - } - - /// - /// Gets the identifier of the entity. - /// - public int EntityId { get; } - - /// - /// Gets the tagged properties. - /// - public IEnumerable TaggedProperties { get; } + EntityId = entityId; + TaggedProperties = taggedProperties; } + + /// + /// Gets the identifier of the entity. + /// + public int EntityId { get; } + + /// + /// Gets the tagged properties. + /// + public IEnumerable TaggedProperties { get; } } diff --git a/src/Umbraco.Core/Models/TaggedProperty.cs b/src/Umbraco.Core/Models/TaggedProperty.cs index 24ef9ccc45..90257a1a3e 100644 --- a/src/Umbraco.Core/Models/TaggedProperty.cs +++ b/src/Umbraco.Core/Models/TaggedProperty.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a tagged property on an entity. +/// +public class TaggedProperty { /// - /// Represents a tagged property on an entity. + /// Initializes a new instance of the class. /// - public class TaggedProperty + public TaggedProperty(int propertyTypeId, string? propertyTypeAlias, IEnumerable tags) { - /// - /// Initializes a new instance of the class. - /// - public TaggedProperty(int propertyTypeId, string? propertyTypeAlias, IEnumerable tags) - { - PropertyTypeId = propertyTypeId; - PropertyTypeAlias = propertyTypeAlias; - Tags = tags; - } - - /// - /// Gets the identifier of the property type. - /// - public int PropertyTypeId { get; } - - /// - /// Gets the alias of the property type. - /// - public string? PropertyTypeAlias { get; } - - /// - /// Gets the tags. - /// - public IEnumerable Tags { get; } + PropertyTypeId = propertyTypeId; + PropertyTypeAlias = propertyTypeAlias; + Tags = tags; } + + /// + /// Gets the identifier of the property type. + /// + public int PropertyTypeId { get; } + + /// + /// Gets the alias of the property type. + /// + public string? PropertyTypeAlias { get; } + + /// + /// Gets the tags. + /// + public IEnumerable Tags { get; } } diff --git a/src/Umbraco.Core/Models/TagsStorageType.cs b/src/Umbraco.Core/Models/TagsStorageType.cs index 7bd8ea7937..ccff41bb72 100644 --- a/src/Umbraco.Core/Models/TagsStorageType.cs +++ b/src/Umbraco.Core/Models/TagsStorageType.cs @@ -1,20 +1,21 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines how tags are stored. +/// +/// +/// Tags are always stored as a string, but the string can +/// either be a delimited string, or a serialized Json array. +/// +public enum TagsStorageType { /// - /// Defines how tags are stored. + /// Store tags as a delimited string. /// - /// Tags are always stored as a string, but the string can - /// either be a delimited string, or a serialized Json array. - public enum TagsStorageType - { - /// - /// Store tags as a delimited string. - /// - Csv, + Csv, - /// - /// Store tags as serialized Json. - /// - Json - } + /// + /// Store tags as serialized Json. + /// + Json, } diff --git a/src/Umbraco.Core/Models/TelemetryLevel.cs b/src/Umbraco.Core/Models/TelemetryLevel.cs index 26a714b385..cdf1d24e90 100644 --- a/src/Umbraco.Core/Models/TelemetryLevel.cs +++ b/src/Umbraco.Core/Models/TelemetryLevel.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public enum TelemetryLevel { - [DataContract] - public enum TelemetryLevel - { - Minimal, - Basic, - Detailed, - } + Minimal, + Basic, + Detailed, } diff --git a/src/Umbraco.Core/Models/TelemetryResource.cs b/src/Umbraco.Core/Models/TelemetryResource.cs index 401e07848f..1c62842381 100644 --- a/src/Umbraco.Core/Models/TelemetryResource.cs +++ b/src/Umbraco.Core/Models/TelemetryResource.cs @@ -1,11 +1,10 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class TelemetryResource { - [DataContract] - public class TelemetryResource - { - [DataMember] - public TelemetryLevel TelemetryLevel { get; set; } - } + [DataMember] + public TelemetryLevel TelemetryLevel { get; set; } } diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index 7efccf1e7d..1900233aa9 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -1,86 +1,85 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Template file. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Template : File, ITemplate { - /// - /// Represents a Template file. - /// - [Serializable] - [DataContract(IsReference = true)] - public class Template : File, ITemplate + private readonly IShortStringHelper _shortStringHelper; + private string _alias; + private string? _masterTemplateAlias; + private Lazy? _masterTemplateId; + private string? _name; + + public Template(IShortStringHelper shortStringHelper, string? name, string? alias) + : this(shortStringHelper, name, alias, null) { - private string _alias; - private readonly IShortStringHelper _shortStringHelper; - private string? _name; - private string? _masterTemplateAlias; - private Lazy? _masterTemplateId; + } - public Template(IShortStringHelper shortStringHelper, string? name, string? alias) - : this(shortStringHelper, name, alias, null) - { } + public Template(IShortStringHelper shortStringHelper, string? name, string? alias, Func? getFileContent) + : base(string.Empty, getFileContent) + { + _shortStringHelper = shortStringHelper; + _name = name; + _alias = alias?.ToCleanString(shortStringHelper, CleanStringType.UnderscoreAlias) ?? string.Empty; + _masterTemplateId = new Lazy(() => -1); + } - public Template(IShortStringHelper shortStringHelper, string? name, string? alias, Func? getFileContent) - : base(string.Empty, getFileContent) + [DataMember] + public Lazy? MasterTemplateId + { + get => _masterTemplateId; + set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateId, nameof(MasterTemplateId)); + } + + public string? MasterTemplateAlias + { + get => _masterTemplateAlias; + set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateAlias, nameof(MasterTemplateAlias)); + } + + [DataMember] + public new string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + [DataMember] + public new string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias), ref _alias!, nameof(Alias)); + } + + /// + /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') + /// + public bool IsMasterTemplate { get; set; } + + public void SetMasterTemplate(ITemplate? masterTemplate) + { + if (masterTemplate == null) { - _shortStringHelper = shortStringHelper; - _name = name; - _alias = alias?.ToCleanString(shortStringHelper, CleanStringType.UnderscoreAlias) ?? string.Empty; - _masterTemplateId = new Lazy(() => -1); + MasterTemplateId = new Lazy(() => -1); + MasterTemplateAlias = null; } - - [DataMember] - public Lazy? MasterTemplateId + else { - get => _masterTemplateId; - set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateId, nameof(MasterTemplateId)); - } - - public string? MasterTemplateAlias - { - get => _masterTemplateAlias; - set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateAlias, nameof(MasterTemplateAlias)); - } - - [DataMember] - public new string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - [DataMember] - public new string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias), ref _alias!, nameof(Alias)); - } - - /// - /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') - /// - public bool IsMasterTemplate { get; set; } - - public void SetMasterTemplate(ITemplate? masterTemplate) - { - if (masterTemplate == null) - { - MasterTemplateId = new Lazy(() => -1); - MasterTemplateAlias = null; - } - else - { - MasterTemplateId = new Lazy(() => masterTemplate.Id); - MasterTemplateAlias = masterTemplate.Alias; - } - - } - - protected override void DeepCloneNameAndAlias(File clone) - { - // do nothing - prevents File from doing its stuff + MasterTemplateId = new Lazy(() => masterTemplate.Id); + MasterTemplateAlias = masterTemplate.Alias; } } + + protected override void DeepCloneNameAndAlias(File clone) + { + // do nothing - prevents File from doing its stuff + } } diff --git a/src/Umbraco.Core/Models/TemplateNode.cs b/src/Umbraco.Core/Models/TemplateNode.cs index 339f4efee3..f02988e6d2 100644 --- a/src/Umbraco.Core/Models/TemplateNode.cs +++ b/src/Umbraco.Core/Models/TemplateNode.cs @@ -1,34 +1,31 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a template in a template tree +/// +public class TemplateNode { - /// - /// Represents a template in a template tree - /// - public class TemplateNode + public TemplateNode(ITemplate template) { - public TemplateNode(ITemplate template) - { - Template = template; - Children = new List(); - } - - /// - /// The current template - /// - public ITemplate Template { get; set; } - - /// - /// The children of the current template - /// - public IEnumerable Children { get; set; } - - /// - /// The parent template to the current template - /// - /// - /// Will be null if there is no parent - /// - public TemplateNode? Parent { get; set; } + Template = template; + Children = new List(); } + + /// + /// The current template + /// + public ITemplate Template { get; set; } + + /// + /// The children of the current template + /// + public IEnumerable Children { get; set; } + + /// + /// The parent template to the current template + /// + /// + /// Will be null if there is no parent + /// + public TemplateNode? Parent { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateOnDisk.cs b/src/Umbraco.Core/Models/TemplateOnDisk.cs index 61c10ba456..04fffb7c10 100644 --- a/src/Umbraco.Core/Models/TemplateOnDisk.cs +++ b/src/Umbraco.Core/Models/TemplateOnDisk.cs @@ -1,52 +1,50 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Template file that can have its content on disk. +/// +[Serializable] +[DataContract(IsReference = true)] +public class TemplateOnDisk : Template { /// - /// Represents a Template file that can have its content on disk. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class TemplateOnDisk : Template + /// The name of the template. + /// The alias of the template. + /// The short string helper + public TemplateOnDisk(IShortStringHelper shortStringHelper, string name, string alias) + : base(shortStringHelper, name, alias) => + IsOnDisk = true; + + /// + /// Gets or sets a value indicating whether the content is on disk already. + /// + public bool IsOnDisk { get; set; } + + /// + /// Gets or sets the content. + /// + /// + /// + /// Getting the content while the template is "on disk" throws, + /// the template must be saved before its content can be retrieved. + /// + /// + /// Setting the content means it is not "on disk" anymore, and the + /// template becomes (and behaves like) a normal template. + /// + /// + public override string? Content { - /// - /// Initializes a new instance of the class. - /// - /// The name of the template. - /// The alias of the template. - public TemplateOnDisk(IShortStringHelper shortStringHelper, string name, string alias) - : base(shortStringHelper, name, alias) + get => IsOnDisk ? string.Empty : base.Content; + set { - IsOnDisk = true; - } - - /// - /// Gets or sets a value indicating whether the content is on disk already. - /// - public bool IsOnDisk { get; set; } - - /// - /// Gets or sets the content. - /// - /// - /// Getting the content while the template is "on disk" throws, - /// the template must be saved before its content can be retrieved. - /// Setting the content means it is not "on disk" anymore, and the - /// template becomes (and behaves like) a normal template. - /// - public override string? Content - { - get - { - return IsOnDisk ? string.Empty : base.Content; - } - set - { - base.Content = value; - IsOnDisk = false; - } + base.Content = value; + IsOnDisk = false; } } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs b/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs index f4f3e7bc59..c94cd67b8a 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery -{ - public class ContentTypeModel - { - public string? Alias { get; set; } +namespace Umbraco.Cms.Core.Models.TemplateQuery; - public string? Name { get; set; } - } +public class ContentTypeModel +{ + public string? Alias { get; set; } + + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/Operator.cs b/src/Umbraco.Core/Models/TemplateQuery/Operator.cs index eb3fe4be29..c76202fb68 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/Operator.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/Operator.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public enum Operator { - public enum Operator - { - Equals = 1, - NotEquals = 2, - Contains = 3, - NotContains = 4, - LessThan = 5, - LessThanEqualTo = 6, - GreaterThan = 7, - GreaterThanEqualTo = 8 - } + Equals = 1, + NotEquals = 2, + Contains = 3, + NotContains = 4, + LessThan = 5, + LessThanEqualTo = 6, + GreaterThan = 7, + GreaterThanEqualTo = 8, } diff --git a/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs b/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs index a8e3b40fef..fc23ebdb3d 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs @@ -1,32 +1,34 @@ -using System; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public static class OperatorFactory { - public static class OperatorFactory + public static Operator FromString(string stringOperator) { - public static Operator FromString(string stringOperator) + if (stringOperator == null) { - if (stringOperator == null) throw new ArgumentNullException(nameof(stringOperator)); + throw new ArgumentNullException(nameof(stringOperator)); + } - switch (stringOperator) - { - case "=": - case "==": - return Operator.Equals; - case "!=": - case "<>": - return Operator.NotEquals; - case "<": - return Operator.LessThan; - case "<=": - return Operator.LessThanEqualTo; - case ">": - return Operator.GreaterThan; - case ">=": - return Operator.GreaterThanEqualTo; - default: - throw new ArgumentException($"A operator cannot be created from the specified string '{stringOperator}'", nameof(stringOperator)); - } + switch (stringOperator) + { + case "=": + case "==": + return Operator.Equals; + case "!=": + case "<>": + return Operator.NotEquals; + case "<": + return Operator.LessThan; + case "<=": + return Operator.LessThanEqualTo; + case ">": + return Operator.GreaterThan; + case ">=": + return Operator.GreaterThanEqualTo; + default: + throw new ArgumentException( + $"A operator cannot be created from the specified string '{stringOperator}'", + nameof(stringOperator)); } } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs b/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs index ce66965c68..d2a8c8e0db 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs @@ -1,25 +1,24 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public class OperatorTerm { - public class OperatorTerm + public OperatorTerm() { - public OperatorTerm() - { - Name = "is"; - Operator = Operator.Equals; - AppliesTo = new [] { "string" }; - } - - public OperatorTerm(string name, Operator @operator, IEnumerable appliesTo) - { - Name = name; - Operator = @operator; - AppliesTo = appliesTo; - } - - public string Name { get; set; } - public Operator Operator { get; set; } - public IEnumerable AppliesTo { get; set; } + Name = "is"; + Operator = Operator.Equals; + AppliesTo = new[] { "string" }; } + + public OperatorTerm(string name, Operator @operator, IEnumerable appliesTo) + { + Name = name; + Operator = @operator; + AppliesTo = appliesTo; + } + + public string Name { get; set; } + + public Operator Operator { get; set; } + + public IEnumerable AppliesTo { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs b/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs index 3ea4059b7e..39ea100e7d 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class PropertyModel { - public class PropertyModel - { - public string? Name { get; set; } + public string? Name { get; set; } - public string Alias { get; set; } = string.Empty; + public string Alias { get; set; } = string.Empty; - public string? Type { get; set; } - } + public string? Type { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs index b6305f16a8..2c64f13876 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class QueryCondition { - public class QueryCondition - { - public PropertyModel Property { get; set; } = new PropertyModel(); - public OperatorTerm Term { get; set; } = new OperatorTerm(); - public string ConstraintValue { get; set; } = string.Empty; - } + public PropertyModel Property { get; set; } = new(); + + public OperatorTerm Term { get; set; } = new(); + + public string ConstraintValue { get; set; } = string.Empty; } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs index 962cf92558..0722422aae 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs @@ -1,75 +1,74 @@ -using System; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class QueryConditionExtensions { - public static class QueryConditionExtensions + private static Lazy StringContainsMethodInfo => + new(() => typeof(string).GetMethod("Contains", new[] { typeof(string) })!); + + public static Expression> BuildCondition(this QueryCondition condition, string parameterAlias) { - private static Lazy StringContainsMethodInfo => - new Lazy(() => typeof(string).GetMethod("Contains", new[] {typeof(string)})!); - - public static Expression> BuildCondition(this QueryCondition condition, string parameterAlias) + object constraintValue; + switch (condition.Property.Type?.ToLowerInvariant()) { - object constraintValue; - switch (condition.Property.Type?.ToLowerInvariant()) - { - case "string": - constraintValue = condition.ConstraintValue; - break; - case "datetime": - constraintValue = DateTime.Parse(condition.ConstraintValue); - break; - case "boolean": - constraintValue = Boolean.Parse(condition.ConstraintValue); - break; - default: - constraintValue = Convert.ChangeType(condition.ConstraintValue, typeof(int)); - break; - } - - var parameterExpression = Expression.Parameter(typeof(T), parameterAlias); - var propertyExpression = Expression.Property(parameterExpression, condition.Property.Alias); - - var valueExpression = Expression.Constant(constraintValue); - Expression bodyExpression; - switch (condition.Term.Operator) - { - case Operator.NotEquals: - bodyExpression = Expression.NotEqual(propertyExpression, valueExpression); - break; - case Operator.GreaterThan: - bodyExpression = Expression.GreaterThan(propertyExpression, valueExpression); - break; - case Operator.GreaterThanEqualTo: - bodyExpression = Expression.GreaterThanOrEqual(propertyExpression, valueExpression); - break; - case Operator.LessThan: - bodyExpression = Expression.LessThan(propertyExpression, valueExpression); - break; - case Operator.LessThanEqualTo: - bodyExpression = Expression.LessThanOrEqual(propertyExpression, valueExpression); - break; - case Operator.Contains: - bodyExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, - valueExpression); - break; - case Operator.NotContains: - var tempExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, - valueExpression); - bodyExpression = Expression.Equal(tempExpression, Expression.Constant(false)); - break; - default: - case Operator.Equals: - bodyExpression = Expression.Equal(propertyExpression, valueExpression); - break; - } - - var predicate = - Expression.Lambda>(bodyExpression.Reduce(), parameterExpression); - - return predicate; + case "string": + constraintValue = condition.ConstraintValue; + break; + case "datetime": + constraintValue = DateTime.Parse(condition.ConstraintValue); + break; + case "boolean": + constraintValue = bool.Parse(condition.ConstraintValue); + break; + default: + constraintValue = Convert.ChangeType(condition.ConstraintValue, typeof(int)); + break; } + + ParameterExpression parameterExpression = Expression.Parameter(typeof(T), parameterAlias); + MemberExpression propertyExpression = Expression.Property(parameterExpression, condition.Property.Alias); + + ConstantExpression valueExpression = Expression.Constant(constraintValue); + Expression bodyExpression; + switch (condition.Term.Operator) + { + case Operator.NotEquals: + bodyExpression = Expression.NotEqual(propertyExpression, valueExpression); + break; + case Operator.GreaterThan: + bodyExpression = Expression.GreaterThan(propertyExpression, valueExpression); + break; + case Operator.GreaterThanEqualTo: + bodyExpression = Expression.GreaterThanOrEqual(propertyExpression, valueExpression); + break; + case Operator.LessThan: + bodyExpression = Expression.LessThan(propertyExpression, valueExpression); + break; + case Operator.LessThanEqualTo: + bodyExpression = Expression.LessThanOrEqual(propertyExpression, valueExpression); + break; + case Operator.Contains: + bodyExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, valueExpression); + break; + case Operator.NotContains: + MethodCallExpression tempExpression = Expression.Call( + propertyExpression, + StringContainsMethodInfo.Value, + valueExpression); + bodyExpression = Expression.Equal(tempExpression, Expression.Constant(false)); + break; + default: + case Operator.Equals: + bodyExpression = Expression.Equal(propertyExpression, valueExpression); + break; + } + + var predicate = + Expression.Lambda>(bodyExpression.Reduce(), parameterExpression); + + return predicate; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs index 48d6506143..06f5c82d19 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public class QueryModel { - public class QueryModel - { - public ContentTypeModel? ContentType { get; set; } - public SourceModel? Source { get; set; } - public IEnumerable? Filters { get; set; } - public SortExpression? Sort { get; set; } - public int Take { get; set; } - } + public ContentTypeModel? ContentType { get; set; } + + public SourceModel? Source { get; set; } + + public IEnumerable? Filters { get; set; } + + public SortExpression? Sort { get; set; } + + public int Take { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs index 8605f92423..61845214a5 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public class QueryResultModel { + public string? QueryExpression { get; set; } - public class QueryResultModel - { + public IEnumerable? SampleResults { get; set; } - public string? QueryExpression { get; set; } - public IEnumerable? SampleResults { get; set; } - public int ResultCount { get; set; } - public long ExecutionTime { get; set; } - public int Take { get; set; } - } + public int ResultCount { get; set; } + + public long ExecutionTime { get; set; } + + public int Take { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs b/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs index c68b366ba5..b5accd7ccd 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery -{ - public class SortExpression - { - public PropertyModel? Property { get; set; } +namespace Umbraco.Cms.Core.Models.TemplateQuery; - public string? Direction { get; set; } - } +public class SortExpression +{ + public PropertyModel? Property { get; set; } + + public string? Direction { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs b/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs index 4b67f7e73c..a36ae38a9e 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class SourceModel { - public class SourceModel - { - public int Id { get; set; } - public string? Name { get; set; } - } + public int Id { get; set; } + + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs b/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs index 95615b4d0d..4e56beb635 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery -{ - public class TemplateQueryResult - { - public string? Icon { get; set; } +namespace Umbraco.Cms.Core.Models.TemplateQuery; - public string? Name { get; set; } - } +public class TemplateQueryResult +{ + public string? Icon { get; set; } + + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs b/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs index c89fb402d0..7fdcc4f76f 100644 --- a/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs @@ -1,54 +1,52 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Trees +namespace Umbraco.Cms.Core.Models.Trees; + +/// +/// +/// A menu item that represents some JS that needs to execute when the menu item is clicked. +/// +/// +/// These types of menu items are rare but they do exist. Things like refresh node simply execute +/// JS and don't launch a dialog. +/// Each action menu item describes what angular service that it's method exists in and what the method name is. +/// An action menu item must describe the angular service name for which it's method exists. It may also define what +/// the +/// method name is that will be called in this service but if one is not specified then we will assume the method name +/// is the +/// same as the Type name of the current action menu class. +/// +public abstract class ActionMenuItem : MenuItem { - /// - /// - /// A menu item that represents some JS that needs to execute when the menu item is clicked. - /// - /// - /// These types of menu items are rare but they do exist. Things like refresh node simply execute - /// JS and don't launch a dialog. - /// Each action menu item describes what angular service that it's method exists in and what the method name is. - /// An action menu item must describe the angular service name for which it's method exists. It may also define what the - /// method name is that will be called in this service but if one is not specified then we will assume the method name is the - /// same as the Type name of the current action menu class. - /// - public abstract class ActionMenuItem : MenuItem + protected ActionMenuItem(string alias, string name) + : base(alias, name) => Initialize(); + + protected ActionMenuItem(string alias, ILocalizedTextService textService) + : base(alias, textService) => + Initialize(); + + /// + /// The angular service name containing the + /// + public abstract string AngularServiceName { get; } + + /// + /// The angular service method name to call for this menu item + /// + public virtual string? AngularServiceMethodName { get; } = null; + + private void Initialize() { - /// - /// The angular service name containing the - /// - public abstract string AngularServiceName { get; } - - /// - /// The angular service method name to call for this menu item - /// - public virtual string? AngularServiceMethodName { get; } = null; - - protected ActionMenuItem(string alias, string name) : base(alias, name) + // add the current type to the metadata + if (AngularServiceMethodName.IsNullOrWhiteSpace()) { - Initialize(); + // if no method name is supplied we will assume that the menu action is the type name of the current menu class + ExecuteJsMethod($"{AngularServiceName}.{GetType().Name}"); } - - protected ActionMenuItem(string alias, ILocalizedTextService textService) : base(alias, textService) + else { - Initialize(); - } - - private void Initialize() - { - //add the current type to the metadata - if (AngularServiceMethodName.IsNullOrWhiteSpace()) - { - //if no method name is supplied we will assume that the menu action is the type name of the current menu class - ExecuteJsMethod($"{AngularServiceName}.{this.GetType().Name}"); - } - else - { - ExecuteJsMethod($"{AngularServiceName}.{AngularServiceMethodName}"); - } + ExecuteJsMethod($"{AngularServiceName}.{AngularServiceMethodName}"); } } } diff --git a/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs b/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs index a8d945242e..7b3d8f2932 100644 --- a/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs +++ b/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs @@ -1,27 +1,31 @@ using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Trees -{ - /// +namespace Umbraco.Cms.Core.Models.Trees; + +/// /// Represents the refresh node menu item - /// - public sealed class CreateChildEntity : ActionMenuItem +/// +public sealed class CreateChildEntity : ActionMenuItem +{ + private const string icon = "icon-add"; + + public CreateChildEntity(string name, bool separatorBefore = false) + : base(ActionNew.ActionAlias, name) { - public override string AngularServiceName => "umbracoMenuActions"; - - public CreateChildEntity(string name, bool separatorBefore = false) - : base(ActionNew.ActionAlias, name) - { - Icon = "add"; Name = name; - SeparatorBefore = separatorBefore; - } - - public CreateChildEntity(ILocalizedTextService textService, bool separatorBefore = false) - : base(ActionNew.ActionAlias, textService) - { - Icon = "add"; - SeparatorBefore = separatorBefore; - } + Icon = icon; + Name = name; + SeparatorBefore = separatorBefore; + UseLegacyIcon = false; } + + public CreateChildEntity(ILocalizedTextService textService, bool separatorBefore = false) + : base(ActionNew.ActionAlias, textService) + { + Icon = icon; + SeparatorBefore = separatorBefore; + UseLegacyIcon = false; + } + + public override string AngularServiceName => "umbracoMenuActions"; } diff --git a/src/Umbraco.Core/Models/Trees/ExportMember.cs b/src/Umbraco.Core/Models/Trees/ExportMember.cs index 30f904f952..2039a827cf 100644 --- a/src/Umbraco.Core/Models/Trees/ExportMember.cs +++ b/src/Umbraco.Core/Models/Trees/ExportMember.cs @@ -1,17 +1,18 @@ using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Trees -{ - /// - /// Represents the export member menu item - /// - public sealed class ExportMember : ActionMenuItem - { - public override string AngularServiceName => "umbracoMenuActions"; +namespace Umbraco.Cms.Core.Models.Trees; - public ExportMember(ILocalizedTextService textService) : base("export", textService) - { - Icon = "download-alt"; - } +/// +/// Represents the export member menu item +/// +public sealed class ExportMember : ActionMenuItem +{ + public ExportMember(ILocalizedTextService textService) + : base("export", textService) + { + Icon = "icon-download-alt"; + UseLegacyIcon = false; } + + public override string AngularServiceName => "umbracoMenuActions"; } diff --git a/src/Umbraco.Core/Models/Trees/MenuItem.cs b/src/Umbraco.Core/Models/Trees/MenuItem.cs index e56a2440a8..e99e0d598a 100644 --- a/src/Umbraco.Core/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/MenuItem.cs @@ -1,213 +1,208 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -using System.Threading; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Trees; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Trees -{ - /// +namespace Umbraco.Cms.Core.Models.Trees; + +/// /// A context menu item - /// - [DataContract(Name = "menuItem", Namespace = "")] - public class MenuItem +/// +[DataContract(Name = "menuItem", Namespace = "")] +public class MenuItem +{ + #region Constructors + + public MenuItem() { - #region Constructors - public MenuItem() - { - AdditionalData = new Dictionary(); - Icon = "folder"; - } + AdditionalData = new Dictionary(); + Icon = Constants.Icons.Folder; + } - public MenuItem(string alias, string name) - : this() - { - Alias = alias; - Name = name; - } + public MenuItem(string alias, string name) + : this() + { + Alias = alias; + Name = name; + } - public MenuItem(string alias, ILocalizedTextService textService) - : this() - { - Alias = alias; - Name = textService.Localize("actions", Alias); + public MenuItem(string alias, ILocalizedTextService textService) + : this() + { + Alias = alias; + Name = textService.Localize("actions", Alias); TextDescription = textService.Localize("visuallyHiddenTexts", alias + "_description", Thread.CurrentThread.CurrentUICulture); - } + } - /// + /// /// Create a menu item based on an definition - /// - /// - /// - public MenuItem(IAction action, string name = "") - : this() - { - Name = name.IsNullOrWhiteSpace() ? action.Alias : name; - Alias = action.Alias; - SeparatorBefore = false; - Icon = action.Icon; - Action = action; - } - #endregion + /// + /// + /// + public MenuItem(IAction action, string name = "") + : this() + { + Name = name.IsNullOrWhiteSpace() ? action.Alias : name; + Alias = action.Alias; + SeparatorBefore = false; + Icon = action.Icon; + Action = action; + } - #region Properties - [IgnoreDataMember] - public IAction? Action { get; set; } + #endregion - /// - /// A dictionary to support any additional meta data that should be rendered for the node which is - /// useful for custom action commands such as 'create', 'copy', etc... - /// - /// - /// We will also use the meta data collection for dealing with legacy menu items (i.e. for loading custom URLs or - /// executing custom JS). - /// - [DataMember(Name = "metaData")] - public Dictionary AdditionalData { get; private set; } + #region Properties - [DataMember(Name = "name", IsRequired = true)] - [Required] - public string? Name { get; set; } + [IgnoreDataMember] + public IAction? Action { get; set; } - [DataMember(Name = "alias", IsRequired = true)] - [Required] - public string? Alias { get; set; } + /// + /// A dictionary to support any additional meta data that should be rendered for the node which is + /// useful for custom action commands such as 'create', 'copy', etc... + /// + /// + /// We will also use the meta data collection for dealing with legacy menu items (i.e. for loading custom URLs or + /// executing custom JS). + /// + [DataMember(Name = "metaData")] + public Dictionary AdditionalData { get; private set; } - [DataMember(Name = "textDescription")] - public string? TextDescription { get; set; } + [DataMember(Name = "name", IsRequired = true)] + [Required] + public string? Name { get; set; } - /// + [DataMember(Name = "alias", IsRequired = true)] + [Required] + public string? Alias { get; set; } + + [DataMember(Name = "textDescription")] + public string? TextDescription { get; set; } + + /// /// Ensures a menu separator will exist before this menu item - /// - [DataMember(Name = "separator")] - public bool SeparatorBefore { get; set; } + /// + [DataMember(Name = "separator")] + public bool SeparatorBefore { get; set; } - [DataMember(Name = "cssclass")] - public string Icon { get; set; } + /// + /// Icon to use at action menu item. + /// + [DataMember(Name = "icon")] + public string Icon { get; set; } + + /// + /// Used in the UI to indicate whether icons should be prefixed with "icon-". + /// If not legacy icon full icon name should be specified. + /// + [DataMember(Name = "useLegacyIcon")] + public bool UseLegacyIcon { get; set; } = true; /// /// Used in the UI to inform the user that the menu item will open a dialog/confirmation - /// - [DataMember(Name = "opensDialog")] - public bool OpensDialog { get; set; } + /// + [DataMember(Name = "opensDialog")] + public bool OpensDialog { get; set; } - #endregion + #endregion - #region Constants + #region Constants - /// - /// Used as a key for the AdditionalData to specify a specific dialog title instead of the menu title - /// - internal const string DialogTitleKey = "dialogTitle"; + /// + /// Used as a key for the AdditionalData to specify a specific dialog title instead of the menu title + /// + internal const string DialogTitleKey = "dialogTitle"; - /// - /// Used to specify the URL that the dialog will launch to in an iframe - /// - internal const string ActionUrlKey = "actionUrl"; + /// + /// Used to specify the URL that the dialog will launch to in an iframe + /// + internal const string ActionUrlKey = "actionUrl"; - // TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with - // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog - // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, - // though would be v-easy, just not sure we want to ever support that? - internal const string ActionUrlMethodKey = "actionUrlMethod"; + // TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with + // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog + // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, + // though would be v-easy, just not sure we want to ever support that? + internal const string ActionUrlMethodKey = "actionUrlMethod"; - /// - /// Used to specify the angular view that the dialog will launch - /// - internal const string ActionViewKey = "actionView"; + /// + /// Used to specify the angular view that the dialog will launch + /// + internal const string ActionViewKey = "actionView"; - /// - /// Used to specify the js method to execute for the menu item - /// - internal const string JsActionKey = "jsAction"; + /// + /// Used to specify the js method to execute for the menu item + /// + internal const string JsActionKey = "jsAction"; - /// - /// Used to specify an angular route to go to for the menu item - /// - internal const string ActionRouteKey = "actionRoute"; + /// + /// Used to specify an angular route to go to for the menu item + /// + internal const string ActionRouteKey = "actionRoute"; - #endregion + #endregion - #region Methods + #region Methods - /// - /// Sets the menu item to navigate to the specified angular route path - /// - /// - public void NavigateToRoute(string route) - { - AdditionalData[ActionRouteKey] = route; - } + /// + /// Sets the menu item to navigate to the specified angular route path + /// + /// + public void NavigateToRoute(string route) => AdditionalData[ActionRouteKey] = route; - /// - /// Adds the required meta data to the menu item so that angular knows to attempt to call the Js method. - /// - /// - public void ExecuteJsMethod(string jsToExecute) - { - SetJsAction(jsToExecute); - } + /// + /// Adds the required meta data to the menu item so that angular knows to attempt to call the Js method. + /// + /// + public void ExecuteJsMethod(string jsToExecute) => SetJsAction(jsToExecute); - /// - /// Sets the menu item to display a dialog based on an angular view path - /// - /// - /// - public void LaunchDialogView(string view, string dialogTitle) - { - SetDialogTitle(dialogTitle); - SetActionView(view); - } - - /// - /// Sets the menu item to display a dialog based on a URL path in an iframe - /// - /// - /// - public void LaunchDialogUrl(string url, string dialogTitle) - { - SetDialogTitle(dialogTitle); - SetActionUrl(url); - } - - private void SetJsAction(string jsToExecute) - { - AdditionalData[JsActionKey] = jsToExecute; - } - - /// - /// Puts a dialog title into the meta data to be displayed on the dialog of the menu item (if there is one) - /// instead of the menu name - /// - /// - private void SetDialogTitle(string dialogTitle) - { - AdditionalData[DialogTitleKey] = dialogTitle; - } - - /// - /// Configures the menu item to launch a specific view - /// - /// - private void SetActionView(string view) - { - AdditionalData[ActionViewKey] = view; - } - - /// - /// Configures the menu item to launch a URL with the specified action (dialog or new window) - /// - /// - /// - private void SetActionUrl(string url, ActionUrlMethod method = ActionUrlMethod.Dialog) - { - AdditionalData[ActionUrlKey] = url; - AdditionalData[ActionUrlMethodKey] = method; - } - - #endregion + /// + /// Sets the menu item to display a dialog based on an angular view path + /// + /// + /// + public void LaunchDialogView(string view, string dialogTitle) + { + SetDialogTitle(dialogTitle); + SetActionView(view); } + + /// + /// Sets the menu item to display a dialog based on a URL path in an iframe + /// + /// + /// + public void LaunchDialogUrl(string url, string dialogTitle) + { + SetDialogTitle(dialogTitle); + SetActionUrl(url); + } + + private void SetJsAction(string jsToExecute) => AdditionalData[JsActionKey] = jsToExecute; + + /// + /// Puts a dialog title into the meta data to be displayed on the dialog of the menu item (if there is one) + /// instead of the menu name + /// + /// + private void SetDialogTitle(string dialogTitle) => AdditionalData[DialogTitleKey] = dialogTitle; + + /// + /// Configures the menu item to launch a specific view + /// + /// + private void SetActionView(string view) => AdditionalData[ActionViewKey] = view; + + /// + /// Configures the menu item to launch a URL with the specified action (dialog or new window) + /// + /// + /// + private void SetActionUrl(string url, ActionUrlMethod method = ActionUrlMethod.Dialog) + { + AdditionalData[ActionUrlKey] = url; + AdditionalData[ActionUrlMethodKey] = method; + } + + #endregion } diff --git a/src/Umbraco.Core/Models/Trees/RefreshNode.cs b/src/Umbraco.Core/Models/Trees/RefreshNode.cs index 01eb2fa34a..c10cdfcdf5 100644 --- a/src/Umbraco.Core/Models/Trees/RefreshNode.cs +++ b/src/Umbraco.Core/Models/Trees/RefreshNode.cs @@ -1,27 +1,30 @@ using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Trees +namespace Umbraco.Cms.Core.Models.Trees; + +/// +/// +/// Represents the refresh node menu item +/// +public sealed class RefreshNode : ActionMenuItem { - /// - /// - /// Represents the refresh node menu item - /// - public sealed class RefreshNode : ActionMenuItem + private const string icon = "icon-refresh"; + + public RefreshNode(string name, bool separatorBefore = false) + : base("refreshNode", name) { - public override string AngularServiceName => "umbracoMenuActions"; - - public RefreshNode(string name, bool separatorBefore = false) - : base("refreshNode", name) - { - Icon = "refresh"; - SeparatorBefore = separatorBefore; - } - - public RefreshNode(ILocalizedTextService textService, bool separatorBefore = false) - : base("refreshNode", textService) - { - Icon = "refresh"; - SeparatorBefore = separatorBefore; - } + Icon = icon; + SeparatorBefore = separatorBefore; + UseLegacyIcon = false; } + + public RefreshNode(ILocalizedTextService textService, bool separatorBefore = false) + : base("refreshNode", textService) + { + Icon = icon; + SeparatorBefore = separatorBefore; + UseLegacyIcon = false; + } + + public override string AngularServiceName => "umbracoMenuActions"; } diff --git a/src/Umbraco.Core/Models/TwoFactorLogin.cs b/src/Umbraco.Core/Models/TwoFactorLogin.cs index c38105626c..551482e3a2 100644 --- a/src/Umbraco.Core/Models/TwoFactorLogin.cs +++ b/src/Umbraco.Core/Models/TwoFactorLogin.cs @@ -1,13 +1,14 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class TwoFactorLogin : EntityBase, ITwoFactorLogin { - public class TwoFactorLogin : EntityBase, ITwoFactorLogin - { - public string ProviderName { get; set; } = null!; - public string Secret { get; set; } = null!; - public Guid UserOrMemberKey { get; set; } - public bool Confirmed { get; set; } - } + public bool Confirmed { get; set; } + + public string ProviderName { get; set; } = null!; + + public string Secret { get; set; } = null!; + + public Guid UserOrMemberKey { get; set; } } diff --git a/src/Umbraco.Core/Models/UmbracoDomain.cs b/src/Umbraco.Core/Models/UmbracoDomain.cs index 3f2eb00f51..c883e14770 100644 --- a/src/Umbraco.Core/Models/UmbracoDomain.cs +++ b/src/Umbraco.Core/Models/UmbracoDomain.cs @@ -1,54 +1,47 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class UmbracoDomain : EntityBase, IDomain { - [Serializable] - [DataContract(IsReference = true)] - public class UmbracoDomain : EntityBase, IDomain + private int? _contentId; + private string _domainName; + private int? _languageId; + + public UmbracoDomain(string domainName) => _domainName = domainName; + + public UmbracoDomain(string domainName, string languageIsoCode) + : this(domainName) => + LanguageIsoCode = languageIsoCode; + + [DataMember] + public int? LanguageId { - public UmbracoDomain(string domainName) - { - _domainName = domainName; - } - - public UmbracoDomain(string domainName, string languageIsoCode) - : this(domainName) - { - LanguageIsoCode = languageIsoCode; - } - - private int? _contentId; - private int? _languageId; - private string _domainName; - - [DataMember] - public int? LanguageId - { - get => _languageId; - set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); - } - - [DataMember] - public string DomainName - { - get => _domainName; - set => SetPropertyValueAndDetectChanges(value, ref _domainName!, nameof(DomainName)); - } - - [DataMember] - public int? RootContentId - { - get => _contentId; - set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(RootContentId)); - } - - public bool IsWildcard => string.IsNullOrWhiteSpace(DomainName) || DomainName.StartsWith("*"); - - /// - /// Readonly value of the language ISO code for the domain - /// - public string? LanguageIsoCode { get; set; } + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); } + + [DataMember] + public string DomainName + { + get => _domainName; + set => SetPropertyValueAndDetectChanges(value, ref _domainName!, nameof(DomainName)); + } + + [DataMember] + public int? RootContentId + { + get => _contentId; + set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(RootContentId)); + } + + public bool IsWildcard => string.IsNullOrWhiteSpace(DomainName) || DomainName.StartsWith("*"); + + /// + /// Readonly value of the language ISO code for the domain + /// + public string? LanguageIsoCode { get; set; } } diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 00dbd490f8..600927db84 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -1,178 +1,175 @@ -using Umbraco.Cms.Core.CodeAnnotations; +using Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Enum used to represent the Umbraco Object Types and their associated GUIDs +/// +public enum UmbracoObjectTypes { /// - /// Enum used to represent the Umbraco Object Types and their associated GUIDs + /// Default value /// - public enum UmbracoObjectTypes - { - /// - /// Default value - /// - Unknown, + Unknown, + /// + /// Root + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.SystemRoot)] + [FriendlyName("Root")] + ROOT, - /// - /// Root - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.SystemRoot)] - [FriendlyName("Root")] - ROOT, + /// + /// Document + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Document, typeof(IContent))] + [FriendlyName("Document")] + [UmbracoUdiType(Constants.UdiEntityType.Document)] + Document, - /// - /// Document - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Document, typeof(IContent))] - [FriendlyName("Document")] - [UmbracoUdiType(Constants.UdiEntityType.Document)] - Document, + /// + /// Media + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Media, typeof(IMedia))] + [FriendlyName("Media")] + [UmbracoUdiType(Constants.UdiEntityType.Media)] + Media, - /// - /// Media - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Media, typeof(IMedia))] - [FriendlyName("Media")] - [UmbracoUdiType(Constants.UdiEntityType.Media)] - Media, + /// + /// Member Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberType, typeof(IMemberType))] + [FriendlyName("Member Type")] + [UmbracoUdiType(Constants.UdiEntityType.MemberType)] + MemberType, - /// - /// Member Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberType, typeof(IMemberType))] - [FriendlyName("Member Type")] - [UmbracoUdiType(Constants.UdiEntityType.MemberType)] - MemberType, + /// + /// Template + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Template, typeof(ITemplate))] + [FriendlyName("Template")] + [UmbracoUdiType(Constants.UdiEntityType.Template)] + Template, - /// - /// Template - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Template, typeof(ITemplate))] - [FriendlyName("Template")] - [UmbracoUdiType(Constants.UdiEntityType.Template)] - Template, + /// + /// Member Group + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup)] + [FriendlyName("Member Group")] + [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] + MemberGroup, - /// - /// Member Group - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup)] - [FriendlyName("Member Group")] - [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] - MemberGroup, + /// + /// "Media Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaType, typeof(IMediaType))] + [FriendlyName("Media Type")] + [UmbracoUdiType(Constants.UdiEntityType.MediaType)] + MediaType, - /// - /// "Media Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaType, typeof(IMediaType))] - [FriendlyName("Media Type")] - [UmbracoUdiType(Constants.UdiEntityType.MediaType)] - MediaType, + /// + /// Document Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentType, typeof(IContentType))] + [FriendlyName("Document Type")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentType)] + DocumentType, - /// - /// Document Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentType, typeof(IContentType))] - [FriendlyName("Document Type")] - [UmbracoUdiType(Constants.UdiEntityType.DocumentType)] - DocumentType, + /// + /// Recycle Bin + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.ContentRecycleBin)] + [FriendlyName("Recycle Bin")] + RecycleBin, - /// - /// Recycle Bin - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.ContentRecycleBin)] - [FriendlyName("Recycle Bin")] - RecycleBin, + /// + /// Member + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Member, typeof(IMember))] + [FriendlyName("Member")] + [UmbracoUdiType(Constants.UdiEntityType.Member)] + Member, - /// - /// Member - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Member, typeof(IMember))] - [FriendlyName("Member")] - [UmbracoUdiType(Constants.UdiEntityType.Member)] - Member, + /// + /// Data Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DataType, typeof(IDataType))] + [FriendlyName("Data Type")] + [UmbracoUdiType(Constants.UdiEntityType.DataType)] + DataType, - /// - /// Data Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DataType, typeof(IDataType))] - [FriendlyName("Data Type")] - [UmbracoUdiType(Constants.UdiEntityType.DataType)] - DataType, + /// + /// Document type container + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentTypeContainer)] + [FriendlyName("Document Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentTypeContainer)] + DocumentTypeContainer, - /// - /// Document type container - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentTypeContainer)] - [FriendlyName("Document Type Container")] - [UmbracoUdiType(Constants.UdiEntityType.DocumentTypeContainer)] - DocumentTypeContainer, + /// + /// Media type container + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaTypeContainer)] + [FriendlyName("Media Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.MediaTypeContainer)] + MediaTypeContainer, - /// - /// Media type container - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaTypeContainer)] - [FriendlyName("Media Type Container")] - [UmbracoUdiType(Constants.UdiEntityType.MediaTypeContainer)] - MediaTypeContainer, + /// + /// Media type container + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DataTypeContainer)] + [FriendlyName("Data Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] + DataTypeContainer, - /// - /// Media type container - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DataTypeContainer)] - [FriendlyName("Data Type Container")] - [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] - DataTypeContainer, + /// + /// Relation type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.RelationType)] + [FriendlyName("Relation Type")] + [UmbracoUdiType(Constants.UdiEntityType.RelationType)] + RelationType, - /// - /// Relation type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.RelationType)] - [FriendlyName("Relation Type")] - [UmbracoUdiType(Constants.UdiEntityType.RelationType)] - RelationType, + /// + /// Forms Form + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsForm)] + [FriendlyName("Form")] + FormsForm, - /// - /// Forms Form - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsForm)] - [FriendlyName("Form")] - FormsForm, + /// + /// Forms PreValue + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsPreValue)] + [FriendlyName("PreValue")] + FormsPreValue, - /// - /// Forms PreValue - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsPreValue)] - [FriendlyName("PreValue")] - FormsPreValue, + /// + /// Forms DataSource + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsDataSource)] + [FriendlyName("DataSource")] + FormsDataSource, - /// - /// Forms DataSource - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsDataSource)] - [FriendlyName("DataSource")] - FormsDataSource, + /// + /// Language + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Language)] + [FriendlyName("Language")] + Language, - /// - /// Language - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Language)] - [FriendlyName("Language")] - Language, + /// + /// Document Blueprint + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentBlueprint, typeof(IContent))] + [FriendlyName("DocumentBlueprint")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentBlueprint)] + DocumentBlueprint, - /// - /// Document Blueprint - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentBlueprint, typeof(IContent))] - [FriendlyName("DocumentBlueprint")] - [UmbracoUdiType(Constants.UdiEntityType.DocumentBlueprint)] - DocumentBlueprint, - - /// - /// Reserved Identifier - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.IdReservation)] - [FriendlyName("Identifier Reservation")] - IdReservation - - } + /// + /// Reserved Identifier + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.IdReservation)] + [FriendlyName("Identifier Reservation")] + IdReservation, } diff --git a/src/Umbraco.Core/Models/UmbracoUserExtensions.cs b/src/Umbraco.Core/Models/UmbracoUserExtensions.cs index 71612f3531..d708704fac 100644 --- a/src/Umbraco.Core/Models/UmbracoUserExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoUserExtensions.cs @@ -1,79 +1,90 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoUserExtensions { - public static class UmbracoUserExtensions + public static IEnumerable GetPermissions(this IUser user, string path, IUserService userService) => + userService.GetPermissionsForPath(user, path).GetAllPermissions(); + + public static bool HasSectionAccess(this IUser user, string app) { - public static IEnumerable GetPermissions(this IUser user, string path, IUserService userService) + IEnumerable apps = user.AllowedSections; + return apps.Any(uApp => uApp.InvariantEquals(app)); + } + + /// + /// Determines whether this user is the 'super' user. + /// + public static bool IsSuper(this IUser user) + { + if (user == null) { - return userService.GetPermissionsForPath(user, path).GetAllPermissions(); + throw new ArgumentNullException(nameof(user)); } - public static bool HasSectionAccess(this IUser user, string app) + return user.Id == Constants.Security.SuperUserId; + } + + /// + /// Determines whether this user belongs to the administrators group. + /// + /// The 'super' user does not automatically belongs to the administrators group. + public static bool IsAdmin(this IUser user) + { + if (user == null) { - var apps = user.AllowedSections; - return apps.Any(uApp => uApp.InvariantEquals(app)); + throw new ArgumentNullException(nameof(user)); } - /// - /// Determines whether this user is the 'super' user. - /// - public static bool IsSuper(this IUser user) + return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); + } + + /// + /// Returns the culture info associated with this user, based on the language they're assigned to in the back office + /// + /// + /// + /// + /// + public static CultureInfo GetUserCulture(this IUser user, ILocalizedTextService textService, GlobalSettings globalSettings) + { + if (user == null) { - if (user == null) throw new ArgumentNullException(nameof(user)); - return user.Id == Constants.Security.SuperUserId; + throw new ArgumentNullException(nameof(user)); } - /// - /// Determines whether this user belongs to the administrators group. - /// - /// The 'super' user does not automatically belongs to the administrators group. - public static bool IsAdmin(this IUser user) + if (textService == null) { - if (user == null) throw new ArgumentNullException(nameof(user)); - return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); + throw new ArgumentNullException(nameof(textService)); } - /// - /// Returns the culture info associated with this user, based on the language they're assigned to in the back office - /// - /// - /// - /// - /// - public static CultureInfo GetUserCulture(this IUser user, ILocalizedTextService textService, GlobalSettings globalSettings) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (textService == null) throw new ArgumentNullException(nameof(textService)); - return GetUserCulture(user.Language, textService, globalSettings); - } + return GetUserCulture(user.Language, textService, globalSettings); + } - public static CultureInfo GetUserCulture(string? userLanguage, ILocalizedTextService textService, GlobalSettings globalSettings) + public static CultureInfo GetUserCulture(string? userLanguage, ILocalizedTextService textService, GlobalSettings globalSettings) + { + try { - try - { - var culture = CultureInfo.GetCultureInfo(userLanguage!.Replace("_", "-")); - // TODO: This is a hack because we store the user language as 2 chars instead of the full culture - // which is actually stored in the language files (which are also named with 2 chars!) so we need to attempt - // to convert to a supported full culture - var result = textService.ConvertToSupportedCultureWithRegionCode(culture); - return result; - } - catch (CultureNotFoundException) - { - //return the default one - return CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage); - } + var culture = CultureInfo.GetCultureInfo(userLanguage!.Replace("_", "-")); + + // TODO: This is a hack because we store the user language as 2 chars instead of the full culture + // which is actually stored in the language files (which are also named with 2 chars!) so we need to attempt + // to convert to a supported full culture + CultureInfo result = textService.ConvertToSupportedCultureWithRegionCode(culture); + return result; + } + catch (CultureNotFoundException) + { + // return the default one + return CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage); } } } diff --git a/src/Umbraco.Core/Models/UnLinkLoginModel.cs b/src/Umbraco.Core/Models/UnLinkLoginModel.cs index d8c9920c5e..c121230810 100644 --- a/src/Umbraco.Core/Models/UnLinkLoginModel.cs +++ b/src/Umbraco.Core/Models/UnLinkLoginModel.cs @@ -1,16 +1,15 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ - public class UnLinkLoginModel - { - [Required] - [DataMember(Name = "loginProvider", IsRequired = true)] - public string? LoginProvider { get; set; } +namespace Umbraco.Cms.Core.Models; - [Required] - [DataMember(Name = "providerKey", IsRequired = true)] - public string? ProviderKey { get; set; } - } +public class UnLinkLoginModel +{ + [Required] + [DataMember(Name = "loginProvider", IsRequired = true)] + public string? LoginProvider { get; set; } + + [Required] + [DataMember(Name = "providerKey", IsRequired = true)] + public string? ProviderKey { get; set; } } diff --git a/src/Umbraco.Core/Models/UpgradeCheckResponse.cs b/src/Umbraco.Core/Models/UpgradeCheckResponse.cs index 3238720541..b639616524 100644 --- a/src/Umbraco.Core/Models/UpgradeCheckResponse.cs +++ b/src/Umbraco.Core/Models/UpgradeCheckResponse.cs @@ -2,26 +2,28 @@ using System.Net; using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "upgrade", Namespace = "")] +public class UpgradeCheckResponse { - [DataContract(Name = "upgrade", Namespace = "")] - public class UpgradeCheckResponse + public UpgradeCheckResponse() { - [DataMember(Name = "type")] - public string? Type { get; set; } - - [DataMember(Name = "comment")] - public string? Comment { get; set; } - - [DataMember(Name = "url")] - public string? Url { get; set; } - - public UpgradeCheckResponse() { } - public UpgradeCheckResponse(string upgradeType, string upgradeComment, string upgradeUrl, IUmbracoVersion umbracoVersion) - { - Type = upgradeType; - Comment = upgradeComment; - Url = upgradeUrl + "?version=" + WebUtility.UrlEncode(umbracoVersion.Version?.ToString(3)); - } } + + public UpgradeCheckResponse(string upgradeType, string upgradeComment, string upgradeUrl, IUmbracoVersion umbracoVersion) + { + Type = upgradeType; + Comment = upgradeComment; + Url = upgradeUrl + "?version=" + WebUtility.UrlEncode(umbracoVersion.Version?.ToString(3)); + } + + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "comment")] + public string? Comment { get; set; } + + [DataMember(Name = "url")] + public string? Url { get; set; } } diff --git a/src/Umbraco.Core/Models/UsageInformation.cs b/src/Umbraco.Core/Models/UsageInformation.cs index e2bedd6f0f..3de3a1201a 100644 --- a/src/Umbraco.Core/Models/UsageInformation.cs +++ b/src/Umbraco.Core/Models/UsageInformation.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class UsageInformation { - [DataContract] - public class UsageInformation + public UsageInformation(string name, object data) { - [DataMember(Name = "name")] - public string Name { get; } - - [DataMember(Name = "data")] - public object Data { get; } - - public UsageInformation(string name, object data) - { - Name = name; - Data = data; - } + Name = name; + Data = data; } + + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "data")] + public object Data { get; } } diff --git a/src/Umbraco.Core/Models/UserData.cs b/src/Umbraco.Core/Models/UserData.cs index 07b45b3c54..144871c3f7 100644 --- a/src/Umbraco.Core/Models/UserData.cs +++ b/src/Umbraco.Core/Models/UserData.cs @@ -1,19 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class UserData { - [DataContract] - public class UserData + public UserData(string name, string data) { - [DataMember(Name = "name")] - public string Name { get; } - [DataMember(Name = "data")] - public string Data { get; } - - public UserData(string name, string data) - { - Name = name; - Data = data; - } + Name = name; + Data = data; } + + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "data")] + public string Data { get; } } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 924b11bcc4..f17f6e4de0 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Security.Cryptography; using Umbraco.Cms.Core.Cache; @@ -12,285 +9,376 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public static class UserExtensions { - public static class UserExtensions + /// + /// Tries to lookup the user's Gravatar to see if the endpoint can be reached, if so it returns the valid URL + /// + /// + /// + /// + /// + /// + /// A list of 5 different sized avatar URLs + /// + public static string[] GetUserAvatarUrls(this IUser user, IAppCache cache, MediaFileManager mediaFileManager, IImageUrlGenerator imageUrlGenerator) { - /// - /// Tries to lookup the user's Gravatar to see if the endpoint can be reached, if so it returns the valid URL - /// - /// - /// - /// - /// - /// A list of 5 different sized avatar URLs - /// - public static string[] GetUserAvatarUrls(this IUser user, IAppCache cache, MediaFileManager mediaFileManager, IImageUrlGenerator imageUrlGenerator) + // If FIPS is required, never check the Gravatar service as it only supports MD5 hashing. + // Unfortunately, if the FIPS setting is enabled on Windows, using MD5 will throw an exception + // and the website will not run. + // Also, check if the user has explicitly removed all avatars including a Gravatar, this will be possible and the value will be "none" + if (user.Avatar == "none" || CryptoConfig.AllowOnlyFipsAlgorithms) { - // If FIPS is required, never check the Gravatar service as it only supports MD5 hashing. - // Unfortunately, if the FIPS setting is enabled on Windows, using MD5 will throw an exception - // and the website will not run. - // Also, check if the user has explicitly removed all avatars including a Gravatar, this will be possible and the value will be "none" - if (user.Avatar == "none" || CryptoConfig.AllowOnlyFipsAlgorithms) - { - return new string[0]; - } + return new string[0]; + } - if (user.Avatar.IsNullOrWhiteSpace()) - { - var gravatarHash = user.Email?.GenerateHash(); - var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; + if (user.Avatar.IsNullOrWhiteSpace()) + { + var gravatarHash = user.Email?.GenerateHash(); + var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; - //try Gravatar - var gravatarAccess = cache.GetCacheItem("UserAvatar" + user.Id, () => + // try Gravatar + var gravatarAccess = cache.GetCacheItem("UserAvatar" + user.Id, () => + { + // Test if we can reach this URL, will fail when there's network or firewall errors + var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); + + // Require response within 10 seconds + request.Timeout = 10000; + try { - // Test if we can reach this URL, will fail when there's network or firewall errors - var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); - // Require response within 10 seconds - request.Timeout = 10000; - try + using ((HttpWebResponse)request.GetResponse()) { - using ((HttpWebResponse)request.GetResponse()) { } } - catch (Exception) - { - // There was an HTTP or other error, return an null instead - return false; - } - return true; - }); - - if (gravatarAccess) + } + catch (Exception) { - return new[] - { - gravatarUrl + "&s=30", - gravatarUrl + "&s=60", - gravatarUrl + "&s=90", - gravatarUrl + "&s=150", - gravatarUrl + "&s=300" - }; + // There was an HTTP or other error, return an null instead + return false; } - return new string[0]; + return true; + }); + + if (gravatarAccess) + { + return new[] + { + gravatarUrl + "&s=30", gravatarUrl + "&s=60", gravatarUrl + "&s=90", gravatarUrl + "&s=150", + gravatarUrl + "&s=300", + }; } - //use the custom avatar - var avatarUrl = mediaFileManager.FileSystem.GetUrl(user.Avatar); - return new[] + return new string[0]; + } + + // use the custom avatar + var avatarUrl = mediaFileManager.FileSystem.GetUrl(user.Avatar); + return new[] + { + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 30, Height = 30 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 60, Height = 60 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 90, Height = 90 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 150, Height = 150 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 300, Height = 300 }), - }.WhereNotNull().ToArray(); - - } - - - - internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - return ContentPermissions.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - public static bool HasPathAccess(this IUser user, IMedia? media, IEntityService entityService, AppCaches appCaches) - { - if (media == null) throw new ArgumentNullException(nameof(media)); - return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissions.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - /// - /// Determines whether this user has access to view sensitive data - /// - /// - public static bool HasAccessToSensitiveData(this IUser user) - { - if (user == null) throw new ArgumentNullException("user"); - return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); - } - - /// - /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin - /// - public static int[]? CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) - { - var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => + ImageCropMode = ImageCropMode.Crop, Width = 30, Height = 30, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { - // This returns a nullable array even though we're checking if items have value and there cannot be null - // We use Cast to recast into non-nullable array - var gsn = user.Groups.Where(x => x.StartContentId is not null).Select(x => x.StartContentId).Distinct().Cast().ToArray(); - var usn = user.StartContentIds; - if (usn is not null) - { - var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); - return vals; - } + ImageCropMode = ImageCropMode.Crop, Width = 60, Height = 60, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) + { + ImageCropMode = ImageCropMode.Crop, Width = 90, Height = 90, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) + { + ImageCropMode = ImageCropMode.Crop, Width = 150, Height = 150, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) + { + ImageCropMode = ImageCropMode.Crop, Width = 300, Height = 300, + }), + }.WhereNotNull().ToArray(); + } - return null; - }, TimeSpan.FromMinutes(2), true); - - return result; + public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); } - /// - /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin - /// - /// - /// - /// - /// - public static int[]? CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + return ContentPermissions.HasPathAccess( + content.Path, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + } + + internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RootString, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + + internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RecycleBinContentString, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + + internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RootString, + user.CalculateMediaStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinMedia); + + internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RecycleBinMediaString, + user.CalculateMediaStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinMedia); + + public static bool HasPathAccess(this IUser user, IMedia? media, IEntityService entityService, AppCaches appCaches) + { + if (media == null) { - var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => - { - var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId!.Value).Distinct().ToArray(); - var usn = user.StartMediaIds; - if (usn is not null) - { - var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); - return vals; - } - - return null; - }, TimeSpan.FromMinutes(2), true); - - return result; + throw new ArgumentNullException(nameof(media)); } - public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + } + + public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) + { + if (entity == null) { - var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => + throw new ArgumentNullException(nameof(entity)); + } + + return ContentPermissions.HasPathAccess( + entity.Path, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + } + + public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + } + + /// + /// Determines whether this user has access to view sensitive data + /// + /// + public static bool HasAccessToSensitiveData(this IUser user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); + } + + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + public static int[]? CalculateAllowedLanguageIds(this IUser user, ILocalizationService localizationService) + { + var hasAccessToAllLanguages = user.Groups.Any(x => x.HasAccessToAllLanguages); + + return hasAccessToAllLanguages + ? localizationService.GetAllLanguages().Select(x => x.Id).ToArray() + : user.Groups.SelectMany(x => x.AllowedLanguages).Distinct().ToArray(); + } + + public static int[]? CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => + { + // This returns a nullable array even though we're checking if items have value and there cannot be null + // We use Cast to recast into non-nullable array + var gsn = user.Groups.Where(x => x.StartContentId is not null).Select(x => x.StartContentId).Distinct() + .Cast().ToArray(); + var usn = user.StartContentIds; + if (usn is not null) { - var startNodeIds = user.CalculateMediaStartNodeIds(entityService, appCaches); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); return vals; - }, TimeSpan.FromMinutes(2), true); + } - return result; - } + return null; + }, + TimeSpan.FromMinutes(2), + true); - public static string[]? GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + return result; + } + + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + /// + /// + /// + /// + public static int[]? CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => { - var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => + var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId!.Value).Distinct() + .ToArray(); + var usn = user.StartMediaIds; + if (usn is not null) { - var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); + var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); return vals; - }, TimeSpan.FromMinutes(2), true); + } - return result; - } + return null; + }, + TimeSpan.FromMinutes(2), + true); - private static bool StartsWithPath(string test, string path) + return result; + } + + public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => { - return test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; - } + var startNodeIds = user.CalculateMediaStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, + TimeSpan.FromMinutes(2), + true); - private static string GetBinPath(UmbracoObjectTypes objectType) + return result; + } + + public static string[]? GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => { - var binPath = Constants.System.RootString + ","; - switch (objectType) - { - case UmbracoObjectTypes.Document: - binPath += Constants.System.RecycleBinContentString; - break; - case UmbracoObjectTypes.Media: - binPath += Constants.System.RecycleBinMediaString; - break; - default: - throw new ArgumentOutOfRangeException(nameof(objectType)); - } - return binPath; - } + var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path) + .ToArray(); + return vals; + }, + TimeSpan.FromMinutes(2), + true); - internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) + return result; + } + + internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) + { + // assume groupSn and userSn each don't contain duplicates + var asn = groupSn.Concat(userSn).Distinct().ToArray(); + Dictionary paths = asn.Length > 0 + ? entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path) + : new Dictionary(); + + paths[Constants.System.Root] = Constants.System.RootString; // entityService does not get that one + + var binPath = GetBinPath(objectType); + + var lsn = new List(); + foreach (var sn in groupSn) { - // assume groupSn and userSn each don't contain duplicates - - var asn = groupSn.Concat(userSn).Distinct().ToArray(); - var paths = asn.Length > 0 - ? entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path) - : new Dictionary(); - - paths[Constants.System.Root] = Constants.System.RootString; // entityService does not get that one - - var binPath = GetBinPath(objectType); - - var lsn = new List(); - foreach (var sn in groupSn) + if (paths.TryGetValue(sn, out var snp) == false) { - if (paths.TryGetValue(sn, out var snp) == false) continue; // ignore rogue node (no path) - - if (StartsWithPath(snp, binPath)) continue; // ignore bin - - if (lsn.Any(x => StartsWithPath(snp, paths[x]))) continue; // skip if something above this sn - lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn - lsn.Add(sn); + continue; // ignore rogue node (no path) } - var usn = new List(); - foreach (var sn in userSn) + if (StartsWithPath(snp, binPath)) { - if (paths.TryGetValue(sn, out var snp) == false) continue; // ignore rogue node (no path) - - if (StartsWithPath(snp, binPath)) continue; // ignore bin - - if (usn.Any(x => StartsWithPath(paths[x], snp))) continue; // skip if something below this sn - usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn - usn.Add(sn); + continue; // ignore bin } - foreach (var sn in usn) + if (lsn.Any(x => StartsWithPath(snp, paths[x]))) { - var snp = paths[sn]; // has to be here now - lsn.RemoveAll(x => StartsWithPath(snp, paths[x]) || StartsWithPath(paths[x], snp)); // remove anything above or below this sn - lsn.Add(sn); + continue; // skip if something above this sn } - return lsn.ToArray(); + lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn + lsn.Add(sn); } + + var usn = new List(); + foreach (var sn in userSn) + { + if (paths.TryGetValue(sn, out var snp) == false) + { + continue; // ignore rogue node (no path) + } + + if (StartsWithPath(snp, binPath)) + { + continue; // ignore bin + } + + if (usn.Any(x => StartsWithPath(paths[x], snp))) + { + continue; // skip if something below this sn + } + + usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn + usn.Add(sn); + } + + foreach (var sn in usn) + { + var snp = paths[sn]; // has to be here now + lsn.RemoveAll(x => + StartsWithPath(snp, paths[x]) || + StartsWithPath(paths[x], snp)); // remove anything above or below this sn + lsn.Add(sn); + } + + return lsn.ToArray(); + } + + private static bool StartsWithPath(string test, string path) => + test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; + + private static string GetBinPath(UmbracoObjectTypes objectType) + { + var binPath = Constants.System.RootString + ","; + switch (objectType) + { + case UmbracoObjectTypes.Document: + binPath += Constants.System.RecycleBinContentString; + break; + case UmbracoObjectTypes.Media: + binPath += Constants.System.RecycleBinMediaString; + break; + default: + throw new ArgumentOutOfRangeException(nameof(objectType)); + } + + return binPath; } } diff --git a/src/Umbraco.Core/Models/UserTourStatus.cs b/src/Umbraco.Core/Models/UserTourStatus.cs index 72e0a81cba..a954a0b864 100644 --- a/src/Umbraco.Core/Models/UserTourStatus.cs +++ b/src/Umbraco.Core/Models/UserTourStatus.cs @@ -1,60 +1,69 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing the tours a user has taken/completed +/// +[DataContract(Name = "userTourStatus", Namespace = "")] +public class UserTourStatus : IEquatable { /// - /// A model representing the tours a user has taken/completed + /// The tour alias /// - [DataContract(Name = "userTourStatus", Namespace = "")] - public class UserTourStatus : IEquatable + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; + + /// + /// If the tour is completed + /// + [DataMember(Name = "completed")] + public bool Completed { get; set; } + + /// + /// If the tour is disabled + /// + [DataMember(Name = "disabled")] + public bool Disabled { get; set; } + + public static bool operator ==(UserTourStatus? left, UserTourStatus? right) => Equals(left, right); + + public bool Equals(UserTourStatus? other) { - /// - /// The tour alias - /// - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; - - /// - /// If the tour is completed - /// - [DataMember(Name = "completed")] - public bool Completed { get; set; } - - /// - /// If the tour is disabled - /// - [DataMember(Name = "disabled")] - public bool Disabled { get; set; } - - public bool Equals(UserTourStatus? other) + if (ReferenceEquals(null, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Alias, other.Alias); + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((UserTourStatus) obj); + return true; } - public override int GetHashCode() - { - return Alias.GetHashCode(); - } - - public static bool operator ==(UserTourStatus? left, UserTourStatus? right) - { - return Equals(left, right); - } - - public static bool operator !=(UserTourStatus? left, UserTourStatus? right) - { - return !Equals(left, right); - } + return string.Equals(Alias, other.Alias); } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((UserTourStatus)obj); + } + + public override int GetHashCode() => Alias.GetHashCode(); + + public static bool operator !=(UserTourStatus? left, UserTourStatus? right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs index 095d4f50a9..acdaed7dd9 100644 --- a/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs +++ b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class UserTwoFactorProviderModel { - [DataContract] - public class UserTwoFactorProviderModel + public UserTwoFactorProviderModel(string providerName, bool isEnabledOnUser) { - public UserTwoFactorProviderModel(string providerName, bool isEnabledOnUser) - { - ProviderName = providerName; - IsEnabledOnUser = isEnabledOnUser; - } - - [DataMember(Name = "providerName")] - public string ProviderName { get; } - - [DataMember(Name = "isEnabledOnUser")] - public bool IsEnabledOnUser { get; } + ProviderName = providerName; + IsEnabledOnUser = isEnabledOnUser; } + + [DataMember(Name = "providerName")] + public string ProviderName { get; } + + [DataMember(Name = "isEnabledOnUser")] + public bool IsEnabledOnUser { get; } } diff --git a/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs b/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs index d104383b38..b4ebb89e47 100644 --- a/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs +++ b/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs @@ -1,19 +1,17 @@ -using System; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ - [Serializable] - [DataContract(Name = "validatePasswordReset", Namespace = "")] - public class ValidatePasswordResetCodeModel - { - [Required] - [DataMember(Name = "userId", IsRequired = true)] - public int UserId { get; set; } +namespace Umbraco.Cms.Core.Models; - [Required] - [DataMember(Name = "resetCode", IsRequired = true)] - public string? ResetCode { get; set; } - } +[Serializable] +[DataContract(Name = "validatePasswordReset", Namespace = "")] +public class ValidatePasswordResetCodeModel +{ + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } + + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string? ResetCode { get; set; } } diff --git a/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs b/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs index 10133a7f36..bffd551815 100644 --- a/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs +++ b/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs @@ -1,31 +1,32 @@ -using System.ComponentModel.DataAnnotations; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Reflection; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.Validation +namespace Umbraco.Cms.Core.Models.Validation; + +/// +/// Specifies that a data field value is required in order to persist an object. +/// +/// +/// +/// There are two levels of validation in Umbraco. (1) value validation is performed by +/// +/// instances; it can prevent a content item from being published, but not from being saved. (2) required +/// validation +/// of properties marked with ; it does prevent an object from being +/// saved +/// and is used for properties that are absolutely mandatory, such as the name of a content item. +/// +/// +public class RequiredForPersistenceAttribute : RequiredAttribute { /// - /// Specifies that a data field value is required in order to persist an object. + /// Determines whether an object has all required values for persistence. /// - /// - /// There are two levels of validation in Umbraco. (1) value validation is performed by - /// instances; it can prevent a content item from being published, but not from being saved. (2) required validation - /// of properties marked with ; it does prevent an object from being saved - /// and is used for properties that are absolutely mandatory, such as the name of a content item. - /// - public class RequiredForPersistenceAttribute : RequiredAttribute - { - /// - /// Determines whether an object has all required values for persistence. - /// - public static bool HasRequiredValuesForPersistence(object model) + public static bool HasRequiredValuesForPersistence(object model) => + model.GetType().GetProperties().All(x => { - return model.GetType().GetProperties().All(x => - { - var a = x.GetCustomAttribute(); - return a == null || a.IsValid(x.GetValue(model)); - }); - } - } + RequiredForPersistenceAttribute? a = x.GetCustomAttribute(); + return a == null || a.IsValid(x.GetValue(model)); + }); } diff --git a/src/Umbraco.Core/Models/ValueStorageType.cs b/src/Umbraco.Core/Models/ValueStorageType.cs index cca84b72b7..975369f993 100644 --- a/src/Umbraco.Core/Models/ValueStorageType.cs +++ b/src/Umbraco.Core/Models/ValueStorageType.cs @@ -1,47 +1,45 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the supported database types for storing a value. +/// +[Serializable] +[DataContract] +public enum ValueStorageType { + // note: these values are written out in the database in some places, + // and then parsed back in a case-sensitive way - think about it before + // changing the casing of values. + /// - /// Represents the supported database types for storing a value. + /// Store property value as NText. /// - [Serializable] - [DataContract] - public enum ValueStorageType - { - // note: these values are written out in the database in some places, - // and then parsed back in a case-sensitive way - think about it before - // changing the casing of values. + [EnumMember] + Ntext, - /// - /// Store property value as NText. - /// - [EnumMember] - Ntext, + /// + /// Store property value as NVarChar. + /// + [EnumMember] + Nvarchar, - /// - /// Store property value as NVarChar. - /// - [EnumMember] - Nvarchar, + /// + /// Store property value as Integer. + /// + [EnumMember] + Integer, - /// - /// Store property value as Integer. - /// - [EnumMember] - Integer, + /// + /// Store property value as Date. + /// + [EnumMember] + Date, - /// - /// Store property value as Date. - /// - [EnumMember] - Date, - - /// - /// Store property value as Decimal. - /// - [EnumMember] - Decimal - } + /// + /// Store property value as Decimal. + /// + [EnumMember] + Decimal, } diff --git a/src/Umbraco.Core/MonitorLock.cs b/src/Umbraco.Core/MonitorLock.cs index 11651aaa6c..45dbdbbd10 100644 --- a/src/Umbraco.Core/MonitorLock.cs +++ b/src/Umbraco.Core/MonitorLock.cs @@ -1,32 +1,31 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Provides an equivalent to the c# lock statement, to be used in a using block. +/// +/// Ie replace lock (o) {...} by using (new MonitorLock(o)) { ... } +public class MonitorLock : IDisposable { + private readonly bool _entered; + private readonly object _locker; + /// - /// Provides an equivalent to the c# lock statement, to be used in a using block. + /// Initializes a new instance of the class with an object to lock. /// - /// Ie replace lock (o) {...} by using (new MonitorLock(o)) { ... } - public class MonitorLock : IDisposable + /// The object to lock. + /// Should always be used within a using block. + public MonitorLock(object locker) { - private readonly object _locker; - private readonly bool _entered; + _locker = locker; + _entered = false; + Monitor.Enter(_locker, ref _entered); + } - /// - /// Initializes a new instance of the class with an object to lock. - /// - /// The object to lock. - /// Should always be used within a using block. - public MonitorLock(object locker) + void IDisposable.Dispose() + { + if (_entered) { - _locker = locker; - _entered = false; - System.Threading.Monitor.Enter(_locker, ref _entered); - } - - void IDisposable.Dispose() - { - if (_entered) - System.Threading.Monitor.Exit(_locker); + Monitor.Exit(_locker); } } } diff --git a/src/Umbraco.Core/NamedUdiRange.cs b/src/Umbraco.Core/NamedUdiRange.cs index 5855f27926..e0d52df9f4 100644 --- a/src/Umbraco.Core/NamedUdiRange.cs +++ b/src/Umbraco.Core/NamedUdiRange.cs @@ -1,34 +1,34 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a complemented with a name. +/// +public class NamedUdiRange : UdiRange { /// - /// Represents a complemented with a name. + /// Initializes a new instance of the class with a and an optional + /// selector. /// - public class NamedUdiRange : UdiRange + /// A . + /// An optional selector. + public NamedUdiRange(Udi udi, string selector = Constants.DeploySelector.This) + : base(udi, selector) { - /// - /// Initializes a new instance of the class with a and an optional selector. - /// - /// A . - /// An optional selector. - public NamedUdiRange(Udi udi, string selector = Constants.DeploySelector.This) - : base(udi, selector) - { } - - /// - /// Initializes a new instance of the class with a , a name, and an optional selector. - /// - /// A . - /// A name. - /// An optional selector. - public NamedUdiRange(Udi udi, string name, string selector = Constants.DeploySelector.This) - : base(udi, selector) - { - Name = name; - } - - /// - /// Gets or sets the name of the range. - /// - public string? Name { get; set; } } + + /// + /// Initializes a new instance of the class with a , a name, and an + /// optional selector. + /// + /// A . + /// A name. + /// An optional selector. + public NamedUdiRange(Udi udi, string name, string selector = Constants.DeploySelector.This) + : base(udi, selector) => + Name = name; + + /// + /// Gets or sets the name of the range. + /// + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Net/IIpResolver.cs b/src/Umbraco.Core/Net/IIpResolver.cs index 6c7ab72dec..edc9c6428c 100644 --- a/src/Umbraco.Core/Net/IIpResolver.cs +++ b/src/Umbraco.Core/Net/IIpResolver.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public interface IIpResolver { - public interface IIpResolver - { - string GetCurrentRequestIpAddress(); - } + string GetCurrentRequestIpAddress(); } diff --git a/src/Umbraco.Core/Net/ISessionIdResolver.cs b/src/Umbraco.Core/Net/ISessionIdResolver.cs index f5d6b4de29..4ec6248b39 100644 --- a/src/Umbraco.Core/Net/ISessionIdResolver.cs +++ b/src/Umbraco.Core/Net/ISessionIdResolver.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public interface ISessionIdResolver { - public interface ISessionIdResolver - { - string? SessionId { get; } - } + string? SessionId { get; } } diff --git a/src/Umbraco.Core/Net/IUserAgentProvider.cs b/src/Umbraco.Core/Net/IUserAgentProvider.cs index ba4f61b897..6916ee8d37 100644 --- a/src/Umbraco.Core/Net/IUserAgentProvider.cs +++ b/src/Umbraco.Core/Net/IUserAgentProvider.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public interface IUserAgentProvider { - public interface IUserAgentProvider - { - string? GetUserAgent(); - } + string? GetUserAgent(); } diff --git a/src/Umbraco.Core/Net/NullSessionIdResolver.cs b/src/Umbraco.Core/Net/NullSessionIdResolver.cs index 207a9c6048..c76c6c8632 100644 --- a/src/Umbraco.Core/Net/NullSessionIdResolver.cs +++ b/src/Umbraco.Core/Net/NullSessionIdResolver.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public class NullSessionIdResolver : ISessionIdResolver { - public class NullSessionIdResolver : ISessionIdResolver - { - public string? SessionId => null; - } + public string? SessionId => null; } diff --git a/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs index eb596a3a0b..cd0b1326f4 100644 --- a/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs @@ -1,11 +1,13 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ApplicationCacheRefresherNotification : CacheRefresherNotification { - public class ApplicationCacheRefresherNotification : CacheRefresherNotification + public ApplicationCacheRefresherNotification(object messageObject, MessageType messageType) + : base( + messageObject, + messageType) { - public ApplicationCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs b/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs index adcf14d636..23438827fd 100644 --- a/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications -{ - public class AssignedMemberRolesNotification : MemberRolesNotification - { - public AssignedMemberRolesNotification(int[] memberIds, string[] roles) : base(memberIds, roles) - { +namespace Umbraco.Cms.Core.Notifications; - } +public class AssignedMemberRolesNotification : MemberRolesNotification +{ + public AssignedMemberRolesNotification(int[] memberIds, string[] roles) + : base(memberIds, roles) + { } } diff --git a/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs b/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs index 18425f2393..347f1934bc 100644 --- a/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs +++ b/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public class AssignedUserGroupPermissionsNotification : EnumerableObjectNotification - { - public AssignedUserGroupPermissionsNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IEnumerable EntityPermissions => Target; +public class AssignedUserGroupPermissionsNotification : EnumerableObjectNotification +{ + public AssignedUserGroupPermissionsNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } + + public IEnumerable EntityPermissions => Target; } diff --git a/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs index bd110ad878..637c05dfb0 100644 --- a/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs @@ -1,20 +1,19 @@ -using System; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications -{ - /// - /// Base class for cache refresher notifications - /// - public abstract class CacheRefresherNotification : INotification - { - public CacheRefresherNotification(object messageObject, MessageType messageType) - { - MessageObject = messageObject ?? throw new ArgumentNullException(nameof(messageObject)); - MessageType = messageType; - } +namespace Umbraco.Cms.Core.Notifications; - public object MessageObject { get; } - public MessageType MessageType { get; } +/// +/// Base class for cache refresher notifications +/// +public abstract class CacheRefresherNotification : INotification +{ + public CacheRefresherNotification(object messageObject, MessageType messageType) + { + MessageObject = messageObject ?? throw new ArgumentNullException(nameof(messageObject)); + MessageType = messageType; } + + public object MessageObject { get; } + + public MessageType MessageType { get; } } diff --git a/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs b/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs index ea7476cd3f..1f51e68409 100644 --- a/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs @@ -1,18 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CancelableEnumerableObjectNotification : CancelableObjectNotification> { - public abstract class CancelableEnumerableObjectNotification : CancelableObjectNotification> + protected CancelableEnumerableObjectNotification(T target, EventMessages messages) + : base(new[] { target }, messages) + { + } + + protected CancelableEnumerableObjectNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) { - protected CancelableEnumerableObjectNotification(T target, EventMessages messages) : base(new [] {target}, messages) - { - } - protected CancelableEnumerableObjectNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/CancelableNotification.cs b/src/Umbraco.Core/Notifications/CancelableNotification.cs index 13989d50da..438bc1ee99 100644 --- a/src/Umbraco.Core/Notifications/CancelableNotification.cs +++ b/src/Umbraco.Core/Notifications/CancelableNotification.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class CancelableNotification : StatefulNotification, ICancelableNotification { - public class CancelableNotification : StatefulNotification, ICancelableNotification + public CancelableNotification(EventMessages messages) => Messages = messages; + + public EventMessages Messages { get; } + + public bool Cancel { get; set; } + + public void CancelOperation(EventMessage cancellationMessage) { - public CancelableNotification(EventMessages messages) => Messages = messages; - - public EventMessages Messages { get; } - - public bool Cancel { get; set; } - - public void CancelOperation(EventMessage cancellationMessage) - { - Cancel = true; - cancellationMessage.IsDefaultEventMessage = true; - Messages.Add(cancellationMessage); - } + Cancel = true; + cancellationMessage.IsDefaultEventMessage = true; + Messages.Add(cancellationMessage); } } diff --git a/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs b/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs index 25f6a4474f..be15626eb0 100644 --- a/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs @@ -3,21 +3,22 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CancelableObjectNotification : ObjectNotification, ICancelableNotification + where T : class { - public abstract class CancelableObjectNotification : ObjectNotification, ICancelableNotification where T : class + protected CancelableObjectNotification(T target, EventMessages messages) + : base(target, messages) { - protected CancelableObjectNotification(T target, EventMessages messages) : base(target, messages) - { - } + } - public bool Cancel { get; set; } + public bool Cancel { get; set; } - public void CancelOperation(EventMessage cancelationMessage) - { - Cancel = true; - cancelationMessage.IsDefaultEventMessage = true; - Messages.Add(cancelationMessage); - } + public void CancelOperation(EventMessage cancelationMessage) + { + Cancel = true; + cancelationMessage.IsDefaultEventMessage = true; + Messages.Add(cancelationMessage); } } diff --git a/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs index 35b4f472c7..67a43b5ac2 100644 --- a/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs @@ -1,11 +1,13 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ContentCacheRefresherNotification : CacheRefresherNotification { - public class ContentCacheRefresherNotification : CacheRefresherNotification + public ContentCacheRefresherNotification(object messageObject, MessageType messageType) + : base( + messageObject, + messageType) { - public ContentCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs b/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs index 6399fb714d..a5c6ede432 100644 --- a/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs @@ -4,13 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentCopiedNotification : CopiedNotification { - public sealed class ContentCopiedNotification : CopiedNotification + public ContentCopiedNotification(IContent original, IContent copy, int parentId, bool relateToOriginal, EventMessages messages) + : base(original, copy, parentId, relateToOriginal, messages) { - public ContentCopiedNotification(IContent original, IContent copy, int parentId, bool relateToOriginal, EventMessages messages) - : base(original, copy, parentId, relateToOriginal, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs b/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs index d30d49efeb..ef8eb48058 100644 --- a/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs @@ -4,13 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentCopyingNotification : CopyingNotification { - public sealed class ContentCopyingNotification : CopyingNotification + public ContentCopyingNotification(IContent original, IContent copy, int parentId, EventMessages messages) + : base(original, copy, parentId, messages) { - public ContentCopyingNotification(IContent original, IContent copy, int parentId, EventMessages messages) - : base(original, copy, parentId, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs index 1c516a295f..884fcf493b 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs @@ -1,22 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletedBlueprintNotification : EnumerableObjectNotification { - public sealed class ContentDeletedBlueprintNotification : EnumerableObjectNotification + public ContentDeletedBlueprintNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentDeletedBlueprintNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentDeletedBlueprintNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable DeletedBlueprints => Target; } + + public ContentDeletedBlueprintNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) + { + } + + public IEnumerable DeletedBlueprints => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs index 6398c4f28e..c68a07b1f0 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletedNotification : DeletedNotification { - public sealed class ContentDeletedNotification : DeletedNotification + public ContentDeletedNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentDeletedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs index 30f00b52bf..5e2b646008 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs @@ -1,16 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletedVersionsNotification : DeletedVersionsNotification { - public sealed class ContentDeletedVersionsNotification : DeletedVersionsNotification + public ContentDeletedVersionsNotification( + int id, + EventMessages messages, + int specificVersion = default, + bool deletePriorVersions = false, + DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public ContentDeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs index ee02c6f339..de4176a01b 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentDeletingNotification : DeletingNotification - { - public ContentDeletingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentDeletingNotification : DeletingNotification +{ + public ContentDeletingNotification(IContent target, EventMessages messages) + : base(target, messages) + { + } + + public ContentDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs index 340aaaa559..5d173bcc0c 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletingVersionsNotification : DeletingVersionsNotification { - public sealed class ContentDeletingVersionsNotification : DeletingVersionsNotification + public ContentDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public ContentDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs index 1453553efa..9a1637dda9 100644 --- a/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs @@ -1,16 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentEmptiedRecycleBinNotification : EmptiedRecycleBinNotification { - public sealed class ContentEmptiedRecycleBinNotification : EmptiedRecycleBinNotification + public ContentEmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) + : base( + deletedEntities, messages) { - public ContentEmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs index 134e65d982..f55d1166ce 100644 --- a/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs @@ -1,16 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentEmptyingRecycleBinNotification : EmptyingRecycleBinNotification { - public sealed class ContentEmptyingRecycleBinNotification : EmptyingRecycleBinNotification + public ContentEmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) + : base( + deletedEntities, messages) { - public ContentEmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentMovedNotification.cs b/src/Umbraco.Core/Notifications/ContentMovedNotification.cs index 607d678049..50bd24876d 100644 --- a/src/Umbraco.Core/Notifications/ContentMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovedNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovedNotification : MovedNotification - { - public ContentMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovedNotification : MovedNotification +{ + public ContentMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public ContentMovedNotification(IEnumerable> target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs index 3b736b1409..bf5415d9d1 100644 --- a/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovedToRecycleBinNotification : MovedToRecycleBinNotification - { - public ContentMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovedToRecycleBinNotification : MovedToRecycleBinNotification +{ + public ContentMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentMovingNotification.cs b/src/Umbraco.Core/Notifications/ContentMovingNotification.cs index 01c04eb226..eddc7a13f7 100644 --- a/src/Umbraco.Core/Notifications/ContentMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovingNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovingNotification : MovingNotification - { - public ContentMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovingNotification : MovingNotification +{ + public ContentMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public ContentMovingNotification(IEnumerable> target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs index 88aa48c7b8..5a691c6487 100644 --- a/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovingToRecycleBinNotification : MovingToRecycleBinNotification - { - public ContentMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovingToRecycleBinNotification : MovingToRecycleBinNotification +{ + public ContentMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs b/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs index c009b1cb62..a7449f24cc 100644 --- a/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs +++ b/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs @@ -3,65 +3,67 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public static class ContentNotificationExtensions { - public static class ContentNotificationExtensions - { - /// - /// Determines whether a culture is being saved, during a Saving notification - /// - public static bool IsSavingCulture(this SavingNotification notification, T content, string culture) where T : IContentBase - => (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && cultureInfo.IsDirty(); + /// + /// Determines whether a culture is being saved, during a Saving notification + /// + public static bool IsSavingCulture(this SavingNotification notification, T content, string culture) + where T : IContentBase + => (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && + cultureInfo.IsDirty(); - /// - /// Determines whether a culture has been saved, during a Saved notification - /// - public static bool HasSavedCulture(this SavedNotification notification, T content, string culture) where T : IContentBase - => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UpdatedCulture + culture); + /// + /// Determines whether a culture has been saved, during a Saved notification + /// + public static bool HasSavedCulture(this SavedNotification notification, T content, string culture) + where T : IContentBase + => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UpdatedCulture + culture); - /// - /// Determines whether a culture is being published, during a Publishing notification - /// - public static bool IsPublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) - => IsPublishingCulture(content, culture); + /// + /// Determines whether a culture is being published, during a Publishing notification + /// + public static bool IsPublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) + => IsPublishingCulture(content, culture); - /// - /// Determines whether a culture is being unpublished, during an Publishing notification - /// - public static bool IsUnpublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) - => IsUnpublishingCulture(content, culture); + /// + /// Determines whether a culture is being unpublished, during an Publishing notification + /// + public static bool IsUnpublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) + => IsUnpublishingCulture(content, culture); - /// - /// Determines whether a culture is being unpublished, during a Unpublishing notification - /// - public static bool IsUnpublishingCulture(this ContentUnpublishingNotification notification, IContent content, string culture) - => IsUnpublishingCulture(content, culture); + /// + /// Determines whether a culture is being unpublished, during a Unpublishing notification + /// + public static bool IsUnpublishingCulture(this ContentUnpublishingNotification notification, IContent content, string culture) => IsUnpublishingCulture(content, culture); - /// - /// Determines whether a culture has been published, during a Published notification - /// - public static bool HasPublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) - => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.ChangedCulture + culture); + /// + /// Determines whether a culture has been published, during a Published notification + /// + public static bool HasPublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) + => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.ChangedCulture + culture); - /// - /// Determines whether a culture has been unpublished, during a Published notification - /// - public static bool HasUnpublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) - => HasUnpublishedCulture(content, culture); + /// + /// Determines whether a culture has been unpublished, during a Published notification + /// + public static bool HasUnpublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) + => HasUnpublishedCulture(content, culture); - /// - /// Determines whether a culture has been unpublished, during an Unpublished notification - /// - public static bool HasUnpublishedCulture(this ContentUnpublishedNotification notification, IContent content, string culture) - => HasUnpublishedCulture(content, culture); + /// + /// Determines whether a culture has been unpublished, during an Unpublished notification + /// + public static bool HasUnpublishedCulture(this ContentUnpublishedNotification notification, IContent content, string culture) + => HasUnpublishedCulture(content, culture); - private static bool IsUnpublishingCulture(IContent content, string culture) - => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); + public static bool IsPublishingCulture(IContent content, string culture) + => (content.PublishCultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && + cultureInfo.IsDirty(); - public static bool IsPublishingCulture(IContent content, string culture) - => (content.PublishCultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && cultureInfo.IsDirty(); + private static bool IsUnpublishingCulture(IContent content, string culture) + => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); - public static bool HasUnpublishedCulture(IContent content, string culture) - => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); - } + public static bool HasUnpublishedCulture(IContent content, string culture) + => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); } diff --git a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs index 69d1751e58..0400155d3c 100644 --- a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentPublishedNotification : EnumerableObjectNotification { - public sealed class ContentPublishedNotification : EnumerableObjectNotification + public ContentPublishedNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentPublishedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentPublishedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable PublishedEntities => Target; } + + public ContentPublishedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable PublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs b/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs index 65a8efdadf..c9a1110089 100644 --- a/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentPublishingNotification : CancelableEnumerableObjectNotification { - public sealed class ContentPublishingNotification : CancelableEnumerableObjectNotification + public ContentPublishingNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentPublishingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentPublishingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable PublishedEntities => Target; } + + public ContentPublishingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable PublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs b/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs index b9cda7722c..f2d18fbba1 100644 --- a/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs @@ -1,17 +1,15 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ +namespace Umbraco.Cms.Core.Notifications; - [Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class ContentRefreshNotification : EntityRefreshNotification +[Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class ContentRefreshNotification : EntityRefreshNotification +{ + public ContentRefreshNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentRefreshNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs b/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs index a1f370bd94..50b89e10b8 100644 --- a/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentRolledBackNotification : RolledBackNotification { - public sealed class ContentRolledBackNotification : RolledBackNotification + public ContentRolledBackNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentRolledBackNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs b/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs index e12bfa1631..29b864853c 100644 --- a/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentRollingBackNotification : RollingBackNotification { - public sealed class ContentRollingBackNotification : RollingBackNotification + public ContentRollingBackNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentRollingBackNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs index 6addde88c1..d06f364ed2 100644 --- a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs @@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSavedBlueprintNotification : ObjectNotification - { - public ContentSavedBlueprintNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContent SavedBlueprint => Target; +public sealed class ContentSavedBlueprintNotification : ObjectNotification +{ + public ContentSavedBlueprintNotification(IContent target, EventMessages messages) + : base(target, messages) + { } + + public IContent SavedBlueprint => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSavedNotification.cs b/src/Umbraco.Core/Notifications/ContentSavedNotification.cs index b58a366368..2d3253117d 100644 --- a/src/Umbraco.Core/Notifications/ContentSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSavedNotification : SavedNotification - { - public ContentSavedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentSavedNotification : SavedNotification +{ + public ContentSavedNotification(IContent target, EventMessages messages) + : base(target, messages) + { + } + + public ContentSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentSavingNotification.cs b/src/Umbraco.Core/Notifications/ContentSavingNotification.cs index afe21bf870..4a57a10f29 100644 --- a/src/Umbraco.Core/Notifications/ContentSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSavingNotification : SavingNotification - { - public ContentSavingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentSavingNotification : SavingNotification +{ + public ContentSavingNotification(IContent target, EventMessages messages) + : base(target, messages) + { + } + + public ContentSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs b/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs index 0a5c018883..7d5ee26130 100644 --- a/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs @@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSendingToPublishNotification : CancelableObjectNotification - { - public ContentSendingToPublishNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContent Entity => Target; +public sealed class ContentSendingToPublishNotification : CancelableObjectNotification +{ + public ContentSendingToPublishNotification(IContent target, EventMessages messages) + : base(target, messages) + { } + + public IContent Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs b/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs index c5e2e5dc3b..e10b9930e3 100644 --- a/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs @@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSentToPublishNotification : ObjectNotification - { - public ContentSentToPublishNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContent Entity => Target; +public sealed class ContentSentToPublishNotification : ObjectNotification +{ + public ContentSentToPublishNotification(IContent target, EventMessages messages) + : base(target, messages) + { } + + public IContent Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSortedNotification.cs b/src/Umbraco.Core/Notifications/ContentSortedNotification.cs index 0a299e3c0a..8f0d6304ff 100644 --- a/src/Umbraco.Core/Notifications/ContentSortedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSortedNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSortedNotification : SortedNotification { - public sealed class ContentSortedNotification : SortedNotification + public ContentSortedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) { - public ContentSortedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentSortingNotification.cs b/src/Umbraco.Core/Notifications/ContentSortingNotification.cs index 1d6cd31c5a..bc3e94a464 100644 --- a/src/Umbraco.Core/Notifications/ContentSortingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSortingNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSortingNotification : SortingNotification { - public sealed class ContentSortingNotification : SortingNotification + public ContentSortingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) { - public ContentSortingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs b/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs index b5b100038b..df5aab16c7 100644 --- a/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs @@ -1,31 +1,35 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTreeChangeNotification : TreeChangeNotification { - public class ContentTreeChangeNotification : TreeChangeNotification + public ContentTreeChangeNotification(TreeChange target, EventMessages messages) + : base(target, messages) { - public ContentTreeChangeNotification(TreeChange target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTreeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } + public ContentTreeChangeNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { + } - public ContentTreeChangeNotification(IEnumerable target, - TreeChangeTypes changeTypes, - EventMessages messages) : base(target.Select(x => new TreeChange(x, changeTypes)), messages) - { - } + public ContentTreeChangeNotification( + IEnumerable target, + TreeChangeTypes changeTypes, + EventMessages messages) + : base(target.Select(x => new TreeChange(x, changeTypes)), messages) + { + } - public ContentTreeChangeNotification(IContent target, - TreeChangeTypes changeTypes, - EventMessages messages) : base(new TreeChange(target, changeTypes), messages) - { - } + public ContentTreeChangeNotification( + IContent target, + TreeChangeTypes changeTypes, + EventMessages messages) + : base(new TreeChange(target, changeTypes), messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs index 8bd06a4c46..d4ced3496d 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs @@ -1,11 +1,13 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeCacheRefresherNotification : CacheRefresherNotification { - public class ContentTypeCacheRefresherNotification : CacheRefresherNotification + public ContentTypeCacheRefresherNotification(object messageObject, MessageType messageType) + : base( + messageObject, + messageType) { - public ContentTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs index e03f381318..606a6fb34e 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs @@ -1,20 +1,24 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class ContentTypeChangeNotification : EnumerableObjectNotification> + where T : class, IContentTypeComposition { - public abstract class ContentTypeChangeNotification : EnumerableObjectNotification> where T : class, IContentTypeComposition + protected ContentTypeChangeNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) { - protected ContentTypeChangeNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } - - protected ContentTypeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> Changes => Target; } + + protected ContentTypeChangeNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { + } + + public IEnumerable> Changes => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs index e0aca73cd2..0456ebc9cf 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs @@ -1,18 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeChangedNotification : ContentTypeChangeNotification - { - public ContentTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeChangedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeChangedNotification : ContentTypeChangeNotification +{ + public ContentTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentTypeChangedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs index d5b2b3e28e..92092d1a57 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeDeletedNotification : DeletedNotification - { - public ContentTypeDeletedNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeDeletedNotification : DeletedNotification +{ + public ContentTypeDeletedNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeDeletedNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs index 56863b93fb..0313ffcc17 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeDeletingNotification : DeletingNotification - { - public ContentTypeDeletingNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeDeletingNotification : DeletingNotification +{ + public ContentTypeDeletingNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs index d4794329cf..4fab7a67ac 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs @@ -1,17 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeMovedNotification : MovedNotification - { - public ContentTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeMovedNotification : MovedNotification +{ + public ContentTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentTypeMovedNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs index a888150097..210dcf43f2 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeMovingNotification : MovingNotification - { - public ContentTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeMovingNotification : MovingNotification +{ + public ContentTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentTypeMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs index 717225db2d..108e72aecc 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs @@ -1,18 +1,22 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class ContentTypeRefreshNotification : ContentTypeChangeNotification where T: class, IContentTypeComposition - { - protected ContentTypeRefreshNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - protected ContentTypeRefreshNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public abstract class ContentTypeRefreshNotification : ContentTypeChangeNotification + where T : class, IContentTypeComposition +{ + protected ContentTypeRefreshNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) + { + } + + protected ContentTypeRefreshNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs index 72d111bb67..b49eef2876 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class ContentTypeRefreshedNotification : ContentTypeRefreshNotification - { - public ContentTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeRefreshedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +[Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class ContentTypeRefreshedNotification : ContentTypeRefreshNotification +{ + public ContentTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeRefreshedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs index 5b9a231d60..f5c45c6323 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeSavedNotification : SavedNotification - { - public ContentTypeSavedNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeSavedNotification : SavedNotification +{ + public ContentTypeSavedNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs index 85deb91418..5c1bc5d611 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeSavingNotification : SavingNotification - { - public ContentTypeSavingNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeSavingNotification : SavingNotification +{ + public ContentTypeSavingNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs b/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs index c08d79ac59..2677ef5a08 100644 --- a/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentUnpublishedNotification : EnumerableObjectNotification { - public sealed class ContentUnpublishedNotification : EnumerableObjectNotification + public ContentUnpublishedNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentUnpublishedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentUnpublishedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable UnpublishedEntities => Target; } + + public ContentUnpublishedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable UnpublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs b/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs index 5fb5034515..7fc0717c04 100644 --- a/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentUnpublishingNotification : CancelableEnumerableObjectNotification { - public sealed class ContentUnpublishingNotification : CancelableEnumerableObjectNotification + public ContentUnpublishingNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentUnpublishingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentUnpublishingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable UnpublishedEntities => Target; } + + public ContentUnpublishingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable UnpublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/CopiedNotification.cs b/src/Umbraco.Core/Notifications/CopiedNotification.cs index c7d6c88bcd..13b9cf25ba 100644 --- a/src/Umbraco.Core/Notifications/CopiedNotification.cs +++ b/src/Umbraco.Core/Notifications/CopiedNotification.cs @@ -3,22 +3,24 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CopiedNotification : ObjectNotification + where T : class { - public abstract class CopiedNotification : ObjectNotification where T : class + protected CopiedNotification(T original, T copy, int parentId, bool relateToOriginal, EventMessages messages) + : base(original, messages) { - protected CopiedNotification(T original, T copy, int parentId, bool relateToOriginal, EventMessages messages) : base(original, messages) - { - Copy = copy; - ParentId = parentId; - RelateToOriginal = relateToOriginal; - } - - public T Original => Target; - - public T Copy { get; } - - public int ParentId { get; } - public bool RelateToOriginal { get; } + Copy = copy; + ParentId = parentId; + RelateToOriginal = relateToOriginal; } + + public T Original => Target; + + public T Copy { get; } + + public int ParentId { get; } + + public bool RelateToOriginal { get; } } diff --git a/src/Umbraco.Core/Notifications/CopyingNotification.cs b/src/Umbraco.Core/Notifications/CopyingNotification.cs index 99f46f8b43..0992f9708b 100644 --- a/src/Umbraco.Core/Notifications/CopyingNotification.cs +++ b/src/Umbraco.Core/Notifications/CopyingNotification.cs @@ -3,20 +3,21 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CopyingNotification : CancelableObjectNotification + where T : class { - public abstract class CopyingNotification : CancelableObjectNotification where T : class + protected CopyingNotification(T original, T copy, int parentId, EventMessages messages) + : base(original, messages) { - protected CopyingNotification(T original, T copy, int parentId, EventMessages messages) : base(original, messages) - { - Copy = copy; - ParentId = parentId; - } - - public T Original => Target; - - public T Copy { get; } - - public int ParentId { get; } + Copy = copy; + ParentId = parentId; } + + public T Original => Target; + + public T Copy { get; } + + public int ParentId { get; } } diff --git a/src/Umbraco.Core/Notifications/CreatedNotification.cs b/src/Umbraco.Core/Notifications/CreatedNotification.cs index 2108b5fb5c..8667e4bdcc 100644 --- a/src/Umbraco.Core/Notifications/CreatedNotification.cs +++ b/src/Umbraco.Core/Notifications/CreatedNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class CreatedNotification : ObjectNotification where T : class - { - protected CreatedNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T CreatedEntity => Target; +public abstract class CreatedNotification : ObjectNotification + where T : class +{ + protected CreatedNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T CreatedEntity => Target; } diff --git a/src/Umbraco.Core/Notifications/CreatingNotification.cs b/src/Umbraco.Core/Notifications/CreatingNotification.cs index da4fbfe742..f76a3d8839 100644 --- a/src/Umbraco.Core/Notifications/CreatingNotification.cs +++ b/src/Umbraco.Core/Notifications/CreatingNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class CreatingNotification : CancelableObjectNotification where T : class - { - protected CreatingNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T CreatedEntity => Target; +public abstract class CreatingNotification : CancelableObjectNotification + where T : class +{ + protected CreatingNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T CreatedEntity => Target; } diff --git a/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs b/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs index aacca17afb..2ea921ceb6 100644 --- a/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs +++ b/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs @@ -1,20 +1,17 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +/// +/// Used for notifying when an Umbraco request is being created +/// +public class CreatingRequestNotification : INotification { /// - /// Used for notifying when an Umbraco request is being created + /// Initializes a new instance of the class. /// - public class CreatingRequestNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public CreatingRequestNotification(Uri url) => Url = url; + public CreatingRequestNotification(Uri url) => Url = url; - /// - /// Gets or sets the URL for the request - /// - public Uri Url { get; set; } - } + /// + /// Gets or sets the URL for the request + /// + public Uri Url { get; set; } } diff --git a/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs index f59de3ebd0..5f8b34fb22 100644 --- a/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeCacheRefresherNotification : CacheRefresherNotification { - public class DataTypeCacheRefresherNotification : CacheRefresherNotification + public DataTypeCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public DataTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs index 405af74c1c..839fa00230 100644 --- a/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeDeletedNotification : DeletedNotification { - public class DataTypeDeletedNotification : DeletedNotification + public DataTypeDeletedNotification(IDataType target, EventMessages messages) + : base(target, messages) { - public DataTypeDeletedNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs index ab997a0def..70035a5237 100644 --- a/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeDeletingNotification : DeletingNotification { - public class DataTypeDeletingNotification : DeletingNotification + public DataTypeDeletingNotification(IDataType target, EventMessages messages) + : base(target, messages) { - public DataTypeDeletingNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs index 150582547b..27065b8619 100644 --- a/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeMovedNotification : MovedNotification { - public class DataTypeMovedNotification : MovedNotification + public DataTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public DataTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs index ae8fb4be6e..1a54f14622 100644 --- a/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeMovingNotification : MovingNotification { - public class DataTypeMovingNotification : MovingNotification + public DataTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public DataTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs index 6c1a806069..ca23336ce1 100644 --- a/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DataTypeSavedNotification : SavedNotification - { - public DataTypeSavedNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DataTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DataTypeSavedNotification : SavedNotification +{ + public DataTypeSavedNotification(IDataType target, EventMessages messages) + : base(target, messages) + { + } + + public DataTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs index 3538950b12..8099431da6 100644 --- a/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DataTypeSavingNotification : SavingNotification - { - public DataTypeSavingNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DataTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DataTypeSavingNotification : SavingNotification +{ + public DataTypeSavingNotification(IDataType target, EventMessages messages) + : base(target, messages) + { + } + + public DataTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DeletedNotification.cs b/src/Umbraco.Core/Notifications/DeletedNotification.cs index 3b2a370388..69af0581af 100644 --- a/src/Umbraco.Core/Notifications/DeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletedNotification : EnumerableObjectNotification { - public abstract class DeletedNotification : EnumerableObjectNotification + protected DeletedNotification(T target, EventMessages messages) + : base(target, messages) { - protected DeletedNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected DeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable DeletedEntities => Target; } + + protected DeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable DeletedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs b/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs index 420323afaf..03b8e150b7 100644 --- a/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletedVersionsNotification : DeletedVersionsNotificationBase + where T : class { - public abstract class DeletedVersionsNotification : DeletedVersionsNotificationBase where T : class + protected DeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - protected DeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) - : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs b/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs index 352eee8cae..a68593de80 100644 --- a/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs +++ b/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs @@ -1,30 +1,34 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletedVersionsNotificationBase : StatefulNotification + where T : class { - public abstract class DeletedVersionsNotificationBase : StatefulNotification where T : class + protected DeletedVersionsNotificationBase( + int id, + EventMessages messages, + int specificVersion = default, + bool deletePriorVersions = false, + DateTime dateToRetain = default) { - protected DeletedVersionsNotificationBase(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) - { - Id = id; - Messages = messages; - SpecificVersion = specificVersion; - DeletePriorVersions = deletePriorVersions; - DateToRetain = dateToRetain; - } - - public int Id { get; } - - public EventMessages Messages { get; } - - public int SpecificVersion { get; } - - public bool DeletePriorVersions { get; } - - public DateTime DateToRetain { get; } + Id = id; + Messages = messages; + SpecificVersion = specificVersion; + DeletePriorVersions = deletePriorVersions; + DateToRetain = dateToRetain; } + + public int Id { get; } + + public EventMessages Messages { get; } + + public int SpecificVersion { get; } + + public bool DeletePriorVersions { get; } + + public DateTime DateToRetain { get; } } diff --git a/src/Umbraco.Core/Notifications/DeletingNotification.cs b/src/Umbraco.Core/Notifications/DeletingNotification.cs index b4090a5b83..ab630468dd 100644 --- a/src/Umbraco.Core/Notifications/DeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletingNotification : CancelableEnumerableObjectNotification { - public abstract class DeletingNotification : CancelableEnumerableObjectNotification + protected DeletingNotification(T target, EventMessages messages) + : base(target, messages) { - protected DeletingNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected DeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable DeletedEntities => Target; } + + protected DeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable DeletedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs b/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs index ca8f1f2514..6b708da28b 100644 --- a/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs @@ -1,18 +1,17 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class DeletingVersionsNotification : DeletedVersionsNotificationBase, ICancelableNotification where T : class - { - protected DeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) - : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } +namespace Umbraco.Cms.Core.Notifications; - public bool Cancel { get; set; } +public abstract class DeletingVersionsNotification : DeletedVersionsNotificationBase, ICancelableNotification + where T : class +{ + protected DeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) + { } + + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs index b36939800e..170e8e21be 100644 --- a/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DictionaryCacheRefresherNotification : CacheRefresherNotification { - public class DictionaryCacheRefresherNotification : CacheRefresherNotification + public DictionaryCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public DictionaryCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs index c151e7ec60..c62f6d3f7d 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DictionaryItemDeletedNotification : DeletedNotification { - public class DictionaryItemDeletedNotification : DeletedNotification + public DictionaryItemDeletedNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) { - public DictionaryItemDeletedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs index 5be95c478b..d882bb594f 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DictionaryItemDeletingNotification : DeletingNotification - { - public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DictionaryItemDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DictionaryItemDeletingNotification : DeletingNotification +{ + public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) + { + } + + public DictionaryItemDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs index dc5194b847..386871a28b 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DictionaryItemSavedNotification : SavedNotification - { - public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DictionaryItemSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DictionaryItemSavedNotification : SavedNotification +{ + public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) + { + } + + public DictionaryItemSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs index 79fef15aef..517fc772a0 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DictionaryItemSavingNotification : SavingNotification - { - public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DictionaryItemSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DictionaryItemSavingNotification : SavingNotification +{ + public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) + { + } + + public DictionaryItemSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs index 326a7d2b64..86114b5003 100644 --- a/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DomainCacheRefresherNotification : CacheRefresherNotification { - public class DomainCacheRefresherNotification : CacheRefresherNotification + public DomainCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public DomainCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs b/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs index b1b3a80ba1..c569afc7b4 100644 --- a/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DomainDeletedNotification : DeletedNotification { - public class DomainDeletedNotification : DeletedNotification + public DomainDeletedNotification(IDomain target, EventMessages messages) + : base(target, messages) { - public DomainDeletedNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs b/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs index cd678d3689..afeb3fa67c 100644 --- a/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DomainDeletingNotification : DeletingNotification - { - public DomainDeletingNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DomainDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DomainDeletingNotification : DeletingNotification +{ + public DomainDeletingNotification(IDomain target, EventMessages messages) + : base(target, messages) + { + } + + public DomainDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DomainSavedNotification.cs b/src/Umbraco.Core/Notifications/DomainSavedNotification.cs index 61bd8ba3a4..75c93e15b7 100644 --- a/src/Umbraco.Core/Notifications/DomainSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DomainSavedNotification : SavedNotification - { - public DomainSavedNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DomainSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DomainSavedNotification : SavedNotification +{ + public DomainSavedNotification(IDomain target, EventMessages messages) + : base(target, messages) + { + } + + public DomainSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DomainSavingNotification.cs b/src/Umbraco.Core/Notifications/DomainSavingNotification.cs index 32a2d71a73..673ed92c72 100644 --- a/src/Umbraco.Core/Notifications/DomainSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DomainSavingNotification : SavingNotification - { - public DomainSavingNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DomainSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DomainSavingNotification : SavingNotification +{ + public DomainSavingNotification(IDomain target, EventMessages messages) + : base(target, messages) + { + } + + public DomainSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs index 2773f3140f..8e648ac14d 100644 --- a/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs @@ -1,21 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class EmptiedRecycleBinNotification : StatefulNotification + where T : class { - public abstract class EmptiedRecycleBinNotification : StatefulNotification where T : class + protected EmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) { - protected EmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) - { - DeletedEntities = deletedEntities; - Messages = messages; - } - - public IEnumerable DeletedEntities { get; } - - public EventMessages Messages { get; } + DeletedEntities = deletedEntities; + Messages = messages; } + + public IEnumerable DeletedEntities { get; } + + public EventMessages Messages { get; } } diff --git a/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs index 42005fc9f4..5701819415 100644 --- a/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs @@ -1,23 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class EmptyingRecycleBinNotification : StatefulNotification, ICancelableNotification + where T : class { - public abstract class EmptyingRecycleBinNotification : StatefulNotification, ICancelableNotification where T : class + protected EmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) { - protected EmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) - { - DeletedEntities = deletedEntities; - Messages = messages; - } - - public IEnumerable? DeletedEntities { get; } - - public EventMessages Messages { get; } - - public bool Cancel { get; set; } + DeletedEntities = deletedEntities; + Messages = messages; } + + public IEnumerable? DeletedEntities { get; } + + public EventMessages Messages { get; } + + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs index 66c55e94ad..5074aa3893 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerDeletedNotification : DeletedNotification { - public class EntityContainerDeletedNotification : DeletedNotification + public EntityContainerDeletedNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerDeletedNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs index 45a7a5b6c8..4d22d7715a 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerDeletingNotification : DeletingNotification { - public class EntityContainerDeletingNotification : DeletingNotification + public EntityContainerDeletingNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerDeletingNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs index e6046c9a58..11e7100b91 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerRenamedNotification : RenamedNotification { - public class EntityContainerRenamedNotification : RenamedNotification + public EntityContainerRenamedNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerRenamedNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs index c03d5f5ee3..9e1b795d9f 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerRenamingNotification : RenamingNotification { - public class EntityContainerRenamingNotification : RenamingNotification + public EntityContainerRenamingNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerRenamingNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs index 33cac9effd..4fa3446834 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerSavedNotification : SavedNotification { - public class EntityContainerSavedNotification : SavedNotification + public EntityContainerSavedNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerSavedNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs index 25cbfc9311..6c5455e762 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerSavingNotification : SavingNotification { - public class EntityContainerSavingNotification : SavingNotification + public EntityContainerSavingNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerSavingNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs b/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs index 1afc1fa078..4a5aaa4216 100644 --- a/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs @@ -1,14 +1,15 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class EntityRefreshNotification : ObjectNotification where T : class, IContentBase - { - public EntityRefreshNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T Entity => Target; +public class EntityRefreshNotification : ObjectNotification + where T : class, IContentBase +{ + public EntityRefreshNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs b/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs index fde93f0139..3989e34b4b 100644 --- a/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs @@ -1,19 +1,19 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class EnumerableObjectNotification : ObjectNotification> - { - protected EnumerableObjectNotification(T target, EventMessages messages) : base(new [] {target}, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - protected EnumerableObjectNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public abstract class EnumerableObjectNotification : ObjectNotification> +{ + protected EnumerableObjectNotification(T target, EventMessages messages) + : base(new[] { target }, messages) + { + } + + protected EnumerableObjectNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs b/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs index 9cf69032e6..29c843945c 100644 --- a/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs +++ b/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ExportedMemberNotification : INotification { - public class ExportedMemberNotification : INotification + public ExportedMemberNotification(IMember member, MemberExportModel exported) { - public ExportedMemberNotification(IMember member, MemberExportModel exported) - { - Member = member; - Exported = exported; - } - - public IMember Member { get; } - - public MemberExportModel Exported { get; } + Member = member; + Exported = exported; } + + public IMember Member { get; } + + public MemberExportModel Exported { get; } } diff --git a/src/Umbraco.Core/Notifications/ICancelableNotification.cs b/src/Umbraco.Core/Notifications/ICancelableNotification.cs index c30e6613fe..e4d1b61309 100644 --- a/src/Umbraco.Core/Notifications/ICancelableNotification.cs +++ b/src/Umbraco.Core/Notifications/ICancelableNotification.cs @@ -1,10 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public interface ICancelableNotification : INotification { - public interface ICancelableNotification : INotification - { - bool Cancel { get; set; } - } + bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/INotification.cs b/src/Umbraco.Core/Notifications/INotification.cs index 2427da1454..fc73fba39b 100644 --- a/src/Umbraco.Core/Notifications/INotification.cs +++ b/src/Umbraco.Core/Notifications/INotification.cs @@ -1,12 +1,11 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// A marker interface to represent a notification. +/// +public interface INotification { - /// - /// A marker interface to represent a notification. - /// - public interface INotification - { - } } diff --git a/src/Umbraco.Core/Notifications/IStatefulNotification.cs b/src/Umbraco.Core/Notifications/IStatefulNotification.cs index c7319524ff..65603f5bfa 100644 --- a/src/Umbraco.Core/Notifications/IStatefulNotification.cs +++ b/src/Umbraco.Core/Notifications/IStatefulNotification.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public interface IStatefulNotification : INotification { - public interface IStatefulNotification : INotification - { - IDictionary State { get; set; } - } + IDictionary State { get; set; } } diff --git a/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs index 4b0ea6826a..8d8ea73fe6 100644 --- a/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs +++ b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. +/// +/// +public interface IUmbracoApplicationLifetimeNotification : INotification { /// - /// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. + /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). /// - /// - public interface IUmbracoApplicationLifetimeNotification : INotification - { - /// - /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). - /// - /// - /// true if Umbraco is restarting; otherwise, false. - /// - bool IsRestarting { get; } - } + /// + /// true if Umbraco is restarting; otherwise, false. + /// + bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs b/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs index 8f3538d448..62114722c1 100644 --- a/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs +++ b/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs @@ -1,15 +1,11 @@ -using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; -namespace Umbraco.Cms.Core.Notifications -{ - public class ImportedPackageNotification : StatefulNotification - { - public ImportedPackageNotification(InstallationSummary installationSummary) - { - InstallationSummary = installationSummary; - } +namespace Umbraco.Cms.Core.Notifications; - public InstallationSummary InstallationSummary { get; } - } +public class ImportedPackageNotification : StatefulNotification +{ + public ImportedPackageNotification(InstallationSummary installationSummary) => + InstallationSummary = installationSummary; + + public InstallationSummary InstallationSummary { get; } } diff --git a/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs b/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs index 7fb6c8f9fc..67a02f254c 100644 --- a/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs +++ b/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs @@ -1,16 +1,10 @@ -using Umbraco.Cms.Core.Models.Packaging; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public class ImportingPackageNotification : StatefulNotification, ICancelableNotification { - public class ImportingPackageNotification : StatefulNotification, ICancelableNotification - { - public ImportingPackageNotification(string packageName) - { - PackageName = packageName; - } + public ImportingPackageNotification(string packageName) => PackageName = packageName; - public string PackageName { get; } + public string PackageName { get; } - public bool Cancel { get; set; } - } + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs index 436d8a4fbf..8e62c68b1d 100644 --- a/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class LanguageCacheRefresherNotification : CacheRefresherNotification { - public class LanguageCacheRefresherNotification : CacheRefresherNotification + public LanguageCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public LanguageCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs b/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs index ccc17c8a90..9f435775aa 100644 --- a/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class LanguageDeletedNotification : DeletedNotification { - public class LanguageDeletedNotification : DeletedNotification + public LanguageDeletedNotification(ILanguage target, EventMessages messages) + : base(target, messages) { - public LanguageDeletedNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs b/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs index c4e4682500..1fdff6538f 100644 --- a/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class LanguageDeletingNotification : DeletingNotification - { - public LanguageDeletingNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public LanguageDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class LanguageDeletingNotification : DeletingNotification +{ + public LanguageDeletingNotification(ILanguage target, EventMessages messages) + : base(target, messages) + { + } + + public LanguageDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs b/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs index 29265c86ca..b3e58e9b83 100644 --- a/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class LanguageSavedNotification : SavedNotification - { - public LanguageSavedNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public LanguageSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class LanguageSavedNotification : SavedNotification +{ + public LanguageSavedNotification(ILanguage target, EventMessages messages) + : base(target, messages) + { + } + + public LanguageSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs b/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs index 5fcb892e25..adbba95ad4 100644 --- a/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class LanguageSavingNotification : SavingNotification - { - public LanguageSavingNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public LanguageSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class LanguageSavingNotification : SavingNotification +{ + public LanguageSavingNotification(ILanguage target, EventMessages messages) + : base(target, messages) + { + } + + public LanguageSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs index 5fb5554b1b..4d88155074 100644 --- a/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MacroCacheRefresherNotification : CacheRefresherNotification { - public class MacroCacheRefresherNotification : CacheRefresherNotification + public MacroCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MacroCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs b/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs index 237cce38fe..b42779415a 100644 --- a/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MacroDeletedNotification : DeletedNotification { - public class MacroDeletedNotification : DeletedNotification + public MacroDeletedNotification(IMacro target, EventMessages messages) + : base(target, messages) { - public MacroDeletedNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs b/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs index d36a9896bc..8d262cb8aa 100644 --- a/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MacroDeletingNotification : DeletingNotification - { - public MacroDeletingNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MacroDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MacroDeletingNotification : DeletingNotification +{ + public MacroDeletingNotification(IMacro target, EventMessages messages) + : base(target, messages) + { + } + + public MacroDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MacroSavedNotification.cs b/src/Umbraco.Core/Notifications/MacroSavedNotification.cs index 8aa776dcc6..145ac6eb3d 100644 --- a/src/Umbraco.Core/Notifications/MacroSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MacroSavedNotification : SavedNotification - { - public MacroSavedNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MacroSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MacroSavedNotification : SavedNotification +{ + public MacroSavedNotification(IMacro target, EventMessages messages) + : base(target, messages) + { + } + + public MacroSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MacroSavingNotification.cs b/src/Umbraco.Core/Notifications/MacroSavingNotification.cs index 965ee6b22e..5786b76d81 100644 --- a/src/Umbraco.Core/Notifications/MacroSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MacroSavingNotification : SavingNotification - { - public MacroSavingNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MacroSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MacroSavingNotification : SavingNotification +{ + public MacroSavingNotification(IMacro target, EventMessages messages) + : base(target, messages) + { + } + + public MacroSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs index 079475232d..9277e20423 100644 --- a/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MediaCacheRefresherNotification : CacheRefresherNotification { - public class MediaCacheRefresherNotification : CacheRefresherNotification + public MediaCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MediaCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs index b8cce7e747..643f907ab8 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaDeletedNotification : DeletedNotification { - public sealed class MediaDeletedNotification : DeletedNotification + public MediaDeletedNotification(IMedia target, EventMessages messages) + : base(target, messages) { - public MediaDeletedNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs index 6bbdb3c098..b8520e5274 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaDeletedVersionsNotification : DeletedVersionsNotification { - public sealed class MediaDeletedVersionsNotification : DeletedVersionsNotification + public MediaDeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public MediaDeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs index 358a553b28..8973b9861f 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaDeletingNotification : DeletingNotification - { - public MediaDeletingNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaDeletingNotification : DeletingNotification +{ + public MediaDeletingNotification(IMedia target, EventMessages messages) + : base(target, messages) + { + } + + public MediaDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs index fa7b3ba8e0..0d7ff01ca3 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaDeletingVersionsNotification : DeletingVersionsNotification { - public sealed class MediaDeletingVersionsNotification : DeletingVersionsNotification + public MediaDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public MediaDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs index 0862296925..3aea97d608 100644 --- a/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaEmptiedRecycleBinNotification : EmptiedRecycleBinNotification { - public sealed class MediaEmptiedRecycleBinNotification : EmptiedRecycleBinNotification + public MediaEmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) + : base(deletedEntities, messages) { - public MediaEmptiedRecycleBinNotification(IEnumerable deletedEntities,EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs index 4e257cfb38..432d480847 100644 --- a/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaEmptyingRecycleBinNotification : EmptyingRecycleBinNotification { - public sealed class MediaEmptyingRecycleBinNotification : EmptyingRecycleBinNotification + public MediaEmptyingRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) + : base(deletedEntities, messages) { - public MediaEmptyingRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaMovedNotification.cs b/src/Umbraco.Core/Notifications/MediaMovedNotification.cs index 2012f16f4b..d7cf614ed9 100644 --- a/src/Umbraco.Core/Notifications/MediaMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovedNotification : MovedNotification - { - public MediaMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovedNotification : MovedNotification +{ + public MediaMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs index 44120674bd..78d771847b 100644 --- a/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovedToRecycleBinNotification : MovedToRecycleBinNotification - { - public MediaMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovedToRecycleBinNotification : MovedToRecycleBinNotification +{ + public MediaMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaMovingNotification.cs b/src/Umbraco.Core/Notifications/MediaMovingNotification.cs index fcfb50787b..c1f5a7ab94 100644 --- a/src/Umbraco.Core/Notifications/MediaMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovingNotification : MovingNotification - { - public MediaMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovingNotification : MovingNotification +{ + public MediaMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs index 856b66c0c4..ee5618f9fb 100644 --- a/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovingToRecycleBinNotification : MovingToRecycleBinNotification - { - public MediaMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovingToRecycleBinNotification : MovingToRecycleBinNotification +{ + public MediaMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs b/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs index 1c8b8b9bea..bd4cb3efda 100644 --- a/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs @@ -1,16 +1,15 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MediaRefreshNotification : EntityRefreshNotification { - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MediaRefreshNotification : EntityRefreshNotification + public MediaRefreshNotification(IMedia target, EventMessages messages) + : base(target, messages) { - public MediaRefreshNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaSavedNotification.cs b/src/Umbraco.Core/Notifications/MediaSavedNotification.cs index addeda617e..bf9f507521 100644 --- a/src/Umbraco.Core/Notifications/MediaSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaSavedNotification : SavedNotification - { - public MediaSavedNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaSavedNotification : SavedNotification +{ + public MediaSavedNotification(IMedia target, EventMessages messages) + : base(target, messages) + { + } + + public MediaSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaSavingNotification.cs b/src/Umbraco.Core/Notifications/MediaSavingNotification.cs index 638d27c968..d902de6ba7 100644 --- a/src/Umbraco.Core/Notifications/MediaSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaSavingNotification : SavingNotification - { - public MediaSavingNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaSavingNotification : SavingNotification +{ + public MediaSavingNotification(IMedia target, EventMessages messages) + : base(target, messages) + { + } + + public MediaSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs b/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs index 00e0e6b42c..cd896cd1fc 100644 --- a/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs @@ -1,30 +1,31 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTreeChangeNotification : TreeChangeNotification { - public class MediaTreeChangeNotification : TreeChangeNotification + public MediaTreeChangeNotification(TreeChange target, EventMessages messages) + : base(target, messages) { - public MediaTreeChangeNotification(TreeChange target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTreeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } + public MediaTreeChangeNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } - public MediaTreeChangeNotification(IEnumerable target, - TreeChangeTypes changeTypes, - EventMessages messages) : base(target.Select(x => new TreeChange(x, changeTypes)), messages) - { - } + public MediaTreeChangeNotification( + IEnumerable target, + TreeChangeTypes changeTypes, + EventMessages messages) + : base(target.Select(x => new TreeChange(x, changeTypes)), messages) + { + } - public MediaTreeChangeNotification(IMedia target, TreeChangeTypes changeTypes, EventMessages messages) : base( - new TreeChange(target, changeTypes), messages) - { - } + public MediaTreeChangeNotification(IMedia target, TreeChangeTypes changeTypes, EventMessages messages) + : base(new TreeChange(target, changeTypes), messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs index 322a6bb1ab..1882c7cc74 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeChangedNotification : ContentTypeChangeNotification - { - public MediaTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeChangedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeChangedNotification : ContentTypeChangeNotification +{ + public MediaTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeChangedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs index 59c7114ca0..8ad8e1bce5 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeDeletedNotification : DeletedNotification - { - public MediaTypeDeletedNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeDeletedNotification : DeletedNotification +{ + public MediaTypeDeletedNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeDeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs index 1cb4f7c99d..a819ef0d8c 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeDeletingNotification : DeletingNotification - { - public MediaTypeDeletingNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeDeletingNotification : DeletingNotification +{ + public MediaTypeDeletingNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs index c17aa222de..f05d5fd37b 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeMovedNotification : MovedNotification - { - public MediaTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeMovedNotification : MovedNotification +{ + public MediaTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeMovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs index 43499430b0..9b7ac27c13 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeMovingNotification : MovingNotification - { - public MediaTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeMovingNotification : MovingNotification +{ + public MediaTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs index 6b59e3220e..5b6814fdb1 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MediaTypeRefreshedNotification : ContentTypeRefreshNotification - { - public MediaTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeRefreshedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MediaTypeRefreshedNotification : ContentTypeRefreshNotification +{ + public MediaTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeRefreshedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs index b4b2372b7f..17063f5252 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeSavedNotification : SavedNotification - { - public MediaTypeSavedNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeSavedNotification : SavedNotification +{ + public MediaTypeSavedNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs index 0a93f08671..46bc588b39 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeSavingNotification : SavingNotification - { - public MediaTypeSavingNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeSavingNotification : SavingNotification +{ + public MediaTypeSavingNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs index c2d920843d..46101878aa 100644 --- a/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MemberCacheRefresherNotification : CacheRefresherNotification { - public class MemberCacheRefresherNotification : CacheRefresherNotification + public MemberCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MemberCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs b/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs index 7539d6b133..b1578fd998 100644 --- a/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MemberDeletedNotification : DeletedNotification { - public sealed class MemberDeletedNotification : DeletedNotification + public MemberDeletedNotification(IMember target, EventMessages messages) + : base(target, messages) { - public MemberDeletedNotification(IMember target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs b/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs index 9d09d40e15..df599d7b08 100644 --- a/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MemberDeletingNotification : DeletingNotification - { - public MemberDeletingNotification(IMember target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MemberDeletingNotification : DeletingNotification +{ + public MemberDeletingNotification(IMember target, EventMessages messages) + : base(target, messages) + { + } + + public MemberDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs index f882b61167..333a8fbb55 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MemberGroupCacheRefresherNotification : CacheRefresherNotification { - public class MemberGroupCacheRefresherNotification : CacheRefresherNotification + public MemberGroupCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MemberGroupCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs index 8665cc5f71..528dc37254 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MemberGroupDeletedNotification : DeletedNotification { - public class MemberGroupDeletedNotification : DeletedNotification + public MemberGroupDeletedNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) { - public MemberGroupDeletedNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs index 2b0f94af64..f0ed3dc49c 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberGroupDeletingNotification : DeletingNotification - { - public MemberGroupDeletingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberGroupDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberGroupDeletingNotification : DeletingNotification +{ + public MemberGroupDeletingNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) + { + } + + public MemberGroupDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs index e5beffe76b..9f8671d923 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberGroupSavedNotification : SavedNotification - { - public MemberGroupSavedNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberGroupSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberGroupSavedNotification : SavedNotification +{ + public MemberGroupSavedNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) + { + } + + public MemberGroupSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs index a0341ab2ef..233714c542 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberGroupSavingNotification : SavingNotification - { - public MemberGroupSavingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberGroupSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberGroupSavingNotification : SavingNotification +{ + public MemberGroupSavingNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) + { + } + + public MemberGroupSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs b/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs index a22c48348f..ddab089c0b 100644 --- a/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs @@ -1,16 +1,15 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MemberRefreshNotification : EntityRefreshNotification { - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MemberRefreshNotification : EntityRefreshNotification + public MemberRefreshNotification(IMember target, EventMessages messages) + : base(target, messages) { - public MemberRefreshNotification(IMember target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberRolesNotification.cs b/src/Umbraco.Core/Notifications/MemberRolesNotification.cs index 9ea6548833..446faee237 100644 --- a/src/Umbraco.Core/Notifications/MemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberRolesNotification.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MemberRolesNotification : INotification { - public abstract class MemberRolesNotification : INotification + protected MemberRolesNotification(int[] memberIds, string[] roles) { - protected MemberRolesNotification(int[] memberIds, string[] roles) - { - MemberIds = memberIds; - Roles = roles; - } - - public int[] MemberIds { get; } - - public string[] Roles { get; } + MemberIds = memberIds; + Roles = roles; } + + public int[] MemberIds { get; } + + public string[] Roles { get; } } diff --git a/src/Umbraco.Core/Notifications/MemberSavedNotification.cs b/src/Umbraco.Core/Notifications/MemberSavedNotification.cs index 2c4f4755eb..f59f41f0ec 100644 --- a/src/Umbraco.Core/Notifications/MemberSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MemberSavedNotification : SavedNotification - { - public MemberSavedNotification(IMember target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MemberSavedNotification : SavedNotification +{ + public MemberSavedNotification(IMember target, EventMessages messages) + : base(target, messages) + { + } + + public MemberSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberSavingNotification.cs b/src/Umbraco.Core/Notifications/MemberSavingNotification.cs index fc8198c6f9..813e6f7269 100644 --- a/src/Umbraco.Core/Notifications/MemberSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MemberSavingNotification : SavingNotification - { - public MemberSavingNotification(IMember target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MemberSavingNotification : SavingNotification +{ + public MemberSavingNotification(IMember target, EventMessages messages) + : base(target, messages) + { + } + + public MemberSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs index e06de2624e..fc9e392598 100644 --- a/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs @@ -1,14 +1,8 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public class MemberTwoFactorRequestedNotification : INotification { - public class MemberTwoFactorRequestedNotification : INotification - { - public MemberTwoFactorRequestedNotification(Guid? memberKey) - { - MemberKey = memberKey; - } + public MemberTwoFactorRequestedNotification(Guid? memberKey) => MemberKey = memberKey; - public Guid? MemberKey { get; } - } + public Guid? MemberKey { get; } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs index c22908c108..cbce239394 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeChangedNotification : ContentTypeChangeNotification - { - public MemberTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeChangedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeChangedNotification : ContentTypeChangeNotification +{ + public MemberTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeChangedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs index 490db24cf3..b3061cc074 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeDeletedNotification : DeletedNotification - { - public MemberTypeDeletedNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeDeletedNotification : DeletedNotification +{ + public MemberTypeDeletedNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeDeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs index 04821eb0c2..d80fcd1c16 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeDeletingNotification : DeletingNotification - { - public MemberTypeDeletingNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeDeletingNotification : DeletingNotification +{ + public MemberTypeDeletingNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs index 8e74076119..5ab6056124 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeMovedNotification : MovedNotification - { - public MemberTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeMovedNotification : MovedNotification +{ + public MemberTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeMovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs index b4627aaf30..9b4445c171 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeMovingNotification : MovingNotification - { - public MemberTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeMovingNotification : MovingNotification +{ + public MemberTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs index 89147a523f..050c24a9e7 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MemberTypeRefreshedNotification : ContentTypeRefreshNotification - { - public MemberTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeRefreshedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MemberTypeRefreshedNotification : ContentTypeRefreshNotification +{ + public MemberTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeRefreshedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs index 768f9e8bb0..3101c794e2 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeSavedNotification : SavedNotification - { - public MemberTypeSavedNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeSavedNotification : SavedNotification +{ + public MemberTypeSavedNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs index 598aadffa4..7cfcb12b91 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeSavingNotification : SavingNotification - { - public MemberTypeSavingNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeSavingNotification : SavingNotification +{ + public MemberTypeSavingNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs b/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs index e4adadcd52..0048699e09 100644 --- a/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs +++ b/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs @@ -1,37 +1,35 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Contains event data for the event. +/// +public class ModelBindingErrorNotification : INotification { /// - /// Contains event data for the event. + /// Initializes a new instance of the class. /// - public class ModelBindingErrorNotification : INotification + public ModelBindingErrorNotification(Type sourceType, Type modelType, StringBuilder message) { - /// - /// Initializes a new instance of the class. - /// - public ModelBindingErrorNotification(Type sourceType, Type modelType, StringBuilder message) - { - SourceType = sourceType; - ModelType = modelType; - Message = message; - } - - /// - /// Gets the type of the source object. - /// - public Type SourceType { get; } - - /// - /// Gets the type of the view model. - /// - public Type ModelType { get; } - - /// - /// Gets the message string builder. - /// - /// Handlers of the event can append text to the message. - public StringBuilder Message { get; } + SourceType = sourceType; + ModelType = modelType; + Message = message; } + + /// + /// Gets the type of the source object. + /// + public Type SourceType { get; } + + /// + /// Gets the type of the view model. + /// + public Type ModelType { get; } + + /// + /// Gets the message string builder. + /// + /// Handlers of the event can append text to the message. + public StringBuilder Message { get; } } diff --git a/src/Umbraco.Core/Notifications/MovedNotification.cs b/src/Umbraco.Core/Notifications/MovedNotification.cs index 4573d5e45a..f67273a6d4 100644 --- a/src/Umbraco.Core/Notifications/MovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MovedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovedNotification : ObjectNotification>> { - public abstract class MovedNotification : ObjectNotification>> + protected MovedNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovedNotification(MoveEventInfo target, EventMessages messages) : base(new[] { target }, messages) - { - } - - protected MovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs index 1e02d30eb7..fddb0ab106 100644 --- a/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovedToRecycleBinNotification : ObjectNotification>> { - public abstract class MovedToRecycleBinNotification : ObjectNotification>> + protected MovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(new[] { target }, messages) - { - } - - protected MovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/MovingNotification.cs b/src/Umbraco.Core/Notifications/MovingNotification.cs index 6bf493fc1b..47a2ecf7bf 100644 --- a/src/Umbraco.Core/Notifications/MovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MovingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovingNotification : CancelableObjectNotification>> { - public abstract class MovingNotification : CancelableObjectNotification>> + protected MovingNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovingNotification(MoveEventInfo target, EventMessages messages) : base(new[] {target}, messages) - { - } - - protected MovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs index ef8c36ce6f..37e486e3ff 100644 --- a/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovingToRecycleBinNotification : CancelableObjectNotification>> { - public abstract class MovingToRecycleBinNotification : CancelableObjectNotification>> + protected MovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(new[] { target }, messages) - { - } - - protected MovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/NotificationExtensions.cs b/src/Umbraco.Core/Notifications/NotificationExtensions.cs index d907d3dcfa..540cf0840a 100644 --- a/src/Umbraco.Core/Notifications/NotificationExtensions.cs +++ b/src/Umbraco.Core/Notifications/NotificationExtensions.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public static class NotificationExtensions { - public static class NotificationExtensions + public static T WithState(this T notification, IDictionary? state) + where T : IStatefulNotification { - public static T WithState(this T notification, IDictionary? state) where T : IStatefulNotification - { - notification.State = state!; - return notification; - } - - public static T WithStateFrom(this T notification, TSource source) - where T : IStatefulNotification where TSource : IStatefulNotification - => notification.WithState(source.State); + notification.State = state!; + return notification; } + + public static T WithStateFrom(this T notification, TSource source) + where T : IStatefulNotification + where TSource : IStatefulNotification + => notification.WithState(source.State); } diff --git a/src/Umbraco.Core/Notifications/ObjectNotification.cs b/src/Umbraco.Core/Notifications/ObjectNotification.cs index a550754d32..e7c60c5bbc 100644 --- a/src/Umbraco.Core/Notifications/ObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/ObjectNotification.cs @@ -3,18 +3,18 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class ObjectNotification : StatefulNotification + where T : class { - public abstract class ObjectNotification : StatefulNotification where T : class + protected ObjectNotification(T target, EventMessages messages) { - protected ObjectNotification(T target, EventMessages messages) - { - Messages = messages; - Target = target; - } - - public EventMessages Messages { get; } - - protected T Target { get; } + Messages = messages; + Target = target; } + + public EventMessages Messages { get; } + + protected T Target { get; } } diff --git a/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs b/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs index 3f34c4b1c6..3fe571843d 100644 --- a/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewCreatedNotification : CreatedNotification { - public class PartialViewCreatedNotification : CreatedNotification + public PartialViewCreatedNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewCreatedNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs b/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs index 425879fb06..d53b4eb1c8 100644 --- a/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewCreatingNotification : CreatingNotification { - public class PartialViewCreatingNotification : CreatingNotification + public PartialViewCreatingNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewCreatingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs b/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs index 4ef4058b5c..29e1548bf3 100644 --- a/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewDeletedNotification : DeletedNotification { - public class PartialViewDeletedNotification : DeletedNotification + public PartialViewDeletedNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewDeletedNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs b/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs index 6473713408..26a6fa86e0 100644 --- a/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class PartialViewDeletingNotification : DeletingNotification - { - public PartialViewDeletingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PartialViewDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class PartialViewDeletingNotification : DeletingNotification +{ + public PartialViewDeletingNotification(IPartialView target, EventMessages messages) + : base(target, messages) + { + } + + public PartialViewDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs b/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs index d50ed08faf..e7d0702e02 100644 --- a/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class PartialViewSavedNotification : SavedNotification - { - public PartialViewSavedNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PartialViewSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class PartialViewSavedNotification : SavedNotification +{ + public PartialViewSavedNotification(IPartialView target, EventMessages messages) + : base(target, messages) + { + } + + public PartialViewSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs b/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs index fd2e0ee34a..ee7401c772 100644 --- a/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class PartialViewSavingNotification : SavingNotification - { - public PartialViewSavingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PartialViewSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class PartialViewSavingNotification : SavingNotification +{ + public PartialViewSavingNotification(IPartialView target, EventMessages messages) + : base(target, messages) + { + } + + public PartialViewSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs index 1e753217ab..223cf16cc3 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PublicAccessCacheRefresherNotification : CacheRefresherNotification { - public class PublicAccessCacheRefresherNotification : CacheRefresherNotification + public PublicAccessCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public PublicAccessCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs index f6aa16500a..a90601cf50 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class PublicAccessEntryDeletedNotification : DeletedNotification { - public sealed class PublicAccessEntryDeletedNotification : DeletedNotification + public PublicAccessEntryDeletedNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) { - public PublicAccessEntryDeletedNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs index 42c4c1bdb9..d135af805b 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class PublicAccessEntryDeletingNotification : DeletingNotification - { - public PublicAccessEntryDeletingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PublicAccessEntryDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class PublicAccessEntryDeletingNotification : DeletingNotification +{ + public PublicAccessEntryDeletingNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) + { + } + + public PublicAccessEntryDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs index 8c0d253500..1f92d935d7 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class PublicAccessEntrySavedNotification : SavedNotification - { - public PublicAccessEntrySavedNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PublicAccessEntrySavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class PublicAccessEntrySavedNotification : SavedNotification +{ + public PublicAccessEntrySavedNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) + { + } + + public PublicAccessEntrySavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs index 3fbd666b8d..9f9e6f8a4a 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class PublicAccessEntrySavingNotification : SavingNotification - { - public PublicAccessEntrySavingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PublicAccessEntrySavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class PublicAccessEntrySavingNotification : SavingNotification +{ + public PublicAccessEntrySavingNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) + { + } + + public PublicAccessEntrySavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs b/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs index f7af0e9b29..2d93e077c5 100644 --- a/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationDeletedNotification : DeletedNotification - { - public RelationDeletedNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationDeletedNotification : DeletedNotification +{ + public RelationDeletedNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationDeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs b/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs index 8873d95226..54b49afb54 100644 --- a/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationDeletingNotification : DeletingNotification - { - public RelationDeletingNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationDeletingNotification : DeletingNotification +{ + public RelationDeletingNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationSavedNotification.cs b/src/Umbraco.Core/Notifications/RelationSavedNotification.cs index 8b0313f87c..3a0b4d9ec8 100644 --- a/src/Umbraco.Core/Notifications/RelationSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationSavedNotification : SavedNotification - { - public RelationSavedNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationSavedNotification : SavedNotification +{ + public RelationSavedNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationSavingNotification.cs b/src/Umbraco.Core/Notifications/RelationSavingNotification.cs index 5afe71da53..069e0d5fdc 100644 --- a/src/Umbraco.Core/Notifications/RelationSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationSavingNotification : SavingNotification - { - public RelationSavingNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationSavingNotification : SavingNotification +{ + public RelationSavingNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs index ff8cf52891..1d816a4067 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class RelationTypeCacheRefresherNotification : CacheRefresherNotification { - public class RelationTypeCacheRefresherNotification : CacheRefresherNotification + public RelationTypeCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public RelationTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs index 8534edcb49..498a4c4370 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class RelationTypeDeletedNotification : DeletedNotification { - public class RelationTypeDeletedNotification : DeletedNotification + public RelationTypeDeletedNotification(IRelationType target, EventMessages messages) + : base(target, messages) { - public RelationTypeDeletedNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs index 904a82c08b..d9ba61b2b5 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationTypeDeletingNotification : DeletingNotification - { - public RelationTypeDeletingNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationTypeDeletingNotification : DeletingNotification +{ + public RelationTypeDeletingNotification(IRelationType target, EventMessages messages) + : base(target, messages) + { + } + + public RelationTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs index e2e69475d7..d0a1aaf16e 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationTypeSavedNotification : SavedNotification - { - public RelationTypeSavedNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationTypeSavedNotification : SavedNotification +{ + public RelationTypeSavedNotification(IRelationType target, EventMessages messages) + : base(target, messages) + { + } + + public RelationTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs index 2fdebe97e7..e2f7979e86 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationTypeSavingNotification : SavingNotification - { - public RelationTypeSavingNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationTypeSavingNotification : SavingNotification +{ + public RelationTypeSavingNotification(IRelationType target, EventMessages messages) + : base(target, messages) + { + } + + public RelationTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs b/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs index ed76cfbf69..4ae0a720f7 100644 --- a/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications -{ - public class RemovedMemberRolesNotification : MemberRolesNotification - { - public RemovedMemberRolesNotification(int[] memberIds, string[] roles) : base(memberIds, roles) - { +namespace Umbraco.Cms.Core.Notifications; - } +public class RemovedMemberRolesNotification : MemberRolesNotification +{ + public RemovedMemberRolesNotification(int[] memberIds, string[] roles) + : base(memberIds, roles) + { } } diff --git a/src/Umbraco.Core/Notifications/RenamedNotification.cs b/src/Umbraco.Core/Notifications/RenamedNotification.cs index 724069aba7..ab25fbdeb9 100644 --- a/src/Umbraco.Core/Notifications/RenamedNotification.cs +++ b/src/Umbraco.Core/Notifications/RenamedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class RenamedNotification : EnumerableObjectNotification { - public abstract class RenamedNotification : EnumerableObjectNotification + protected RenamedNotification(T target, EventMessages messages) + : base(target, messages) { - protected RenamedNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected RenamedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable Entities => Target; } + + protected RenamedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable Entities => Target; } diff --git a/src/Umbraco.Core/Notifications/RenamingNotification.cs b/src/Umbraco.Core/Notifications/RenamingNotification.cs index 1e4184bc3d..4f15827ae4 100644 --- a/src/Umbraco.Core/Notifications/RenamingNotification.cs +++ b/src/Umbraco.Core/Notifications/RenamingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class RenamingNotification : CancelableEnumerableObjectNotification { - public abstract class RenamingNotification : CancelableEnumerableObjectNotification + protected RenamingNotification(T target, EventMessages messages) + : base(target, messages) { - protected RenamingNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected RenamingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable Entities => Target; } + + protected RenamingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable Entities => Target; } diff --git a/src/Umbraco.Core/Notifications/RolledBackNotification.cs b/src/Umbraco.Core/Notifications/RolledBackNotification.cs index fded45c6b1..280f55538e 100644 --- a/src/Umbraco.Core/Notifications/RolledBackNotification.cs +++ b/src/Umbraco.Core/Notifications/RolledBackNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class RolledBackNotification : ObjectNotification where T : class - { - protected RolledBackNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T Entity => Target; +public abstract class RolledBackNotification : ObjectNotification + where T : class +{ + protected RolledBackNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/RollingBackNotification.cs b/src/Umbraco.Core/Notifications/RollingBackNotification.cs index 1064a7897c..3d06d443ea 100644 --- a/src/Umbraco.Core/Notifications/RollingBackNotification.cs +++ b/src/Umbraco.Core/Notifications/RollingBackNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class RollingBackNotification : CancelableObjectNotification where T : class - { - protected RollingBackNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T Entity => Target; +public abstract class RollingBackNotification : CancelableObjectNotification + where T : class +{ + protected RollingBackNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs b/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs index c8b2d8e0d6..b5169aa0ab 100644 --- a/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs +++ b/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used for notifying when an Umbraco request is being built +/// +public class RoutingRequestNotification : INotification { /// - /// Used for notifying when an Umbraco request is being built + /// Initializes a new instance of the class. /// - public class RoutingRequestNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public RoutingRequestNotification(IPublishedRequestBuilder requestBuilder) => RequestBuilder = requestBuilder; + public RoutingRequestNotification(IPublishedRequestBuilder requestBuilder) => RequestBuilder = requestBuilder; - /// - /// Gets the - /// - public IPublishedRequestBuilder RequestBuilder { get; } - } + /// + /// Gets the + /// + public IPublishedRequestBuilder RequestBuilder { get; } } diff --git a/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs b/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs index f638ec2d3c..e0ef991e70 100644 --- a/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs +++ b/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used to notify when the core runtime can do an unattended install. +/// +/// +/// It is entirely up to the handler to determine if an unattended installation should occur and +/// to perform the logic. +/// +public class RuntimeUnattendedInstallNotification : INotification { - /// - /// Used to notify when the core runtime can do an unattended install. - /// - /// - /// It is entirely up to the handler to determine if an unattended installation should occur and - /// to perform the logic. - /// - public class RuntimeUnattendedInstallNotification : INotification - { - } } diff --git a/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs b/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs index 4d676f68ce..fd1fa02113 100644 --- a/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs +++ b/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs @@ -1,26 +1,24 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used to notify when the core runtime can do an unattended upgrade. +/// +/// +/// It is entirely up to the handler to determine if an unattended upgrade should occur and +/// to perform the logic. +/// +public class RuntimeUnattendedUpgradeNotification : INotification { + public enum UpgradeResult + { + NotRequired = 0, + HasErrors = 1, + CoreUpgradeComplete = 100, + PackageMigrationComplete = 101, + } /// - /// Used to notify when the core runtime can do an unattended upgrade. + /// Gets/sets the result of the unattended upgrade /// - /// - /// It is entirely up to the handler to determine if an unattended upgrade should occur and - /// to perform the logic. - /// - public class RuntimeUnattendedUpgradeNotification : INotification - { - /// - /// Gets/sets the result of the unattended upgrade - /// - public UpgradeResult UnattendedUpgradeResult { get; set; } = UpgradeResult.NotRequired; - - public enum UpgradeResult - { - NotRequired = 0, - HasErrors = 1, - CoreUpgradeComplete = 100, - PackageMigrationComplete = 101 - } - } + public UpgradeResult UnattendedUpgradeResult { get; set; } = UpgradeResult.NotRequired; } diff --git a/src/Umbraco.Core/Notifications/SavedNotification.cs b/src/Umbraco.Core/Notifications/SavedNotification.cs index 0a9af8c1ff..655b9b66d1 100644 --- a/src/Umbraco.Core/Notifications/SavedNotification.cs +++ b/src/Umbraco.Core/Notifications/SavedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class SavedNotification : EnumerableObjectNotification { - public abstract class SavedNotification : EnumerableObjectNotification + protected SavedNotification(T target, EventMessages messages) + : base(target, messages) { - protected SavedNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected SavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable SavedEntities => Target; } + + protected SavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable SavedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/SavingNotification.cs b/src/Umbraco.Core/Notifications/SavingNotification.cs index 34962f5396..9724d4580a 100644 --- a/src/Umbraco.Core/Notifications/SavingNotification.cs +++ b/src/Umbraco.Core/Notifications/SavingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class SavingNotification : CancelableEnumerableObjectNotification { - public abstract class SavingNotification : CancelableEnumerableObjectNotification + protected SavingNotification(T target, EventMessages messages) + : base(target, messages) { - protected SavingNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected SavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable SavedEntities => Target; } + + protected SavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable SavedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs b/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs index 307ae2103c..f72af376c3 100644 --- a/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs +++ b/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs @@ -1,18 +1,17 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class ScopedEntityRemoveNotification : ObjectNotification - { - public ScopedEntityRemoveNotification(IContentBase target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContentBase Entity => Target; +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class ScopedEntityRemoveNotification : ObjectNotification +{ + public ScopedEntityRemoveNotification(IContentBase target, EventMessages messages) + : base(target, messages) + { } + + public IContentBase Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs b/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs index 650f2d0564..3ca5f1dc42 100644 --- a/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ScriptDeletedNotification : DeletedNotification { - public class ScriptDeletedNotification : DeletedNotification + public ScriptDeletedNotification(IScript target, EventMessages messages) + : base(target, messages) { - public ScriptDeletedNotification(IScript target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs b/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs index 085c98d600..946dc7f750 100644 --- a/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ScriptDeletingNotification : DeletingNotification - { - public ScriptDeletingNotification(IScript target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ScriptDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ScriptDeletingNotification : DeletingNotification +{ + public ScriptDeletingNotification(IScript target, EventMessages messages) + : base(target, messages) + { + } + + public ScriptDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs b/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs index 6ccb9f1446..2a292383e9 100644 --- a/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ScriptSavedNotification : SavedNotification - { - public ScriptSavedNotification(IScript target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ScriptSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ScriptSavedNotification : SavedNotification +{ + public ScriptSavedNotification(IScript target, EventMessages messages) + : base(target, messages) + { + } + + public ScriptSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs b/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs index 92ad0ded4e..3ab2b13ce4 100644 --- a/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ScriptSavingNotification : SavingNotification - { - public ScriptSavingNotification(IScript target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ScriptSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ScriptSavingNotification : SavingNotification +{ + public ScriptSavingNotification(IScript target, EventMessages messages) + : base(target, messages) + { + } + + public ScriptSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/SendEmailNotification.cs b/src/Umbraco.Core/Notifications/SendEmailNotification.cs index f87a2a0ba8..66d7ee038a 100644 --- a/src/Umbraco.Core/Notifications/SendEmailNotification.cs +++ b/src/Umbraco.Core/Notifications/SendEmailNotification.cs @@ -1,30 +1,29 @@ using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendEmailNotification : INotification { - public class SendEmailNotification : INotification + public SendEmailNotification(NotificationEmailModel message, string emailType) { - public SendEmailNotification(NotificationEmailModel message, string emailType) - { - Message = message; - EmailType = emailType; - } - - public NotificationEmailModel Message { get; } - - /// - /// Some metadata about the email which can be used by handlers to determine if they should handle the email or not - /// - public string EmailType { get; } - - /// - /// Call to tell Umbraco that the email sending is handled. - /// - public void HandleEmail() => IsHandled = true; - - /// - /// Returns true if the email sending is handled. - /// - public bool IsHandled { get; private set; } + Message = message; + EmailType = emailType; } + + public NotificationEmailModel Message { get; } + + /// + /// Some metadata about the email which can be used by handlers to determine if they should handle the email or not + /// + public string EmailType { get; } + + /// + /// Returns true if the email sending is handled. + /// + public bool IsHandled { get; private set; } + + /// + /// Call to tell Umbraco that the email sending is handled. + /// + public void HandleEmail() => IsHandled = true; } diff --git a/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs b/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs index 07ab3c3626..ff57f9c902 100644 --- a/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs @@ -1,19 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingAllowedChildrenNotification : INotification { - public class SendingAllowedChildrenNotification : INotification + public SendingAllowedChildrenNotification(IEnumerable children, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public IEnumerable Children { get; set; } - - public SendingAllowedChildrenNotification(IEnumerable children, IUmbracoContext umbracoContext) - { - UmbracoContext = umbracoContext; - Children = children; - } + UmbracoContext = umbracoContext; + Children = children; } + + public IUmbracoContext UmbracoContext { get; } + + public IEnumerable Children { get; set; } } diff --git a/src/Umbraco.Core/Notifications/SendingContentNotification.cs b/src/Umbraco.Core/Notifications/SendingContentNotification.cs index 4d8d93ce75..a42fefca68 100644 --- a/src/Umbraco.Core/Notifications/SendingContentNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingContentNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingContentNotification : INotification { - public class SendingContentNotification : INotification + public SendingContentNotification(ContentItemDisplay content, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public ContentItemDisplay Content { get; } - - public SendingContentNotification(ContentItemDisplay content, IUmbracoContext umbracoContext) - { - Content = content; - UmbracoContext = umbracoContext; - } + Content = content; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public ContentItemDisplay Content { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs b/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs index b81339fcbf..886e257529 100644 --- a/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingDashboardsNotification : INotification { - public class SendingDashboardsNotification : INotification + public SendingDashboardsNotification(IEnumerable> dashboards, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public IEnumerable> Dashboards { get; } - - public SendingDashboardsNotification(IEnumerable> dashboards, IUmbracoContext umbracoContext) - { - Dashboards = dashboards; - UmbracoContext = umbracoContext; - } + Dashboards = dashboards; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public IEnumerable> Dashboards { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingMediaNotification.cs b/src/Umbraco.Core/Notifications/SendingMediaNotification.cs index 2fd8f65a4d..cca282b3ea 100644 --- a/src/Umbraco.Core/Notifications/SendingMediaNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingMediaNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingMediaNotification : INotification { - public class SendingMediaNotification : INotification + public SendingMediaNotification(MediaItemDisplay media, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public MediaItemDisplay Media { get; } - - public SendingMediaNotification(MediaItemDisplay media, IUmbracoContext umbracoContext) - { - Media = media; - UmbracoContext = umbracoContext; - } + Media = media; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public MediaItemDisplay Media { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingMemberNotification.cs b/src/Umbraco.Core/Notifications/SendingMemberNotification.cs index cc868836f9..e9e03a868f 100644 --- a/src/Umbraco.Core/Notifications/SendingMemberNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingMemberNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingMemberNotification : INotification { - public class SendingMemberNotification : INotification + public SendingMemberNotification(MemberDisplay member, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public MemberDisplay Member { get; } - - public SendingMemberNotification(MemberDisplay member, IUmbracoContext umbracoContext) - { - Member = member; - UmbracoContext = umbracoContext; - } + Member = member; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public MemberDisplay Member { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingUserNotification.cs b/src/Umbraco.Core/Notifications/SendingUserNotification.cs index 9e3422f1d9..da46ec749e 100644 --- a/src/Umbraco.Core/Notifications/SendingUserNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingUserNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingUserNotification : INotification { - public class SendingUserNotification : INotification + public SendingUserNotification(UserDisplay user, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public UserDisplay User { get; } - - public SendingUserNotification(UserDisplay user, IUmbracoContext umbracoContext) - { - User = user; - UmbracoContext = umbracoContext; - } + User = user; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public UserDisplay User { get; } } diff --git a/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs b/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs index 7fa83a5a6d..0171009bf2 100644 --- a/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs +++ b/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +/// +/// A notification for when server variables are parsing +/// +public class ServerVariablesParsingNotification : INotification { /// - /// A notification for when server variables are parsing + /// Initializes a new instance of the class. /// - public class ServerVariablesParsingNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public ServerVariablesParsingNotification(IDictionary serverVariables) => ServerVariables = serverVariables; + public ServerVariablesParsingNotification(IDictionary serverVariables) => + ServerVariables = serverVariables; - /// - /// Gets a mutable dictionary of server variables - /// - public IDictionary ServerVariables { get; } - } + /// + /// Gets a mutable dictionary of server variables + /// + public IDictionary ServerVariables { get; } } diff --git a/src/Umbraco.Core/Notifications/SortedNotification.cs b/src/Umbraco.Core/Notifications/SortedNotification.cs index ffc50d6bc9..49910f8223 100644 --- a/src/Umbraco.Core/Notifications/SortedNotification.cs +++ b/src/Umbraco.Core/Notifications/SortedNotification.cs @@ -1,17 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class SortedNotification : EnumerableObjectNotification - { - protected SortedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IEnumerable SortedEntities => Target; +public abstract class SortedNotification : EnumerableObjectNotification +{ + protected SortedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } + + public IEnumerable SortedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/SortingNotification.cs b/src/Umbraco.Core/Notifications/SortingNotification.cs index 1801bfa656..26e735f91b 100644 --- a/src/Umbraco.Core/Notifications/SortingNotification.cs +++ b/src/Umbraco.Core/Notifications/SortingNotification.cs @@ -1,17 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class SortingNotification : CancelableEnumerableObjectNotification - { - protected SortingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IEnumerable SortedEntities => Target; +public abstract class SortingNotification : CancelableEnumerableObjectNotification +{ + protected SortingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } + + public IEnumerable SortedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/StatefulNotification.cs b/src/Umbraco.Core/Notifications/StatefulNotification.cs index 15ee320a40..5f84000d48 100644 --- a/src/Umbraco.Core/Notifications/StatefulNotification.cs +++ b/src/Umbraco.Core/Notifications/StatefulNotification.cs @@ -1,21 +1,18 @@ // Copyright (c) Umbraco. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public abstract class StatefulNotification : IStatefulNotification { - public abstract class StatefulNotification : IStatefulNotification - { - private IDictionary? _state; + private IDictionary? _state; - /// - /// This can be used by event subscribers to store state in the notification so they easily deal with custom state data between - /// a starting ("ing") and an ending ("ed") notification - /// - public IDictionary State - { - get => _state ??= new Dictionary(); - set => _state = value; - } + /// + /// This can be used by event subscribers to store state in the notification so they easily deal with custom state data + /// between a starting ("ing") and an ending ("ed") notification + /// + public IDictionary State + { + get => _state ??= new Dictionary(); + set => _state = value; } } diff --git a/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs b/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs index 743cadab63..4b359d60ec 100644 --- a/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class StylesheetDeletedNotification : DeletedNotification { - public class StylesheetDeletedNotification : DeletedNotification + public StylesheetDeletedNotification(IStylesheet target, EventMessages messages) + : base(target, messages) { - public StylesheetDeletedNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs b/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs index 8a0c411b13..8689363577 100644 --- a/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class StylesheetDeletingNotification : DeletingNotification - { - public StylesheetDeletingNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public StylesheetDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class StylesheetDeletingNotification : DeletingNotification +{ + public StylesheetDeletingNotification(IStylesheet target, EventMessages messages) + : base(target, messages) + { + } + + public StylesheetDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs b/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs index 0ceeb209e0..2f12bebe15 100644 --- a/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class StylesheetSavedNotification : SavedNotification - { - public StylesheetSavedNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public StylesheetSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class StylesheetSavedNotification : SavedNotification +{ + public StylesheetSavedNotification(IStylesheet target, EventMessages messages) + : base(target, messages) + { + } + + public StylesheetSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs b/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs index d08bdebac4..0d6804a76c 100644 --- a/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class StylesheetSavingNotification : SavingNotification - { - public StylesheetSavingNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public StylesheetSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class StylesheetSavingNotification : SavingNotification +{ + public StylesheetSavingNotification(IStylesheet target, EventMessages messages) + : base(target, messages) + { + } + + public StylesheetSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs index 689d2a52ff..a8b119390f 100644 --- a/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs @@ -1,11 +1,11 @@ using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateCacheRefresherNotification : CacheRefresherNotification { - public class TemplateCacheRefresherNotification : CacheRefresherNotification + public TemplateCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public TemplateCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs b/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs index 01d6dc7e6d..1bab7d2dc5 100644 --- a/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateDeletedNotification : DeletedNotification { - public class TemplateDeletedNotification : DeletedNotification + public TemplateDeletedNotification(ITemplate target, EventMessages messages) + : base(target, messages) { - public TemplateDeletedNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs b/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs index 6434c47c46..791f43d116 100644 --- a/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class TemplateDeletingNotification : DeletingNotification - { - public TemplateDeletingNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public TemplateDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class TemplateDeletingNotification : DeletingNotification +{ + public TemplateDeletingNotification(ITemplate target, EventMessages messages) + : base(target, messages) + { + } + + public TemplateDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs b/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs index ad75a32c02..8b51e795d4 100644 --- a/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs @@ -1,68 +1,69 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateSavedNotification : SavedNotification { - public class TemplateSavedNotification : SavedNotification + private const string TemplateForContentTypeKey = "CreateTemplateForContentType"; + private const string ContentTypeAliasKey = "ContentTypeAlias"; + + public TemplateSavedNotification(ITemplate target, EventMessages messages) + : base(target, messages) { - private const string s_templateForContentTypeKey = "CreateTemplateForContentType"; - private const string s_contentTypeAliasKey = "ContentTypeAlias"; + } - public TemplateSavedNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } + public TemplateSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } - public TemplateSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + public bool CreateTemplateForContentType + { + get { - } - - public bool CreateTemplateForContentType - { - get + if (State?.TryGetValue(TemplateForContentTypeKey, out var result) ?? false) { - if (State?.TryGetValue(s_templateForContentTypeKey, out var result) ?? false) + if (result is not bool createTemplate) { - if (result is not bool createTemplate) - { - return false; - } - - return createTemplate; + return false; } - return false; - } - set - { - if (!value is bool && State is not null) - { - State[s_templateForContentTypeKey] = value; - } - } - } - - public string? ContentTypeAlias - { - get - { - if (State?.TryGetValue(s_contentTypeAliasKey, out var result) ?? false) - { - return result as string; - } - - return null; + return createTemplate; } - set + return false; + } + + set + { + if (!value is bool && State is not null) { - if (value is not null && State is not null) - { - State[s_contentTypeAliasKey] = value; - } + State[TemplateForContentTypeKey] = value; + } + } + } + + public string? ContentTypeAlias + { + get + { + if (State?.TryGetValue(ContentTypeAliasKey, out var result) ?? false) + { + return result as string; + } + + return null; + } + + set + { + if (value is not null && State is not null) + { + State[ContentTypeAliasKey] = value; } } } diff --git a/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs b/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs index 95a681d2f8..45a325feed 100644 --- a/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs @@ -1,83 +1,83 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateSavingNotification : SavingNotification { - public class TemplateSavingNotification : SavingNotification + private const string TemplateForContentTypeKey = "CreateTemplateForContentType"; + private const string ContentTypeAliasKey = "ContentTypeAlias"; + + public TemplateSavingNotification(ITemplate target, EventMessages messages) + : base(target, messages) { - private const string s_templateForContentTypeKey = "CreateTemplateForContentType"; - private const string s_contentTypeAliasKey = "ContentTypeAlias"; + } - public TemplateSavingNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } + public TemplateSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } - public TemplateSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } + public TemplateSavingNotification(ITemplate target, EventMessages messages, bool createTemplateForContentType, string contentTypeAlias) + : base(target, messages) + { + CreateTemplateForContentType = createTemplateForContentType; + ContentTypeAlias = contentTypeAlias; + } - public TemplateSavingNotification(ITemplate target, EventMessages messages, bool createTemplateForContentType, - string contentTypeAlias) : base(target, messages) - { - CreateTemplateForContentType = createTemplateForContentType; - ContentTypeAlias = contentTypeAlias; - } + public TemplateSavingNotification(IEnumerable target, EventMessages messages, bool createTemplateForContentType, string contentTypeAlias) + : base(target, messages) + { + CreateTemplateForContentType = createTemplateForContentType; + ContentTypeAlias = contentTypeAlias; + } - public TemplateSavingNotification(IEnumerable target, EventMessages messages, - bool createTemplateForContentType, - string contentTypeAlias) : base(target, messages) + public bool CreateTemplateForContentType + { + get { - CreateTemplateForContentType = createTemplateForContentType; - ContentTypeAlias = contentTypeAlias; - } - - public bool CreateTemplateForContentType - { - get + if (State?.TryGetValue(TemplateForContentTypeKey, out var result) ?? false) { - if (State?.TryGetValue(s_templateForContentTypeKey, out var result) ?? false) + if (result is not bool createTemplate) { - if (result is not bool createTemplate) - { - return false; - } - - return createTemplate; + return false; } - return false; - } - set - { - if (!value is bool && State is not null) - { - State[s_templateForContentTypeKey] = value; - } - } - } - - public string? ContentTypeAlias - { - get - { - if (State?.TryGetValue(s_contentTypeAliasKey, out var result) ?? false) - { - return result as string; - } - - return null; + return createTemplate; } - set + return false; + } + + set + { + if (!value is bool && State is not null) { - if (value is not null && State is not null) - { - State[s_contentTypeAliasKey] = value; - } + State[TemplateForContentTypeKey] = value; + } + } + } + + public string? ContentTypeAlias + { + get + { + if (State?.TryGetValue(ContentTypeAliasKey, out var result) ?? false) + { + return result as string; + } + + return null; + } + + set + { + if (value is not null && State is not null) + { + State[ContentTypeAliasKey] = value; } } } diff --git a/src/Umbraco.Core/Notifications/TreeChangeNotification.cs b/src/Umbraco.Core/Notifications/TreeChangeNotification.cs index bdbd0fc044..2187f72659 100644 --- a/src/Umbraco.Core/Notifications/TreeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/TreeChangeNotification.cs @@ -1,19 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class TreeChangeNotification : EnumerableObjectNotification> { - public abstract class TreeChangeNotification : EnumerableObjectNotification> + protected TreeChangeNotification(TreeChange target, EventMessages messages) + : base(target, messages) { - protected TreeChangeNotification(TreeChange target, EventMessages messages) : base(target, messages) - { - } - - protected TreeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> Changes => Target; } + + protected TreeChangeNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> Changes => Target; } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs index 196af7dfe1..1e3f2b7bfd 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. +/// +/// +public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification { /// - /// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification - { - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index fa5cf1f5e8..49eaac02f2 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,9 +1,8 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications -{ - /// - /// Notification that occurs at the very end of the Umbraco boot process (after all s are initialized). +/// +/// Notification that occurs at the very end of the Umbraco boot process (after all s are +/// initialized). /// /// public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification @@ -19,15 +18,14 @@ namespace Umbraco.Cms.Core.Notifications IsRestarting = isRestarting; } - /// - /// Gets the runtime level. - /// - /// - /// The runtime level. - /// - public RuntimeLevel RuntimeLevel { get; } + /// + /// Gets the runtime level. + /// + /// + /// The runtime level. + /// + public RuntimeLevel RuntimeLevel { get; } - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs index c6dac40a26..ce9936a137 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification that occurs when Umbraco has completely shutdown. +/// +/// +public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification { /// - /// Notification that occurs when Umbraco has completely shutdown. + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification - { - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index 999b531bc5..8face75954 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,7 +1,6 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; + -namespace Umbraco.Cms.Core.Notifications -{ /// /// Notification that occurs when Umbraco is shutting down (after all s are terminated). /// @@ -14,7 +13,6 @@ namespace Umbraco.Cms.Core.Notifications /// Indicates whether Umbraco is restarting. public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs b/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs index 76683f8d65..fedbb6c35b 100644 --- a/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs @@ -3,21 +3,20 @@ using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification raised on each request begin. +/// +public class UmbracoRequestBeginNotification : INotification { /// - /// Notification raised on each request begin. + /// Initializes a new instance of the class. /// - public class UmbracoRequestBeginNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestBeginNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; + public UmbracoRequestBeginNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; - /// - /// Gets the - /// - public IUmbracoContext UmbracoContext { get; } - } + /// + /// Gets the + /// + public IUmbracoContext UmbracoContext { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs b/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs index 27fb6ff09d..a3f9771153 100644 --- a/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs @@ -3,21 +3,20 @@ using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification raised on each request end. +/// +public class UmbracoRequestEndNotification : INotification { /// - /// Notification raised on each request end. + /// Initializes a new instance of the class. /// - public class UmbracoRequestEndNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestEndNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; + public UmbracoRequestEndNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; - /// - /// Gets the - /// - public IUmbracoContext UmbracoContext { get; } - } + /// + /// Gets the + /// + public IUmbracoContext UmbracoContext { get; } } diff --git a/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs b/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs index 7f9b239ce2..c2e4f27b49 100644 --- a/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs +++ b/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used to notify that an Unattended install has completed +/// +public class UnattendedInstallNotification : INotification { - /// - /// Used to notify that an Unattended install has completed - /// - public class UnattendedInstallNotification : INotification - { - } } diff --git a/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs index 4181d74dd7..589a2df682 100644 --- a/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs @@ -1,11 +1,11 @@ using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserCacheRefresherNotification : CacheRefresherNotification { - public class UserCacheRefresherNotification : CacheRefresherNotification + public UserCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public UserCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserDeletedNotification.cs b/src/Umbraco.Core/Notifications/UserDeletedNotification.cs index c272e51b22..a5d89bf167 100644 --- a/src/Umbraco.Core/Notifications/UserDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserDeletedNotification : DeletedNotification { - public sealed class UserDeletedNotification : DeletedNotification + public UserDeletedNotification(IUser target, EventMessages messages) + : base(target, messages) { - public UserDeletedNotification(IUser target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserDeletingNotification.cs b/src/Umbraco.Core/Notifications/UserDeletingNotification.cs index febfa27d94..611f8aa0ea 100644 --- a/src/Umbraco.Core/Notifications/UserDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserDeletingNotification : DeletingNotification - { - public UserDeletingNotification(IUser target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserDeletingNotification : DeletingNotification +{ + public UserDeletingNotification(IUser target, EventMessages messages) + : base(target, messages) + { + } + + public UserDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs b/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs index b4e93f8b67..b40e902e10 100644 --- a/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserForgotPasswordChangedNotification : UserNotification { - public class UserForgotPasswordChangedNotification : UserNotification + public UserForgotPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserForgotPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs b/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs index 608e5c0f63..6181a33809 100644 --- a/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserForgotPasswordRequestedNotification : UserNotification { - public class UserForgotPasswordRequestedNotification : UserNotification + public UserForgotPasswordRequestedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserForgotPasswordRequestedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs index 7aca0d5edb..d8e519ee9d 100644 --- a/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs @@ -1,11 +1,11 @@ using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserGroupCacheRefresherNotification : CacheRefresherNotification { - public class UserGroupCacheRefresherNotification : CacheRefresherNotification + public UserGroupCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public UserGroupCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs b/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs index 9877d95441..0555611f3a 100644 --- a/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserGroupDeletedNotification : DeletedNotification { - public sealed class UserGroupDeletedNotification : DeletedNotification + public UserGroupDeletedNotification(IUserGroup target, EventMessages messages) + : base(target, messages) { - public UserGroupDeletedNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs b/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs index af0e8d76d6..aea73393ab 100644 --- a/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupDeletingNotification : DeletingNotification - { - public UserGroupDeletingNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupDeletingNotification : DeletingNotification +{ + public UserGroupDeletingNotification(IUserGroup target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs b/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs index fee23c06ea..aa4484c3d3 100644 --- a/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupSavedNotification : SavedNotification - { - public UserGroupSavedNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupSavedNotification : SavedNotification +{ + public UserGroupSavedNotification(IUserGroup target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs b/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs index 0dc074bfdc..06c82c0298 100644 --- a/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupSavingNotification : SavingNotification - { - public UserGroupSavingNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupSavingNotification : SavingNotification +{ + public UserGroupSavingNotification(IUserGroup target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs index 5e239660aa..399d194690 100644 --- a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs @@ -1,19 +1,19 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupWithUsersSavedNotification : SavedNotification - { - public UserGroupWithUsersSavedNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupWithUsersSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupWithUsersSavedNotification : SavedNotification +{ + public UserGroupWithUsersSavedNotification(UserGroupWithUsers target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupWithUsersSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs index f3dd362c20..c34d66841c 100644 --- a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs @@ -1,19 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupWithUsersSavingNotification : SavingNotification - { - public UserGroupWithUsersSavingNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupWithUsersSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupWithUsersSavingNotification : SavingNotification +{ + public UserGroupWithUsersSavingNotification(UserGroupWithUsers target, EventMessages messages) + : base( + target, + messages) + { + } + + public UserGroupWithUsersSavingNotification(IEnumerable target, EventMessages messages) + : base( + target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserLockedNotification.cs b/src/Umbraco.Core/Notifications/UserLockedNotification.cs index b7485d9852..81fc798f63 100644 --- a/src/Umbraco.Core/Notifications/UserLockedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLockedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLockedNotification : UserNotification { - public class UserLockedNotification : UserNotification + public UserLockedNotification(string ipAddress, string? affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserLockedNotification(string ipAddress, string? affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs b/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs index ff07b57832..a8cb3e9cc4 100644 --- a/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLoginFailedNotification : UserNotification { - public class UserLoginFailedNotification : UserNotification + public UserLoginFailedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base( + ipAddress, affectedUserId, performingUserId) { - public UserLoginFailedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs b/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs index 5a975a1951..57f037712c 100644 --- a/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLoginRequiresVerificationNotification : UserNotification { - public class UserLoginRequiresVerificationNotification : UserNotification + public UserLoginRequiresVerificationNotification(string ipAddress, string? affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserLoginRequiresVerificationNotification(string ipAddress, string? affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs b/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs index e9b79c68fe..5b20ca48ef 100644 --- a/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLoginSuccessNotification : UserNotification { - public class UserLoginSuccessNotification : UserNotification + public UserLoginSuccessNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserLoginSuccessNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs b/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs index 92e7dea03f..c93d42accf 100644 --- a/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Notifications -{ - public class UserLogoutSuccessNotification : UserNotification - { - public UserLogoutSuccessNotification(string ipAddress, string? affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } +namespace Umbraco.Cms.Core.Notifications; - public string? SignOutRedirectUrl { get; set; } +public class UserLogoutSuccessNotification : UserNotification +{ + public UserLogoutSuccessNotification(string ipAddress, string? affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) + { } + + public string? SignOutRedirectUrl { get; set; } } diff --git a/src/Umbraco.Core/Notifications/UserNotification.cs b/src/Umbraco.Core/Notifications/UserNotification.cs index f0ce83c8fb..6141cdf389 100644 --- a/src/Umbraco.Core/Notifications/UserNotification.cs +++ b/src/Umbraco.Core/Notifications/UserNotification.cs @@ -1,35 +1,32 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public abstract class UserNotification : INotification { - public abstract class UserNotification : INotification + protected UserNotification(string ipAddress, string? affectedUserId, string performingUserId) { - protected UserNotification(string ipAddress, string? affectedUserId, string performingUserId) - { - DateTimeUtc = DateTime.UtcNow; - IpAddress = ipAddress; - AffectedUserId = affectedUserId; - PerformingUserId = performingUserId; - } - - /// - /// Current date/time in UTC format - /// - public DateTime DateTimeUtc { get; } - - /// - /// The source IP address of the user performing the action - /// - public string IpAddress { get; } - - /// - /// The user affected by the event raised - /// - public string? AffectedUserId { get; } - - /// - /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 - /// - public string PerformingUserId { get; } + DateTimeUtc = DateTime.UtcNow; + IpAddress = ipAddress; + AffectedUserId = affectedUserId; + PerformingUserId = performingUserId; } + + /// + /// Current date/time in UTC format + /// + public DateTime DateTimeUtc { get; } + + /// + /// The source IP address of the user performing the action + /// + public string IpAddress { get; } + + /// + /// The user affected by the event raised + /// + public string? AffectedUserId { get; } + + /// + /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 + /// + public string PerformingUserId { get; } } diff --git a/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs b/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs index 098be36867..a7cd1e51ae 100644 --- a/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserPasswordChangedNotification : UserNotification { - public class UserPasswordChangedNotification : UserNotification + public UserPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base( + ipAddress, affectedUserId, performingUserId) { - public UserPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs b/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs index fc60eef61e..8b23b5aa4f 100644 --- a/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs +++ b/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserPasswordResetNotification : UserNotification { - public class UserPasswordResetNotification : UserNotification + public UserPasswordResetNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserPasswordResetNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs b/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs index 5cd03cc140..f1cce2df63 100644 --- a/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs +++ b/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserResetAccessFailedCountNotification : UserNotification { - public class UserResetAccessFailedCountNotification : UserNotification + public UserResetAccessFailedCountNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserResetAccessFailedCountNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserSavedNotification.cs b/src/Umbraco.Core/Notifications/UserSavedNotification.cs index 892218af82..8292cb9f6d 100644 --- a/src/Umbraco.Core/Notifications/UserSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserSavedNotification : SavedNotification - { - public UserSavedNotification(IUser target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserSavedNotification : SavedNotification +{ + public UserSavedNotification(IUser target, EventMessages messages) + : base(target, messages) + { + } + + public UserSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserSavingNotification.cs b/src/Umbraco.Core/Notifications/UserSavingNotification.cs index 57c0d867fa..3760f02881 100644 --- a/src/Umbraco.Core/Notifications/UserSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserSavingNotification : SavingNotification - { - public UserSavingNotification(IUser target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserSavingNotification : SavingNotification +{ + public UserSavingNotification(IUser target, EventMessages messages) + : base(target, messages) + { + } + + public UserSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs b/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs index ccb07c593c..1eb6d774d0 100644 --- a/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs @@ -1,14 +1,8 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public class UserTwoFactorRequestedNotification : INotification { - public class UserTwoFactorRequestedNotification : INotification - { - public UserTwoFactorRequestedNotification(Guid userKey) - { - UserKey = userKey; - } + public UserTwoFactorRequestedNotification(Guid userKey) => UserKey = userKey; - public Guid UserKey { get; } - } + public Guid UserKey { get; } } diff --git a/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs b/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs index 0c6cc7b9fd..7883595733 100644 --- a/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserUnlockedNotification : UserNotification { - public class UserUnlockedNotification : UserNotification + public UserUnlockedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserUnlockedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs b/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs index 16cd4ad0a4..fdb76f4bc2 100644 --- a/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs +++ b/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs @@ -1,73 +1,86 @@ -using System; -using System.IO; -using System.Linq; using System.Xml.Linq; using Umbraco.Cms.Core.Models.Packaging; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Parses the xml document contained in a compiled (zip) Umbraco package +/// +public class CompiledPackageXmlParser { - /// - /// Parses the xml document contained in a compiled (zip) Umbraco package - /// - public class CompiledPackageXmlParser + private readonly ConflictingPackageData _conflictingPackageData; + + public CompiledPackageXmlParser(ConflictingPackageData conflictingPackageData) => + _conflictingPackageData = conflictingPackageData; + + public CompiledPackage ToCompiledPackage(XDocument xml) { - private readonly ConflictingPackageData _conflictingPackageData; - - public CompiledPackageXmlParser(ConflictingPackageData conflictingPackageData) => _conflictingPackageData = conflictingPackageData; - - public CompiledPackage ToCompiledPackage(XDocument xml) + if (xml is null) { - if (xml is null) - { - throw new ArgumentNullException(nameof(xml)); - } - - if (xml.Root == null) throw new InvalidOperationException("The xml document is invalid"); - if (xml.Root.Name != "umbPackage") throw new FormatException("The xml document is invalid"); - - var info = xml.Root.Element("info"); - if (info == null) throw new FormatException("The xml document is invalid"); - var package = info.Element("package"); - if (package == null) throw new FormatException("The xml document is invalid"); - - var def = new CompiledPackage - { - // will be null because we don't know where this data is coming from and - // this value is irrelevant during install. - PackageFile = null, - Name = package.Element("name")?.Value ?? string.Empty, - Macros = xml.Root.Element("Macros")?.Elements("macro") ?? Enumerable.Empty(), - MacroPartialViews = xml.Root.Element("MacroPartialViews")?.Elements("View") ?? Enumerable.Empty(), - PartialViews = xml.Root.Element("PartialViews")?.Elements("View") ?? Enumerable.Empty(), - Templates = xml.Root.Element("Templates")?.Elements("Template") ?? Enumerable.Empty(), - Stylesheets = xml.Root.Element("Stylesheets")?.Elements("Stylesheet") ?? Enumerable.Empty(), - Scripts = xml.Root.Element("Scripts")?.Elements("Script") ?? Enumerable.Empty(), - DataTypes = xml.Root.Element("DataTypes")?.Elements("DataType") ?? Enumerable.Empty(), - Languages = xml.Root.Element("Languages")?.Elements("Language") ?? Enumerable.Empty(), - DictionaryItems = xml.Root.Element("DictionaryItems")?.Elements("DictionaryItem") ?? Enumerable.Empty(), - DocumentTypes = xml.Root.Element("DocumentTypes")?.Elements("DocumentType") ?? Enumerable.Empty(), - MediaTypes = xml.Root.Element("MediaTypes")?.Elements("MediaType") ?? Enumerable.Empty(), - Documents = xml.Root.Element("Documents")?.Elements("DocumentSet")?.Select(CompiledPackageContentBase.Create) ?? Enumerable.Empty(), - Media = xml.Root.Element("MediaItems")?.Elements()?.Select(CompiledPackageContentBase.Create) ?? Enumerable.Empty(), - }; - - def.Warnings = GetInstallWarnings(def); - - return def; + throw new ArgumentNullException(nameof(xml)); } - private InstallWarnings GetInstallWarnings(CompiledPackage package) + if (xml.Root == null) { - var installWarnings = new InstallWarnings - { - ConflictingMacros = _conflictingPackageData.FindConflictingMacros(package.Macros), - ConflictingTemplates = _conflictingPackageData.FindConflictingTemplates(package.Templates), - ConflictingStylesheets = _conflictingPackageData.FindConflictingStylesheets(package.Stylesheets) - }; - - return installWarnings; + throw new InvalidOperationException("The xml document is invalid"); } + if (xml.Root.Name != "umbPackage") + { + throw new FormatException("The xml document is invalid"); + } + + XElement? info = xml.Root.Element("info"); + if (info == null) + { + throw new FormatException("The xml document is invalid"); + } + + XElement? package = info.Element("package"); + if (package == null) + { + throw new FormatException("The xml document is invalid"); + } + + var def = new CompiledPackage + { + // will be null because we don't know where this data is coming from and + // this value is irrelevant during install. + PackageFile = null, + Name = package.Element("name")?.Value ?? string.Empty, + Macros = xml.Root.Element("Macros")?.Elements("macro") ?? Enumerable.Empty(), + MacroPartialViews = xml.Root.Element("MacroPartialViews")?.Elements("View") ?? Enumerable.Empty(), + PartialViews = xml.Root.Element("PartialViews")?.Elements("View") ?? Enumerable.Empty(), + Templates = xml.Root.Element("Templates")?.Elements("Template") ?? Enumerable.Empty(), + Stylesheets = xml.Root.Element("Stylesheets")?.Elements("Stylesheet") ?? Enumerable.Empty(), + Scripts = xml.Root.Element("Scripts")?.Elements("Script") ?? Enumerable.Empty(), + DataTypes = xml.Root.Element("DataTypes")?.Elements("DataType") ?? Enumerable.Empty(), + Languages = xml.Root.Element("Languages")?.Elements("Language") ?? Enumerable.Empty(), + DictionaryItems = + xml.Root.Element("DictionaryItems")?.Elements("DictionaryItem") ?? Enumerable.Empty(), + DocumentTypes = xml.Root.Element("DocumentTypes")?.Elements("DocumentType") ?? Enumerable.Empty(), + MediaTypes = xml.Root.Element("MediaTypes")?.Elements("MediaType") ?? Enumerable.Empty(), + Documents = + xml.Root.Element("Documents")?.Elements("DocumentSet")?.Select(CompiledPackageContentBase.Create) ?? + Enumerable.Empty(), + Media = xml.Root.Element("MediaItems")?.Elements()?.Select(CompiledPackageContentBase.Create) ?? + Enumerable.Empty(), + }; + + def.Warnings = GetInstallWarnings(def); + + return def; + } + + private InstallWarnings GetInstallWarnings(CompiledPackage package) + { + var installWarnings = new InstallWarnings + { + ConflictingMacros = _conflictingPackageData.FindConflictingMacros(package.Macros), + ConflictingTemplates = _conflictingPackageData.FindConflictingTemplates(package.Templates), + ConflictingStylesheets = _conflictingPackageData.FindConflictingStylesheets(package.Stylesheets), + }; + + return installWarnings; } } diff --git a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs index 239f1ba66d..d71eada618 100644 --- a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs +++ b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs @@ -1,64 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Xml.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class ConflictingPackageData { - public class ConflictingPackageData + private readonly IFileService _fileService; + private readonly IMacroService _macroService; + + public ConflictingPackageData(IMacroService macroService, IFileService fileService) { - private readonly IMacroService _macroService; - private readonly IFileService _fileService; - - public ConflictingPackageData(IMacroService macroService, IFileService fileService) - { - _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); - _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); - } - - public IEnumerable? FindConflictingStylesheets(IEnumerable? stylesheetNodes) - { - return stylesheetNodes? - .Select(n => - { - var xElement = n.Element("Name") ?? n.Element("name"); - if (xElement == null) - throw new FormatException("Missing \"Name\" element"); - - return _fileService.GetStylesheet(xElement.Value) as IFile; - }) - .Where(v => v != null); - } - - public IEnumerable? FindConflictingTemplates(IEnumerable? templateNodes) - { - return templateNodes? - .Select(n => - { - var xElement = n.Element("Alias") ?? n.Element("alias"); - if (xElement == null) - throw new FormatException("missing a \"Alias\" element"); - - return _fileService.GetTemplate(xElement.Value); - }) - .WhereNotNull(); - } - - public IEnumerable? FindConflictingMacros(IEnumerable? macroNodes) - { - return macroNodes? - .Select(n => - { - var xElement = n.Element("alias") ?? n.Element("Alias"); - if (xElement == null) - throw new FormatException("missing a \"alias\" element in alias element"); - - return _macroService.GetByAlias(xElement.Value); - }) - .Where(v => v != null); - } + _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); + _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); } + + public IEnumerable? FindConflictingStylesheets(IEnumerable? stylesheetNodes) => + stylesheetNodes? + .Select(n => + { + XElement? xElement = n.Element("Name") ?? n.Element("name"); + if (xElement == null) + { + throw new FormatException("Missing \"Name\" element"); + } + + return _fileService.GetStylesheet(xElement.Value) as IFile; + }) + .Where(v => v != null); + + public IEnumerable? FindConflictingTemplates(IEnumerable? templateNodes) => + templateNodes? + .Select(n => + { + XElement? xElement = n.Element("Alias") ?? n.Element("alias"); + if (xElement == null) + { + throw new FormatException("missing a \"Alias\" element"); + } + + return _fileService.GetTemplate(xElement.Value); + }) + .WhereNotNull(); + + public IEnumerable? FindConflictingMacros(IEnumerable? macroNodes) => + macroNodes? + .Select(n => + { + XElement? xElement = n.Element("alias") ?? n.Element("Alias"); + if (xElement == null) + { + throw new FormatException("missing a \"alias\" element in alias element"); + } + + return _macroService.GetByAlias(xElement.Value); + }) + .Where(v => v != null); } diff --git a/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs b/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs index ba99fdd9a7..3c873eb908 100644 --- a/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Manages the storage of created package definitions +/// +public interface ICreatedPackagesRepository : IPackageDefinitionRepository { /// - /// Manages the storage of created package definitions + /// Creates the package file and returns it's physical path /// - public interface ICreatedPackagesRepository : IPackageDefinitionRepository - { - /// - /// Creates the package file and returns it's physical path - /// - /// - string ExportPackage(PackageDefinition definition); - } + /// + string ExportPackage(PackageDefinition definition); } diff --git a/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs b/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs index fe015006a8..b66f4884af 100644 --- a/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs +++ b/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs @@ -1,22 +1,21 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Packaging; -namespace Umbraco.Cms.Core.Packaging +/// +/// Defines methods for persisting package definitions to storage +/// +public interface IPackageDefinitionRepository { - /// - /// Defines methods for persisting package definitions to storage - /// - public interface IPackageDefinitionRepository - { - IEnumerable GetAll(); - PackageDefinition? GetById(int id); - void Delete(int id); + IEnumerable GetAll(); - /// - /// Persists a package definition to storage - /// - /// - /// true if creating/updating the package was successful, otherwise false - /// - bool SavePackage(PackageDefinition definition); - } + PackageDefinition? GetById(int id); + + void Delete(int id); + + /// + /// Persists a package definition to storage + /// + /// + /// true if creating/updating the package was successful, otherwise false + /// + bool SavePackage(PackageDefinition definition); } diff --git a/src/Umbraco.Core/Packaging/IPackageInstallation.cs b/src/Umbraco.Core/Packaging/IPackageInstallation.cs index 9a744a91fa..7fc714bfdb 100644 --- a/src/Umbraco.Core/Packaging/IPackageInstallation.cs +++ b/src/Umbraco.Core/Packaging/IPackageInstallation.cs @@ -1,30 +1,28 @@ -using System.IO; using System.Xml.Linq; using Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Packaging -{ - public interface IPackageInstallation - { - /// - /// Installs a packages data and entities - /// - /// - /// - /// - /// - // TODO: The resulting PackageDefinition is only if we wanted to persist what was saved during package data installation. - // This used to be for the installedPackages.config but we don't have that anymore and don't really want it if we can help it. - // Possibly, we could continue to persist that file so that you could uninstall package data for an installed package in the - // back office (but it won't actually uninstall the package until you do that via nuget). If we want that functionality we'll have - // to restore a bunch of deleted code. - InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition); +namespace Umbraco.Cms.Core.Packaging; - /// - /// Reads the package xml and returns the model - /// - /// - /// - CompiledPackage ReadPackage(XDocument? packageXmlFile); - } +public interface IPackageInstallation +{ + /// + /// Installs a packages data and entities + /// + /// + /// + /// + /// + // TODO: The resulting PackageDefinition is only if we wanted to persist what was saved during package data installation. + // This used to be for the installedPackages.config but we don't have that anymore and don't really want it if we can help it. + // Possibly, we could continue to persist that file so that you could uninstall package data for an installed package in the + // back office (but it won't actually uninstall the package until you do that via nuget). If we want that functionality we'll have + // to restore a bunch of deleted code. + InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition); + + /// + /// Reads the package xml and returns the model + /// + /// + /// + CompiledPackage ReadPackage(XDocument? packageXmlFile); } diff --git a/src/Umbraco.Core/Packaging/InstallationSummary.cs b/src/Umbraco.Core/Packaging/InstallationSummary.cs index d5d7ad343b..d72ede1494 100644 --- a/src/Umbraco.Core/Packaging/InstallationSummary.cs +++ b/src/Umbraco.Core/Packaging/InstallationSummary.cs @@ -1,88 +1,97 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using System.Text; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +[Serializable] +[DataContract(IsReference = true)] +public class InstallationSummary { - [Serializable] - [DataContract(IsReference = true)] - public class InstallationSummary + public InstallationSummary(string packageName) + => PackageName = packageName; + + public string PackageName { get; } + + public InstallWarnings Warnings { get; set; } = new(); + + public IEnumerable DataTypesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable LanguagesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable DictionaryItemsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MacrosInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MacroPartialViewsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable TemplatesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable DocumentTypesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MediaTypesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable StylesheetsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable ScriptsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable PartialViewsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable ContentInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MediaInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable EntityContainersInstalled { get; set; } = Enumerable.Empty(); + + public override string ToString() { - public InstallationSummary(string packageName) - => PackageName = packageName; + var sb = new StringBuilder(); - public string PackageName { get; } - - public InstallWarnings Warnings { get; set; } = new InstallWarnings(); - - public IEnumerable DataTypesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable LanguagesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable DictionaryItemsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MacrosInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MacroPartialViewsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable TemplatesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable DocumentTypesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MediaTypesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable StylesheetsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable ScriptsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable PartialViewsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable ContentInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MediaInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable EntityContainersInstalled { get; set; } = Enumerable.Empty(); - - public override string ToString() + void WriteConflicts(IEnumerable? source, Func selector, string message, bool appendLine = true) { - var sb = new StringBuilder(); - - void WriteConflicts(IEnumerable? source, Func selector, string message, bool appendLine = true) - { - var result = source?.Select(selector).ToList(); - if (result?.Count > 0) - { - sb.Append(message); - sb.Append(string.Join(", ", result)); - - if (appendLine) - { - sb.AppendLine(); - } - } - } - - void WriteCount(string message, IEnumerable source, bool appendLine = true) + var result = source?.Select(selector).ToList(); + if (result?.Count > 0) { sb.Append(message); - sb.Append(source?.Count() ?? 0); + sb.Append(string.Join(", ", result)); if (appendLine) { sb.AppendLine(); } } - - WriteConflicts(Warnings?.ConflictingMacros, x => x?.Alias, "Conflicting macros found, they will be overwritten: "); - WriteConflicts(Warnings?.ConflictingTemplates, x => x.Alias, "Conflicting templates found, they will be overwritten: "); - WriteConflicts(Warnings?.ConflictingStylesheets, x => x?.Alias, "Conflicting stylesheets found, they will be overwritten: "); - WriteCount("Data types installed: ", DataTypesInstalled); - WriteCount("Languages installed: ", LanguagesInstalled); - WriteCount("Dictionary items installed: ", DictionaryItemsInstalled); - WriteCount("Macros installed: ", MacrosInstalled); - WriteCount("Macro partial views installed: ", MacroPartialViewsInstalled); - WriteCount("Templates installed: ", TemplatesInstalled); - WriteCount("Document types installed: ", DocumentTypesInstalled); - WriteCount("Media types installed: ", MediaTypesInstalled); - WriteCount("Stylesheets installed: ", StylesheetsInstalled); - WriteCount("Scripts installed: ", ScriptsInstalled); - WriteCount("Partial views installed: ", PartialViewsInstalled); - WriteCount("Entity containers installed: ", EntityContainersInstalled); - WriteCount("Content items installed: ", ContentInstalled); - WriteCount("Media items installed: ", MediaInstalled, false); - - return sb.ToString(); } + + void WriteCount(string message, IEnumerable source, bool appendLine = true) + { + sb.Append(message); + sb.Append(source?.Count() ?? 0); + + if (appendLine) + { + sb.AppendLine(); + } + } + + WriteConflicts(Warnings?.ConflictingMacros, x => x?.Alias, "Conflicting macros found, they will be overwritten: "); + WriteConflicts(Warnings?.ConflictingTemplates, x => x.Alias, "Conflicting templates found, they will be overwritten: "); + WriteConflicts(Warnings?.ConflictingStylesheets, x => x?.Alias, "Conflicting stylesheets found, they will be overwritten: "); + WriteCount("Data types installed: ", DataTypesInstalled); + WriteCount("Languages installed: ", LanguagesInstalled); + WriteCount("Dictionary items installed: ", DictionaryItemsInstalled); + WriteCount("Macros installed: ", MacrosInstalled); + WriteCount("Macro partial views installed: ", MacroPartialViewsInstalled); + WriteCount("Templates installed: ", TemplatesInstalled); + WriteCount("Document types installed: ", DocumentTypesInstalled); + WriteCount("Media types installed: ", MediaTypesInstalled); + WriteCount("Stylesheets installed: ", StylesheetsInstalled); + WriteCount("Scripts installed: ", ScriptsInstalled); + WriteCount("Partial views installed: ", PartialViewsInstalled); + WriteCount("Entity containers installed: ", EntityContainersInstalled); + WriteCount("Content items installed: ", ContentInstalled); + WriteCount("Media items installed: ", MediaInstalled, false); + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Packaging/InstalledPackage.cs b/src/Umbraco.Core/Packaging/InstalledPackage.cs index ded901512b..8a18cd1da6 100644 --- a/src/Umbraco.Core/Packaging/InstalledPackage.cs +++ b/src/Umbraco.Core/Packaging/InstalledPackage.cs @@ -1,36 +1,35 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +[DataContract(Name = "installedPackage")] +public class InstalledPackage { - [DataContract(Name = "installedPackage")] - public class InstalledPackage - { - [DataMember(Name = "name", IsRequired = true)] - [Required] - public string? PackageName { get; set; } + [DataMember(Name = "name", IsRequired = true)] + [Required] + public string? PackageName { get; set; } - // TODO: Version? Icon? Other metadata? This would need to come from querying the package on Our + // TODO: Version? Icon? Other metadata? This would need to come from querying the package on Our + [DataMember(Name = "packageView")] + public string? PackageView { get; set; } - [DataMember(Name = "packageView")] - public string? PackageView { get; set; } + [DataMember(Name = "version")] + public string? Version { get; set; } - [DataMember(Name = "plans")] - public IEnumerable PackageMigrationPlans { get; set; } = Enumerable.Empty(); + [DataMember(Name = "plans")] + public IEnumerable PackageMigrationPlans { get; set; } = + Enumerable.Empty(); - /// - /// It the package contains any migrations at all - /// - [DataMember(Name = "hasMigrations")] - public bool HasMigrations => PackageMigrationPlans.Any(); - - /// - /// If the package has any pending migrations to run - /// - [DataMember(Name = "hasPendingMigrations")] - public bool HasPendingMigrations => PackageMigrationPlans.Any(x => x.HasPendingMigrations); - } + /// + /// It the package contains any migrations at all + /// + [DataMember(Name = "hasMigrations")] + public bool HasMigrations => PackageMigrationPlans.Any(); + /// + /// If the package has any pending migrations to run + /// + [DataMember(Name = "hasPendingMigrations")] + public bool HasPendingMigrations => PackageMigrationPlans.Any(x => x.HasPendingMigrations); } diff --git a/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs b/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs index 5aaca2e9f2..50cafd1d20 100644 --- a/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs +++ b/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs @@ -1,27 +1,25 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +[DataContract(Name = "installedPackageMigrations")] +public class InstalledPackageMigrationPlans { - [DataContract(Name = "installedPackageMigrations")] - public class InstalledPackageMigrationPlans - { - [DataMember(Name = "hasPendingMigrations")] - public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId; + [DataMember(Name = "hasPendingMigrations")] + public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId; - /// - /// If the package has migrations, this will be it's final migration Id - /// - /// - /// This can be used to determine if the package advertises any migrations - /// - [DataMember(Name = "finalMigrationId")] - public string? FinalMigrationId { get; set; } - - /// - /// If the package has migrations, this will be it's current migration Id - /// - [DataMember(Name = "currentMigrationId")] - public string? CurrentMigrationId { get; set; } - } + /// + /// If the package has migrations, this will be it's final migration Id + /// + /// + /// This can be used to determine if the package advertises any migrations + /// + [DataMember(Name = "finalMigrationId")] + public string? FinalMigrationId { get; set; } + /// + /// If the package has migrations, this will be it's current migration Id + /// + [DataMember(Name = "currentMigrationId")] + public string? CurrentMigrationId { get; set; } } diff --git a/src/Umbraco.Core/Packaging/PackageDefinition.cs b/src/Umbraco.Core/Packaging/PackageDefinition.cs index 66a0a9e102..7b0b5f5df4 100644 --- a/src/Umbraco.Core/Packaging/PackageDefinition.cs +++ b/src/Umbraco.Core/Packaging/PackageDefinition.cs @@ -1,79 +1,74 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -using Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// A created package in the back office. +/// +/// +/// This data structure is persisted to createdPackages.config when creating packages in the back office. +/// +[DataContract(Name = "packageInstance")] +public class PackageDefinition { + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "packageGuid")] + public Guid PackageId { get; set; } + + [DataMember(Name = "name")] + [Required] + public string Name { get; set; } = string.Empty; /// - /// A created package in the back office. + /// The full path to the package's XML file. /// - /// - /// This data structure is persisted to createdPackages.config when creating packages in the back office. - /// - [DataContract(Name = "packageInstance")] - public class PackageDefinition - { - [DataMember(Name = "id")] - public int Id { get; set; } + [ReadOnly(true)] + [DataMember(Name = "packagePath")] + public string PackagePath { get; set; } = string.Empty; - [DataMember(Name = "packageGuid")] - public Guid PackageId { get; set; } + [DataMember(Name = "contentLoadChildNodes")] + public bool ContentLoadChildNodes { get; set; } - [DataMember(Name = "name")] - [Required] - public string Name { get; set; } = string.Empty; + [DataMember(Name = "contentNodeId")] + public string? ContentNodeId { get; set; } - /// - /// The full path to the package's XML file. - /// - [ReadOnly(true)] - [DataMember(Name = "packagePath")] - public string PackagePath { get; set; } = string.Empty; + [DataMember(Name = "macros")] + public IList Macros { get; set; } = new List(); - [DataMember(Name = "contentLoadChildNodes")] - public bool ContentLoadChildNodes { get; set; } + [DataMember(Name = "languages")] + public IList Languages { get; set; } = new List(); - [DataMember(Name = "contentNodeId")] - public string? ContentNodeId { get; set; } + [DataMember(Name = "dictionaryItems")] + public IList DictionaryItems { get; set; } = new List(); - [DataMember(Name = "macros")] - public IList Macros { get; set; } = new List(); + [DataMember(Name = "templates")] + public IList Templates { get; set; } = new List(); - [DataMember(Name = "languages")] - public IList Languages { get; set; } = new List(); + [DataMember(Name = "partialViews")] + public IList PartialViews { get; set; } = new List(); - [DataMember(Name = "dictionaryItems")] - public IList DictionaryItems { get; set; } = new List(); + [DataMember(Name = "documentTypes")] + public IList DocumentTypes { get; set; } = new List(); - [DataMember(Name = "templates")] - public IList Templates { get; set; } = new List(); + [DataMember(Name = "mediaTypes")] + public IList MediaTypes { get; set; } = new List(); - [DataMember(Name = "partialViews")] - public IList PartialViews { get; set; } = new List(); + [DataMember(Name = "stylesheets")] + public IList Stylesheets { get; set; } = new List(); - [DataMember(Name = "documentTypes")] - public IList DocumentTypes { get; set; } = new List(); + [DataMember(Name = "scripts")] + public IList Scripts { get; set; } = new List(); - [DataMember(Name = "mediaTypes")] - public IList MediaTypes { get; set; } = new List(); + [DataMember(Name = "dataTypes")] + public IList DataTypes { get; set; } = new List(); - [DataMember(Name = "stylesheets")] - public IList Stylesheets { get; set; } = new List(); + [DataMember(Name = "mediaUdis")] + public IList MediaUdis { get; set; } = new List(); - [DataMember(Name = "scripts")] - public IList Scripts { get; set; } = new List(); - - [DataMember(Name = "dataTypes")] - public IList DataTypes { get; set; } = new List(); - - [DataMember(Name = "mediaUdis")] - public IList MediaUdis { get; set; } = new List(); - - [DataMember(Name = "mediaLoadChildNodes")] - public bool MediaLoadChildNodes { get; set; } - } + [DataMember(Name = "mediaLoadChildNodes")] + public bool MediaLoadChildNodes { get; set; } } diff --git a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs index df5375ad92..99a18dbcf9 100644 --- a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs +++ b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs @@ -1,84 +1,104 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Xml.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Converts a to and from XML +/// +public class PackageDefinitionXmlParser { - /// - /// Converts a to and from XML - /// - public class PackageDefinitionXmlParser + private static readonly IList EmptyStringList = new List(); + private static readonly IList EmptyGuidUdiList = new List(); + + public PackageDefinition? ToPackageDefinition(XElement xml) { - private static readonly IList s_emptyStringList = new List(); - private static readonly IList s_emptyGuidUdiList = new List(); - - - public PackageDefinition? ToPackageDefinition(XElement xml) + if (xml == null) { - if (xml == null) - { - return null; - } - - var retVal = new PackageDefinition - { - Id = xml.AttributeValue("id"), - Name = xml.AttributeValue("name") ?? string.Empty, - PackagePath = xml.AttributeValue("packagePath") ?? string.Empty, - PackageId = xml.AttributeValue("packageGuid"), - ContentNodeId = xml.Element("content")?.AttributeValue("nodeId") ?? string.Empty, - ContentLoadChildNodes = xml.Element("content")?.AttributeValue("loadChildNodes") ?? false, - MediaUdis = xml.Element("media")?.Elements("nodeUdi").Select(x => (GuidUdi)UdiParser.Parse(x.Value)).ToList() ?? s_emptyGuidUdiList, - MediaLoadChildNodes = xml.Element("media")?.AttributeValue("loadChildNodes") ?? false, - Macros = xml.Element("macros")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Templates = xml.Element("templates")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Stylesheets = xml.Element("stylesheets")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Scripts = xml.Element("scripts")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - PartialViews = xml.Element("partialViews")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - DocumentTypes = xml.Element("documentTypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - MediaTypes = xml.Element("mediaTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Languages = xml.Element("languages")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - DataTypes = xml.Element("datatypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - }; - - return retVal; + return null; } - public XElement ToXml(PackageDefinition def) + var retVal = new PackageDefinition { - var packageXml = new XElement("package", - new XAttribute("id", def.Id), - new XAttribute("name", def.Name ?? string.Empty), - new XAttribute("packagePath", def.PackagePath ?? string.Empty), - new XAttribute("packageGuid", def.PackageId), - new XElement("datatypes", string.Join(",", def.DataTypes ?? Array.Empty())), - - new XElement("content", - new XAttribute("nodeId", def.ContentNodeId ?? string.Empty), - new XAttribute("loadChildNodes", def.ContentLoadChildNodes)), - - new XElement("templates", string.Join(",", def.Templates ?? Array.Empty())), - new XElement("stylesheets", string.Join(",", def.Stylesheets ?? Array.Empty())), - new XElement("scripts", string.Join(",", def.Scripts ?? Array.Empty())), - new XElement("partialViews", string.Join(",", def.PartialViews ?? Array.Empty())), - new XElement("documentTypes", string.Join(",", def.DocumentTypes ?? Array.Empty())), - new XElement("mediaTypes", string.Join(",", def.MediaTypes ?? Array.Empty())), - new XElement("macros", string.Join(",", def.Macros ?? Array.Empty())), - new XElement("languages", string.Join(",", def.Languages ?? Array.Empty())), - new XElement("dictionaryitems", string.Join(",", def.DictionaryItems ?? Array.Empty())), - - new XElement( - "media", - def.MediaUdis.Select(x => (object)new XElement("nodeUdi", x)) - .Union(new[] { new XAttribute("loadChildNodes", def.MediaLoadChildNodes) })) - ); - return packageXml; - } + Id = xml.AttributeValue("id"), + Name = xml.AttributeValue("name") ?? string.Empty, + PackagePath = xml.AttributeValue("packagePath") ?? string.Empty, + PackageId = xml.AttributeValue("packageGuid"), + ContentNodeId = xml.Element("content")?.AttributeValue("nodeId") ?? string.Empty, + ContentLoadChildNodes = xml.Element("content")?.AttributeValue("loadChildNodes") ?? false, + MediaUdis = + xml.Element("media")?.Elements("nodeUdi").Select(x => (GuidUdi)UdiParser.Parse(x.Value)).ToList() ?? + EmptyGuidUdiList, + MediaLoadChildNodes = xml.Element("media")?.AttributeValue("loadChildNodes") ?? false, + Macros = + xml.Element("macros")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + Templates = + xml.Element("templates")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + Stylesheets = + xml.Element("stylesheets")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + Scripts = + xml.Element("scripts")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + PartialViews = + xml.Element("partialViews")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + DocumentTypes = + xml.Element("documentTypes")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + MediaTypes = + xml.Element("mediaTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .ToList() ?? EmptyStringList, + Languages = + xml.Element("languages")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + DictionaryItems = + xml.Element("dictionaryitems")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + DataTypes = xml.Element("datatypes")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + }; + return retVal; + } + public XElement ToXml(PackageDefinition def) + { + var packageXml = new XElement( + "package", + new XAttribute("id", def.Id), + new XAttribute("name", def.Name ?? string.Empty), + new XAttribute("packagePath", def.PackagePath ?? string.Empty), + new XAttribute("packageGuid", def.PackageId), + new XElement("datatypes", string.Join(",", def.DataTypes ?? Array.Empty())), + new XElement( + "content", + new XAttribute("nodeId", def.ContentNodeId ?? string.Empty), + new XAttribute("loadChildNodes", def.ContentLoadChildNodes)), + new XElement("templates", string.Join(",", def.Templates ?? Array.Empty())), + new XElement("stylesheets", string.Join(",", def.Stylesheets ?? Array.Empty())), + new XElement("scripts", string.Join(",", def.Scripts ?? Array.Empty())), + new XElement("partialViews", string.Join(",", def.PartialViews ?? Array.Empty())), + new XElement("documentTypes", string.Join(",", def.DocumentTypes ?? Array.Empty())), + new XElement("mediaTypes", string.Join(",", def.MediaTypes ?? Array.Empty())), + new XElement("macros", string.Join(",", def.Macros ?? Array.Empty())), + new XElement("languages", string.Join(",", def.Languages ?? Array.Empty())), + new XElement("dictionaryitems", string.Join(",", def.DictionaryItems ?? Array.Empty())), + new XElement( + "media", + def.MediaUdis.Select(x => (object)new XElement("nodeUdi", x)) + .Union(new[] { new XAttribute("loadChildNodes", def.MediaLoadChildNodes) }))); + return packageXml; } } diff --git a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs index b972a2cf08..0d72cad38a 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs @@ -1,127 +1,118 @@ -using System; -using System.IO; using System.IO.Compression; using System.Reflection; -using System.Security.Cryptography; -using System.Text; using System.Xml; using System.Xml.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public static class PackageMigrationResource { - public static class PackageMigrationResource + public static XDocument? GetEmbeddedPackageDataManifest(Type planType, out ZipArchive? zipArchive) { - private static Stream? GetEmbeddedPackageZipStream(Type planType) + XDocument? packageXml; + Stream? zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) { - // lookup the embedded resource by convention - Assembly currentAssembly = planType.Assembly; - var fileName = $"{planType.Namespace}.package.zip"; - Stream? stream = currentAssembly.GetManifestResourceStream(fileName); - - return stream; - } - - public static XDocument? GetEmbeddedPackageDataManifest(Type planType, out ZipArchive? zipArchive) - { - XDocument? packageXml; - var zipStream = GetEmbeddedPackageZipStream(planType); - if (zipStream is not null) - { - zipArchive = GetPackageDataManifest(zipStream, out packageXml); - return packageXml; - } - - zipArchive = null; - packageXml = GetEmbeddedPackageXmlDoc(planType); + zipArchive = GetPackageDataManifest(zipStream, out packageXml); return packageXml; } - public static XDocument? GetEmbeddedPackageDataManifest(Type planType) + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml; + } + + private static Stream? GetEmbeddedPackageZipStream(Type planType) + { + // lookup the embedded resource by convention + Assembly currentAssembly = planType.Assembly; + var fileName = $"{planType.Namespace}.package.zip"; + Stream? stream = currentAssembly.GetManifestResourceStream(fileName); + + return stream; + } + + public static XDocument? GetEmbeddedPackageDataManifest(Type planType) => + GetEmbeddedPackageDataManifest(planType, out _); + + public static string GetEmbeddedPackageDataManifestHash(Type planType) + { + // SEE: HashFromStreams in the benchmarks project for how fast this is. It will run + // on every startup for every embedded package.zip. The bigger the zip, the more time it takes. + // But it is still very fast ~303ms for a 100MB file. This will only be an issue if there are + // several very large package.zips. + using Stream? stream = GetEmbeddedPackageZipStream(planType); + + if (stream is not null) { - return GetEmbeddedPackageDataManifest(planType, out _); + return stream.GetStreamHash(); } - private static XDocument? GetEmbeddedPackageXmlDoc(Type planType) + XDocument? xml = GetEmbeddedPackageXmlDoc(planType); + + if (xml is not null) { - // lookup the embedded resource by convention - Assembly currentAssembly = planType.Assembly; - var fileName = $"{planType.Namespace}.package.xml"; - Stream? stream = currentAssembly.GetManifestResourceStream(fileName); - if (stream == null) - { - return null; - } - XDocument xml; - using (stream) - { - xml = XDocument.Load(stream); - } - return xml; + return xml.ToString(); } - public static string GetEmbeddedPackageDataManifestHash(Type planType) + throw new IOException("Missing embedded files for planType: " + planType); + } + + private static XDocument? GetEmbeddedPackageXmlDoc(Type planType) + { + // lookup the embedded resource by convention + Assembly currentAssembly = planType.Assembly; + var fileName = $"{planType.Namespace}.package.xml"; + Stream? stream = currentAssembly.GetManifestResourceStream(fileName); + if (stream == null) { - // SEE: HashFromStreams in the benchmarks project for how fast this is. It will run - // on every startup for every embedded package.zip. The bigger the zip, the more time it takes. - // But it is still very fast ~303ms for a 100MB file. This will only be an issue if there are - // several very large package.zips. - - using Stream? stream = GetEmbeddedPackageZipStream(planType); - - if (stream is not null) - { - return stream.GetStreamHash(); - } - - var xml = GetEmbeddedPackageXmlDoc(planType); - - if (xml is not null) - { - return xml.ToString(); - } - - throw new IOException("Missing embedded files for planType: " + planType); + return null; } - public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument? packageXml, out ZipArchive? zipArchive) + XDocument xml; + using (stream) { - var zipStream = GetEmbeddedPackageZipStream(planType); - if (zipStream is not null) - { - zipArchive = GetPackageDataManifest(zipStream, out packageXml); - return true; - } - - zipArchive = null; - packageXml = GetEmbeddedPackageXmlDoc(planType); - return packageXml is not null; + xml = XDocument.Load(stream); } - public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) + return xml; + } + + public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument? packageXml, out ZipArchive? zipArchive) + { + Stream? zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) { - if (packageZipStream == null) - { - throw new ArgumentNullException(nameof(packageZipStream)); - } - - var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); - ZipArchiveEntry? packageXmlEntry = zip.GetEntry("package.xml"); - if (packageXmlEntry == null) - { - throw new InvalidOperationException("Zip package does not contain the required package.xml file"); - } - - using (Stream packageXmlStream = packageXmlEntry.Open()) - using (var xmlReader = XmlReader.Create(packageXmlStream, new XmlReaderSettings - { - IgnoreWhitespace = true - })) - { - packageXml = XDocument.Load(xmlReader); - } - - return zip; + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return true; } + + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml is not null; + } + + public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) + { + if (packageZipStream == null) + { + throw new ArgumentNullException(nameof(packageZipStream)); + } + + var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); + ZipArchiveEntry? packageXmlEntry = zip.GetEntry("package.xml"); + if (packageXmlEntry == null) + { + throw new InvalidOperationException("Zip package does not contain the required package.xml file"); + } + + using (Stream packageXmlStream = packageXmlEntry.Open()) + using (var xmlReader = XmlReader.Create(packageXmlStream, new XmlReaderSettings { IgnoreWhitespace = true })) + { + packageXml = XDocument.Load(xmlReader); + } + + return zip; } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 174faa37fd..a5982aef7e 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -15,749 +11,848 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; using File = System.IO.File; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Manages the storage of installed/created package definitions +/// +[Obsolete( + "Packages have now been moved to the database instead of local files, please use CreatedPackageSchemaRepository instead")] +public class PackagesRepository : ICreatedPackagesRepository { + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + private readonly string _createdPackagesFolderPath; + private readonly IDataTypeService _dataTypeService; + private readonly IFileService _fileService; + private readonly FileSystems _fileSystems; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizationService _languageService; + private readonly IMacroService _macroService; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly string _packageRepositoryFileName; + private readonly string _packagesFolderPath; + private readonly PackageDefinitionXmlParser _parser; + private readonly IEntityXmlSerializer _serializer; + private readonly string _tempFolderPath; + /// - /// Manages the storage of installed/created package definitions + /// Constructor /// - [Obsolete("Packages have now been moved to the database instead of local files, please use CreatedPackageSchemaRepository instead")] - public class PackagesRepository : ICreatedPackagesRepository + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The file name for storing the package definitions (i.e. "createdPackages.config") + /// + /// + /// + /// + /// + /// + /// + /// + public PackagesRepository( + IContentService contentService, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IFileService fileService, + IMacroService macroService, + ILocalizationService languageService, + IHostingEnvironment hostingEnvironment, + IEntityXmlSerializer serializer, + IOptions globalSettings, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + MediaFileManager mediaFileManager, + FileSystems fileSystems, + string packageRepositoryFileName, + string? tempFolderPath = null, + string? packagesFolderPath = null, + string? mediaFolderPath = null) { - private readonly IContentService _contentService; - private readonly IContentTypeService _contentTypeService; - private readonly IDataTypeService _dataTypeService; - private readonly IFileService _fileService; - private readonly IMacroService _macroService; - private readonly ILocalizationService _languageService; - private readonly IEntityXmlSerializer _serializer; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly string _packageRepositoryFileName; - private readonly string _createdPackagesFolderPath; - private readonly string _packagesFolderPath; - private readonly string _tempFolderPath; - private readonly PackageDefinitionXmlParser _parser; - private readonly IMediaService _mediaService; - private readonly IMediaTypeService _mediaTypeService; - private readonly MediaFileManager _mediaFileManager; - private readonly FileSystems _fileSystems; - - /// - /// Constructor - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// The file name for storing the package definitions (i.e. "createdPackages.config") - /// - /// - /// - /// - public PackagesRepository( - IContentService contentService, - IContentTypeService contentTypeService, - IDataTypeService dataTypeService, - IFileService fileService, - IMacroService macroService, - ILocalizationService languageService, - IHostingEnvironment hostingEnvironment, - IEntityXmlSerializer serializer, - IOptions globalSettings, - IMediaService mediaService, - IMediaTypeService mediaTypeService, - MediaFileManager mediaFileManager, - FileSystems fileSystems, - string packageRepositoryFileName, - string? tempFolderPath = null, - string? packagesFolderPath = null, - string? mediaFolderPath = null) + if (string.IsNullOrWhiteSpace(packageRepositoryFileName)) { - if (string.IsNullOrWhiteSpace(packageRepositoryFileName)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(packageRepositoryFileName)); - _contentService = contentService; - _contentTypeService = contentTypeService; - _dataTypeService = dataTypeService; - _fileService = fileService; - _macroService = macroService; - _languageService = languageService; - _serializer = serializer; - _hostingEnvironment = hostingEnvironment; - _packageRepositoryFileName = packageRepositoryFileName; - - _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; - _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; - _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; - - _parser = new PackageDefinitionXmlParser(); - _mediaService = mediaService; - _mediaTypeService = mediaTypeService; - _mediaFileManager = mediaFileManager; - _fileSystems = fileSystems; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(packageRepositoryFileName)); } - private string CreatedPackagesFile => _packagesFolderPath.EnsureEndsWith('/') + _packageRepositoryFileName; + _contentService = contentService; + _contentTypeService = contentTypeService; + _dataTypeService = dataTypeService; + _fileService = fileService; + _macroService = macroService; + _languageService = languageService; + _serializer = serializer; + _hostingEnvironment = hostingEnvironment; + _packageRepositoryFileName = packageRepositoryFileName; - public IEnumerable GetAll() + _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; + _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; + _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; + + _parser = new PackageDefinitionXmlParser(); + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + _mediaFileManager = mediaFileManager; + _fileSystems = fileSystems; + } + + private string CreatedPackagesFile => _packagesFolderPath.EnsureEndsWith('/') + _packageRepositoryFileName; + + public IEnumerable GetAll() + { + XDocument packagesXml = EnsureStorage(out _); + if (packagesXml?.Root == null) { - var packagesXml = EnsureStorage(out _); - if (packagesXml?.Root == null) - yield break; - - foreach (var packageXml in packagesXml.Root.Elements("package")) - yield return _parser.ToPackageDefinition(packageXml); + yield break; } - public PackageDefinition? GetById(int id) + foreach (XElement packageXml in packagesXml.Root.Elements("package")) { - var packagesXml = EnsureStorage(out var packageFile); - var packageXml = packagesXml?.Root?.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == id); - return packageXml == null ? null : _parser.ToPackageDefinition(packageXml); + yield return _parser.ToPackageDefinition(packageXml); + } + } + + public PackageDefinition? GetById(int id) + { + XDocument packagesXml = EnsureStorage(out var packageFile); + XElement? packageXml = packagesXml?.Root?.Elements("package") + .FirstOrDefault(x => x.AttributeValue("id") == id); + return packageXml == null ? null : _parser.ToPackageDefinition(packageXml); + } + + public void Delete(int id) + { + XDocument packagesXml = EnsureStorage(out var packagesFile); + XElement? packageXml = packagesXml?.Root?.Elements("package") + .FirstOrDefault(x => x.AttributeValue("id") == id); + if (packageXml == null) + { + return; } - public void Delete(int id) + packageXml.Remove(); + + packagesXml?.Save(packagesFile); + } + + public bool SavePackage(PackageDefinition definition) + { + if (definition == null) { - var packagesXml = EnsureStorage(out var packagesFile); - var packageXml = packagesXml?.Root?.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == id); + throw new ArgumentNullException(nameof(definition)); + } + + XDocument packagesXml = EnsureStorage(out var packagesFile); + + if (packagesXml?.Root == null) + { + return false; + } + + // ensure it's valid + ValidatePackage(definition); + + if (definition.Id == default) + { + // need to gen an id and persist + // Find max id + var maxId = packagesXml.Root.Elements("package").Max(x => x.AttributeValue("id")) ?? 0; + var newId = maxId + 1; + definition.Id = newId; + definition.PackageId = definition.PackageId == default ? Guid.NewGuid() : definition.PackageId; + XElement packageXml = _parser.ToXml(definition); + packagesXml.Root.Add(packageXml); + } + else + { + // existing + XElement? packageXml = packagesXml.Root.Elements("package") + .FirstOrDefault(x => x.AttributeValue("id") == definition.Id); if (packageXml == null) - return; + { + return false; + } - packageXml.Remove(); - - packagesXml?.Save(packagesFile); + XElement updatedXml = _parser.ToXml(definition); + packageXml.ReplaceWith(updatedXml); } - public bool SavePackage(PackageDefinition definition) + packagesXml.Save(packagesFile); + + return true; + } + + public string ExportPackage(PackageDefinition definition) + { + if (definition.Id == default) { - if (definition == null) - throw new ArgumentNullException(nameof(definition)); + throw new ArgumentException( + "The package definition does not have an ID, it must be saved before being exported"); + } - var packagesXml = EnsureStorage(out var packagesFile); + if (definition.PackageId == default) + { + throw new ArgumentException( + "the package definition does not have a GUID, it must be saved before being exported"); + } - if (packagesXml?.Root == null) - return false; + // ensure it's valid + ValidatePackage(definition); - //ensure it's valid - ValidatePackage(definition); + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); + if (Directory.Exists(temporaryPath) == false) + { + Directory.CreateDirectory(temporaryPath); + } - if (definition.Id == default) + try + { + // Init package file + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + + // Info section + root.Add(GetPackageInfoXml(definition)); + + PackageDocumentsAndTags(definition, root); + PackageDocumentTypes(definition, root); + PackageMediaTypes(definition, root); + PackageTemplates(definition, root); + PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem); + PackageMacros(definition, root); + PackageDictionaryItems(definition, root); + PackageLanguages(definition, root); + PackageDataTypes(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); + + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) { - //need to gen an id and persist - // Find max id - var maxId = packagesXml.Root.Elements("package").Max(x => x.AttributeValue("id")) ?? 0; - var newId = maxId + 1; - definition.Id = newId; - definition.PackageId = definition.PackageId == default ? Guid.NewGuid() : definition.PackageId; - var packageXml = _parser.ToXml(definition); - packagesXml.Root.Add(packageXml); + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + { + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) + { + compiledPackageXml.Save(entryStream); + } + + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) + { + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); + } + } + } } else { - //existing - var packageXml = packagesXml.Root.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == definition.Id); - if (packageXml == null) - return false; + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); - var updatedXml = _parser.ToXml(definition); - packageXml.ReplaceWith(updatedXml); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } } - packagesXml.Save(packagesFile); + var directoryName = + _hostingEnvironment.MapPathContentRoot(Path.Combine( + _createdPackagesFolderPath, + definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); - return true; + var finalPackagePath = Path.Combine(directoryName, fileName); + + if (File.Exists(finalPackagePath)) + { + File.Delete(finalPackagePath); + } + + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; + SavePackage(definition); + + return finalPackagePath; + } + finally + { + // Clean up + Directory.Delete(temporaryPath, true); + } + } + + public void DeleteLocalRepositoryFiles() + { + var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); + if (File.Exists(packagesFile)) + { + File.Delete(packagesFile); } - public string ExportPackage(PackageDefinition definition) + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); + if (Directory.Exists(packagesFolder)) { - if (definition.Id == default) - throw new ArgumentException("The package definition does not have an ID, it must be saved before being exported"); - if (definition.PackageId == default) - throw new ArgumentException("the package definition does not have a GUID, it must be saved before being exported"); + Directory.Delete(packagesFolder); + } + } - //ensure it's valid - ValidatePackage(definition); + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); - //Create a folder for building this package - var temporaryPath = _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); - if (Directory.Exists(temporaryPath) == false) + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } + + private void ValidatePackage(PackageDefinition definition) + { + // ensure it's valid + var context = new ValidationContext(definition, null, null); + var results = new List(); + var isValid = Validator.TryValidateObject(definition, context, results); + if (!isValid) + { + throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + + string.Join(", ", results.Select(x => x.ErrorMessage))); + } + } + + private void PackageDataTypes(PackageDefinition definition, XContainer root) + { + var dataTypes = new XElement("DataTypes"); + foreach (var dtId in definition.DataTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - Directory.CreateDirectory(temporaryPath); + continue; } - try + IDataType? dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) { - //Init package file - XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + continue; + } - //Info section - root.Add(GetPackageInfoXml(definition)); + dataTypes.Add(_serializer.Serialize(dataType)); + } - PackageDocumentsAndTags(definition, root); - PackageDocumentTypes(definition, root); - PackageMediaTypes(definition, root); - PackageTemplates(definition, root); - PackageStylesheets(definition, root); - PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem); - PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem); - PackageMacros(definition, root); - PackageDictionaryItems(definition, root); - PackageLanguages(definition, root); - PackageDataTypes(definition, root); - Dictionary mediaFiles = PackageMedia(definition, root); + root.Add(dataTypes); + } - string fileName; - string tempPackagePath; - if (mediaFiles.Count > 0) + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) + { + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ILanguage? lang = _languageService.GetLanguageById(outInt); + if (lang == null) + { + continue; + } + + languages.Add(_serializer.Serialize(lang)); + } + + root.Add(languages); + } + + private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + { + var rootDictionaryItems = new XElement("DictionaryItems"); + var items = new Dictionary(); + + foreach (var dictionaryId in definition.DictionaryItems) + { + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDictionaryItem? di = _languageService.GetDictionaryItemById(outInt); + + if (di == null) + { + continue; + } + + items[di.Key] = (di, _serializer.Serialize(di, false)); + } + + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) + { + foreach (Guid key in items.Keys.ToList()) + { + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; + + if (!dictionaryItem.ParentId.HasValue) { - fileName = "package.zip"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) - { - ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); - using (Stream entryStream = packageXmlEntry.Open()) - { - compiledPackageXml.Save(entryStream); - } - - foreach (KeyValuePair mediaFile in mediaFiles) - { - var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); - using (Stream entryStream = mediaEntry.Open()) - using (mediaFile.Value) - { - mediaFile.Value.Seek(0, SeekOrigin.Begin); - mediaFile.Value.CopyTo(entryStream); - } - } - } + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } else { - fileName = "package.xml"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) { - compiledPackageXml.Save(fileStream); + // we've processed this parent element already so we can just append this xml child to it + AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, serializedDictionaryValue); } - } - - var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); - Directory.CreateDirectory(directoryName); - - var finalPackagePath = Path.Combine(directoryName, fileName); - - if (File.Exists(finalPackagePath)) - { - File.Delete(finalPackagePath); - } - - File.Move(tempPackagePath, finalPackagePath); - - definition.PackagePath = finalPackagePath; - SavePackage(definition); - - return finalPackagePath; - } - finally - { - // Clean up - Directory.Delete(temporaryPath, true); - } - } - - private void ValidatePackage(PackageDefinition definition) - { - // ensure it's valid - var context = new ValidationContext(definition, serviceProvider: null, items: null); - var results = new List(); - var isValid = Validator.TryValidateObject(definition, context, results); - if (!isValid) - throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + string.Join(", ", results.Select(x => x.ErrorMessage))); - } - - private void PackageDataTypes(PackageDefinition definition, XContainer root) - { - var dataTypes = new XElement("DataTypes"); - foreach (var dtId in definition.DataTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var dataType = _dataTypeService.GetDataType(outInt); - if (dataType == null) - continue; - dataTypes.Add(_serializer.Serialize(dataType)); - } - root.Add(dataTypes); - } - - private void PackageLanguages(PackageDefinition definition, XContainer root) - { - var languages = new XElement("Languages"); - foreach (var langId in definition.Languages) - { - if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var lang = _languageService.GetLanguageById(outInt); - if (lang == null) - continue; - languages.Add(_serializer.Serialize(lang)); - } - root.Add(languages); - } - - private void PackageDictionaryItems(PackageDefinition definition, XContainer root) - { - var rootDictionaryItems = new XElement("DictionaryItems"); - var items = new Dictionary(); - - foreach (var dictionaryId in definition.DictionaryItems) - { - if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IDictionaryItem? di = _languageService.GetDictionaryItemById(outInt); - - if (di == null) - { - continue; - } - - items[di.Key] = (di, _serializer.Serialize(di, false)); - } - - // organize them in hierarchy ... - var itemCount = items.Count; - var processed = new Dictionary(); - while (processed.Count < itemCount) - { - foreach (Guid key in items.Keys.ToList()) - { - (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - - if (!dictionaryItem.ParentId.HasValue) + else if (items.ContainsKey(dictionaryItem.ParentId.Value)) { - // if it has no parent, its definitely just at the root - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + // we know the parent exists in the dictionary but + // we haven't processed it yet so we'll leave it for the next loop } else { - if (processed.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we've processed this parent element already so we can just append this xml child to it - AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, serializedDictionaryValue); - } - else if (items.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we know the parent exists in the dictionary but - // we haven't processed it yet so we'll leave it for the next loop - continue; - } - else - { - // in this case, the parent of this item doesn't exist in our collection, we have no - // choice but to add it to the root. - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); - } + // in this case, the parent of this item doesn't exist in our collection, we have no + // choice but to add it to the root. + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } } } + } - root.Add(rootDictionaryItems); + root.Add(rootDictionaryItems); - static void AppendDictionaryElement(XElement rootDictionaryItems, Dictionary items, Dictionary processed, Guid key, XElement serializedDictionaryValue) + static void AppendDictionaryElement( + XElement rootDictionaryItems, + Dictionary items, + Dictionary processed, + Guid key, + XElement serializedDictionaryValue) + { + // track it + processed.Add(key, serializedDictionaryValue); + + // append it + rootDictionaryItems.Add(serializedDictionaryValue); + + // remove it so its not re-processed + items.Remove(key); + } + } + + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) + { + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - // track it - processed.Add(key, serializedDictionaryValue); - // append it - rootDictionaryItems.Add(serializedDictionaryValue); - // remove it so its not re-processed - items.Remove(key); + continue; + } + + XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); + if (macroXml == null) + { + continue; + } + + macros.Add(macroXml); + if (macro is not null) + { + packagedMacros.Add(macro); } } - private void PackageMacros(PackageDefinition definition, XContainer root) + root.Add(macros); + + // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) + IEnumerable views = packagedMacros.Where(x => x.MacroSource is not null) + .Where(x => x.MacroSource!.StartsWith(Constants.SystemDirectories.MacroPartials)) + .Select(x => x.MacroSource![Constants.SystemDirectories.MacroPartials.Length..].Replace('/', '\\')); + PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem); + } + + private void PackageStylesheets(PackageDefinition definition, XContainer root) + { + var stylesheetsXml = new XElement("Stylesheets"); + foreach (var stylesheet in definition.Stylesheets) { - var packagedMacros = new List(); - var macros = new XElement("Macros"); - foreach (var macroId in definition.Macros) + if (stylesheet.IsNullOrWhiteSpace()) { - if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt)) - { - continue; - } - - XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); - if (macroXml == null) - { - continue; - } - - macros.Add(macroXml); - if (macro is not null) - { - packagedMacros.Add(macro); - } + continue; } - root.Add(macros); - - // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) - IEnumerable views = packagedMacros.Where(x => x.MacroSource is not null).Where(x => x.MacroSource!.StartsWith(Constants.SystemDirectories.MacroPartials)) - .Select(x => x.MacroSource!.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); - PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem); + XElement? xml = GetStylesheetXml(stylesheet, true); + if (xml is not null) + { + stylesheetsXml.Add(xml); + } } - private void PackageStylesheets(PackageDefinition definition, XContainer root) - { - var stylesheetsXml = new XElement("Stylesheets"); - foreach (var stylesheet in definition.Stylesheets) - { - if (stylesheet.IsNullOrWhiteSpace()) - { - continue; - } + root.Add(stylesheetsXml); + } - XElement? xml = GetStylesheetXml(stylesheet, true); - if (xml is not null) + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem? fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) + { + if (file.IsNullOrWhiteSpace()) + { + continue; + } + + if (!fileSystem?.FileExists(file) ?? false) + { + throw new InvalidOperationException("No file found with path " + file); + } + + using (Stream stream = fileSystem!.OpenFile(file)) + { + using (var reader = new StreamReader(stream)) { - stylesheetsXml.Add(xml); + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); } } - root.Add(stylesheetsXml); } - private void PackageStaticFiles( - IEnumerable filePaths, - XContainer root, - string containerName, - string elementName, - IFileSystem? fileSystem) + root.Add(scriptsXml); + } + + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) { - var scriptsXml = new XElement(containerName); - foreach (var file in filePaths) + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (file.IsNullOrWhiteSpace()) - { - continue; - } + continue; + } - if (!fileSystem?.FileExists(file) ?? false) - { - throw new InvalidOperationException("No file found with path " + file); - } + ITemplate? template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; + } - using (Stream stream = fileSystem!.OpenFile(file)) + templatesXml.Add(_serializer.Serialize(template)); + } + + root.Add(templatesXml); + } + + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + { + var contentTypes = new HashSet(); + var docTypesXml = new XElement("DocumentTypes"); + foreach (var dtId in definition.DocumentTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IContentType? contentType = _contentTypeService.Get(outInt); + if (contentType == null) + { + continue; + } + + AddDocumentType(contentType, contentTypes); + } + + foreach (IContentType contentType in contentTypes) + { + docTypesXml.Add(_serializer.Serialize(contentType)); + } + + root.Add(docTypesXml); + } + + private void PackageMediaTypes(PackageDefinition definition, XContainer root) + { + var mediaTypes = new HashSet(); + var mediaTypesXml = new XElement("MediaTypes"); + foreach (var mediaTypeId in definition.MediaTypes) + { + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IMediaType? mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) + { + continue; + } + + AddMediaType(mediaType, mediaTypes); + } + + foreach (IMediaType mediaType in mediaTypes) + { + mediaTypesXml.Add(_serializer.Serialize(mediaType)); + } + + root.Add(mediaTypesXml); + } + + private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + { + // Documents and tags + if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) + { + if (contentNodeId > 0) + { + // load content from umbraco. + IContent? content = _contentService.GetById(contentNodeId); + if (content != null) { - using (var reader = new StreamReader(stream)) - { - var fileContents = reader.ReadToEnd(); - scriptsXml.Add( + XElement contentXml = definition.ContentLoadChildNodes + ? content.ToDeepXml(_serializer) + : content.ToXml(_serializer); + + // Create the Documents/DocumentSet node + root.Add( + new XElement( + "Documents", new XElement( - elementName, - new XAttribute("path", file), - new XCData(fileContents))); - } + "DocumentSet", + new XAttribute("importMode", "root"), + contentXml))); + + // TODO: I guess tags has been broken for a very long time for packaging, we should get this working again sometime + ////Create the TagProperties node - this is used to store a definition for all + //// document properties that are tags, this ensures that we can re-import tags properly + // XmlNode tagProps = new XElement("TagProperties"); + + ////before we try to populate this, we'll do a quick lookup to see if any of the documents + //// being exported contain published tags. + // var allExportedIds = documents.SelectNodes("//@id").Cast() + // .Select(x => x.Value.TryConvertTo()) + // .Where(x => x.Success) + // .Select(x => x.Result) + // .ToArray(); + // var allContentTags = new List(); + // foreach (var exportedId in allExportedIds) + // { + // allContentTags.AddRange( + // Current.Services.TagService.GetTagsForEntity(exportedId)); + // } + + ////This is pretty round-about but it works. Essentially we need to get the properties that are tagged + //// but to do that we need to lookup by a tag (string) + // var allTaggedEntities = new List(); + // foreach (var group in allContentTags.Select(x => x.Group).Distinct()) + // { + // allTaggedEntities.AddRange( + // Current.Services.TagService.GetTaggedContentByTagGroup(group)); + // } + + ////Now, we have all property Ids/Aliases and their referenced document Ids and tags + // var allExportedTaggedEntities = allTaggedEntities.Where(x => allExportedIds.Contains(x.EntityId)) + // .DistinctBy(x => x.EntityId) + // .OrderBy(x => x.EntityId); + + // foreach (var taggedEntity in allExportedTaggedEntities) + // { + // foreach (var taggedProperty in taggedEntity.TaggedProperties.Where(x => x.Tags.Any())) + // { + // XmlNode tagProp = new XElement("TagProperty"); + // var docId = packageManifest.CreateAttribute("docId", ""); + // docId.Value = taggedEntity.EntityId.ToString(CultureInfo.InvariantCulture); + // tagProp.Attributes.Append(docId); + + // var propertyAlias = packageManifest.CreateAttribute("propertyAlias", ""); + // propertyAlias.Value = taggedProperty.PropertyTypeAlias; + // tagProp.Attributes.Append(propertyAlias); + + // var group = packageManifest.CreateAttribute("group", ""); + // group.Value = taggedProperty.Tags.First().Group; + // tagProp.Attributes.Append(group); + + // tagProp.AppendChild(packageManifest.CreateCDataSection( + // JsonConvert.SerializeObject(taggedProperty.Tags.Select(x => x.Text).ToArray()))); + + // tagProps.AppendChild(tagProp); + // } + // } + + // manifestRoot.Add(tagProps); } } - - root.Add(scriptsXml); - } - - private void PackageTemplates(PackageDefinition definition, XContainer root) - { - var templatesXml = new XElement("Templates"); - foreach (var templateId in definition.Templates) - { - if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var template = _fileService.GetTemplate(outInt); - if (template == null) - continue; - templatesXml.Add(_serializer.Serialize(template)); - } - root.Add(templatesXml); - } - - private void PackageDocumentTypes(PackageDefinition definition, XContainer root) - { - var contentTypes = new HashSet(); - var docTypesXml = new XElement("DocumentTypes"); - foreach (var dtId in definition.DocumentTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var contentType = _contentTypeService.Get(outInt); - if (contentType == null) - continue; - AddDocumentType(contentType, contentTypes); - } - foreach (var contentType in contentTypes) - docTypesXml.Add(_serializer.Serialize(contentType)); - - root.Add(docTypesXml); - } - - private void PackageMediaTypes(PackageDefinition definition, XContainer root) - { - var mediaTypes = new HashSet(); - var mediaTypesXml = new XElement("MediaTypes"); - foreach (var mediaTypeId in definition.MediaTypes) - { - if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var mediaType = _mediaTypeService.Get(outInt); - if (mediaType == null) - continue; - AddMediaType(mediaType, mediaTypes); - } - foreach (var mediaType in mediaTypes) - mediaTypesXml.Add(_serializer.Serialize(mediaType)); - - root.Add(mediaTypesXml); - } - - private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) - { - //Documents and tags - if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) - { - if (contentNodeId > 0) - { - //load content from umbraco. - var content = _contentService.GetById(contentNodeId); - if (content != null) - { - var contentXml = definition.ContentLoadChildNodes ? content.ToDeepXml(_serializer) : content.ToXml(_serializer); - - //Create the Documents/DocumentSet node - - root.Add( - new XElement("Documents", - new XElement("DocumentSet", - new XAttribute("importMode", "root"), - contentXml))); - - // TODO: I guess tags has been broken for a very long time for packaging, we should get this working again sometime - ////Create the TagProperties node - this is used to store a definition for all - //// document properties that are tags, this ensures that we can re-import tags properly - //XmlNode tagProps = new XElement("TagProperties"); - - ////before we try to populate this, we'll do a quick lookup to see if any of the documents - //// being exported contain published tags. - //var allExportedIds = documents.SelectNodes("//@id").Cast() - // .Select(x => x.Value.TryConvertTo()) - // .Where(x => x.Success) - // .Select(x => x.Result) - // .ToArray(); - //var allContentTags = new List(); - //foreach (var exportedId in allExportedIds) - //{ - // allContentTags.AddRange( - // Current.Services.TagService.GetTagsForEntity(exportedId)); - //} - - ////This is pretty round-about but it works. Essentially we need to get the properties that are tagged - //// but to do that we need to lookup by a tag (string) - //var allTaggedEntities = new List(); - //foreach (var group in allContentTags.Select(x => x.Group).Distinct()) - //{ - // allTaggedEntities.AddRange( - // Current.Services.TagService.GetTaggedContentByTagGroup(group)); - //} - - ////Now, we have all property Ids/Aliases and their referenced document Ids and tags - //var allExportedTaggedEntities = allTaggedEntities.Where(x => allExportedIds.Contains(x.EntityId)) - // .DistinctBy(x => x.EntityId) - // .OrderBy(x => x.EntityId); - - //foreach (var taggedEntity in allExportedTaggedEntities) - //{ - // foreach (var taggedProperty in taggedEntity.TaggedProperties.Where(x => x.Tags.Any())) - // { - // XmlNode tagProp = new XElement("TagProperty"); - // var docId = packageManifest.CreateAttribute("docId", ""); - // docId.Value = taggedEntity.EntityId.ToString(CultureInfo.InvariantCulture); - // tagProp.Attributes.Append(docId); - - // var propertyAlias = packageManifest.CreateAttribute("propertyAlias", ""); - // propertyAlias.Value = taggedProperty.PropertyTypeAlias; - // tagProp.Attributes.Append(propertyAlias); - - // var group = packageManifest.CreateAttribute("group", ""); - // group.Value = taggedProperty.Tags.First().Group; - // tagProp.Attributes.Append(group); - - // tagProp.AppendChild(packageManifest.CreateCDataSection( - // JsonConvert.SerializeObject(taggedProperty.Tags.Select(x => x.Text).ToArray()))); - - // tagProps.AppendChild(tagProp); - // } - //} - - //manifestRoot.Add(tagProps); - } - } - } - } - - - private Dictionary PackageMedia(PackageDefinition definition, XElement root) - { - var mediaStreams = new Dictionary(); - - // callback that occurs on each serialized media item - void OnSerializedMedia(IMedia media, XElement xmlMedia) - { - // get the media file path and store that separately in the XML. - // the media file path is different from the URL and is specifically - // extracted using the property editor for this media file and the current media file system. - Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); - if (mediaStream != null && mediaFilePath is not null) - { - xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); - - // add the stream to our outgoing stream - mediaStreams.Add(mediaFilePath, mediaStream); - } - } - - IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - - var mediaXml = new XElement( - "MediaItems", - medias.Select(media => - { - XElement serializedMedia = _serializer.Serialize( - media, - definition.MediaLoadChildNodes, - OnSerializedMedia); - - return new XElement("MediaSet", serializedMedia); - })); - - root.Add(mediaXml); - - return mediaStreams; - } - - // TODO: Delete this - /// - private XElement? GetMacroXml(int macroId, out IMacro? macro) - { - macro = _macroService.GetById(macroId); - if (macro == null) - return null; - var xml = _serializer.Serialize(macro); - return xml; - } - - /// - /// Converts a umbraco stylesheet to a package xml node - /// - /// The path of the stylesheet. - /// if set to true [include properties]. - /// - private XElement? GetStylesheetXml(string path, bool includeProperties) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - } - - IStylesheet? stylesheet = _fileService.GetStylesheet(path); - if (stylesheet == null) - { - return null; - } - - return _serializer.Serialize(stylesheet, includeProperties); - } - - private void AddDocumentType(IContentType dt, HashSet dtl) - { - if (dt.ParentId > 0) - { - var parent = _contentTypeService.Get(dt.ParentId); - if (parent != null) // could be a container - AddDocumentType(parent, dtl); - } - - if (!dtl.Contains(dt)) - dtl.Add(dt); - } - - private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) - { - if (mediaType.ParentId > 0) - { - var parent = _mediaTypeService.Get(mediaType.ParentId); - if (parent != null) // could be a container - AddMediaType(parent, mediaTypes); - } - - if (!mediaTypes.Contains(mediaType)) - mediaTypes.Add(mediaType); - } - - private static XElement GetPackageInfoXml(PackageDefinition definition) - { - var info = new XElement("info"); - - //Package info - var package = new XElement("package"); - package.Add(new XElement("name", definition.Name)); - info.Add(package); - return info; - } - - private static XDocument CreateCompiledPackageXml(out XElement root) - { - root = new XElement("umbPackage"); - var compiledPackageXml = new XDocument(root); - return compiledPackageXml; - } - - private XDocument EnsureStorage(out string packagesFile) - { - var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - Directory.CreateDirectory(packagesFolder); - - packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); - if (!File.Exists(packagesFile)) - { - var xml = new XDocument(new XElement("packages")); - xml.Save(packagesFile); - - return xml; - } - - var packagesXml = XDocument.Load(packagesFile); - return packagesXml; - } - - public void DeleteLocalRepositoryFiles() - { - var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); - if (File.Exists(packagesFile)) - { - File.Delete(packagesFile); - } - - var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - if (Directory.Exists(packagesFolder)) - { - Directory.Delete(packagesFolder); - } } } + + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); + + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) + { + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + if (mediaStream != null && mediaFilePath is not null) + { + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); + + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); + } + } + + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); + + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => + { + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); + + return new XElement("MediaSet", serializedMedia); + })); + + root.Add(mediaXml); + + return mediaStreams; + } + + // TODO: Delete this + private XElement? GetMacroXml(int macroId, out IMacro? macro) + { + macro = _macroService.GetById(macroId); + if (macro == null) + { + return null; + } + + XElement xml = _serializer.Serialize(macro); + return xml; + } + + /// + /// Converts a umbraco stylesheet to a package xml node + /// + /// The path of the stylesheet. + /// if set to true [include properties]. + /// + private XElement? GetStylesheetXml(string path, bool includeProperties) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + } + + IStylesheet? stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) + { + return null; + } + + return _serializer.Serialize(stylesheet, includeProperties); + } + + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) + { + IContentType? parent = _contentTypeService.Get(dt.ParentId); + + // could be a container + if (parent != null) + { + AddDocumentType(parent, dtl); + } + } + + if (!dtl.Contains(dt)) + { + dtl.Add(dt); + } + } + + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) + { + IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); + + // could be a container + if (parent != null) + { + AddMediaType(parent, mediaTypes); + } + } + + if (!mediaTypes.Contains(mediaType)) + { + mediaTypes.Add(mediaType); + } + } + + private static XDocument CreateCompiledPackageXml(out XElement root) + { + root = new XElement("umbPackage"); + var compiledPackageXml = new XDocument(root); + return compiledPackageXml; + } + + private XDocument EnsureStorage(out string packagesFile) + { + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); + Directory.CreateDirectory(packagesFolder); + + packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); + if (!File.Exists(packagesFile)) + { + var xml = new XDocument(new XElement("packages")); + xml.Save(packagesFile); + + return xml; + } + + var packagesXml = XDocument.Load(packagesFile); + return packagesXml; + } } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index de5b8c04ae..420f36c759 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -1,90 +1,91 @@ // ReSharper disable once CheckNamespace -namespace Umbraco.Cms.Core + +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class DatabaseSchema { - public static class DatabaseSchema + // TODO: Why aren't all table names with the same prefix? + public const string TableNamePrefix = "umbraco"; + + public static class Tables { - //TODO: Why aren't all table names with the same prefix? - public const string TableNamePrefix = "umbraco"; + public const string Lock = TableNamePrefix + "Lock"; + public const string Log = TableNamePrefix + "Log"; - public static class Tables - { - public const string Lock = TableNamePrefix + "Lock"; - public const string Log = TableNamePrefix + "Log"; + public const string Node = TableNamePrefix + "Node"; + public const string NodeData = /*TableNamePrefix*/ "cms" + "ContentNu"; - public const string Node = TableNamePrefix + "Node"; - public const string NodeData = /*TableNamePrefix*/ "cms" + "ContentNu"; + public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; + public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType"; + public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; + public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; + public const string DataType = TableNamePrefix + "DataType"; + public const string Template = /*TableNamePrefix*/ "cms" + "Template"; - public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; - public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType"; - public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; - public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; - public const string DataType = TableNamePrefix + "DataType"; - public const string Template = /*TableNamePrefix*/ "cms" + "Template"; + public const string Content = TableNamePrefix + "Content"; + public const string ContentVersion = TableNamePrefix + "ContentVersion"; + public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; + public const string ContentVersionCleanupPolicy = TableNamePrefix + "ContentVersionCleanupPolicy"; - public const string Content = TableNamePrefix + "Content"; - public const string ContentVersion = TableNamePrefix + "ContentVersion"; - public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; - public const string ContentVersionCleanupPolicy = TableNamePrefix + "ContentVersionCleanupPolicy"; + public const string Document = TableNamePrefix + "Document"; + public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; + public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; + public const string MediaVersion = TableNamePrefix + "MediaVersion"; + public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; - public const string Document = TableNamePrefix + "Document"; - public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; - public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; - public const string MediaVersion = TableNamePrefix + "MediaVersion"; - public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; + public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType"; + public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; + public const string PropertyData = TableNamePrefix + "PropertyData"; - public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType"; - public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; - public const string PropertyData = TableNamePrefix + "PropertyData"; + public const string RelationType = TableNamePrefix + "RelationType"; + public const string Relation = TableNamePrefix + "Relation"; - public const string RelationType = TableNamePrefix + "RelationType"; - public const string Relation = TableNamePrefix + "Relation"; + public const string Domain = TableNamePrefix + "Domain"; + public const string Language = TableNamePrefix + "Language"; + public const string DictionaryEntry = /*TableNamePrefix*/ "cms" + "Dictionary"; + public const string DictionaryValue = /*TableNamePrefix*/ "cms" + "LanguageText"; - public const string Domain = TableNamePrefix + "Domain"; - public const string Language = TableNamePrefix + "Language"; - public const string DictionaryEntry = /*TableNamePrefix*/ "cms" + "Dictionary"; - public const string DictionaryValue = /*TableNamePrefix*/ "cms" + "LanguageText"; + public const string User = TableNamePrefix + "User"; + public const string UserGroup = TableNamePrefix + "UserGroup"; + public const string UserStartNode = TableNamePrefix + "UserStartNode"; + public const string User2UserGroup = TableNamePrefix + "User2UserGroup"; + public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify"; + public const string UserGroup2App = TableNamePrefix + "UserGroup2App"; + public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node"; + public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission"; + public const string UserGroup2Language = TableNamePrefix + "UserGroup2Language"; + public const string ExternalLogin = TableNamePrefix + "ExternalLogin"; + public const string TwoFactorLogin = TableNamePrefix + "TwoFactorLogin"; + public const string ExternalLoginToken = TableNamePrefix + "ExternalLoginToken"; - public const string User = TableNamePrefix + "User"; - public const string UserGroup = TableNamePrefix + "UserGroup"; - public const string UserStartNode = TableNamePrefix + "UserStartNode"; - public const string User2UserGroup = TableNamePrefix + "User2UserGroup"; - public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify"; - public const string UserGroup2App = TableNamePrefix + "UserGroup2App"; - public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node"; - public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission"; - public const string ExternalLogin = TableNamePrefix + "ExternalLogin"; - public const string TwoFactorLogin = TableNamePrefix + "TwoFactorLogin"; - public const string ExternalLoginToken = TableNamePrefix + "ExternalLoginToken"; + public const string Macro = /*TableNamePrefix*/ "cms" + "Macro"; + public const string MacroProperty = /*TableNamePrefix*/ "cms" + "MacroProperty"; - public const string Macro = /*TableNamePrefix*/ "cms" + "Macro"; - public const string MacroProperty = /*TableNamePrefix*/ "cms" + "MacroProperty"; + public const string Member = /*TableNamePrefix*/ "cms" + "Member"; + public const string MemberPropertyType = /*TableNamePrefix*/ "cms" + "MemberType"; + public const string Member2MemberGroup = /*TableNamePrefix*/ "cms" + "Member2MemberGroup"; - public const string Member = /*TableNamePrefix*/ "cms" + "Member"; - public const string MemberPropertyType = /*TableNamePrefix*/ "cms" + "MemberType"; - public const string Member2MemberGroup = /*TableNamePrefix*/ "cms" + "Member2MemberGroup"; + public const string Access = TableNamePrefix + "Access"; + public const string AccessRule = TableNamePrefix + "AccessRule"; + public const string RedirectUrl = TableNamePrefix + "RedirectUrl"; - public const string Access = TableNamePrefix + "Access"; - public const string AccessRule = TableNamePrefix + "AccessRule"; - public const string RedirectUrl = TableNamePrefix + "RedirectUrl"; + public const string CacheInstruction = TableNamePrefix + "CacheInstruction"; + public const string Server = TableNamePrefix + "Server"; - public const string CacheInstruction = TableNamePrefix + "CacheInstruction"; - public const string Server = TableNamePrefix + "Server"; + public const string Tag = /*TableNamePrefix*/ "cms" + "Tags"; + public const string TagRelationship = /*TableNamePrefix*/ "cms" + "TagRelationship"; - public const string Tag = /*TableNamePrefix*/ "cms" + "Tags"; - public const string TagRelationship = /*TableNamePrefix*/ "cms" + "TagRelationship"; + public const string KeyValue = TableNamePrefix + "KeyValue"; - public const string KeyValue = TableNamePrefix + "KeyValue"; + public const string AuditEntry = TableNamePrefix + "Audit"; + public const string Consent = TableNamePrefix + "Consent"; + public const string UserLogin = TableNamePrefix + "UserLogin"; - public const string AuditEntry = TableNamePrefix + "Audit"; - public const string Consent = TableNamePrefix + "Consent"; - public const string UserLogin = TableNamePrefix + "UserLogin"; + public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; - public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; - - public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; - } + public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; } } } diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 3c0b2c4d28..e97f16a663 100644 --- a/src/Umbraco.Core/Persistence/Constants-Locks.cs +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -1,75 +1,74 @@ -// ReSharper disable once CheckNamespace +// ReSharper disable once CheckNamespace using Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - static partial class Constants + /// + /// Defines lock objects. + /// + public static class Locks { /// - /// Defines lock objects. + /// The lock /// - public static class Locks - { - /// - /// The lock - /// - public const int MainDom = -1000; + public const int MainDom = -1000; - /// - /// All servers. - /// - public const int Servers = -331; + /// + /// All servers. + /// + public const int Servers = -331; - /// - /// All content and media types. - /// - public const int ContentTypes = -332; + /// + /// All content and media types. + /// + public const int ContentTypes = -332; - /// - /// The entire content tree, i.e. all content items. - /// - public const int ContentTree = -333; + /// + /// The entire content tree, i.e. all content items. + /// + public const int ContentTree = -333; - /// - /// The entire media tree, i.e. all media items. - /// - public const int MediaTree = -334; + /// + /// The entire media tree, i.e. all media items. + /// + public const int MediaTree = -334; - /// - /// The entire member tree, i.e. all members. - /// - public const int MemberTree = -335; + /// + /// The entire member tree, i.e. all members. + /// + public const int MemberTree = -335; - /// - /// All media types. - /// - public const int MediaTypes = -336; + /// + /// All media types. + /// + public const int MediaTypes = -336; - /// - /// All member types. - /// - public const int MemberTypes = -337; + /// + /// All member types. + /// + public const int MemberTypes = -337; - /// - /// All domains. - /// - public const int Domains = -338; + /// + /// All domains. + /// + public const int Domains = -338; - /// - /// All key-values. - /// - public const int KeyValues = -339; + /// + /// All key-values. + /// + public const int KeyValues = -339; - /// - /// All languages. - /// - public const int Languages = -340; + /// + /// All languages. + /// + public const int Languages = -340; - /// - /// ScheduledPublishing job. - /// - public const int ScheduledPublishing = -341; - } + /// + /// ScheduledPublishing job. + /// + public const int ScheduledPublishing = -341; } } diff --git a/src/Umbraco.Core/Persistence/IQueryRepository.cs b/src/Umbraco.Core/Persistence/IQueryRepository.cs index 1a8dbaf971..e0e507abc1 100644 --- a/src/Umbraco.Core/Persistence/IQueryRepository.cs +++ b/src/Umbraco.Core/Persistence/IQueryRepository.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a querying repository. +/// +public interface IQueryRepository : IRepository { /// - /// Defines the base implementation of a querying repository. + /// Gets entities. /// - public interface IQueryRepository : IRepository - { - /// - /// Gets entities. - /// - IEnumerable Get(IQuery query); + IEnumerable Get(IQuery query); - /// - /// Counts entities. - /// - int Count(IQuery query); - } + /// + /// Counts entities. + /// + int Count(IQuery query); } diff --git a/src/Umbraco.Core/Persistence/IReadRepository.cs b/src/Umbraco.Core/Persistence/IReadRepository.cs index 0f757ae04a..6503019988 100644 --- a/src/Umbraco.Core/Persistence/IReadRepository.cs +++ b/src/Umbraco.Core/Persistence/IReadRepository.cs @@ -1,25 +1,22 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Persistence; -namespace Umbraco.Cms.Core.Persistence +/// +/// Defines the base implementation of a reading repository. +/// +public interface IReadRepository : IRepository { /// - /// Defines the base implementation of a reading repository. + /// Gets an entity. /// - public interface IReadRepository : IRepository - { - /// - /// Gets an entity. - /// - TEntity? Get(TId? id); + TEntity? Get(TId? id); - /// - /// Gets entities. - /// - IEnumerable GetMany(params TId[]? ids); + /// + /// Gets entities. + /// + IEnumerable GetMany(params TId[]? ids); - /// - /// Gets a value indicating whether an entity exists. - /// - bool Exists(TId id); - } + /// + /// Gets a value indicating whether an entity exists. + /// + bool Exists(TId id); } diff --git a/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs b/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs index b260144de6..40eb92bef6 100644 --- a/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs +++ b/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs @@ -1,8 +1,9 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a reading, writing and querying repository. +/// +public interface IReadWriteQueryRepository : IReadRepository, IWriteRepository, + IQueryRepository { - /// - /// Defines the base implementation of a reading, writing and querying repository. - /// - public interface IReadWriteQueryRepository : IReadRepository, IWriteRepository, IQueryRepository - { } } diff --git a/src/Umbraco.Core/Persistence/IRepository.cs b/src/Umbraco.Core/Persistence/IRepository.cs index f91c4c998b..2629e14c04 100644 --- a/src/Umbraco.Core/Persistence/IRepository.cs +++ b/src/Umbraco.Core/Persistence/IRepository.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a repository. +/// +public interface IRepository { - /// - /// Defines the base implementation of a repository. - /// - public interface IRepository - { } } diff --git a/src/Umbraco.Core/Persistence/IWriteRepository.cs b/src/Umbraco.Core/Persistence/IWriteRepository.cs index ff766fbe36..26e1548bc6 100644 --- a/src/Umbraco.Core/Persistence/IWriteRepository.cs +++ b/src/Umbraco.Core/Persistence/IWriteRepository.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a writing repository. +/// +public interface IWriteRepository : IRepository { /// - /// Defines the base implementation of a writing repository. + /// Saves an entity. /// - public interface IWriteRepository : IRepository - { - /// - /// Saves an entity. - /// - void Save(TEntity entity); + void Save(TEntity entity); - /// - /// Deletes an entity. - /// - /// - void Delete(TEntity entity); - } + /// + /// Deletes an entity. + /// + /// + void Delete(TEntity entity); } diff --git a/src/Umbraco.Core/Persistence/Querying/IQuery.cs b/src/Umbraco.Core/Persistence/Querying/IQuery.cs index d2a3b0830f..8803d69fc0 100644 --- a/src/Umbraco.Core/Persistence/Querying/IQuery.cs +++ b/src/Umbraco.Core/Persistence/Querying/IQuery.cs @@ -1,42 +1,39 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Linq.Expressions; -namespace Umbraco.Cms.Core.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence.Querying; + +/// +/// Represents a query for building Linq translatable SQL queries +/// +/// +public interface IQuery { /// - /// Represents a query for building Linq translatable SQL queries + /// Adds a where clause to the query /// - /// - public interface IQuery - { - /// - /// Adds a where clause to the query - /// - /// - /// This instance so calls to this method are chainable - IQuery Where(Expression> predicate); + /// + /// This instance so calls to this method are chainable + IQuery Where(Expression> predicate); - /// - /// Returns all translated where clauses and their sql parameters - /// - /// - IEnumerable> GetWhereClauses(); + /// + /// Returns all translated where clauses and their sql parameters + /// + /// + IEnumerable> GetWhereClauses(); - /// - /// Adds a where-in clause to the query - /// - /// - /// - /// This instance so calls to this method are chainable - IQuery WhereIn(Expression> fieldSelector, IEnumerable? values); + /// + /// Adds a where-in clause to the query + /// + /// + /// + /// This instance so calls to this method are chainable + IQuery WhereIn(Expression> fieldSelector, IEnumerable? values); - /// - /// Adds a set of OR-ed where clauses to the query. - /// - /// - /// This instance so calls to this method are chainable. - IQuery WhereAny(IEnumerable>> predicates); - } + /// + /// Adds a set of OR-ed where clauses to the query. + /// + /// + /// This instance so calls to this method are chainable. + IQuery WhereAny(IEnumerable>> predicates); } diff --git a/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs b/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs index 3e48a00d05..fa8e674b97 100644 --- a/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs +++ b/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs @@ -1,15 +1,15 @@ -namespace Umbraco.Cms.Core.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence.Querying; + +/// +/// Determines how to match a string property value +/// +public enum StringPropertyMatchType { - /// - /// Determines how to match a string property value - /// - public enum StringPropertyMatchType - { - Exact, - Contains, - StartsWith, - EndsWith, - //Deals with % as wildcard chars in a string - Wildcard - } + Exact, + Contains, + StartsWith, + EndsWith, + + // Deals with % as wildcard chars in a string + Wildcard, } diff --git a/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs b/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs index 58daf2e577..ab6fd4f938 100644 --- a/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs +++ b/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence.Querying; + +/// +/// Determine how to match a number or data value +/// +public enum ValuePropertyMatchType { - /// - /// Determine how to match a number or data value - /// - public enum ValuePropertyMatchType - { - Exact, - GreaterThan, - LessThan, - GreaterThanOrEqualTo, - LessThanOrEqualTo - } + Exact, + GreaterThan, + LessThan, + GreaterThanOrEqualTo, + LessThanOrEqualTo, } diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs index 159267c16e..ade100f0d2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Represents a repository for entities. +/// +public interface IAuditEntryRepository : IReadWriteQueryRepository { /// - /// Represents a repository for entities. + /// Gets a page of entries. /// - public interface IAuditEntryRepository : IReadWriteQueryRepository - { - /// - /// Gets a page of entries. - /// - IEnumerable GetPage(long pageIndex, int pageCount, out long records); + IEnumerable GetPage(long pageIndex, int pageCount, out long records); - /// - /// Determines whether the repository is available. - /// - /// During an upgrade, the repository may not be available, until the table has been created. - bool IsAvailable(); - } + /// + /// Determines whether the repository is available. + /// + /// During an upgrade, the repository may not be available, until the table has been created. + bool IsAvailable(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs index 6d28a86b64..acceefef5d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs @@ -1,38 +1,40 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IAuditRepository : IReadRepository, IWriteRepository, + IQueryRepository { - public interface IAuditRepository : IReadRepository, IWriteRepository, IQueryRepository - { - void CleanLogs(int maximumAgeOfLogsInMinutes); + void CleanLogs(int maximumAgeOfLogsInMinutes); - /// - /// Return the audit items as paged result - /// - /// - /// The query coming from the service - /// - /// - /// - /// - /// - /// - /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter - /// so we need to do that here - /// - /// - /// A user supplied custom filter - /// - /// - IEnumerable GetPagedResultsByQuery( - IQuery query, - long pageIndex, int pageSize, out long totalRecords, - Direction orderDirection, - AuditType[]? auditTypeFilter, - IQuery? customFilter); + /// + /// Return the audit items as paged result + /// + /// + /// The query coming from the service + /// + /// + /// + /// + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query + /// or the custom filter + /// so we need to do that here + /// + /// + /// A user supplied custom filter + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, + long pageIndex, + int pageSize, + out long totalRecords, + Direction orderDirection, + AuditType[]? auditTypeFilter, + IQuery? customFilter); - IEnumerable Get(AuditType type, IQuery query); - } + IEnumerable Get(AuditType type, IQuery query); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs index e93f5829a1..f11ddf10e3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs @@ -1,50 +1,47 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Represents a repository for entities. +/// +public interface ICacheInstructionRepository : IRepository { /// - /// Represents a repository for entities. + /// Gets the count of pending cache instruction records. /// - public interface ICacheInstructionRepository : IRepository - { - /// - /// Gets the count of pending cache instruction records. - /// - int CountAll(); + int CountAll(); - /// - /// Gets the count of pending cache instructions. - /// - int CountPendingInstructions(int lastId); + /// + /// Gets the count of pending cache instructions. + /// + int CountPendingInstructions(int lastId); - /// - /// Gets the most recent cache instruction record Id. - /// - /// - int GetMaxId(); + /// + /// Gets the most recent cache instruction record Id. + /// + /// + int GetMaxId(); - /// - /// Checks to see if a single cache instruction by Id exists. - /// - bool Exists(int id); + /// + /// Checks to see if a single cache instruction by Id exists. + /// + bool Exists(int id); - /// - /// Adds a new cache instruction record. - /// - void Add(CacheInstruction cacheInstruction); + /// + /// Adds a new cache instruction record. + /// + void Add(CacheInstruction cacheInstruction); - /// - /// Gets a collection of cache instructions created later than the provided Id. - /// - /// Last id processed. - /// The maximum number of instructions to retrieve. - IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve); + /// + /// Gets a collection of cache instructions created later than the provided Id. + /// + /// Last id processed. + /// The maximum number of instructions to retrieve. + IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve); - /// - /// Deletes cache instructions older than the provided date. - /// - void DeleteInstructionsOlderThan(DateTime pruneDate); - } + /// + /// Deletes cache instructions older than the provided date. + /// + void DeleteInstructionsOlderThan(DateTime pruneDate); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs index a89ed56285..7fcdb9d2d9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Represents a repository for entities. +/// +public interface IConsentRepository : IReadWriteQueryRepository { /// - /// Represents a repository for entities. + /// Clears the current flag. /// - public interface IConsentRepository : IReadWriteQueryRepository - { - /// - /// Clears the current flag. - /// - void ClearCurrent(string source, string context, string action); - } + void ClearCurrent(string source, string context, string action); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index b753d35544..1172512228 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -1,83 +1,85 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Defines the base implementation of a repository for content items. +/// +public interface IContentRepository : IReadWriteQueryRepository + where TEntity : IUmbracoEntity { /// - /// Defines the base implementation of a repository for content items. + /// Gets the recycle bin identifier. /// - public interface IContentRepository : IReadWriteQueryRepository - where TEntity : IUmbracoEntity - { - /// - /// Gets versions. - /// - /// Current version is first, and then versions are ordered with most recent first. - IEnumerable GetAllVersions(int nodeId); + int RecycleBinId { get; } - /// - /// Gets versions. - /// - /// Current version is first, and then versions are ordered with most recent first. - IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take); + /// + /// Gets versions. + /// + /// Current version is first, and then versions are ordered with most recent first. + IEnumerable GetAllVersions(int nodeId); - /// - /// Gets version identifiers. - /// - /// Current version is first, and then versions are ordered with most recent first. - IEnumerable GetVersionIds(int id, int topRows); + /// + /// Gets versions. + /// + /// Current version is first, and then versions are ordered with most recent first. + IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take); - /// - /// Gets a version. - /// - TEntity? GetVersion(int versionId); + /// + /// Gets version identifiers. + /// + /// Current version is first, and then versions are ordered with most recent first. + IEnumerable GetVersionIds(int id, int topRows); - /// - /// Deletes a version. - /// - void DeleteVersion(int versionId); + /// + /// Gets a version. + /// + TEntity? GetVersion(int versionId); - /// - /// Deletes all versions older than a date. - /// - void DeleteVersions(int nodeId, DateTime versionDate); + /// + /// Deletes a version. + /// + void DeleteVersion(int versionId); - /// - /// Gets the recycle bin identifier. - /// - int RecycleBinId { get; } + /// + /// Deletes all versions older than a date. + /// + void DeleteVersions(int nodeId, DateTime versionDate); - /// - /// Gets the recycle bin content. - /// - IEnumerable? GetRecycleBin(); + /// + /// Gets the recycle bin content. + /// + IEnumerable? GetRecycleBin(); - /// - /// Gets the count of content items of a given content type. - /// - int Count(string? contentTypeAlias = null); + /// + /// Gets the count of content items of a given content type. + /// + int Count(string? contentTypeAlias = null); - /// - /// Gets the count of child content items of a given parent content, of a given content type. - /// - int CountChildren(int parentId, string? contentTypeAlias = null); + /// + /// Gets the count of child content items of a given parent content, of a given content type. + /// + int CountChildren(int parentId, string? contentTypeAlias = null); - /// - /// Gets the count of descendant content items of a given parent content, of a given content type. - /// - int CountDescendants(int parentId, string? contentTypeAlias = null); + /// + /// Gets the count of descendant content items of a given parent content, of a given content type. + /// + int CountDescendants(int parentId, string? contentTypeAlias = null); - /// - /// Gets paged content items. - /// - /// Here, can be null but cannot. - IEnumerable GetPage(IQuery? query, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering); + /// + /// Gets paged content items. + /// + /// Here, can be null but cannot. + IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter, + Ordering? ordering); - ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); - } + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs index 7bdfa294c8..5b122d860d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +// TODO +// this should be IContentTypeRepository, and what is IContentTypeRepository at the moment should +// become IDocumentTypeRepository - but since these interfaces are public, that would be breaking + +/// +/// Represents the content types common repository, dealing with document, media and member types. +/// +public interface IContentTypeCommonRepository { - // TODO - // this should be IContentTypeRepository, and what is IContentTypeRepository at the moment should - // become IDocumentTypeRepository - but since these interfaces are public, that would be breaking + /// + /// Gets and cache all types. + /// + IEnumerable? GetAllTypes(); /// - /// Represents the content types common repository, dealing with document, media and member types. + /// Clears the cache. /// - public interface IContentTypeCommonRepository - { - /// - /// Gets and cache all types. - /// - IEnumerable? GetAllTypes(); - - /// - /// Clears the cache. - /// - void ClearCache(); - } + void ClearCache(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs index 148132dc29..77adda5860 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs @@ -1,35 +1,32 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IContentTypeRepository : IContentTypeRepositoryBase { - public interface IContentTypeRepository : IContentTypeRepositoryBase - { - /// - /// Gets all entities of the specified query - /// - /// - /// An enumerable list of objects - IEnumerable GetByQuery(IQuery query); + /// + /// Gets all entities of the specified query + /// + /// + /// An enumerable list of objects + IEnumerable GetByQuery(IQuery query); - /// - /// Gets all property type aliases. - /// - /// - IEnumerable GetAllPropertyTypeAliases(); + /// + /// Gets all property type aliases. + /// + /// + IEnumerable GetAllPropertyTypeAliases(); - /// - /// Gets all content type aliases - /// - /// - /// If this list is empty, it will return all content type aliases for media, members and content, otherwise - /// it will only return content type aliases for the object types specified - /// - /// - IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); - IEnumerable GetAllContentTypeIds(string[] aliases); - } + IEnumerable GetAllContentTypeIds(string[] aliases); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 2a427da9dd..e90c70e89d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -1,45 +1,42 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IContentTypeRepositoryBase : IReadWriteQueryRepository, IReadRepository + where TItem : IContentTypeComposition { - public interface IContentTypeRepositoryBase : IReadWriteQueryRepository, IReadRepository - where TItem : IContentTypeComposition - { - TItem? Get(string alias); - IEnumerable> Move(TItem moving, EntityContainer container); + TItem? Get(string alias); - /// - /// Derives a unique alias from an existing alias. - /// - /// The original alias. - /// The original alias with a number appended to it, so that it is unique. - /// Unique across all content, media and member types. - string GetUniqueAlias(string alias); + IEnumerable> Move(TItem moving, EntityContainer container); + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + /// Unique across all content, media and member types. + string GetUniqueAlias(string alias); - /// - /// Gets a value indicating whether there is a list view content item in the path. - /// - /// - /// - bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(string contentPath); - /// - /// Gets a value indicating whether there is a list view content item in the path. - /// - /// - /// - bool HasContainerInPath(params int[] ids); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); - /// - /// Returns true or false depending on whether content nodes have been created based on the provided content type id. - /// - bool HasContentNodes(int id); - } + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + bool HasContentNodes(int id); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs index 3e19c08f99..69caeb8038 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDataTypeContainerRepository : IEntityContainerRepository { - public interface IDataTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs index e9063416af..060d2f2e1d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs @@ -1,18 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IDataTypeRepository : IReadWriteQueryRepository - { - IEnumerable> Move(IDataType toMove, EntityContainer? container); +namespace Umbraco.Cms.Core.Persistence.Repositories; - /// - /// Returns a dictionary of content type s and the property type aliases that use a - /// - /// - /// - IReadOnlyDictionary> FindUsages(int id); - } +public interface IDataTypeRepository : IReadWriteQueryRepository +{ + IEnumerable> Move(IDataType toMove, EntityContainer? container); + + /// + /// Returns a dictionary of content type s and the property type aliases that use a + /// + /// + /// + /// + IReadOnlyDictionary> FindUsages(int id); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeUsageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeUsageRepository.cs new file mode 100644 index 0000000000..ed52e4fb3f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeUsageRepository.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDataTypeUsageRepository +{ + bool HasSavedValues(int dataTypeId); +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs index 555624b1a0..db2347e925 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDictionaryRepository : IReadWriteQueryRepository { - public interface IDictionaryRepository : IReadWriteQueryRepository - { - IDictionaryItem? Get(Guid uniqueId); - IDictionaryItem? Get(string key); - IEnumerable GetDictionaryItemDescendants(Guid? parentId); - Dictionary GetDictionaryItemKeyMap(); - } + IDictionaryItem? Get(Guid uniqueId); + + IDictionaryItem? Get(string key); + + IEnumerable GetDictionaryItemDescendants(Guid? parentId); + + Dictionary GetDictionaryItemKeyMap(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs index e5e6e0f418..12857f0588 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentBlueprintRepository : IDocumentRepository { - public interface IDocumentBlueprintRepository : IDocumentRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index e0b7f234ec..15312ccbf2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -1,96 +1,98 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentRepository : IContentRepository, IReadRepository { - public interface IDocumentRepository : IContentRepository, IReadRepository - { - /// - /// Gets publish/unpublish schedule for a content node. - /// - /// - /// - ContentScheduleCollection GetContentSchedule(int contentId); + /// + /// Gets publish/unpublish schedule for a content node. + /// + /// + /// + /// + /// + ContentScheduleCollection GetContentSchedule(int contentId); - /// - /// Persists publish/unpublish schedule for a content node. - /// - /// - /// - void PersistContentSchedule(IContent content, ContentScheduleCollection schedule); + /// + /// Persists publish/unpublish schedule for a content node. + /// + /// + /// + void PersistContentSchedule(IContent content, ContentScheduleCollection schedule); - /// - /// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date. - /// - void ClearSchedule(DateTime date); + /// + /// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date. + /// + void ClearSchedule(DateTime date); - void ClearSchedule(DateTime date, ContentScheduleAction action); + void ClearSchedule(DateTime date, ContentScheduleAction action); - bool HasContentForExpiration(DateTime date); - bool HasContentForRelease(DateTime date); + bool HasContentForExpiration(DateTime date); - /// - /// Gets objects having an expiration date before (lower than, or equal to) a specified date. - /// - /// - /// The content returned from this method may be culture variant, in which case the resulting should be queried - /// for which culture(s) have been scheduled. - /// - IEnumerable GetContentForExpiration(DateTime date); + bool HasContentForRelease(DateTime date); - /// - /// Gets objects having a release date before (lower than, or equal to) a specified date. - /// - /// - /// The content returned from this method may be culture variant, in which case the resulting should be queried - /// for which culture(s) have been scheduled. - /// - IEnumerable GetContentForRelease(DateTime date); + /// + /// Gets objects having an expiration date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting + /// should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForExpiration(DateTime date); - /// - /// Get the count of published items - /// - /// - /// - /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter - /// - int CountPublished(string? contentTypeAlias = null); + /// + /// Gets objects having a release date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting + /// should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForRelease(DateTime date); - bool IsPathPublished(IContent? content); + /// + /// Get the count of published items + /// + /// + /// + /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter + /// + int CountPublished(string? contentTypeAlias = null); - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - void ReplaceContentPermissions(EntityPermissionSet permissionSet); + bool IsPathPublished(IContent? content); - /// - /// Assigns a single permission to the current content item for the specified user group ids - /// - /// - /// - /// - void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + void ReplaceContentPermissions(EntityPermissionSet permissionSet); - /// - /// Gets the explicit list of permissions for the content item - /// - /// - /// - EntityPermissionCollection GetPermissionsForEntity(int entityId); + /// + /// Assigns a single permission to the current content item for the specified user group ids + /// + /// + /// + /// + void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); - /// - /// Used to add/update a permission for a content item - /// - /// - void AddOrUpdatePermissions(ContentPermissionSet permission); + /// + /// Gets the explicit list of permissions for the content item + /// + /// + /// + EntityPermissionCollection GetPermissionsForEntity(int entityId); - /// - /// Returns true if there is any content in the recycle bin - /// - bool RecycleBinSmells(); - } + /// + /// Used to add/update a permission for a content item + /// + /// + void AddOrUpdatePermissions(ContentPermissionSet permission); + + /// + /// Returns true if there is any content in the recycle bin + /// + bool RecycleBinSmells(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs index 53fd62fdbe..ed604ec165 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentTypeContainerRepository : IEntityContainerRepository { - public interface IDocumentTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs index ee46db3690..7526d83cd0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs @@ -1,38 +1,36 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentVersionRepository : IRepository { - public interface IDocumentVersionRepository : IRepository - { - /// - /// Gets a list of all historic content versions. - /// - public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup(); + /// + /// Gets a list of all historic content versions. + /// + public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup(); - /// - /// Gets cleanup policy override settings per content type. - /// - public IReadOnlyCollection? GetCleanupPolicies(); + /// + /// Gets cleanup policy override settings per content type. + /// + public IReadOnlyCollection? GetCleanupPolicies(); - /// - /// Gets paginated content versions for given content id paginated. - /// - public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null); + /// + /// Gets paginated content versions for given content id paginated. + /// + public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null); - /// - /// Deletes multiple content versions by ID. - /// - void DeleteVersions(IEnumerable versionIds); + /// + /// Deletes multiple content versions by ID. + /// + void DeleteVersions(IEnumerable versionIds); - /// - /// Updates the prevent cleanup flag on a content version. - /// - void SetPreventCleanup(int versionId, bool preventCleanup); + /// + /// Updates the prevent cleanup flag on a content version. + /// + void SetPreventCleanup(int versionId, bool preventCleanup); - /// - /// Gets the content version metadata for a specific version. - /// - ContentVersionMeta? Get(int versionId); - } + /// + /// Gets the content version metadata for a specific version. + /// + ContentVersionMeta? Get(int versionId); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs index a24b76f90a..18b2ef1f8e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDomainRepository : IReadWriteQueryRepository { - public interface IDomainRepository : IReadWriteQueryRepository - { - IDomain? GetByName(string domainName); - bool Exists(string domainName); - IEnumerable GetAll(bool includeWildcards); - IEnumerable GetAssignedDomains(int contentId, bool includeWildcards); - } + IDomain? GetByName(string domainName); + + bool Exists(string domainName); + + IEnumerable GetAll(bool includeWildcards); + + IEnumerable GetAssignedDomains(int contentId, bool includeWildcards); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs index 6b8ece1bfd..3e2ae8c7b5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IEntityContainerRepository : IReadRepository, IWriteRepository - { - EntityContainer? Get(Guid id); +namespace Umbraco.Cms.Core.Persistence.Repositories; - IEnumerable Get(string name, int level); - } +public interface IEntityContainerRepository : IReadRepository, IWriteRepository +{ + EntityContainer? Get(Guid id); + + IEnumerable Get(string name, int level); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 8eeab0b834..ff7c8f12d9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -1,59 +1,70 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IEntityRepository : IRepository { - public interface IEntityRepository : IRepository - { - IEntitySlim? Get(int id); - IEntitySlim? Get(Guid key); - IEntitySlim? Get(int id, Guid objectTypeId); - IEntitySlim? Get(Guid key, Guid objectTypeId); + IEntitySlim? Get(int id); - IEnumerable GetAll(Guid objectType, params int[] ids); - IEnumerable GetAll(Guid objectType, params Guid[] keys); + IEntitySlim? Get(Guid key); - /// - /// Gets entities for a query - /// - /// - /// - IEnumerable GetByQuery(IQuery query); + IEntitySlim? Get(int id, Guid objectTypeId); - /// - /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized - /// - /// - /// - /// - IEnumerable GetByQuery(IQuery query, Guid objectType); + IEntitySlim? Get(Guid key, Guid objectTypeId); - UmbracoObjectTypes GetObjectType(int id); - UmbracoObjectTypes GetObjectType(Guid key); - int ReserveId(Guid key); + IEnumerable GetAll(Guid objectType, params int[] ids); - IEnumerable GetAllPaths(Guid objectType, params int[]? ids); - IEnumerable GetAllPaths(Guid objectType, params Guid[] keys); + IEnumerable GetAll(Guid objectType, params Guid[] keys); - bool Exists(int id); - bool Exists(Guid key); + /// + /// Gets entities for a query + /// + /// + /// + IEnumerable GetByQuery(IQuery query); - /// - /// Gets paged entities for a query and a specific object type - /// - /// - /// - /// - /// - /// - /// - /// - /// - IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering); - } + /// + /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized + /// + /// + /// + /// + IEnumerable GetByQuery(IQuery query, Guid objectType); + + UmbracoObjectTypes GetObjectType(int id); + + UmbracoObjectTypes GetObjectType(Guid key); + + int ReserveId(Guid key); + + IEnumerable GetAllPaths(Guid objectType, params int[]? ids); + + IEnumerable GetAllPaths(Guid objectType, params Guid[] keys); + + bool Exists(int id); + + bool Exists(Guid key); + + /// + /// Gets paged entities for a query and a specific object type + /// + /// + /// + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, + Guid objectType, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter, + Ordering? ordering); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs index 0a4b9e76cf..ec9a79530c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Repository for external logins with Guid as key, so it can be shared for members and users +/// +public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository, + IQueryRepository { + /// + /// Replaces all external login providers for the user/member key + /// + void Save(Guid userOrMemberKey, IEnumerable logins); /// - /// Repository for external logins with Guid as key, so it can be shared for members and users + /// Replaces all external login provider tokens for the providers specified for the user/member key /// - public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository, IQueryRepository - { - /// - /// Replaces all external login providers for the user/member key - /// - void Save(Guid userOrMemberKey, IEnumerable logins); + void Save(Guid userOrMemberKey, IEnumerable tokens); - /// - /// Replaces all external login provider tokens for the providers specified for the user/member key - /// - void Save(Guid userOrMemberKey, IEnumerable tokens); - - /// - /// Deletes all external logins for the specified the user/member key - /// - void DeleteUserLogins(Guid userOrMemberKey); - } + /// + /// Deletes all external logins for the specified the user/member key + /// + void DeleteUserLogins(Guid userOrMemberKey); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs index ce76086ed2..53e1bb4074 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs @@ -1,13 +1,10 @@ -using System.IO; +namespace Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Persistence.Repositories +public interface IFileRepository { - public interface IFileRepository - { - Stream GetFileContentStream(string filepath); + Stream GetFileContentStream(string filepath); - void SetFileContent(string filepath, Stream content); + void SetFileContent(string filepath, Stream content); - long GetFileSize(string filepath); - } + long GetFileSize(string filepath); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs index 77c2f9d40b..9914e49b26 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IFileWithFoldersRepository - { - void AddFolder(string folderPath); +namespace Umbraco.Cms.Core.Persistence.Repositories; - void DeleteFolder(string folderPath); - } +public interface IFileWithFoldersRepository +{ + void AddFolder(string folderPath); + + void DeleteFolder(string folderPath); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs index b2c7bc9aa1..6520644a7f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Persistence.Repositories; @@ -6,5 +5,6 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; public interface IIdKeyMapRepository { int? GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType); + Guid? GetIdForKey(int id, UmbracoObjectTypes umbracoObjectType); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs index 5dc7ab0555..f12bd612fc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs @@ -1,9 +1,6 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Persistence.Repositories +public interface IInstallationRepository { - public interface IInstallationRepository - { - Task SaveInstallLogAsync(InstallLog installLog); - } + Task SaveInstallLogAsync(InstallLog installLog); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs index c9ee7a9d25..c9792f009d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IKeyValueRepository : IReadRepository, IWriteRepository { - public interface IKeyValueRepository : IReadRepository, IWriteRepository - { - /// - /// Returns key/value pairs for all keys with the specified prefix. - /// - /// - /// - IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix); - } + /// + /// Returns key/value pairs for all keys with the specified prefix. + /// + /// + /// + IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs index 1be32de989..e7fff03bd7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs @@ -1,41 +1,40 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ILanguageRepository : IReadWriteQueryRepository { - public interface ILanguageRepository : IReadWriteQueryRepository - { - ILanguage? GetByIsoCode(string isoCode); + ILanguage? GetByIsoCode(string isoCode); - /// - /// Gets a language identifier from its ISO code. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true); + /// + /// Gets a language identifier from its ISO code. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true); - /// - /// Gets a language ISO code from its identifier. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - string? GetIsoCodeById(int? id, bool throwOnNotFound = true); + /// + /// Gets a language ISO code from its identifier. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + string? GetIsoCodeById(int? id, bool throwOnNotFound = true); - /// - /// Gets the default language ISO code. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - string GetDefaultIsoCode(); + /// + /// Gets the default language ISO code. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + string GetDefaultIsoCode(); - /// - /// Gets the default language identifier. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - int? GetDefaultId(); - } + /// + /// Gets the default language identifier. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + int? GetDefaultId(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs index 8e3d779b9d..0d1da11c9d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs @@ -1,9 +1,8 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ILogViewerQueryRepository : IReadWriteQueryRepository { - public interface ILogViewerQueryRepository : IReadWriteQueryRepository - { - ILogViewerQuery? GetByName(string name); - } + ILogViewerQuery? GetByName(string name); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs index 5db9663f9e..136abec3c4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs @@ -1,12 +1,11 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMacroRepository : IReadWriteQueryRepository, IReadRepository { - public interface IMacroRepository : IReadWriteQueryRepository, IReadRepository - { - IMacro? GetByAlias(string alias); + IMacro? GetByAlias(string alias); + + IEnumerable GetAllByAlias(string[] aliases); - IEnumerable GetAllByAlias(string[] aliases); - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs index ad268c6292..d51f031071 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs @@ -1,11 +1,10 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMediaRepository : IContentRepository, IReadRepository { - public interface IMediaRepository : IContentRepository, IReadRepository - { - IMedia? GetMediaByPath(string mediaPath); - bool RecycleBinSmells(); - } + IMedia? GetMediaByPath(string mediaPath); + + bool RecycleBinSmells(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs index cf2c181d5f..fe8c798915 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMediaTypeContainerRepository : IEntityContainerRepository { - public interface IMediaTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs index 2a1168ae57..ac06431ee8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMediaTypeRepository : IContentTypeRepositoryBase { - public interface IMediaTypeRepository : IContentTypeRepositoryBase - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs index a7187ec1ca..fc12afe1d3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs @@ -1,51 +1,46 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberGroupRepository : IReadWriteQueryRepository { - public interface IMemberGroupRepository : IReadWriteQueryRepository - { - /// - /// Gets a member group by it's uniqueId - /// - /// - /// - IMemberGroup? Get(Guid uniqueId); + /// + /// Gets a member group by it's uniqueId + /// + /// + /// + IMemberGroup? Get(Guid uniqueId); - /// - /// Gets a member group by it's name - /// - /// - /// - IMemberGroup? GetByName(string? name); + /// + /// Gets a member group by it's name + /// + /// + /// + IMemberGroup? GetByName(string? name); - /// - /// Creates the new member group if it doesn't already exist - /// - /// - IMemberGroup? CreateIfNotExists(string roleName); + /// + /// Creates the new member group if it doesn't already exist + /// + /// + IMemberGroup? CreateIfNotExists(string roleName); - /// - /// Returns the member groups for a given member - /// - /// - /// - IEnumerable GetMemberGroupsForMember(int memberId); + /// + /// Returns the member groups for a given member + /// + /// + /// + IEnumerable GetMemberGroupsForMember(int memberId); - /// - /// Returns the member groups for a given member - /// - /// - /// - IEnumerable GetMemberGroupsForMember(string? username); + /// + /// Returns the member groups for a given member + /// + /// + /// + IEnumerable GetMemberGroupsForMember(string? username); - void ReplaceRoles(int[] memberIds, string[] roleNames); + void ReplaceRoles(int[] memberIds, string[] roleNames); - void AssignRoles(int[] memberIds, string[] roleNames); + void AssignRoles(int[] memberIds, string[] roleNames); - void DissociateRoles(int[] memberIds, string[] roleNames); - - - } + void DissociateRoles(int[] memberIds, string[] roleNames); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs index e3e9af91d7..32c04bdb4b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs @@ -1,44 +1,41 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberRepository : IContentRepository { - public interface IMemberRepository : IContentRepository - { - int[] GetMemberIds(string[] names); + int[] GetMemberIds(string[] names); - IMember? GetByUsername(string? username); + IMember? GetByUsername(string? username); - /// - /// Finds members in a given role - /// - /// - /// - /// - /// - IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + /// + /// Finds members in a given role + /// + /// + /// + /// + /// + IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - /// - /// Get all members in a specific group - /// - /// - /// - IEnumerable GetByMemberGroup(string groupName); + /// + /// Get all members in a specific group + /// + /// + /// + IEnumerable GetByMemberGroup(string groupName); - /// - /// Checks if a member with the username exists - /// - /// - /// - bool Exists(string username); + /// + /// Checks if a member with the username exists + /// + /// + /// + bool Exists(string username); - /// - /// Gets the count of items based on a complex query - /// - /// - /// - int GetCountByQuery(IQuery? query); - } + /// + /// Gets the count of items based on a complex query + /// + /// + /// + int GetCountByQuery(IQuery? query); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs index 1ccf3e756c..255e872206 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberTypeContainerRepository : IEntityContainerRepository { - public interface IMemberTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs index 0b31f0ba46..f9cd35534a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberTypeRepository : IContentTypeRepositoryBase { - public interface IMemberTypeRepository : IContentTypeRepositoryBase - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs index 4ae191fa72..5f93a912fc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs @@ -1,9 +1,8 @@ -using System; - namespace Umbraco.Cms.Core.Persistence.Repositories; public interface INodeCountRepository { int GetNodeCount(Guid nodeType); + int GetMediaCount(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs index be1a00a130..5a3f63f8cb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs @@ -1,20 +1,24 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface INotificationsRepository : IRepository { - public interface INotificationsRepository : IRepository - { - Notification CreateNotification(IUser user, IEntity entity, string action); - int DeleteNotifications(IUser user); - int DeleteNotifications(IEntity entity); - int DeleteNotifications(IUser user, IEntity entity); - IEnumerable? GetEntityNotifications(IEntity entity); - IEnumerable? GetUserNotifications(IUser user); - IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType); - IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions); - } + Notification CreateNotification(IUser user, IEntity entity, string action); + + int DeleteNotifications(IUser user); + + int DeleteNotifications(IEntity entity); + + int DeleteNotifications(IUser user, IEntity entity); + + IEnumerable? GetEntityNotifications(IEntity entity); + + IEnumerable? GetUserNotifications(IUser user); + + IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType); + + IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs index c731d39780..ba6d24c2d8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +// this only exists to differentiate with IPartialViewRepository in IoC +// without resorting to constants, names, whatever - and IPartialViewRepository +// is implemented by PartialViewRepository and IPartialViewMacroRepository by +// PartialViewMacroRepository - just to inject the proper filesystem. +public interface IPartialViewMacroRepository : IPartialViewRepository { - // this only exists to differentiate with IPartialViewRepository in IoC - // without resorting to constants, names, whatever - and IPartialViewRepository - // is implemented by PartialViewRepository and IPartialViewMacroRepository by - // PartialViewMacroRepository - just to inject the proper filesystem. - public interface IPartialViewMacroRepository : IPartialViewRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs index a8a84079fa..72b8fa2af0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs @@ -1,8 +1,8 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IPartialViewRepository : IReadRepository, IWriteRepository, + IFileRepository, IFileWithFoldersRepository { - public interface IPartialViewRepository : IReadRepository, IWriteRepository, IFileRepository, IFileWithFoldersRepository - { - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IPropertyTypeUsageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPropertyTypeUsageRepository.cs new file mode 100644 index 0000000000..d6f1053122 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IPropertyTypeUsageRepository.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IPropertyTypeUsageRepository +{ + bool HasSavedPropertyValues(string propertyTypeAlias); +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs index 2190782d3b..84ef0e92f5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs @@ -1,8 +1,7 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IPublicAccessRepository : IReadWriteQueryRepository { - public interface IPublicAccessRepository : IReadWriteQueryRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs index 17be5b3856..b7f29edc92 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs @@ -1,89 +1,102 @@ -using System; -using System.Collections.Generic; +using System.Threading.Tasks; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Defines the repository. +/// +public interface IRedirectUrlRepository : IReadWriteQueryRepository { /// - /// Defines the repository. + /// Gets a redirect URL. /// - public interface IRedirectUrlRepository : IReadWriteQueryRepository - { - /// - /// Gets a redirect URL. - /// - /// The Umbraco redirect URL route. - /// The content unique key. - /// The culture. - /// - IRedirectUrl? Get(string url, Guid contentKey, string? culture); + /// The Umbraco redirect URL route. + /// The content unique key. + /// The culture. + /// + IRedirectUrl? Get(string url, Guid contentKey, string? culture); - /// - /// Deletes a redirect URL. - /// - /// The redirect URL identifier. - void Delete(Guid id); + /// + /// Deletes a redirect URL. + /// + /// The redirect URL identifier. + void Delete(Guid id); - /// - /// Deletes all redirect URLs. - /// - void DeleteAll(); + /// + /// Deletes all redirect URLs. + /// + void DeleteAll(); - /// - /// Deletes all redirect URLs for a given content. - /// - /// The content unique key. - void DeleteContentUrls(Guid contentKey); + /// + /// Deletes all redirect URLs for a given content. + /// + /// The content unique key. + void DeleteContentUrls(Guid contentKey); - /// - /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. - /// - /// The Umbraco redirect URL route. - /// The most recent redirect URL corresponding to the route. - IRedirectUrl? GetMostRecentUrl(string url); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The most recent redirect URL corresponding to the route. + IRedirectUrl? GetMostRecentUrl(string url); - /// - /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. - /// - /// The Umbraco redirect URL route. - /// The culture the domain is associated with - /// The most recent redirect URL corresponding to the route. - IRedirectUrl? GetMostRecentUrl(string url, string culture); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The most recent redirect URL corresponding to the route. + Task GetMostRecentUrlAsync(string url) => Task.FromResult(GetMostRecentUrl(url)); - /// - /// Gets all redirect URLs for a content item. - /// - /// The content unique key. - /// All redirect URLs for the content item. - IEnumerable GetContentUrls(Guid contentKey); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The culture the domain is associated with + /// The most recent redirect URL corresponding to the route. + IRedirectUrl? GetMostRecentUrl(string url, string culture); - /// - /// Gets all redirect URLs. - /// - /// The page index. - /// The page size. - /// The total count of redirect URLs. - /// The redirect URLs. - IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The culture the domain is associated with + /// The most recent redirect URL corresponding to the route. + Task GetMostRecentUrlAsync(string url, string culture) => Task.FromResult(GetMostRecentUrl(url, culture)); - /// - /// Gets all redirect URLs below a given content item. - /// - /// The content unique identifier. - /// The page index. - /// The page size. - /// The total count of redirect URLs. - /// The redirect URLs. - IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total); + /// + /// Gets all redirect URLs for a content item. + /// + /// The content unique key. + /// All redirect URLs for the content item. + IEnumerable GetContentUrls(Guid contentKey); - /// - /// Searches for all redirect URLs that contain a given search term in their URL property. - /// - /// The term to search for. - /// The page index. - /// The page size. - /// The total count of redirect URLs. - /// The redirect URLs. - IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total); - } + /// + /// Gets all redirect URLs. + /// + /// The page index. + /// The page size. + /// The total count of redirect URLs. + /// The redirect URLs. + IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total); + + /// + /// Gets all redirect URLs below a given content item. + /// + /// The content unique identifier. + /// The page index. + /// The page size. + /// The total count of redirect URLs. + /// The redirect URLs. + IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total); + + /// + /// Searches for all redirect URLs that contain a given search term in their URL property. + /// + /// The term to search for. + /// The page index. + /// The page size. + /// The total count of redirect URLs. + /// The redirect URLs. + IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 0165b9eb39..8077a80dc1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,39 +1,36 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IRelationRepository : IReadWriteQueryRepository { - public interface IRelationRepository : IReadWriteQueryRepository - { - IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering); + IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering); - /// - /// Persist multiple at once - /// - /// - void Save(IEnumerable relations); + /// + /// Persist multiple at once + /// + /// + void Save(IEnumerable relations); - /// - /// Persist multiple at once but Ids are not returned on created relations - /// - /// - void SaveBulk(IEnumerable relations); + /// + /// Persist multiple at once but Ids are not returned on created relations + /// + /// + void SaveBulk(IEnumerable relations); - /// - /// Deletes all relations for a parent for any specified relation type alias - /// - /// - /// - /// A list of relation types to match for deletion, if none are specified then all relations for this parent id are deleted - /// - void DeleteByParent(int parentId, params string[] relationTypeAliases); + /// + /// Deletes all relations for a parent for any specified relation type alias + /// + /// + /// + /// A list of relation types to match for deletion, if none are specified then all relations for this parent id are deleted. + /// + void DeleteByParent(int parentId, params string[] relationTypeAliases); - IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); + IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); - IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); - } + IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs index 26dfe4acba..19929ee83f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs @@ -1,8 +1,8 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IRelationTypeRepository : IReadWriteQueryRepository, + IReadRepository { - public interface IRelationTypeRepository : IReadWriteQueryRepository, IReadRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs index 604e1da8d2..f0cfe94902 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs @@ -1,9 +1,8 @@ -using System.IO; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IScriptRepository : IReadRepository, IWriteRepository, IFileRepository, + IFileWithFoldersRepository { - public interface IScriptRepository : IReadRepository, IWriteRepository, IFileRepository, IFileWithFoldersRepository - { - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs index af3555160e..5593dec09a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs @@ -1,12 +1,10 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IServerRegistrationRepository : IReadWriteQueryRepository - { - void DeactiveStaleServers(TimeSpan staleTimeout); +namespace Umbraco.Cms.Core.Persistence.Repositories; - void ClearCache(); - } +public interface IServerRegistrationRepository : IReadWriteQueryRepository +{ + void DeactiveStaleServers(TimeSpan staleTimeout); + + void ClearCache(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs index dcdb5debe7..29f132a74a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs @@ -1,8 +1,8 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IStylesheetRepository : IReadRepository, IWriteRepository, + IFileRepository, IFileWithFoldersRepository { - public interface IStylesheetRepository : IReadRepository, IWriteRepository, IFileRepository, IFileWithFoldersRepository - { - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs index e2fa2e4406..35c134adb3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs @@ -1,95 +1,98 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITagRepository : IReadWriteQueryRepository { - public interface ITagRepository : IReadWriteQueryRepository - { - #region Assign and Remove Tags + #region Assign and Remove Tags - /// - /// Assign tags to a content property. - /// - /// The identifier of the content item. - /// The identifier of the property type. - /// The tags to assign. - /// A value indicating whether to replace already assigned tags. - /// - /// When is false, the tags specified in are added to those already assigned. - /// When is empty and is true, all assigned tags are removed. - /// - // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it - void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true); + /// + /// Assign tags to a content property. + /// + /// The identifier of the content item. + /// The identifier of the property type. + /// The tags to assign. + /// A value indicating whether to replace already assigned tags. + /// + /// + /// When is false, the tags specified in are added to + /// those already assigned. + /// + /// + /// When is empty and is true, all assigned tags are + /// removed. + /// + /// + // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it + void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true); - /// - /// Removes assigned tags from a content property. - /// - /// The identifier of the content item. - /// The identifier of the property type. - /// The tags to remove. - void Remove(int contentId, int propertyTypeId, IEnumerable tags); + /// + /// Removes assigned tags from a content property. + /// + /// The identifier of the content item. + /// The identifier of the property type. + /// The tags to remove. + void Remove(int contentId, int propertyTypeId, IEnumerable tags); - /// - /// Removes all assigned tags from a content item. - /// - /// The identifier of the content item. - void RemoveAll(int contentId); + /// + /// Removes all assigned tags from a content item. + /// + /// The identifier of the content item. + void RemoveAll(int contentId); - /// - /// Removes all assigned tags from a content property. - /// - /// The identifier of the content item. - /// The identifier of the property type. - void RemoveAll(int contentId, int propertyTypeId); + /// + /// Removes all assigned tags from a content property. + /// + /// The identifier of the content item. + /// The identifier of the property type. + void RemoveAll(int contentId, int propertyTypeId); - #endregion + #endregion - #region Queries + #region Queries - /// - /// Gets a tagged entity. - /// - TaggedEntity? GetTaggedEntityByKey(Guid key); + /// + /// Gets a tagged entity. + /// + TaggedEntity? GetTaggedEntityByKey(Guid key); - /// - /// Gets a tagged entity. - /// - TaggedEntity? GetTaggedEntityById(int id); + /// + /// Gets a tagged entity. + /// + TaggedEntity? GetTaggedEntityById(int id); - /// Gets all entities of a type, tagged with any tag in the specified group. - IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string? culture = null); + /// Gets all entities of a type, tagged with any tag in the specified group. + IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string? culture = null); - /// - /// Gets all entities of a type, tagged with the specified tag. - /// - IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string? group = null, string? culture = null); + /// + /// Gets all entities of a type, tagged with the specified tag. + /// + IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string? group = null, string? culture = null); - /// - /// Gets all tags for an entity type. - /// - IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, string? culture = null); + /// + /// Gets all tags for an entity type. + /// + IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity. - /// - IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity. + /// + IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity. - /// - IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity. + /// + IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity via a property. - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity via a property. - /// - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null); - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs index 185973623c..5c5881ef7a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITemplateRepository : IReadWriteQueryRepository, IFileRepository { - public interface ITemplateRepository : IReadWriteQueryRepository, IFileRepository - { - ITemplate? Get(string? alias); + ITemplate? Get(string? alias); - IEnumerable GetAll(params string[] aliases); + IEnumerable GetAll(params string[] aliases); - IEnumerable GetChildren(int masterTemplateId); + IEnumerable GetChildren(int masterTemplateId); - IEnumerable GetDescendants(int masterTemplateId); - } + IEnumerable GetDescendants(int masterTemplateId); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs index e6ca8eaa50..a69722c04a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs @@ -1,42 +1,49 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITrackedReferencesRepository { - public interface ITrackedReferencesRepository - { - /// - /// Gets a page of items which are in relation with the current item. - /// Basically, shows the items which depend on the current item. - /// - /// The identifier of the entity to retrieve relations for. - /// The page index. - /// The page size. - /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). - /// The total count of the items with reference to the current item. - /// An enumerable list of objects. - IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); + /// + /// Gets a page of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + /// The identifier of the entity to retrieve relations for. + /// The page index. + /// The page size. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of the items with reference to the current item. + /// An enumerable list of objects. + IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); - /// - /// Gets a page of items used in any kind of relation from selected integer ids. - /// - /// The identifiers of the entities to check for relations. - /// The page index. - /// The page size. - /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). - /// The total count of the items in any kind of relation. - /// An enumerable list of objects. - IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); + /// + /// Gets a page of items used in any kind of relation from selected integer ids. + /// + /// The identifiers of the entities to check for relations. + /// The page index. + /// The page size. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of the items in any kind of relation. + /// An enumerable list of objects. + IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); - /// - /// Gets a page of the descending items that have any references, given a parent id. - /// - /// The unique identifier of the parent to retrieve descendants for. - /// The page index. - /// The page size. - /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). - /// The total count of descending items. - /// An enumerable list of objects. - IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); - } + /// + /// Gets a page of the descending items that have any references, given a parent id. + /// + /// The unique identifier of the parent to retrieve descendants for. + /// The page index. + /// The page size. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of descending items. + /// An enumerable list of objects. + IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs index 63622f8e82..31a279eb62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITwoFactorLoginRepository : IReadRepository, IWriteRepository { - public interface ITwoFactorLoginRepository: IReadRepository, IWriteRepository - { - Task DeleteUserLoginsAsync(Guid userOrMemberKey); - Task DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName); + Task DeleteUserLoginsAsync(Guid userOrMemberKey); - Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey); - } + Task DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName); + Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs index d64f177f14..7a0d8b6f74 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IUpgradeCheckRepository { - public interface IUpgradeCheckRepository - { - Task CheckUpgradeAsync(SemVersion version); - } + Task CheckUpgradeAsync(SemVersion version); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs index d5cf6fd762..0959019af2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs @@ -1,59 +1,63 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IUserGroupRepository : IReadWriteQueryRepository { - public interface IUserGroupRepository : IReadWriteQueryRepository - { - /// - /// Gets a group by it's alias - /// - /// - /// - IUserGroup? Get(string alias); + /// + /// Gets a group by it's alias + /// + /// + /// + IUserGroup? Get(string alias); - /// - /// This is useful when an entire section is removed from config - /// - /// - IEnumerable GetGroupsAssignedToSection(string sectionAlias); + /// + /// This is useful when an entire section is removed from config + /// + /// + IEnumerable GetGroupsAssignedToSection(string sectionAlias); - /// - /// Used to add or update a user group and assign users to it - /// - /// - /// - void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds); + /// + /// Used to add or update a user group and assign users to it + /// + /// + /// + void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds); - /// - /// Gets explicitly defined permissions for the group for specified entities - /// - /// - /// Array of entity Ids, if empty will return permissions for the group for all entities - EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds); + /// + /// Gets explicitly defined permissions for the group for specified entities + /// + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds); - /// - /// Gets explicit and default permissions (if requested) permissions for the group for specified entities - /// - /// - /// If true will include the group's default permissions if no permissions are explicitly assigned - /// Array of entity Ids, if empty will return permissions for the group for all entities - EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds); + /// + /// Gets explicit and default permissions (if requested) permissions for the group for specified entities + /// + /// + /// + /// If true will include the group's default permissions if no permissions are + /// explicitly assigned + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds); - /// - /// Replaces the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. - void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds); + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// + /// Specify the nodes to replace permissions for. If nothing is specified all permissions are + /// removed. + /// + void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds); - /// - /// Assigns the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for - void AssignGroupPermission(int groupId, char permission, params int[] entityIds); - } + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for + void AssignGroupPermission(int groupId, char permission, params int[] entityIds); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs index d5bfa0709c..35458d6eba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs @@ -1,104 +1,113 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; +using System.Linq.Expressions; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IUserRepository : IReadWriteQueryRepository { - public interface IUserRepository : IReadWriteQueryRepository - { - /// - /// Gets the count of items based on a complex query - /// - /// - /// - int GetCountByQuery(IQuery? query); + /// + /// Gets the count of items based on a complex query + /// + /// + /// + int GetCountByQuery(IQuery? query); - /// - /// Checks if a user with the username exists - /// - /// - /// - bool ExistsByUserName(string username); + /// + /// Checks if a user with the username exists + /// + /// + /// + bool ExistsByUserName(string username); + /// + /// Checks if a user with the login exists + /// + /// + /// + bool ExistsByLogin(string login); - /// - /// Checks if a user with the login exists - /// - /// - /// - bool ExistsByLogin(string login); + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + IEnumerable GetAllInGroup(int groupId); - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - IEnumerable GetAllInGroup(int groupId); + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + IEnumerable GetAllNotInGroup(int groupId); - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - IEnumerable GetAllNotInGroup(int groupId); + /// + /// Gets paged user results + /// + /// + /// + /// + /// + /// + /// + /// + /// A filter to only include user that belong to these user groups + /// + /// + /// A filter to only include users that do not belong to these user groups + /// + /// Optional parameter to filter by specified user state + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + Expression> orderBy, + Direction orderDirection = Direction.Ascending, + string[]? includeUserGroups = null, + string[]? excludeUserGroups = null, + UserState[]? userState = null, + IQuery? filter = null); - /// - /// Gets paged user results - /// - /// - /// - /// - /// - /// - /// - /// - /// A filter to only include user that belong to these user groups - /// - /// - /// A filter to only include users that do not belong to these user groups - /// - /// Optional parameter to filter by specified user state - /// - /// - IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, - Expression> orderBy, Direction orderDirection = Direction.Ascending, - string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, - IQuery? filter = null); + /// + /// Returns a user by username + /// + /// + /// + /// This is only used for a shim in order to upgrade to 7.7 + /// + /// + /// A non cached instance + /// + IUser? GetByUsername(string username, bool includeSecurityData); - /// - /// Returns a user by username - /// - /// - /// - /// This is only used for a shim in order to upgrade to 7.7 - /// - /// - /// A non cached instance - /// - IUser? GetByUsername(string username, bool includeSecurityData); + /// + /// Returns a user by id + /// + /// + /// + /// This is only used for a shim in order to upgrade to 7.7 + /// + /// + /// A non cached instance + /// + IUser? Get(int? id, bool includeSecurityData); - /// - /// Returns a user by id - /// - /// - /// - /// This is only used for a shim in order to upgrade to 7.7 - /// - /// - /// A non cached instance - /// - IUser? Get(int? id, bool includeSecurityData); + IProfile? GetProfile(string username); - IProfile? GetProfile(string username); - IProfile? GetProfile(int id); - IDictionary GetUserStates(); + IProfile? GetProfile(int id); - Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true); - bool ValidateLoginSession(int userId, Guid sessionId); - int ClearLoginSessions(int userId); - int ClearLoginSessions(TimeSpan timespan); - void ClearLoginSession(Guid sessionId); + IDictionary GetUserStates(); - IEnumerable GetNextUsers(int id, int count); - } + Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true); + + bool ValidateLoginSession(int userId, Guid sessionId); + + int ClearLoginSessions(int userId); + + int ClearLoginSessions(TimeSpan timespan); + + void ClearLoginSession(Guid sessionId); + + IEnumerable GetNextUsers(int id, int count); } diff --git a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs index cd3e31559b..c30015a7a0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs @@ -1,35 +1,33 @@ -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public class InstallationRepository : IInstallationRepository - { - private readonly IJsonSerializer _jsonSerializer; - private static HttpClient? _httpClient; - private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; +namespace Umbraco.Cms.Core.Persistence.Repositories; - public InstallationRepository(IJsonSerializer jsonSerializer) +public class InstallationRepository : IInstallationRepository +{ + private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; + private static HttpClient? _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + public InstallationRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public async Task SaveInstallLogAsync(InstallLog installLog) + { + try { - _jsonSerializer = jsonSerializer; + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } + + var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); + + await _httpClient.PostAsync(RestApiInstallUrl, content); } - public async Task SaveInstallLogAsync(InstallLog installLog) + // this occurs if the server for Our is down or cannot be reached + catch (HttpRequestException) { - try - { - if (_httpClient == null) - _httpClient = new HttpClient(); - - var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); - - await _httpClient.PostAsync(RestApiInstallUrl, content); - } - // this occurs if the server for Our is down or cannot be reached - catch (HttpRequestException) - { } } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs index db0ebd7be5..a6b6c16aa5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs @@ -1,37 +1,31 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Persistence.Repositories +/// +/// Provides cache keys for repositories. +/// +public static class RepositoryCacheKeys { - /// - /// Provides cache keys for repositories. - /// - public static class RepositoryCacheKeys + // used to cache keys so we don't keep allocating strings + private static readonly Dictionary Keys = new(); + + public static string GetKey() { - // used to cache keys so we don't keep allocating strings - private static readonly Dictionary s_keys = new Dictionary(); + Type type = typeof(T); + return Keys.TryGetValue(type, out var key) ? key : Keys[type] = "uRepo_" + type.Name + "_"; + } - public static string GetKey() + public static string GetKey(TId? id) + { + if (EqualityComparer.Default.Equals(id, default)) { - Type type = typeof(T); - return s_keys.TryGetValue(type, out var key) ? key : (s_keys[type] = "uRepo_" + type.Name + "_"); + return string.Empty; } - public static string GetKey(TId? id) + if (typeof(TId).IsValueType) { - if (EqualityComparer.Default.Equals(id, default)) - { - return string.Empty; - } - - if (typeof(TId).IsValueType) - { - return GetKey() + id; - } - else - { - return GetKey() + id?.ToString()?.ToUpperInvariant(); - } + return GetKey() + id; } + + return GetKey() + id?.ToString()?.ToUpperInvariant(); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs index c36156e54b..4d4e642d9d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs @@ -1,59 +1,58 @@ -using System; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public class UpgradeCheckRepository : IUpgradeCheckRepository { - public class UpgradeCheckRepository : IUpgradeCheckRepository + private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; + private static HttpClient? _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + public UpgradeCheckRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public async Task CheckUpgradeAsync(SemVersion version) { - private readonly IJsonSerializer _jsonSerializer; - private static HttpClient? _httpClient; - private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; - - public UpgradeCheckRepository(IJsonSerializer jsonSerializer) + try { - _jsonSerializer = jsonSerializer; + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } + + var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); + + _httpClient.Timeout = TimeSpan.FromSeconds(1); + HttpResponseMessage task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); + var json = await task.Content.ReadAsStringAsync(); + UpgradeResult? result = _jsonSerializer.Deserialize(json); + + return result ?? new UpgradeResult("None", string.Empty, string.Empty); } - - public async Task CheckUpgradeAsync(SemVersion version) + catch (HttpRequestException) { - try - { - if (_httpClient == null) - _httpClient = new HttpClient(); - - var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); - - _httpClient.Timeout = TimeSpan.FromSeconds(1); - var task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl,content); - var json = await task.Content.ReadAsStringAsync(); - var result = _jsonSerializer.Deserialize(json); - - return result ?? new UpgradeResult("None", "", ""); - } - catch (HttpRequestException) - { - // this occurs if the server for Our is down or cannot be reached - return new UpgradeResult("None", "", ""); - } - } - private class CheckUpgradeDto - { - public CheckUpgradeDto(SemVersion version) - { - VersionMajor = version.Major; - VersionMinor = version.Minor; - VersionPatch = version.Patch; - VersionComment = version.Prerelease; - } - - public int VersionMajor { get; } - public int VersionMinor { get; } - public int VersionPatch { get; } - public string VersionComment { get; } + // this occurs if the server for Our is down or cannot be reached + return new UpgradeResult("None", string.Empty, string.Empty); } } + + private class CheckUpgradeDto + { + public CheckUpgradeDto(SemVersion version) + { + VersionMajor = version.Major; + VersionMinor = version.Minor; + VersionPatch = version.Patch; + VersionComment = version.Prerelease; + } + + public int VersionMajor { get; } + + public int VersionMinor { get; } + + public int VersionPatch { get; } + + public string VersionComment { get; } + } } diff --git a/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs b/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs index 8eb27f1a81..20db5106d7 100644 --- a/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs @@ -1,49 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// String extension methods used specifically to translate into SQL +/// +public static class SqlExpressionExtensions { /// - /// String extension methods used specifically to translate into SQL + /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. /// - public static class SqlExpressionExtensions + /// The nullable type. + /// The value to compare. + /// The value to compare to. + /// The value to use when any value is null. + /// Do not use outside of Sql expressions. + // see usage in ExpressionVisitorBase + public static bool SqlNullableEquals(this T? value, T? other, T fallbackValue) + where T : struct => + (value ?? fallbackValue).Equals(other ?? fallbackValue); + + public static bool SqlIn(this IEnumerable collection, T item) => collection.Contains(item); + + public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) { - /// - /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. - /// - /// The nullable type. - /// The value to compare. - /// The value to compare to. - /// The value to use when any value is null. - /// Do not use outside of Sql expressions. - // see usage in ExpressionVisitorBase - public static bool SqlNullableEquals(this T? value, T? other, T fallbackValue) - where T : struct - { - return (value ?? fallbackValue).Equals(other ?? fallbackValue); - } + var wildcardmatch = new Regex("^" + Regex.Escape(txt). - public static bool SqlIn(this IEnumerable collection, T item) => collection.Contains(item); + // deal with any wildcard chars % + Replace(@"\%", ".*") + "$"); - public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) - { - var wildcardmatch = new Regex("^" + Regex.Escape(txt). - //deal with any wildcard chars % - Replace(@"\%", ".*") + "$"); - - return wildcardmatch.IsMatch(str); - } + return wildcardmatch.IsMatch(str); + } #pragma warning disable IDE0060 // Remove unused parameter - public static bool SqlContains(this string str, string txt, TextColumnType columnType) => str.InvariantContains(txt); + public static bool SqlContains(this string str, string txt, TextColumnType columnType) => + str.InvariantContains(txt); - public static bool SqlEquals(this string str, string txt, TextColumnType columnType) => str.InvariantEquals(txt); + public static bool SqlEquals(this string str, string txt, TextColumnType columnType) => str.InvariantEquals(txt); - public static bool SqlStartsWith(this string? str, string txt, TextColumnType columnType) => str?.InvariantStartsWith(txt) ?? false; + public static bool SqlStartsWith(this string? str, string txt, TextColumnType columnType) => + str?.InvariantStartsWith(txt) ?? false; - public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) => str.InvariantEndsWith(txt); + public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) => + str.InvariantEndsWith(txt); #pragma warning restore IDE0060 // Remove unused parameter - } } diff --git a/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs b/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs index d0f32fb971..506e516447 100644 --- a/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs +++ b/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs @@ -1,45 +1,44 @@ -using System; +namespace Umbraco.Cms.Core.Persistence; -namespace Umbraco.Cms.Core.Persistence +/// +/// Provides a mean to express aliases in SELECT Sql statements. +/// +/// +/// +/// First register with using static Umbraco.Core.Persistence.NPocoSqlExtensions.Aliaser, +/// then use eg Sql{Foo}(x => Alias(x.Id, "id")). +/// +/// +public static class SqlExtensionsStatics { /// - /// Provides a mean to express aliases in SELECT Sql statements. + /// Aliases a field. /// - /// - /// First register with using static Umbraco.Core.Persistence.NPocoSqlExtensions.Aliaser, - /// then use eg Sql{Foo}(x => Alias(x.Id, "id")). - /// - public static class SqlExtensionsStatics - { - /// - /// Aliases a field. - /// - /// The field to alias. - /// The alias. - public static object? Alias(object? field, string alias) => field; + /// The field to alias. + /// The alias. + public static object? Alias(object? field, string alias) => field; - /// - /// Produces Sql text. - /// - /// The name of the field. - /// A function producing Sql text. - public static T? SqlText(string field, Func expr) => default; + /// + /// Produces Sql text. + /// + /// The name of the field. + /// A function producing Sql text. + public static T? SqlText(string field, Func expr) => default; - /// - /// Produces Sql text. - /// - /// The name of the first field. - /// The name of the second field. - /// A function producing Sql text. - public static T? SqlText(string field1, string field2, Func expr) => default; + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// A function producing Sql text. + public static T? SqlText(string field1, string field2, Func expr) => default; - /// - /// Produces Sql text. - /// - /// The name of the first field. - /// The name of the second field. - /// The name of the third field. - /// A function producing Sql text. - public static T? SqlText(string field1, string field2, string field3, Func expr) => default; - } + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// The name of the third field. + /// A function producing Sql text. + public static T? SqlText(string field1, string field2, string field3, Func expr) => default; } diff --git a/src/Umbraco.Core/Persistence/TextColumnType.cs b/src/Umbraco.Core/Persistence/TextColumnType.cs index dc0b8d56bd..9e3a4dd71b 100644 --- a/src/Umbraco.Core/Persistence/TextColumnType.cs +++ b/src/Umbraco.Core/Persistence/TextColumnType.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +public enum TextColumnType { - public enum TextColumnType - { - NVarchar, - NText - } + NVarchar, + NText, } diff --git a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs index c799a00df6..ddb1a0f0e8 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs @@ -1,71 +1,68 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// The configuration object for the Block List editor +/// +public class BlockListConfiguration { - /// - /// The configuration object for the Block List editor - /// - public class BlockListConfiguration + [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] + public BlockConfiguration[] Blocks { get; set; } = null!; + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] + public NumberRange ValidationLimit { get; set; } = new(); + + [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")] + public bool UseLiveEditing { get; set; } + + [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] + public bool UseInlineEditingAsDefault { get; set; } + + [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "Optional CSS override, example: 800px or 100%")] + public string? MaxPropertyWidth { get; set; } + + [DataContract] + public class BlockConfiguration { - [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] - public BlockConfiguration[] Blocks { get; set; } = null!; + [DataMember(Name = "backgroundColor")] + public string? BackgroundColor { get; set; } - [DataContract] - public class BlockConfiguration - { + [DataMember(Name = "iconColor")] + public string? IconColor { get; set; } - [DataMember(Name ="backgroundColor")] - public string? BackgroundColor { get; set; } + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } - [DataMember(Name ="iconColor")] - public string? IconColor { get; set; } + [DataMember(Name = "contentElementTypeKey")] + public Guid ContentElementTypeKey { get; set; } - [DataMember(Name ="thumbnail")] - public string? Thumbnail { get; set; } + [DataMember(Name = "settingsElementTypeKey")] + public Guid? SettingsElementTypeKey { get; set; } - [DataMember(Name ="contentElementTypeKey")] - public Guid ContentElementTypeKey { get; set; } + [DataMember(Name = "view")] + public string? View { get; set; } - [DataMember(Name ="settingsElementTypeKey")] - public Guid? SettingsElementTypeKey { get; set; } + [DataMember(Name = "stylesheet")] + public string? Stylesheet { get; set; } - [DataMember(Name ="view")] - public string? View { get; set; } + [DataMember(Name = "label")] + public string? Label { get; set; } - [DataMember(Name ="stylesheet")] - public string? Stylesheet { get; set; } + [DataMember(Name = "editorSize")] + public string? EditorSize { get; set; } - [DataMember(Name ="label")] - public string? Label { get; set; } + [DataMember(Name = "forceHideContentEditorInOverlay")] + public bool ForceHideContentEditorInOverlay { get; set; } + } - [DataMember(Name ="editorSize")] - public string? EditorSize { get; set; } + [DataContract] + public class NumberRange + { + [DataMember(Name = "min")] + public int? Min { get; set; } - [DataMember(Name ="forceHideContentEditorInOverlay")] - public bool ForceHideContentEditorInOverlay { get; set; } - } - - [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] - public NumberRange ValidationLimit { get; set; } = new NumberRange(); - - [DataContract] - public class NumberRange - { - [DataMember(Name ="min")] - public int? Min { get; set; } - - [DataMember(Name ="max")] - public int? Max { get; set; } - } - - [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")] - public bool UseLiveEditing { get; set; } - - [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] - public bool UseInlineEditingAsDefault { get; set; } - - [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] - public string? MaxPropertyWidth { get; set; } + [DataMember(Name = "max")] + public int? Max { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs index 80350bb350..02fc30d68b 100644 --- a/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs @@ -1,11 +1,14 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the color picker value editor. +/// +public class ColorPickerConfiguration : ValueListConfiguration { - /// - /// Represents the configuration for the color picker value editor. - /// - public class ColorPickerConfiguration : ValueListConfiguration - { - [ConfigurationField("useLabel", "Include labels?", "boolean", Description = "Stores colors as a Json object containing both the color hex string and label, rather than just the hex string.")] - public bool UseLabel { get; set; } - } + [ConfigurationField( + "useLabel", + "Include labels?", + "boolean", + Description = "Stores colors as a Json object containing both the color hex string and label, rather than just the hex string.")] + public bool UseLabel { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index 89d19c5115..25aeb93418 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -1,137 +1,149 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Serialization; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data type configuration editor. +/// +[DataContract] +public class ConfigurationEditor : IConfigurationEditor { + private IDictionary _defaultConfiguration; + /// - /// Represents a data type configuration editor. + /// Initializes a new instance of the class. /// - [DataContract] - public class ConfigurationEditor : IConfigurationEditor + public ConfigurationEditor() { - private IDictionary _defaultConfiguration; - - /// - /// Initializes a new instance of the class. - /// - public ConfigurationEditor() - { - Fields = new List(); - _defaultConfiguration = new Dictionary(); - } - - /// - /// Initializes a new instance of the class. - /// - protected ConfigurationEditor(List fields) - { - Fields = fields; - _defaultConfiguration = new Dictionary(); - } - - /// - /// Gets the fields. - /// - [DataMember(Name = "fields")] - public List Fields { get; } - - /// - /// Gets a field by its property name. - /// - /// Can be used in constructors to add infos to a field that has been defined - /// by a property marked with the . - protected ConfigurationField Field(string name) - => Fields.First(x => x.PropertyName == name); - - /// - /// Gets the configuration as a typed object. - /// - public static TConfiguration? ConfigurationAs(object? obj) - { - if (obj == null) return default; - if (obj is TConfiguration configuration) return configuration; - throw new InvalidCastException( - $"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}."); - } - - /// - /// Converts a configuration object into a serialized database value. - /// - public static string? ToDatabase(object? configuration, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - => configuration == null ? null : configurationEditorJsonSerializer.Serialize(configuration); - - /// - [DataMember(Name = "defaultConfig")] - public virtual IDictionary DefaultConfiguration - { - get => _defaultConfiguration; - set => _defaultConfiguration = value; - } - - /// - public virtual object? DefaultConfigurationObject => DefaultConfiguration; - - /// - public virtual bool IsConfiguration(object obj) => obj is IDictionary; - - - /// - public virtual object FromDatabase(string? configurationJson, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - => string.IsNullOrWhiteSpace(configurationJson) - ? new Dictionary() - : configurationEditorJsonSerializer.Deserialize>(configurationJson)!; - - /// - public virtual object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) - { - // by default, return the posted dictionary - // but only keep entries that have a non-null/empty value - // rest will fall back to default during ToConfigurationEditor() - - var keys = editorValues?.Where(x => - x.Value == null || x.Value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) - .Select(x => x.Key).ToList(); - - if (keys is not null) - { - foreach (var key in keys) - { - editorValues?.Remove(key); - } - } - - return editorValues; - } - - /// - public virtual IDictionary ToConfigurationEditor(object? configuration) - { - // editors that do not override ToEditor/FromEditor have their configuration - // as a dictionary of and, by default, we merge their default - // configuration with their current configuration - - if (configuration == null) - configuration = new Dictionary(); - - if (!(configuration is IDictionary c)) - throw new ArgumentException( - $"Expecting a {typeof(Dictionary).Name} instance but got {configuration.GetType().Name}.", - nameof(configuration)); - - // clone the default configuration, and apply the current configuration values - var d = new Dictionary(DefaultConfiguration); - foreach (var (key, value) in c) - d[key] = value; - return d; - } - - /// - public virtual IDictionary ToValueEditor(object? configuration) - => ToConfigurationEditor(configuration); - + Fields = new List(); + _defaultConfiguration = new Dictionary(); } + + /// + /// Initializes a new instance of the class. + /// + protected ConfigurationEditor(List fields) + { + Fields = fields; + _defaultConfiguration = new Dictionary(); + } + + /// + /// Gets the fields. + /// + [DataMember(Name = "fields")] + public List Fields { get; } + + /// + [DataMember(Name = "defaultConfig")] + public virtual IDictionary DefaultConfiguration + { + get => _defaultConfiguration; + set => _defaultConfiguration = value; + } + + /// + public virtual object? DefaultConfigurationObject => DefaultConfiguration; + + /// + /// Converts a configuration object into a serialized database value. + /// + public static string? ToDatabase( + object? configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => configuration == null ? null : configurationEditorJsonSerializer.Serialize(configuration); + + /// + /// Gets the configuration as a typed object. + /// + public static TConfiguration? ConfigurationAs(object? obj) + { + if (obj == null) + { + return default; + } + + if (obj is TConfiguration configuration) + { + return configuration; + } + + throw new InvalidCastException( + $"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}."); + } + + /// + public virtual bool IsConfiguration(object obj) => obj is IDictionary; + + /// + public virtual object FromDatabase( + string? configurationJson, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => string.IsNullOrWhiteSpace(configurationJson) + ? new Dictionary() + : configurationEditorJsonSerializer.Deserialize>(configurationJson)!; + + /// + public virtual object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) + { + // by default, return the posted dictionary + // but only keep entries that have a non-null/empty value + // rest will fall back to default during ToConfigurationEditor() + var keys = editorValues?.Where(x => + x.Value == null || (x.Value is string stringValue && string.IsNullOrWhiteSpace(stringValue))) + .Select(x => x.Key).ToList(); + + if (keys is not null) + { + foreach (var key in keys) + { + editorValues?.Remove(key); + } + } + + return editorValues; + } + + /// + public virtual IDictionary ToConfigurationEditor(object? configuration) + { + // editors that do not override ToEditor/FromEditor have their configuration + // as a dictionary of and, by default, we merge their default + // configuration with their current configuration + if (configuration == null) + { + configuration = new Dictionary(); + } + + if (!(configuration is IDictionary c)) + { + throw new ArgumentException( + $"Expecting a {typeof(Dictionary).Name} instance but got {configuration.GetType().Name}.", + nameof(configuration)); + } + + // clone the default configuration, and apply the current configuration values + var d = new Dictionary(DefaultConfiguration); + foreach ((string key, object value) in c) + { + d[key] = value; + } + + return d; + } + + /// + public virtual IDictionary ToValueEditor(object? configuration) + => ToConfigurationEditor(configuration); + + /// + /// Gets a field by its property name. + /// + /// + /// Can be used in constructors to add infos to a field that has been defined + /// by a property marked with the . + /// + protected ConfigurationField Field(string name) + => Fields.First(x => x.PropertyName == name); } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs index fa2427a048..6d64bc2d19 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; @@ -12,151 +10,178 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data type configuration editor with a typed configuration. +/// +public abstract class ConfigurationEditor : ConfigurationEditor + where TConfiguration : new() { - /// - /// Represents a data type configuration editor with a typed configuration. - /// - public abstract class ConfigurationEditor : ConfigurationEditor - where TConfiguration : new() - { - private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IEditorConfigurationParser _editorConfigurationParser; - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - protected ConfigurationEditor(IIOHelper ioHelper) + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + protected ConfigurationEditor(IIOHelper ioHelper) : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + { + } - /// - /// Initializes a new instance of the class. - /// - protected ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + /// + /// Initializes a new instance of the class. + /// + protected ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(DiscoverFields(ioHelper)) => - _editorConfigurationParser = editorConfigurationParser; + _editorConfigurationParser = editorConfigurationParser; - /// - /// Discovers fields from configuration properties marked with the field attribute. - /// - private static List DiscoverFields(IIOHelper ioHelper) + /// + public override IDictionary DefaultConfiguration => + ToConfigurationEditor(DefaultConfigurationObject); + + /// + public override object DefaultConfigurationObject => new TConfiguration(); + + /// + public override bool IsConfiguration(object obj) + => obj is TConfiguration; + + /// + public override object FromDatabase( + string? configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + { + try { - var fields = new List(); - var properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration)); - - foreach (var property in properties) + if (string.IsNullOrWhiteSpace(configuration)) { - var attribute = property.GetCustomAttribute(false); - if (attribute == null) continue; - - ConfigurationField field; - - var attributeView = ioHelper.ResolveRelativeOrVirtualUrl(attribute.View); - // if the field does not have its own type, use the base type - if (attribute.Type == null) - { - field = new ConfigurationField - { - // if the key is empty then use the property name - Key = string.IsNullOrWhiteSpace(attribute.Key) ? property.Name : attribute.Key, - Name = attribute.Name, - PropertyName = property.Name, - PropertyType = property.PropertyType, - Description = attribute.Description, - HideLabel = attribute.HideLabel, - View = attributeView - }; - - fields.Add(field); - continue; - } - - // if the field has its own type, instantiate it - try - { - field = (ConfigurationField) Activator.CreateInstance(attribute.Type)!; - } - catch (Exception ex) - { - throw new Exception($"Failed to create an instance of type \"{attribute.Type}\" for property \"{property.Name}\" of configuration \"{typeof(TConfiguration).Name}\" (see inner exception).", ex); - } - - // then add it, and overwrite values if they are assigned in the attribute - fields.Add(field); - - field.PropertyName = property.Name; - field.PropertyType = property.PropertyType; - - if (!string.IsNullOrWhiteSpace(attribute.Key)) - field.Key = attribute.Key; - - // if the key is still empty then use the property name - if (string.IsNullOrWhiteSpace(field.Key)) - field.Key = property.Name; - - if (!string.IsNullOrWhiteSpace(attribute.Name)) - field.Name = attribute.Name; - - if (!string.IsNullOrWhiteSpace(attribute.View)) - field.View = attributeView; - - if (!string.IsNullOrWhiteSpace(attribute.Description)) - field.Description = attribute.Description; - - if (attribute.HideLabelSettable.HasValue) - field.HideLabel = attribute.HideLabel; + return new TConfiguration(); } - return fields; + return configurationEditorJsonSerializer.Deserialize(configuration)!; } - - /// - public override IDictionary DefaultConfiguration => ToConfigurationEditor(DefaultConfigurationObject); - - /// - public override object DefaultConfigurationObject => new TConfiguration(); - - /// - public override bool IsConfiguration(object obj) - => obj is TConfiguration; - - /// - public override object FromDatabase(string? configuration, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + catch (Exception e) { + throw new InvalidOperationException( + $"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", + e); + } + } + + /// + public sealed override object? FromConfigurationEditor( + IDictionary? editorValues, + object? configuration) => FromConfigurationEditor(editorValues, (TConfiguration?)configuration); + + /// + /// Converts the configuration posted by the editor. + /// + /// The configuration object posted by the editor. + /// The current configuration object. + public virtual TConfiguration? FromConfigurationEditor( + IDictionary? editorValues, + TConfiguration? configuration) => + _editorConfigurationParser.ParseFromConfigurationEditor(editorValues, Fields); + + /// + public sealed override IDictionary ToConfigurationEditor(object? configuration) => + ToConfigurationEditor((TConfiguration?)configuration); + + /// + /// Converts configuration values to values for the editor. + /// + /// The configuration. + public virtual Dictionary ToConfigurationEditor(TConfiguration? configuration) => + _editorConfigurationParser.ParseToConfigurationEditor(configuration); + + /// + /// Discovers fields from configuration properties marked with the field attribute. + /// + private static List DiscoverFields(IIOHelper ioHelper) + { + var fields = new List(); + PropertyInfo[] properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration)); + + foreach (PropertyInfo property in properties) + { + ConfigurationFieldAttribute? attribute = property.GetCustomAttribute(false); + if (attribute == null) + { + continue; + } + + ConfigurationField field; + + var attributeView = ioHelper.ResolveRelativeOrVirtualUrl(attribute.View); + + // if the field does not have its own type, use the base type + if (attribute.Type == null) + { + field = new ConfigurationField + { + // if the key is empty then use the property name + Key = string.IsNullOrWhiteSpace(attribute.Key) ? property.Name : attribute.Key, + Name = attribute.Name, + PropertyName = property.Name, + PropertyType = property.PropertyType, + Description = attribute.Description, + HideLabel = attribute.HideLabel, + View = attributeView, + }; + + fields.Add(field); + continue; + } + + // if the field has its own type, instantiate it try { - if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration(); - return configurationEditorJsonSerializer.Deserialize(configuration)!; + field = (ConfigurationField)Activator.CreateInstance(attribute.Type)!; } - catch (Exception e) + catch (Exception ex) { - throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e); + throw new Exception( + $"Failed to create an instance of type \"{attribute.Type}\" for property \"{property.Name}\" of configuration \"{typeof(TConfiguration).Name}\" (see inner exception).", + ex); + } + + // then add it, and overwrite values if they are assigned in the attribute + fields.Add(field); + + field.PropertyName = property.Name; + field.PropertyType = property.PropertyType; + + if (!string.IsNullOrWhiteSpace(attribute.Key)) + { + field.Key = attribute.Key; + } + + // if the key is still empty then use the property name + if (string.IsNullOrWhiteSpace(field.Key)) + { + field.Key = property.Name; + } + + if (!string.IsNullOrWhiteSpace(attribute.Name)) + { + field.Name = attribute.Name; + } + + if (!string.IsNullOrWhiteSpace(attribute.View)) + { + field.View = attributeView; + } + + if (!string.IsNullOrWhiteSpace(attribute.Description)) + { + field.Description = attribute.Description; + } + + if (attribute.HideLabelSettable.HasValue) + { + field.HideLabel = attribute.HideLabel; } } - /// - public sealed override object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) - { - return FromConfigurationEditor(editorValues, (TConfiguration?) configuration); - } - - /// - /// Converts the configuration posted by the editor. - /// - /// The configuration object posted by the editor. - /// The current configuration object. - public virtual TConfiguration? FromConfigurationEditor(IDictionary? editorValues, TConfiguration? configuration) => _editorConfigurationParser.ParseFromConfigurationEditor(editorValues, Fields); - - /// - public sealed override IDictionary ToConfigurationEditor(object? configuration) - { - return ToConfigurationEditor((TConfiguration?) configuration); - } - - /// - /// Converts configuration values to values for the editor. - /// - /// The configuration. - public virtual Dictionary ToConfigurationEditor(TConfiguration? configuration) => _editorConfigurationParser.ParseToConfigurationEditor(configuration); + return fields; } } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs index 0e679f9dc1..40bd0c0ca9 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs @@ -1,106 +1,109 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a datatype configuration field for editing. +/// +[DataContract] +public class ConfigurationField { + private readonly string? _view; + /// - /// Represents a datatype configuration field for editing. + /// Initializes a new instance of the class. /// - [DataContract] - public class ConfigurationField + public ConfigurationField() + : this(new List()) { - private string? _view; + } - /// - /// Initializes a new instance of the class. - /// - public ConfigurationField() - : this(new List()) - { } + /// + /// Initializes a new instance of the class. + /// + public ConfigurationField(params IValueValidator[] validators) + : this(validators.ToList()) + { + } - /// - /// Initializes a new instance of the class. - /// - public ConfigurationField(params IValueValidator[] validators) - : this(validators.ToList()) - { } + /// + /// Initializes a new instance of the class. + /// + private ConfigurationField(List validators) + { + Validators = validators; + Config = new Dictionary(); - /// - /// Initializes a new instance of the class. - /// - private ConfigurationField(List validators) + // fill details from attribute, if any + ConfigurationFieldAttribute? attribute = GetType().GetCustomAttribute(false); + if (attribute is null) { - Validators = validators; - Config = new Dictionary(); - - // fill details from attribute, if any - var attribute = GetType().GetCustomAttribute(false); - if (attribute is null) return; - - Name = attribute.Name; - Description = attribute.Description; - HideLabel = attribute.HideLabel; - Key = attribute.Key; - View = attribute.View; + return; } - /// - /// Gets or sets the key of the field. - /// - [DataMember(Name = "key", IsRequired = true)] - public string Key { get; set; } = null!; - - /// - /// Gets or sets the name of the field. - /// - [DataMember(Name = "label", IsRequired = true)] - public string? Name { get; set; } - - /// - /// Gets or sets the property name of the field. - /// - public string? PropertyName { get; set; } - - /// - /// Gets or sets the property CLR type of the field. - /// - public Type? PropertyType { get; set; } - - /// - /// Gets or sets the description of the field. - /// - [DataMember(Name = "description")] - public string? Description { get; set; } - - /// - /// Gets or sets a value indicating whether to hide the label of the field. - /// - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } - - /// - /// Gets or sets the view to used in the editor. - /// - /// - /// Can be the full virtual path, or the relative path to the Umbraco folder, - /// or a simple view name which will map to ~/Views/PreValueEditors/{view}.html. - /// - [DataMember(Name = "view", IsRequired = true)] - public string? View { get; set; } - - /// - /// Gets the validators of the field. - /// - [DataMember(Name = "validation")] - public List Validators { get; } - - /// - /// Gets or sets extra configuration properties for the editor. - /// - [DataMember(Name = "config")] - public IDictionary Config { get; set; } + Name = attribute.Name; + Description = attribute.Description; + HideLabel = attribute.HideLabel; + Key = attribute.Key; + View = attribute.View; } + + /// + /// Gets or sets the key of the field. + /// + [DataMember(Name = "key", IsRequired = true)] + public string Key { get; set; } = null!; + + /// + /// Gets or sets the name of the field. + /// + [DataMember(Name = "label", IsRequired = true)] + public string? Name { get; set; } + + /// + /// Gets or sets the property name of the field. + /// + public string? PropertyName { get; set; } + + /// + /// Gets or sets the property CLR type of the field. + /// + public Type? PropertyType { get; set; } + + /// + /// Gets or sets the description of the field. + /// + [DataMember(Name = "description")] + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether to hide the label of the field. + /// + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } + + /// + /// Gets or sets the view to used in the editor. + /// + /// + /// + /// Can be the full virtual path, or the relative path to the Umbraco folder, + /// or a simple view name which will map to ~/Views/PreValueEditors/{view}.html. + /// + /// + [DataMember(Name = "view", IsRequired = true)] + public string? View { get; set; } + + /// + /// Gets the validators of the field. + /// + [DataMember(Name = "validation")] + public List Validators { get; } + + /// + /// Gets or sets extra configuration properties for the editor. + /// + [DataMember(Name = "config")] + public IDictionary Config { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs index 79e9655e25..c504a790be 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs @@ -1,117 +1,169 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Marks a ConfigurationEditor property as a configuration field, and a class as a configuration field type. +/// +/// Properties marked with this attribute are discovered as fields. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)] +public class ConfigurationFieldAttribute : Attribute { + private Type? _type; + /// - /// Marks a ConfigurationEditor property as a configuration field, and a class as a configuration field type. + /// Initializes a new instance of the class. /// - /// Properties marked with this attribute are discovered as fields. - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)] - public class ConfigurationFieldAttribute : Attribute + public ConfigurationFieldAttribute(Type type) { - private Type? _type; + Type = type; + Key = string.Empty; + } - /// - /// Initializes a new instance of the class. - /// - public ConfigurationFieldAttribute(Type type) + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the field. + /// The friendly name of the field. + /// The view to use to render the field editor. + public ConfigurationFieldAttribute(string key, string name, string view) + { + if (key == null) { - Type = type; - Key = string.Empty; + throw new ArgumentNullException(nameof(key)); } - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the field. - /// The friendly name of the field. - /// The view to use to render the field editor. - public ConfigurationFieldAttribute(string key, string name, string view) + if (string.IsNullOrWhiteSpace(key)) { - if (key == null) throw new ArgumentNullException(nameof(key)); - if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(key)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (view == null) throw new ArgumentNullException(nameof(view)); - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); - - Key = key; - Name = name; - View = view; + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(key)); } - /// - /// Initializes a new instance of the class. - /// - /// The friendly name of the field. - /// The view to use to render the field editor. - /// When no key is specified, the will derive a key - /// from the name of the property marked with this attribute. - public ConfigurationFieldAttribute(string name, string view) + if (name == null) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (view == null) throw new ArgumentNullException(nameof(view)); - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); - - Name = name; - View = view; - Key = string.Empty; + throw new ArgumentNullException(nameof(name)); } - /// - /// Gets or sets the key of the field. - /// - /// When null or empty, the should derive a key - /// from the name of the property marked with this attribute. - public string Key { get; } - - /// - /// Gets the friendly name of the field. - /// - public string? Name { get; } - - /// - /// Gets or sets the view to use to render the field editor. - /// - public string? View { get; } - - /// - /// Gets or sets the description of the field. - /// - public string? Description { get; set; } - - /// - /// Gets or sets a value indicating whether the field editor should be displayed without its label. - /// - public bool HideLabel + if (string.IsNullOrWhiteSpace(name)) { - get => HideLabelSettable.ValueOrDefault(false); - set => HideLabelSettable.Set(value); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Gets the settable underlying . - /// - public Settable HideLabelSettable { get; } = new Settable(); - - /// - /// Gets or sets the type of the field. - /// - /// - /// By default, fields are created as instances, - /// unless specified otherwise through this property. - /// The specified type must inherit from . - /// - public Type? Type + if (view == null) { - get => _type; - set + throw new ArgumentNullException(nameof(view)); + } + + if (string.IsNullOrWhiteSpace(view)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(view)); + } + + Key = key; + Name = name; + View = view; + } + + /// + /// Initializes a new instance of the class. + /// + /// The friendly name of the field. + /// The view to use to render the field editor. + /// + /// When no key is specified, the will derive a key + /// from the name of the property marked with this attribute. + /// + public ConfigurationFieldAttribute(string name, string view) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + if (view == null) + { + throw new ArgumentNullException(nameof(view)); + } + + if (string.IsNullOrWhiteSpace(view)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(view)); + } + + Name = name; + View = view; + Key = string.Empty; + } + + /// + /// Gets or sets the key of the field. + /// + /// + /// When null or empty, the should derive a key + /// from the name of the property marked with this attribute. + /// + public string Key { get; } + + /// + /// Gets the friendly name of the field. + /// + public string? Name { get; } + + /// + /// Gets or sets the view to use to render the field editor. + /// + public string? View { get; } + + /// + /// Gets or sets the description of the field. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether the field editor should be displayed without its label. + /// + public bool HideLabel + { + get => HideLabelSettable.ValueOrDefault(false); + set => HideLabelSettable.Set(value); + } + + /// + /// Gets the settable underlying . + /// + public Settable HideLabelSettable { get; } = new(); + + /// + /// Gets or sets the type of the field. + /// + /// + /// + /// By default, fields are created as instances, + /// unless specified otherwise through this property. + /// + /// The specified type must inherit from . + /// + public Type? Type + { + get => _type; + set + { + if (!typeof(ConfigurationField).IsAssignableFrom(value)) { - if (!typeof(ConfigurationField).IsAssignableFrom(value)) - throw new ArgumentException("Type must inherit from ConfigurationField.", nameof(value)); - _type = value; + throw new ArgumentException("Type must inherit from ConfigurationField.", nameof(value)); } + + _type = value; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs index 555d6f8418..8cbaecdbdb 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs @@ -1,16 +1,17 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ContentPickerConfiguration : IIgnoreUserStartNodesConfig { - public class ContentPickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] - public bool ShowOpenButton { get; set; } + [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] + public bool ShowOpenButton { get; set; } - [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor - public Udi? StartNodeId { get; set; } + [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor + public Udi? StartNodeId { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs index 4932030db2..3bffa4ad61 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs @@ -1,38 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class ContentPickerConfigurationEditor : ConfigurationEditor { - internal class ContentPickerConfigurationEditor : ConfigurationEditor + public ContentPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) => + + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + Field(nameof(ContentPickerConfiguration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; + + public override IDictionary ToValueEditor(object? configuration) { - public ContentPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - // configure fields - // this is not part of ContentPickerConfiguration, - // but is required to configure the UI editor (when editing the configuration) - Field(nameof(ContentPickerConfiguration.StartNodeId)) - .Config = new Dictionary { { "idType", "udi" } }; - } + // get the configuration fields + IDictionary d = base.ToValueEditor(configuration); - public override IDictionary ToValueEditor(object? configuration) - { - // get the configuration fields - var d = base.ToValueEditor(configuration); + // add extra fields + // not part of ContentPickerConfiguration but used to configure the UI editor + d["showEditButton"] = false; + d["showPathOnHover"] = false; + d["idType"] = "udi"; - // add extra fields - // not part of ContentPickerConfiguration but used to configure the UI editor - d["showEditButton"] = false; - d["showPathOnHover"] = false; - d["idType"] = "udi"; - - return d; - } + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs index 5ca0564e69..6cd7645868 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs @@ -1,11 +1,7 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -14,69 +10,73 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Content property editor that stores UDI +/// +[DataEditor( + Constants.PropertyEditors.Aliases.ContentPicker, + EditorType.PropertyValue | EditorType.MacroParameter, + "Content Picker", + "contentpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.Pickers)] +public class ContentPickerPropertyEditor : DataEditor { - /// - /// Content property editor that stores UDI - /// - [DataEditor( - Constants.PropertyEditors.Aliases.ContentPicker, - EditorType.PropertyValue | EditorType.MacroParameter, - "Content Picker", - "contentpicker", - ValueType = ValueTypes.String, - Group = Constants.PropertyEditors.Groups.Pickers)] - public class ContentPickerPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public ContentPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public ContentPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + public ContentPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + SupportsReadOnly = true; + } - public ContentPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, + protected override IConfigurationEditor CreateConfigurationEditor() => + new ContentPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public ContentPickerPropertyValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) + DataEditorAttribute attribute) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; } - protected override IConfigurationEditor CreateConfigurationEditor() + public IEnumerable GetReferences(object? value) { - return new ContentPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); - } + var asString = value is string str ? str : value?.ToString(); - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - - internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference - { - public ContentPickerPropertyValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + if (string.IsNullOrEmpty(asString)) { + yield break; } - public IEnumerable GetReferences(object? value) + if (UdiParser.TryParse(asString, out Udi? udi)) { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (UdiParser.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); + yield return new UmbracoEntityReference(udi); } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 5619a1bb87..b2b95f475b 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -1,201 +1,223 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.Serialization; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data editor. +/// +/// +/// +/// Editors can be deserialized from e.g. manifests, which is. why the class is not abstract, +/// the json serialization attributes are required, and the properties have an internal setter. +/// +/// +[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "(),nq}")] +[HideFromTypeFinder] +[DataContract] +public class DataEditor : IDataEditor { + private IDictionary? _defaultConfiguration; + /// - /// Represents a data editor. + /// Initializes a new instance of the class. /// - /// - /// Editors can be deserialized from e.g. manifests, which is. why the class is not abstract, - /// the json serialization attributes are required, and the properties have an internal setter. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "(),nq}")] - [HideFromTypeFinder] - [DataContract] - public class DataEditor : IDataEditor + public DataEditor(IDataValueEditorFactory dataValueEditorFactory, EditorType type = EditorType.PropertyValue) { - private IDictionary? _defaultConfiguration; + // defaults + DataValueEditorFactory = dataValueEditorFactory; + Type = type; + Icon = Constants.Icons.PropertyEditor; + Group = Constants.PropertyEditors.Groups.Common; - /// - /// Initializes a new instance of the class. - /// - public DataEditor(IDataValueEditorFactory dataValueEditorFactory, EditorType type = EditorType.PropertyValue) + // assign properties based on the attribute, if it is found + Attribute = GetType().GetCustomAttribute(false); + if (Attribute == null) { - - // defaults - DataValueEditorFactory = dataValueEditorFactory; - Type = type; - Icon = Constants.Icons.PropertyEditor; - Group = Constants.PropertyEditors.Groups.Common; - - // assign properties based on the attribute, if it is found - Attribute = GetType().GetCustomAttribute(false); - if (Attribute == null) - { - Alias = string.Empty; - Name = string.Empty; - return; - } - - Alias = Attribute.Alias; - Type = Attribute.Type; - Name = Attribute.Name; - Icon = Attribute.Icon; - Group = Attribute.Group; - IsDeprecated = Attribute.IsDeprecated; + Alias = string.Empty; + Name = string.Empty; + return; } - /// - /// Gets the editor attribute. - /// - protected DataEditorAttribute? Attribute { get; } - - /// - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } - - protected IDataValueEditorFactory DataValueEditorFactory { get; } - - /// - [IgnoreDataMember] - public EditorType Type { get; } - - /// - [DataMember(Name = "name", IsRequired = true)] - public string Name { get; internal set; } - - /// - [DataMember(Name = "icon")] - public string Icon { get; internal set; } - - /// - [DataMember(Name = "group")] - public string Group { get; internal set; } - - /// - [IgnoreDataMember] - public bool IsDeprecated { get; } - - /// - /// - /// If an explicit value editor has been assigned, then this explicit - /// instance is returned. Otherwise, a new instance is created by CreateValueEditor. - /// The instance created by CreateValueEditor is not cached, i.e. - /// a new instance is created each time the property value is retrieved. The - /// property editor is a singleton, and the value editor cannot be a singleton - /// since it depends on the datatype configuration. - /// Technically, it could be cached by datatype but let's keep things - /// simple enough for now. - /// - // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); - - /// - /// - /// If an explicit value editor has been assigned, then this explicit - /// instance is returned. Otherwise, a new instance is created by CreateValueEditor, - /// and configured with the configuration. - /// The instance created by CreateValueEditor is not cached, i.e. - /// a new instance is created each time the property value is retrieved. The - /// property editor is a singleton, and the value editor cannot be a singleton - /// since it depends on the datatype configuration. - /// Technically, it could be cached by datatype but let's keep things - /// simple enough for now. - /// - public virtual IDataValueEditor GetValueEditor(object? configuration) - { - // if an explicit value editor has been set (by the manifest parser) - // then return it, and ignore the configuration, which is going to be - // empty anyways - if (ExplicitValueEditor != null) - return ExplicitValueEditor; - - var editor = CreateValueEditor(); - if (configuration is not null) - { - ((DataValueEditor)editor).Configuration = configuration; // TODO: casting is bad - } - - return editor; - } - - /// - /// Gets or sets an explicit value editor. - /// - /// Used for manifest data editors. - [DataMember(Name = "editor")] - public IDataValueEditor? ExplicitValueEditor { get; set; } - - /// - /// - /// If an explicit configuration editor has been assigned, then this explicit - /// instance is returned. Otherwise, a new instance is created by CreateConfigurationEditor. - /// The instance created by CreateConfigurationEditor is not cached, i.e. - /// a new instance is created each time. The property editor is a singleton, and although the - /// configuration editor could technically be a singleton too, we'd rather not keep configuration editor - /// cached. - /// - public IConfigurationEditor GetConfigurationEditor() => ExplicitConfigurationEditor ?? CreateConfigurationEditor(); - - /// - /// Gets or sets an explicit configuration editor. - /// - /// Used for manifest data editors. - [DataMember(Name = "config")] - public IConfigurationEditor? ExplicitConfigurationEditor { get; set; } - - /// - [DataMember(Name = "defaultConfig")] - public IDictionary DefaultConfiguration - { - // for property value editors, get the ConfigurationEditor.DefaultConfiguration - // else fallback to a default, empty dictionary - - get => _defaultConfiguration ?? ((Type & EditorType.PropertyValue) > 0 ? GetConfigurationEditor().DefaultConfiguration : (_defaultConfiguration = new Dictionary())); - set => _defaultConfiguration = value; - } - - /// - public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); - - /// - /// Creates a value editor instance. - /// - /// - protected virtual IDataValueEditor CreateValueEditor() - { - if (Attribute == null) - throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); - - return DataValueEditorFactory.Create(Attribute); - } - - /// - /// Creates a configuration editor instance. - /// - protected virtual IConfigurationEditor CreateConfigurationEditor() - { - var editor = new ConfigurationEditor(); - // pass the default configuration if this is not a property value editor - if ((Type & EditorType.PropertyValue) == 0 && _defaultConfiguration is not null) - { - editor.DefaultConfiguration = _defaultConfiguration; - } - return editor; - } - - /// - /// Provides a summary of the PropertyEditor for use with the . - /// - protected virtual string DebuggerDisplay() - { - return $"Name: {Name}, Alias: {Alias}"; - } + Alias = Attribute.Alias; + Type = Attribute.Type; + Name = Attribute.Name; + Icon = Attribute.Icon; + Group = Attribute.Group; + IsDeprecated = Attribute.IsDeprecated; } + + /// + /// Gets or sets an explicit value editor. + /// + /// Used for manifest data editors. + [DataMember(Name = "editor")] + public IDataValueEditor? ExplicitValueEditor { get; set; } + + /// + /// Gets the editor attribute. + /// + protected DataEditorAttribute? Attribute { get; } + + protected IDataValueEditorFactory DataValueEditorFactory { get; } + + /// + /// Gets or sets an explicit configuration editor. + /// + /// Used for manifest data editors. + [DataMember(Name = "config")] + public IConfigurationEditor? ExplicitConfigurationEditor { get; set; } + + /// + [DataMember(Name = "alias", IsRequired = true)] + public string Alias { get; set; } + + /// + [DataMember(Name = "supportsReadOnly", IsRequired = true)] + public bool SupportsReadOnly { get; set; } + + /// + [IgnoreDataMember] + public EditorType Type { get; } + + /// + [DataMember(Name = "name", IsRequired = true)] + public string Name { get; internal set; } + + /// + [DataMember(Name = "icon")] + public string Icon { get; internal set; } + + /// + [DataMember(Name = "group")] + public string Group { get; internal set; } + + /// + [IgnoreDataMember] + public bool IsDeprecated { get; } + + /// + [DataMember(Name = "defaultConfig")] + public IDictionary DefaultConfiguration + { + // for property value editors, get the ConfigurationEditor.DefaultConfiguration + // else fallback to a default, empty dictionary + get => _defaultConfiguration ?? ((Type & EditorType.PropertyValue) > 0 + ? GetConfigurationEditor().DefaultConfiguration + : _defaultConfiguration = new Dictionary()); + set => _defaultConfiguration = value; + } + + /// + /// + /// + /// If an explicit value editor has been assigned, then this explicit + /// instance is returned. Otherwise, a new instance is created by CreateValueEditor. + /// + /// + /// The instance created by CreateValueEditor is not cached, i.e. + /// a new instance is created each time the property value is retrieved. The + /// property editor is a singleton, and the value editor cannot be a singleton + /// since it depends on the datatype configuration. + /// + /// + /// Technically, it could be cached by datatype but let's keep things + /// simple enough for now. + /// + /// + // TODO: point of that one? shouldn't we always configure? + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); + + /// + /// + /// + /// If an explicit value editor has been assigned, then this explicit + /// instance is returned. Otherwise, a new instance is created by CreateValueEditor, + /// and configured with the configuration. + /// + /// + /// The instance created by CreateValueEditor is not cached, i.e. + /// a new instance is created each time the property value is retrieved. The + /// property editor is a singleton, and the value editor cannot be a singleton + /// since it depends on the datatype configuration. + /// + /// + /// Technically, it could be cached by datatype but let's keep things + /// simple enough for now. + /// + /// + public virtual IDataValueEditor GetValueEditor(object? configuration) + { + // if an explicit value editor has been set (by the manifest parser) + // then return it, and ignore the configuration, which is going to be + // empty anyways + if (ExplicitValueEditor != null) + { + return ExplicitValueEditor; + } + + IDataValueEditor editor = CreateValueEditor(); + if (configuration is not null) + { + ((DataValueEditor)editor).Configuration = configuration; // TODO: casting is bad + } + + return editor; + } + + /// + /// + /// + /// If an explicit configuration editor has been assigned, then this explicit + /// instance is returned. Otherwise, a new instance is created by CreateConfigurationEditor. + /// + /// + /// The instance created by CreateConfigurationEditor is not cached, i.e. + /// a new instance is created each time. The property editor is a singleton, and although the + /// configuration editor could technically be a singleton too, we'd rather not keep configuration editor + /// cached. + /// + /// + public IConfigurationEditor GetConfigurationEditor() => ExplicitConfigurationEditor ?? CreateConfigurationEditor(); + + /// + public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); + + /// + /// Creates a value editor instance. + /// + /// + protected virtual IDataValueEditor CreateValueEditor() + { + if (Attribute == null) + { + throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); + } + + return DataValueEditorFactory.Create(Attribute); + } + + /// + /// Creates a configuration editor instance. + /// + protected virtual IConfigurationEditor CreateConfigurationEditor() + { + var editor = new ConfigurationEditor(); + + // pass the default configuration if this is not a property value editor + if ((Type & EditorType.PropertyValue) == 0 && _defaultConfiguration is not null) + { + editor.DefaultConfiguration = _defaultConfiguration; + } + + return editor; + } + + /// + /// Provides a summary of the PropertyEditor for use with the . + /// + protected virtual string DebuggerDisplay() => $"Name: {Name}, Alias: {Alias}"; } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index d99acb4781..ce15c66a80 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -1,134 +1,181 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Marks a class that represents a data editor. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class DataEditorAttribute : Attribute { + /// + /// Gets a special value indicating that the view should be null. + /// + public const string + NullView = "EXPLICITELY-SET-VIEW-TO-NULL-2B5B0B73D3DD47B28DDB84E02C349DFB"; // just a random string + + private string _valueType = ValueTypes.String; /// - /// Marks a class that represents a data editor. + /// Initializes a new instance of the class for a property editor. /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class DataEditorAttribute : Attribute + /// The unique identifier of the editor. + /// The friendly name of the editor. + public DataEditorAttribute(string alias, string name) + : this(alias, EditorType.PropertyValue, name, NullView) { - private string _valueType = ValueTypes.String; - - /// - /// Initializes a new instance of the class for a property editor. - /// - /// The unique identifier of the editor. - /// The friendly name of the editor. - public DataEditorAttribute(string alias, string name) - : this(alias, EditorType.PropertyValue, name, NullView) - { } - - /// - /// Initializes a new instance of the class for a property editor. - /// - /// The unique identifier of the editor. - /// The friendly name of the editor. - /// The view to use to render the editor. - public DataEditorAttribute(string alias, string name, string view) - : this(alias, EditorType.PropertyValue, name, view) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the editor. - /// The type of the editor. - /// The friendly name of the editor. - public DataEditorAttribute(string alias, EditorType type, string name) - : this(alias, type, name, NullView) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the editor. - /// The type of the editor. - /// The friendly name of the editor. - /// The view to use to render the editor. - /// - /// Set to to explicitly set the view to null. - /// Otherwise, cannot be null nor empty. - /// - public DataEditorAttribute(string alias, EditorType type, string name, string view) - { - if (alias == null) throw new ArgumentNullException(nameof(alias)); - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); - if ((type & ~(EditorType.PropertyValue | EditorType.MacroParameter)) > 0) throw new ArgumentOutOfRangeException(nameof(type), type, $"Not a valid {typeof(EditorType)} value."); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - if (view == null) throw new ArgumentNullException(nameof(view)); - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); - - Type = type; - Alias = alias; - Name = name; - View = view == NullView ? null : view; - } - - /// - /// Gets a special value indicating that the view should be null. - /// - public const string NullView = "EXPLICITELY-SET-VIEW-TO-NULL-2B5B0B73D3DD47B28DDB84E02C349DFB"; // just a random string - - /// - /// Gets the unique alias of the editor. - /// - public string Alias { get; } - - /// - /// Gets the type of the editor. - /// - public EditorType Type { get; } - - /// - /// Gets the friendly name of the editor. - /// - public string Name { get; } - - /// - /// Gets the view to use to render the editor. - /// - public string? View { get; } - - /// - /// Gets or sets the type of the edited value. - /// - /// Must be a valid value. - public string ValueType { - get => _valueType; - set - { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(value)); - if (!ValueTypes.IsValue(value)) throw new ArgumentOutOfRangeException(nameof(value), value, $"Not a valid {typeof(ValueTypes)} value."); - - _valueType = value; - } - } - - /// - /// Gets or sets a value indicating whether the editor should be displayed without its label. - /// - public bool HideLabel { get; set; } - - /// - /// Gets or sets an optional icon. - /// - /// The icon can be used for example when presenting datatypes based upon the editor. - public string Icon { get; set; } = Constants.Icons.PropertyEditor; - - /// - /// Gets or sets an optional group. - /// - /// The group can be used for example to group the editors by category. - public string Group { get; set; } = Constants.PropertyEditors.Groups.Common; - - /// - /// Gets or sets a value indicating whether the value editor is deprecated. - /// - /// A deprecated editor is still supported but not proposed in the UI. - public bool IsDeprecated { get; set; } } + + /// + /// Initializes a new instance of the class for a property editor. + /// + /// The unique identifier of the editor. + /// The friendly name of the editor. + /// The view to use to render the editor. + public DataEditorAttribute(string alias, string name, string view) + : this(alias, EditorType.PropertyValue, name, view) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the editor. + /// The type of the editor. + /// The friendly name of the editor. + public DataEditorAttribute(string alias, EditorType type, string name) + : this(alias, type, name, NullView) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the editor. + /// The type of the editor. + /// The friendly name of the editor. + /// The view to use to render the editor. + /// + /// Set to to explicitly set the view to null. + /// Otherwise, cannot be null nor empty. + /// + public DataEditorAttribute(string alias, EditorType type, string name, string view) + { + if (alias == null) + { + throw new ArgumentNullException(nameof(alias)); + } + + if (string.IsNullOrWhiteSpace(alias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(alias)); + } + + if ((type & ~(EditorType.PropertyValue | EditorType.MacroParameter)) > 0) + { + throw new ArgumentOutOfRangeException(nameof(type), type, $"Not a valid {typeof(EditorType)} value."); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + if (view == null) + { + throw new ArgumentNullException(nameof(view)); + } + + if (string.IsNullOrWhiteSpace(view)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(view)); + } + + Type = type; + Alias = alias; + Name = name; + View = view == NullView ? null : view; + } + + /// + /// Gets the unique alias of the editor. + /// + public string Alias { get; } + + /// + /// Gets the type of the editor. + /// + public EditorType Type { get; } + + /// + /// Gets the friendly name of the editor. + /// + public string Name { get; } + + /// + /// Gets the view to use to render the editor. + /// + public string? View { get; } + + /// + /// Gets or sets the type of the edited value. + /// + /// Must be a valid value. + public string ValueType + { + get => _valueType; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(value)); + } + + if (!ValueTypes.IsValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value), value, $"Not a valid {typeof(ValueTypes)} value."); + } + + _valueType = value; + } + } + + /// + /// Gets or sets a value indicating whether the editor should be displayed without its label. + /// + public bool HideLabel { get; set; } + + /// + /// Gets or sets an optional icon. + /// + /// The icon can be used for example when presenting datatypes based upon the editor. + public string Icon { get; set; } = Constants.Icons.PropertyEditor; + + /// + /// Gets or sets an optional group. + /// + /// The group can be used for example to group the editors by category. + public string Group { get; set; } = Constants.PropertyEditors.Groups.Common; + + /// + /// Gets or sets a value indicating whether the value editor is deprecated. + /// + /// A deprecated editor is still supported but not proposed in the UI. + public bool IsDeprecated { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs index 0c4ca93fc1..40daf7ec7c 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataEditorCollection : BuilderCollectionBase { - public class DataEditorCollection : BuilderCollectionBase + public DataEditorCollection(Func> items) + : base(items) { - public DataEditorCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs index 4794d37c21..36e70f2738 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs @@ -1,9 +1,9 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class + DataEditorCollectionBuilder : LazyCollectionBuilderBase { - public class DataEditorCollectionBuilder : LazyCollectionBuilderBase - { - protected override DataEditorCollectionBuilder This => this; - } + protected override DataEditorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index b8e3e597a4..41389a165d 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.Linq; using System.Runtime.Serialization; using System.Xml.Linq; using Microsoft.Extensions.Logging; @@ -15,385 +12,437 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a value editor. +/// +[DataContract] +public class DataValueEditor : IDataValueEditor { + private readonly IJsonSerializer? _jsonSerializer; + private readonly ILocalizedTextService _localizedTextService; + private readonly IShortStringHelper _shortStringHelper; + /// - /// Represents a value editor. + /// Initializes a new instance of the class. /// - [DataContract] - public class DataValueEditor : IDataValueEditor + public DataValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer? jsonSerializer) // for tests, and manifest { - private readonly ILocalizedTextService _localizedTextService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IJsonSerializer? _jsonSerializer; + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + ValueType = ValueTypes.String; + Validators = new List(); + } - /// - /// Initializes a new instance of the class. - /// - public DataValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer? jsonSerializer) // for tests, and manifest + /// + /// Initializes a new instance of the class. + /// + public DataValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + { + if (attribute == null) { - _localizedTextService = localizedTextService; - _shortStringHelper = shortStringHelper; - _jsonSerializer = jsonSerializer; - ValueType = ValueTypes.String; - Validators = new List(); + throw new ArgumentNullException(nameof(attribute)); } - /// - /// Initializes a new instance of the class. - /// - public DataValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + + var view = attribute.View; + if (string.IsNullOrWhiteSpace(view)) { - if (attribute == null) throw new ArgumentNullException(nameof(attribute)); - _localizedTextService = localizedTextService; - _shortStringHelper = shortStringHelper; - _jsonSerializer = jsonSerializer; - - var view = attribute.View; - if (string.IsNullOrWhiteSpace(view)) - throw new ArgumentException("The attribute does not specify a view.", nameof(attribute)); - - if (view.StartsWith("~/")) - { - view = ioHelper.ResolveRelativeOrVirtualUrl(view); - } - - View = view; - ValueType = attribute.ValueType; - HideLabel = attribute.HideLabel; + throw new ArgumentException("The attribute does not specify a view.", nameof(attribute)); } - /// - /// Gets or sets the value editor configuration. - /// - public virtual object? Configuration { get; set; } - - /// - /// Gets or sets the editor view. - /// - /// - /// The view can be three things: (1) the full virtual path, or (2) the relative path to the current Umbraco - /// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html. - /// - [Required] - [DataMember(Name = "view")] - public string? View { get; set; } - - /// - /// The value type which reflects how it is validated and stored in the database - /// - [DataMember(Name = "valueType")] - public string ValueType { get; set; } - - /// - public IEnumerable Validate(object? value, bool required, string? format) + if (view.StartsWith("~/")) { - List? results = null; - var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList(); - if (r.Any()) { results = r; } - - // mandatory and regex validators cannot be part of valueEditor.Validators because they - // depend on values that are not part of the configuration, .Mandatory and .ValidationRegEx, - // so they have to be explicitly invoked here. - - if (required) - { - r = RequiredValidator.ValidateRequired(value, ValueType).ToList(); - if (r.Any()) { if (results == null) results = r; else results.AddRange(r); } - } - - var stringValue = value?.ToString(); - if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue)) - { - r = FormatValidator.ValidateFormat(value, ValueType, format).ToList(); - if (r.Any()) { if (results == null) results = r; else results.AddRange(r); } - } - - return results ?? Enumerable.Empty(); + view = ioHelper.ResolveRelativeOrVirtualUrl(view); } - /// - /// A collection of validators for the pre value editor - /// - [DataMember(Name = "validation")] - public List Validators { get; private set; } = new List(); + View = view; + ValueType = attribute.ValueType; + HideLabel = attribute.HideLabel; + } - /// - /// Gets the validator used to validate the special property type -level "required". - /// - public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService); + /// + /// Gets or sets the value editor configuration. + /// + public virtual object? Configuration { get; set; } - /// - /// Gets the validator used to validate the special property type -level "format". - /// - public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService); + public bool SupportsReadOnly { get; set; } - /// - /// If this is true than the editor will be displayed full width without a label - /// - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } + /// + /// Gets the validator used to validate the special property type -level "required". + /// + public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService); - /// - /// Set this to true if the property editor is for display purposes only - /// - public virtual bool IsReadOnly => false; + /// + /// Gets the validator used to validate the special property type -level "format". + /// + public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService); - /// - /// Used to try to convert the string value to the correct CLR type based on the specified for this value editor. - /// - /// The value. - /// - /// The result of the conversion attempt. - /// - /// ValueType was out of range. - internal Attempt TryConvertValueToCrlType(object? value) + /// + /// Gets or sets the editor view. + /// + /// + /// + /// The view can be three things: (1) the full virtual path, or (2) the relative path to the current Umbraco + /// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html. + /// + /// + [Required] + [DataMember(Name = "view")] + public string? View { get; set; } + + /// + /// The value type which reflects how it is validated and stored in the database + /// + [DataMember(Name = "valueType")] + public string ValueType { get; set; } + + /// + /// A collection of validators for the pre value editor + /// + [DataMember(Name = "validation")] + public List Validators { get; private set; } = new(); + + /// + public IEnumerable Validate(object? value, bool required, string? format) + { + List? results = null; + var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList(); + if (r.Any()) { - // Ensure empty string and JSON values are converted to null - if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) + results = r; + } + + // mandatory and regex validators cannot be part of valueEditor.Validators because they + // depend on values that are not part of the configuration, .Mandatory and .ValidationRegEx, + // so they have to be explicitly invoked here. + if (required) + { + r = RequiredValidator.ValidateRequired(value, ValueType).ToList(); + if (r.Any()) { - value = null; - } - else if (value is not string && ValueType.InvariantEquals(ValueTypes.Json)) - { - // Only serialize value when it's not already a string - var jsonValue = _jsonSerializer?.Serialize(value); - if (jsonValue?.DetectIsEmptyJson() ?? false) + if (results == null) { - value = null; + results = r; } else { - value = jsonValue; + results.AddRange(r); } } - - // Convert the string to a known type - Type valueType; - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Ntext: - case ValueStorageType.Nvarchar: - valueType = typeof(string); - break; - - case ValueStorageType.Integer: - // Ensure these are nullable so we can return a null if required - // NOTE: This is allowing type of 'long' because I think JSON.NEt will deserialize a numerical value as long instead of int - // Even though our DB will not support this (will get truncated), we'll at least parse to this - valueType = typeof(long?); - - // If parsing is successful, we need to return as an int, we're only dealing with long's here because of JSON.NET, - // we actually don't support long values and if we return a long value, it will get set as a 'long' on the Property.Value (object) and then - // when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. - var result = value.TryConvertTo(valueType); - - return result.Success && result.Result != null - ? Attempt.Succeed((int)(long)result.Result) - : result; - - case ValueStorageType.Decimal: - // Ensure these are nullable so we can return a null if required - valueType = typeof(decimal?); - break; - - case ValueStorageType.Date: - // Ensure these are nullable so we can return a null if required - valueType = typeof(DateTime?); - break; - - default: - throw new ArgumentOutOfRangeException("ValueType was out of range."); - } - - return value.TryConvertTo(valueType); } - /// - /// A method to deserialize the string value that has been saved in the content editor to an object to be stored in the database. - /// - /// The value returned by the editor. - /// The current value that has been persisted to the database for this editor. This value may be useful for how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used. - /// The value that gets persisted to the database. - /// - /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. - /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the - /// value to the DB will fail when it tries to validate the value type. - /// - public virtual object? FromEditor(ContentPropertyData editorValue, object? currentValue) + var stringValue = value?.ToString(); + if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue)) { - var result = TryConvertValueToCrlType(editorValue.Value); - if (result.Success == false) + r = FormatValidator.ValidateFormat(value, ValueType, format).ToList(); + if (r.Any()) { - StaticApplicationLogging.Logger.LogWarning("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); - return null; + if (results == null) + { + results = r; + } + else + { + results.AddRange(r); + } } - - return result.Result; } - /// - /// A method used to format the database value to a value that can be used by the editor. - /// - /// The property. - /// The culture. - /// The segment. - /// - /// ValueType was out of range. - /// - /// The object returned will automatically be serialized into JSON notation. For most property editors - /// the value returned is probably just a string, but in some cases a JSON structure will be returned. - /// - public virtual object? ToEditor(IProperty property, string? culture = null, string? segment = null) - { - var value = property.GetValue(culture, segment); - if (value == null) - { - return string.Empty; - } + return results ?? Enumerable.Empty(); + } - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Ntext: - case ValueStorageType.Nvarchar: - // If it is a string type, we will attempt to see if it is JSON stored data, if it is we'll try to convert - // to a real JSON object so we can pass the true JSON object directly to Angular! - var stringValue = value as string ?? value.ToString(); - if (stringValue!.DetectIsJson()) + /// + /// If this is true than the editor will be displayed full width without a label + /// + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } + + /// + /// Set this to true if the property editor is for display purposes only + /// + public virtual bool IsReadOnly => false; + + /// + /// A method to deserialize the string value that has been saved in the content editor to an object to be stored in the + /// database. + /// + /// The value returned by the editor. + /// + /// The current value that has been persisted to the database for this editor. This value may be + /// useful for how the value then get's deserialized again to be re-persisted. In most cases it will probably not be + /// used. + /// + /// The value that gets persisted to the database. + /// + /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. + /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the + /// value to the DB will fail when it tries to validate the value type. + /// + public virtual object? FromEditor(ContentPropertyData editorValue, object? currentValue) + { + Attempt result = TryConvertValueToCrlType(editorValue.Value); + if (result.Success == false) + { + StaticApplicationLogging.Logger.LogWarning( + "The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); + return null; + } + + return result.Result; + } + + /// + /// A method used to format the database value to a value that can be used by the editor. + /// + /// The property. + /// The culture. + /// The segment. + /// + /// ValueType was out of range. + /// + /// The object returned will automatically be serialized into JSON notation. For most property editors + /// the value returned is probably just a string, but in some cases a JSON structure will be returned. + /// + public virtual object? ToEditor(IProperty property, string? culture = null, string? segment = null) + { + var value = property.GetValue(culture, segment); + if (value == null) + { + return string.Empty; + } + + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Ntext: + case ValueStorageType.Nvarchar: + // If it is a string type, we will attempt to see if it is JSON stored data, if it is we'll try to convert + // to a real JSON object so we can pass the true JSON object directly to Angular! + var stringValue = value as string ?? value.ToString(); + if (stringValue!.DetectIsJson()) + { + try { - try - { - var json = _jsonSerializer?.Deserialize(stringValue!); - return json; - } - catch - { - // Swallow this exception, we thought it was JSON but it really isn't so continue returning a string - } + dynamic? json = _jsonSerializer?.Deserialize(stringValue!); + return json; } - - return stringValue; - - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - // Decimals need to be formatted with invariant culture (dots, not commas) - // Anything else falls back to ToString() - var decimalValue = value.TryConvertTo(); - - return decimalValue.Success - ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) - : value.ToString(); - - case ValueStorageType.Date: - var dateValue = value.TryConvertTo(); - if (dateValue.Success == false || dateValue.Result == null) + catch { - return string.Empty; + // Swallow this exception, we thought it was JSON but it really isn't so continue returning a string } + } - // Dates will be formatted as yyyy-MM-dd HH:mm:ss - return dateValue.Result.Value.ToIsoString(); + return stringValue; - default: - throw new ArgumentOutOfRangeException("ValueType was out of range."); - } - } + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + // Decimals need to be formatted with invariant culture (dots, not commas) + // Anything else falls back to ToString() + Attempt decimalValue = value.TryConvertTo(); - // TODO: the methods below should be replaced by proper property value convert ToXPath usage! + return decimalValue.Success + ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) + : value.ToString(); - /// - /// Converts a property to Xml fragments. - /// - public IEnumerable ConvertDbToXml(IProperty property, bool published) - { - published &= property.PropertyType.SupportsPublishing; + case ValueStorageType.Date: + Attempt dateValue = value.TryConvertTo(); + if (dateValue.Success == false || dateValue.Result == null) + { + return string.Empty; + } - var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper); + // Dates will be formatted as yyyy-MM-dd HH:mm:ss + return dateValue.Result.Value.ToIsoString(); - foreach (var pvalue in property.Values) - { - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - if (value == null || value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) - continue; - - var xElement = new XElement(nodeName); - if (pvalue.Culture != null) - xElement.Add(new XAttribute("lang", pvalue.Culture)); - if (pvalue.Segment != null) - xElement.Add(new XAttribute("segment", pvalue.Segment)); - - var xValue = ConvertDbToXml(property.PropertyType, value); - xElement.Add(xValue); - - yield return xElement; - } - } - - /// - /// Converts a property value to an Xml fragment. - /// - /// - /// By default, this returns the value of ConvertDbToString but ensures that if the db value type is - /// NVarchar or NText, the value is returned as a CDATA fragment - else it's a Text fragment. - /// Returns an XText or XCData instance which must be wrapped in a element. - /// If the value is empty we will not return as CDATA since that will just take up more space in the file. - /// - public XNode ConvertDbToXml(IPropertyType propertyType, object? value) - { - //check for null or empty value, we don't want to return CDATA if that is the case - if (value == null || value.ToString().IsNullOrWhiteSpace()) - { - return new XText(ConvertDbToString(propertyType, value)); - } - - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Date: - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - return new XText(ConvertDbToString(propertyType, value)); - case ValueStorageType.Nvarchar: - case ValueStorageType.Ntext: - //put text in cdata - return new XCData(ConvertDbToString(propertyType, value)); - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Converts a property value to a string. - /// - public virtual string ConvertDbToString(IPropertyType propertyType, object? value) - { - if (value == null) - return string.Empty; - - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Nvarchar: - case ValueStorageType.Ntext: - return value.ToXmlString(); - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - return value.ToXmlString(value.GetType()); - case ValueStorageType.Date: - //treat dates differently, output the format as xml format - var date = value.TryConvertTo(); - if (date.Success == false || date.Result == null) - return string.Empty; - return date.Result.ToXmlString(); - default: - throw new ArgumentOutOfRangeException(); - } + default: + throw new ArgumentOutOfRangeException("ValueType was out of range."); } } + + // TODO: the methods below should be replaced by proper property value convert ToXPath usage! + + /// + /// Converts a property to Xml fragments. + /// + public IEnumerable ConvertDbToXml(IProperty property, bool published) + { + published &= property.PropertyType.SupportsPublishing; + + var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper); + + foreach (IPropertyValue pvalue in property.Values) + { + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + if (value == null || (value is string stringValue && string.IsNullOrWhiteSpace(stringValue))) + { + continue; + } + + var xElement = new XElement(nodeName); + if (pvalue.Culture != null) + { + xElement.Add(new XAttribute("lang", pvalue.Culture)); + } + + if (pvalue.Segment != null) + { + xElement.Add(new XAttribute("segment", pvalue.Segment)); + } + + XNode xValue = ConvertDbToXml(property.PropertyType, value); + xElement.Add(xValue); + + yield return xElement; + } + } + + /// + /// Converts a property value to an Xml fragment. + /// + /// + /// + /// By default, this returns the value of ConvertDbToString but ensures that if the db value type is + /// NVarchar or NText, the value is returned as a CDATA fragment - else it's a Text fragment. + /// + /// Returns an XText or XCData instance which must be wrapped in a element. + /// If the value is empty we will not return as CDATA since that will just take up more space in the file. + /// + public XNode ConvertDbToXml(IPropertyType propertyType, object? value) + { + // check for null or empty value, we don't want to return CDATA if that is the case + if (value == null || value.ToString().IsNullOrWhiteSpace()) + { + return new XText(ConvertDbToString(propertyType, value)); + } + + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Date: + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + return new XText(ConvertDbToString(propertyType, value)); + case ValueStorageType.Nvarchar: + case ValueStorageType.Ntext: + // put text in cdata + return new XCData(ConvertDbToString(propertyType, value)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Converts a property value to a string. + /// + public virtual string ConvertDbToString(IPropertyType propertyType, object? value) + { + if (value == null) + { + return string.Empty; + } + + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Nvarchar: + case ValueStorageType.Ntext: + return value.ToXmlString(); + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + return value.ToXmlString(value.GetType()); + case ValueStorageType.Date: + // treat dates differently, output the format as xml format + Attempt date = value.TryConvertTo(); + if (date.Success == false || date.Result == null) + { + return string.Empty; + } + + return date.Result.ToXmlString(); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Used to try to convert the string value to the correct CLR type based on the specified for + /// this value editor. + /// + /// The value. + /// + /// The result of the conversion attempt. + /// + /// ValueType was out of range. + internal Attempt TryConvertValueToCrlType(object? value) + { + // Ensure empty string and JSON values are converted to null + if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) + { + value = null; + } + else if (value is not string && ValueType.InvariantEquals(ValueTypes.Json)) + { + // Only serialize value when it's not already a string + var jsonValue = _jsonSerializer?.Serialize(value); + if (jsonValue?.DetectIsEmptyJson() ?? false) + { + value = null; + } + else + { + value = jsonValue; + } + } + + // Convert the string to a known type + Type valueType; + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Ntext: + case ValueStorageType.Nvarchar: + valueType = typeof(string); + break; + + case ValueStorageType.Integer: + // Ensure these are nullable so we can return a null if required + // NOTE: This is allowing type of 'long' because I think JSON.NEt will deserialize a numerical value as long instead of int + // Even though our DB will not support this (will get truncated), we'll at least parse to this + valueType = typeof(long?); + + // If parsing is successful, we need to return as an int, we're only dealing with long's here because of JSON.NET, + // we actually don't support long values and if we return a long value, it will get set as a 'long' on the Property.Value (object) and then + // when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. + Attempt result = value.TryConvertTo(valueType); + + return result.Success && result.Result != null + ? Attempt.Succeed((int)(long)result.Result) + : result; + + case ValueStorageType.Decimal: + // Ensure these are nullable so we can return a null if required + valueType = typeof(decimal?); + break; + + case ValueStorageType.Date: + // Ensure these are nullable so we can return a null if required + valueType = typeof(DateTime?); + break; + + default: + throw new ArgumentOutOfRangeException("ValueType was out of range."); + } + + return value.TryConvertTo(valueType); + } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs index 300bdde672..86b771bcaa 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs @@ -1,18 +1,15 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataValueEditorFactory : IDataValueEditorFactory { - public class DataValueEditorFactory : IDataValueEditorFactory - { - private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _serviceProvider; - public DataValueEditorFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public DataValueEditorFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - public TDataValueEditor Create(params object[] args) - where TDataValueEditor: class, IDataValueEditor - => _serviceProvider.CreateInstance(args); - - } + public TDataValueEditor Create(params object[] args) + where TDataValueEditor : class, IDataValueEditor + => _serviceProvider.CreateInstance(args); } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 099fa9126f..24d6f17eb0 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -1,61 +1,67 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataValueReferenceFactoryCollection : BuilderCollectionBase { - public class DataValueReferenceFactoryCollection : BuilderCollectionBase + public DataValueReferenceFactoryCollection(Func> items) + : base(items) { - public DataValueReferenceFactoryCollection(System.Func> items) : base(items) + } + + // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented + // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection + public IEnumerable GetAllReferences( + IPropertyCollection properties, + PropertyEditorCollection propertyEditors) + { + var trackedRelations = new HashSet(); + + foreach (IProperty p in properties) { - } - - // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented - // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection - - public IEnumerable GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors) - { - var trackedRelations = new HashSet(); - - foreach (var p in properties) + if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out IDataEditor? editor)) { - if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + continue; + } - //TODO: We will need to change this once we support tracking via variants/segments - // for now, we are tracking values from ALL variants + // TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants + foreach (IPropertyValue propertyVal in p.Values) + { + var val = propertyVal.EditedValue; - foreach (var propertyVal in p.Values) + IDataValueEditor? valueEditor = editor?.GetValueEditor(); + if (valueEditor is IDataValueReference reference) { - var val = propertyVal.EditedValue; - - var valueEditor = editor?.GetValueEditor(); - if (valueEditor is IDataValueReference reference) + IEnumerable refs = reference.GetReferences(val); + foreach (UmbracoEntityReference r in refs) { - var refs = reference.GetReferences(val); - foreach (var r in refs) - trackedRelations.Add(r); + trackedRelations.Add(r); } + } - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (var item in this) + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (IDataValueReferenceFactory item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media/content items - if (item.IsForEditor(editor)) + foreach (UmbracoEntityReference r in item.GetDataValueReference().GetReferences(val)) { - foreach (var r in item.GetDataValueReference().GetReferences(val)) - trackedRelations.Add(r); + trackedRelations.Add(r); } } } } - - return trackedRelations; } + + return trackedRelations; } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs index b42ea74e88..f286827653 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase { - public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase - { - protected override DataValueReferenceFactoryCollectionBuilder This => this; - } + protected override DataValueReferenceFactoryCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs index 985d58f06d..27c1445160 100644 --- a/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs @@ -1,20 +1,22 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the datetime value editor. +/// +public class DateTimeConfiguration { - /// - /// Represents the configuration for the datetime value editor. - /// - public class DateTimeConfiguration - { - [ConfigurationField("format", "Date format", "textstring", Description = "If left empty then the format is YYYY-MM-DD. (see momentjs.com for supported formats)")] - public string Format { get; set; } + public DateTimeConfiguration() => - public DateTimeConfiguration() - { - // different default values - Format = "YYYY-MM-DD HH:mm:ss"; - } + // different default values + Format = "YYYY-MM-DD HH:mm:ss"; - [ConfigurationField("offsetTime", "Offset time", "boolean", Description = "When enabled the time displayed will be offset with the server's timezone, this is useful for scenarios like scheduled publishing when an editor is in a different timezone than the hosted server")] - public bool OffsetTime { get; set; } - } + [ConfigurationField("format", "Date format", "textstring", Description = "If left empty then the format is YYYY-MM-DD. (see momentjs.com for supported formats)")] + public string Format { get; set; } + + [ConfigurationField( + "offsetTime", + "Offset time", + "boolean", + Description = "When enabled the time displayed will be offset with the server's timezone, this is useful for scenarios like scheduled publishing when an editor is in a different timezone than the hosted server")] + public bool OffsetTime { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs index 36c82175c2..d97f7e2c6d 100644 --- a/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs @@ -1,40 +1,42 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the datetime value editor. +/// +public class DateTimeConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the datetime value editor. - /// - public class DateTimeConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public DateTimeConfigurationEditor(IIOHelper ioHelper) + : this( + ioHelper, + StaticServiceProvider.Instance.GetRequiredService()) { - public override IDictionary ToValueEditor(object? configuration) - { - var d = base.ToValueEditor(configuration); + } - var format = d["format"].ToString()!; + public DateTimeConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base( + ioHelper, editorConfigurationParser) + { + } - d["pickTime"] = format.ContainsAny(new string[] { "H", "m", "s" }); + public override IDictionary ToValueEditor(object? configuration) + { + IDictionary d = base.ToValueEditor(configuration); - return d; - } + var format = d["format"].ToString()!; - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public DateTimeConfigurationEditor(IIOHelper ioHelper) : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + d["pickTime"] = format.ContainsAny(new[] { "H", "m", "s" }); - public DateTimeConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs index 1e65429b6e..25cb2c42ed 100644 --- a/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs @@ -1,5 +1,3 @@ -using System; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors.Validators; @@ -8,34 +6,32 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// CUstom value editor so we can serialize with the correct date format (excluding time) +/// and includes the date validator +/// +internal class DateValueEditor : DataValueEditor { - /// - /// CUstom value editor so we can serialize with the correct date format (excluding time) - /// and includes the date validator - /// - internal class DateValueEditor : DataValueEditor + public DateValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => + Validators.Add(new DateTimeValidator()); + + public override object ToEditor(IProperty property, string? culture = null, string? segment = null) { - public DateValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + Attempt date = property.GetValue(culture, segment).TryConvertTo(); + if (date.Success == false || date.Result == null) { - Validators.Add(new DateTimeValidator()); + return string.Empty; } - public override object ToEditor(IProperty property, string? culture= null, string? segment = null) - { - var date = property.GetValue(culture, segment).TryConvertTo(); - if (date.Success == false || date.Result == null) - { - return String.Empty; - } - //Dates will be formatted as yyyy-MM-dd - return date.Result.Value.ToString("yyyy-MM-dd"); - } + // Dates will be formatted as yyyy-MM-dd + return date.Result.Value.ToString("yyyy-MM-dd"); } } diff --git a/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs index 52eefbd400..1b4a094ca2 100644 --- a/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs @@ -1,37 +1,36 @@ -using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.PropertyEditors.Validators; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. +/// +public class DecimalConfigurationEditor : ConfigurationEditor { - /// - /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. - /// - public class DecimalConfigurationEditor : ConfigurationEditor + public DecimalConfigurationEditor() { - public DecimalConfigurationEditor() + Fields.Add(new ConfigurationField(new DecimalValidator()) { - Fields.Add(new ConfigurationField(new DecimalValidator()) - { - Description = "Enter the minimum amount of number to be entered", - Key = "min", - View = "decimal", - Name = "Minimum" - }); + Description = "Enter the minimum amount of number to be entered", + Key = "min", + View = "decimal", + Name = "Minimum", + }); - Fields.Add(new ConfigurationField(new DecimalValidator()) - { - Description = "Enter the intervals amount between each step of number to be entered", - Key = "step", - View = "decimal", - Name = "Step Size" - }); + Fields.Add(new ConfigurationField(new DecimalValidator()) + { + Description = "Enter the intervals amount between each step of number to be entered", + Key = "step", + View = "decimal", + Name = "Step Size", + }); - Fields.Add(new ConfigurationField(new DecimalValidator()) - { - Description = "Enter the maximum amount of number to be entered", - Key = "max", - View = "decimal", - Name = "Maximum" - }); - } + Fields.Add(new ConfigurationField(new DecimalValidator()) + { + Description = "Enter the maximum amount of number to be entered", + Key = "max", + View = "decimal", + Name = "Maximum", + }); } } diff --git a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs index c940560c90..a936a72512 100644 --- a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs @@ -1,41 +1,35 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors.Validators; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a decimal property and parameter editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.Decimal, + EditorType.PropertyValue | EditorType.MacroParameter, + "Decimal", + "decimal", + ValueType = ValueTypes.Decimal)] +public class DecimalPropertyEditor : DataEditor { /// - /// Represents a decimal property and parameter editor. + /// Initializes a new instance of the class. /// - [DataEditor( - Constants.PropertyEditors.Aliases.Decimal, - EditorType.PropertyValue | EditorType.MacroParameter, - "Decimal", - "decimal", - ValueType = ValueTypes.Decimal)] - public class DecimalPropertyEditor : DataEditor + public DecimalPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + SupportsReadOnly = true; + + /// + protected override IDataValueEditor CreateValueEditor() { - /// - /// Initializes a new instance of the class. - /// - public DecimalPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } - - /// - protected override IDataValueEditor CreateValueEditor() - { - var editor = base.CreateValueEditor(); - editor.Validators.Add(new DecimalValidator()); - return editor; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new DecimalConfigurationEditor(); + IDataValueEditor editor = base.CreateValueEditor(); + editor.Validators.Add(new DecimalValidator()); + return editor; } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new DecimalConfigurationEditor(); } diff --git a/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs index 5cb11b7071..705ab034fc 100644 --- a/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs @@ -1,20 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Provides a default implementation for +/// , returning a single field to index containing the property value. +/// +public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory { - /// - /// Provides a default implementation for , returning a single field to index containing the property value. - /// - public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory + /// + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) { - /// - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) - { - yield return new KeyValuePair>( - property.Alias, - property.GetValue(culture, segment, published).Yield()); - } + yield return new KeyValuePair>( + property.Alias, + property.GetValue(culture, segment, published).Yield()); } } diff --git a/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs b/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs index a38ea29e0b..b74d9903cf 100644 --- a/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs @@ -1,33 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Indicates that this is a default property value converter (shipped with Umbraco) +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class DefaultPropertyValueConverterAttribute : Attribute { + public DefaultPropertyValueConverterAttribute() => DefaultConvertersToShadow = Array.Empty(); + + public DefaultPropertyValueConverterAttribute(params Type[] convertersToShadow) => + DefaultConvertersToShadow = convertersToShadow; + /// - /// Indicates that this is a default property value converter (shipped with Umbraco) + /// A DefaultPropertyValueConverter can 'shadow' other default property value converters so that + /// a DefaultPropertyValueConverter can be more specific than another one. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class DefaultPropertyValueConverterAttribute : Attribute - { - public DefaultPropertyValueConverterAttribute() - { - DefaultConvertersToShadow = Array.Empty(); - } - - public DefaultPropertyValueConverterAttribute(params Type[] convertersToShadow) - { - DefaultConvertersToShadow = convertersToShadow; - } - - /// - /// A DefaultPropertyValueConverter can 'shadow' other default property value converters so that - /// a DefaultPropertyValueConverter can be more specific than another one. - /// - /// - /// An example where this is useful is that both the RelatedLiksEditorValueConverter and the JsonValueConverter - /// will be returned as value converters for the Related Links Property editor, however the JsonValueConverter - /// is a very generic converter and the RelatedLiksEditorValueConverter is more specific than it, so the RelatedLiksEditorValueConverter - /// can specify that it 'shadows' the JsonValueConverter. - /// - public Type[] DefaultConvertersToShadow { get; } - } + /// + /// An example where this is useful is that both the RelatedLiksEditorValueConverter and the JsonValueConverter + /// will be returned as value converters for the Related Links Property editor, however the JsonValueConverter + /// is a very generic converter and the RelatedLiksEditorValueConverter is more specific than it, so the + /// RelatedLiksEditorValueConverter + /// can specify that it 'shadows' the JsonValueConverter. + /// + public Type[] DefaultConvertersToShadow { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index 4d74f4aec2..c0132d574d 100644 --- a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs @@ -1,8 +1,11 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DropDownFlexibleConfiguration : ValueListConfiguration { - public class DropDownFlexibleConfiguration : ValueListConfiguration - { - [ConfigurationField("multiple", "Enable multiple choice", "boolean", Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")] - public bool Multiple { get; set; } - } + [ConfigurationField( + "multiple", + "Enable multiple choice", + "boolean", + Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")] + public bool Multiple { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/EditorType.cs b/src/Umbraco.Core/PropertyEditors/EditorType.cs index 93d0b91b18..15469e1e51 100644 --- a/src/Umbraco.Core/PropertyEditors/EditorType.cs +++ b/src/Umbraco.Core/PropertyEditors/EditorType.cs @@ -1,26 +1,23 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the type of an editor. +/// +[Flags] +public enum EditorType { /// - /// Represents the type of an editor. + /// Nothing. /// - [Flags] - public enum EditorType - { - /// - /// Nothing. - /// - Nothing = 0, + Nothing = 0, - /// - /// Property value editor. - /// - PropertyValue = 1, + /// + /// Property value editor. + /// + PropertyValue = 1, - /// - /// Macro parameter editor. - /// - MacroParameter = 2 - } + /// + /// Macro parameter editor. + /// + MacroParameter = 2, } diff --git a/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs b/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs index 380d54dcad..cf3452c114 100644 --- a/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs @@ -1,14 +1,11 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the configuration for the email address value editor. +/// +public class EmailAddressConfiguration { - /// - /// Represents the configuration for the email address value editor. - /// - public class EmailAddressConfiguration - { - [ConfigurationField("IsRequired", "Required?", "hidden", Description = "Deprecated; Make this required by selecting mandatory when adding to the document type")] - [Obsolete("No longer used, use `Mandatory` for the property instead. Will be removed in the next major version")] - public bool IsRequired { get; set; } - } + [ConfigurationField("IsRequired", "Required?", "hidden", Description = "Deprecated; Make this required by selecting mandatory when adding to the document type")] + [Obsolete("No longer used, use `Mandatory` for the property instead. Will be removed in the next major version")] + public bool IsRequired { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs index e1e528dda2..2eb5075195 100644 --- a/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs @@ -1,28 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the email address value editor. - /// - public class EmailAddressConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public EmailAddressConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public EmailAddressConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the email address value editor. +/// +public class EmailAddressConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public EmailAddressConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public EmailAddressConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs index 9c2dffb61d..e9c8255a19 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration for the Eye Dropper picker value editor. - /// - public class EyeDropperColorPickerConfiguration - { - [ConfigurationField("showAlpha", "Show alpha", "boolean", Description = "Allow alpha transparency selection.")] - public bool ShowAlpha { get; set; } +namespace Umbraco.Cms.Core.PropertyEditors; - [ConfigurationField("showPalette", "Show palette", "boolean", Description = "Show a palette next to the color picker.")] - public bool ShowPalette { get; set; } - } +/// +/// Represents the configuration for the Eye Dropper picker value editor. +/// +public class EyeDropperColorPickerConfiguration +{ + [ConfigurationField("showAlpha", "Show alpha", "boolean", Description = "Allow alpha transparency selection.")] + public bool ShowAlpha { get; set; } + + [ConfigurationField("showPalette", "Show palette", "boolean", Description = "Show a palette next to the color picker.")] + public bool ShowPalette { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs index 49611f09b9..487034a6b1 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs @@ -1,51 +1,50 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class EyeDropperColorPickerConfigurationEditor : ConfigurationEditor { - internal class EyeDropperColorPickerConfigurationEditor : ConfigurationEditor + public EyeDropperColorPickerConfigurationEditor( + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public EyeDropperColorPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } + } - /// - public override Dictionary ToConfigurationEditor(EyeDropperColorPickerConfiguration? configuration) + /// + public override Dictionary ToConfigurationEditor(EyeDropperColorPickerConfiguration? configuration) => + new() { - return new Dictionary + { "showAlpha", configuration?.ShowAlpha ?? false }, { "showPalette", configuration?.ShowPalette ?? false }, + }; + + /// + public override EyeDropperColorPickerConfiguration FromConfigurationEditor( + IDictionary? editorValues, EyeDropperColorPickerConfiguration? configuration) + { + var showAlpha = true; + var showPalette = true; + + if (editorValues is not null && editorValues.TryGetValue("showAlpha", out var alpha)) + { + Attempt attempt = alpha.TryConvertTo(); + if (attempt.Success) { - { "showAlpha", configuration?.ShowAlpha ?? false }, - { "showPalette", configuration?.ShowPalette ?? false }, - }; - } - - /// - public override EyeDropperColorPickerConfiguration FromConfigurationEditor(IDictionary? editorValues, EyeDropperColorPickerConfiguration? configuration) - { - var showAlpha = true; - var showPalette = true; - - if (editorValues is not null && editorValues.TryGetValue("showAlpha", out var alpha)) - { - var attempt = alpha.TryConvertTo(); - if (attempt.Success) - showAlpha = attempt.Result; + showAlpha = attempt.Result; } - - if (editorValues is not null && editorValues.TryGetValue("showPalette", out var palette)) - { - var attempt = palette.TryConvertTo(); - if (attempt.Success) - showPalette = attempt.Result; - } - - return new EyeDropperColorPickerConfiguration - { - ShowAlpha = showAlpha, - ShowPalette = showPalette - }; } + + if (editorValues is not null && editorValues.TryGetValue("showPalette", out var palette)) + { + Attempt attempt = palette.TryConvertTo(); + if (attempt.Success) + { + showPalette = attempt.Result; + } + } + + return new EyeDropperColorPickerConfiguration { ShowAlpha = showAlpha, ShowPalette = showPalette }; } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs index e19a380334..12b1b2c8ef 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs @@ -1,48 +1,45 @@ -using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DataEditor( + Constants.PropertyEditors.Aliases.ColorPickerEyeDropper, + EditorType.PropertyValue | EditorType.MacroParameter, + "Eye Dropper Color Picker", + "eyedropper", + Icon = "icon-colorpicker", + Group = Constants.PropertyEditors.Groups.Pickers)] +public class EyeDropperColorPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.ColorPickerEyeDropper, - EditorType.PropertyValue | EditorType.MacroParameter, - "Eye Dropper Color Picker", - "eyedropper", - Icon = "icon-colorpicker", - Group = Constants.PropertyEditors.Groups.Pickers)] - public class EyeDropperColorPickerPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public EyeDropperColorPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + EditorType type = EditorType.PropertyValue) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService(), type) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public EyeDropperColorPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - EditorType type = EditorType.PropertyValue) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService(), type) - { - } - - public EyeDropperColorPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser, - EditorType type = EditorType.PropertyValue) - : base(dataValueEditorFactory, type) - { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new EyeDropperColorPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); } + + public EyeDropperColorPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser, + EditorType type = EditorType.PropertyValue) + : base(dataValueEditorFactory, type) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + SupportsReadOnly = true; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new EyeDropperColorPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); } diff --git a/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs b/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs index 2b1997459c..4444466c03 100644 --- a/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs +++ b/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs @@ -1,14 +1,13 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DataContract] +public class FileExtensionConfigItem : IFileExtensionConfigItem { - [DataContract] - public class FileExtensionConfigItem : IFileExtensionConfigItem - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "value")] - public string? Value { get; set; } - } + [DataMember(Name = "value")] + public string? Value { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs b/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs index 2953e2a1ed..289f649b00 100644 --- a/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the configuration for the file upload address value editor. +/// +public class FileUploadConfiguration : IFileExtensionsConfig { - /// - /// Represents the configuration for the file upload address value editor. - /// - public class FileUploadConfiguration : IFileExtensionsConfig - { - [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")] - public List FileExtensions { get; set; } = new List(); - } + [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")] + public List FileExtensions { get; set; } = new(); } diff --git a/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs index e8aa86e5d8..732e2d795a 100644 --- a/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs @@ -1,25 +1,24 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the file upload value editor. - /// - public class FileUploadConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public FileUploadConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public FileUploadConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the file upload value editor. +/// +public class FileUploadConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public FileUploadConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public FileUploadConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/GridEditor.cs b/src/Umbraco.Core/PropertyEditors/GridEditor.cs index 0e7b238900..d661fa9704 100644 --- a/src/Umbraco.Core/PropertyEditors/GridEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/GridEditor.cs @@ -1,69 +1,73 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration.Grid; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DataContract] +public class GridEditor : IGridEditorConfig { - - [DataContract] - public class GridEditor : IGridEditorConfig + public GridEditor() { - public GridEditor() - { - Config = new Dictionary(); - Alias = string.Empty; - } - - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } - - [DataMember(Name = "nameTemplate")] - public string? NameTemplate { get; set; } - - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } - - [DataMember(Name = "view", IsRequired = true)] - public string? View{ get; set; } - - [DataMember(Name = "render")] - public string? Render { get; set; } - - [DataMember(Name = "icon", IsRequired = true)] - public string? Icon { get; set; } - - [DataMember(Name = "config")] - public IDictionary Config { get; set; } - - protected bool Equals(GridEditor other) - { - return string.Equals(Alias, other.Alias); - } - - /// - /// Determines whether the specified is equal to the current . - /// - /// - /// true if the specified object is equal to the current object; otherwise, false. - /// - /// The object to compare with the current object. - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((GridEditor) obj); - } - - /// - /// Serves as a hash function for a particular type. - /// - /// - /// A hash code for the current . - /// - public override int GetHashCode() - { - return Alias.GetHashCode(); - } + Config = new Dictionary(); + Alias = string.Empty; } + + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } + + [DataMember(Name = "nameTemplate")] + public string? NameTemplate { get; set; } + + [DataMember(Name = "alias", IsRequired = true)] + public string Alias { get; set; } + + [DataMember(Name = "view", IsRequired = true)] + public string? View { get; set; } + + [DataMember(Name = "render")] + public string? Render { get; set; } + + [DataMember(Name = "icon", IsRequired = true)] + public string? Icon { get; set; } + + [DataMember(Name = "config")] + public IDictionary Config { get; set; } + + /// + /// Determines whether the specified is equal to the current + /// . + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((GridEditor)obj); + } + + protected bool Equals(GridEditor other) => string.Equals(Alias, other.Alias); + + /// + /// Serves as a hash function for a particular type. + /// + /// + /// A hash code for the current . + /// + public override int GetHashCode() => Alias.GetHashCode(); } diff --git a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs index b4d41f8e33..cbcb945c77 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs @@ -1,76 +1,88 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents an editor for editing the configuration of editors. +/// +public interface IConfigurationEditor { /// - /// Represents an editor for editing the configuration of editors. + /// Gets the fields. /// - public interface IConfigurationEditor - { - /// - /// Gets the fields. - /// - [DataMember(Name = "fields")] - List Fields { get; } + [DataMember(Name = "fields")] + List Fields { get; } - /// - /// Gets the default configuration. - /// - /// - /// For basic configuration editors, this will be a dictionary of key/values. For advanced editors - /// which inherit from , this will be the dictionary - /// equivalent of an actual configuration object (ie an instance of TConfiguration, obtained - /// via . - /// - [DataMember(Name = "defaultConfig")] - IDictionary DefaultConfiguration { get; } + /// + /// Gets the default configuration. + /// + /// + /// + /// For basic configuration editors, this will be a dictionary of key/values. For advanced editors + /// which inherit from , this will be the dictionary + /// equivalent of an actual configuration object (ie an instance of TConfiguration, obtained + /// via . + /// + /// + [DataMember(Name = "defaultConfig")] + IDictionary DefaultConfiguration { get; } - /// - /// Gets the default configuration object. - /// - /// - /// For basic configuration editors, this will be , ie a - /// dictionary of key/values. For advanced editors which inherit from , - /// this will be an actual configuration object (ie an instance of TConfiguration. - /// - object? DefaultConfigurationObject { get; } + /// + /// Gets the default configuration object. + /// + /// + /// + /// For basic configuration editors, this will be , ie a + /// dictionary of key/values. For advanced editors which inherit from + /// , + /// this will be an actual configuration object (ie an instance of TConfiguration. + /// + /// + object? DefaultConfigurationObject { get; } - /// - /// Determines whether a configuration object is of the type expected by the configuration editor. - /// - bool IsConfiguration(object obj); + /// + /// Determines whether a configuration object is of the type expected by the configuration editor. + /// + bool IsConfiguration(object obj); - // notes - // ToConfigurationEditor returns a dictionary, and FromConfigurationEditor accepts a dictionary. - // this is due to the way our front-end editors work, see DataTypeController.PostSave - // and DataTypeConfigurationFieldDisplayResolver - we are not going to change it now. + // notes + // ToConfigurationEditor returns a dictionary, and FromConfigurationEditor accepts a dictionary. + // this is due to the way our front-end editors work, see DataTypeController.PostSave + // and DataTypeConfigurationFieldDisplayResolver - we are not going to change it now. - /// - /// Converts the serialized database value into the actual configuration object. - /// - /// Converting the configuration object to the serialized database value is - /// achieved by simply serializing the configuration. See . - object FromDatabase(string? configurationJson, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); + /// + /// Converts the serialized database value into the actual configuration object. + /// + /// + /// Converting the configuration object to the serialized database value is + /// achieved by simply serializing the configuration. See . + /// + object FromDatabase( + string? configurationJson, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); - /// - /// Converts the values posted by the configuration editor into the actual configuration object. - /// - /// The values posted by the configuration editor. - /// The current configuration object. - object? FromConfigurationEditor(IDictionary? editorValues, object? configuration); + /// + /// Converts the values posted by the configuration editor into the actual configuration object. + /// + /// The values posted by the configuration editor. + /// The current configuration object. + object? FromConfigurationEditor(IDictionary? editorValues, object? configuration); - /// - /// Converts the configuration object to values for the configuration editor. - /// - /// The configuration. - IDictionary ToConfigurationEditor(object? configuration); + /// + /// Converts the configuration object to values for the configuration editor. + /// + /// The configuration. + [Obsolete("The value type parameter of the dictionary will be made nullable in V11, use ToConfigurationEditorNullable.")] + IDictionary ToConfigurationEditor(object? configuration); - /// - /// Converts the configuration object to values for the value editor. - /// - /// The configuration. - IDictionary? ToValueEditor(object? configuration); - } + // TODO: Obsolete in V11. + IDictionary ToConfigurationEditorNullable(object? configuration) => + ToConfigurationEditor(configuration)!; + + /// + /// Converts the configuration object to values for the value editor. + /// + /// The configuration. + IDictionary? ToValueEditor(object? configuration); } diff --git a/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs b/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs index 831d5d19fd..47768838d6 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs @@ -1,18 +1,17 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a configuration that configures the value type. +/// +/// +/// This is used in to get the value type from the configuration. +/// +public interface IConfigureValueType { /// - /// Represents a configuration that configures the value type. + /// Gets the value type. /// - /// - /// This is used in to get the value type from the configuration. - /// - public interface IConfigureValueType - { - /// - /// Gets the value type. - /// - string ValueType { get; } - } + string ValueType { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs index dba30aaf60..0569f8ab9a 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs @@ -1,75 +1,75 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data editor. +/// +/// This is the base interface for parameter and property editors. +public interface IDataEditor : IDiscoverable { /// - /// Represents a data editor. + /// Gets the alias of the editor. /// - /// This is the base interface for parameter and property editors. - public interface IDataEditor : IDiscoverable - { - /// - /// Gets the alias of the editor. - /// - string Alias { get; } + string Alias { get; } - /// - /// Gets the type of the editor. - /// - /// An editor can be a property value editor, or a parameter editor. - EditorType Type { get; } + bool SupportsReadOnly => false; - /// - /// Gets the name of the editor. - /// - string Name { get; } + /// + /// Gets the type of the editor. + /// + /// An editor can be a property value editor, or a parameter editor. + EditorType Type { get; } - /// - /// Gets the icon of the editor. - /// - /// Can be used to display editors when presenting them. - string Icon { get; } + /// + /// Gets the name of the editor. + /// + string Name { get; } - /// - /// Gets the group of the editor. - /// - /// Can be used to organize editors when presenting them. - string Group { get; } + /// + /// Gets the icon of the editor. + /// + /// Can be used to display editors when presenting them. + string Icon { get; } - /// - /// Gets a value indicating whether the editor is deprecated. - /// - /// Deprecated editors are supported but not proposed in the UI. - bool IsDeprecated { get; } + /// + /// Gets the group of the editor. + /// + /// Can be used to organize editors when presenting them. + string Group { get; } - /// - /// Gets a value editor. - /// - IDataValueEditor GetValueEditor(); // TODO: should be configured?! + /// + /// Gets a value indicating whether the editor is deprecated. + /// + /// Deprecated editors are supported but not proposed in the UI. + bool IsDeprecated { get; } - /// - /// Gets a configured value editor. - /// - IDataValueEditor GetValueEditor(object? configuration); + /// + /// Gets the configuration for the value editor. + /// + IDictionary? DefaultConfiguration { get; } - /// - /// Gets the configuration for the value editor. - /// - IDictionary? DefaultConfiguration { get; } + /// + /// Gets the index value factory for the editor. + /// + IPropertyIndexValueFactory PropertyIndexValueFactory { get; } - /// - /// Gets an editor to edit the value editor configuration. - /// - /// - /// Is expected to throw if the editor does not support being configured, e.g. for most parameter editors. - /// - IConfigurationEditor GetConfigurationEditor(); + /// + /// Gets a value editor. + /// + IDataValueEditor GetValueEditor(); // TODO: should be configured?! - /// - /// Gets the index value factory for the editor. - /// - IPropertyIndexValueFactory PropertyIndexValueFactory { get; } - } + /// + /// Gets a configured value editor. + /// + IDataValueEditor GetValueEditor(object? configuration); + + /// + /// Gets an editor to edit the value editor configuration. + /// + /// + /// Is expected to throw if the editor does not support being configured, e.g. for most parameter editors. + /// + IConfigurationEditor GetConfigurationEditor(); } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs b/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs index 663c7db6d6..a2f84cd71c 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs @@ -1,11 +1,9 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IDataValueEditorFactory { - public interface IDataValueEditorFactory - { - TDataValueEditor Create(params object[] args) - where TDataValueEditor : class, IDataValueEditor; - } + TDataValueEditor Create(params object[] args) + where TDataValueEditor : class, IDataValueEditor; } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index d44d732464..39d7d7e130 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -1,19 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Resolve references from values +/// +public interface IDataValueReference { /// - /// Resolve references from values + /// Returns any references contained in the value /// - public interface IDataValueReference - { - /// - /// Returns any references contained in the value - /// - /// - /// - IEnumerable GetReferences(object? value); - } + /// + /// + IEnumerable GetReferences(object? value); } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs index fd1f2f50d2..8c768c295f 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs @@ -1,18 +1,16 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - public interface IDataValueReferenceFactory - { - /// - /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). - /// - /// - /// A value indicating whether the converter supports a datatype. - bool IsForEditor(IDataEditor? dataEditor); +namespace Umbraco.Cms.Core.PropertyEditors; - /// - /// - /// - /// - IDataValueReference GetDataValueReference(); - } +public interface IDataValueReferenceFactory +{ + /// + /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). + /// + /// + /// A value indicating whether the converter supports a datatype. + bool IsForEditor(IDataEditor? dataEditor); + + /// + /// + /// + IDataValueReference GetDataValueReference(); } diff --git a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs index 6e9e9221f6..6119543956 100644 --- a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs +++ b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Marker interface for any editor configuration that supports defining file extensions +/// +public interface IFileExtensionsConfig { - /// - /// Marker interface for any editor configuration that supports defining file extensions - /// - public interface IFileExtensionsConfig - { - List FileExtensions { get; set; } - } + List FileExtensions { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs index d32005fb7f..fa2e8fa5f6 100644 --- a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs +++ b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - public interface IFileExtensionConfigItem - { - int Id { get; set; } +namespace Umbraco.Cms.Core.PropertyEditors; - string? Value { get; set; } - } +public interface IFileExtensionConfigItem +{ + int Id { get; set; } + + string? Value { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs index d6c20b9cdb..7e6b0c4410 100644 --- a/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs +++ b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Marker interface for any editor configuration that supports Ignoring user start nodes +/// +public interface IIgnoreUserStartNodesConfig { - /// - /// Marker interface for any editor configuration that supports Ignoring user start nodes - /// - public interface IIgnoreUserStartNodesConfig - { - bool IgnoreUserStartNodes { get; set; } - } + bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs b/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs index 28cf26022f..31078649a5 100644 --- a/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a value validator that can be referenced in a manifest. +/// +/// If the manifest can be configured, then it should expose a Configuration property. +public interface IManifestValueValidator : IValueValidator { /// - /// Defines a value validator that can be referenced in a manifest. + /// Gets the name of the validator. /// - /// If the manifest can be configured, then it should expose a Configuration property. - public interface IManifestValueValidator : IValueValidator - { - /// - /// Gets the name of the validator. - /// - string ValidationName { get; } - } + string ValidationName { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index 61f31a85c9..2af36b856f 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Determines if a property type's value should be compressed in memory +/// +/// +/// +public interface IPropertyCacheCompression { /// - /// Determines if a property type's value should be compressed in memory + /// Whether a property on the content is/should be compressed /// - /// - /// - /// - public interface IPropertyCacheCompression - {/// - /// Whether a property on the content is/should be compressed - /// - /// The content - /// The property to compress or not - /// Whether this content is the published version - bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published); - } + /// The content + /// The property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published); } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs index a63029fc3d..1cff2e7552 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -1,16 +1,15 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IPropertyCacheCompressionOptions { - public interface IPropertyCacheCompressionOptions - { - /// - /// Whether a property on the content is/should be compressed - /// - /// The content - /// The property to compress or not - /// The datatype of the property to compress or not - /// Whether this content is the published version - bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published); - } + /// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// The datatype of the property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published); } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs index 6ac6b46f50..fd607f4054 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs @@ -1,24 +1,26 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a property index value factory. +/// +public interface IPropertyIndexValueFactory { /// - /// Represents a property index value factory. + /// Gets the index values for a property. /// - public interface IPropertyIndexValueFactory - { - /// - /// Gets the index values for a property. - /// - /// - /// Returns key-value pairs, where keys are indexed field names. By default, that would be the property alias, - /// and there would be only one pair, but some implementations (see for instance the grid one) may return more than - /// one pair, with different indexed field names. - /// And then, values are an enumerable of objects, because each indexed field can in turn have multiple - /// values. By default, there would be only one object: the property value. But some implementations may return - /// more than one value for a given field. - /// - IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published); - } + /// + /// + /// Returns key-value pairs, where keys are indexed field names. By default, that would be the property alias, + /// and there would be only one pair, but some implementations (see for instance the grid one) may return more than + /// one pair, with different indexed field names. + /// + /// + /// And then, values are an enumerable of objects, because each indexed field can in turn have multiple + /// values. By default, there would be only one object: the property value. But some implementations may return + /// more than one value for a given field. + /// + /// + IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published); } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs index 499a691204..37d6b82475 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -1,112 +1,132 @@ -using System; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Provides published content properties conversion service. +/// +/// This is not a simple "value converter" because it really works only for properties. +public interface IPropertyValueConverter : IDiscoverable { /// - /// Provides published content properties conversion service. + /// Gets a value indicating whether the converter supports a property type. /// - /// This is not a simple "value converter" because it really works only for properties. - public interface IPropertyValueConverter : IDiscoverable - { - /// - /// Gets a value indicating whether the converter supports a property type. - /// - /// The property type. - /// A value indicating whether the converter supports a property type. - bool IsConverter(IPublishedPropertyType propertyType); + /// The property type. + /// A value indicating whether the converter supports a property type. + bool IsConverter(IPublishedPropertyType propertyType); - /// - /// Determines whether a value is an actual value, or not a value. - /// - /// - /// Called for Source, Inter and Object levels, until one does not return null. - /// Can return true (is a value), false (is not a value), or null to indicate that it - /// cannot be determined at the specified level. For instance, if source is a string that - /// could contain JSON, the decision could be made on the intermediate value. Or, if it is - /// a picker, it could be made on the object value (the actual picked object). - /// - bool? IsValue(object? value, PropertyValueLevel level); + /// + /// Determines whether a value is an actual value, or not a value. + /// + /// + /// Called for Source, Inter and Object levels, until one does not return null. + /// + /// Can return true (is a value), false (is not a value), or null to indicate that it + /// cannot be determined at the specified level. For instance, if source is a string that + /// could contain JSON, the decision could be made on the intermediate value. Or, if it is + /// a picker, it could be made on the object value (the actual picked object). + /// + /// + bool? IsValue(object? value, PropertyValueLevel level); - /// - /// Gets the type of values returned by the converter. - /// - /// The property type. - /// The CLR type of values returned by the converter. - /// Some of the CLR types may be generated, therefore this method cannot directly return - /// a Type object (which may not exist yet). In which case it needs to return a ModelType instance. - Type GetPropertyValueType(IPublishedPropertyType propertyType); + /// + /// Gets the type of values returned by the converter. + /// + /// The property type. + /// The CLR type of values returned by the converter. + /// + /// Some of the CLR types may be generated, therefore this method cannot directly return + /// a Type object (which may not exist yet). In which case it needs to return a ModelType instance. + /// + Type GetPropertyValueType(IPublishedPropertyType propertyType); - /// - /// Gets the property cache level. - /// - /// The property type. - /// The property cache level. - PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType); + /// + /// Gets the property cache level. + /// + /// The property type. + /// The property cache level. + PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType); - /// - /// Converts a property source value to an intermediate value. - /// - /// The property set owning the property. - /// The property type. - /// The source value. - /// A value indicating whether conversion should take place in preview mode. - /// The result of the conversion. - /// - /// The converter should know how to convert a null source value, meaning that no - /// value has been assigned to the property. The intermediate value can be null. - /// With the XML cache, source values come from the XML cache and therefore are strings. - /// With objects caches, source values would come from the database and therefore be either - /// ints, DateTimes, decimals, or strings. - /// The converter should be prepared to handle both situations. - /// When source values are strings, the converter must handle empty strings, whitespace - /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve - /// white spaces. - /// - object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview); + /// + /// Converts a property source value to an intermediate value. + /// + /// The property set owning the property. + /// The property type. + /// The source value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// + /// The converter should know how to convert a null source value, meaning that no + /// value has been assigned to the property. The intermediate value can be null. + /// + /// With the XML cache, source values come from the XML cache and therefore are strings. + /// + /// With objects caches, source values would come from the database and therefore be either + /// ints, DateTimes, decimals, or strings. + /// + /// The converter should be prepared to handle both situations. + /// + /// When source values are strings, the converter must handle empty strings, whitespace + /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve + /// white spaces. + /// + /// + object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview); - /// - /// Converts a property intermediate value to an Object value. - /// - /// The property set owning the property. - /// The property type. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether conversion should take place in preview mode. - /// The result of the conversion. - /// - /// The converter should know how to convert a null intermediate value, or any intermediate value - /// indicating that no value has been assigned to the property. It is up to the converter to determine - /// what to return in that case: either null, or the default value... - /// The is passed to the converter so that it can be, in turn, - /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage - /// the cache levels of property values. It is not meant to be used by the converter. - /// - object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + /// + /// Converts a property intermediate value to an Object value. + /// + /// The property set owning the property. + /// The property type. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// + /// The converter should know how to convert a null intermediate value, or any intermediate value + /// indicating that no value has been assigned to the property. It is up to the converter to determine + /// what to return in that case: either null, or the default value... + /// + /// + /// The is passed to the converter so that it can be, in turn, + /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage + /// the cache levels of property values. It is not meant to be used by the converter. + /// + /// + object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); - /// - /// Converts a property intermediate value to an XPath value. - /// - /// The property set owning the property. - /// The property type. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether conversion should take place in preview mode. - /// The result of the conversion. - /// - /// The converter should know how to convert a null intermediate value, or any intermediate value - /// indicating that no value has been assigned to the property. It is up to the converter to determine - /// what to return in that case: either null, or the default value... - /// If successful, the result should be either null, a string, or an XPathNavigator - /// instance. Whether an xml-whitespace string should be returned as null or literally, is - /// up to the converter. - /// The converter may want to return an XML fragment that represent a part of the content tree, - /// but should pay attention not to create infinite loops that would kill XPath and XSLT. - /// The is passed to the converter so that it can be, in turn, - /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage - /// the cache levels of property values. It is not meant to be used by the converter. - /// - object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); - } + /// + /// Converts a property intermediate value to an XPath value. + /// + /// The property set owning the property. + /// The property type. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// + /// The converter should know how to convert a null intermediate value, or any intermediate value + /// indicating that no value has been assigned to the property. It is up to the converter to determine + /// what to return in that case: either null, or the default value... + /// + /// + /// If successful, the result should be either null, a string, or an XPathNavigator + /// instance. Whether an xml-whitespace string should be returned as null or literally, is + /// up to the converter. + /// + /// + /// The converter may want to return an XML fragment that represent a part of the content tree, + /// but should pay attention not to create infinite loops that would kill XPath and XSLT. + /// + /// + /// The is passed to the converter so that it can be, in turn, + /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage + /// the cache levels of property values. It is not meant to be used by the converter. + /// + /// + object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); } diff --git a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs index 9674eaea98..6070512329 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a value format validator. +/// +public interface IValueFormatValidator { /// - /// Defines a value format validator. + /// Validates a value. /// - public interface IValueFormatValidator - { - /// - /// Validates a value. - /// - /// The value to validate. - /// The value type. - /// A format definition. - /// Validation results. - /// - /// The is expected to be a valid regular expression. - /// This is used to validate values against the property type validation regular expression. - /// - IEnumerable ValidateFormat(object? value, string valueType, string format); - } + /// The value to validate. + /// The value type. + /// A format definition. + /// Validation results. + /// + /// The is expected to be a valid regular expression. + /// This is used to validate values against the property type validation regular expression. + /// + IEnumerable ValidateFormat(object? value, string valueType, string format); } diff --git a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs index 439bfcdc81..3bbc348431 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a required value validator. +/// +public interface IValueRequiredValidator { /// - /// Defines a required value validator. + /// Validates a value. /// - public interface IValueRequiredValidator - { - /// - /// Validates a value. - /// - /// The value to validate. - /// The value type. - /// Validation results. - /// - /// This is used to validate values when the property type specifies that a value is required. - /// - IEnumerable ValidateRequired(object? value, string valueType); - } + /// The value to validate. + /// The value type. + /// Validation results. + /// + /// This is used to validate values when the property type specifies that a value is required. + /// + IEnumerable ValidateRequired(object? value, string valueType); } diff --git a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs index b4304fad59..7d26f8a96c 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs @@ -1,23 +1,24 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a value validator. +/// +public interface IValueValidator { /// - /// Defines a value validator. + /// Validates a value. /// - public interface IValueValidator - { - /// - /// Validates a value. - /// - /// The value to validate. - /// The value type. - /// A datatype configuration. - /// Validation results. - /// - /// The value can be a string, a Json structure (JObject, JArray...)... corresponding to what was posted by an editor. - /// - IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration); - } + /// The value to validate. + /// The value type. + /// A datatype configuration. + /// Validation results. + /// + /// + /// The value can be a string, a Json structure (JObject, JArray...)... corresponding to what was posted by an + /// editor. + /// + /// + IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration); } diff --git a/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs index e7c2114dd2..e5d01900c6 100644 --- a/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs @@ -1,37 +1,36 @@ -using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.PropertyEditors.Validators; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. +/// +public class IntegerConfigurationEditor : ConfigurationEditor { - /// - /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. - /// - public class IntegerConfigurationEditor : ConfigurationEditor + public IntegerConfigurationEditor() { - public IntegerConfigurationEditor() + Fields.Add(new ConfigurationField(new IntegerValidator()) { - Fields.Add(new ConfigurationField(new IntegerValidator()) - { - Description = "Enter the minimum amount of number to be entered", - Key = "min", - View = "number", - Name = "Minimum" - }); + Description = "Enter the minimum amount of number to be entered", + Key = "min", + View = "number", + Name = "Minimum", + }); - Fields.Add(new ConfigurationField(new IntegerValidator()) - { - Description = "Enter the intervals amount between each step of number to be entered", - Key = "step", - View = "number", - Name = "Step Size" - }); + Fields.Add(new ConfigurationField(new IntegerValidator()) + { + Description = "Enter the intervals amount between each step of number to be entered", + Key = "step", + View = "number", + Name = "Step Size", + }); - Fields.Add(new ConfigurationField(new IntegerValidator()) - { - Description = "Enter the maximum amount of number to be entered", - Key = "max", - View = "number", - Name = "Maximum" - }); - } + Fields.Add(new ConfigurationField(new IntegerValidator()) + { + Description = "Enter the maximum amount of number to be entered", + Key = "max", + View = "number", + Name = "Maximum", + }); } } diff --git a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs index f243158db3..a504c7df31 100644 --- a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs @@ -1,38 +1,32 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors.Validators; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents an integer property and parameter editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.Integer, + EditorType.PropertyValue | EditorType.MacroParameter, + "Numeric", + "integer", + ValueType = ValueTypes.Integer)] +public class IntegerPropertyEditor : DataEditor { - /// - /// Represents an integer property and parameter editor. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.Integer, - EditorType.PropertyValue | EditorType.MacroParameter, - "Numeric", - "integer", - ValueType = ValueTypes.Integer)] - public class IntegerPropertyEditor : DataEditor + public IntegerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + SupportsReadOnly = true; + + /// + protected override IDataValueEditor CreateValueEditor() { - public IntegerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } - - /// - protected override IDataValueEditor CreateValueEditor() - { - var editor = base.CreateValueEditor(); - editor.Validators.Add(new IntegerValidator()); // ensure the value is validated - return editor; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new IntegerConfigurationEditor(); + IDataValueEditor editor = base.CreateValueEditor(); + editor.Validators.Add(new IntegerValidator()); // ensure the value is validated + return editor; } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new IntegerConfigurationEditor(); } diff --git a/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs b/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs index 28fe05d151..f023b86a78 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the label value editor. +/// +public class LabelConfiguration : IConfigureValueType { - /// - /// Represents the configuration for the label value editor. - /// - public class LabelConfiguration : IConfigureValueType - { - [ConfigurationField(Constants.PropertyEditors.ConfigurationKeys.DataValueType, "Value type", "valuetype")] - public string ValueType { get; set; } = ValueTypes.String; - } + [ConfigurationField(Constants.PropertyEditors.ConfigurationKeys.DataValueType, "Value type", "valuetype")] + public string ValueType { get; set; } = ValueTypes.String; } diff --git a/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs index b2a214f729..cb5a531f65 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs @@ -1,49 +1,51 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the label value editor. +/// +public class LabelConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration for the label value editor. - /// - public class LabelConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes and IEditorConfigurationParser instead")] + public LabelConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes and IEditorConfigurationParser instead")] - public LabelConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + } + + public LabelConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + } + + /// + public override LabelConfiguration FromConfigurationEditor( + IDictionary? editorValues, + LabelConfiguration? configuration) + { + var newConfiguration = new LabelConfiguration(); + + // get the value type + // not simply deserializing Json because we want to validate the valueType + if (editorValues is not null && editorValues.TryGetValue( + Constants.PropertyEditors.ConfigurationKeys.DataValueType, + out var valueTypeObj) + && valueTypeObj is string stringValue) { - } - - public LabelConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } - - /// - public override LabelConfiguration FromConfigurationEditor(IDictionary? editorValues, LabelConfiguration? configuration) - { - var newConfiguration = new LabelConfiguration(); - - // get the value type - // not simply deserializing Json because we want to validate the valueType - - if (editorValues is not null && editorValues.TryGetValue(Cms.Core.Constants.PropertyEditors.ConfigurationKeys.DataValueType, out var valueTypeObj) - && valueTypeObj is string stringValue) + // validate + if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) { - if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) // validate - newConfiguration.ValueType = stringValue; + newConfiguration.ValueType = stringValue; } - - return newConfiguration; } - + return newConfiguration; } } diff --git a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs index c142b581d0..ae2f4c0897 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs @@ -1,7 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -10,61 +9,66 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a property editor for label properties. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.Label, + "Label", + "readonlyvalue", + Icon = "icon-readonly")] +public class LabelPropertyEditor : DataEditor { - /// - /// Represents a property editor for label properties. - /// - [DataEditor( - Cms.Core.Constants.PropertyEditors.Aliases.Label, - "Label", - "readonlyvalue", - Icon = "icon-readonly")] - public class LabelPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public LabelPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public LabelPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + /// + /// Initializes a new instance of the class. + /// + public LabelPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + SupportsReadOnly = true; + } - /// - /// Initializes a new instance of the class. - /// - public LabelPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) + /// + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new LabelConfigurationEditor(_ioHelper, _editorConfigurationParser); + + // provides the property value editor + internal class LabelPropertyValueEditor : DataValueEditor + { + public LabelPropertyValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; } /// - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new LabelConfigurationEditor(_ioHelper, _editorConfigurationParser); - - // provides the property value editor - internal class LabelPropertyValueEditor : DataValueEditor - { - public LabelPropertyValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) - { } - - /// - public override bool IsReadOnly => true; - } + public override bool IsReadOnly => true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs index 055867e80b..13f423a328 100644 --- a/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs @@ -1,128 +1,153 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the listview value editor. +/// +public class ListViewConfiguration { - /// - /// Represents the configuration for the listview value editor. - /// - public class ListViewConfiguration + public ListViewConfiguration() { - public ListViewConfiguration() + // initialize defaults + PageSize = 10; + OrderBy = "SortOrder"; + OrderDirection = "asc"; + + BulkActionPermissions = new BulkActionPermissionSettings { - // initialize defaults + AllowBulkPublish = true, + AllowBulkUnpublish = true, + AllowBulkCopy = true, + AllowBulkMove = true, + AllowBulkDelete = true, + }; - PageSize = 10; - OrderBy = "SortOrder"; - OrderDirection = "asc"; - - BulkActionPermissions = new BulkActionPermissionSettings + Layouts = new[] + { + new Layout { - AllowBulkPublish = true, - AllowBulkUnpublish = true, - AllowBulkCopy = true, - AllowBulkMove = true, - AllowBulkDelete = true - }; - - Layouts = new[] + Name = "List", + Icon = "icon-list", + IsSystem = 1, + Selected = true, + Path = "views/propertyeditors/listview/layouts/list/list.html", + }, + new Layout { - new Layout { Name = "List", Icon = "icon-list", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/list/list.html" }, - new Layout { Name = "Grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } - }; + Name = "Grid", + Icon = "icon-thumbnails-small", + IsSystem = 1, + Selected = true, + Path = "views/propertyeditors/listview/layouts/grid/grid.html", + }, + }; - IncludeProperties = new[] - { - new Property { Alias = "sortOrder", Header = "Sort order", IsSystem = 1 }, - new Property { Alias = "updateDate", Header = "Last edited", IsSystem = 1 }, - new Property { Alias = "owner", Header = "Created by", IsSystem = 1 } - }; - } + IncludeProperties = new[] + { + new Property { Alias = "sortOrder", Header = "Sort order", IsSystem = 1 }, + new Property { Alias = "updateDate", Header = "Last edited", IsSystem = 1 }, + new Property { Alias = "owner", Header = "Created by", IsSystem = 1 }, + }; + } - [ConfigurationField("pageSize", "Page Size", "number", Description = "Number of items per page")] - public int PageSize { get; set; } + [ConfigurationField("pageSize", "Page Size", "number", Description = "Number of items per page")] + public int PageSize { get; set; } - [ConfigurationField("orderBy", "Order By", "views/propertyeditors/listview/sortby.prevalues.html", - Description = "The default sort order for the list")] - public string OrderBy { get; set; } + [ConfigurationField("orderBy", "Order By", "views/propertyeditors/listview/sortby.prevalues.html", Description = "The default sort order for the list")] + public string OrderBy { get; set; } - [ConfigurationField("orderDirection", "Order Direction", "views/propertyeditors/listview/orderDirection.prevalues.html")] - public string OrderDirection { get; set; } + [ConfigurationField("orderDirection", "Order Direction", "views/propertyeditors/listview/orderDirection.prevalues.html")] + public string OrderDirection { get; set; } - [ConfigurationField("includeProperties", "Columns Displayed", "views/propertyeditors/listview/includeproperties.prevalues.html", - Description = "The properties that will be displayed for each column")] - public Property[] IncludeProperties { get; set; } + [ConfigurationField( + "includeProperties", + "Columns Displayed", + "views/propertyeditors/listview/includeproperties.prevalues.html", + Description = "The properties that will be displayed for each column")] + public Property[] IncludeProperties { get; set; } - [ConfigurationField("layouts", "Layouts", "views/propertyeditors/listview/layouts.prevalues.html")] - public Layout[] Layouts { get; set; } + [ConfigurationField("layouts", "Layouts", "views/propertyeditors/listview/layouts.prevalues.html")] + public Layout[] Layouts { get; set; } - [ConfigurationField("bulkActionPermissions", "Bulk Action Permissions", "views/propertyeditors/listview/bulkActionPermissions.prevalues.html", - Description = "The bulk actions that are allowed from the list view")] - public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new BulkActionPermissionSettings(); // TODO: managing defaults? + [ConfigurationField( + "bulkActionPermissions", + "Bulk Action Permissions", + "views/propertyeditors/listview/bulkActionPermissions.prevalues.html", + Description = "The bulk actions that are allowed from the list view")] + public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new(); // TODO: managing defaults? - [ConfigurationField("icon", "Content app icon", "views/propertyeditors/listview/icon.prevalues.html", Description = "The icon of the listview content app")] + [ConfigurationField("icon", "Content app icon", "views/propertyeditors/listview/icon.prevalues.html", Description = "The icon of the listview content app")] + public string? Icon { get; set; } + + [ConfigurationField("tabName", "Content app name", "textstring", Description = "The name of the listview content app (default if empty: 'Child Items')")] + public string? TabName { get; set; } + + [ConfigurationField( + "showContentFirst", + "Show Content App First", + "boolean", + Description = "Enable this to show the content app by default instead of the list view app")] + public bool ShowContentFirst { get; set; } + + [ConfigurationField( + "useInfiniteEditor", + "Edit in Infinite Editor", + "boolean", + Description = "Enable this to use infinite editing to edit the content of the list view")] + public bool UseInfiniteEditor { get; set; } + + [DataContract] + public class Property + { + [DataMember(Name = "alias")] + public string? Alias { get; set; } + + [DataMember(Name = "header")] + public string? Header { get; set; } + + [DataMember(Name = "nameTemplate")] + public string? Template { get; set; } + + [DataMember(Name = "isSystem")] + public int IsSystem { get; set; } // TODO: bool + } + + [DataContract] + public class Layout + { + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "path")] + public string? Path { get; set; } + + [DataMember(Name = "icon")] public string? Icon { get; set; } - [ConfigurationField("tabName", "Content app name", "textstring", Description = "The name of the listview content app (default if empty: 'Child Items')")] - public string? TabName { get; set; } + [DataMember(Name = "isSystem")] + public int IsSystem { get; set; } // TODO: bool - [ConfigurationField("showContentFirst", "Show Content App First", "boolean", Description = "Enable this to show the content app by default instead of the list view app")] - public bool ShowContentFirst { get; set; } + [DataMember(Name = "selected")] + public bool Selected { get; set; } + } - [ConfigurationField("useInfiniteEditor", "Edit in Infinite Editor", "boolean", Description = "Enable this to use infinite editing to edit the content of the list view")] - public bool UseInfiniteEditor { get; set; } + [DataContract] + public class BulkActionPermissionSettings + { + [DataMember(Name = "allowBulkPublish")] + public bool AllowBulkPublish { get; set; } = true; - [DataContract] - public class Property - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "allowBulkUnpublish")] + public bool AllowBulkUnpublish { get; set; } = true; - [DataMember(Name = "header")] - public string? Header { get; set; } + [DataMember(Name = "allowBulkCopy")] + public bool AllowBulkCopy { get; set; } = true; - [DataMember(Name = "nameTemplate")] - public string? Template { get; set; } + [DataMember(Name = "allowBulkMove")] + public bool AllowBulkMove { get; set; } = true; - [DataMember(Name = "isSystem")] - public int IsSystem { get; set; } // TODO: bool - } - - [DataContract] - public class Layout - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "path")] - public string? Path { get; set; } - - [DataMember(Name = "icon")] - public string? Icon { get; set; } - - [DataMember(Name = "isSystem")] - public int IsSystem { get; set; } // TODO: bool - - [DataMember(Name = "selected")] - public bool Selected { get; set; } - } - - [DataContract] - public class BulkActionPermissionSettings - { - [DataMember(Name = "allowBulkPublish")] - public bool AllowBulkPublish { get; set; } = true; - - [DataMember(Name = "allowBulkUnpublish")] - public bool AllowBulkUnpublish { get; set; } = true; - - [DataMember(Name = "allowBulkCopy")] - public bool AllowBulkCopy { get; set; } = true; - - [DataMember(Name = "allowBulkMove")] - public bool AllowBulkMove { get; set; } = true; - - [DataMember(Name = "allowBulkDelete")] - public bool AllowBulkDelete { get; set; } = true; - } + [DataMember(Name = "allowBulkDelete")] + public bool AllowBulkDelete { get; set; } = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs index d673ce4ee6..8ecab6d751 100644 --- a/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the listview value editor. - /// - public class ListViewConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public ListViewConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public ListViewConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the listview value editor. +/// +public class ListViewConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public ListViewConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public ListViewConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs index 81b1c1fba1..f2a08076b9 100644 --- a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs @@ -1,32 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ManifestValueValidatorCollection : BuilderCollectionBase { - public class ManifestValueValidatorCollection : BuilderCollectionBase + public ManifestValueValidatorCollection(Func> items) + : base(items) { - public ManifestValueValidatorCollection(Func> items) : base(items) + } + + public IManifestValueValidator? Create(string name) + { + IManifestValueValidator v = GetByName(name); + + // TODO: what is this exactly? + // we cannot return this instance, need to clone it? + return (IManifestValueValidator?)Activator.CreateInstance(v.GetType()); // ouch + } + + public IManifestValueValidator GetByName(string name) + { + IManifestValueValidator? v = this.FirstOrDefault(x => x.ValidationName.InvariantEquals(name)); + if (v == null) { + throw new InvalidOperationException($"Could not find a validator named \"{name}\"."); } - public IManifestValueValidator? Create(string name) - { - var v = GetByName(name); - - // TODO: what is this exactly? - // we cannot return this instance, need to clone it? - return (IManifestValueValidator?) Activator.CreateInstance(v.GetType()); // ouch - } - - public IManifestValueValidator GetByName(string name) - { - var v = this.FirstOrDefault(x => x.ValidationName.InvariantEquals(name)); - if (v == null) - throw new InvalidOperationException($"Could not find a validator named \"{name}\"."); - return v; - } + return v; } } diff --git a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs index 66a967c828..044c7f2c0c 100644 --- a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ManifestValueValidatorCollectionBuilder : SetCollectionBuilderBase { - public class ManifestValueValidatorCollectionBuilder : SetCollectionBuilderBase - { - protected override ManifestValueValidatorCollectionBuilder This => this; - } + protected override ManifestValueValidatorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs index 62ddd4c053..b11ef08f30 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs @@ -1,18 +1,16 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the markdown value editor. +/// +public class MarkdownConfiguration { - /// - /// Represents the configuration for the markdown value editor. - /// - public class MarkdownConfiguration - { - [ConfigurationField("preview", "Preview", "boolean", Description = "Display a live preview")] - public bool DisplayLivePreview { get; set; } + [ConfigurationField("preview", "Preview", "boolean", Description = "Display a live preview")] + public bool DisplayLivePreview { get; set; } - [ConfigurationField("defaultValue", "Default value", "textarea", Description = "If value is blank, the editor will show this")] - public string? DefaultValue { get; set; } + [ConfigurationField("defaultValue", "Default value", "textarea", Description = "If value is blank, the editor will show this")] + public string? DefaultValue { get; set; } - - [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] - public string? OverlaySize { get; set; } - } + [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] + public string? OverlaySize { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs index 3f9bc61275..032bafd12b 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs @@ -4,15 +4,15 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editorfor the markdown value editor. +/// +internal class MarkdownConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editorfor the markdown value editor. - /// - internal class MarkdownConfigurationEditor : ConfigurationEditor + public MarkdownConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public MarkdownConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } } } diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs index 6db2ac552e..aa6e881aa2 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs @@ -1,52 +1,52 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a markdown editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.MarkdownEditor, + "Markdown editor", + "markdowneditor", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.RichContent, + Icon = "icon-code")] +public class MarkdownPropertyEditor : DataEditor { - /// - /// Represents a markdown editor. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.MarkdownEditor, - "Markdown editor", - "markdowneditor", - ValueType = ValueTypes.Text, - Group = Constants.PropertyEditors.Groups.RichContent, - Icon = "icon-code")] - public class MarkdownPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MarkdownPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MarkdownPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - /// - /// Initializes a new instance of the class. - /// - public MarkdownPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) - { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new MarkdownConfigurationEditor(_ioHelper, _editorConfigurationParser); } + + /// + /// Initializes a new instance of the class. + /// + public MarkdownPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + SupportsReadOnly = true; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new MarkdownConfigurationEditor(_ioHelper, _editorConfigurationParser); } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs b/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs index 8b843fdf85..11ed4d1afd 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs @@ -1,61 +1,64 @@ using System.Runtime.Serialization; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the configuration for the media picker value editor. +/// +public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the media picker value editor. - /// - public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig + [ConfigurationField("filter", "Accepted types", "treesourcetypepicker", Description = "Limit to specific types")] + public string? Filter { get; set; } + + [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")] + public bool Multiple { get; set; } + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] + public NumberRange ValidationLimit { get; set; } = new(); + + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi? StartNodeId { get; set; } + + [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] + public bool EnableLocalFocalPoint { get; set; } + + [ConfigurationField( + "crops", + "Image Crops", + "views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html", + Description = "Local crops, stored on document")] + public CropConfiguration[]? Crops { get; set; } + + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } + + [DataContract] + public class NumberRange { - [ConfigurationField("filter", "Accepted types", "treesourcetypepicker", - Description = "Limit to specific types")] - public string? Filter { get; set; } + [DataMember(Name = "min")] + public int? Min { get; set; } - [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")] - public bool Multiple { get; set; } + [DataMember(Name = "max")] + public int? Max { get; set; } + } - [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] - public NumberRange ValidationLimit { get; set; } = new NumberRange(); + [DataContract] + public class CropConfiguration + { + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataContract] - public class NumberRange - { - [DataMember(Name = "min")] - public int? Min { get; set; } + [DataMember(Name = "label")] + public string? Label { get; set; } - [DataMember(Name = "max")] - public int? Max { get; set; } - } + [DataMember(Name = "width")] + public int Width { get; set; } - [ConfigurationField("startNodeId", "Start node", "mediapicker")] - public Udi? StartNodeId { get; set; } - - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - - [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] - public bool EnableLocalFocalPoint { get; set; } - - [ConfigurationField("crops", "Image Crops", "views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html", Description = "Local crops, stored on document")] - public CropConfiguration[]? Crops { get; set; } - - [DataContract] - public class CropConfiguration - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - [DataMember(Name = "label")] - public string? Label { get; set; } - - [DataMember(Name = "width")] - public int Width { get; set; } - - [DataMember(Name = "height")] - public int Height { get; set; } - } + [DataMember(Name = "height")] + public int Height { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs index c5ab1c403c..9ccf64a6f0 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs @@ -1,38 +1,35 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the media picker value editor. +/// +public class MediaPicker3ConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the media picker value editor. - /// - public class MediaPicker3ConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MediaPicker3ConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MediaPicker3ConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + } - /// - /// Initializes a new instance of the class. - /// - public MediaPicker3ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - // configure fields - // this is not part of ContentPickerConfiguration, - // but is required to configure the UI editor (when editing the configuration) + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + Field(nameof(MediaPicker3Configuration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; - Field(nameof(MediaPicker3Configuration.StartNodeId)) - .Config = new Dictionary { { "idType", "udi" } }; - - Field(nameof(MediaPicker3Configuration.Filter)) - .Config = new Dictionary { { "itemType", "media" } }; - } + Field(nameof(MediaPicker3Configuration.Filter)) + .Config = new Dictionary { { "itemType", "media" } }; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs index d18eeac644..055f4fea4d 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs @@ -1,25 +1,26 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the media picker value editor. +/// +public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the media picker value editor. - /// - public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] - public bool Multiple { get; set; } + [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] + public bool Multiple { get; set; } - [ConfigurationField("onlyImages", "Pick only images", "boolean", Description = "Only let the editor choose images from media.")] - public bool OnlyImages { get; set; } + [ConfigurationField("onlyImages", "Pick only images", "boolean", Description = "Only let the editor choose images from media.")] + public bool OnlyImages { get; set; } - [ConfigurationField("disableFolderSelect", "Disable folder select", "boolean", Description = "Do not allow folders to be picked.")] - public bool DisableFolderSelect { get; set; } + [ConfigurationField("disableFolderSelect", "Disable folder select", "boolean", Description = "Do not allow folders to be picked.")] + public bool DisableFolderSelect { get; set; } - [ConfigurationField("startNodeId", "Start node", "mediapicker")] - public Udi? StartNodeId { get; set; } + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi? StartNodeId { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs index a3dbbc04d7..62e9eac439 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs @@ -1,49 +1,46 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the media picker value editor. +/// +public class MediaPickerConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the media picker value editor. - /// - public class MediaPickerConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MediaPickerConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MediaPickerConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + } - /// - /// Initializes a new instance of the class. - /// - public MediaPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - // configure fields - // this is not part of ContentPickerConfiguration, - // but is required to configure the UI editor (when editing the configuration) - Field(nameof(MediaPickerConfiguration.StartNodeId)) - .Config = new Dictionary { { "idType", "udi" } }; - } + /// + /// Initializes a new instance of the class. + /// + public MediaPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) => - public override IDictionary ToValueEditor(object? configuration) - { - // get the configuration fields - var d = base.ToValueEditor(configuration); + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + Field(nameof(MediaPickerConfiguration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; - // add extra fields - // not part of ContentPickerConfiguration but used to configure the UI editor - d["idType"] = "udi"; + public override IDictionary ToValueEditor(object? configuration) + { + // get the configuration fields + IDictionary d = base.ToValueEditor(configuration); - return d; - } + // add extra fields + // not part of ContentPickerConfiguration but used to configure the UI editor + d["idType"] = "udi"; + + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs index a58203c7b5..360ba1b023 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs @@ -1,34 +1,32 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors -{ - public class MediaUrlGeneratorCollection : BuilderCollectionBase - { - public MediaUrlGeneratorCollection(Func> items) - : base(items) - { } +namespace Umbraco.Cms.Core.PropertyEditors; - public bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath) +public class MediaUrlGeneratorCollection : BuilderCollectionBase +{ + public MediaUrlGeneratorCollection(Func> items) + : base(items) + { + } + + public bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath) + { + // We can't get a media path from a null value + // The value will be null when uploading a brand new image, since we try to get the "old path" which doesn't exist yet + if (value is not null) { - // We can't get a media path from a null value - // The value will be null when uploading a brand new image, since we try to get the "old path" which doesn't exist yet - if (value is not null) + foreach (IMediaUrlGenerator generator in this) { - foreach (IMediaUrlGenerator generator in this) + if (generator.TryGetMediaPath(propertyEditorAlias, value, out var generatorMediaPath)) { - if (generator.TryGetMediaPath(propertyEditorAlias, value, out var generatorMediaPath)) - { - mediaPath = generatorMediaPath; - return true; - } + mediaPath = generatorMediaPath; + return true; } } - - mediaPath = null; - return false; } + + mediaPath = null; + return false; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs index 57ab93832b..0c9bf6070f 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs @@ -1,10 +1,9 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class MediaUrlGeneratorCollectionBuilder : SetCollectionBuilderBase { - public class MediaUrlGeneratorCollectionBuilder : SetCollectionBuilderBase - { - protected override MediaUrlGeneratorCollectionBuilder This => this; - } + protected override MediaUrlGeneratorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs index cccf0fe2b7..e839c0b527 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -1,23 +1,16 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +[DataEditor( + Constants.PropertyEditors.Aliases.MemberGroupPicker, + "Member Group Picker", + "membergrouppicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.MemberGroup)] +public class MemberGroupPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.MemberGroupPicker, - "Member Group Picker", - "membergrouppicker", - ValueType = ValueTypes.Text, - Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.MemberGroup)] - public class MemberGroupPickerPropertyEditor : DataEditor - { - public MemberGroupPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } - } + public MemberGroupPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + SupportsReadOnly = true; } diff --git a/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs index 6d6fb3a8b7..dc0ab648df 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs @@ -1,12 +1,7 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +public class MemberPickerConfiguration : ConfigurationEditor { - public class MemberPickerConfiguration : ConfigurationEditor - { - public override IDictionary DefaultConfiguration => new Dictionary - { - { "idType", "udi" } - }; - } + public override IDictionary DefaultConfiguration => + new Dictionary { { "idType", "udi" } }; } diff --git a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs index d348d6f22e..241736737e 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs @@ -1,25 +1,18 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +[DataEditor( + Constants.PropertyEditors.Aliases.MemberPicker, + "Member Picker", + "memberpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.Member)] +public class MemberPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.MemberPicker, - "Member Picker", - "memberpicker", - ValueType = ValueTypes.String, - Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.Member)] - public class MemberPickerPropertyEditor : DataEditor - { - public MemberPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } + public MemberPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + SupportsReadOnly = true; - protected override IConfigurationEditor CreateConfigurationEditor() => new MemberPickerConfiguration(); - } + protected override IConfigurationEditor CreateConfigurationEditor() => new MemberPickerConfiguration(); } diff --git a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs index ba1d03e7bb..c256c7b483 100644 --- a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs @@ -1,43 +1,32 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a temporary representation of an editor for cases where a data type is created but not editor is +/// available. +/// +public class MissingPropertyEditor : IDataEditor { - /// - /// Represents a temporary representation of an editor for cases where a data type is created but not editor is available. - /// - public class MissingPropertyEditor : IDataEditor - { - public string Alias => "Umbraco.Missing"; + public string Alias => "Umbraco.Missing"; - public EditorType Type => EditorType.Nothing; + public EditorType Type => EditorType.Nothing; - public string Name => "Missing property editor"; + public string Name => "Missing property editor"; - public string Icon => string.Empty; + public string Icon => string.Empty; - public string Group => string.Empty; + public string Group => string.Empty; - public bool IsDeprecated => false; + public bool IsDeprecated => false; - public IDictionary DefaultConfiguration => throw new NotImplementedException(); + public IDictionary DefaultConfiguration => throw new NotImplementedException(); - public IPropertyIndexValueFactory PropertyIndexValueFactory => throw new NotImplementedException(); + public IPropertyIndexValueFactory PropertyIndexValueFactory => throw new NotImplementedException(); - public IConfigurationEditor GetConfigurationEditor() - { - return new ConfigurationEditor(); - } + public IConfigurationEditor GetConfigurationEditor() => new ConfigurationEditor(); - public IDataValueEditor GetValueEditor() - { - throw new NotImplementedException(); - } + public IDataValueEditor GetValueEditor() => throw new NotImplementedException(); - public IDataValueEditor GetValueEditor(object? configuration) - { - throw new NotImplementedException(); - } - } + public IDataValueEditor GetValueEditor(object? configuration) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs index 2825b5b8af..c1ca368c47 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs @@ -1,28 +1,29 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the multinode picker value editor. +/// +public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the multinode picker value editor. - /// - public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("startNode", "Node type", "treesource")] - public MultiNodePickerConfigurationTreeSource? TreeSource { get; set; } + [ConfigurationField("startNode", "Node type", "treesource")] + public MultiNodePickerConfigurationTreeSource? TreeSource { get; set; } - [ConfigurationField("filter", "Allow items of type", "treesourcetypepicker", Description = "Select the applicable types")] - public string? Filter { get; set; } + [ConfigurationField("filter", "Allow items of type", "treesourcetypepicker", Description = "Select the applicable types")] + public string? Filter { get; set; } - [ConfigurationField("minNumber", "Minimum number of items", "number")] - public int MinNumber { get; set; } + [ConfigurationField("minNumber", "Minimum number of items", "number")] + public int MinNumber { get; set; } - [ConfigurationField("maxNumber", "Maximum number of items", "number")] - public int MaxNumber { get; set; } + [ConfigurationField("maxNumber", "Maximum number of items", "number")] + public int MaxNumber { get; set; } - [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] - public bool ShowOpen { get; set; } + [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] + public bool ShowOpen { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs index aa66be9d39..a377dae5db 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs @@ -1,53 +1,49 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the multinode picker value editor. +/// +public class MultiNodePickerConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration for the multinode picker value editor. - /// - public class MultiNodePickerConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MultiNodePickerConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MultiNodePickerConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + } - public MultiNodePickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - Field(nameof(MultiNodePickerConfiguration.TreeSource)) - .Config = new Dictionary { { "idType", "udi" } }; - } + public MultiNodePickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) => + Field(nameof(MultiNodePickerConfiguration.TreeSource)) + .Config = new Dictionary { { "idType", "udi" } }; - /// - public override Dictionary ToConfigurationEditor(MultiNodePickerConfiguration? configuration) - { - // sanitize configuration - var output = base.ToConfigurationEditor(configuration); + /// + public override Dictionary ToConfigurationEditor(MultiNodePickerConfiguration? configuration) + { + // sanitize configuration + Dictionary output = base.ToConfigurationEditor(configuration); - output["multiPicker"] = configuration?.MaxNumber > 1; + output["multiPicker"] = configuration?.MaxNumber > 1; - return output; - } + return output; + } - /// - public override IDictionary ToValueEditor(object? configuration) - { - var d = base.ToValueEditor(configuration); - d["multiPicker"] = true; - d["showEditButton"] = false; - d["showPathOnHover"] = false; - d["idType"] = "udi"; - return d; - } + /// + public override IDictionary ToValueEditor(object? configuration) + { + IDictionary d = base.ToValueEditor(configuration); + d["multiPicker"] = true; + d["showEditButton"] = false; + d["showPathOnHover"] = false; + d["idType"] = "udi"; + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs index bc48bbdd54..2dcd0f6e93 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the 'startNode' value for the +/// +[DataContract] +public class MultiNodePickerConfigurationTreeSource { - /// - /// Represents the 'startNode' value for the - /// - [DataContract] - public class MultiNodePickerConfigurationTreeSource - { - [DataMember(Name = "type")] - public string? ObjectType { get; set; } + [DataMember(Name = "type")] + public string? ObjectType { get; set; } - [DataMember(Name = "query")] - public string? StartNodeQuery { get; set; } + [DataMember(Name = "query")] + public string? StartNodeQuery { get; set; } - [DataMember(Name = "id")] - public Udi? StartNodeId { get; set; } - } + [DataMember(Name = "id")] + public Udi? StartNodeId { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs index caf933e6ad..35d51cb944 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class MultiUrlPickerConfiguration : IIgnoreUserStartNodesConfig { + [ConfigurationField("minNumber", "Minimum number of items", "number")] + public int MinNumber { get; set; } - public class MultiUrlPickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("minNumber", "Minimum number of items", "number")] - public int MinNumber { get; set; } + [ConfigurationField("maxNumber", "Maximum number of items", "number")] + public int MaxNumber { get; set; } - [ConfigurationField("maxNumber", "Maximum number of items", "number")] - public int MaxNumber { get; set; } + [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay.")] + public string? OverlaySize { get; set; } - [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay.")] - public string? OverlaySize { get; set; } + [ConfigurationField( + "hideAnchor", + "Hide anchor/query string input", + "boolean", + Description = "Selecting this hides the anchor/query string input field in the linkpicker overlay.")] + public bool HideAnchor { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore user start nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - - [ConfigurationField("hideAnchor", - "Hide anchor/query string input", "boolean", - Description = "Selecting this hides the anchor/query string input field in the linkpicker overlay.")] - public bool HideAnchor { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore user start nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs index f5baa18c04..f85cafa817 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs @@ -1,26 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class MultiUrlPickerConfigurationEditor : ConfigurationEditor { - public class MultiUrlPickerConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + } - { - } - - public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } + public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs index 506b3bebc9..6c7f93374d 100644 --- a/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs @@ -1,14 +1,12 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for a multiple textstring value editor. +/// +public class MultipleTextStringConfiguration { - /// - /// Represents the configuration for a multiple textstring value editor. - /// - public class MultipleTextStringConfiguration - { - // fields are configured in the editor + // fields are configured in the editor + public int Minimum { get; set; } - public int Minimum { get; set; } - - public int Maximum {get; set; } - } + public int Maximum { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs b/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs index aed6b5cd00..fdef902b58 100644 --- a/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs @@ -1,43 +1,43 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the nested content value editor. +/// +public class NestedContentConfiguration { + [ConfigurationField("contentTypes", "Element Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the Element Types to use as models for the items.")] + public ContentType[]? ContentTypes { get; set; } - /// - /// Represents the configuration for the nested content value editor. - /// - public class NestedContentConfiguration + [ConfigurationField("minItems", "Min Items", "number", Description = "Minimum number of items allowed.")] + public int? MinItems { get; set; } + + [ConfigurationField("maxItems", "Max Items", "number", Description = "Maximum number of items allowed.")] + public int? MaxItems { get; set; } + + [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Requires editor confirmation for delete actions.")] + public bool ConfirmDeletes { get; set; } = true; + + [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Show the Element Type icons.")] + public bool ShowIcons { get; set; } = true; + + [ConfigurationField("expandsOnLoad", "Expands on load", "boolean", Description = "A single item is automatically expanded")] + public bool ExpandsOnLoad { get; set; } = true; + + [ConfigurationField("hideLabel", "Hide Label", "boolean", Description = "Hide the property label and let the item list span the full width of the editor window.")] + public bool HideLabel { get; set; } + + [DataContract] + public class ContentType { - [ConfigurationField("contentTypes", "Element Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the Element Types to use as models for the items.")] - public ContentType[]? ContentTypes { get; set; } + [DataMember(Name = "ncAlias")] + public string? Alias { get; set; } - [ConfigurationField("minItems", "Min Items", "number", Description = "Minimum number of items allowed.")] - public int? MinItems { get; set; } + [DataMember(Name = "ncTabAlias")] + public string? TabAlias { get; set; } - [ConfigurationField("maxItems", "Max Items", "number", Description = "Maximum number of items allowed.")] - public int? MaxItems { get; set; } - - [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Requires editor confirmation for delete actions.")] - public bool ConfirmDeletes { get; set; } = true; - - [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Show the Element Type icons.")] - public bool ShowIcons { get; set; } = true; - - [ConfigurationField("hideLabel", "Hide Label", "boolean", Description = "Hide the property label and let the item list span the full width of the editor window.")] - public bool HideLabel { get; set; } - - - [DataContract] - public class ContentType - { - [DataMember(Name = "ncAlias")] - public string? Alias { get; set; } - - [DataMember(Name = "ncTabAlias")] - public string? TabAlias { get; set; } - - [DataMember(Name = "nameTemplate")] - public string? Template { get; set; } - } + [DataMember(Name = "nameTemplate")] + public string? Template { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs index bab2038d2d..5adb06b42f 100644 --- a/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs @@ -1,28 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the nested content value editor. - /// - public class NestedContentConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public NestedContentConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public NestedContentConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the nested content value editor. +/// +public class NestedContentConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public NestedContentConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public NestedContentConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs index 7e91d8e3ee..f1d295bc3d 100644 --- a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Default implementation for which does not compress any property +/// data +/// +public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - /// - /// Default implementation for which does not compress any property data - /// - public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions - { - public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) => false; - } + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) => false; } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs index c58c962df4..eec435ddf6 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs @@ -1,25 +1,24 @@ -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ParameterEditorCollection : BuilderCollectionBase { - public class ParameterEditorCollection : BuilderCollectionBase + public ParameterEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) + : base(() => dataEditors + .Where(x => (x.Type & EditorType.MacroParameter) > 0) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { - public ParameterEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) - : base(() => dataEditors - .Where(x => (x.Type & EditorType.MacroParameter) > 0) - .Union(manifestParser.CombinedManifest.PropertyEditors)) - { } + } - // note: virtual so it can be mocked - public virtual IDataEditor? this[string alias] - => this.SingleOrDefault(x => x.Alias == alias); + // note: virtual so it can be mocked + public virtual IDataEditor? this[string alias] + => this.SingleOrDefault(x => x.Alias == alias); - public virtual bool TryGet(string alias, out IDataEditor? editor) - { - editor = this.FirstOrDefault(x => x.Alias == alias); - return editor != null; - } + public virtual bool TryGet(string alias, out IDataEditor? editor) + { + editor = this.FirstOrDefault(x => x.Alias == alias); + return editor != null; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs index c7d8067fff..a283b33747 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs @@ -1,31 +1,25 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +/// +/// Represents a content type parameter editor. +/// +[DataEditor( + "contentType", + EditorType.MacroParameter, + "Content Type Picker", + "entitypicker")] +public class ContentTypeParameterEditor : DataEditor { /// - /// Represents a content type parameter editor. + /// Initializes a new instance of the class. /// - [DataEditor( - "contentType", - EditorType.MacroParameter, - "Content Type Picker", - "entitypicker")] - public class ContentTypeParameterEditor : DataEditor + public ContentTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - public ContentTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", false); - DefaultConfiguration.Add("entityType", "DocumentType"); - } + // configure + DefaultConfiguration.Add("multiple", false); + DefaultConfiguration.Add("entityType", "DocumentType"); + SupportsReadOnly = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs index 65056c75ce..b5e7adb670 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs @@ -1,44 +1,53 @@ -using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; + +/// +/// Represents a parameter editor of some sort. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.MultiNodeTreePicker, + EditorType.MacroParameter, + "Multiple Content Picker", + "contentpicker")] +public class MultipleContentPickerParameterEditor : DataEditor { /// - /// Represents a parameter editor of some sort. + /// Initializes a new instance of the class. /// - [DataEditor( - Constants.PropertyEditors.Aliases.MultiNodeTreePicker, - EditorType.MacroParameter, - "Multiple Content Picker", - "contentpicker")] - public class MultipleContentPickerParameterEditor : DataEditor + public MultipleContentPickerParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - public MultipleContentPickerParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) + // configure + DefaultConfiguration.Add("multiPicker", "1"); + DefaultConfiguration.Add("minNumber", 0); + DefaultConfiguration.Add("maxNumber", 0); + SupportsReadOnly = true; + } + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase + { + public MultipleContentPickerParamateterValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IEntityService entityService) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) { - // configure - DefaultConfiguration.Add("multiPicker", "1"); - DefaultConfiguration.Add("minNumber",0 ); - DefaultConfiguration.Add("maxNumber", 0); } - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + public override string UdiEntityType { get; } = Constants.UdiEntityType.Document; - internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase - { - public MultipleContentPickerParamateterValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) - { - } - - public override string UdiEntityType { get; } = Constants.UdiEntityType.Document; - public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Document; - } + public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Document; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs index 01bae2ada2..d024ba839f 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs @@ -1,25 +1,19 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "contentTypeMultiple", + EditorType.MacroParameter, + "Multiple Content Type Picker", + "entitypicker")] +public class MultipleContentTypeParameterEditor : DataEditor { - [DataEditor( - "contentTypeMultiple", - EditorType.MacroParameter, - "Multiple Content Type Picker", - "entitypicker")] - public class MultipleContentTypeParameterEditor : DataEditor + public MultipleContentTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MultipleContentTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", true); - DefaultConfiguration.Add("entityType", "DocumentType"); - } + // configure + DefaultConfiguration.Add("multiple", true); + DefaultConfiguration.Add("entityType", "DocumentType"); + SupportsReadOnly = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs index 4a6bab528c..a60ff0ff0d 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs @@ -1,46 +1,51 @@ -using System; -using System.Collections.Generic; -using System.Reflection.Metadata; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; + +/// +/// Represents a multiple media picker macro parameter editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.MultipleMediaPicker, + EditorType.MacroParameter, + "Multiple Media Picker", + "mediapicker", + ValueType = ValueTypes.Text)] +public class MultipleMediaPickerParameterEditor : DataEditor { /// - /// Represents a multiple media picker macro parameter editor. + /// Initializes a new instance of the class. /// - [DataEditor( - Constants.PropertyEditors.Aliases.MultipleMediaPicker, - EditorType.MacroParameter, - "Multiple Media Picker", - "mediapicker", - ValueType = ValueTypes.Text)] - public class MultipleMediaPickerParameterEditor : DataEditor + public MultipleMediaPickerParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - public MultipleMediaPickerParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) + DefaultConfiguration.Add("multiPicker", "1"); + SupportsReadOnly = true; + } + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + internal class MultipleMediaPickerPropertyValueEditor : MultiplePickerParamateterValueEditorBase + { + public MultipleMediaPickerPropertyValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IEntityService entityService) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) { - DefaultConfiguration.Add("multiPicker", "1"); } - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + public override string UdiEntityType { get; } = Constants.UdiEntityType.Media; - internal class MultipleMediaPickerPropertyValueEditor : MultiplePickerParamateterValueEditorBase - { - public MultipleMediaPickerPropertyValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) - { - } - - public override string UdiEntityType { get; } = Constants.UdiEntityType.Media; - public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Media; - } + public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Media; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs index 5182c1fbd2..8aaea32ab4 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -7,53 +5,51 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors -{ - internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference - { - private readonly IEntityService _entityService; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; - public MultiplePickerParamateterValueEditorBase( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute, - IEntityService entityService) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) +internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference +{ + private readonly IEntityService _entityService; + + public MultiplePickerParamateterValueEditorBase( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IEntityService entityService) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => + _entityService = entityService; + + public abstract string UdiEntityType { get; } + + public abstract UmbracoObjectTypes UmbracoObjectType { get; } + + public IEnumerable GetReferences(object? value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) { - _entityService = entityService; + yield break; } - public abstract string UdiEntityType { get; } - public abstract UmbracoObjectTypes UmbracoObjectType { get; } - public IEnumerable GetReferences(object? value) + foreach (var udiStr in asString.Split(',')) { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) + if (UdiParser.TryParse(udiStr, out Udi? udi)) { - yield break; + yield return new UmbracoEntityReference(udi); } - foreach (var udiStr in asString.Split(',')) + // this is needed to support the legacy case when the multiple media picker parameter editor stores ints not udis + if (int.TryParse(udiStr, out var id)) { - if (UdiParser.TryParse(udiStr, out Udi? udi)) + Attempt guidAttempt = _entityService.GetKey(id, UmbracoObjectType); + Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty; + + if (guid != Guid.Empty) { - yield return new UmbracoEntityReference(udi); - } - - // this is needed to support the legacy case when the multiple media picker parameter editor stores ints not udis - if (int.TryParse(udiStr, out var id)) - { - Attempt guidAttempt = _entityService.GetKey(id, UmbracoObjectType); - Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty; - - if (guid != Guid.Empty) - { - yield return new UmbracoEntityReference(new GuidUdi(Constants.UdiEntityType.Media, guid)); - } - + yield return new UmbracoEntityReference(new GuidUdi(Constants.UdiEntityType.Media, guid)); } } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs index d39f792971..0a6c6d4fe5 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs @@ -1,27 +1,22 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "tabPickerMultiple", + EditorType.MacroParameter, + "Multiple Tab Picker", + "entitypicker")] +public class MultiplePropertyGroupParameterEditor : DataEditor { - [DataEditor( - "tabPickerMultiple", - EditorType.MacroParameter, - "Multiple Tab Picker", - "entitypicker")] - public class MultiplePropertyGroupParameterEditor : DataEditor + public MultiplePropertyGroupParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MultiplePropertyGroupParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", true); - DefaultConfiguration.Add("entityType", "PropertyGroup"); - //don't publish the id for a property group, publish its alias, which is actually just its lower cased name - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", true); + DefaultConfiguration.Add("entityType", "PropertyGroup"); + + // don't publish the id for a property group, publish its alias, which is actually just its lower cased name + DefaultConfiguration.Add("publishBy", "alias"); + SupportsReadOnly = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs index 64e310551b..3fc2c31d18 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs @@ -1,27 +1,22 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "propertyTypePickerMultiple", + EditorType.MacroParameter, + "Multiple Property Type Picker", + "entitypicker")] +public class MultiplePropertyTypeParameterEditor : DataEditor { - [DataEditor( - "propertyTypePickerMultiple", - EditorType.MacroParameter, - "Multiple Property Type Picker", - "entitypicker")] - public class MultiplePropertyTypeParameterEditor : DataEditor + public MultiplePropertyTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MultiplePropertyTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", "1"); - DefaultConfiguration.Add("entityType", "PropertyType"); - //don't publish the id for a property type, publish its alias - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", "1"); + DefaultConfiguration.Add("entityType", "PropertyType"); + + // don't publish the id for a property type, publish its alias + DefaultConfiguration.Add("publishBy", "alias"); + SupportsReadOnly = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs index 6441e8cb24..dd642dd38f 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs @@ -1,27 +1,22 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "tabPicker", + EditorType.MacroParameter, + "Tab Picker", + "entitypicker")] +public class PropertyGroupParameterEditor : DataEditor { - [DataEditor( - "tabPicker", - EditorType.MacroParameter, - "Tab Picker", - "entitypicker")] - public class PropertyGroupParameterEditor : DataEditor + public PropertyGroupParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public PropertyGroupParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", "0"); - DefaultConfiguration.Add("entityType", "PropertyGroup"); - //don't publish the id for a property group, publish it's alias (which is actually just it's lower cased name) - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", "0"); + DefaultConfiguration.Add("entityType", "PropertyGroup"); + + // don't publish the id for a property group, publish it's alias (which is actually just it's lower cased name) + DefaultConfiguration.Add("publishBy", "alias"); + SupportsReadOnly = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs index 9e253d4e41..8d7f252615 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs @@ -1,27 +1,22 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "propertyTypePicker", + EditorType.MacroParameter, + "Property Type Picker", + "entitypicker")] +public class PropertyTypeParameterEditor : DataEditor { - [DataEditor( - "propertyTypePicker", - EditorType.MacroParameter, - "Property Type Picker", - "entitypicker")] - public class PropertyTypeParameterEditor : DataEditor + public PropertyTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public PropertyTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", "0"); - DefaultConfiguration.Add("entityType", "PropertyType"); - //don't publish the id for a property type, publish its alias - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", "0"); + DefaultConfiguration.Add("entityType", "PropertyType"); + + // don't publish the id for a property type, publish its alias + DefaultConfiguration.Add("publishBy", "alias"); + SupportsReadOnly = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs index ac275c46e3..75342371a4 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -1,53 +1,56 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Compresses property data based on config +/// +public class PropertyCacheCompression : IPropertyCacheCompression { + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly IReadOnlyDictionary _contentTypes; - /// - /// Compresses property data based on config - /// - public class PropertyCacheCompression : IPropertyCacheCompression + private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias, bool published), bool> + _isCompressedCache; + + private readonly PropertyEditorCollection _propertyEditors; + + public PropertyCacheCompression( + IPropertyCacheCompressionOptions compressionOptions, + IReadOnlyDictionary contentTypes, + PropertyEditorCollection propertyEditors, + ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache) { - private readonly IPropertyCacheCompressionOptions _compressionOptions; - private readonly IReadOnlyDictionary _contentTypes; - private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias, bool published), bool> _isCompressedCache; + _compressionOptions = compressionOptions; + _contentTypes = contentTypes ?? throw new ArgumentNullException(nameof(contentTypes)); + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + _isCompressedCache = compressedStoragePropertyEditorCache; + } - public PropertyCacheCompression( - IPropertyCacheCompressionOptions compressionOptions, - IReadOnlyDictionary contentTypes, - PropertyEditorCollection propertyEditors, - ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache) + public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) + { + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => { - _compressionOptions = compressionOptions; - _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); - _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); - _isCompressedCache = compressedStoragePropertyEditorCache; - } - - public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) - { - var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => + if (!_contentTypes.TryGetValue(x.contentTypeId, out IContentTypeComposition? ct)) { - if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct)) - return false; + return false; + } - var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); - if (propertyType == null) - return false; + IPropertyType? propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); + if (propertyType == null) + { + return false; + } - if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) - return false; + if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out IDataEditor? propertyEditor)) + { + return false; + } - return _compressionOptions.IsCompressed(content, propertyType, propertyEditor!, published); - }); + return _compressionOptions.IsCompressed(content, propertyType, propertyEditor, published); + }); - return compressedStorage; - } + return compressedStorage; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs index 9c94008616..c835c0ae95 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs @@ -1,39 +1,40 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Specifies the level of cache for a property value. +/// +public enum PropertyCacheLevel { /// - /// Specifies the level of cache for a property value. + /// Default value. /// - public enum PropertyCacheLevel - { - /// - /// Default value. - /// - Unknown = 0, + Unknown = 0, - /// - /// Indicates that the property value can be cached at the element level, i.e. it can be - /// cached until the element itself is modified. - /// - Element = 1, + /// + /// Indicates that the property value can be cached at the element level, i.e. it can be + /// cached until the element itself is modified. + /// + Element = 1, - /// - /// Indicates that the property value can be cached at the elements level, i.e. it can - /// be cached until any element is modified. - /// - Elements = 2, + /// + /// Indicates that the property value can be cached at the elements level, i.e. it can + /// be cached until any element is modified. + /// + Elements = 2, - /// - /// Indicates that the property value can be cached at the snapshot level, i.e. it can be - /// cached for the duration of the current snapshot. - /// - /// In most cases, a snapshot is created per request, and therefore this is - /// equivalent to cache the value for the duration of the request. - Snapshot = 3, + /// + /// Indicates that the property value can be cached at the snapshot level, i.e. it can be + /// cached for the duration of the current snapshot. + /// + /// + /// In most cases, a snapshot is created per request, and therefore this is + /// equivalent to cache the value for the duration of the request. + /// + Snapshot = 3, - /// - /// Indicates that the property value cannot be cached and has to be converted each time - /// it is requested. - /// - None = 4 - } + /// + /// Indicates that the property value cannot be cached and has to be converted each time + /// it is requested. + /// + None = 4, } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs index 34f72cf5c0..ff700431d5 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs @@ -1,31 +1,31 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class PropertyEditorCollection : BuilderCollectionBase { - public class PropertyEditorCollection : BuilderCollectionBase + public PropertyEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) + : base(() => dataEditors + .Where(x => (x.Type & EditorType.PropertyValue) > 0) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { - public PropertyEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) - : base(() => dataEditors - .Where(x => (x.Type & EditorType.PropertyValue) > 0) - .Union(manifestParser.CombinedManifest.PropertyEditors)) - { } + } - public PropertyEditorCollection(DataEditorCollection dataEditors) - : base(() => dataEditors - .Where(x => (x.Type & EditorType.PropertyValue) > 0)) - { } + public PropertyEditorCollection(DataEditorCollection dataEditors) + : base(() => dataEditors + .Where(x => (x.Type & EditorType.PropertyValue) > 0)) + { + } - // note: virtual so it can be mocked - public virtual IDataEditor? this[string? alias] - => this.SingleOrDefault(x => x.Alias == alias); + // note: virtual so it can be mocked + public virtual IDataEditor? this[string? alias] + => this.SingleOrDefault(x => x.Alias == alias); - public virtual bool TryGet(string? alias, [MaybeNullWhen(false)] out IDataEditor editor) - { - editor = this.FirstOrDefault(x => x.Alias == alias); - return editor != null; - } + public virtual bool TryGet(string? alias, [MaybeNullWhen(false)] out IDataEditor editor) + { + editor = this.FirstOrDefault(x => x.Alias == alias); + return editor != null; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs index fa57956cdd..ff92c2012f 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs @@ -1,22 +1,21 @@ -using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the interface to manage tags. +/// +public static class PropertyEditorTagsExtensions { /// - /// Provides extension methods for the interface to manage tags. + /// Determines whether an editor supports tags. /// - public static class PropertyEditorTagsExtensions - { - /// - /// Determines whether an editor supports tags. - /// - public static bool IsTagsEditor(this IDataEditor editor) - => editor.GetTagAttribute() != null; + public static bool IsTagsEditor(this IDataEditor editor) + => editor.GetTagAttribute() != null; - /// - /// Gets the tags configuration attribute of an editor. - /// - public static TagsPropertyEditorAttribute? GetTagAttribute(this IDataEditor? editor) - => editor?.GetType().GetCustomAttribute(false); - } + /// + /// Gets the tags configuration attribute of an editor. + /// + public static TagsPropertyEditorAttribute? GetTagAttribute(this IDataEditor? editor) + => editor?.GetType().GetCustomAttribute(false); } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs index 172c65502e..2e26894d23 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs @@ -1,54 +1,60 @@ -using System; -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Provides a default implementation for . +/// +/// +public abstract class PropertyValueConverterBase : IPropertyValueConverter { - /// - /// Provides a default implementation for . - /// - /// - public abstract class PropertyValueConverterBase : IPropertyValueConverter + /// + public virtual bool IsConverter(IPublishedPropertyType propertyType) + => false; + + /// + public virtual bool? IsValue(object? value, PropertyValueLevel level) { - /// - public virtual bool IsConverter(IPublishedPropertyType propertyType) - => false; - - /// - public virtual bool? IsValue(object? value, PropertyValueLevel level) + switch (level) { - switch (level) - { - case PropertyValueLevel.Source: - // the default implementation uses the old magic null & string comparisons, - // other implementations may be more clever, and/or test the final converted object values - return value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)); - case PropertyValueLevel.Inter: - return null; - case PropertyValueLevel.Object: - return null; - default: - throw new NotSupportedException($"Invalid level: {level}."); - } + case PropertyValueLevel.Source: + // the default implementation uses the old magic null & string comparisons, + // other implementations may be more clever, and/or test the final converted object values + return value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)); + case PropertyValueLevel.Inter: + return null; + case PropertyValueLevel.Object: + return null; + default: + throw new NotSupportedException($"Invalid level: {level}."); } + } - /// - public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(object); + /// + public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(object); - /// - public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; + /// + public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; - /// - public virtual object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - => source; + /// + public virtual object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source; - /// - public virtual object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - => inter; + /// + public virtual object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + => inter; - /// - public virtual object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - => inter?.ToString() ?? string.Empty; + /// + public virtual object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + => inter?.ToString() ?? string.Empty; + + [Obsolete( + "This method is not part of the IPropertyValueConverter contract, therefore not used and will be removed in future versions; use IsValue instead.")] + public virtual bool HasValue(IPublishedProperty property, string culture, string segment) + { + var value = property.GetSourceValue(culture, segment); + return value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)); } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs index 9214f10482..20eb9ae4c4 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs @@ -1,47 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class PropertyValueConverterCollection : BuilderCollectionBase { - public class PropertyValueConverterCollection : BuilderCollectionBase + private readonly object _locker = new(); + private Dictionary? _defaultConverters; + + public PropertyValueConverterCollection(Func> items) + : base(items) { - public PropertyValueConverterCollection(Func> items) : base(items) - { - } + } - private readonly object _locker = new object(); - private Dictionary? _defaultConverters; - - private Dictionary DefaultConverters + private Dictionary DefaultConverters + { + get { - get + lock (_locker) { - lock (_locker) + if (_defaultConverters != null) { - if (_defaultConverters != null) - return _defaultConverters; - - _defaultConverters = new Dictionary(); - - foreach (var converter in this) - { - var attr = converter.GetType().GetCustomAttribute(false); - if (attr != null) - _defaultConverters[converter] = attr.DefaultConvertersToShadow; - } - return _defaultConverters; } + + _defaultConverters = new Dictionary(); + + foreach (IPropertyValueConverter converter in this) + { + DefaultPropertyValueConverterAttribute? attr = converter.GetType().GetCustomAttribute(false); + if (attr != null) + { + _defaultConverters[converter] = attr.DefaultConvertersToShadow; + } + } + + return _defaultConverters; } } - - internal bool IsDefault(IPropertyValueConverter converter) - => DefaultConverters.ContainsKey(converter); - - internal bool Shadows(IPropertyValueConverter shadowing, IPropertyValueConverter shadowed) - => DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowed.GetType()); } + + internal bool IsDefault(IPropertyValueConverter converter) + => DefaultConverters.ContainsKey(converter); + + internal bool Shadows(IPropertyValueConverter shadowing, IPropertyValueConverter shadowed) + => DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowed.GetType()); } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs index f7bbca2b02..6d1e329c7e 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class PropertyValueConverterCollectionBuilder : OrderedCollectionBuilderBase { - public class PropertyValueConverterCollectionBuilder : OrderedCollectionBuilderBase - { - protected override PropertyValueConverterCollectionBuilder This => this; - } + protected override PropertyValueConverterCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs index 583bf87f3e..52389fb92a 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Indicates the level of a value. +/// +public enum PropertyValueLevel { /// - /// Indicates the level of a value. + /// The source value, i.e. what is in the database. /// - public enum PropertyValueLevel - { - /// - /// The source value, i.e. what is in the database. - /// - Source, + Source, - /// - /// The conversion intermediate value. - /// - Inter, + /// + /// The conversion intermediate value. + /// + Inter, - /// - /// The converted value. - /// - Object - } + /// + /// The converted value. + /// + Object, } diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs index e0bbae88b5..6a80144d0d 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs @@ -1,27 +1,27 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the rich text value editor. +/// +public class RichTextConfiguration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the rich text value editor. - /// - public class RichTextConfiguration : IIgnoreUserStartNodesConfig - { - // TODO: Make these strongly typed, for now this works though - [ConfigurationField("editor", "Editor", "views/propertyeditors/rte/rte.prevalues.html", HideLabel = true)] - public object? Editor { get; set; } + // TODO: Make these strongly typed, for now this works though + [ConfigurationField("editor", "Editor", "views/propertyeditors/rte/rte.prevalues.html", HideLabel = true)] + public object? Editor { get; set; } - [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] - public string? OverlaySize { get; set; } + [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] + public string? OverlaySize { get; set; } - [ConfigurationField("hideLabel", "Hide Label", "boolean")] - public bool HideLabel { get; set; } + [ConfigurationField("hideLabel", "Hide Label", "boolean")] + public bool HideLabel { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } + [ConfigurationField("mediaParentId", "Image Upload Folder", "mediafolderpicker", Description = "Choose the upload location of pasted images")] + public GuidUdi? MediaParentId { get; set; } - [ConfigurationField("mediaParentId", "Image Upload Folder", "mediafolderpicker", - Description = "Choose the upload location of pasted images")] - public GuidUdi? MediaParentId { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs index a967ec2367..4e0b5b557d 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the rich text value editor. - /// - public class RichTextConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public RichTextConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public RichTextConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the rich text value editor. +/// +public class RichTextConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public RichTextConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public RichTextConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs index 8d41873a11..709fb3ce9f 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the slider value editor. +/// +public class SliderConfiguration { - /// - /// Represents the configuration for the slider value editor. - /// - public class SliderConfiguration - { - [ConfigurationField("enableRange", "Enable range", "boolean")] - public bool EnableRange { get; set; } + [ConfigurationField("enableRange", "Enable range", "boolean")] + public bool EnableRange { get; set; } - [ConfigurationField("initVal1", "Initial value", "number")] - public decimal InitialValue { get; set; } + [ConfigurationField("initVal1", "Initial value", "number")] + public decimal InitialValue { get; set; } - [ConfigurationField("initVal2", "Initial value 2", "number", Description = "Used when range is enabled")] - public decimal InitialValue2 { get; set; } + [ConfigurationField("initVal2", "Initial value 2", "number", Description = "Used when range is enabled")] + public decimal InitialValue2 { get; set; } - [ConfigurationField("minVal", "Minimum value", "number")] - public decimal MinimumValue { get; set; } + [ConfigurationField("minVal", "Minimum value", "number")] + public decimal MinimumValue { get; set; } - [ConfigurationField("maxVal", "Maximum value", "number")] - public decimal MaximumValue { get; set; } + [ConfigurationField("maxVal", "Maximum value", "number")] + public decimal MaximumValue { get; set; } - [ConfigurationField("step", "Step increments", "number")] - public decimal StepIncrements { get; set; } - } + [ConfigurationField("step", "Step increments", "number")] + public decimal StepIncrements { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs index 6cd9db8399..586e4cd3af 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs @@ -1,28 +1,28 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the slider value editor. - /// - public class SliderConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public SliderConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public SliderConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the slider value editor. +/// +public class SliderConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public SliderConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public SliderConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base( + ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs index 61fa80472d..5a9808f227 100644 --- a/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs @@ -1,21 +1,22 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the tag value editor. +/// +public class TagConfiguration { - /// - /// Represents the configuration for the tag value editor. - /// - public class TagConfiguration - { - [ConfigurationField("group", "Tag group", "requiredfield", - Description = "Define a tag group")] - public string Group { get; set; } = "default"; + [ConfigurationField("group", "Tag group", "requiredfield", Description = "Define a tag group")] + public string Group { get; set; } = "default"; - [ConfigurationField("storageType", "Storage Type", "views/propertyeditors/tags/tags.prevalues.html", - Description = "Select whether to store the tags in cache as JSON (default) or as CSV. The only benefits of storage as JSON is that you are able to have commas in a tag value")] - public TagsStorageType StorageType { get; set; } = TagsStorageType.Json; + [ConfigurationField( + "storageType", + "Storage Type", + "views/propertyeditors/tags/tags.prevalues.html", + Description = "Select whether to store the tags in cache as JSON (default) or as CSV. The only benefits of storage as JSON is that you are able to have commas in a tag value")] + public TagsStorageType StorageType { get; set; } = TagsStorageType.Json; - // not a field - public char Delimiter { get; set; } - } + // not a field + public char Delimiter { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs index 2f77642e5f..f22f9b74c4 100644 --- a/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs @@ -1,8 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -10,51 +8,55 @@ using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the tag value editor. +/// +public class TagConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the tag value editor. - /// - public class TagConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService) + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService) : this(validators, ioHelper, localizedTextService, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + Field(nameof(TagConfiguration.Group)).Validators.Add(new RequiredValidator(localizedTextService)); + Field(nameof(TagConfiguration.StorageType)).Validators.Add(new RequiredValidator(localizedTextService)); + } + + public override Dictionary ToConfigurationEditor(TagConfiguration? configuration) + { + Dictionary dictionary = base.ToConfigurationEditor(configuration); + + // the front-end editor expects the string value of the storage type + if (!dictionary.TryGetValue("storageType", out var storageType)) { + storageType = TagsStorageType.Json; // default to Json } - public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) + dictionary["storageType"] = storageType.ToString()!; + + return dictionary; + } + + public override TagConfiguration? FromConfigurationEditor( + IDictionary? editorValues, + TagConfiguration? configuration) + { + // the front-end editor returns the string value of the storage type + // pure Json could do with + // [JsonConverter(typeof(StringEnumConverter))] + // but here we're only deserializing to object and it's too late + if (editorValues is not null) { - Field(nameof(TagConfiguration.Group)).Validators.Add(new RequiredValidator(localizedTextService)); - Field(nameof(TagConfiguration.StorageType)).Validators.Add(new RequiredValidator(localizedTextService)); + editorValues["storageType"] = Enum.Parse(typeof(TagsStorageType), (string)editorValues["storageType"]!); } - public override Dictionary ToConfigurationEditor(TagConfiguration? configuration) - { - var dictionary = base.ToConfigurationEditor(configuration); - - // the front-end editor expects the string value of the storage type - if (!dictionary.TryGetValue("storageType", out var storageType)) - storageType = TagsStorageType.Json; //default to Json - dictionary["storageType"] = storageType.ToString()!; - - return dictionary; - } - - public override TagConfiguration? FromConfigurationEditor(IDictionary? editorValues, TagConfiguration? configuration) - { - // the front-end editor returns the string value of the storage type - // pure Json could do with - // [JsonConverter(typeof(StringEnumConverter))] - // but here we're only deserializing to object and it's too late - - if (editorValues is not null) - { - editorValues["storageType"] = Enum.Parse(typeof(TagsStorageType), (string) editorValues["storageType"]!); - } - - return base.FromConfigurationEditor(editorValues, configuration); - } + return base.FromConfigurationEditor(editorValues, configuration); } } diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index c21ea09ac9..849d6446a9 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -1,61 +1,58 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Marks property editors that support tags. +/// +[AttributeUsage(AttributeTargets.Class)] +public class TagsPropertyEditorAttribute : Attribute { /// - /// Marks property editors that support tags. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Class)] - public class TagsPropertyEditorAttribute : Attribute + public TagsPropertyEditorAttribute(Type tagsConfigurationProvider) + : this() => + TagsConfigurationProviderType = tagsConfigurationProvider ?? + throw new ArgumentNullException(nameof(tagsConfigurationProvider)); + + /// + /// Initializes a new instance of the class. + /// + public TagsPropertyEditorAttribute() { - /// - /// Initializes a new instance of the class. - /// - public TagsPropertyEditorAttribute(Type tagsConfigurationProvider) - : this() - { - TagsConfigurationProviderType = tagsConfigurationProvider ?? throw new ArgumentNullException(nameof(tagsConfigurationProvider)); - } - - /// - /// Initializes a new instance of the class. - /// - public TagsPropertyEditorAttribute() - { - Delimiter = ','; - ReplaceTags = true; - TagGroup = "default"; - StorageType = TagsStorageType.Json; - } - - /// - /// Gets or sets a value indicating how tags are stored. - /// - public TagsStorageType StorageType { get; set; } - - /// - /// Gets or sets the delimited for delimited strings. - /// - /// Default is a comma. Has no meaning when tags are stored as Json. - public char Delimiter { get; set; } - - /// - /// Gets or sets a value indicating whether to replace the tags entirely. - /// - // TODO: what's the usage? - public bool ReplaceTags { get; set; } - - /// - /// Gets or sets the tags group. - /// - /// Default is "default". - public string TagGroup { get; set; } - - /// - /// Gets the type of the dynamic configuration provider. - /// - //TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 - public Type? TagsConfigurationProviderType { get; } + Delimiter = ','; + ReplaceTags = true; + TagGroup = "default"; + StorageType = TagsStorageType.Json; } + + /// + /// Gets or sets a value indicating how tags are stored. + /// + public TagsStorageType StorageType { get; set; } + + /// + /// Gets or sets the delimited for delimited strings. + /// + /// Default is a comma. Has no meaning when tags are stored as Json. + public char Delimiter { get; set; } + + /// + /// Gets or sets a value indicating whether to replace the tags entirely. + /// + // TODO: what's the usage? + public bool ReplaceTags { get; set; } + + /// + /// Gets or sets the tags group. + /// + /// Default is "default". + public string TagGroup { get; set; } + + /// + /// Gets the type of the dynamic configuration provider. + /// + // TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 + public Type? TagsConfigurationProviderType { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs index 86ca35ef64..8e6355258b 100644 --- a/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration for the textarea value editor. - /// - public class TextAreaConfiguration - { - [ConfigurationField("maxChars", "Maximum allowed characters", "number", Description = "If empty - no character limit")] - public int? MaxChars { get; set; } +namespace Umbraco.Cms.Core.PropertyEditors; - [ConfigurationField("rows", "Number of rows", "number", Description = "If empty - 10 rows would be set as the default value")] - public int? Rows { get; set; } - } +/// +/// Represents the configuration for the textarea value editor. +/// +public class TextAreaConfiguration +{ + [ConfigurationField("maxChars", "Maximum allowed characters", "number", Description = "If empty - no character limit")] + public int? MaxChars { get; set; } + + [ConfigurationField("rows", "Number of rows", "number", Description = "If empty - 10 rows would be set as the default value")] + public int? Rows { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs index 4fa4e7908c..7ae52825fb 100644 --- a/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the textarea value editor. - /// - public class TextAreaConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TextAreaConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public TextAreaConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the textarea value editor. +/// +public class TextAreaConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TextAreaConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TextAreaConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs index cb401cf92a..6a0995dccd 100644 --- a/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs @@ -1,56 +1,58 @@ -using System; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Custom value editor which ensures that the value stored is just plain text and that +/// no magic json formatting occurs when translating it to and from the database values +/// +public class TextOnlyValueEditor : DataValueEditor { - /// - /// Custom value editor which ensures that the value stored is just plain text and that - /// no magic json formatting occurs when translating it to and from the database values - /// - public class TextOnlyValueEditor : DataValueEditor + public TextOnlyValueEditor( + DataEditorAttribute attribute, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - public TextOnlyValueEditor( - DataEditorAttribute attribute, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) - { } + } - /// - /// A method used to format the database value to a value that can be used by the editor - /// - /// - /// - /// - /// - /// - /// The object returned will always be a string and if the database type is not a valid string type an exception is thrown - /// - public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + /// + /// A method used to format the database value to a value that can be used by the editor + /// + /// + /// + /// + /// + /// + /// The object returned will always be a string and if the database type is not a valid string type an exception is + /// thrown + /// + public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + { + var val = property.GetValue(culture, segment); + + if (val == null) { - var val = property.GetValue(culture, segment); - - if (val == null) return string.Empty; - - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Ntext: - case ValueStorageType.Nvarchar: - return val.ToString() ?? string.Empty; - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - case ValueStorageType.Date: - default: - throw new InvalidOperationException("The " + typeof(TextOnlyValueEditor) + " can only be used with string based property editors"); - } + return string.Empty; } + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Ntext: + case ValueStorageType.Nvarchar: + return val.ToString() ?? string.Empty; + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + case ValueStorageType.Date: + default: + throw new InvalidOperationException("The " + typeof(TextOnlyValueEditor) + + " can only be used with string based property editors"); + } } } diff --git a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs index 60f4169ce6..74de3fea8e 100644 --- a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs @@ -1,58 +1,57 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Templates; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DefaultPropertyValueConverter] +public class TextStringValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class TextStringValueConverter : PropertyValueConverterBase + private static readonly string[] PropertyTypeAliases = { - public TextStringValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser) - { - _linkParser = linkParser; - _urlParser = urlParser; - } + Constants.PropertyEditors.Aliases.TextBox, Constants.PropertyEditors.Aliases.TextArea, + }; - private static readonly string[] PropertyTypeAliases = - { - Constants.PropertyEditors.Aliases.TextBox, - Constants.PropertyEditors.Aliases.TextArea - }; - private readonly HtmlLocalLinkParser _linkParser; - private readonly HtmlUrlParser _urlParser; + private readonly HtmlLocalLinkParser _linkParser; + private readonly HtmlUrlParser _urlParser; - public override bool IsConverter(IPublishedPropertyType propertyType) - => PropertyTypeAliases.Contains(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return null; - var sourceString = source.ToString(); - - // ensures string is parsed for {localLink} and URLs are resolved correctly - sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview); - sourceString = _urlParser.EnsureUrls(sourceString); - - return sourceString; - } - - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return inter ?? string.Empty; - } - - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return inter; - } + public TextStringValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser) + { + _linkParser = linkParser; + _urlParser = urlParser; } + + public override bool IsConverter(IPublishedPropertyType propertyType) + => PropertyTypeAliases.Contains(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) + { + return null; + } + + var sourceString = source.ToString(); + + // ensures string is parsed for {localLink} and URLs are resolved correctly + sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview); + sourceString = _urlParser.EnsureUrls(sourceString); + + return sourceString; + } + + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + inter ?? string.Empty; + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + inter; } diff --git a/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs index fb56567bc5..26262f3589 100644 --- a/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the textbox value editor. +/// +public class TextboxConfiguration { - /// - /// Represents the configuration for the textbox value editor. - /// - public class TextboxConfiguration - { - [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")] - public int? MaxChars { get; set; } - } + [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")] + public int? MaxChars { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs index 81ea1f07b8..69d39a44ab 100644 --- a/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the textbox value editor. - /// - public class TextboxConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TextboxConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public TextboxConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the textbox value editor. +/// +public class TextboxConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TextboxConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TextboxConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs index 945e10fd17..604f4d3c30 100644 --- a/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the boolean value editor. +/// +public class TrueFalseConfiguration { - /// - /// Represents the configuration for the boolean value editor. - /// - public class TrueFalseConfiguration - { - [ConfigurationField("default", "Initial State", "boolean", Description = "The initial state for the toggle, when it is displayed for the first time in the backoffice, eg. for a new content item.")] - public bool Default { get; set; } + [ConfigurationField("default", "Initial State", "boolean", Description = "The initial state for the toggle, when it is displayed for the first time in the backoffice, eg. for a new content item.")] + public bool Default { get; set; } - [ConfigurationField("showLabels", "Show toggle labels", "boolean", Description = "Show labels next to toggle button.")] - public bool ShowLabels { get; set; } + [ConfigurationField("showLabels", "Show toggle labels", "boolean", Description = "Show labels next to toggle button.")] + public bool ShowLabels { get; set; } - [ConfigurationField("labelOn", "Label On", "textstring", Description = "Label text when enabled.")] - public string? LabelOn { get; set; } + [ConfigurationField("labelOn", "Label On", "textstring", Description = "Label text when enabled.")] + public string? LabelOn { get; set; } - [ConfigurationField("labelOff", "Label Off", "textstring", Description = "Label text when disabled.")] - public string? LabelOff { get; set; } - } + [ConfigurationField("labelOff", "Label Off", "textstring", Description = "Label text when disabled.")] + public string? LabelOff { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs index d5210edc87..72578f7c5e 100644 --- a/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs @@ -1,28 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the boolean value editor. - /// - public class TrueFalseConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TrueFalseConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public TrueFalseConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the boolean value editor. +/// +public class TrueFalseConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TrueFalseConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TrueFalseConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs index d8bade11e1..4e5fc41d49 100644 --- a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs @@ -1,25 +1,27 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Compress large, non published text properties +/// +public class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - /// - /// Compress large, non published text properties - /// - public class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) { - public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) + if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) { - if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) - { - //Only compress non published content that supports publishing and the property is text - return true; - } - if (propertyType.ValueStorageType == ValueStorageType.Integer && Constants.PropertyEditors.Aliases.Boolean.Equals(dataEditor.Alias)) - { - //Compress boolean values from int to bool - return true; - } - return false; + // Only compress non published content that supports publishing and the property is text + return true; } + + if (propertyType.ValueStorageType == ValueStorageType.Integer && + Constants.PropertyEditors.Aliases.Boolean.Equals(dataEditor.Alias)) + { + // Compress boolean values from int to bool + return true; + } + + return false; } } diff --git a/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs index 3e2a48ffd6..9dce63bf12 100644 --- a/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs @@ -1,13 +1,9 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +public class UserPickerConfiguration : ConfigurationEditor { - public class UserPickerConfiguration : ConfigurationEditor + public override IDictionary DefaultConfiguration => new Dictionary { - public override IDictionary DefaultConfiguration => new Dictionary - { - { "entityType", "User" }, - { "multiPicker", "0" } - }; - } + { "entityType", "User" }, { "multiPicker", "0" }, + }; } diff --git a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs index 17dd8060f5..20bc2eb120 100644 --- a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs @@ -1,25 +1,18 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +[DataEditor( + Constants.PropertyEditors.Aliases.UserPicker, + "User Picker", + "userpicker", + ValueType = ValueTypes.Integer, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.User)] +public class UserPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.UserPicker, - "User Picker", - "userpicker", - ValueType = ValueTypes.Integer, - Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.User)] - public class UserPickerPropertyEditor : DataEditor - { - public UserPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } + public UserPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + SupportsReadOnly = true; - protected override IConfigurationEditor CreateConfigurationEditor() => new UserPickerConfiguration(); - } + protected override IConfigurationEditor CreateConfigurationEditor() => new UserPickerConfiguration(); } diff --git a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs index ac7eb9ff61..1332b0b03c 100644 --- a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs +++ b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs @@ -1,41 +1,40 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors.Validation +namespace Umbraco.Cms.Core.PropertyEditors.Validation; + +/// +/// A collection of for an element type within complex editor +/// represented by an Element Type +/// +/// +/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: +/// https://github.com/umbraco/Umbraco-CMS/pull/8339 +/// +public class ComplexEditorElementTypeValidationResult : ValidationResult { + public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId) + : base(string.Empty) + { + ElementTypeAlias = elementTypeAlias; + BlockId = blockId; + } + + public IList ValidationResults { get; } = + new List(); + /// - /// A collection of for an element type within complex editor represented by an Element Type + /// The element type alias of the validation result /// /// - /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: - /// https://github.com/umbraco/Umbraco-CMS/pull/8339 + /// This is useful for debugging purposes but it's not actively used in the angular app /// - public class ComplexEditorElementTypeValidationResult : ValidationResult - { - public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId) - : base(string.Empty) - { - ElementTypeAlias = elementTypeAlias; - BlockId = blockId; - } + public string ElementTypeAlias { get; } - public IList ValidationResults { get; } = new List(); - - /// - /// The element type alias of the validation result - /// - /// - /// This is useful for debugging purposes but it's not actively used in the angular app - /// - public string ElementTypeAlias { get; } - - /// - /// The Block ID of the validation result - /// - /// - /// This is the GUID id of the content item based on the element type - /// - public Guid BlockId { get; } - } + /// + /// The Block ID of the validation result + /// + /// + /// This is the GUID id of the content item based on the element type + /// + public Guid BlockId { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs index 449ef432d8..06749c765a 100644 --- a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs +++ b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs @@ -1,36 +1,35 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -namespace Umbraco.Cms.Core.PropertyEditors.Validation +namespace Umbraco.Cms.Core.PropertyEditors.Validation; + +/// +/// A collection of for a property type within a complex editor represented by an +/// Element Type +/// +/// +/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: +/// https://github.com/umbraco/Umbraco-CMS/pull/8339 +/// +public class ComplexEditorPropertyTypeValidationResult : ValidationResult { - /// - /// A collection of for a property type within a complex editor represented by an Element Type - /// - /// - /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: - /// https://github.com/umbraco/Umbraco-CMS/pull/8339 - /// - public class ComplexEditorPropertyTypeValidationResult : ValidationResult + private readonly List _validationResults = new(); + + public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) + : base(string.Empty) => + PropertyTypeAlias = propertyTypeAlias; + + public IReadOnlyList ValidationResults => _validationResults; + + public string PropertyTypeAlias { get; } + + public void AddValidationResult(ValidationResult validationResult) { - public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) - : base(string.Empty) + if (validationResult is ComplexEditorValidationResult && + _validationResults.Any(x => x is ComplexEditorValidationResult)) { - PropertyTypeAlias = propertyTypeAlias; + throw new InvalidOperationException($"Cannot add more than one {typeof(ComplexEditorValidationResult)}"); } - private readonly List _validationResults = new List(); - - public void AddValidationResult(ValidationResult validationResult) - { - if (validationResult is ComplexEditorValidationResult && _validationResults.Any(x => x is ComplexEditorValidationResult)) - throw new InvalidOperationException($"Cannot add more than one {typeof(ComplexEditorValidationResult)}"); - - _validationResults.Add(validationResult); - } - - public IReadOnlyList ValidationResults => _validationResults; - public string PropertyTypeAlias { get; } + _validationResults.Add(validationResult); } } diff --git a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs index 225963f461..6ea03ae60f 100644 --- a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs +++ b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs @@ -1,25 +1,24 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors.Validation +namespace Umbraco.Cms.Core.PropertyEditors.Validation; + +/// +/// A collection of for a complex editor represented by an +/// Element Type +/// +/// +/// For example, each represents validation results for a row in Nested +/// Content. +/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: +/// https://github.com/umbraco/Umbraco-CMS/pull/8339 +/// +public class ComplexEditorValidationResult : ValidationResult { - - /// - /// A collection of for a complex editor represented by an Element Type - /// - /// - /// For example, each represents validation results for a row in Nested Content. - /// - /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: - /// https://github.com/umbraco/Umbraco-CMS/pull/8339 - /// - public class ComplexEditorValidationResult : ValidationResult + public ComplexEditorValidationResult() + : base(string.Empty) { - public ComplexEditorValidationResult() - : base(string.Empty) - { - } - - public IList ValidationResults { get; } = new List(); } + + public IList ValidationResults { get; } = + new List(); } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs index 7c15a418d8..530935d276 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs @@ -1,34 +1,31 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators -{ - /// - /// Used to validate if the value is a valid date/time - /// - public class DateTimeValidator : IValueValidator - { - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) - { - //don't validate if empty - if (value == null || value.ToString().IsNullOrWhiteSpace()) - { - yield break; - } +namespace Umbraco.Cms.Core.PropertyEditors.Validators; - DateTime dt; - if (DateTime.TryParse(value.ToString(), out dt) == false) - { - yield return new ValidationResult(string.Format("The string value {0} cannot be parsed into a DateTime", value), - new[] - { - //we only store a single value for this editor so the 'member' or 'field' - // we'll associate this error with will simply be called 'value' - "value" - }); - } +/// +/// Used to validate if the value is a valid date/time +/// +public class DateTimeValidator : IValueValidator +{ + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + // don't validate if empty + if (value == null || value.ToString().IsNullOrWhiteSpace()) + { + yield break; + } + + if (DateTime.TryParse(value.ToString(), out DateTime dt) == false) + { + yield return new ValidationResult( + string.Format("The string value {0} cannot be parsed into a DateTime", value), + new[] + { + // we only store a single value for this editor so the 'member' or 'field' + // we'll associate this error with will simply be called 'value' + "value", + }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs index 1fb2486e45..cc00b4614e 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs @@ -1,26 +1,28 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates that the value is a valid decimal +/// +public sealed class DecimalValidator : IManifestValueValidator { - /// - /// A validator that validates that the value is a valid decimal - /// - public sealed class DecimalValidator : IManifestValueValidator + /// + public string ValidationName => "Decimal"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { - /// - public string ValidationName => "Decimal"; - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + if (value == null || value.ToString() == string.Empty) { - if (value == null || value.ToString() == string.Empty) - yield break; + yield break; + } - var result = value.TryConvertTo(); - if (result.Success == false) - yield return new ValidationResult("The value " + value + " is not a valid decimal", new[] { "value" }); + Attempt result = value.TryConvertTo(); + if (result.Success == false) + { + yield return new ValidationResult("The value " + value + " is not a valid decimal", new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs index 8e93e5189e..73907a4266 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs @@ -1,59 +1,58 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates a delimited set of values against a common regex +/// +public sealed class DelimitedValueValidator : IManifestValueValidator { /// - /// A validator that validates a delimited set of values against a common regex + /// Gets or sets the configuration, when parsed as . /// - public sealed class DelimitedValueValidator : IManifestValueValidator + public DelimitedValueValidatorConfig? Configuration { get; set; } + + /// + public string ValidationName => "Delimited"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { - /// - public string ValidationName => "Delimited"; - - /// - /// Gets or sets the configuration, when parsed as . - /// - public DelimitedValueValidatorConfig? Configuration { get; set; } - - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + // TODO: localize these! + if (value != null) { - // TODO: localize these! - if (value != null) - { - var delimiter = Configuration?.Delimiter ?? ","; - var regex = (Configuration?.Pattern != null) ? new Regex(Configuration.Pattern) : null; + var delimiter = Configuration?.Delimiter ?? ","; + Regex? regex = Configuration?.Pattern != null ? new Regex(Configuration.Pattern) : null; - var stringVal = value.ToString(); - var split = stringVal!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - for (var i = 0; i < split.Length; i++) + var stringVal = value.ToString(); + var split = stringVal!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); + for (var i = 0; i < split.Length; i++) + { + var s = split[i]; + + // next if we have a regex statement validate with that + if (regex != null) { - var s = split[i]; - //next if we have a regex statement validate with that - if (regex != null) + if (regex.IsMatch(s) == false) { - if (regex.IsMatch(s) == false) - { - yield return new ValidationResult("The item at index " + i + " did not match the expression " + regex, - new[] - { - //make the field name called 'value0' where 0 is the index - "value" + i - }); - } + yield return new ValidationResult( + "The item at index " + i + " did not match the expression " + regex, + new[] + { + // make the field name called 'value0' where 0 is the index + "value" + i, + }); } } } } } - - public class DelimitedValueValidatorConfig - { - public string? Delimiter { get; set; } - public string? Pattern { get; set; } - } +} + +public class DelimitedValueValidatorConfig +{ + public string? Delimiter { get; set; } + + public string? Pattern { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs index 0db537ede5..8b984dc533 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates an email address +/// +public sealed class EmailValidator : IManifestValueValidator { - /// - /// A validator that validates an email address - /// - public sealed class EmailValidator : IManifestValueValidator + /// + public string ValidationName => "Email"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { - /// - public string ValidationName => "Email"; + var asString = value == null ? string.Empty : value.ToString(); - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + var emailVal = new EmailAddressAttribute(); + + if (asString != string.Empty && emailVal.IsValid(asString) == false) { - var asString = value == null ? "" : value.ToString(); - - var emailVal = new EmailAddressAttribute(); - - if (asString != string.Empty && emailVal.IsValid(asString) == false) - { - // TODO: localize these! - yield return new ValidationResult("Email is invalid", new[] { "value" }); - } + // TODO: localize these! + yield return new ValidationResult("Email is invalid", new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs index 351d0de82d..2123d213f6 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs @@ -1,27 +1,25 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators -{ - /// - /// A validator that validates that the value is a valid integer - /// - public sealed class IntegerValidator : IManifestValueValidator - { - /// - public string ValidationName => "Integer"; +namespace Umbraco.Cms.Core.PropertyEditors.Validators; - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) +/// +/// A validator that validates that the value is a valid integer +/// +public sealed class IntegerValidator : IManifestValueValidator +{ + /// + public string ValidationName => "Integer"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + if (value != null && value.ToString() != string.Empty) { - if (value != null && value.ToString() != string.Empty) + Attempt result = value.TryConvertTo(); + if (result.Success == false) { - var result = value.TryConvertTo(); - if (result.Success == false) - { - yield return new ValidationResult("The value " + value + " is not a valid integer", new[] { "value" }); - } + yield return new ValidationResult("The value " + value + " is not a valid integer", new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs index ead85c30e4..5a9032303c 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs @@ -1,84 +1,107 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates that the value against a regular expression. +/// +public sealed class RegexValidator : IValueFormatValidator, IManifestValueValidator { + private const string ValueIsInvalid = "Value is invalid, it does not match the correct pattern"; + private readonly ILocalizedTextService _textService; + private string _regex; + /// - /// A validator that validates that the value against a regular expression. + /// Initializes a new instance of the class. /// - public sealed class RegexValidator : IValueFormatValidator, IManifestValueValidator + /// + /// Use this constructor when the validator is used as an , + /// and the regular expression is supplied at validation time. This constructor is also used when + /// the validator is used as an and the regular expression + /// is supplied via the method. + /// + public RegexValidator(ILocalizedTextService textService) + : this(textService, string.Empty) { - private readonly ILocalizedTextService _textService; - private string _regex; + } - const string ValueIsInvalid = "Value is invalid, it does not match the correct pattern"; + /// + /// Initializes a new instance of the class. + /// + /// + /// Use this constructor when the validator is used as an , + /// and the regular expression must be supplied when the validator is created. + /// + public RegexValidator(ILocalizedTextService textService, string regex) + { + _textService = textService; + _regex = regex; + } - /// - public string ValidationName => "Regex"; - - /// - /// Initializes a new instance of the class. - /// - /// Use this constructor when the validator is used as an , - /// and the regular expression is supplied at validation time. This constructor is also used when - /// the validator is used as an and the regular expression - /// is supplied via the method. - public RegexValidator(ILocalizedTextService textService) : this(textService, string.Empty) - { } - - /// - /// Initializes a new instance of the class. - /// - /// Use this constructor when the validator is used as an , - /// and the regular expression must be supplied when the validator is created. - public RegexValidator(ILocalizedTextService textService, string regex) + /// + /// Gets or sets the configuration, when parsed as . + /// + public string Configuration + { + get => _regex; + set { - _textService = textService; - _regex = regex; - } - - /// - /// Gets or sets the configuration, when parsed as . - /// - public string Configuration - { - get => _regex; - set + if (value == null) { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(value)); - - _regex = value; - } - } - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) - { - if (_regex == null) - { - throw new InvalidOperationException("The validator has not been configured."); + throw new ArgumentNullException(nameof(value)); } - return ValidateFormat(value, valueType, _regex); + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(value)); + } + + _regex = value; + } + } + + /// + public string ValidationName => "Regex"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + if (_regex == null) + { + throw new InvalidOperationException("The validator has not been configured."); } - /// - public IEnumerable ValidateFormat(object? value, string? valueType, string format) + return ValidateFormat(value, valueType, _regex); + } + + /// + public IEnumerable ValidateFormat(object? value, string? valueType, string format) + { + if (format == null) { - if (format == null) throw new ArgumentNullException(nameof(format)); - if (string.IsNullOrWhiteSpace(format)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(format)); - if (value == null || !new Regex(format).IsMatch(value.ToString()!)) - { - yield return new ValidationResult(_textService?.Localize("validation", "invalidPattern") ?? ValueIsInvalid, new[] { "value" }); - } + throw new ArgumentNullException(nameof(format)); + } + + if (string.IsNullOrWhiteSpace(format)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(format)); + } + + if (value == null || !new Regex(format).IsMatch(value.ToString()!)) + { + yield return new ValidationResult( + _textService?.Localize("validation", "invalidPattern") ?? ValueIsInvalid, + new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs index 050ba5a388..296e8eed36 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs @@ -1,56 +1,53 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates that the value is not null or empty (if it is a string) +/// +public sealed class RequiredValidator : IValueRequiredValidator, IManifestValueValidator { - /// - /// A validator that validates that the value is not null or empty (if it is a string) - /// - public sealed class RequiredValidator : IValueRequiredValidator, IManifestValueValidator + private const string ValueCannotBeNull = "Value cannot be null"; + private const string ValueCannotBeEmpty = "Value cannot be empty"; + private readonly ILocalizedTextService _textService; + + public RequiredValidator(ILocalizedTextService textService) => _textService = textService; + + /// + public string ValidationName => "Required"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) => + ValidateRequired(value, valueType); + + /// + public IEnumerable ValidateRequired(object? value, string? valueType) { - private readonly ILocalizedTextService _textService; - const string ValueCannotBeNull = "Value cannot be null"; - const string ValueCannotBeEmpty = "Value cannot be empty"; - public RequiredValidator(ILocalizedTextService textService) + if (value == null) { - _textService = textService; + yield return new ValidationResult( + _textService?.Localize("validation", "invalidNull") ?? ValueCannotBeNull, + new[] { "value" }); + yield break; } - /// - public string ValidationName => "Required"; - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + if (valueType.InvariantEquals(ValueTypes.Json)) { - return ValidateRequired(value, valueType); + if (value.ToString()?.DetectIsEmptyJson() ?? false) + { + yield return new ValidationResult( + _textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); + } + + yield break; } - /// - public IEnumerable ValidateRequired(object? value, string? valueType) + if (value.ToString().IsNullOrWhiteSpace()) { - if (value == null) - { - yield return new ValidationResult(_textService?.Localize("validation", "invalidNull") ?? ValueCannotBeNull, new[] {"value"}); - yield break; - } - - if (valueType.InvariantEquals(ValueTypes.Json)) - { - if (value.ToString()?.DetectIsEmptyJson() ?? false) - { - - yield return new ValidationResult(_textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); - } - - yield break; - } - - if (value.ToString().IsNullOrWhiteSpace()) - { - yield return new ValidationResult(_textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); - } + yield return new ValidationResult( + _textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs index 2aeee98bf4..2e5c17fe7e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs @@ -1,39 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class CheckboxListValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class CheckboxListValueConverter : PropertyValueConverterBase + private readonly IJsonSerializer _jsonSerializer; + + public CheckboxListValueConverter(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) { - private readonly IJsonSerializer _jsonSerializer; + var sourceString = source?.ToString() ?? string.Empty; - public CheckboxListValueConverter(IJsonSerializer jsonSerializer) + if (string.IsNullOrEmpty(sourceString)) { - _jsonSerializer = jsonSerializer; + return Enumerable.Empty(); } - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - var sourceString = source?.ToString() ?? string.Empty; - - if (string.IsNullOrEmpty(sourceString)) - return Enumerable.Empty(); - - return _jsonSerializer.Deserialize(sourceString); - } + return _jsonSerializer.Deserialize(sourceString); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index 126b4516d1..eded7b7329 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs @@ -1,92 +1,111 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +internal class ContentPickerValueConverter : PropertyValueConverterBase { - internal class ContentPickerValueConverter : PropertyValueConverterBase + private static readonly List PropertiesToExclude = new() { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), + Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture), + }; - private static readonly List PropertiesToExclude = new List + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public ContentPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) => + _publishedSnapshotAccessor = publishedSnapshotAccessor; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IPublishedContent); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Elements; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), - Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) - }; - - public ContentPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) => _publishedSnapshotAccessor = publishedSnapshotAccessor; - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IPublishedContent); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Elements; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return null; - - - if(source is not string) - { - var attemptConvertInt = source.TryConvertTo(); - if (attemptConvertInt.Success) - return attemptConvertInt.Result; - } - //Don't attempt to convert to int for UDI - if( source is string strSource - && !string.IsNullOrWhiteSpace(strSource) - && !strSource.StartsWith("umb") - && int.TryParse(strSource, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) - { - return intValue; - } - - var attemptConvertUdi = source.TryConvertTo(); - if (attemptConvertUdi.Success) - return attemptConvertUdi.Result; return null; } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + if (source is not string) { - if (inter == null) - return null; - - if ((propertyType.Alias != null && PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false) + Attempt attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) { - IPublishedContent? content; - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - if (inter is int id) + return attemptConvertInt.Result; + } + } + + // Don't attempt to convert to int for UDI + if (source is string strSource + && !string.IsNullOrWhiteSpace(strSource) + && !strSource.StartsWith("umb") + && int.TryParse(strSource, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + return intValue; + } + + Attempt attemptConvertUdi = source.TryConvertTo(); + if (attemptConvertUdi.Success) + { + return attemptConvertUdi.Result; + } + + return null; + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + { + if (inter == null) + { + return null; + } + + if ((propertyType.Alias != null && + PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false) + { + IPublishedContent? content; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + if (inter is int id) + { + content = publishedSnapshot.Content?.GetById(id); + if (content != null) { - content = publishedSnapshot.Content?.GetById(id); - if (content != null) - return content; - } - else - { - var udi = inter as GuidUdi; - if (udi is null) - return null; - content = publishedSnapshot.Content?.GetById(udi.Guid); - if (content != null && content.ContentType.ItemType == PublishedItemType.Content) - return content; + return content; } } + else + { + if (inter is not GuidUdi udi) + { + return null; + } - return inter; + content = publishedSnapshot.Content?.GetById(udi.Guid); + if (content != null && content.ContentType.ItemType == PublishedItemType.Content) + { + return content; + } + } } - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + return inter; + } + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + { + if (inter == null) { - if (inter == null) return null; - return inter.ToString(); + return null; } + + return inter.ToString(); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 7182719ee1..7941946964 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -1,52 +1,57 @@ -using System; using System.Xml; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class DatePickerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class DatePickerValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(DateTime); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (DateTime); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + if (source == null) { - if (source == null) return DateTime.MinValue; - - // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" - // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: - // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 - // We should just be using TryConvertTo instead. - - if (source is string sourceString) - { - var attempt = sourceString.TryConvertTo(); - return attempt.Success == false ? DateTime.MinValue : attempt.Result; - } - - // in the database a DateTime is: DateTime - // default value is: DateTime.MinValue - return source is DateTime ? source : DateTime.MinValue; + return DateTime.MinValue; } - // default ConvertSourceToObject just returns source ie a DateTime value - - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" + // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: + // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 + // We should just be using TryConvertTo instead. + if (source is string sourceString) { - // source should come from ConvertSource and be a DateTime already - if (inter is null) - { - return null; - } - return XmlConvert.ToString((DateTime) inter, XmlDateTimeSerializationMode.Unspecified); + Attempt attempt = sourceString.TryConvertTo(); + return attempt.Success == false ? DateTime.MinValue : attempt.Result; } + + // in the database a DateTime is: DateTime + // default value is: DateTime.MinValue + return source is DateTime ? source : DateTime.MinValue; + } + + // default ConvertSourceToObject just returns source ie a DateTime value + public override object? ConvertIntermediateToXPath( + IPublishedElement owner, + IPublishedPropertyType propertyType, + PropertyCacheLevel referenceCacheLevel, + object? inter, + bool preview) + { + // source should come from ConvertSource and be a DateTime already + if (inter is null) + { + return null; + } + + return XmlConvert.ToString((DateTime)inter, XmlDateTimeSerializationMode.Unspecified); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index 06eb23bc70..5a7f0a4adc 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -1,48 +1,48 @@ -using System; using System.Globalization; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class DecimalValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class DecimalValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Decimal.Equals(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(decimal); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.Decimal.Equals(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (decimal); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + if (source == null) { - if (source == null) - { - return 0M; - } - - // is it already a decimal? - if(source is decimal) - { - return source; - } - - // is it a double? - if(source is double sourceDouble) - { - return Convert.ToDecimal(sourceDouble); - } - - // is it a string? - if (source is string sourceString) - { - return decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out decimal d) ? d : 0M; - } - - // couldn't convert the source value - default to zero return 0M; } + + // is it already a decimal? + if (source is decimal) + { + return source; + } + + // is it a double? + if (source is double sourceDouble) + { + return Convert.ToDecimal(sourceDouble); + } + + // is it a string? + if (source is string sourceString) + { + return decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var d) + ? d + : 0M; + } + + // couldn't convert the source value - default to zero + return 0M; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs index ea7a8b2301..97074b66a3 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs @@ -1,24 +1,25 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class EmailAddressValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class EmailAddressValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.EmailAddress); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.EmailAddress); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - return source?.ToString() ?? string.Empty; - } - } + public override object ConvertIntermediateToObject( + IPublishedElement owner, + IPublishedPropertyType propertyType, + PropertyCacheLevel cacheLevel, + object? source, + bool preview) => + source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs index 6ea5aae9bb..b6bbff3b41 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs @@ -1,22 +1,20 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class EyeDropperValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class EyeDropperValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPickerEyeDropper); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPickerEyeDropper); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - => source?.ToString() ?? string.Empty; - } + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + => source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs index 4bffc5a928..f0be275436 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs @@ -1,24 +1,24 @@ -using System; -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class IntegerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class IntegerValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.Integer.Equals(propertyType.EditorAlias); + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Integer.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (int); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(int); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source.TryConvertTo().Result; - } - } + public override object ConvertSourceToIntermediate( + IPublishedElement owner, + IPublishedPropertyType propertyType, + object? source, + bool preview) => + source.TryConvertTo().Result; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs index 9f2c06cdf9..81f163745a 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -1,85 +1,125 @@ -using System; -using System.Globalization; +using System.Globalization; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// We need this property converter so that we always force the value of a label to be a string +/// +/// +/// Without a property converter defined for the label type, the value will be converted with +/// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this +/// can cause issues if the string is detected as a number and then strips leading zeros. +/// Example: http://issues.umbraco.org/issue/U4-7929 +/// +[DefaultPropertyValueConverter] +public class LabelValueConverter : PropertyValueConverterBase { - /// - /// We need this property converter so that we always force the value of a label to be a string - /// - /// - /// Without a property converter defined for the label type, the value will be converted with - /// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this - /// can cause issues if the string is detected as a number and then strips leading zeros. - /// Example: http://issues.umbraco.org/issue/U4-7929 - /// - [DefaultPropertyValueConverter] - public class LabelValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + LabelConfiguration? valueType = + ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); + switch (valueType?.ValueType) { - var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); - switch (valueType?.ValueType) - { - case ValueTypes.DateTime: - case ValueTypes.Date: - return typeof(DateTime); - case ValueTypes.Time: - return typeof(TimeSpan); - case ValueTypes.Decimal: - return typeof(decimal); - case ValueTypes.Integer: - return typeof(int); - case ValueTypes.Bigint: - return typeof(long); - default: // everything else is a string - return typeof(string); - } + case ValueTypes.DateTime: + case ValueTypes.Date: + return typeof(DateTime); + case ValueTypes.Time: + return typeof(TimeSpan); + case ValueTypes.Decimal: + return typeof(decimal); + case ValueTypes.Integer: + return typeof(int); + case ValueTypes.Bigint: + return typeof(long); + default: // everything else is a string + return typeof(string); } + } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + LabelConfiguration? valueType = + ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); + switch (valueType?.ValueType) { - var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); - switch (valueType?.ValueType) - { - case ValueTypes.DateTime: - case ValueTypes.Date: - if (source is DateTime sourceDateTime) - return sourceDateTime; - if (source is string sourceDateTimeString) - return DateTime.TryParse(sourceDateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue; - return DateTime.MinValue; - case ValueTypes.Time: - if (source is DateTime sourceTime) - return sourceTime.TimeOfDay; - if (source is string sourceTimeString) - return TimeSpan.TryParse(sourceTimeString, CultureInfo.InvariantCulture, out var ts) ? ts : TimeSpan.Zero; - return TimeSpan.Zero; - case ValueTypes.Decimal: - if (source is decimal sourceDecimal) return sourceDecimal; - if (source is string sourceDecimalString) - return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0; - if (source is double sourceDouble) - return Convert.ToDecimal(sourceDouble); - return (decimal)0; - case ValueTypes.Integer: - if (source is int sourceInt) return sourceInt; - if (source is string sourceIntString) - return int.TryParse(sourceIntString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : 0; - return 0; - case ValueTypes.Bigint: - if (source is string sourceLongString) - return long.TryParse(sourceLongString, out var i) ? i : 0; - return (long)0; - default: // everything else is a string - return source?.ToString() ?? string.Empty; - } + case ValueTypes.DateTime: + case ValueTypes.Date: + if (source is DateTime sourceDateTime) + { + return sourceDateTime; + } + + if (source is string sourceDateTimeString) + { + return DateTime.TryParse(sourceDateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dt) + ? dt + : DateTime.MinValue; + } + + return DateTime.MinValue; + case ValueTypes.Time: + if (source is DateTime sourceTime) + { + return sourceTime.TimeOfDay; + } + + if (source is string sourceTimeString) + { + return TimeSpan.TryParse(sourceTimeString, CultureInfo.InvariantCulture, out TimeSpan ts) + ? ts + : TimeSpan.Zero; + } + + return TimeSpan.Zero; + case ValueTypes.Decimal: + if (source is decimal sourceDecimal) + { + return sourceDecimal; + } + + if (source is string sourceDecimalString) + { + return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) + ? d + : 0; + } + + if (source is double sourceDouble) + { + return Convert.ToDecimal(sourceDouble); + } + + return 0M; + case ValueTypes.Integer: + if (source is int sourceInt) + { + return sourceInt; + } + + if (source is string sourceIntString) + { + return int.TryParse(sourceIntString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? i + : 0; + } + + return 0; + case ValueTypes.Bigint: + if (source is string sourceLongString) + { + return long.TryParse(sourceLongString, out var i) ? i : 0; + } + + return 0L; + default: // everything else is a string + return source?.ToString() ?? string.Empty; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index 2df96fc310..06269ef8e8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -1,92 +1,105 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// The media picker property value converter. +/// +[DefaultPropertyValueConverter] +public class MediaPickerValueConverter : PropertyValueConverterBase { - /// - /// The media picker property value converter. - /// - [DefaultPropertyValueConverter] - public class MediaPickerValueConverter : PropertyValueConverterBase + // hard-coding "image" here but that's how it works at UI level too + private const string ImageTypeAlias = "image"; + + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public MediaPickerValueConverter( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedModelFactory publishedModelFactory) { - // hard-coding "image" here but that's how it works at UI level too - private const string ImageTypeAlias = "image"; + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? + throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _publishedModelFactory = publishedModelFactory; + } - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + public override bool IsConverter(IPublishedPropertyType propertyType) => + propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); - public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, - IPublishedModelFactory publishedModelFactory) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + var isMultiple = IsMultipleDataType(propertyType.DataType); + return isMultiple + ? typeof(IEnumerable) + : typeof(IPublishedContent); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? - throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); - _publishedModelFactory = publishedModelFactory; + return null; } - public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); + Udi[]? nodeIds = source.ToString()? + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(UdiParser.Parse) + .ToArray(); + return nodeIds; + } - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + private bool IsMultipleDataType(PublishedDataType dataType) + { + MediaPickerConfiguration? config = + ConfigurationEditor.ConfigurationAs(dataType.Configuration); + return config?.Multiple ?? false; + } + + public override object? ConvertIntermediateToObject( + IPublishedElement owner, + IPublishedPropertyType propertyType, + PropertyCacheLevel cacheLevel, + object? source, + bool preview) + { + var isMultiple = IsMultipleDataType(propertyType.DataType); + + var udis = (Udi[]?)source; + var mediaItems = new List(); + + if (source == null) { - var isMultiple = IsMultipleDataType(propertyType.DataType); - return isMultiple - ? typeof(IEnumerable) - : typeof(IPublishedContent); + return isMultiple ? mediaItems : null; } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - private bool IsMultipleDataType(PublishedDataType dataType) + if (udis?.Any() ?? false) { - var config = ConfigurationEditor.ConfigurationAs(dataType.Configuration); - return config?.Multiple ?? false; - } - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, - object? source, bool preview) - { - if (source == null) return null; - - var nodeIds = source.ToString()? - .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(UdiParser.Parse) - .ToArray(); - return nodeIds; - } - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, - PropertyCacheLevel cacheLevel, object? source, bool preview) - { - var isMultiple = IsMultipleDataType(propertyType.DataType); - - var udis = (Udi[]?)source; - var mediaItems = new List(); - - if (source == null) return isMultiple ? mediaItems : null; - - if (udis?.Any() ?? false) + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + foreach (Udi udi in udis) { - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - foreach (var udi in udis) + if (udi is not GuidUdi guidUdi) { - var guidUdi = udi as GuidUdi; - if (guidUdi is null) continue; - var item = publishedSnapshot?.Media?.GetById(guidUdi.Guid); - if (item != null) - mediaItems.Add(item); + continue; } - return isMultiple ? mediaItems : FirstOrDefault(mediaItems); + IPublishedContent? item = publishedSnapshot?.Media?.GetById(guidUdi.Guid); + if (item != null) + { + mediaItems.Add(item); + } } - return source; + return isMultiple ? mediaItems : FirstOrDefault(mediaItems); } - private object? FirstOrDefault(IList mediaItems) => mediaItems.Count == 0 ? null : mediaItems[0]; + return source; } + + private object? FirstOrDefault(IList mediaItems) => mediaItems.Count == 0 ? null : mediaItems[0]; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs index 2fcaa011fd..a94da59c36 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs @@ -1,24 +1,19 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class MemberGroupPickerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class MemberGroupPickerValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberGroupPicker); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberGroupPicker); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source?.ToString() ?? string.Empty; - } - } + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs index 241b968df9..8c12264198 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -6,91 +5,100 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class MemberPickerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class MemberPickerValueConverter : PropertyValueConverterBase + private readonly IMemberService _memberService; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public MemberPickerValueConverter( + IMemberService memberService, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IUmbracoContextAccessor umbracoContextAccessor) { - private readonly IMemberService _memberService; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + _memberService = memberService; + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _umbracoContextAccessor = umbracoContextAccessor; + } - public MemberPickerValueConverter( - IMemberService memberService, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberPicker); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IPublishedContent); + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - _memberService = memberService; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _umbracoContextAccessor = umbracoContextAccessor; - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberPicker); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IPublishedContent); - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) - return null; - - var attemptConvertInt = source.TryConvertTo(); - if (attemptConvertInt.Success) - return attemptConvertInt.Result; - var attemptConvertUdi = source.TryConvertTo(); - if (attemptConvertUdi.Success) - return attemptConvertUdi.Result; return null; } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + Attempt attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) { - if (source == null) + return attemptConvertInt.Result; + } + + Attempt attemptConvertUdi = source.TryConvertTo(); + if (attemptConvertUdi.Success) + { + return attemptConvertUdi.Result; + } + + return null; + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + if (source == null) + { + return null; + } + + IPublishedContent? member; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + if (source is int id) + { + IMember? m = _memberService.GetById(id); + if (m == null) { return null; } - IPublishedContent? member; - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - if (source is int id) + member = publishedSnapshot?.Members?.Get(m); + if (member != null) { - IMember? m = _memberService.GetById(id); - if (m == null) - { - return null; - } - member = publishedSnapshot?.Members?.Get(m); - if (member != null) - { - return member; - } + return member; } - else - { - var sourceUdi = source as GuidUdi; - if (sourceUdi is null) - return null; - - IMember? m = _memberService.GetByKey(sourceUdi.Guid); - if (m == null) - { - return null; - } - - member = publishedSnapshot?.Members?.Get(m); - - if (member != null) - { - return member; - } - } - - return source; } + else + { + if (source is not GuidUdi sourceUdi) + { + return null; + } + + IMember? m = _memberService.GetByKey(sourceUdi.Guid); + if (m == null) + { + return null; + } + + member = publishedSnapshot?.Members?.Get(m); + + if (member != null) + { + return member; + } + } + + return source; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index faab6e712e..de8965ef3b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -9,163 +6,187 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// The multi node tree picker property editor value converter. +/// +[DefaultPropertyValueConverter(typeof(MustBeStringValueConverter))] +public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase { - - /// - /// The multi node tree picker property editor value converter. - /// - [DefaultPropertyValueConverter(typeof(MustBeStringValueConverter))] - public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase + private static readonly List PropertiesToExclude = new() { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IMemberService _memberService; + Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), + Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture), + }; - private static readonly List PropertiesToExclude = new List + private readonly IMemberService _memberService; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public MultiNodeTreePickerValueConverter( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IUmbracoContextAccessor umbracoContextAccessor, + IMemberService memberService) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? + throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _umbracoContextAccessor = umbracoContextAccessor; + _memberService = memberService; + } + + public override bool IsConverter(IPublishedPropertyType propertyType) => + propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsSingleNodePicker(propertyType) + ? typeof(IPublishedContent) + : typeof(IEnumerable); + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), - Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) - }; - - public MultiNodeTreePickerValueConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor, - IMemberService memberService) - { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); - _umbracoContextAccessor = umbracoContextAccessor; - _memberService = memberService; - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - { - return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - } - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => IsSingleNodePicker(propertyType) - ? typeof(IPublishedContent) - : typeof(IEnumerable); - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return null; - - if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) - { - var nodeIds = source.ToString()? - .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(UdiParser.Parse) - .ToArray(); - return nodeIds; - } return null; } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { - if (source == null) - { - return null; - } + Udi[]? nodeIds = source.ToString()? + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(UdiParser.Parse) + .ToArray(); + return nodeIds; + } - // TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton - if (_umbracoContextAccessor.TryGetUmbracoContext(out _)) + return null; + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + if (source == null) + { + return null; + } + + // TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton + if (_umbracoContextAccessor.TryGetUmbracoContext(out _)) + { + if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { - if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) + var udis = (Udi[])source; + var isSingleNodePicker = IsSingleNodePicker(propertyType); + + if ((propertyType.Alias != null && PropertiesToExclude.InvariantContains(propertyType.Alias)) == false) { - var udis = (Udi[])source; - var isSingleNodePicker = IsSingleNodePicker(propertyType); + var multiNodeTreePicker = new List(); - if ((propertyType.Alias != null && PropertiesToExclude.InvariantContains(propertyType.Alias)) == false) + UmbracoObjectTypes objectType = UmbracoObjectTypes.Unknown; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + foreach (Udi udi in udis) { - var multiNodeTreePicker = new List(); - - var objectType = UmbracoObjectTypes.Unknown; - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - foreach (var udi in udis) + if (udi is not GuidUdi guidUdi) { - var guidUdi = udi as GuidUdi; - if (guidUdi is null) continue; + continue; + } - IPublishedContent? multiNodeTreePickerItem = null; - switch (udi.EntityType) - { - case Constants.UdiEntityType.Document: - multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Document, id => publishedSnapshot.Content?.GetById(guidUdi.Guid)); - break; - case Constants.UdiEntityType.Media: - multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Media, id => publishedSnapshot.Media?.GetById(guidUdi.Guid)); - break; - case Constants.UdiEntityType.Member: - multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Member, id => + IPublishedContent? multiNodeTreePickerItem = null; + switch (udi.EntityType) + { + case Constants.UdiEntityType.Document: + multiNodeTreePickerItem = GetPublishedContent( + udi, + ref objectType, + UmbracoObjectTypes.Document, + id => publishedSnapshot.Content?.GetById(guidUdi.Guid)); + break; + case Constants.UdiEntityType.Media: + multiNodeTreePickerItem = GetPublishedContent( + udi, + ref objectType, + UmbracoObjectTypes.Media, + id => publishedSnapshot.Media?.GetById(guidUdi.Guid)); + break; + case Constants.UdiEntityType.Member: + multiNodeTreePickerItem = GetPublishedContent( + udi, + ref objectType, + UmbracoObjectTypes.Member, + id => { IMember? m = _memberService.GetByKey(guidUdi.Guid); if (m == null) { return null; } + IPublishedContent? member = publishedSnapshot?.Members?.Get(m); return member; }); - break; - } - - if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) - { - multiNodeTreePicker.Add(multiNodeTreePickerItem); - if (isSingleNodePicker) - { - break; - } - } + break; } - if (isSingleNodePicker) + if (multiNodeTreePickerItem != null && + multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) { - return multiNodeTreePicker.FirstOrDefault(); + multiNodeTreePicker.Add(multiNodeTreePickerItem); + if (isSingleNodePicker) + { + break; + } } - return multiNodeTreePicker; } - // return the first nodeId as this is one of the excluded properties that expects a single id - return udis.FirstOrDefault(); + if (isSingleNodePicker) + { + return multiNodeTreePicker.FirstOrDefault(); + } + + return multiNodeTreePicker; } + + // return the first nodeId as this is one of the excluded properties that expects a single id + return udis.FirstOrDefault(); } - return source; } - /// - /// Attempt to get an IPublishedContent instance based on ID and content type - /// - /// The content node ID - /// The type of content being requested - /// The type of content expected/supported by - /// A function to fetch content of type - /// The requested content, or null if either it does not exist or does not match - private IPublishedContent? GetPublishedContent(T nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func contentFetcher) + return source; + } + + private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) => + propertyType.DataType.ConfigurationAs()?.MaxNumber == 1; + + /// + /// Attempt to get an IPublishedContent instance based on ID and content type + /// + /// The content node ID + /// The type of content being requested + /// The type of content expected/supported by + /// A function to fetch content of type + /// + /// The requested content, or null if either it does not exist or does not match + /// + /// + private IPublishedContent? GetPublishedContent(T nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func contentFetcher) + { + // is the actual type supported by the content fetcher? + if (actualType != UmbracoObjectTypes.Unknown && actualType != expectedType) { - // is the actual type supported by the content fetcher? - if (actualType != UmbracoObjectTypes.Unknown && actualType != expectedType) - { - // no, return null - return null; - } - - // attempt to get the content - var content = contentFetcher(nodeId); - if (content != null) - { - // if we found the content, assign the expected type to the actual type so we don't have to keep looking for other types of content - actualType = expectedType; - } - return content; + // no, return null + return null; } - private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) => propertyType.DataType.ConfigurationAs()?.MaxNumber == 1; + // attempt to get the content + IPublishedContent? content = contentFetcher(nodeId); + if (content != null) + { + // if we found the content, assign the expected type to the actual type so we don't have to keep looking for other types of content + actualType = expectedType; + } + + return content; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index b4ce51c077..3d631afead 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -1,81 +1,79 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml; +using System.Xml; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class MultipleTextStringValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class MultipleTextStringValueConverter : PropertyValueConverterBase + private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + // data is (both in database and xml): + // + // + // Strong + // Flexible + // Efficient + // + // + var sourceString = source?.ToString(); + if (string.IsNullOrWhiteSpace(sourceString)) { - // data is (both in database and xml): - // - // - // Strong - // Flexible - // Efficient - // - // - - var sourceString = source?.ToString(); - if (string.IsNullOrWhiteSpace(sourceString)) return Enumerable.Empty(); - - //SD: I have no idea why this logic is here, I'm pretty sure we've never saved the multiple txt string - // as xml in the database, it's always been new line delimited. Will ask Stephen about this. - // In the meantime, we'll do this xml check, see if it parses and if not just continue with - // splitting by newline - // - // RS: SD/Stephan Please consider post before deciding to remove - //// https://our.umbraco.com/forum/contributing-to-umbraco-cms/76989-keep-the-xml-values-in-the-multipletextstringvalueconverter - var values = new List(); - var pos = sourceString.IndexOf("", StringComparison.Ordinal); - while (pos >= 0) - { - pos += "".Length; - var npos = sourceString.IndexOf("<", pos, StringComparison.Ordinal); - var value = sourceString.Substring(pos, npos - pos); - values.Add(value); - pos = sourceString.IndexOf("", pos, StringComparison.Ordinal); - } - - // fall back on normal behaviour - return values.Any() == false - ? sourceString.Split(NewLineDelimiters, StringSplitOptions.None) - : values.ToArray(); + return Enumerable.Empty(); } - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + // SD: I have no idea why this logic is here, I'm pretty sure we've never saved the multiple txt string + // as xml in the database, it's always been new line delimited. Will ask Stephen about this. + // In the meantime, we'll do this xml check, see if it parses and if not just continue with + // splitting by newline + // + // RS: SD/Stephan Please consider post before deciding to remove + //// https://our.umbraco.com/forum/contributing-to-umbraco-cms/76989-keep-the-xml-values-in-the-multipletextstringvalueconverter + var values = new List(); + var pos = sourceString.IndexOf("", StringComparison.Ordinal); + while (pos >= 0) { - var d = new XmlDocument(); - var e = d.CreateElement("values"); - d.AppendChild(e); - - var values = (IEnumerable?) inter; - if (values is not null) - { - foreach (var value in values) - { - var ee = d.CreateElement("value"); - ee.InnerText = value; - e.AppendChild(ee); - } - } - - return d.CreateNavigator(); + pos += "".Length; + var npos = sourceString.IndexOf("<", pos, StringComparison.Ordinal); + var value = sourceString.Substring(pos, npos - pos); + values.Add(value); + pos = sourceString.IndexOf("", pos, StringComparison.Ordinal); } + + // fall back on normal behaviour + return values.Any() == false + ? sourceString.Split(NewLineDelimiters, StringSplitOptions.None) + : values.ToArray(); + } + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + { + var d = new XmlDocument(); + XmlElement e = d.CreateElement("values"); + d.AppendChild(e); + + var values = (IEnumerable?)inter; + if (values is not null) + { + foreach (var value in values) + { + XmlElement ee = d.CreateElement("value"); + ee.InnerText = value; + e.AppendChild(ee); + } + } + + return d.CreateNavigator(); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs index d172e534c4..141cfe53ec 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs @@ -1,39 +1,34 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// Ensures that no matter what is selected in (editor), the value results in a string. +/// +/// +/// +/// For more details see issues http://issues.umbraco.org/issue/U4-3776 (MNTP) +/// and http://issues.umbraco.org/issue/U4-4160 (media picker). +/// +/// +/// The cache level is set to .Content because the string is supposed to depend +/// on the source value only, and not on any other content. It is NOT appropriate +/// to use that converter for values whose .ToString() would depend on other content. +/// +/// +[DefaultPropertyValueConverter] +public class MustBeStringValueConverter : PropertyValueConverterBase { - /// - /// Ensures that no matter what is selected in (editor), the value results in a string. - /// - /// - /// For more details see issues http://issues.umbraco.org/issue/U4-3776 (MNTP) - /// and http://issues.umbraco.org/issue/U4-4160 (media picker). - /// The cache level is set to .Content because the string is supposed to depend - /// on the source value only, and not on any other content. It is NOT appropriate - /// to use that converter for values whose .ToString() would depend on other content. - /// - [DefaultPropertyValueConverter] - public class MustBeStringValueConverter : PropertyValueConverterBase - { - private static readonly string[] Aliases = - { - Constants.PropertyEditors.Aliases.MultiNodeTreePicker - }; + private static readonly string[] Aliases = { Constants.PropertyEditors.Aliases.MultiNodeTreePicker }; - public override bool IsConverter(IPublishedPropertyType propertyType) - => Aliases.Contains(propertyType.EditorAlias); + public override bool IsConverter(IPublishedPropertyType propertyType) + => Aliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source?.ToString(); - } - } + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => source?.ToString(); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs index 162764fbf5..c18363a2db 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs @@ -1,29 +1,29 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class RadioButtonListValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class RadioButtonListValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); + Attempt attempt = source.TryConvertTo(); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + if (attempt.Success) { - var attempt = source.TryConvertTo(); - - if (attempt.Success) - return attempt.Result; - - return null; + return attempt.Result; } + + return null; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs index 1ad867bfd0..7503e6711f 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs @@ -1,43 +1,38 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. +/// +[DefaultPropertyValueConverter] +public class SimpleTinyMceValueConverter : PropertyValueConverterBase { - /// - /// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. - /// - [DefaultPropertyValueConverter] - public class SimpleTinyMceValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce; + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce; - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IHtmlEncodedString); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IHtmlEncodedString); - // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - // in xml a string is: string - // in the database a string is: string - // default value is: null - return source; - } + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return new HtmlEncodedString(inter == null ? string.Empty : (string)inter); - } + // in xml a string is: string + // in the database a string is: string + // default value is: null + source; - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return inter; - } - } + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + new HtmlEncodedString(inter == null ? string.Empty : (string)inter); + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + inter; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs index 1da3458dab..76f5b62265 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -1,86 +1,79 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class SliderValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class SliderValueConverter : PropertyValueConverterBase + private static readonly ConcurrentDictionary Storages = new(); + private readonly IDataTypeService _dataTypeService; + + public SliderValueConverter(IDataTypeService dataTypeService) => _dataTypeService = + dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + + public static void ClearCaches() => Storages.Clear(); + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsRangeDataType(propertyType.DataType.Id) ? typeof(Range) : typeof(decimal); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) { - private readonly IDataTypeService _dataTypeService; - - public SliderValueConverter(IDataTypeService dataTypeService) + if (source == null) { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => IsRangeDataType(propertyType.DataType.Id) ? typeof (Range) : typeof (decimal); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - if (source == null) - return null; - - if (IsRangeDataType(propertyType.DataType.Id)) - { - var rangeRawValues = source.ToString()!.Split(Constants.CharArrays.Comma); - var minimumAttempt = rangeRawValues[0].TryConvertTo(); - var maximumAttempt = rangeRawValues[1].TryConvertTo(); - - if ((minimumAttempt.Success) && (maximumAttempt.Success)) - { - return new Range { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result }; - } - } - - var valueAttempt = source.ToString().TryConvertTo(); - if (valueAttempt.Success) - return valueAttempt.Result; - - // Something failed in the conversion of the strings to decimals return null; - } - /// - /// Discovers if the slider is set to range mode. - /// - /// - /// The data type id. - /// - /// - /// The . - /// - private bool IsRangeDataType(int dataTypeId) + if (IsRangeDataType(propertyType.DataType.Id)) { - // GetPreValuesCollectionByDataTypeId is cached at repository level; - // still, the collection is deep-cloned so this is kinda expensive, - // better to cache here + trigger refresh in DataTypeCacheRefresher - // TODO: this is cheap now, remove the caching + var rangeRawValues = source.ToString()!.Split(Constants.CharArrays.Comma); + Attempt minimumAttempt = rangeRawValues[0].TryConvertTo(); + Attempt maximumAttempt = rangeRawValues[1].TryConvertTo(); - return Storages.GetOrAdd(dataTypeId, id => + if (minimumAttempt.Success && maximumAttempt.Success) { - var dataType = _dataTypeService.GetDataType(id); - var configuration = dataType?.ConfigurationAs(); - return configuration?.EnableRange ?? false; - }); + return new Range { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result }; + } } - private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); - - public static void ClearCaches() + Attempt valueAttempt = source.ToString().TryConvertTo(); + if (valueAttempt.Success) { - Storages.Clear(); + return valueAttempt.Result; } + + // Something failed in the conversion of the strings to decimals + return null; } + + /// + /// Discovers if the slider is set to range mode. + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool IsRangeDataType(int dataTypeId) => + + // GetPreValuesCollectionByDataTypeId is cached at repository level; + // still, the collection is deep-cloned so this is kinda expensive, + // better to cache here + trigger refresh in DataTypeCacheRefresher + // TODO: this is cheap now, remove the caching + Storages.GetOrAdd(dataTypeId, id => + { + IDataType? dataType = _dataTypeService.GetDataType(id); + SliderConfiguration? configuration = dataType?.ConfigurationAs(); + return configuration?.EnableRange ?? false; + }); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs index da5dfd5416..3afc5a6596 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -1,82 +1,75 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class TagsValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class TagsValueConverter : PropertyValueConverterBase + private static readonly ConcurrentDictionary Storages = new(); + private readonly IDataTypeService _dataTypeService; + private readonly IJsonSerializer _jsonSerializer; + + public TagsValueConverter(IDataTypeService dataTypeService, IJsonSerializer jsonSerializer) { - private readonly IDataTypeService _dataTypeService; - private readonly IJsonSerializer _jsonSerializer; - - public TagsValueConverter(IDataTypeService dataTypeService, IJsonSerializer jsonSerializer) - { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return Array.Empty(); - - // if Json storage type deserialize and return as string array - if (JsonStorageType(propertyType.DataType.Id)) - { - var array = source.ToString() is not null ? _jsonSerializer.Deserialize(source.ToString()!) : null; - return array ?? Array.Empty(); - } - - // Otherwise assume CSV storage type and return as string array - return source.ToString()?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - } - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - return (string[]?) source; - } - - /// - /// Discovers if the tags data type is storing its data in a Json format - /// - /// - /// The data type id. - /// - /// - /// The . - /// - private bool JsonStorageType(int dataTypeId) - { - // GetDataType(id) is cached at repository level; still, there is some - // deep-cloning involved (expensive) - better cache here + trigger - // refresh in DataTypeCacheRefresher - - return Storages.GetOrAdd(dataTypeId, id => - { - var configuration = _dataTypeService.GetDataType(id)?.ConfigurationAs(); - return configuration?.StorageType == TagsStorageType.Json; - }); - } - - private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); - - public static void ClearCaches() - { - Storages.Clear(); - } + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); } + + public static void ClearCaches() => Storages.Clear(); + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) + { + return Array.Empty(); + } + + // if Json storage type deserialize and return as string array + if (JsonStorageType(propertyType.DataType.Id)) + { + var array = source.ToString() is not null + ? _jsonSerializer.Deserialize(source.ToString()!) + : null; + return array ?? Array.Empty(); + } + + // Otherwise assume CSV storage type and return as string array + return source.ToString()?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) => (string[]?)source; + + /// + /// Discovers if the tags data type is storing its data in a Json format + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool JsonStorageType(int dataTypeId) => + + // GetDataType(id) is cached at repository level; still, there is some + // deep-cloning involved (expensive) - better cache here + trigger + // refresh in DataTypeCacheRefresher + Storages.GetOrAdd(dataTypeId, id => + { + TagConfiguration? configuration = _dataTypeService.GetDataType(id)?.ConfigurationAs(); + return configuration?.StorageType == TagsStorageType.Json; + }); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs index a554e7d134..7a9ab907d8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs @@ -1,26 +1,21 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// The upload property value converter. +/// +[DefaultPropertyValueConverter] +public class UploadPropertyConverter : PropertyValueConverterBase { - /// - /// The upload property value converter. - /// - [DefaultPropertyValueConverter] - public class UploadPropertyConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.UploadField); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.UploadField); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - return source?.ToString() ?? ""; - } - } + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) => source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 6534ce3f14..ab7f99e7f8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -1,58 +1,63 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class YesNoValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class YesNoValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.Boolean; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(bool); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.Boolean; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (bool); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + // in xml a boolean is: string + // in the database a boolean is: string "1" or "0" or empty + // typically the converter does not need to handle anything else ("true"...) + // however there are cases where the value passed to the converter could be a non-string object, e.g. int, bool + if (source is string s) { - // in xml a boolean is: string - // in the database a boolean is: string "1" or "0" or empty - // typically the converter does not need to handle anything else ("true"...) - // however there are cases where the value passed to the converter could be a non-string object, e.g. int, bool - - if (source is string s) + if (s.Length == 0 || s == "0") { - if (s.Length == 0 || s == "0") - return false; - - if (s == "1") - return true; - - return bool.TryParse(s, out bool result) && result; + return false; } - if (source is int) - return (int)source == 1; + if (s == "1") + { + return true; + } - // this is required for correct true/false handling in nested content elements - if (source is long) - return (long)source == 1; - - if (source is bool) - return (bool)source; - - // default value is: false - return false; + return bool.TryParse(s, out var result) && result; } - // default ConvertSourceToObject just returns source ie a boolean value - - public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + if (source is int) { - // source should come from ConvertSource and be a boolean already - return (bool?)inter ?? false ? "1" : "0"; + return (int)source == 1; } + + // this is required for correct true/false handling in nested content elements + if (source is long) + { + return (long)source == 1; + } + + if (source is bool) + { + return (bool)source; + } + + // default value is: false + return false; } + + // default ConvertSourceToObject just returns source ie a boolean value + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a boolean already + (bool?)inter ?? false ? "1" : "0"; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs index 61b8a02f0e..ca727f7008 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the ValueList editor configuration. +/// +public class ValueListConfiguration { - /// - /// Represents the ValueList editor configuration. - /// - public class ValueListConfiguration + [ConfigurationField("items", "Configure", "multivalues", Description = "Add, remove or sort values for the list.")] + public List Items { get; set; } = new(); + + [DataContract] + public class ValueListItem { - [ConfigurationField("items", "Configure", "multivalues", Description = "Add, remove or sort values for the list.")] - public List Items { get; set; } = new List(); + [DataMember(Name = "id")] + public int Id { get; set; } - [DataContract] - public class ValueListItem - { - [DataMember(Name = "id")] - public int Id { get; set; } - - [DataMember(Name = "value")] - public string? Value { get; set; } - } + [DataMember(Name = "value")] + public string? Value { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueTypes.cs b/src/Umbraco.Core/PropertyEditors/ValueTypes.cs index 3a99a70a14..ac6e6a9bb8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueTypes.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueTypes.cs @@ -1,113 +1,113 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the types of the edited values. +/// +/// +/// +/// These types are used to determine the storage type, but also for +/// validation. Therefore, they are more detailed than the storage types. +/// +/// +public static class ValueTypes { /// - /// Represents the types of the edited values. + /// Date value. /// - /// - /// These types are used to determine the storage type, but also for - /// validation. Therefore, they are more detailed than the storage types. - /// - public static class ValueTypes + public const string Date = "DATE"; // Date + + /// + /// DateTime value. + /// + public const string DateTime = "DATETIME"; // Date + + /// + /// Decimal value. + /// + public const string Decimal = "DECIMAL"; // Decimal + + /// + /// Integer value. + /// + public const string Integer = "INT"; // Integer + + /// + /// Integer value. + /// + public const string Bigint = "BIGINT"; // String + + /// + /// Json value. + /// + public const string Json = "JSON"; // NText + + /// + /// Text value (maps to text database type). + /// + public const string Text = "TEXT"; // NText + + /// + /// Time value. + /// + public const string Time = "TIME"; // Date + + /// + /// Text value (maps to varchar database type). + /// + public const string String = "STRING"; // NVarchar + + /// + /// Xml value. + /// + public const string Xml = "XML"; // NText + + // the auto, static, set of valid values + private static readonly HashSet Values + = new(typeof(ValueTypes) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(x => x.IsLiteral && !x.IsInitOnly) + .Select(x => (string?)x.GetRawConstantValue())); + + /// + /// Determines whether a string value is a valid ValueTypes value. + /// + public static bool IsValue(string s) + => Values.Contains(s); + + /// + /// Gets the value corresponding to a ValueTypes value. + /// + public static ValueStorageType ToStorageType(string valueType) { - // the auto, static, set of valid values - private static readonly HashSet Values - = new HashSet(typeof(ValueTypes) - .GetFields(BindingFlags.Static | BindingFlags.Public) - .Where(x => x.IsLiteral && !x.IsInitOnly) - .Select(x => (string?) x.GetRawConstantValue())); - - /// - /// Date value. - /// - public const string Date = "DATE"; // Date - - /// - /// DateTime value. - /// - public const string DateTime = "DATETIME"; // Date - - /// - /// Decimal value. - /// - public const string Decimal = "DECIMAL"; // Decimal - - /// - /// Integer value. - /// - public const string Integer = "INT"; // Integer - - /// - /// Integer value. - /// - public const string Bigint = "BIGINT"; // String - - /// - /// Json value. - /// - public const string Json = "JSON"; // NText - - /// - /// Text value (maps to text database type). - /// - public const string Text = "TEXT"; // NText - - /// - /// Time value. - /// - public const string Time = "TIME"; // Date - - /// - /// Text value (maps to varchar database type). - /// - public const string String = "STRING"; // NVarchar - - /// - /// Xml value. - /// - public const string Xml = "XML"; // NText - - /// - /// Determines whether a string value is a valid ValueTypes value. - /// - public static bool IsValue(string s) - => Values.Contains(s); - - /// - /// Gets the value corresponding to a ValueTypes value. - /// - public static ValueStorageType ToStorageType(string valueType) + switch (valueType.ToUpperInvariant()) { - switch (valueType.ToUpperInvariant()) - { - case Integer: - return ValueStorageType.Integer; + case Integer: + return ValueStorageType.Integer; - case Decimal: - return ValueStorageType.Decimal; + case Decimal: + return ValueStorageType.Decimal; - case String: - case Bigint: - return ValueStorageType.Nvarchar; + case String: + case Bigint: + return ValueStorageType.Nvarchar; - case Text: - case Json: - case Xml: - return ValueStorageType.Ntext; + case Text: + case Json: + case Xml: + return ValueStorageType.Ntext; - case DateTime: - case Date: - case Time: - return ValueStorageType.Date; + case DateTime: + case Date: + case Time: + return ValueStorageType.Date; - default: - throw new ArgumentOutOfRangeException(nameof(valueType), $"Value \"{valueType}\" is not a valid ValueTypes."); - } + default: + throw new ArgumentOutOfRangeException( + nameof(valueType), + $"Value \"{valueType}\" is not a valid ValueTypes."); } } } diff --git a/src/Umbraco.Core/PropertyEditors/VoidEditor.cs b/src/Umbraco.Core/PropertyEditors/VoidEditor.cs index 28a0afb6ce..f272dc49bd 100644 --- a/src/Umbraco.Core/PropertyEditors/VoidEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/VoidEditor.cs @@ -1,46 +1,49 @@ -using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a void editor. +/// +/// +/// Can be used in some places where an editor is needed but no actual +/// editor is available. Not to be used otherwise. Not discovered, and therefore +/// not part of the editors collection. +/// +[HideFromTypeFinder] +public class VoidEditor : DataEditor { /// - /// Represents a void editor. + /// Initializes a new instance of the class. /// - /// Can be used in some places where an editor is needed but no actual - /// editor is available. Not to be used otherwise. Not discovered, and therefore - /// not part of the editors collection. - [HideFromTypeFinder] - public class VoidEditor : DataEditor + /// An optional alias suffix. + /// A logger factory. + /// + /// The default alias of the editor is "Umbraco.Void". When a suffix is provided, + /// it is appended to the alias. Eg if the suffix is "Foo" the alias is "Umbraco.Void.Foo". + /// + public VoidEditor( + string? aliasSuffix, + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - /// An optional alias suffix. - /// A logger factory. - /// The default alias of the editor is "Umbraco.Void". When a suffix is provided, - /// it is appended to the alias. Eg if the suffix is "Foo" the alias is "Umbraco.Void.Foo". - public VoidEditor( - string? aliasSuffix, - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) + Alias = "Umbraco.Void"; + if (string.IsNullOrWhiteSpace(aliasSuffix)) { - Alias = "Umbraco.Void"; - if (string.IsNullOrWhiteSpace(aliasSuffix)) return; - Alias += "." + aliasSuffix; + return; } - /// - /// Initializes a new instance of the class. - /// - /// A logger factory. - /// The alias of the editor is "Umbraco.Void". - public VoidEditor( - IDataValueEditorFactory dataValueEditorFactory) - : this(null, dataValueEditorFactory) - { } + Alias += "." + aliasSuffix; + } + + /// + /// Initializes a new instance of the class. + /// + /// A logger factory. + /// The alias of the editor is "Umbraco.Void". + public VoidEditor( + IDataValueEditorFactory dataValueEditorFactory) + : this(null, dataValueEditorFactory) + { } } diff --git a/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs b/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs index 648041a3a4..4068bc4477 100644 --- a/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs @@ -1,33 +1,31 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Provides the default implementation of . +/// +public class DefaultCultureAccessor : IDefaultCultureAccessor { + private readonly ILocalizationService _localizationService; + private readonly IRuntimeState _runtimeState; + private GlobalSettings _options; + /// - /// Provides the default implementation of . + /// Initializes a new instance of the class. /// - public class DefaultCultureAccessor : IDefaultCultureAccessor + public DefaultCultureAccessor(ILocalizationService localizationService, IRuntimeState runtimeState, IOptionsMonitor options) { - private readonly ILocalizationService _localizationService; - private readonly IRuntimeState _runtimeState; - private GlobalSettings _options; - - - /// - /// Initializes a new instance of the class. - /// - public DefaultCultureAccessor(ILocalizationService localizationService, IRuntimeState runtimeState, IOptionsMonitor options) - { - _localizationService = localizationService; - _runtimeState = runtimeState; - _options = options.CurrentValue; - options.OnChange(x => _options = x); - } - - /// - public string DefaultCulture => _runtimeState.Level == RuntimeLevel.Run - ? _localizationService.GetDefaultLanguageIsoCode() ?? "" // fast - : _options.DefaultUILanguage; // default for install and upgrade, when the service is n/a + _localizationService = localizationService; + _runtimeState = runtimeState; + _options = options.CurrentValue; + options.OnChange(x => _options = x); } + + /// + public string DefaultCulture => _runtimeState.Level == RuntimeLevel.Run + ? _localizationService.GetDefaultLanguageIsoCode() ?? string.Empty // fast + : _options.DefaultUILanguage; // default for install and upgrade, when the service is n/a } diff --git a/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs b/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs index 58844562a7..583daca2f3 100644 --- a/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Gives access to the default culture. +/// +public interface IDefaultCultureAccessor { /// - /// Gives access to the default culture. + /// Gets the system default culture. /// - public interface IDefaultCultureAccessor - { - /// - /// Gets the system default culture. - /// - /// - /// Implementations must NOT return a null value. Return an empty string for the invariant culture. - /// - string DefaultCulture { get; } - } + /// + /// Implementations must NOT return a null value. Return an empty string for the invariant culture. + /// + string DefaultCulture { get; } } diff --git a/src/Umbraco.Core/PublishedCache/IDomainCache.cs b/src/Umbraco.Core/PublishedCache/IDomainCache.cs index 0555960dfa..41443ef1f6 100644 --- a/src/Umbraco.Core/PublishedCache/IDomainCache.cs +++ b/src/Umbraco.Core/PublishedCache/IDomainCache.cs @@ -1,34 +1,33 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IDomainCache { - public interface IDomainCache - { - /// - /// Gets all in the current domain cache, including any domains that may be referenced by documents that are no longer published. - /// - /// - /// - IEnumerable GetAll(bool includeWildcards); + /// + /// Gets the system default culture. + /// + string DefaultCulture { get; } - /// - /// Gets all assigned for specified document, even if it is not published. - /// - /// The document identifier. - /// A value indicating whether to consider wildcard domains. - IEnumerable GetAssigned(int documentId, bool includeWildcards = false); + /// + /// Gets all in the current domain cache, including any domains that may be referenced by + /// documents that are no longer published. + /// + /// + /// + IEnumerable GetAll(bool includeWildcards); - /// - /// Determines whether a document has domains. - /// - /// The document identifier. - /// A value indicating whether to consider wildcard domains. - bool HasAssigned(int documentId, bool includeWildcards = false); + /// + /// Gets all assigned for specified document, even if it is not published. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + IEnumerable GetAssigned(int documentId, bool includeWildcards = false); - /// - /// Gets the system default culture. - /// - string DefaultCulture { get; } - } + /// + /// Determines whether a document has domains. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + bool HasAssigned(int documentId, bool includeWildcards = false); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs index 5a06d88ee5..0ee2ca38ed 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs @@ -1,245 +1,244 @@ -using System; -using System.Collections.Generic; using System.Xml.XPath; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Provides access to cached contents. +/// +public interface IPublishedCache : IXPathNavigable { /// - /// Provides access to cached contents. + /// Gets a content identified by its unique identifier. /// - public interface IPublishedCache : IXPathNavigable - { - /// - /// Gets a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetById(bool preview, int contentId); + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetById(bool preview, int contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetById(bool preview, Guid contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetById(bool preview, Guid contentId); - /// - /// Gets a content identified by its Udi identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content Udi identifier. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetById(bool preview, Udi contentId); + /// + /// Gets a content identified by its Udi identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content Udi identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetById(bool preview, Udi contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetById(int contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetById(int contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetById(Guid contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetById(Guid contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetById(Udi contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetById(Udi contentId); - /// - /// Gets a value indicating whether the cache contains a specified content. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// A value indicating whether to the cache contains the specified content. - /// The value of overrides defaults. - bool HasById(bool preview, int contentId); + /// + /// Gets a value indicating whether the cache contains a specified content. + /// + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// A value indicating whether to the cache contains the specified content. + /// The value of overrides defaults. + bool HasById(bool preview, int contentId); - /// - /// Gets a value indicating whether the cache contains a specified content. - /// - /// The content unique identifier. - /// A value indicating whether to the cache contains the specified content. - /// Considers published or unpublished content depending on defaults. - bool HasById(int contentId); + /// + /// Gets a value indicating whether the cache contains a specified content. + /// + /// The content unique identifier. + /// A value indicating whether to the cache contains the specified content. + /// Considers published or unpublished content depending on defaults. + bool HasById(int contentId); - /// - /// Gets contents at root. - /// - /// A value indicating whether to consider unpublished content. - /// A culture. - /// The contents. - /// The value of overrides defaults. - IEnumerable GetAtRoot(bool preview, string? culture = null); + /// + /// Gets contents at root. + /// + /// A value indicating whether to consider unpublished content. + /// A culture. + /// The contents. + /// The value of overrides defaults. + IEnumerable GetAtRoot(bool preview, string? culture = null); - /// - /// Gets contents at root. - /// - /// A culture. - /// The contents. - /// Considers published or unpublished content depending on defaults. - IEnumerable GetAtRoot(string? culture = null); + /// + /// Gets contents at root. + /// + /// A culture. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetAtRoot(string? culture = null); - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); - /// - /// Gets a content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetSingleByXPath(string xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetSingleByXPath(string xpath, params XPathVariable[] vars); - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); - /// - /// Gets a content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetSingleByXPath(XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetSingleByXPath(XPathExpression xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// The value of overrides defaults. - IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// The value of overrides defaults. + IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// Considers published or unpublished content depending on defaults. - IEnumerable GetByXPath(string xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetByXPath(string xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// The value of overrides defaults. - IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// The value of overrides defaults. + IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// Considers published or unpublished content depending on defaults. - IEnumerable GetByXPath(XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetByXPath(XPathExpression xpath, params XPathVariable[] vars); - /// - /// Creates an XPath navigator that can be used to navigate contents. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath navigator. - /// - /// The value of overrides the context. - /// The navigator is already a safe clone (no need to clone it again). - /// - XPathNavigator CreateNavigator(bool preview); + /// + /// Creates an XPath navigator that can be used to navigate contents. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath navigator. + /// + /// The value of overrides the context. + /// The navigator is already a safe clone (no need to clone it again). + /// + XPathNavigator CreateNavigator(bool preview); - /// - /// Creates an XPath navigator that can be used to navigate one node. - /// - /// The node identifier. - /// A value indicating whether to consider unpublished content. - /// The XPath navigator, or null. - /// - /// The value of overrides the context. - /// The navigator is already a safe clone (no need to clone it again). - /// Navigates over the node - and only the node, ie no children. Exists only for backward - /// compatibility + transition reasons, we should obsolete that one as soon as possible. - /// If the node does not exist, returns null. - /// - XPathNavigator? CreateNodeNavigator(int id, bool preview); + /// + /// Creates an XPath navigator that can be used to navigate one node. + /// + /// The node identifier. + /// A value indicating whether to consider unpublished content. + /// The XPath navigator, or null. + /// + /// The value of overrides the context. + /// The navigator is already a safe clone (no need to clone it again). + /// + /// Navigates over the node - and only the node, ie no children. Exists only for backward + /// compatibility + transition reasons, we should obsolete that one as soon as possible. + /// + /// If the node does not exist, returns null. + /// + XPathNavigator? CreateNodeNavigator(int id, bool preview); - /// - /// Gets a value indicating whether the cache contains published content. - /// - /// A value indicating whether to consider unpublished content. - /// A value indicating whether the cache contains published content. - /// The value of overrides defaults. - bool HasContent(bool preview); + /// + /// Gets a value indicating whether the cache contains published content. + /// + /// A value indicating whether to consider unpublished content. + /// A value indicating whether the cache contains published content. + /// The value of overrides defaults. + bool HasContent(bool preview); - /// - /// Gets a value indicating whether the cache contains published content. - /// - /// A value indicating whether the cache contains published content. - /// Considers published or unpublished content depending on defaults. - bool HasContent(); + /// + /// Gets a value indicating whether the cache contains published content. + /// + /// A value indicating whether the cache contains published content. + /// Considers published or unpublished content depending on defaults. + bool HasContent(); - /// - /// Gets a content type identified by its unique identifier. - /// - /// The content type unique identifier. - /// The content type, or null. - IPublishedContentType? GetContentType(int id); + /// + /// Gets a content type identified by its unique identifier. + /// + /// The content type unique identifier. + /// The content type, or null. + IPublishedContentType? GetContentType(int id); - /// - /// Gets a content type identified by its alias. - /// - /// The content type alias. - /// The content type, or null. - /// The alias is case-insensitive. - IPublishedContentType? GetContentType(string alias); + /// + /// Gets a content type identified by its alias. + /// + /// The content type alias. + /// The content type, or null. + /// The alias is case-insensitive. + IPublishedContentType? GetContentType(string alias); - /// - /// Gets contents of a given content type. - /// - /// The content type. - /// The contents. - IEnumerable GetByContentType(IPublishedContentType contentType); + /// + /// Gets contents of a given content type. + /// + /// The content type. + /// The contents. + IEnumerable GetByContentType(IPublishedContentType contentType); - /// - /// Gets a content type identified by its alias. - /// - /// The content type key. - /// The content type, or null. - IPublishedContentType? GetContentType(Guid key); - } + /// + /// Gets a content type identified by its alias. + /// + /// The content type key. + /// The content type, or null. + IPublishedContentType? GetContentType(Guid key); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs index 4621adcb82..7526226302 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs @@ -1,61 +1,80 @@ -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IPublishedContentCache : IPublishedCache { - public interface IPublishedContentCache : IPublishedCache - { - /// - /// Gets content identified by a route. - /// - /// A value indicating whether to consider unpublished content. - /// The route - /// A value forcing the HideTopLevelNode setting. - /// The content, or null. - /// - /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. - /// If is null then the settings value is used. - /// The value of overrides defaults. - /// - IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null); + /// + /// Gets content identified by a route. + /// + /// A value indicating whether to consider unpublished content. + /// The route + /// A value forcing the HideTopLevelNode setting. + /// the culture + /// The content, or null. + /// + /// + /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg + /// 123/foo/bar/nil. + /// + /// + /// If + /// + /// is null then the settings value is used. + /// + /// The value of overrides defaults. + /// + IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null); - /// - /// Gets content identified by a route. - /// - /// The route - /// A value forcing the HideTopLevelNode setting. - /// The content, or null. - /// - /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. - /// If is null then the settings value is used. - /// Considers published or unpublished content depending on defaults. - /// - IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null); + /// + /// Gets content identified by a route. + /// + /// The route + /// A value forcing the HideTopLevelNode setting. + /// The culture + /// The content, or null. + /// + /// + /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg + /// 123/foo/bar/nil. + /// + /// + /// If + /// + /// is null then the settings value is used. + /// + /// Considers published or unpublished content depending on defaults. + /// + IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null); - /// - /// Gets the route for a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// A special string formatted route path. - /// - /// - /// The resulting string is a special encoded route string that may contain the domain ID - /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item - /// - /// The value of overrides defaults. - /// - string? GetRouteById(bool preview, int contentId, string? culture = null); + /// + /// Gets the route for a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// The culture + /// A special string formatted route path. + /// + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: + /// {domainId}/route-path-of-item + /// + /// The value of overrides defaults. + /// + string? GetRouteById(bool preview, int contentId, string? culture = null); - /// - /// Gets the route for a content identified by its unique identifier. - /// - /// The content unique identifier. - /// A special string formatted route path. - /// Considers published or unpublished content depending on defaults. - /// - /// The resulting string is a special encoded route string that may contain the domain ID - /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item - /// - string? GetRouteById(int contentId, string? culture = null); - } + /// + /// Gets the route for a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The culture + /// A special string formatted route path. + /// Considers published or unpublished content depending on defaults. + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: + /// {domainId}/route-path-of-item + /// + string? GetRouteById(int contentId, string? culture = null); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs index 1c10776d11..b0fd46748e 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IPublishedMediaCache : IPublishedCache { - public interface IPublishedMediaCache : IPublishedCache - { } } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs index 1f5344df4c..43e6291701 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs @@ -1,61 +1,67 @@ -using System; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Specifies a published snapshot. +/// +/// +/// A published snapshot is a point-in-time capture of the current state of +/// everything that is "published". +/// +public interface IPublishedSnapshot : IDisposable { /// - /// Specifies a published snapshot. + /// Gets the . /// - /// A published snapshot is a point-in-time capture of the current state of - /// everything that is "published". - public interface IPublishedSnapshot : IDisposable - { - /// - /// Gets the . - /// - IPublishedContentCache? Content { get; } + IPublishedContentCache? Content { get; } - /// - /// Gets the . - /// - IPublishedMediaCache? Media { get; } + /// + /// Gets the . + /// + IPublishedMediaCache? Media { get; } - /// - /// Gets the . - /// - IPublishedMemberCache? Members { get; } + /// + /// Gets the . + /// + IPublishedMemberCache? Members { get; } - /// - /// Gets the . - /// - IDomainCache? Domains { get; } + /// + /// Gets the . + /// + IDomainCache? Domains { get; } - /// - /// Gets the snapshot-level cache. - /// - /// - /// The snapshot-level cache belongs to this snapshot only. - /// - IAppCache? SnapshotCache { get; } + /// + /// Gets the snapshot-level cache. + /// + /// + /// The snapshot-level cache belongs to this snapshot only. + /// + IAppCache? SnapshotCache { get; } - /// - /// Gets the elements-level cache. - /// - /// - /// The elements-level cache is shared by all snapshots relying on the same elements, - /// ie all snapshots built on top of unchanging content / media / etc. - /// - IAppCache? ElementsCache { get; } + /// + /// Gets the elements-level cache. + /// + /// + /// + /// The elements-level cache is shared by all snapshots relying on the same elements, + /// ie all snapshots built on top of unchanging content / media / etc. + /// + /// + IAppCache? ElementsCache { get; } - /// - /// Forces the preview mode. - /// - /// The forced preview mode. - /// A callback to execute when reverting to previous preview. - /// - /// Forcing to false means no preview. Forcing to true means 'full' preview if the snapshot is not already previewing; - /// otherwise the snapshot keeps previewing according to whatever settings it is using already. - /// Stops forcing preview when disposed. - IDisposable ForcedPreview(bool preview, Action? callback = null); - } + /// + /// Forces the preview mode. + /// + /// The forced preview mode. + /// A callback to execute when reverting to previous preview. + /// + /// + /// Forcing to false means no preview. Forcing to true means 'full' preview if the snapshot is not already + /// previewing; + /// otherwise the snapshot keeps previewing according to whatever settings it is using already. + /// + /// Stops forcing preview when disposed. + /// + IDisposable ForcedPreview(bool preview, Action? callback = null); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs index 3a4b5a24b0..0f9cc8fca9 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Provides access to a TryGetPublishedSnapshot bool method that will return true if the "current" +/// is not null. +/// +public interface IPublishedSnapshotAccessor { - /// - /// Provides access to a TryGetPublishedSnapshot bool method that will return true if the "current" is not null. - /// - public interface IPublishedSnapshotAccessor - { - bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot); - } + bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index 210739c6a2..f8d158dce9 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -1,102 +1,112 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Creates and manages instances. +/// +public interface IPublishedSnapshotService : IDisposable { + /* Various places (such as Node) want to access the XML content, today as an XmlDocument + * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need + * to find out how to get that navigator. + * + * Because a cache such as NuCache is contextual i.e. it has a "snapshot" thing and remains + * consistent over the snapshot, the navigator has to come from the "current" snapshot. + * + * So although everything should be injected... we also need a notion of "the current published + * snapshot". This is provided by the IPublishedSnapshotAccessor. + * + */ /// - /// Creates and manages instances. + /// Creates a published snapshot. /// - public interface IPublishedSnapshotService : IDisposable - { - /* Various places (such as Node) want to access the XML content, today as an XmlDocument - * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need - * to find out how to get that navigator. - * - * Because a cache such as NuCache is contextual i.e. it has a "snapshot" thing and remains - * consistent over the snapshot, the navigator has to come from the "current" snapshot. - * - * So although everything should be injected... we also need a notion of "the current published - * snapshot". This is provided by the IPublishedSnapshotAccessor. - * - */ + /// A preview token, or null if not previewing. + /// A published snapshot. + /// + /// If is null, the snapshot is not previewing, else it + /// is previewing, and what is or is not visible in preview depends on the content of the token, + /// which is not specified and depends on the actual published snapshot service implementation. + /// + IPublishedSnapshot CreatePublishedSnapshot(string? previewToken); - /// - /// Creates a published snapshot. - /// - /// A preview token, or null if not previewing. - /// A published snapshot. - /// If is null, the snapshot is not previewing, else it - /// is previewing, and what is or is not visible in preview depends on the content of the token, - /// which is not specified and depends on the actual published snapshot service implementation. - IPublishedSnapshot CreatePublishedSnapshot(string? previewToken); + /// + /// Rebuilds internal database caches (but does not reload). + /// + /// + /// If not null will process content for the matching content types, if empty will process all + /// content + /// + /// + /// If not null will process content for the matching media types, if empty will process all + /// media + /// + /// + /// If not null will process content for the matching members types, if empty will process all + /// members + /// + /// + /// + /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches + /// may rely on a database table to store pre-serialized version of documents. + /// + /// + /// This does *not* reload the caches. Caches need to be reloaded, for instance via + /// RefreshAllPublishedSnapshot method. + /// + /// + void Rebuild( + IReadOnlyCollection? contentTypeIds = null, + IReadOnlyCollection? mediaTypeIds = null, + IReadOnlyCollection? memberTypeIds = null); - /// - /// Rebuilds internal database caches (but does not reload). - /// - /// If not null will process content for the matching content types, if empty will process all content - /// If not null will process content for the matching media types, if empty will process all media - /// If not null will process content for the matching members types, if empty will process all members - /// - /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches - /// may rely on a database table to store pre-serialized version of documents. - /// This does *not* reload the caches. Caches need to be reloaded, for instance via - /// RefreshAllPublishedSnapshot method. - /// - void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null); + /* An IPublishedCachesService implementation can rely on transaction-level events to update + * its internal, database-level data, as these events are purely internal. However, it cannot + * rely on cache refreshers CacheUpdated events to update itself, as these events are external + * and the order-of-execution of the handlers cannot be guaranteed, which means that some + * user code may run before Umbraco is finished updating itself. Instead, the cache refreshers + * explicitly notify the service of changes. + * + */ - /* An IPublishedCachesService implementation can rely on transaction-level events to update - * its internal, database-level data, as these events are purely internal. However, it cannot - * rely on cache refreshers CacheUpdated events to update itself, as these events are external - * and the order-of-execution of the handlers cannot be guaranteed, which means that some - * user code may run before Umbraco is finished updating itself. Instead, the cache refreshers - * explicitly notify the service of changes. - * - */ + /// + /// Notifies of content cache refresher changes. + /// + /// The changes. + /// A value indicating whether draft contents have been changed in the cache. + /// A value indicating whether published contents have been changed in the cache. + void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); - /// - /// Notifies of content cache refresher changes. - /// - /// The changes. - /// A value indicating whether draft contents have been changed in the cache. - /// A value indicating whether published contents have been changed in the cache. - void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); + /// + /// Notifies of media cache refresher changes. + /// + /// The changes. + /// A value indicating whether medias have been changed in the cache. + void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); - /// - /// Notifies of media cache refresher changes. - /// - /// The changes. - /// A value indicating whether medias have been changed in the cache. - void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); + // there is no NotifyChanges for MemberCacheRefresher because we're not caching members. - // there is no NotifyChanges for MemberCacheRefresher because we're not caching members. + /// + /// Notifies of content type refresher changes. + /// + /// The changes. + void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); - /// - /// Notifies of content type refresher changes. - /// - /// The changes. - void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); + /// + /// Notifies of data type refresher changes. + /// + /// The changes. + void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); - /// - /// Notifies of data type refresher changes. - /// - /// The changes. - void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); + /// + /// Notifies of domain refresher changes. + /// + /// The changes. + void Notify(DomainCacheRefresher.JsonPayload[] payloads); - /// - /// Notifies of domain refresher changes. - /// - /// The changes. - void Notify(DomainCacheRefresher.JsonPayload[] payloads); - - /// - /// Cleans up unused snapshots - /// - Task CollectAsync(); - } + /// + /// Cleans up unused snapshots + /// + Task CollectAsync(); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs index 5695f03377..1eb09c8144 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Returns the currents status for nucache +/// +public interface IPublishedSnapshotStatus { /// - /// Returns the currents status for nucache + /// Gets the URL used to retreive the status /// - public interface IPublishedSnapshotStatus - { - /// - /// Gets the status report as a string - /// - string GetStatus(); + string StatusUrl { get; } - /// - /// Gets the URL used to retreive the status - /// - string StatusUrl { get; } - } + /// + /// Gets the status report as a string + /// + string GetStatus(); } diff --git a/src/Umbraco.Core/PublishedCache/ITagQuery.cs b/src/Umbraco.Core/PublishedCache/ITagQuery.cs index 9a59cac9d6..2deaf75108 100644 --- a/src/Umbraco.Core/PublishedCache/ITagQuery.cs +++ b/src/Umbraco.Core/PublishedCache/ITagQuery.cs @@ -1,59 +1,57 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface ITagQuery { - public interface ITagQuery - { - /// - /// Gets all documents tagged with the specified tag. - /// - IEnumerable GetContentByTag(string tag, string? group = null, string? culture = null); + /// + /// Gets all documents tagged with the specified tag. + /// + IEnumerable GetContentByTag(string tag, string? group = null, string? culture = null); - /// - /// Gets all documents tagged with any tag in the specified group. - /// - IEnumerable GetContentByTagGroup(string group, string? culture = null); + /// + /// Gets all documents tagged with any tag in the specified group. + /// + IEnumerable GetContentByTagGroup(string group, string? culture = null); - /// - /// Gets all media tagged with the specified tag. - /// - IEnumerable GetMediaByTag(string tag, string? group = null, string? culture = null); + /// + /// Gets all media tagged with the specified tag. + /// + IEnumerable GetMediaByTag(string tag, string? group = null, string? culture = null); - /// - /// Gets all media tagged with any tag in the specified group. - /// - IEnumerable GetMediaByTagGroup(string group, string? culture = null); + /// + /// Gets all media tagged with any tag in the specified group. + /// + IEnumerable GetMediaByTagGroup(string group, string? culture = null); - /// - /// Gets all tags. - /// - IEnumerable GetAllTags(string? group = null, string? culture = null); + /// + /// Gets all tags. + /// + IEnumerable GetAllTags(string? group = null, string? culture = null); - /// - /// Gets all document tags. - /// - IEnumerable GetAllContentTags(string? group = null, string? culture = null); + /// + /// Gets all document tags. + /// + IEnumerable GetAllContentTags(string? group = null, string? culture = null); - /// - /// Gets all media tags. - /// - IEnumerable GetAllMediaTags(string? group = null, string? culture = null); + /// + /// Gets all media tags. + /// + IEnumerable GetAllMediaTags(string? group = null, string? culture = null); - /// - /// Gets all member tags. - /// - IEnumerable GetAllMemberTags(string? group = null, string? culture = null); + /// + /// Gets all member tags. + /// + IEnumerable GetAllMemberTags(string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity via a property. - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity. - /// - IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); - } + /// + /// Gets all tags attached to an entity. + /// + IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs index 557d5469b6..0659e835a3 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs @@ -1,107 +1,107 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class InternalPublishedContent : IPublishedContent { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class InternalPublishedContent : IPublishedContent + private Dictionary? _cultures; + + public InternalPublishedContent(IPublishedContentType contentType) { - public InternalPublishedContent(IPublishedContentType contentType) - { - // initialize boring stuff - TemplateId = 0; - WriterId = CreatorId = 0; - CreateDate = UpdateDate = DateTime.Now; - Version = Guid.Empty; - Path = string.Empty; - ContentType = contentType; - Properties = Enumerable.Empty(); - } + // initialize boring stuff + TemplateId = 0; + WriterId = CreatorId = 0; + CreateDate = UpdateDate = DateTime.Now; + Version = Guid.Empty; + Path = string.Empty; + ContentType = contentType; + Properties = Enumerable.Empty(); + } - private Dictionary? _cultures; + public Guid Version { get; set; } - private Dictionary GetCultures() => new Dictionary { { string.Empty, new PublishedCultureInfo(string.Empty, Name, UrlSegment, UpdateDate) } }; + public int ParentId { get; set; } - public int Id { get; set; } + public IEnumerable? ChildIds { get; set; } - public Guid Key { get; set; } + public int Id { get; set; } - public int? TemplateId { get; set; } - - public int SortOrder { get; set; } - - public string? Name { get; set; } - - public IReadOnlyDictionary Cultures => _cultures ??= GetCultures(); - - public string? UrlSegment { get; set; } - - public int WriterId { get; set; } - - public int CreatorId { get; set; } - - public string Path { get; set; } - - public DateTime CreateDate { get; set; } - - public DateTime UpdateDate { get; set; } - - public Guid Version { get; set; } - - public int Level { get; set; } - - public PublishedItemType ItemType => PublishedItemType.Content; - - public bool IsDraft(string? culture = null) => false; - - public bool IsPublished(string? culture = null) => true; - - public int ParentId { get; set; } - - public IEnumerable? ChildIds { get; set; } - - public IPublishedContent? Parent { get; set; } - - public IEnumerable? Children { get; set; } - - public IEnumerable? ChildrenForAllCultures => Children; - - public IPublishedContentType ContentType { get; set; } - - public IEnumerable Properties { get; set; } - - public IPublishedProperty? GetProperty(string alias) => Properties?.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); - - public IPublishedProperty? GetProperty(string alias, bool recurse) + public object? this[string alias] + { + get { IPublishedProperty? property = GetProperty(alias); - if (recurse == false) - { - return property; - } + return property == null || property.HasValue() == false ? null : property.GetValue(); + } + } - IPublishedContent? content = this; - while (content != null && (property == null || property.HasValue() == false)) - { - content = content.Parent; - property = content?.GetProperty(alias); - } + public Guid Key { get; set; } + public int? TemplateId { get; set; } + + public int SortOrder { get; set; } + + public string? Name { get; set; } + + public IReadOnlyDictionary Cultures => _cultures ??= GetCultures(); + + public string? UrlSegment { get; set; } + + public int WriterId { get; set; } + + public int CreatorId { get; set; } + + public string Path { get; set; } + + public DateTime CreateDate { get; set; } + + public DateTime UpdateDate { get; set; } + + public int Level { get; set; } + + public PublishedItemType ItemType => PublishedItemType.Content; + + public IPublishedContent? Parent { get; set; } + + public bool IsDraft(string? culture = null) => false; + + public bool IsPublished(string? culture = null) => true; + + public IEnumerable? Children { get; set; } + + public IEnumerable? ChildrenForAllCultures => Children; + + public IPublishedContentType ContentType { get; set; } + + public IEnumerable Properties { get; set; } + + public IPublishedProperty? GetProperty(string alias) => + Properties?.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); + + public IPublishedProperty? GetProperty(string alias, bool recurse) + { + IPublishedProperty? property = GetProperty(alias); + if (recurse == false) + { return property; } - public object? this[string alias] + IPublishedContent? content = this; + while (content != null && (property == null || property.HasValue() == false)) { - get - { - var property = GetProperty(alias); - return property == null || property.HasValue() == false ? null : property.GetValue(); - } + content = content.Parent; + property = content?.GetProperty(alias); } + + return property; } + + private Dictionary GetCultures() => new() + { + { string.Empty, new PublishedCultureInfo(string.Empty, Name, UrlSegment, UpdateDate) }, + }; } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs index abeb19e4ec..e4e9010f5b 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs @@ -1,65 +1,70 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; +using System.Xml.XPath; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache + private readonly Dictionary _content = new(); + + public InternalPublishedContentCache() + : base(false) { - private readonly Dictionary _content = new Dictionary(); - - public InternalPublishedContentCache() - : base(false) - { - } - - //public void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of()); - - public void Clear() => _content.Clear(); - - public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); - - public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); - - public string GetRouteById(bool preview, int contentId, string? culture = null) => throw new NotImplementedException(); - - public string GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException(); - - public override IPublishedContent? GetById(bool preview, int contentId) => _content.ContainsKey(contentId) ? _content[contentId] : null; - - public override IPublishedContent GetById(bool preview, Guid contentId) => throw new NotImplementedException(); - - public override IPublishedContent GetById(bool preview, Udi nodeId) => throw new NotSupportedException(); - - public override bool HasById(bool preview, int contentId) => _content.ContainsKey(contentId); - - public override IEnumerable GetAtRoot(bool preview, string? culture = null) => _content.Values.Where(x => x.Parent == null); - - public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override IPublishedContent GetSingleByXPath(bool preview, System.Xml.XPath.XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override IEnumerable GetByXPath(bool preview, System.Xml.XPath.XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override System.Xml.XPath.XPathNavigator CreateNavigator(bool preview) => throw new NotImplementedException(); - - public override System.Xml.XPath.XPathNavigator CreateNodeNavigator(int id, bool preview) => throw new NotImplementedException(); - - public override bool HasContent(bool preview) => _content.Count > 0; - - public override IPublishedContentType GetContentType(int id) => throw new NotImplementedException(); - - public override IPublishedContentType GetContentType(string alias) => throw new NotImplementedException(); - - public override IPublishedContentType GetContentType(Guid key) => throw new NotImplementedException(); - - public override IEnumerable GetByContentType(IPublishedContentType contentType) => throw new NotImplementedException(); } + + public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); + + public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => + throw new NotImplementedException(); + + public string GetRouteById(bool preview, int contentId, string? culture = null) => + throw new NotImplementedException(); + + public string GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException(); + + public override IPublishedContent? GetById(bool preview, int contentId) => + _content.ContainsKey(contentId) ? _content[contentId] : null; + + public override IPublishedContent GetById(bool preview, Guid contentId) => throw new NotImplementedException(); + + public override IPublishedContent GetById(bool preview, Udi nodeId) => throw new NotSupportedException(); + + public override bool HasById(bool preview, int contentId) => _content.ContainsKey(contentId); + + public override IEnumerable GetAtRoot(bool preview, string? culture = null) => + _content.Values.Where(x => x.Parent == null); + + public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars) => + throw new NotImplementedException(); + + public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) => + throw new NotImplementedException(); + + public override IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars) => + throw new NotImplementedException(); + + public override IEnumerable + GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); + + public override XPathNavigator CreateNavigator(bool preview) => throw new NotImplementedException(); + + public override XPathNavigator CreateNodeNavigator(int id, bool preview) => throw new NotImplementedException(); + + public override bool HasContent(bool preview) => _content.Count > 0; + + public override IPublishedContentType GetContentType(int id) => throw new NotImplementedException(); + + public override IPublishedContentType GetContentType(string alias) => throw new NotImplementedException(); + + public override IPublishedContentType GetContentType(Guid key) => throw new NotImplementedException(); + + public override IEnumerable GetByContentType(IPublishedContentType contentType) => + throw new NotImplementedException(); + + // public void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of()); + public void Clear() => _content.Clear(); } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs index 0e7280d443..d9437e6b8c 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs @@ -1,30 +1,29 @@ using System.ComponentModel; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public class InternalPublishedProperty : IPublishedProperty { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public class InternalPublishedProperty : IPublishedProperty - { - public IPublishedPropertyType PropertyType { get; set; } = null!; + public object? SolidSourceValue { get; set; } - public string Alias { get; set; } = string.Empty; + public object? SolidValue { get; set; } - public object? SolidSourceValue { get; set; } + public bool SolidHasValue { get; set; } - public object? SolidValue { get; set; } + public object? SolidXPathValue { get; set; } - public bool SolidHasValue { get; set; } + public IPublishedPropertyType PropertyType { get; set; } = null!; - public object? SolidXPathValue { get; set; } + public string Alias { get; set; } = string.Empty; - public virtual object? GetSourceValue(string? culture = null, string? segment = null) => SolidSourceValue; + public virtual object? GetSourceValue(string? culture = null, string? segment = null) => SolidSourceValue; - public virtual object? GetValue(string? culture = null, string? segment = null) => SolidValue; + public virtual object? GetValue(string? culture = null, string? segment = null) => SolidValue; - public virtual object? GetXPathValue(string? culture = null, string? segment = null) => SolidXPathValue; + public virtual object? GetXPathValue(string? culture = null, string? segment = null) => SolidXPathValue; - public virtual bool HasValue(string? culture = null, string? segment = null) => SolidHasValue; - } + public virtual bool HasValue(string? culture = null, string? segment = null) => SolidHasValue; } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs index 0516edc47b..015962b5aa 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs @@ -1,37 +1,36 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class InternalPublishedSnapshot : IPublishedSnapshot { + public InternalPublishedContentCache InnerContentCache { get; } = new(); - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class InternalPublishedSnapshot : IPublishedSnapshot + public InternalPublishedContentCache InnerMediaCache { get; } = new(); + + public IPublishedContentCache Content => InnerContentCache; + + public IPublishedMediaCache Media => InnerMediaCache; + + public IPublishedMemberCache? Members => null; + + public IDomainCache? Domains => null; + + public IAppCache? SnapshotCache => null; + + public IDisposable ForcedPreview(bool forcedPreview, Action? callback = null) => + throw new NotImplementedException(); + + public IAppCache? ElementsCache => null; + + public void Dispose() { - public InternalPublishedContentCache InnerContentCache { get; } = new InternalPublishedContentCache(); - public InternalPublishedContentCache InnerMediaCache { get; } = new InternalPublishedContentCache(); + } - public IPublishedContentCache Content => InnerContentCache; - - public IPublishedMediaCache Media => InnerMediaCache; - - public IPublishedMemberCache? Members => null; - - public IDomainCache? Domains => null; - - public IDisposable ForcedPreview(bool forcedPreview, Action? callback = null) => throw new NotImplementedException(); - - public void Resync() - { - } - - public IAppCache? SnapshotCache => null; - - public IAppCache? ElementsCache => null; - - public void Dispose() - { - } + public void Resync() + { } } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs index bbf121b457..09de76ace5 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs @@ -1,63 +1,55 @@ -using System.Collections.Generic; using System.ComponentModel; -using System.Threading.Tasks; using Umbraco.Cms.Core.Cache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public class InternalPublishedSnapshotService : IPublishedSnapshotService { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public class InternalPublishedSnapshotService : IPublishedSnapshotService + private InternalPublishedSnapshot? _previewSnapshot; + private InternalPublishedSnapshot? _snapshot; + + public Task CollectAsync() => Task.CompletedTask; + + public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) { - private InternalPublishedSnapshot? _snapshot; - private InternalPublishedSnapshot? _previewSnapshot; - - public Task CollectAsync() => Task.CompletedTask; - - public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) + if (previewToken.IsNullOrWhiteSpace()) { - if (previewToken.IsNullOrWhiteSpace()) - { - return _snapshot ??= new InternalPublishedSnapshot(); - } - else - { - return _previewSnapshot ??= new InternalPublishedSnapshot(); - } + return _snapshot ??= new InternalPublishedSnapshot(); } - public void Dispose() - { - _snapshot?.Dispose(); - _previewSnapshot?.Dispose(); - } + return _previewSnapshot ??= new InternalPublishedSnapshot(); + } - public void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) - { - draftChanged = false; - publishedChanged = false; - } + public void Dispose() + { + _snapshot?.Dispose(); + _previewSnapshot?.Dispose(); + } - public void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) - { - anythingChanged = false; - } + public void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) + { + draftChanged = false; + publishedChanged = false; + } - public void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) - { - } + public void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) => anythingChanged = false; - public void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) - { - } + public void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) + { + } - public void Notify(DomainCacheRefresher.JsonPayload[] payloads) - { - } + public void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) + { + } - public void Rebuild(IReadOnlyCollection? contentTypeIds = null, IReadOnlyCollection? mediaTypeIds = null, IReadOnlyCollection? memberTypeIds = null) - { - } + public void Notify(DomainCacheRefresher.JsonPayload[] payloads) + { + } + + public void Rebuild(IReadOnlyCollection? contentTypeIds = null, IReadOnlyCollection? mediaTypeIds = null, IReadOnlyCollection? memberTypeIds = null) + { } } diff --git a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs index b374424b8b..3e961ce434 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs @@ -1,111 +1,87 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Xml.XPath; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public abstract class PublishedCacheBase : IPublishedCache { - public abstract class PublishedCacheBase : IPublishedCache - { - private readonly IVariationContextAccessor? _variationContextAccessor; + private readonly IVariationContextAccessor? _variationContextAccessor; - public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) - { - _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) => _variationContextAccessor = + variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - } - public bool PreviewDefault { get; } + protected PublishedCacheBase(bool previewDefault) => PreviewDefault = previewDefault; - protected PublishedCacheBase(bool previewDefault) - { - PreviewDefault = previewDefault; - } + public bool PreviewDefault { get; } - public abstract IPublishedContent? GetById(bool preview, int contentId); + public abstract IPublishedContent? GetById(bool preview, int contentId); - public IPublishedContent? GetById(int contentId) - => GetById(PreviewDefault, contentId); + public IPublishedContent? GetById(int contentId) + => GetById(PreviewDefault, contentId); - public abstract IPublishedContent? GetById(bool preview, Guid contentId); + public abstract IPublishedContent? GetById(bool preview, Guid contentId); - public IPublishedContent? GetById(Guid contentId) - => GetById(PreviewDefault, contentId); + public IPublishedContent? GetById(Guid contentId) + => GetById(PreviewDefault, contentId); - public abstract IPublishedContent? GetById(bool preview, Udi contentId); + public abstract IPublishedContent? GetById(bool preview, Udi contentId); - public IPublishedContent? GetById(Udi contentId) - => GetById(PreviewDefault, contentId); + public IPublishedContent? GetById(Udi contentId) + => GetById(PreviewDefault, contentId); - public abstract bool HasById(bool preview, int contentId); + public abstract bool HasById(bool preview, int contentId); - public bool HasById(int contentId) - => HasById(PreviewDefault, contentId); + public bool HasById(int contentId) + => HasById(PreviewDefault, contentId); - public abstract IEnumerable GetAtRoot(bool preview, string? culture = null); + public abstract IEnumerable GetAtRoot(bool preview, string? culture = null); - public IEnumerable GetAtRoot(string? culture = null) - { - return GetAtRoot(PreviewDefault, culture); - } + public IEnumerable GetAtRoot(string? culture = null) => GetAtRoot(PreviewDefault, culture); - public abstract IPublishedContent? GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); + public abstract IPublishedContent? GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); - public IPublishedContent? GetSingleByXPath(string xpath, XPathVariable[] vars) - { - return GetSingleByXPath(PreviewDefault, xpath, vars); - } + public IPublishedContent? GetSingleByXPath(string xpath, XPathVariable[] vars) => + GetSingleByXPath(PreviewDefault, xpath, vars); - public abstract IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + public abstract IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); - public IPublishedContent? GetSingleByXPath(XPathExpression xpath, XPathVariable[] vars) - { - return GetSingleByXPath(PreviewDefault, xpath, vars); - } + public IPublishedContent? GetSingleByXPath(XPathExpression xpath, XPathVariable[] vars) => + GetSingleByXPath(PreviewDefault, xpath, vars); - public abstract IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars); + public abstract IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars); - public IEnumerable GetByXPath(string xpath, XPathVariable[] vars) - { - return GetByXPath(PreviewDefault, xpath, vars); - } + public IEnumerable GetByXPath(string xpath, XPathVariable[] vars) => + GetByXPath(PreviewDefault, xpath, vars); - public abstract IEnumerable GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + public abstract IEnumerable + GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); - public IEnumerable GetByXPath(XPathExpression xpath, XPathVariable[] vars) - { - return GetByXPath(PreviewDefault, xpath, vars); - } + public IEnumerable GetByXPath(XPathExpression xpath, XPathVariable[] vars) => + GetByXPath(PreviewDefault, xpath, vars); - public abstract XPathNavigator CreateNavigator(bool preview); + public abstract XPathNavigator CreateNavigator(bool preview); - public XPathNavigator CreateNavigator() - { - return CreateNavigator(PreviewDefault); - } + public XPathNavigator CreateNavigator() => CreateNavigator(PreviewDefault); - public abstract XPathNavigator? CreateNodeNavigator(int id, bool preview); + public abstract XPathNavigator? CreateNodeNavigator(int id, bool preview); - public abstract bool HasContent(bool preview); + public abstract bool HasContent(bool preview); - public bool HasContent() - { - return HasContent(PreviewDefault); - } + public bool HasContent() => HasContent(PreviewDefault); - public abstract IPublishedContentType? GetContentType(int id); - public abstract IPublishedContentType? GetContentType(string alias); - public abstract IPublishedContentType? GetContentType(Guid key); + public abstract IPublishedContentType? GetContentType(int id); - public virtual IEnumerable GetByContentType(IPublishedContentType contentType) - { - // this is probably not super-efficient, but works - // some cache implementation may want to override it, though - return GetAtRoot() - .SelectMany(x => x.DescendantsOrSelf(_variationContextAccessor!)) - .Where(x => x.ContentType.Id == contentType.Id); - } - } + public abstract IPublishedContentType? GetContentType(string alias); + + public abstract IPublishedContentType? GetContentType(Guid key); + + public virtual IEnumerable GetByContentType(IPublishedContentType contentType) => + + // this is probably not super-efficient, but works + // some cache implementation may want to override it, though + GetAtRoot() + .SelectMany(x => x.DescendantsOrSelf(_variationContextAccessor!)) + .Where(x => x.ContentType.Id == contentType.Id); } diff --git a/src/Umbraco.Core/PublishedCache/PublishedElement.cs b/src/Umbraco.Core/PublishedCache/PublishedElement.cs index c67e3b0e40..297a62b589 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedElement.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedElement.cs @@ -1,89 +1,88 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +// notes: +// a published element does NOT manage any tree-like elements, neither the +// original NestedContent (from Lee) nor the DetachedPublishedContent POC did. +// +// at the moment we do NOT support models for sets - that would require +// an entirely new models factory + not even sure it makes sense at all since +// sets are created manually todo yes it does! - what does this all mean? +// +public class PublishedElement : IPublishedElement { - // notes: - // a published element does NOT manage any tree-like elements, neither the - // original NestedContent (from Lee) nor the DetachedPublishedContent POC did. - // - // at the moment we do NOT support models for sets - that would require - // an entirely new models factory + not even sure it makes sense at all since - // sets are created manually todo yes it does! - what does this all mean? - // - public class PublishedElement : IPublishedElement + + private readonly IPublishedProperty[] _propertiesArray; + + // initializes a new instance of the PublishedElement class + // within the context of a published snapshot service (eg a published content property value) + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, PropertyCacheLevel referenceCacheLevel, IPublishedSnapshotAccessor? publishedSnapshotAccessor) { - // initializes a new instance of the PublishedElement class - // within the context of a published snapshot service (eg a published content property value) - public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, - PropertyCacheLevel referenceCacheLevel, IPublishedSnapshotAccessor? publishedSnapshotAccessor) + if (key == Guid.Empty) { - if (key == Guid.Empty) throw new ArgumentException("Empty guid."); - if (values == null) throw new ArgumentNullException(nameof(values)); - if (referenceCacheLevel != PropertyCacheLevel.None && publishedSnapshotAccessor == null) - throw new ArgumentNullException("A published snapshot accessor is required when referenceCacheLevel != None.", nameof(publishedSnapshotAccessor)); - - ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); - Key = key; - - values = GetCaseInsensitiveValueDictionary(values); - - _propertiesArray = contentType - .PropertyTypes? - .Select(propertyType => - { - values.TryGetValue(propertyType.Alias, out var value); - return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel, value, publishedSnapshotAccessor); - }) - .ToArray() - ?? new IPublishedProperty[0]; + throw new ArgumentException("Empty guid."); } - // initializes a new instance of the PublishedElement class - // without any context, so it's purely 'standalone' and should NOT interfere with the published snapshot service - // + using an initial reference cache level of .None ensures that everything will be - // cached at .Content level - and that reference cache level will propagate to all - // properties - public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing) - : this(contentType, key, values, previewing, PropertyCacheLevel.None, null) - { } - - private static Dictionary GetCaseInsensitiveValueDictionary(Dictionary values) + if (values == null) { - // ensure we ignore case for property aliases - var comparer = values.Comparer; - var ignoreCase = Equals(comparer, StringComparer.OrdinalIgnoreCase) || Equals(comparer, StringComparer.InvariantCultureIgnoreCase) || Equals(comparer, StringComparer.CurrentCultureIgnoreCase); - return ignoreCase ? values : new Dictionary(values, StringComparer.OrdinalIgnoreCase); + throw new ArgumentNullException(nameof(values)); } - #region ContentType - - public IPublishedContentType ContentType { get; } - - #endregion - - #region PublishedElement - - public Guid Key { get; } - - #endregion - - #region Properties - - private readonly IPublishedProperty[] _propertiesArray; - - public IEnumerable Properties => _propertiesArray; - - public IPublishedProperty? GetProperty(string alias) + if (referenceCacheLevel != PropertyCacheLevel.None && publishedSnapshotAccessor == null) { - var index = ContentType.GetPropertyIndex(alias); - var property = index < 0 ? null : _propertiesArray?[index]; - return property; + throw new ArgumentNullException( + "A published snapshot accessor is required when referenceCacheLevel != None.", + nameof(publishedSnapshotAccessor)); } - #endregion + ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); + Key = key; + + values = GetCaseInsensitiveValueDictionary(values); + + _propertiesArray = contentType + .PropertyTypes? + .Select(propertyType => + { + values.TryGetValue(propertyType.Alias, out var value); + return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel, value, publishedSnapshotAccessor); + }) + .ToArray() + ?? new IPublishedProperty[0]; + } + + // initializes a new instance of the PublishedElement class + // without any context, so it's purely 'standalone' and should NOT interfere with the published snapshot service + // + using an initial reference cache level of .None ensures that everything will be + // cached at .Content level - and that reference cache level will propagate to all + // properties + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing) + : this(contentType, key, values, previewing, PropertyCacheLevel.None, null) + { + } + + public IPublishedContentType ContentType { get; } + + public Guid Key { get; } + + private static Dictionary GetCaseInsensitiveValueDictionary(Dictionary values) + { + // ensure we ignore case for property aliases + IEqualityComparer comparer = values.Comparer; + var ignoreCase = Equals(comparer, StringComparer.OrdinalIgnoreCase) || + Equals(comparer, StringComparer.InvariantCultureIgnoreCase) || + Equals(comparer, StringComparer.CurrentCultureIgnoreCase); + return ignoreCase ? values : new Dictionary(values, StringComparer.OrdinalIgnoreCase); + } + + public IEnumerable Properties => _propertiesArray; + + public IPublishedProperty? GetProperty(string alias) + { + var index = ContentType.GetPropertyIndex(alias); + IPublishedProperty? property = index < 0 ? null : _propertiesArray?[index]; + return property; } } diff --git a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs index c6fe365be8..6beb094bef 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs @@ -1,197 +1,224 @@ -using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +internal class PublishedElementPropertyBase : PublishedPropertyBase { - internal class PublishedElementPropertyBase : PublishedPropertyBase + protected readonly IPublishedElement Element; + + // define constant - determines whether to use cache when previewing + // to store eg routes, property converted values, anything - caching + // means faster execution, but uses memory - not sure if we want it + // so making it configurable. + private const bool FullCacheWhenPreviewing = true; + private readonly object _locko = new(); + private readonly IPublishedSnapshotAccessor? _publishedSnapshotAccessor; + private readonly object? _sourceValue; + protected readonly bool IsMember; + protected readonly bool IsPreviewing; + private CacheValues? _cacheValues; + + private bool _interInitialized; + private object? _interValue; + private string? _valuesCacheKey; + + public PublishedElementPropertyBase( + IPublishedPropertyType propertyType, + IPublishedElement element, + bool previewing, + PropertyCacheLevel referenceCacheLevel, + object? sourceValue = null, + IPublishedSnapshotAccessor? publishedSnapshotAccessor = null) + : base(propertyType, referenceCacheLevel) { - private readonly object _locko = new object(); - private readonly object? _sourceValue; - private readonly IPublishedSnapshotAccessor? _publishedSnapshotAccessor; + _sourceValue = sourceValue; + _publishedSnapshotAccessor = publishedSnapshotAccessor; + Element = element; + IsPreviewing = previewing; + IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; + } - protected readonly IPublishedElement Element; - protected readonly bool IsPreviewing; - protected readonly bool IsMember; + // used to cache the CacheValues of this property + // ReSharper disable InconsistentlySynchronizedField + internal string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValues(Element.Key, Alias, IsPreviewing); - private bool _interInitialized; - private object? _interValue; - private CacheValues? _cacheValues; - private string? _valuesCacheKey; + public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) => + "PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]"; - // define constant - determines whether to use cache when previewing - // to store eg routes, property converted values, anything - caching - // means faster execution, but uses memory - not sure if we want it - // so making it configurable. - private const bool FullCacheWhenPreviewing = true; - - public PublishedElementPropertyBase(IPublishedPropertyType propertyType, IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, object? sourceValue = null, IPublishedSnapshotAccessor? publishedSnapshotAccessor = null) - : base(propertyType, referenceCacheLevel) + // ReSharper restore InconsistentlySynchronizedField + public override bool HasValue(string? culture = null, string? segment = null) + { + var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); + if (hasValue.HasValue) { - _sourceValue = sourceValue; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - Element = element; - IsPreviewing = previewing; - IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; + return hasValue.Value; } - public override bool HasValue(string? culture = null, string? segment = null) + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); + + lock (_locko) { - var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); - if (hasValue.HasValue) return hasValue.Value; - - GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); - - lock (_locko) + var value = GetInterValue(); + hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (hasValue.HasValue) { - var value = GetInterValue(); - hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); - if (hasValue.HasValue) return hasValue.Value; - - var cacheValues = GetCacheValues(cacheLevel); - if (!cacheValues.ObjectInitialized) - { - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing); - cacheValues.ObjectInitialized = true; - } - value = cacheValues.ObjectValue; - return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; + return hasValue.Value; } + + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (!cacheValues.ObjectInitialized) + { + cacheValues.ObjectValue = + PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing); + cacheValues.ObjectInitialized = true; + } + + value = cacheValues.ObjectValue; + return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; + } + } + + public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue; + + private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) + { + // based upon the current reference cache level (ReferenceCacheLevel) and this property + // cache level (PropertyType.CacheLevel), determines both the actual cache level for the + // property, and the new reference cache level. + + // if the property cache level is 'shorter-termed' that the reference + // then use it and it becomes the new reference, else use Content and + // don't change the reference. + // + // examples: + // currently (reference) caching at published snapshot, property specifies + // elements, ok to use element. OTOH, currently caching at elements, + // property specifies snapshot, need to use snapshot. + if (PropertyType.CacheLevel > ReferenceCacheLevel || PropertyType.CacheLevel == PropertyCacheLevel.None) + { + cacheLevel = PropertyType.CacheLevel; + referenceCacheLevel = cacheLevel; + } + else + { + cacheLevel = PropertyCacheLevel.Element; + referenceCacheLevel = ReferenceCacheLevel; + } + } + + private IAppCache? GetSnapshotCache() + { + // cache within the snapshot cache, unless previewing, then use the snapshot or + // elements cache (if we don't want to pollute the elements cache with short-lived + // data) depending on settings + // for members, always cache in the snapshot cache - never pollute elements cache + if (_publishedSnapshotAccessor is null) + { + return null; } - // used to cache the CacheValues of this property - // ReSharper disable InconsistentlySynchronizedField - internal string ValuesCacheKey => _valuesCacheKey - ?? (_valuesCacheKey = PropertyCacheValues(Element.Key, Alias, IsPreviewing)); - // ReSharper restore InconsistentlySynchronizedField - - protected class CacheValues + if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) { - public bool ObjectInitialized; - public object? ObjectValue; - public bool XPathInitialized; - public object? XPathValue; + return null; } - public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) => "PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]"; + return (IsPreviewing == false || FullCacheWhenPreviewing) && IsMember == false + ? publishedSnapshot!.ElementsCache + : publishedSnapshot!.SnapshotCache; + } - private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) + private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) + { + CacheValues cacheValues; + switch (cacheLevel) { - // based upon the current reference cache level (ReferenceCacheLevel) and this property - // cache level (PropertyType.CacheLevel), determines both the actual cache level for the - // property, and the new reference cache level. + case PropertyCacheLevel.None: + // never cache anything + cacheValues = new CacheValues(); + break; + case PropertyCacheLevel.Element: + // cache within the property object itself, ie within the content object + cacheValues = _cacheValues ??= new CacheValues(); + break; + case PropertyCacheLevel.Elements: + // cache within the elements cache, depending... + IAppCache? snapshotCache = GetSnapshotCache(); + cacheValues = (CacheValues?)snapshotCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? + new CacheValues(); + break; + case PropertyCacheLevel.Snapshot: + IPublishedSnapshot? publishedSnapshot = _publishedSnapshotAccessor?.GetRequiredPublishedSnapshot(); - // if the property cache level is 'shorter-termed' that the reference - // then use it and it becomes the new reference, else use Content and - // don't change the reference. - // - // examples: - // currently (reference) caching at published snapshot, property specifies - // elements, ok to use element. OTOH, currently caching at elements, - // property specifies snapshot, need to use snapshot. - // - if (PropertyType.CacheLevel > ReferenceCacheLevel || PropertyType.CacheLevel == PropertyCacheLevel.None) - { - cacheLevel = PropertyType.CacheLevel; - referenceCacheLevel = cacheLevel; - } - else - { - cacheLevel = PropertyCacheLevel.Element; - referenceCacheLevel = ReferenceCacheLevel; - } + // cache within the snapshot cache + IAppCache? facadeCache = publishedSnapshot?.SnapshotCache; + cacheValues = (CacheValues?)facadeCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? + new CacheValues(); + break; + default: + throw new InvalidOperationException("Invalid cache level."); } - private IAppCache? GetSnapshotCache() + return cacheValues; + } + + private object? GetInterValue() + { + if (_interInitialized) { - // cache within the snapshot cache, unless previewing, then use the snapshot or - // elements cache (if we don't want to pollute the elements cache with short-lived - // data) depending on settings - // for members, always cache in the snapshot cache - never pollute elements cache - if (_publishedSnapshotAccessor is null) - { - return null; - } - - if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out var publishedSnapshot)) - { - return null; - } - - return (IsPreviewing == false || FullCacheWhenPreviewing) && IsMember == false - ? publishedSnapshot!.ElementsCache - : publishedSnapshot!.SnapshotCache; - } - - private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) - { - CacheValues cacheValues; - switch (cacheLevel) - { - case PropertyCacheLevel.None: - // never cache anything - cacheValues = new CacheValues(); - break; - case PropertyCacheLevel.Element: - // cache within the property object itself, ie within the content object - cacheValues = _cacheValues ?? (_cacheValues = new CacheValues()); - break; - case PropertyCacheLevel.Elements: - // cache within the elements cache, depending... - var snapshotCache = GetSnapshotCache(); - cacheValues = (CacheValues?) snapshotCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? new CacheValues(); - break; - case PropertyCacheLevel.Snapshot: - var publishedSnapshot = _publishedSnapshotAccessor?.GetRequiredPublishedSnapshot(); - // cache within the snapshot cache - var facadeCache = publishedSnapshot?.SnapshotCache; - cacheValues = (CacheValues?) facadeCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? new CacheValues(); - break; - default: - throw new InvalidOperationException("Invalid cache level."); - } - return cacheValues; - } - - private object? GetInterValue() - { - if (_interInitialized) return _interValue; - - _interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing); - _interInitialized = true; return _interValue; } - public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue; + _interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing); + _interInitialized = true; + return _interValue; + } - public override object? GetValue(string? culture = null, string? segment = null) + public override object? GetValue(string? culture = null, string? segment = null) + { + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); + + lock (_locko) { - GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); - - lock (_locko) + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (cacheValues.ObjectInitialized) { - var cacheValues = GetCacheValues(cacheLevel); - if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); - cacheValues.ObjectInitialized = true; return cacheValues.ObjectValue; } - } - public override object? GetXPathValue(string? culture = null, string? segment = null) - { - GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); - - lock (_locko) - { - var cacheValues = GetCacheValues(cacheLevel); - if (cacheValues.XPathInitialized) return cacheValues.XPathValue; - cacheValues.XPathValue = PropertyType.ConvertInterToXPath(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); - cacheValues.XPathInitialized = true; - return cacheValues.XPathValue; - } + cacheValues.ObjectValue = + PropertyType.ConvertInterToObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); + cacheValues.ObjectInitialized = true; + return cacheValues.ObjectValue; } } + + public override object? GetXPathValue(string? culture = null, string? segment = null) + { + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); + + lock (_locko) + { + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (cacheValues.XPathInitialized) + { + return cacheValues.XPathValue; + } + + cacheValues.XPathValue = + PropertyType.ConvertInterToXPath(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); + cacheValues.XPathInitialized = true; + return cacheValues.XPathValue; + } + } + + protected class CacheValues + { + public bool ObjectInitialized; + public object? ObjectValue; + public bool XPathInitialized; + public object? XPathValue; + } } diff --git a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs index 7f81d066f2..8f3e4fe827 100644 --- a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs @@ -1,46 +1,44 @@ -using System; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +// TODO: This is a mess. This is a circular reference: +// IPublishedSnapshotAccessor -> PublishedSnapshotService -> UmbracoContext -> PublishedSnapshotService -> IPublishedSnapshotAccessor +// Injecting IPublishedSnapshotAccessor into PublishedSnapshotService seems pretty strange +// The underlying reason for this mess is because IPublishedContent is both a service and a model. +// Until that is fixed, IPublishedContent will need to have a IPublishedSnapshotAccessor +public class UmbracoContextPublishedSnapshotAccessor : IPublishedSnapshotAccessor { - // TODO: This is a mess. This is a circular reference: - // IPublishedSnapshotAccessor -> PublishedSnapshotService -> UmbracoContext -> PublishedSnapshotService -> IPublishedSnapshotAccessor - // Injecting IPublishedSnapshotAccessor into PublishedSnapshotService seems pretty strange - // The underlying reason for this mess is because IPublishedContent is both a service and a model. - // Until that is fixed, IPublishedContent will need to have a IPublishedSnapshotAccessor - public class UmbracoContextPublishedSnapshotAccessor : IPublishedSnapshotAccessor + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public UmbracoContextPublishedSnapshotAccessor(IUmbracoContextAccessor umbracoContextAccessor) => + _umbracoContextAccessor = umbracoContextAccessor; + + public IPublishedSnapshot? PublishedSnapshot { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - - public UmbracoContextPublishedSnapshotAccessor(IUmbracoContextAccessor umbracoContextAccessor) + get { - _umbracoContextAccessor = umbracoContextAccessor; - } - - public IPublishedSnapshot? PublishedSnapshot - { - get + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return null; - } - return umbracoContext?.PublishedSnapshot; + return null; } - set => throw new NotSupportedException(); // not ok to set + return umbracoContext?.PublishedSnapshot; } - public bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) + set => throw new NotSupportedException(); // not ok to set + } + + public bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - publishedSnapshot = null; - return false; - } - publishedSnapshot = umbracoContext?.PublishedSnapshot; - - return publishedSnapshot is not null; + publishedSnapshot = null; + return false; } + + publishedSnapshot = umbracoContext?.PublishedSnapshot; + + return publishedSnapshot is not null; } } diff --git a/src/Umbraco.Core/ReflectionUtilities.cs b/src/Umbraco.Core/ReflectionUtilities.cs index 982e0835fb..a6c58466d2 100644 --- a/src/Umbraco.Core/ReflectionUtilities.cs +++ b/src/Umbraco.Core/ReflectionUtilities.cs @@ -1,919 +1,1187 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using System.Reflection; using System.Reflection.Emit; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides utilities to simplify reflection. +/// +/// +/// +/// Readings: +/// * CIL instructions: https://en.wikipedia.org/wiki/List_of_CIL_instructions +/// * ECMA 335: https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf +/// * MSIL programming: http://www.blackbeltcoder.com/Articles/net/msil-programming-part-1 +/// +/// +/// Supports emitting constructors, instance and static methods, instance property getters and +/// setters. Does not support static properties yet. +/// +/// +public static class ReflectionUtilities { + #region Fields + /// - /// Provides utilities to simplify reflection. + /// Emits a field getter. /// - /// - /// Readings: - /// * CIL instructions: https://en.wikipedia.org/wiki/List_of_CIL_instructions - /// * ECMA 335: https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf - /// * MSIL programming: http://www.blackbeltcoder.com/Articles/net/msil-programming-part-1 - /// - /// Supports emitting constructors, instance and static methods, instance property getters and - /// setters. Does not support static properties yet. - /// - public static class ReflectionUtilities + /// The declaring type. + /// The field type. + /// The name of the field. + /// + /// A field getter function. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + public static Func EmitFieldGetter(string fieldName) { - #region Fields - - /// - /// Emits a field getter. - /// - /// The declaring type. - /// The field type. - /// The name of the field. - /// - /// A field getter function. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - public static Func EmitFieldGetter(string fieldName) - { - var field = GetField(fieldName); - return EmitFieldGetter(field); - } - - /// - /// Emits a field setter. - /// - /// The declaring type. - /// The field type. - /// The name of the field. - /// - /// A field setter action. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - public static Action EmitFieldSetter(string fieldName) - { - var field = GetField(fieldName); - return EmitFieldSetter(field); - } - - /// - /// Emits a field getter and setter. - /// - /// The declaring type. - /// The field type. - /// The name of the field. - /// - /// A field getter and setter functions. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - public static (Func, Action) EmitFieldGetterAndSetter(string fieldName) - { - var field = GetField(fieldName); - return (EmitFieldGetter(field), EmitFieldSetter(field)); - } - - /// - /// Gets the field. - /// - /// The type of the declaring. - /// The type of the value. - /// Name of the field. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - private static FieldInfo GetField(string fieldName) - { - if (fieldName == null) throw new ArgumentNullException(nameof(fieldName)); - if (string.IsNullOrWhiteSpace(fieldName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(fieldName)); - - // get the field - var field = typeof(TDeclaring).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (field == null) throw new InvalidOperationException($"Could not find field {typeof(TDeclaring)}.{fieldName}."); - - // validate field type - if (field.FieldType != typeof(TValue)) // strict - throw new ArgumentException($"Value type {typeof(TValue)} does not match field {typeof(TDeclaring)}.{fieldName} type {field.FieldType}."); - - return field; - } - - private static Func EmitFieldGetter(FieldInfo field) - { - // emit - var (dm, ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new [] { typeof(TDeclaring) }, typeof(TValue)); - ilgen.Emit(OpCodes.Ldarg_0); - ilgen.Emit(OpCodes.Ldfld, field); - ilgen.Return(); - - return (Func) (object) dm.CreateDelegate(typeof(Func)); - } - - private static Action EmitFieldSetter(FieldInfo field) - { - // emit - var (dm, ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new [] { typeof(TDeclaring), typeof(TValue) }, typeof(void)); - ilgen.Emit(OpCodes.Ldarg_0); - ilgen.Emit(OpCodes.Ldarg_1); - ilgen.Emit(OpCodes.Stfld, field); - ilgen.Return(); - - return (Action) (object) dm.CreateDelegate(typeof(Action)); - } - - #endregion - - #region Properties - - /// - /// Emits a property getter. - /// - /// The declaring type. - /// The property type. - /// The name of the property. - /// A value indicating whether the property and its getter must exist. - /// - /// A property getter function. If is false, returns null when the property or its getter does not exist. - /// - /// propertyName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match property . type. - /// Could not find property getter for .. - public static Func? EmitPropertyGetter(string propertyName, bool mustExist = true) - { - if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); - - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - if (property?.GetMethod != null) - return EmitMethod>(property.GetMethod); - - if (!mustExist) - return default; - - throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}."); - } - - /// - /// Emits a property setter. - /// - /// The declaring type. - /// The property type. - /// The name of the property. - /// A value indicating whether the property and its setter must exist. - /// - /// A property setter function. If is false, returns null when the property or its setter does not exist. - /// - /// propertyName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match property . type. - /// Could not find property setter for .. - public static Action? EmitPropertySetter(string propertyName, bool mustExist = true) - { - if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); - - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - if (property?.SetMethod != null) - return EmitMethod>(property.SetMethod); - - if (!mustExist) - return default; - - throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); - } - - /// - /// Emits a property getter and setter. - /// - /// The declaring type. - /// The property type. - /// The name of the property. - /// A value indicating whether the property and its getter and setter must exist. - /// - /// A property getter and setter functions. If is false, returns null when the property or its getter or setter does not exist. - /// - /// propertyName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match property . type. - /// Could not find property getter and setter for .. - public static (Func, Action) EmitPropertyGetterAndSetter(string propertyName, bool mustExist = true) - { - if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); - - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - if (property?.GetMethod != null && property.SetMethod != null) - return ( - EmitMethod>(property.GetMethod), - EmitMethod>(property.SetMethod)); - - if (!mustExist) - return default; - - throw new InvalidOperationException($"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); - } - - /// - /// Emits a property getter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property getter function. - /// Occurs when is null. - /// Occurs when the property has no getter. - /// Occurs when does not match the type of the property. - public static Func EmitPropertyGetter(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.GetMethod == null) - throw new ArgumentException("Property has no getter.", nameof(propertyInfo)); - - return EmitMethod>(propertyInfo.GetMethod); - } - - /// - /// Emits a property setter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property setter function. - /// Occurs when is null. - /// Occurs when the property has no setter. - /// Occurs when does not match the type of the property. - public static Action EmitPropertySetter(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); - - return EmitMethod>(propertyInfo.SetMethod); - } - - /// - /// Emits a property getter and setter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property getter and setter functions. - /// Occurs when is null. - /// Occurs when the property has no getter or no setter. - /// Occurs when does not match the type of the property. - public static (Func, Action) EmitPropertyGetterAndSetter(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo)); - - return ( - EmitMethod>(propertyInfo.GetMethod), - EmitMethod>(propertyInfo.SetMethod)); - } - - /// - /// Emits a property setter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property setter function. - /// Occurs when is null. - /// Occurs when the property has no setter. - /// Occurs when does not match the type of the property. - public static Action EmitPropertySetterUnsafe(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); - - return EmitMethodUnsafe>(propertyInfo.SetMethod); - } - - #endregion - - #region Constructors - - /// - /// Emits a constructor. - /// - /// A lambda representing the constructor. - /// A value indicating whether the constructor must exist. - /// The optional type of the class to construct. - /// A constructor function. If is false, returns null when the constructor does not exist. - /// - /// When is not specified, it is the type returned by . - /// The constructor arguments are determined by generic arguments. - /// The type returned by does not need to be exactly , - /// when e.g. that type is not known at compile time, but it has to be a parent type (eg an interface, or object). - /// - /// Occurs when the constructor does not exist and is true. - /// Occurs when is not a Func or when - /// is specified and does not match the function's returned type. - public static TLambda? EmitConstructor(bool mustExist = true, Type? declaring = null) - { - var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); - - // determine returned / declaring type - if (declaring == null) declaring = lambdaReturned; - else if (!lambdaReturned.IsAssignableFrom(declaring)) - throw new ArgumentException($"Type {lambdaReturned} is not assignable from type {declaring}.", nameof(declaring)); - - // get the constructor infos - var ctor = declaring.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); - if (ctor == null) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable) lambdaParameters)})."); - } - - // emit - return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); - } - - /// - /// Emits a constructor. - /// - /// A lambda representing the constructor. - /// The constructor info. - /// A constructor function. - /// Occurs when is not a Func or when its generic - /// arguments do not match those of . - /// Occurs when is null. - public static TLambda EmitConstructor(ConstructorInfo ctor) - { - if (ctor == null) throw new ArgumentNullException(nameof(ctor)); - - var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); - - return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); - } - - private static TLambda EmitConstructorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) - { - // get type and args - var ctorDeclaring = ctor.DeclaringType; - var ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); - - // validate arguments - if (lambdaParameters.Length != ctorParameters.Length) - ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); - for (var i = 0; i < lambdaParameters.Length; i++) - if (lambdaParameters[i] != ctorParameters[i]) // note: relax the constraint with IsAssignableFrom? - ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); - if (!returned.IsAssignableFrom(ctorDeclaring)) - ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); - - // emit - return EmitConstructor(ctorDeclaring, ctorParameters, ctor); - } - - /// - /// Emits a constructor. - /// - /// A lambda representing the constructor. - /// The constructor info. - /// A constructor function. - /// - /// The constructor is emitted in an unsafe way, using the lambda arguments without verifying - /// them at all. This assumes that the calling code is taking care of all verifications, in order - /// to avoid cast errors. - /// - /// Occurs when is not a Func or when its generic - /// arguments do not match those of . - /// Occurs when is null. - public static TLambda EmitConstructorUnsafe(ConstructorInfo ctor) - { - if (ctor == null) throw new ArgumentNullException(nameof(ctor)); - - var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); - - // emit - unsafe - use lambda's args and assume they are correct - return EmitConstructor(lambdaReturned, lambdaParameters, ctor); - } - - private static TLambda EmitConstructor(Type? declaring, Type[] lambdaParameters, ConstructorInfo ctor) - { - // gets the method argument types - var ctorParameters = GetParameters(ctor); - - // emit - var (dm, ilgen) = CreateIlGenerator(ctor.DeclaringType?.Module, lambdaParameters, declaring); - EmitLdargs(ilgen, lambdaParameters, ctorParameters); - ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects - ilgen.Return(); - - return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); - } - - #endregion - - #region Methods - - /// - /// Emits a static method. - /// - /// The declaring type. - /// A lambda representing the method. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// - /// The method. If is false, returns null when the method does not exist. - /// - /// methodName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Occurs when does not match the method signature.. - /// Occurs when no proper method with name could be found. - /// - /// The method arguments are determined by generic arguments. - /// - public static TLambda? EmitMethod(string methodName, bool mustExist = true) - { - return EmitMethod(typeof(TDeclaring), methodName, mustExist); - } - - /// - /// Emits a static method. - /// - /// A lambda representing the method. - /// The declaring type. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// - /// The method. If is false, returns null when the method does not exist. - /// - /// methodName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Occurs when does not match the method signature.. - /// Occurs when no proper method with name could be found. - /// - /// The method arguments are determined by generic arguments. - /// - public static TLambda? EmitMethod(Type declaring, string methodName, bool mustExist = true) - { - if (methodName == null) throw new ArgumentNullException(nameof(methodName)); - if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); - - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, out var isFunction); - - // get the method infos - var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, lambdaParameters, null); - if (method == null || isFunction && !lambdaReturned.IsAssignableFrom(method.ReturnType)) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)})."); - } - - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - /// - /// Emits a method. - /// - /// A lambda representing the method. - /// The method info. - /// The method. - /// Occurs when is null. - /// Occurs when Occurs when does not match the method signature. - public static TLambda EmitMethod(MethodInfo method) - { - if (method == null) throw new ArgumentNullException(nameof(method)); - - // get type and args - var methodDeclaring = method.DeclaringType; - var methodReturned = method.ReturnType; - var methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray(); - - var isStatic = method.IsStatic; - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(isStatic, out var isFunction); - - // if not static, then the first lambda arg must be the method declaring type - if (!isStatic && (methodDeclaring == null || !methodDeclaring.IsAssignableFrom(lambdaDeclaring))) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - if (methodParameters.Length != lambdaParameters.Length) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - for (var i = 0; i < methodParameters.Length; i++) - if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i])) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - // if it's a function then the last lambda arg must match the method returned type - if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned)) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - /// - /// Emits a method. - /// - /// A lambda representing the method. - /// The method info. - /// The method. - /// Occurs when is null. - /// Occurs when Occurs when does not match the method signature. - public static TLambda EmitMethodUnsafe(MethodInfo method) - { - if (method == null) throw new ArgumentNullException(nameof(method)); - - var isStatic = method.IsStatic; - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(isStatic, out _); - - // emit - unsafe - use lambda's args and assume they are correct - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - /// - /// Emits an instance method. - /// - /// A lambda representing the method. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// - /// The method. If is false, returns null when the method does not exist. - /// - /// methodName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Occurs when does not match the method signature.. - /// Occurs when no proper method with name could be found. - /// - /// The method arguments are determined by generic arguments. - /// - public static TLambda? EmitMethod(string methodName, bool mustExist = true) - { - if (methodName == null) throw new ArgumentNullException(nameof(methodName)); - if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); - - // validate lambda type - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(false, out var isFunction); - - // get the method infos - var method = lambdaDeclaring?.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); - if (method == null || isFunction && method.ReturnType != lambdaReturned) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find method {lambdaDeclaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)})."); - } - - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - // lambdaReturned = the lambda returned type (can be void) - // lambdaArgTypes = the lambda argument types - private static TLambda EmitMethod(Type? lambdaDeclaring, Type lambdaReturned, Type[] lambdaParameters, MethodInfo method) - { - // non-static methods need the declaring type as first arg - var parameters = lambdaParameters; - if (!method.IsStatic) - { - parameters = new Type[lambdaParameters.Length + 1]; - parameters[0] = lambdaDeclaring ?? method.DeclaringType!; - Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length); - } - - // gets the method argument types - var methodArgTypes = GetParameters(method, withDeclaring: !method.IsStatic); - - // emit IL - var (dm, ilgen) = CreateIlGenerator(method.DeclaringType?.Module, parameters, lambdaReturned); - EmitLdargs(ilgen, parameters, methodArgTypes); - ilgen.CallMethod(method); - EmitOutputAdapter(ilgen, lambdaReturned, method.ReturnType); - ilgen.Return(); - - // create - return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); - } - - #endregion - - #region Utilities - - // when !isStatic, the first generic argument of the lambda is the declaring type - // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument - // when isFunction, the last generic argument of the lambda is the returned type - // everything in between is parameters - private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, bool isFunction) - { - var typeLambda = typeof(TLambda); - - var (declaring, parameters, returned) = AnalyzeLambda(isStatic, out var maybeFunction); - - if (isFunction) - { - if (!maybeFunction) - throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda)); - } - else - { - if (maybeFunction) - throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda)); - } - - return (declaring, parameters, returned); - } - - // when !isStatic, the first generic argument of the lambda is the declaring type - // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument - // when isFunction, the last generic argument of the lambda is the returned type - // everything in between is parameters - private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, out bool isFunction) - { - isFunction = false; - - var typeLambda = typeof(TLambda); - - var isAction = typeLambda.FullName == "System.Action"; - if (isAction) - { - if (!isStatic) - throw new ArgumentException($"Lambda {typeLambda} is an Action and can be used for static methods exclusively.", nameof(TLambda)); - - return (null, Array.Empty(), typeof(void)); - } - - var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; - var name = genericDefinition?.FullName; - - if (name == null) - throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); - - var isActionOf = name.StartsWith("System.Action`"); - isFunction = name.StartsWith("System.Func`"); - - if (!isActionOf && !isFunction) - throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); - - var genericArgs = typeLambda.GetGenericArguments(); - if (genericArgs.Length == 0) - throw new Exception("Panic: Func<> or Action<> has zero generic arguments."); - - var i = 0; - var declaring = isStatic ? typeof(void) : genericArgs[i++]; - - var parameterCount = genericArgs.Length - (isStatic ? 0 : 1) - (isFunction ? 1 : 0); - if (parameterCount < 0) - throw new ArgumentException($"Lambda {typeLambda} is a Func and requires at least two arguments (declaring type and returned type).", nameof(TLambda)); - - var parameters = new Type[parameterCount]; - for (var j = 0; j < parameterCount; j++) - parameters[j] = genericArgs[i++]; - - var returned = isFunction ? genericArgs[i] : typeof(void); - - return (declaring, parameters, returned); - } - - private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module? module, Type[] arguments, Type? returned) - { - if (module == null) throw new ArgumentNullException(nameof(module)); - var dm = new DynamicMethod(string.Empty, returned, arguments, module, true); - return (dm, dm.GetILGenerator()); - } - - private static Type[] GetParameters(ConstructorInfo ctor) - { - var parameters = ctor.GetParameters(); - var types = new Type[parameters.Length]; - var i = 0; - foreach (var parameter in parameters) - types[i++] = parameter.ParameterType; - return types; - } - - private static Type[] GetParameters(MethodInfo method, bool withDeclaring) - { - var parameters = method.GetParameters(); - var types = new Type[parameters.Length + (withDeclaring ? 1 : 0)]; - var i = 0; - if (withDeclaring) - types[i++] = method.DeclaringType!; - foreach (var parameter in parameters) - types[i++] = parameter.ParameterType; - return types; - } - - // emits args - private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes) - { - var ldargOpCodes = new[] { OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 }; - - if (lambdaArgTypes.Length != methodArgTypes.Length) - throw new Exception("Panic: inconsistent number of args."); - - for (var i = 0; i < lambdaArgTypes.Length; i++) - { - if (lambdaArgTypes.Length < 5) - ilgen.Emit(ldargOpCodes[i]); - else - ilgen.Emit(OpCodes.Ldarg, i); - - //var local = false; - EmitInputAdapter(ilgen, lambdaArgTypes[i], methodArgTypes[i]/*, ref local*/); - } - } - - // emits adapter opcodes after OpCodes.Ldarg - // inputType is the lambda input type - // methodParamType is the actual type expected by the actual method - // adding code to do inputType -> methodParamType - // valueType -> valueType : not supported ('cos, why?) - // valueType -> !valueType : not supported ('cos, why?) - // !valueType -> valueType : unbox and convert - // !valueType -> !valueType : cast (could throw) - private static void EmitInputAdapter(ILGenerator ilgen, Type inputType, Type methodParamType /*, ref bool local*/) - { - if (inputType == methodParamType) return; - - if (methodParamType.IsValueType) - { - if (inputType.IsValueType) - { - // both input and parameter are value types - // not supported, use proper input - // (otherwise, would require converting) - throw new NotSupportedException("ValueTypes conversion."); - } - - // parameter is value type, but input is reference type - // unbox the input to the parameter value type - // this is more or less equivalent to the ToT method below - - var unbox = ilgen.DefineLabel(); - - //if (!local) - //{ - // ilgen.DeclareLocal(typeof(object)); // declare local var for st/ld loc_0 - // local = true; - //} - - // stack: value - - // following code can be replaced with .Dump (and then we don't need the local variable anymore) - //ilgen.Emit(OpCodes.Stloc_0); // pop value into loc.0 - //// stack: - //ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 - //ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 - - ilgen.Emit(OpCodes.Dup); // duplicate top of stack - - // stack: value ; value - - ilgen.Emit(OpCodes.Isinst, methodParamType); // test, pops value, and pushes either a null ref, or an instance of the type - - // stack: inst|null ; value - - ilgen.Emit(OpCodes.Ldnull); // push null - - // stack: null ; inst|null ; value - - ilgen.Emit(OpCodes.Cgt_Un); // compare what isInst returned to null - pops 2 values, and pushes 1 if greater else 0 - - // stack: 0|1 ; value - - ilgen.Emit(OpCodes.Brtrue_S, unbox); // pops value, branches to unbox if true, ie nonzero - - // stack: value - - ilgen.Convert(methodParamType); // convert - - // stack: value|converted - - ilgen.MarkLabel(unbox); - ilgen.Emit(OpCodes.Unbox_Any, methodParamType); - } - else - { - // parameter is reference type, but input is value type - // not supported, input should always be less constrained - // (otherwise, would require boxing and converting) - if (inputType.IsValueType) - throw new NotSupportedException("ValueType boxing."); - - // both input and parameter are reference types - // cast the input to the parameter type - ilgen.Emit(OpCodes.Castclass, methodParamType); - } - } - - //private static T ToT(object o) - //{ - // return o is T t ? t : (T) System.Convert.ChangeType(o, typeof(T)); - //} - - private static MethodInfo? _convertMethod; - private static MethodInfo? _getTypeFromHandle; - - private static void Convert(this ILGenerator ilgen, Type type) - { - - if (_getTypeFromHandle == null) - _getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(RuntimeTypeHandle) }, null); - - if (_convertMethod == null) - _convertMethod = typeof(Convert).GetMethod("ChangeType", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(object), typeof(Type) }, null); - - ilgen.Emit(OpCodes.Ldtoken, type); - ilgen.CallMethod(_getTypeFromHandle); - ilgen.CallMethod(_convertMethod); - } - - // emits adapter code before OpCodes.Ret - // outputType is the lambda output type - // methodReturnedType is the actual type returned by the actual method - // adding code to do methodReturnedType -> outputType - // valueType -> valueType : not supported ('cos, why?) - // valueType -> !valueType : box - // !valueType -> valueType : not supported ('cos, why?) - // !valueType -> !valueType : implicit cast (could throw) - private static void EmitOutputAdapter(ILGenerator ilgen, Type outputType, Type methodReturnedType) - { - if (outputType == methodReturnedType) return; - - // note: the only important thing to support here, is returning a specific type - // as an object, when emitting the method as a Func<..., object> - anything else - // is pointless really - so we box value types, and ensure that non value types - // can be assigned - - if (methodReturnedType.IsValueType) - { - if (outputType.IsValueType) - { - // both returned and output are value types - // not supported, use proper output - // (otherwise, would require converting) - throw new NotSupportedException("ValueTypes conversion."); - } - - // returned is value type, but output is reference type - // box the returned value - ilgen.Emit(OpCodes.Box, methodReturnedType); - } - else - { - // returned is reference type, but output is value type - // not supported, output should always be less constrained - // (otherwise, would require boxing and converting) - if (outputType.IsValueType) - throw new NotSupportedException("ValueType boxing."); - - // both output and returned are reference types - // as long as returned can be assigned to output, good - if (!outputType.IsAssignableFrom(methodReturnedType)) - throw new NotSupportedException("Invalid cast."); - } - } - - private static void ThrowInvalidLambda(string methodName, Type? returned, Type[] args) - { - throw new ArgumentException($"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable) args)}):{returned}.", nameof(TLambda)); - } - - private static void CallMethod(this ILGenerator ilgen, MethodInfo? method) - { - if (method is not null) - { - var virt = !method.IsStatic && (method.IsVirtual || !method.IsFinal); - ilgen.Emit(virt ? OpCodes.Callvirt : OpCodes.Call, method); - } - } - - private static void Return(this ILGenerator ilgen) - { - ilgen.Emit(OpCodes.Ret); - } - - #endregion + FieldInfo field = GetField(fieldName); + return EmitFieldGetter(field); } + + /// + /// Emits a field setter. + /// + /// The declaring type. + /// The field type. + /// The name of the field. + /// + /// A field setter action. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + public static Action EmitFieldSetter(string fieldName) + { + FieldInfo field = GetField(fieldName); + return EmitFieldSetter(field); + } + + /// + /// Emits a field getter and setter. + /// + /// The declaring type. + /// The field type. + /// The name of the field. + /// + /// A field getter and setter functions. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + public static (Func, Action) EmitFieldGetterAndSetter( + string fieldName) + { + FieldInfo field = GetField(fieldName); + return (EmitFieldGetter(field), EmitFieldSetter(field)); + } + + /// + /// Gets the field. + /// + /// The type of the declaring. + /// The type of the value. + /// Name of the field. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + private static FieldInfo GetField(string fieldName) + { + if (fieldName == null) + { + throw new ArgumentNullException(nameof(fieldName)); + } + + if (string.IsNullOrWhiteSpace(fieldName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(fieldName)); + } + + // get the field + FieldInfo? field = typeof(TDeclaring).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field == null) + { + throw new InvalidOperationException($"Could not find field {typeof(TDeclaring)}.{fieldName}."); + } + + // validate field type + if (field.FieldType != typeof(TValue)) + { + throw new ArgumentException( + $"Value type {typeof(TValue)} does not match field {typeof(TDeclaring)}.{fieldName} type {field.FieldType}."); + } + + return field; + } + + private static Func EmitFieldGetter(FieldInfo field) + { + // emit + (DynamicMethod dm, ILGenerator ilgen) = + CreateIlGenerator(field.DeclaringType?.Module, new[] { typeof(TDeclaring) }, typeof(TValue)); + ilgen.Emit(OpCodes.Ldarg_0); + ilgen.Emit(OpCodes.Ldfld, field); + ilgen.Return(); + + return (Func)dm.CreateDelegate(typeof(Func)); + } + + private static Action EmitFieldSetter(FieldInfo field) + { + // emit + (DynamicMethod dm, ILGenerator ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new[] { typeof(TDeclaring), typeof(TValue) }, typeof(void)); + ilgen.Emit(OpCodes.Ldarg_0); + ilgen.Emit(OpCodes.Ldarg_1); + ilgen.Emit(OpCodes.Stfld, field); + ilgen.Return(); + + return (Action)dm.CreateDelegate(typeof(Action)); + } + + #endregion + + #region Properties + + /// + /// Emits a property getter. + /// + /// The declaring type. + /// The property type. + /// The name of the property. + /// A value indicating whether the property and its getter must exist. + /// + /// A property getter function. If is false, returns null when the property or its + /// getter does not exist. + /// + /// propertyName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . + /// type. + /// + /// + /// Could not find property getter for . + /// . + /// + public static Func? EmitPropertyGetter(string propertyName, bool mustExist = true) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } + + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (property?.GetMethod != null) + { + return EmitMethod>(property.GetMethod); + } + + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}."); + } + + /// + /// Emits a property setter. + /// + /// The declaring type. + /// The property type. + /// The name of the property. + /// A value indicating whether the property and its setter must exist. + /// + /// A property setter function. If is false, returns null when the property or its + /// setter does not exist. + /// + /// propertyName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . + /// type. + /// + /// + /// Could not find property setter for . + /// . + /// + public static Action? EmitPropertySetter(string propertyName, bool mustExist = true) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } + + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (property?.SetMethod != null) + { + return EmitMethod>(property.SetMethod); + } + + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); + } + + /// + /// Emits a property getter and setter. + /// + /// The declaring type. + /// The property type. + /// The name of the property. + /// A value indicating whether the property and its getter and setter must exist. + /// + /// A property getter and setter functions. If is false, returns null when the + /// property or its getter or setter does not exist. + /// + /// propertyName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . + /// type. + /// + /// + /// Could not find property getter and setter for + /// .. + /// + public static (Func, Action) + EmitPropertyGetterAndSetter(string propertyName, bool mustExist = true) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } + + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (property?.GetMethod != null && property.SetMethod != null) + { + return ( + EmitMethod>(property.GetMethod), + EmitMethod>(property.SetMethod)); + } + + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); + } + + /// + /// Emits a property getter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property getter function. + /// Occurs when is null. + /// Occurs when the property has no getter. + /// Occurs when does not match the type of the property. + public static Func EmitPropertyGetter(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.GetMethod == null) + { + throw new ArgumentException("Property has no getter.", nameof(propertyInfo)); + } + + return EmitMethod>(propertyInfo.GetMethod); + } + + /// + /// Emits a property setter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property setter function. + /// Occurs when is null. + /// Occurs when the property has no setter. + /// Occurs when does not match the type of the property. + public static Action EmitPropertySetter(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); + } + + return EmitMethod>(propertyInfo.SetMethod); + } + + /// + /// Emits a property getter and setter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property getter and setter functions. + /// Occurs when is null. + /// Occurs when the property has no getter or no setter. + /// Occurs when does not match the type of the property. + public static (Func, Action) + EmitPropertyGetterAndSetter(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo)); + } + + return ( + EmitMethod>(propertyInfo.GetMethod), + EmitMethod>(propertyInfo.SetMethod)); + } + + /// + /// Emits a property setter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property setter function. + /// Occurs when is null. + /// Occurs when the property has no setter. + /// Occurs when does not match the type of the property. + public static Action EmitPropertySetterUnsafe(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); + } + + return EmitMethodUnsafe>(propertyInfo.SetMethod); + } + + #endregion + + #region Constructors + + /// + /// Emits a constructor. + /// + /// A lambda representing the constructor. + /// A value indicating whether the constructor must exist. + /// The optional type of the class to construct. + /// + /// A constructor function. If is false, returns null when the constructor + /// does not exist. + /// + /// + /// + /// When is not specified, it is the type returned by + /// . + /// + /// The constructor arguments are determined by generic arguments. + /// + /// The type returned by does not need to be exactly , + /// when e.g. that type is not known at compile time, but it has to be a parent type (eg an interface, or + /// object). + /// + /// + /// + /// Occurs when the constructor does not exist and + /// is true. + /// + /// + /// Occurs when is not a Func or when + /// is specified and does not match the function's returned type. + /// + public static TLambda? EmitConstructor(bool mustExist = true, Type? declaring = null) + { + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); + + // determine returned / declaring type + if (declaring == null) + { + declaring = lambdaReturned; + } + else if (!lambdaReturned.IsAssignableFrom(declaring)) + { + throw new ArgumentException($"Type {lambdaReturned} is not assignable from type {declaring}.", nameof(declaring)); + } + + // get the constructor infos + ConstructorInfo? ctor = declaring.GetConstructor( + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); + if (ctor == null) + { + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } + + // emit + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); + } + + /// + /// Emits a constructor. + /// + /// A lambda representing the constructor. + /// The constructor info. + /// A constructor function. + /// + /// Occurs when is not a Func or when its generic + /// arguments do not match those of . + /// + /// Occurs when is null. + public static TLambda EmitConstructor(ConstructorInfo ctor) + { + if (ctor == null) + { + throw new ArgumentNullException(nameof(ctor)); + } + + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); + + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); + } + + private static TLambda EmitConstructorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) + { + // get type and args + Type? ctorDeclaring = ctor.DeclaringType; + Type[] ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); + + // validate arguments + if (lambdaParameters.Length != ctorParameters.Length) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + } + + for (var i = 0; i < lambdaParameters.Length; i++) + { + // note: relax the constraint with IsAssignableFrom? + if (lambdaParameters[i] != ctorParameters[i]) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + } + } + + if (!returned.IsAssignableFrom(ctorDeclaring)) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + } + + // emit + return EmitConstructor(ctorDeclaring, ctorParameters, ctor); + } + + /// + /// Emits a constructor. + /// + /// A lambda representing the constructor. + /// The constructor info. + /// A constructor function. + /// + /// + /// The constructor is emitted in an unsafe way, using the lambda arguments without verifying + /// them at all. This assumes that the calling code is taking care of all verifications, in order + /// to avoid cast errors. + /// + /// + /// + /// Occurs when is not a Func or when its generic + /// arguments do not match those of . + /// + /// Occurs when is null. + public static TLambda EmitConstructorUnsafe(ConstructorInfo ctor) + { + if (ctor == null) + { + throw new ArgumentNullException(nameof(ctor)); + } + + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); + + // emit - unsafe - use lambda's args and assume they are correct + return EmitConstructor(lambdaReturned, lambdaParameters, ctor); + } + + private static TLambda EmitConstructor(Type? declaring, Type[] lambdaParameters, ConstructorInfo ctor) + { + // gets the method argument types + Type[] ctorParameters = GetParameters(ctor); + + // emit + (DynamicMethod dm, ILGenerator ilgen) = + CreateIlGenerator(ctor.DeclaringType?.Module, lambdaParameters, declaring); + EmitLdargs(ilgen, lambdaParameters, ctorParameters); + ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects + ilgen.Return(); + + return (TLambda)(object)dm.CreateDelegate(typeof(TLambda)); + } + + #endregion + + #region Methods + + /// + /// Emits a static method. + /// + /// The declaring type. + /// A lambda representing the method. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// + /// + /// Occurs when no proper method with name could + /// be found. + /// + /// + /// The method arguments are determined by generic arguments. + /// + public static TLambda? EmitMethod(string methodName, bool mustExist = true) => + EmitMethod(typeof(TDeclaring), methodName, mustExist); + + /// + /// Emits a static method. + /// + /// A lambda representing the method. + /// The declaring type. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// + /// + /// Occurs when no proper method with name could + /// be found. + /// + /// + /// The method arguments are determined by generic arguments. + /// + public static TLambda? EmitMethod(Type declaring, string methodName, bool mustExist = true) + { + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); + } + + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(true, out var isFunction); + + // get the method infos + MethodInfo? method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, lambdaParameters, null); + if (method == null || (isFunction && !lambdaReturned.IsAssignableFrom(method.ReturnType))) + { + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } + + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + /// + /// Emits a method. + /// + /// A lambda representing the method. + /// The method info. + /// The method. + /// Occurs when is null. + /// + /// Occurs when Occurs when does not match the method + /// signature. + /// + public static TLambda EmitMethod(MethodInfo method) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + // get type and args + Type? methodDeclaring = method.DeclaringType; + Type methodReturned = method.ReturnType; + Type[] methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray(); + + var isStatic = method.IsStatic; + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(isStatic, out var isFunction); + + // if not static, then the first lambda arg must be the method declaring type + if (!isStatic && (methodDeclaring == null || !methodDeclaring.IsAssignableFrom(lambdaDeclaring))) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + + if (methodParameters.Length != lambdaParameters.Length) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + + for (var i = 0; i < methodParameters.Length; i++) + { + if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i])) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + } + + // if it's a function then the last lambda arg must match the method returned type + if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned)) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + /// + /// Emits a method. + /// + /// A lambda representing the method. + /// The method info. + /// The method. + /// Occurs when is null. + /// + /// Occurs when Occurs when does not match the method + /// signature. + /// + public static TLambda EmitMethodUnsafe(MethodInfo method) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + var isStatic = method.IsStatic; + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(isStatic, out _); + + // emit - unsafe - use lambda's args and assume they are correct + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + /// + /// Emits an instance method. + /// + /// A lambda representing the method. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// + /// + /// Occurs when no proper method with name could + /// be found. + /// + /// + /// The method arguments are determined by generic arguments. + /// + public static TLambda? EmitMethod(string methodName, bool mustExist = true) + { + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); + } + + // validate lambda type + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(false, out var isFunction); + + // get the method infos + MethodInfo? method = lambdaDeclaring?.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); + if (method == null || (isFunction && method.ReturnType != lambdaReturned)) + { + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find method {lambdaDeclaring}.{methodName}({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } + + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + // lambdaReturned = the lambda returned type (can be void) + // lambdaArgTypes = the lambda argument types + private static TLambda EmitMethod(Type? lambdaDeclaring, Type lambdaReturned, Type[] lambdaParameters, MethodInfo method) + { + // non-static methods need the declaring type as first arg + Type[] parameters = lambdaParameters; + if (!method.IsStatic) + { + parameters = new Type[lambdaParameters.Length + 1]; + parameters[0] = lambdaDeclaring ?? method.DeclaringType!; + Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length); + } + + // gets the method argument types + Type[] methodArgTypes = GetParameters(method, !method.IsStatic); + + // emit IL + (DynamicMethod dm, ILGenerator ilgen) = + CreateIlGenerator(method.DeclaringType?.Module, parameters, lambdaReturned); + EmitLdargs(ilgen, parameters, methodArgTypes); + ilgen.CallMethod(method); + EmitOutputAdapter(ilgen, lambdaReturned, method.ReturnType); + ilgen.Return(); + + // create + return (TLambda)(object)dm.CreateDelegate(typeof(TLambda)); + } + + #endregion + + #region Utilities + + // when !isStatic, the first generic argument of the lambda is the declaring type + // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument + // when isFunction, the last generic argument of the lambda is the returned type + // everything in between is parameters + private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, bool isFunction) + { + Type typeLambda = typeof(TLambda); + + (Type? declaring, Type[] parameters, Type returned) = AnalyzeLambda(isStatic, out var maybeFunction); + + if (isFunction) + { + if (!maybeFunction) + { + throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda)); + } + } + else + { + if (maybeFunction) + { + throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda)); + } + } + + return (declaring, parameters, returned); + } + + // when !isStatic, the first generic argument of the lambda is the declaring type + // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument + // when isFunction, the last generic argument of the lambda is the returned type + // everything in between is parameters + private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, out bool isFunction) + { + isFunction = false; + + Type typeLambda = typeof(TLambda); + + var isAction = typeLambda.FullName == "System.Action"; + if (isAction) + { + if (!isStatic) + { + throw new ArgumentException( + $"Lambda {typeLambda} is an Action and can be used for static methods exclusively.", + nameof(TLambda)); + } + + return (null, Array.Empty(), typeof(void)); + } + + Type? genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; + var name = genericDefinition?.FullName; + + if (name == null) + { + throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + } + + var isActionOf = name.StartsWith("System.Action`"); + isFunction = name.StartsWith("System.Func`"); + + if (!isActionOf && !isFunction) + { + throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + } + + Type[] genericArgs = typeLambda.GetGenericArguments(); + if (genericArgs.Length == 0) + { + throw new Exception("Panic: Func<> or Action<> has zero generic arguments."); + } + + var i = 0; + Type declaring = isStatic ? typeof(void) : genericArgs[i++]; + + var parameterCount = genericArgs.Length - (isStatic ? 0 : 1) - (isFunction ? 1 : 0); + if (parameterCount < 0) + { + throw new ArgumentException( + $"Lambda {typeLambda} is a Func and requires at least two arguments (declaring type and returned type).", + nameof(TLambda)); + } + + var parameters = new Type[parameterCount]; + for (var j = 0; j < parameterCount; j++) + { + parameters[j] = genericArgs[i++]; + } + + Type returned = isFunction ? genericArgs[i] : typeof(void); + + return (declaring, parameters, returned); + } + + private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module? module, Type[] arguments, Type? returned) + { + if (module == null) + { + throw new ArgumentNullException(nameof(module)); + } + + var dm = new DynamicMethod(string.Empty, returned, arguments, module, true); + return (dm, dm.GetILGenerator()); + } + + private static Type[] GetParameters(ConstructorInfo ctor) + { + ParameterInfo[] parameters = ctor.GetParameters(); + var types = new Type[parameters.Length]; + var i = 0; + foreach (ParameterInfo parameter in parameters) + { + types[i++] = parameter.ParameterType; + } + + return types; + } + + private static Type[] GetParameters(MethodInfo method, bool withDeclaring) + { + ParameterInfo[] parameters = method.GetParameters(); + var types = new Type[parameters.Length + (withDeclaring ? 1 : 0)]; + var i = 0; + if (withDeclaring) + { + types[i++] = method.DeclaringType!; + } + + foreach (ParameterInfo parameter in parameters) + { + types[i++] = parameter.ParameterType; + } + + return types; + } + + // emits args + private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes) + { + OpCode[] ldargOpCodes = new[] { OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 }; + + if (lambdaArgTypes.Length != methodArgTypes.Length) + { + throw new Exception("Panic: inconsistent number of args."); + } + + for (var i = 0; i < lambdaArgTypes.Length; i++) + { + if (lambdaArgTypes.Length < 5) + { + ilgen.Emit(ldargOpCodes[i]); + } + else + { + ilgen.Emit(OpCodes.Ldarg, i); + } + + // var local = false; + EmitInputAdapter(ilgen, lambdaArgTypes[i], methodArgTypes[i] /*, ref local*/); + } + } + + // emits adapter opcodes after OpCodes.Ldarg + // inputType is the lambda input type + // methodParamType is the actual type expected by the actual method + // adding code to do inputType -> methodParamType + // valueType -> valueType : not supported ('cos, why?) + // valueType -> !valueType : not supported ('cos, why?) + // !valueType -> valueType : unbox and convert + // !valueType -> !valueType : cast (could throw) + private static void EmitInputAdapter(ILGenerator ilgen, Type inputType, Type methodParamType /*, ref bool local*/) + { + if (inputType == methodParamType) + { + return; + } + + if (methodParamType.IsValueType) + { + if (inputType.IsValueType) + { + // both input and parameter are value types + // not supported, use proper input + // (otherwise, would require converting) + throw new NotSupportedException("ValueTypes conversion."); + } + + // parameter is value type, but input is reference type + // unbox the input to the parameter value type + // this is more or less equivalent to the ToT method below + Label unbox = ilgen.DefineLabel(); + + // if (!local) + // { + // ilgen.DeclareLocal(typeof(object)); // declare local var for st/ld loc_0 + // local = true; + // } + + // stack: value + + // following code can be replaced with .Dump (and then we don't need the local variable anymore) + // ilgen.Emit(OpCodes.Stloc_0); // pop value into loc.0 + //// stack: + // ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 + // ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 + ilgen.Emit(OpCodes.Dup); // duplicate top of stack + + // stack: value ; value + ilgen.Emit(OpCodes.Isinst, methodParamType); // test, pops value, and pushes either a null ref, or an instance of the type + + // stack: inst|null ; value + ilgen.Emit(OpCodes.Ldnull); // push null + + // stack: null ; inst|null ; value + ilgen.Emit(OpCodes.Cgt_Un); // compare what isInst returned to null - pops 2 values, and pushes 1 if greater else 0 + + // stack: 0|1 ; value + ilgen.Emit(OpCodes.Brtrue_S, unbox); // pops value, branches to unbox if true, ie nonzero + + // stack: value + ilgen.Convert(methodParamType); // convert + + // stack: value|converted + ilgen.MarkLabel(unbox); + ilgen.Emit(OpCodes.Unbox_Any, methodParamType); + } + else + { + // parameter is reference type, but input is value type + // not supported, input should always be less constrained + // (otherwise, would require boxing and converting) + if (inputType.IsValueType) + { + throw new NotSupportedException("ValueType boxing."); + } + + // both input and parameter are reference types + // cast the input to the parameter type + ilgen.Emit(OpCodes.Castclass, methodParamType); + } + } + + // private static T ToT(object o) + // { + // return o is T t ? t : (T) System.Convert.ChangeType(o, typeof(T)); + // } + private static MethodInfo? _convertMethod; + private static MethodInfo? _getTypeFromHandle; + + private static void Convert(this ILGenerator ilgen, Type type) + { + if (_getTypeFromHandle == null) + { + _getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(RuntimeTypeHandle) }, null); + } + + if (_convertMethod == null) + { + _convertMethod = typeof(Convert).GetMethod("ChangeType", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(object), typeof(Type) }, null); + } + + ilgen.Emit(OpCodes.Ldtoken, type); + ilgen.CallMethod(_getTypeFromHandle); + ilgen.CallMethod(_convertMethod); + } + + // emits adapter code before OpCodes.Ret + // outputType is the lambda output type + // methodReturnedType is the actual type returned by the actual method + // adding code to do methodReturnedType -> outputType + // valueType -> valueType : not supported ('cos, why?) + // valueType -> !valueType : box + // !valueType -> valueType : not supported ('cos, why?) + // !valueType -> !valueType : implicit cast (could throw) + private static void EmitOutputAdapter(ILGenerator ilgen, Type outputType, Type methodReturnedType) + { + if (outputType == methodReturnedType) + { + return; + } + + // note: the only important thing to support here, is returning a specific type + // as an object, when emitting the method as a Func<..., object> - anything else + // is pointless really - so we box value types, and ensure that non value types + // can be assigned + if (methodReturnedType.IsValueType) + { + if (outputType.IsValueType) + { + // both returned and output are value types + // not supported, use proper output + // (otherwise, would require converting) + throw new NotSupportedException("ValueTypes conversion."); + } + + // returned is value type, but output is reference type + // box the returned value + ilgen.Emit(OpCodes.Box, methodReturnedType); + } + else + { + // returned is reference type, but output is value type + // not supported, output should always be less constrained + // (otherwise, would require boxing and converting) + if (outputType.IsValueType) + { + throw new NotSupportedException("ValueType boxing."); + } + + // both output and returned are reference types + // as long as returned can be assigned to output, good + if (!outputType.IsAssignableFrom(methodReturnedType)) + { + throw new NotSupportedException("Invalid cast."); + } + } + } + + private static void ThrowInvalidLambda(string methodName, Type? returned, Type[] args) => + throw new ArgumentException( + $"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable)args)}):{returned}.", + nameof(TLambda)); + + private static void CallMethod(this ILGenerator ilgen, MethodInfo? method) + { + if (method is not null) + { + var virt = !method.IsStatic && (method.IsVirtual || !method.IsFinal); + ilgen.Emit(virt ? OpCodes.Callvirt : OpCodes.Call, method); + } + } + + private static void Return(this ILGenerator ilgen) => ilgen.Emit(OpCodes.Ret); + + #endregion } diff --git a/src/Umbraco.Core/Routing/AliasUrlProvider.cs b/src/Umbraco.Core/Routing/AliasUrlProvider.cs index 21fb3e9832..d47680905a 100644 --- a/src/Umbraco.Core/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Core/Routing/AliasUrlProvider.cs @@ -1,149 +1,167 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides URLs using the umbracoUrlAlias property. +/// +public class AliasUrlProvider : IUrlProvider { - /// - /// Provides URLs using the umbracoUrlAlias property. - /// - public class AliasUrlProvider : IUrlProvider + private readonly IPublishedValueFallback _publishedValueFallback; + private readonly ISiteDomainMapper _siteDomainMapper; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly UriUtility _uriUtility; + private RequestHandlerSettings _requestConfig; + + public AliasUrlProvider( + IOptionsMonitor requestConfig, + ISiteDomainMapper siteDomainMapper, + UriUtility uriUtility, + IPublishedValueFallback publishedValueFallback, + IUmbracoContextAccessor umbracoContextAccessor) { - private RequestHandlerSettings _requestConfig; - private readonly ISiteDomainMapper _siteDomainMapper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly UriUtility _uriUtility; - private readonly IPublishedValueFallback _publishedValueFallback; + _requestConfig = requestConfig.CurrentValue; + _siteDomainMapper = siteDomainMapper; + _uriUtility = uriUtility; + _publishedValueFallback = publishedValueFallback; + _umbracoContextAccessor = umbracoContextAccessor; - public AliasUrlProvider(IOptionsMonitor requestConfig, ISiteDomainMapper siteDomainMapper, UriUtility uriUtility, IPublishedValueFallback publishedValueFallback, IUmbracoContextAccessor umbracoContextAccessor) + requestConfig.OnChange(x => _requestConfig = x); + } + + // note - at the moment we seem to accept pretty much anything as an alias + // without any form of validation ... could even prob. kill the XPath ... + // ok, this is somewhat experimental and is NOT enabled by default + #region GetUrl + + /// + public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) => + null; // we have nothing to say + + #endregion + + #region GetOtherUrls + + /// + /// Gets the other URLs of a published content. + /// + /// The published content id. + /// The current absolute URL. + /// The other URLs for the published content. + /// + /// + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + public IEnumerable GetOtherUrls(int id, Uri current) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? node = umbracoContext.Content?.GetById(id); + if (node == null) { - _requestConfig = requestConfig.CurrentValue; - _siteDomainMapper = siteDomainMapper; - _uriUtility = uriUtility; - _publishedValueFallback = publishedValueFallback; - _umbracoContextAccessor = umbracoContextAccessor; - - requestConfig.OnChange(x => _requestConfig = x); + yield break; } - // note - at the moment we seem to accept pretty much anything as an alias - // without any form of validation ... could even prob. kill the XPath ... - // ok, this is somewhat experimental and is NOT enabled by default - - #region GetUrl - - /// - public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) { - return null; // we have nothing to say + yield break; } - #endregion + // look for domains, walking up the tree + IPublishedContent? n = node; + IEnumerable? domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); - #region GetOtherUrls - - /// - /// Gets the other URLs of a published content. - /// - /// The Umbraco context. - /// The published content id. - /// The current absolute URL. - /// The other URLs for the published content. - /// - /// Other URLs are those that GetUrl would not return in the current context, but would be valid - /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - public IEnumerable GetOtherUrls(int id, Uri current) + // n is null at root + while (domainUris == null && n != null) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var node = umbracoContext.Content?.GetById(id); - if (node == null) - yield break; + // move to parent node + n = n.Parent; + domainUris = n == null + ? null + : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); + } - if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) - yield break; + // determine whether the alias property varies + var varies = node.GetProperty(Constants.Conventions.Content.UrlAlias)!.PropertyType.VariesByCulture(); - // look for domains, walking up the tree - var n = node; - var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); - while (domainUris == null && n != null) // n is null at root + if (domainUris == null) + { + // no domain + // if the property is invariant, then URL "/" is ok + // if the property varies, then what are we supposed to do? + // the content finder may work, depending on the 'current' culture, + // but there's no way we can return something meaningful here + if (varies) { - // move to parent node - n = n.Parent; - domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, excludeDefault: false); + yield break; } - // determine whether the alias property varies - var varies = node.GetProperty(Constants.Conventions.Content.UrlAlias)!.PropertyType.VariesByCulture(); + var umbracoUrlName = node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); + var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (domainUris == null) + if (aliases == null || aliases.Any() == false) { - // no domain - // if the property is invariant, then URL "/" is ok - // if the property varies, then what are we supposed to do? - // the content finder may work, depending on the 'current' culture, - // but there's no way we can return something meaningful here - if (varies) - yield break; + yield break; + } + + foreach (var alias in aliases.Distinct()) + { + var path = "/" + alias; + var uri = new Uri(path, UriKind.Relative); + yield return UrlInfo.Url(_uriUtility.UriFromUmbraco(uri, _requestConfig).ToString()); + } + } + else + { + // some domains: one URL per domain, which is "/" + foreach (DomainAndUri domainUri in domainUris) + { + // if the property is invariant, get the invariant value, URL is "/" + // if the property varies, get the variant value, URL is "/" + + // but! only if the culture is published, else ignore + if (varies && !node.HasCulture(domainUri.Culture)) + { + continue; + } + + var umbracoUrlName = varies + ? node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias, domainUri.Culture) + : node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); - var umbracoUrlName = node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); if (aliases == null || aliases.Any() == false) - yield break; + { + continue; + } foreach (var alias in aliases.Distinct()) { var path = "/" + alias; - var uri = new Uri(path, UriKind.Relative); - yield return UrlInfo.Url(_uriUtility.UriFromUmbraco(uri, _requestConfig).ToString()); - } - } - else - { - // some domains: one URL per domain, which is "/" - foreach (var domainUri in domainUris) - { - // if the property is invariant, get the invariant value, URL is "/" - // if the property varies, get the variant value, URL is "/" - - // but! only if the culture is published, else ignore - if (varies && !node.HasCulture(domainUri.Culture)) continue; - - var umbracoUrlName = varies - ? node.Value(_publishedValueFallback,Constants.Conventions.Content.UrlAlias, culture: domainUri.Culture) - : node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); - - var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - - if (aliases == null || aliases.Any() == false) - continue; - - foreach(var alias in aliases.Distinct()) - { - var path = "/" + alias; - var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); - yield return UrlInfo.Url(_uriUtility.UriFromUmbraco(uri, _requestConfig).ToString(), domainUri.Culture); - } + var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); + yield return UrlInfo.Url( + _uriUtility.UriFromUmbraco(uri, _requestConfig).ToString(), + domainUri.Culture); } } } - - #endregion - - #region Utilities - - string CombinePaths(string path1, string path2) - { - string path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; - return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); - } - - #endregion } + + #endregion + + #region Utilities + + private string CombinePaths(string path1, string path2) + { + var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; + return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); + } + + #endregion } diff --git a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs index 380d7459ed..c7089f0824 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs @@ -1,116 +1,117 @@ using System.Globalization; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page identifiers. +/// +/// +/// Handles /1234 where 1234 is the identified of a document. +/// +public class ContentFinderByIdPath : IContentFinder { + private readonly ILogger _logger; + private readonly IRequestAccessor _requestAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private WebRoutingSettings _webRoutingSettings; + /// - /// Provides an implementation of that handles page identifiers. + /// Initializes a new instance of the class. /// - /// - /// Handles /1234 where 1234 is the identified of a document. - /// - public class ContentFinderByIdPath : IContentFinder + public ContentFinderByIdPath( + IOptionsMonitor webRoutingSettings, + ILogger logger, + IRequestAccessor requestAccessor, + IUmbracoContextAccessor umbracoContextAccessor) { - private readonly ILogger _logger; - private readonly IRequestAccessor _requestAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private WebRoutingSettings _webRoutingSettings; + _webRoutingSettings = webRoutingSettings.CurrentValue ?? + throw new ArgumentNullException(nameof(webRoutingSettings)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(requestAccessor)); + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByIdPath( - IOptionsMonitor webRoutingSettings, - ILogger logger, - IRequestAccessor requestAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _webRoutingSettings = webRoutingSettings.CurrentValue ?? throw new System.ArgumentNullException(nameof(webRoutingSettings)); - _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); - _requestAccessor = requestAccessor ?? throw new System.ArgumentNullException(nameof(requestAccessor)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); - - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + return Task.FromResult(false); } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - public async Task TryFindContent(IPublishedRequestBuilder frequest) + if (umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdPath) { - if(!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) + return Task.FromResult(false); + } + + IPublishedContent? node = null; + var path = frequest.AbsolutePathDecoded; + + var nodeId = -1; + + // no id if "/" + if (path != "/") + { + var noSlashPath = path.Substring(1); + + if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out nodeId) == false) { - return false; - } - if (umbracoContext == null || (umbracoContext != null && umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdPath)) - { - return false; + nodeId = -1; } - IPublishedContent? node = null; - var path = frequest.AbsolutePathDecoded; - - var nodeId = -1; - - // no id if "/" - if (path != "/") - { - var noSlashPath = path.Substring(1); - - if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out nodeId) == false) - { - nodeId = -1; - } - - if (nodeId > 0) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Id={NodeId}", nodeId); - } - node = umbracoContext?.Content?.GetById(nodeId); - - if (node != null) - { - - var cultureFromQuerystring = _requestAccessor.GetQueryStringValue("culture"); - - // if we have a node, check if we have a culture in the query string - if (!string.IsNullOrEmpty(cultureFromQuerystring)) - { - // we're assuming it will match a culture, if an invalid one is passed in, an exception will throw (there is no TryGetCultureInfo method), i think this is ok though - frequest.SetCulture(cultureFromQuerystring); - } - - frequest.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Found node with id={PublishedContentId}", node.Id); - } - } - else - { - nodeId = -1; // trigger message below - } - } - } - - if (nodeId == -1) + if (nodeId > 0) { if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Not a node id"); + _logger.LogDebug("Id={NodeId}", nodeId); + } + + node = umbracoContext.Content?.GetById(nodeId); + + if (node != null) + { + var cultureFromQuerystring = _requestAccessor.GetQueryStringValue("culture"); + + // if we have a node, check if we have a culture in the query string + if (!string.IsNullOrEmpty(cultureFromQuerystring)) + { + // we're assuming it will match a culture, if an invalid one is passed in, an exception will throw (there is no TryGetCultureInfo method), i think this is ok though + frequest.SetCulture(cultureFromQuerystring); + } + + frequest.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Found node with id={PublishedContentId}", node.Id); + } + } + else + { + nodeId = -1; // trigger message below } } - - return node != null; } + + if (nodeId == -1) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Not a node id"); + } + } + + return Task.FromResult(node != null); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs b/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs index 646d091ebb..7721551777 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs @@ -1,51 +1,50 @@ using System.Globalization; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// This looks up a document by checking for the umbPageId of a request/query string +/// +/// +/// This is used by library.RenderTemplate and also some of the macro rendering functionality like in +/// macroResultWrapper.aspx +/// +public class ContentFinderByPageIdQuery : IContentFinder { + private readonly IRequestAccessor _requestAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + /// - /// This looks up a document by checking for the umbPageId of a request/query string + /// Initializes a new instance of the class. /// - /// - /// This is used by library.RenderTemplate and also some of the macro rendering functionality like in - /// macroResultWrapper.aspx - /// - public class ContentFinderByPageIdQuery : IContentFinder + public ContentFinderByPageIdQuery(IRequestAccessor requestAccessor, IUmbracoContextAccessor umbracoContextAccessor) { - private readonly IRequestAccessor _requestAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(requestAccessor)); + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + } - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByPageIdQuery(IRequestAccessor requestAccessor, IUmbracoContextAccessor umbracoContextAccessor) + /// + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _requestAccessor = requestAccessor ?? throw new System.ArgumentNullException(nameof(requestAccessor)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + return Task.FromResult(false); } - /// - public async Task TryFindContent(IPublishedRequestBuilder frequest) + if (int.TryParse(_requestAccessor.GetRequestValue("umbPageID"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var pageId)) { - if(!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return false; - } - if (int.TryParse(_requestAccessor.GetRequestValue("umbPageID"), NumberStyles.Integer, CultureInfo.InvariantCulture, out int pageId)) - { - IPublishedContent? doc = umbracoContext.Content?.GetById(pageId); + IPublishedContent? doc = umbracoContext.Content?.GetById(pageId); - if (doc != null) - { - frequest.SetPublishedContent(doc); - return true; - } + if (doc != null) + { + frequest.SetPublishedContent(doc); + return Task.FromResult(true); } - - return false; } + + return Task.FromResult(false); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs index a200afec67..6b5e1590c1 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -7,95 +5,100 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing -{ - /// - /// Provides an implementation of that handles page URL rewrites - /// that are stored when moving, saving, or deleting a node. - /// - /// - /// Assigns a permanent redirect notification to the request. - /// - public class ContentFinderByRedirectUrl : IContentFinder - { - private readonly IRedirectUrlService _redirectUrlService; - private readonly ILogger _logger; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; +namespace Umbraco.Cms.Core.Routing; - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByRedirectUrl( - IRedirectUrlService redirectUrlService, - ILogger logger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoContextAccessor umbracoContextAccessor) +/// +/// Provides an implementation of that handles page URL rewrites +/// that are stored when moving, saving, or deleting a node. +/// +/// +/// Assigns a permanent redirect notification to the request. +/// +public class ContentFinderByRedirectUrl : IContentFinder +{ + private readonly ILogger _logger; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IRedirectUrlService _redirectUrlService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + public ContentFinderByRedirectUrl( + IRedirectUrlService redirectUrlService, + ILogger logger, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoContextAccessor umbracoContextAccessor) + { + _redirectUrlService = redirectUrlService; + _logger = logger; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + /// + /// Optionally, can also assign the template or anything else on the document request, although that is not + /// required. + /// + public async Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _redirectUrlService = redirectUrlService; - _logger = logger; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; + return false; } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - /// Optionally, can also assign the template or anything else on the document request, although that is not required. - public async Task TryFindContent(IPublishedRequestBuilder frequest) + var route = frequest.Domain != null + ? frequest.Domain.ContentId + + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded) + : frequest.AbsolutePathDecoded; + + IRedirectUrl? redirectUrl = await _redirectUrlService.GetMostRecentRedirectUrlAsync(route, frequest.Culture); + + if (redirectUrl == null) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return false; - } - - var route = frequest.Domain != null - ? frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded) - : frequest.AbsolutePathDecoded; - - IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route, frequest.Culture); - - if (redirectUrl == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No match for route: {Route}", route); - } - return false; - } - - IPublishedContent? content = umbracoContext.Content?.GetById(redirectUrl.ContentId); - var url = content == null ? "#" : content.Url(_publishedUrlProvider, redirectUrl.Culture); - if (url.StartsWith("#")) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); - } - return false; - } - - // Appending any querystring from the incoming request to the redirect URL - url = string.IsNullOrEmpty(frequest.Uri.Query) ? url : url + frequest.Uri.Query; if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Route {Route} matches content {ContentId} with URL '{Url}', redirecting.", route, content?.Id, url); + _logger.LogDebug("No match for route: {Route}", route); } - frequest - .SetRedirectPermanent(url) - - // From: http://stackoverflow.com/a/22468386/5018 - // See http://issues.umbraco.org/issue/U4-8361#comment=67-30532 - // Setting automatic 301 redirects to not be cached because browsers cache these very aggressively which then leads - // to problems if you rename a page back to it's original name or create a new page with the original name - .SetNoCacheHeader(true) - .SetCacheExtensions(new List { "no-store, must-revalidate" }) - .SetHeaders(new Dictionary { { "Pragma", "no-cache" }, { "Expires", "0" } }); - - return true; + return false; } + + IPublishedContent? content = umbracoContext.Content?.GetById(redirectUrl.ContentId); + var url = content == null ? "#" : content.Url(_publishedUrlProvider, redirectUrl.Culture); + if (url.StartsWith("#")) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); + } + + return false; + } + + // Appending any querystring from the incoming request to the redirect URL + url = string.IsNullOrEmpty(frequest.Uri.Query) ? url : url + frequest.Uri.Query; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Route {Route} matches content {ContentId} with URL '{Url}', redirecting.", route, content?.Id, url); + } + + frequest + .SetRedirectPermanent(url) + + // From: http://stackoverflow.com/a/22468386/5018 + // See http://issues.umbraco.org/issue/U4-8361#comment=67-30532 + // Setting automatic 301 redirects to not be cached because browsers cache these very aggressively which then leads + // to problems if you rename a page back to it's original name or create a new page with the original name + .SetNoCacheHeader(true) + .SetCacheExtensions(new List { "no-store, must-revalidate" }) + .SetHeaders(new Dictionary { { "Pragma", "no-cache" }, { "Expires", "0" } }); + + return true; } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrl.cs b/src/Umbraco.Core/Routing/ContentFinderByUrl.cs index e95a036215..d2b2a564a7 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrl.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrl.cs @@ -1,98 +1,100 @@ -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page nice URLs. +/// +/// +/// Handles /foo/bar where /foo/bar is the nice URL of a document. +/// +public class ContentFinderByUrl : IContentFinder { + private readonly ILogger _logger; + /// - /// Provides an implementation of that handles page nice URLs. + /// Initializes a new instance of the class. /// - /// - /// Handles /foo/bar where /foo/bar is the nice URL of a document. - /// - public class ContentFinderByUrl : IContentFinder + public ContentFinderByUrl(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) { - private readonly ILogger _logger; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + UmbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + } - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrl(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) + /// + /// Gets the + /// + protected IUmbracoContextAccessor UmbracoContextAccessor { get; } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public virtual Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? _)) { - _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); - UmbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + return Task.FromResult(false); } - /// - /// Gets the - /// - protected IUmbracoContextAccessor UmbracoContextAccessor { get; } - - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - public virtual async Task TryFindContent(IPublishedRequestBuilder frequest) + string route; + if (frequest.Domain != null) { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return false; - } - - string route; - if (frequest.Domain != null) - { - route = frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded); - } - else - { - route = frequest.AbsolutePathDecoded; - } - - IPublishedContent? node = FindContent(frequest, route); - return node != null; + route = frequest.Domain.ContentId + + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded); + } + else + { + route = frequest.AbsolutePathDecoded; } - /// - /// Tries to find an Umbraco document for a PublishedRequest and a route. - /// - /// The document node, or null. - protected IPublishedContent? FindContent(IPublishedRequestBuilder docreq, string route) - { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return null; - } + IPublishedContent? node = FindContent(frequest, route); + return Task.FromResult(node != null); + } - if (docreq == null) - { - throw new System.ArgumentNullException(nameof(docreq)); - } + /// + /// Tries to find an Umbraco document for a PublishedRequest and a route. + /// + /// The document node, or null. + protected IPublishedContent? FindContent(IPublishedRequestBuilder docreq, string route) + { + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + return null; + } + + if (docreq == null) + { + throw new ArgumentNullException(nameof(docreq)); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Test route {Route}", route); + } + + IPublishedContent? node = + umbracoContext.Content?.GetByRoute(umbracoContext.InPreviewMode, route, culture: docreq.Culture); + if (node != null) + { + docreq.SetPublishedContent(node); if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Test route {Route}", route); + _logger.LogDebug("Got content, id={NodeId}", node.Id); } - - IPublishedContent? node = umbracoContext.Content?.GetByRoute(umbracoContext.InPreviewMode, route, culture: docreq.Culture); - if (node != null) - { - docreq.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Got content, id={NodeId}", node.Id); - } - } - else - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No match."); - } - } - - return node; } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("No match."); + } + } + + return node; } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs index 5a8f6e16fe..3a04c2cb5b 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs @@ -1,157 +1,159 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing -{ - /// - /// Provides an implementation of that handles page aliases. - /// - /// - /// Handles /just/about/anything where /just/about/anything is contained in the umbracoUrlAlias property of a document. - /// The alias is the full path to the document. There can be more than one alias, separated by commas. - /// - public class ContentFinderByUrlAlias : IContentFinder - { - private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ILogger _logger; +namespace Umbraco.Cms.Core.Routing; - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrlAlias( - ILogger logger, - IPublishedValueFallback publishedValueFallback, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) +/// +/// Provides an implementation of that handles page aliases. +/// +/// +/// +/// Handles /just/about/anything where /just/about/anything is contained in the +/// umbracoUrlAlias property of a document. +/// +/// The alias is the full path to the document. There can be more than one alias, separated by commas. +/// +public class ContentFinderByUrlAlias : IContentFinder +{ + private readonly ILogger _logger; + private readonly IPublishedValueFallback _publishedValueFallback; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + public ContentFinderByUrlAlias( + ILogger logger, + IPublishedValueFallback publishedValueFallback, + IVariationContextAccessor variationContextAccessor, + IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedValueFallback = publishedValueFallback; + _variationContextAccessor = variationContextAccessor; + _umbracoContextAccessor = umbracoContextAccessor; + _logger = logger; + } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _publishedValueFallback = publishedValueFallback; - _variationContextAccessor = variationContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; - _logger = logger; + return Task.FromResult(false); } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - public async Task TryFindContent(IPublishedRequestBuilder frequest) + IPublishedContent? node = null; + + // no alias if "/" + if (frequest.Uri.AbsolutePath != "/") { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) + node = FindContentByAlias( + umbracoContext.Content, + frequest.Domain != null ? frequest.Domain.ContentId : 0, + frequest.Culture, + frequest.AbsolutePathDecoded); + + if (node != null) + { + frequest.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, node.Id); + } + } + } + + return Task.FromResult(node != null); + } + + private IPublishedContent? FindContentByAlias(IPublishedContentCache? cache, int rootNodeId, string? culture, string alias) + { + if (alias == null) + { + throw new ArgumentNullException(nameof(alias)); + } + + // the alias may be "foo/bar" or "/foo/bar" + // there may be spaces as in "/foo/bar, /foo/nil" + // these should probably be taken care of earlier on + + // TODO: can we normalize the values so that they contain no whitespaces, and no leading slashes? + // and then the comparisons in IsMatch can be way faster - and allocate way less strings + const string propertyAlias = Constants.Conventions.Content.UrlAlias; + + var test1 = alias.TrimStart(Constants.CharArrays.ForwardSlash) + ","; + var test2 = ",/" + test1; // test2 is ",/alias," + test1 = "," + test1; // test1 is ",alias," + + bool IsMatch(IPublishedContent c, string a1, string a2) + { + // this basically implements the original XPath query ;-( + // + // "//* [@isDoc and (" + + // "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')" + + // " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')" + + // ")]" + if (!c.HasProperty(propertyAlias)) { return false; } - IPublishedContent? node = null; - // no alias if "/" - if (frequest.Uri.AbsolutePath != "/") + IPublishedProperty? p = c.GetProperty(propertyAlias); + var varies = p?.PropertyType?.VariesByCulture(); + string? v; + if (varies ?? false) { - node = FindContentByAlias( - umbracoContext!.Content, - frequest.Domain != null ? frequest.Domain.ContentId : 0, - frequest.Culture, - frequest.AbsolutePathDecoded); - - if (node != null) + if (!c.HasCulture(culture)) { - frequest.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, node.Id); - } + return false; } + + v = c.Value(_publishedValueFallback, propertyAlias, culture); + } + else + { + v = c.Value(_publishedValueFallback, propertyAlias); } - return node != null; + if (string.IsNullOrWhiteSpace(v)) + { + return false; + } + + v = "," + v.Replace(" ", string.Empty) + ","; + return v.InvariantContains(a1) || v.InvariantContains(a2); } - private IPublishedContent? FindContentByAlias(IPublishedContentCache? cache, int rootNodeId, string? culture, string alias) + // TODO: even with Linq, what happens below has to be horribly slow + // but the only solution is to entirely refactor URL providers to stop being dynamic + if (rootNodeId > 0) { - if (alias == null) - { - throw new ArgumentNullException(nameof(alias)); - } - - // the alias may be "foo/bar" or "/foo/bar" - // there may be spaces as in "/foo/bar, /foo/nil" - // these should probably be taken care of earlier on - - // TODO: can we normalize the values so that they contain no whitespaces, and no leading slashes? - // and then the comparisons in IsMatch can be way faster - and allocate way less strings - const string propertyAlias = Constants.Conventions.Content.UrlAlias; - - var test1 = alias.TrimStart(Constants.CharArrays.ForwardSlash) + ","; - var test2 = ",/" + test1; // test2 is ",/alias," - test1 = "," + test1; // test1 is ",alias," - - bool IsMatch(IPublishedContent c, string a1, string a2) - { - // this basically implements the original XPath query ;-( - // - // "//* [@isDoc and (" + - // "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')" + - // " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')" + - // ")]" - if (!c.HasProperty(propertyAlias)) - { - return false; - } - - IPublishedProperty? p = c.GetProperty(propertyAlias); - var varies = p!.PropertyType?.VariesByCulture(); - string? v; - if (varies ?? false) - { - if (!c.HasCulture(culture)) - { - return false; - } - - v = c.Value(_publishedValueFallback, propertyAlias, culture); - } - else - { - v = c.Value(_publishedValueFallback, propertyAlias); - } - - if (string.IsNullOrWhiteSpace(v)) - { - return false; - } - - v = "," + v.Replace(" ", string.Empty) + ","; - return v.InvariantContains(a1) || v.InvariantContains(a2); - } - - // TODO: even with Linq, what happens below has to be horribly slow - // but the only solution is to entirely refactor URL providers to stop being dynamic - if (rootNodeId > 0) - { - IPublishedContent? rootNode = cache?.GetById(rootNodeId); - return rootNode?.Descendants(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); - } - - if (cache is not null) - { - foreach (IPublishedContent rootContent in cache.GetAtRoot()) - { - IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); - if (c != null) - { - return c; - } - } - } - - return null; + IPublishedContent? rootNode = cache?.GetById(rootNodeId); + return rootNode?.Descendants(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); } + + if (cache is not null) + { + foreach (IPublishedContent rootContent in cache.GetAtRoot()) + { + IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor) + .FirstOrDefault(x => IsMatch(x, test1, test2)); + if (c != null) + { + return c; + } + } + } + + return null; } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs index f059850086..39fc468cee 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -8,111 +7,121 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page nice URLs and a template. +/// +/// +/// +/// This finder allows for an odd routing pattern similar to altTemplate, probably only use case is if there is +/// an alternative mime type template and it should be routable by something like "/hello/world/json" where the +/// JSON template is to be used for the "world" page +/// +/// +/// Handles /foo/bar/template where /foo/bar is the nice URL of a document, and template a +/// template alias. +/// +/// If successful, then the template of the document request is also assigned. +/// +public class ContentFinderByUrlAndTemplate : ContentFinderByUrl { + private readonly IContentTypeService _contentTypeService; + private readonly IFileService _fileService; + private readonly ILogger _logger; + private WebRoutingSettings _webRoutingSettings; + /// - /// Provides an implementation of that handles page nice URLs and a template. + /// Initializes a new instance of the class. /// - /// - /// This finder allows for an odd routing pattern similar to altTemplate, probably only use case is if there is an alternative mime type template and it should be routable by something like "/hello/world/json" where the JSON template is to be used for the "world" page - /// Handles /foo/bar/template where /foo/bar is the nice URL of a document, and template a template alias. - /// If successful, then the template of the document request is also assigned. - /// - public class ContentFinderByUrlAndTemplate : ContentFinderByUrl + public ContentFinderByUrlAndTemplate( + ILogger logger, + IFileService fileService, + IContentTypeService contentTypeService, + IUmbracoContextAccessor umbracoContextAccessor, + IOptionsMonitor webRoutingSettings) + : base(logger, umbracoContextAccessor) { - private readonly ILogger _logger; - private readonly IFileService _fileService; + _logger = logger; + _fileService = fileService; + _contentTypeService = contentTypeService; + _webRoutingSettings = webRoutingSettings.CurrentValue; + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } - private readonly IContentTypeService _contentTypeService; - private WebRoutingSettings _webRoutingSettings; + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + /// If successful, also assigns the template. + public override Task TryFindContent(IPublishedRequestBuilder frequest) + { + var path = frequest.AbsolutePathDecoded; - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrlAndTemplate( - ILogger logger, - IFileService fileService, - IContentTypeService contentTypeService, - IUmbracoContextAccessor umbracoContextAccessor, - IOptionsMonitor webRoutingSettings) - : base(logger, umbracoContextAccessor) + if (frequest.Domain != null) { - _logger = logger; - _fileService = fileService; - _contentTypeService = contentTypeService; - _webRoutingSettings = webRoutingSettings.CurrentValue; - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + path = DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, path); } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - /// If successful, also assigns the template. - public override async Task TryFindContent(IPublishedRequestBuilder frequest) + // no template if "/" + if (path == "/") { - var path = frequest.AbsolutePathDecoded; - - if (frequest.Domain != null) - { - path = DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, path); - } - - // no template if "/" - if (path == "/") - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No template in path '/'"); - } - return false; - } - - // look for template in last position - var pos = path.LastIndexOf('/'); - var templateAlias = path.Substring(pos + 1); - path = pos == 0 ? "/" : path.Substring(0, pos); - - ITemplate? template = _fileService.GetTemplate(templateAlias); - - if (template == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Not a valid template: '{TemplateAlias}'", templateAlias); - } - return false; - } if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Valid template: '{TemplateAlias}'", templateAlias); + _logger.LogDebug("No template in path '/'"); } - // look for node corresponding to the rest of the route - var route = frequest.Domain != null ? (frequest.Domain.ContentId + path) : path; - IPublishedContent? node = FindContent(frequest, route); - - if (node == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Not a valid route to node: '{Route}'", route); - } - return false; - } - - // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings - if (!node.IsAllowedTemplate(_contentTypeService, _webRoutingSettings, template.Id)) - { - _logger.LogWarning("Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); - frequest.SetPublishedContent(null); // clear - return false; - } - - // got it - frequest.SetTemplate(template); - return true; + return Task.FromResult(false); } + + // look for template in last position + var pos = path.LastIndexOf('/'); + var templateAlias = path.Substring(pos + 1); + path = pos == 0 ? "/" : path.Substring(0, pos);; + + ITemplate? template = _fileService.GetTemplate(templateAlias); + + if (template == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Not a valid template: '{TemplateAlias}'", templateAlias); + } + + return Task.FromResult(false); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Valid template: '{TemplateAlias}'", templateAlias); + } + + // look for node corresponding to the rest of the route + var route = frequest.Domain != null ? frequest.Domain.ContentId + path : path; + IPublishedContent? node = FindContent(frequest, route); + + if (node == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Not a valid route to node: '{Route}'", route); + } + + return Task.FromResult(false); + } + + // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings + if (!node.IsAllowedTemplate(_contentTypeService, _webRoutingSettings, template.Id)) + { + _logger.LogWarning( + "Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); + frequest.SetPublishedContent(null); // clear + return Task.FromResult(false); + } + + // got it + frequest.SetTemplate(template); + return Task.FromResult(true); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderCollection.cs b/src/Umbraco.Core/Routing/ContentFinderCollection.cs index 8965d9d447..cc3b711d98 100644 --- a/src/Umbraco.Core/Routing/ContentFinderCollection.cs +++ b/src/Umbraco.Core/Routing/ContentFinderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class ContentFinderCollection : BuilderCollectionBase { - public class ContentFinderCollection : BuilderCollectionBase + public ContentFinderCollection(Func> items) + : base(items) { - public ContentFinderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs b/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs index d471acf60c..3c8a0e925d 100644 --- a/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs +++ b/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class ContentFinderCollectionBuilder : OrderedCollectionBuilderBase { - public class ContentFinderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override ContentFinderCollectionBuilder This => this; - } + protected override ContentFinderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs index 1afda0175c..d1c79783f0 100644 --- a/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs @@ -1,75 +1,83 @@ -using System; -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Default media URL provider. +/// +public class DefaultMediaUrlProvider : IMediaUrlProvider { - /// - /// Default media URL provider. - /// - public class DefaultMediaUrlProvider : IMediaUrlProvider + private readonly MediaUrlGeneratorCollection _mediaPathGenerators; + private readonly UriUtility _uriUtility; + + public DefaultMediaUrlProvider(MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility) { - private readonly UriUtility _uriUtility; - private readonly MediaUrlGeneratorCollection _mediaPathGenerators; + _mediaPathGenerators = mediaPathGenerators ?? throw new ArgumentNullException(nameof(mediaPathGenerators)); + _uriUtility = uriUtility; + } - public DefaultMediaUrlProvider(MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility) + /// + public virtual UrlInfo? GetMediaUrl( + IPublishedContent content, + string propertyAlias, + UrlMode mode, + string? culture, + Uri current) + { + IPublishedProperty? prop = content.GetProperty(propertyAlias); + + // get the raw source value since this is what is used by IDataEditorWithMediaPath for processing + var value = prop?.GetSourceValue(culture); + if (value == null) { - _mediaPathGenerators = mediaPathGenerators ?? throw new ArgumentNullException(nameof(mediaPathGenerators)); - _uriUtility = uriUtility; - } - - /// - public virtual UrlInfo? GetMediaUrl(IPublishedContent content, - string propertyAlias, UrlMode mode, string? culture, Uri current) - { - var prop = content.GetProperty(propertyAlias); - - // get the raw source value since this is what is used by IDataEditorWithMediaPath for processing - var value = prop?.GetSourceValue(culture); - if (value == null) - { - return null; - } - - var propType = prop?.PropertyType; - - if (_mediaPathGenerators.TryGetMediaPath(propType?.EditorAlias, value, out var path)) - { - var url = AssembleUrl(path!, current, mode); - return UrlInfo.Url(url.ToString(), culture); - } - return null; } - private Uri AssembleUrl(string path, Uri current, UrlMode mode) + IPublishedPropertyType? propType = prop?.PropertyType; + + if (_mediaPathGenerators.TryGetMediaPath(propType?.EditorAlias, value, out var path)) { - if (string.IsNullOrWhiteSpace(path)) - throw new ArgumentException($"{nameof(path)} cannot be null or whitespace", nameof(path)); - - // the stored path is absolute so we just return it as is - if (Uri.IsWellFormedUriString(path, UriKind.Absolute)) - return new Uri(path); - - Uri uri; - - if (current == null) - mode = UrlMode.Relative; // best we can do - - switch (mode) - { - case UrlMode.Absolute: - uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); - break; - case UrlMode.Relative: - case UrlMode.Auto: - uri = new Uri(path, UriKind.Relative); - break; - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } - - return _uriUtility.MediaUriFromUmbraco(uri); + Uri url = AssembleUrl(path!, current, mode); + return UrlInfo.Url(url.ToString(), culture); } + + return null; + } + + private Uri AssembleUrl(string path, Uri current, UrlMode mode) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException($"{nameof(path)} cannot be null or whitespace", nameof(path)); + } + + // the stored path is absolute so we just return it as is + if (Uri.IsWellFormedUriString(path, UriKind.Absolute)) + { + return new Uri(path); + } + + Uri uri; + + if (current == null) + { + mode = UrlMode.Relative; // best we can do + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlMode.Relative: + case UrlMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + + return _uriUtility.MediaUriFromUmbraco(uri); } } diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 5db3c024ac..6506d29725 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -11,20 +9,20 @@ using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides urls. +/// +public class DefaultUrlProvider : IUrlProvider { - /// - /// Provides urls. - /// - public class DefaultUrlProvider : IUrlProvider - { - private readonly ILocalizationService _localizationService; - private readonly ILocalizedTextService? _localizedTextService; - private readonly ILogger _logger; - private RequestHandlerSettings _requestSettings; - private readonly ISiteDomainMapper _siteDomainMapper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly UriUtility _uriUtility; + private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService? _localizedTextService; + private readonly ILogger _logger; + private readonly ISiteDomainMapper _siteDomainMapper; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly UriUtility _uriUtility; + private RequestHandlerSettings _requestSettings; public DefaultUrlProvider( IOptionsMonitor requestSettings, @@ -41,197 +39,205 @@ namespace Umbraco.Cms.Core.Routing _uriUtility = uriUtility; _localizationService = localizationService; - requestSettings.OnChange(x => _requestSettings = x); + requestSettings.OnChange(x => _requestSettings = x); + } + + #region GetOtherUrls + + /// + /// Gets the other URLs of a published content. + /// + /// The Umbraco context. + /// The published content id. + /// The current absolute URL. + /// The other URLs for the published content. + /// + /// + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + public virtual IEnumerable GetOtherUrls(int id, Uri current) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? node = umbracoContext.Content?.GetById(id); + if (node == null) + { + yield break; } - #region GetOtherUrls + // look for domains, walking up the tree + IPublishedContent? n = node; + IEnumerable? domainUris = + DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); - /// - /// Gets the other URLs of a published content. - /// - /// The Umbraco context. - /// The published content id. - /// The current absolute URL. - /// The other URLs for the published content. - /// - /// - /// Other URLs are those that GetUrl would not return in the current context, but would be valid - /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - /// - public virtual IEnumerable GetOtherUrls(int id, Uri current) + // n is null at root + while (domainUris == null && n != null) { - IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IPublishedContent? node = umbracoContext.Content?.GetById(id); - if (node == null) - { - yield break; - } - - // look for domains, walking up the tree - IPublishedContent? n = node; - IEnumerable? domainUris = - DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, - current, false); - while (domainUris == null && n != null) // n is null at root - { - n = n.Parent; // move to parent node - domainUris = n == null - ? null - : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, - current); - } - - // no domains = exit - if (domainUris == null) - { - yield break; - } - - foreach (DomainAndUri d in domainUris) - { - var culture = d.Culture; - - // although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok - var route = umbracoContext.Content?.GetRouteById(id, culture); - if (route == null) - { - continue; - } - - // need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) - var pos = route.IndexOf('/'); - var path = pos == 0 ? route : route.Substring(pos); - - var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); - uri = _uriUtility.UriFromUmbraco(uri, _requestSettings); - yield return UrlInfo.Url(uri.ToString(), culture); - } + n = n.Parent; // move to parent node + domainUris = n == null + ? null + : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current); } - #endregion - - #region GetUrl - - /// - public virtual UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + // no domains = exit + if (domainUris == null) { - if (!current.IsAbsoluteUri) - { - throw new ArgumentException("Current URL must be absolute.", nameof(current)); - } - - IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - // will not use cache if previewing - var route = umbracoContext.Content?.GetRouteById(content.Id, culture); - - return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); + yield break; } - internal UrlInfo? GetUrlFromRoute( - string? route, - IUmbracoContext umbracoContext, - int id, - Uri current, - UrlMode mode, - string? culture) + foreach (DomainAndUri d in domainUris) { - if (string.IsNullOrWhiteSpace(route)) + var culture = d.Culture; + + // although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok + var route = umbracoContext.Content?.GetRouteById(id, culture); + if (route == null) { - _logger.LogDebug( - "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", - id); - return null; + continue; } - // extract domainUri and path - // route is / or / + // need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); - DomainAndUri? domainUri = pos == 0 - ? null - : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture); - var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); - if (domainUri is not null || string.IsNullOrEmpty(culture) || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) - { - var url = AssembleUrl(domainUri, path, current, mode).ToString(); - return UrlInfo.Url(url, culture); - } + var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); + uri = _uriUtility.UriFromUmbraco(uri, _requestSettings); + yield return UrlInfo.Url(uri.ToString(), culture); + } + } + #endregion + + #region GetUrl + + /// + public virtual UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + { + if (!current.IsAbsoluteUri) + { + throw new ArgumentException("Current URL must be absolute.", nameof(current)); + } + + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + // will not use cache if previewing + var route = umbracoContext.Content?.GetRouteById(content.Id, culture); + + return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); + } + + internal UrlInfo? GetUrlFromRoute( + string? route, + IUmbracoContext umbracoContext, + int id, + Uri current, + UrlMode mode, + string? culture) + { + if (string.IsNullOrWhiteSpace(route)) + { + _logger.LogDebug( + "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", + id); return null; } - #endregion + // extract domainUri and path + // route is / or / + var pos = route.IndexOf('/'); + var path = pos == 0 ? route : route[pos..]; + DomainAndUri? domainUri = pos == 0 + ? null + : DomainUtilities.DomainForNode( + umbracoContext.PublishedSnapshot.Domains, + _siteDomainMapper, + int.Parse(route[..pos], CultureInfo.InvariantCulture), + current, + culture); - #region Utilities - - private Uri AssembleUrl(DomainAndUri? domainUri, string path, Uri current, UrlMode mode) + var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); + if (domainUri is not null || string.IsNullOrEmpty(culture) || + culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) { - Uri uri; - - // ignore vdir at that point, UriFromUmbraco will do it - - if (domainUri == null) // no domain was found - { - if (current == null) - { - mode = UrlMode.Relative; // best we can do - } - - switch (mode) - { - case UrlMode.Absolute: - uri = new Uri(current!.GetLeftPart(UriPartial.Authority) + path); - break; - case UrlMode.Relative: - case UrlMode.Auto: - uri = new Uri(path, UriKind.Relative); - break; - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } - } - else // a domain was found - { - if (mode == UrlMode.Auto) - { - //this check is a little tricky, we can't just compare domains - if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == - current.GetLeftPart(UriPartial.Authority)) - { - mode = UrlMode.Relative; - } - else - { - mode = UrlMode.Absolute; - } - } - - switch (mode) - { - case UrlMode.Absolute: - uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); - break; - case UrlMode.Relative: - uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); - break; - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } - } - - // UriFromUmbraco will handle vdir - // meaning it will add vdir into domain URLs too! - return _uriUtility.UriFromUmbraco(uri, _requestSettings); + var url = AssembleUrl(domainUri, path, current, mode).ToString(); + return UrlInfo.Url(url, culture); } - private string CombinePaths(string path1, string path2) - { - var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; - return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); - } - - #endregion + return null; } + + #endregion + + #region Utilities + + private Uri AssembleUrl(DomainAndUri? domainUri, string path, Uri current, UrlMode mode) + { + Uri uri; + + // ignore vdir at that point, UriFromUmbraco will do it + // no domain was found + if (domainUri == null) + { + if (current == null) + { + mode = UrlMode.Relative; // best we can do + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(current!.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlMode.Relative: + case UrlMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + } + + // a domain was found + else + { + if (mode == UrlMode.Auto) + { + // this check is a little tricky, we can't just compare domains + if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == + current.GetLeftPart(UriPartial.Authority)) + { + mode = UrlMode.Relative; + } + else + { + mode = UrlMode.Absolute; + } + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); + break; + case UrlMode.Relative: + uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + } + + // UriFromUmbraco will handle vdir + // meaning it will add vdir into domain URLs too! + return _uriUtility.UriFromUmbraco(uri, _requestSettings); + } + + private string CombinePaths(string path1, string path2) + { + var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; + return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); + } + + #endregion } diff --git a/src/Umbraco.Core/Routing/Domain.cs b/src/Umbraco.Core/Routing/Domain.cs index ecefb07e8b..291d7beed9 100644 --- a/src/Umbraco.Core/Routing/Domain.cs +++ b/src/Umbraco.Core/Routing/Domain.cs @@ -1,63 +1,62 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Represents a published snapshot domain. +/// +public class Domain { /// - /// Represents a published snapshot domain. + /// Initializes a new instance of the class. /// - public class Domain + /// The unique identifier of the domain. + /// The name of the domain. + /// The identifier of the content which supports the domain. + /// The culture of the domain. + /// A value indicating whether the domain is a wildcard domain. + public Domain(int id, string name, int contentId, string? culture, bool isWildcard) { - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the domain. - /// The name of the domain. - /// The identifier of the content which supports the domain. - /// The culture of the domain. - /// A value indicating whether the domain is a wildcard domain. - public Domain(int id, string name, int contentId, string? culture, bool isWildcard) - { - Id = id; - Name = name; - ContentId = contentId; - Culture = culture; - IsWildcard = isWildcard; - } - - /// - /// Initializes a new instance of the class. - /// - /// An origin domain. - protected Domain(Domain domain) - { - Id = domain.Id; - Name = domain.Name; - ContentId = domain.ContentId; - Culture = domain.Culture; - IsWildcard = domain.IsWildcard; - } - - /// - /// Gets the unique identifier of the domain. - /// - public int Id { get; } - - /// - /// Gets the name of the domain. - /// - public string Name { get; } - - /// - /// Gets the identifier of the content which supports the domain. - /// - public int ContentId { get; } - - /// - /// Gets the culture of the domain. - /// - public string? Culture { get; } - - /// - /// Gets a value indicating whether the domain is a wildcard domain. - /// - public bool IsWildcard { get; } + Id = id; + Name = name; + ContentId = contentId; + Culture = culture; + IsWildcard = isWildcard; } + + /// + /// Initializes a new instance of the class. + /// + /// An origin domain. + protected Domain(Domain domain) + { + Id = domain.Id; + Name = domain.Name; + ContentId = domain.ContentId; + Culture = domain.Culture; + IsWildcard = domain.IsWildcard; + } + + /// + /// Gets the unique identifier of the domain. + /// + public int Id { get; } + + /// + /// Gets the name of the domain. + /// + public string Name { get; } + + /// + /// Gets the identifier of the content which supports the domain. + /// + public int ContentId { get; } + + /// + /// Gets the culture of the domain. + /// + public string? Culture { get; } + + /// + /// Gets a value indicating whether the domain is a wildcard domain. + /// + public bool IsWildcard { get; } } diff --git a/src/Umbraco.Core/Routing/DomainAndUri.cs b/src/Umbraco.Core/Routing/DomainAndUri.cs index 751c4ead58..c5f9497d77 100644 --- a/src/Umbraco.Core/Routing/DomainAndUri.cs +++ b/src/Umbraco.Core/Routing/DomainAndUri.cs @@ -1,44 +1,47 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Represents a published snapshot domain with its normalized uri. +/// +/// +/// +/// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com +/// , example.com/foo/. +/// +/// +/// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, +/// https://www.example.com/, http://example.com/foo/. +/// +/// +public class DomainAndUri : Domain { /// - /// Represents a published snapshot domain with its normalized uri. + /// Initializes a new instance of the class. /// - /// - /// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com, example.com/foo/. - /// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, https://www.example.com/, http://example.com/foo/. - /// - public class DomainAndUri : Domain + /// The original domain. + /// The context current Uri. + public DomainAndUri(Domain domain, Uri currentUri) + : base(domain) { - /// - /// Initializes a new instance of the class. - /// - /// The original domain. - /// The context current Uri. - public DomainAndUri(Domain domain, Uri currentUri) - : base(domain) + try { - try - { - Uri = DomainUtilities.ParseUriFromDomainName(Name, currentUri); - } - catch (UriFormatException) - { - throw new ArgumentException($"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{Name.ToCSharpString()}\"." - + " Hostname should be a valid uri.", nameof(domain)); - } + Uri = DomainUtilities.ParseUriFromDomainName(Name, currentUri); } - - /// - /// Gets the normalized uri of the domain, within the current context. - /// - public Uri Uri { get; } - - public override string ToString() + catch (UriFormatException) { - return $"{{ \"{Name}\", \"{Uri}\" }}"; + throw new ArgumentException( + $"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{Name.ToCSharpString()}\"." + + " Hostname should be a valid uri.", + nameof(domain)); } } + + /// + /// Gets the normalized uri of the domain, within the current context. + /// + public Uri Uri { get; } + + public override string ToString() => $"{{ \"{Name}\", \"{Uri}\" }}"; } diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs index 9e762a600e..f31244d2ac 100644 --- a/src/Umbraco.Core/Routing/DomainUtilities.cs +++ b/src/Umbraco.Core/Routing/DomainUtilities.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -32,10 +29,14 @@ namespace Umbraco.Cms.Core.Routing public static string? GetCultureFromDomains(int contentId, string contentPath, Uri? current, IUmbracoContext umbracoContext, ISiteDomainMapper siteDomainMapper) { if (umbracoContext == null) + { throw new InvalidOperationException("A current UmbracoContext is required."); + } if (current == null) + { current = umbracoContext.CleanedUmbracoUrl; + } // get the published route, else the preview route // if both are null then the content does not exist @@ -43,18 +44,28 @@ namespace Umbraco.Cms.Core.Routing umbracoContext.Content?.GetRouteById(true, contentId); if (route == null) + { return null; + } var pos = route.IndexOf('/'); - var domain = pos == 0 + DomainAndUri? domain = pos == 0 ? null : DomainForNode(umbracoContext.Domains, siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current); var rootContentId = domain?.ContentId ?? -1; - var wcDomain = FindWildcardDomainInPath(umbracoContext.Domains?.GetAll(true), contentPath, rootContentId); + Domain? wcDomain = FindWildcardDomainInPath(umbracoContext.Domains?.GetAll(true), contentPath, rootContentId); + + if (wcDomain != null) + { + return wcDomain.Culture; + } + + if (domain != null) + { + return domain.Culture; + } - if (wcDomain != null) return wcDomain.Culture; - if (domain != null) return domain.Culture; return umbracoContext.Domains?.DefaultCulture; } @@ -81,14 +92,18 @@ namespace Umbraco.Cms.Core.Routing { // be safe if (nodeId <= 0) + { return null; + } // get the domains on that node - var domains = domainCache?.GetAssigned(nodeId).ToArray(); + Domain[]? domains = domainCache?.GetAssigned(nodeId).ToArray(); // none? if (domains is null || domains.Length == 0) + { return null; + } // else filter // it could be that none apply (due to culture) @@ -110,17 +125,21 @@ namespace Umbraco.Cms.Core.Routing { // be safe if (nodeId <= 0) + { return null; + } // get the domains on that node - var domains = domainCache?.GetAssigned(nodeId).ToArray(); + Domain[]? domains = domainCache?.GetAssigned(nodeId).ToArray(); // none? if (domains is null || domains.Length == 0) + { return null; + } // get the domains and their uris - var domainAndUris = SelectDomains(domains, current).ToArray(); + DomainAndUri[] domainAndUris = SelectDomains(domains, current).ToArray(); // filter return siteDomainMapper.MapDomains(domainAndUris, current, excludeDefault, null, domainCache?.DefaultCulture).ToArray(); @@ -161,7 +180,9 @@ namespace Umbraco.Cms.Core.Routing // nothing = no magic, return null if (domainsAndUris is null || domainsAndUris.Count == 0) + { return null; + } // sanitize cultures culture = culture?.NullOrWhiteSpaceAsNull(); @@ -179,27 +200,31 @@ namespace Umbraco.Cms.Core.Routing // if a culture is specified, then try to get domains for that culture // (else cultureDomains will be null) // do NOT specify a default culture, else it would pick those domains - var cultureDomains = SelectByCulture(domainsAndUris, culture, defaultCulture: null); + IReadOnlyCollection? cultureDomains = SelectByCulture(domainsAndUris, culture, defaultCulture: null); IReadOnlyCollection considerForBaseDomains = domainsAndUris; if (cultureDomains != null) { if (cultureDomains.Count == 1) // only 1, return + { return cultureDomains.First(); + } // else restrict to those domains, for base lookup considerForBaseDomains = cultureDomains; } // look for domains that would be the base of the uri - var baseDomains = SelectByBase(considerForBaseDomains, uri, culture); + IReadOnlyCollection baseDomains = SelectByBase(considerForBaseDomains, uri, culture); if (baseDomains.Count > 0) // found, return + { return baseDomains.First(); + } // if nothing works, then try to run the filter to select a domain // either restricting on cultureDomains, or on all domains if (filter != null) { - var domainAndUri = filter(cultureDomains ?? domainsAndUris, uri, culture, defaultCulture); + DomainAndUri? domainAndUri = filter(cultureDomains ?? domainsAndUris, uri, culture, defaultCulture); return domainAndUri; } @@ -216,14 +241,16 @@ namespace Umbraco.Cms.Core.Routing { // look for domains that would be the base of the uri // ie current is www.example.com/foo/bar, look for domain www.example.com - var currentWithSlash = uri.EndPathWithSlash(); + Uri currentWithSlash = uri.EndPathWithSlash(); var baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithSlash) && MatchesCulture(d, culture)).ToList(); // if none matches, try again without the port // ie current is www.example.com:1234/foo/bar, look for domain www.example.com - var currentWithoutPort = currentWithSlash.WithoutPort(); + Uri currentWithoutPort = currentWithSlash.WithoutPort(); if (baseDomains.Count == 0) + { baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithoutPort)).ToList(); + } return baseDomains; } @@ -235,13 +262,19 @@ namespace Umbraco.Cms.Core.Routing if (culture != null) // try the supplied culture { var cultureDomains = domainsAndUris.Where(x => x.Culture.InvariantEquals(culture)).ToList(); - if (cultureDomains.Count > 0) return cultureDomains; + if (cultureDomains.Count > 0) + { + return cultureDomains; + } } if (defaultCulture != null) // try the defaultCulture culture { var cultureDomains = domainsAndUris.Where(x => x.Culture.InvariantEquals(defaultCulture)).ToList(); - if (cultureDomains.Count > 0) return cultureDomains; + if (cultureDomains.Count > 0) + { + return cultureDomains; + } } return null; @@ -256,13 +289,19 @@ namespace Umbraco.Cms.Core.Routing if (culture != null) // try the supplied culture { domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(culture)); - if (domainAndUri != null) return domainAndUri; + if (domainAndUri != null) + { + return domainAndUri; + } } if (defaultCulture != null) // try the defaultCulture culture { domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(defaultCulture)); - if (domainAndUri != null) return domainAndUri; + if (domainAndUri != null) + { + return domainAndUri; + } } return domainsAndUris.First(); // what else? diff --git a/src/Umbraco.Core/Routing/IContentFinder.cs b/src/Umbraco.Core/Routing/IContentFinder.cs index ab160715bb..3e4304fe70 100644 --- a/src/Umbraco.Core/Routing/IContentFinder.cs +++ b/src/Umbraco.Core/Routing/IContentFinder.cs @@ -1,18 +1,18 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Routing +/// +/// Provides a method to try to find and assign an Umbraco document to a PublishedRequest. +/// +public interface IContentFinder { /// - /// Provides a method to try to find and assign an Umbraco document to a PublishedRequest. + /// Tries to find and assign an Umbraco document to a PublishedRequest. /// - public interface IContentFinder - { - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - /// Optionally, can also assign the template or anything else on the document request, although that is not required. - Task TryFindContent(IPublishedRequestBuilder request); - } + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + /// + /// Optionally, can also assign the template or anything else on the document request, although that is not + /// required. + /// + Task TryFindContent(IPublishedRequestBuilder request); } diff --git a/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs b/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs index 19e5f80246..ad3959ae42 100644 --- a/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs +++ b/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides a method to try to find and assign an Umbraco document to a PublishedRequest +/// when everything else has failed. +/// +/// Identical to but required in order to differentiate them in ioc. +public interface IContentLastChanceFinder : IContentFinder { - /// - /// Provides a method to try to find and assign an Umbraco document to a PublishedRequest - /// when everything else has failed. - /// - /// Identical to but required in order to differentiate them in ioc. - public interface IContentLastChanceFinder : IContentFinder - { } } diff --git a/src/Umbraco.Core/Routing/IMediaUrlProvider.cs b/src/Umbraco.Core/Routing/IMediaUrlProvider.cs index 4478f60334..9d944efff7 100644 --- a/src/Umbraco.Core/Routing/IMediaUrlProvider.cs +++ b/src/Umbraco.Core/Routing/IMediaUrlProvider.cs @@ -1,30 +1,32 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides media URL. +/// +public interface IMediaUrlProvider { /// - /// Provides media URL. + /// Gets the URL of a media item. /// - public interface IMediaUrlProvider - { - /// - /// Gets the URL of a media item. - /// - /// The published content. - /// The property alias to resolve the URL from. - /// The URL mode. - /// The variation language. - /// The current absolute URL. - /// The URL for the media. - /// - /// The URL is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the URL for the specified culture or, - /// when no culture is specified, the current culture. - /// The URL provider can ignore the mode and always return an absolute URL, - /// e.g. a cdn URL provider will most likely always return an absolute URL. - /// If the provider is unable to provide a URL, it returns null. - /// - UrlInfo? GetMediaUrl(IPublishedContent content, string propertyAlias, UrlMode mode, string? culture, Uri current); - } + /// The published content. + /// The property alias to resolve the URL from. + /// The URL mode. + /// The variation language. + /// The current absolute URL. + /// The URL for the media. + /// + /// The URL is absolute or relative depending on mode and on current. + /// + /// If the media is multi-lingual, gets the URL for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// + /// The URL provider can ignore the mode and always return an absolute URL, + /// e.g. a cdn URL provider will most likely always return an absolute URL. + /// + /// If the provider is unable to provide a URL, it returns null. + /// + UrlInfo? GetMediaUrl(IPublishedContent content, string propertyAlias, UrlMode mode, string? culture, Uri current); } diff --git a/src/Umbraco.Core/Routing/IPublishedRequest.cs b/src/Umbraco.Core/Routing/IPublishedRequest.cs index 9f68c618d2..645de414d7 100644 --- a/src/Umbraco.Core/Routing/IPublishedRequest.cs +++ b/src/Umbraco.Core/Routing/IPublishedRequest.cs @@ -1,99 +1,114 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// The result of Umbraco routing built with the +/// +public interface IPublishedRequest { /// - /// The result of Umbraco routing built with the + /// Gets the cleaned up inbound Uri used for routing. /// - public interface IPublishedRequest - { - /// - /// Gets the cleaned up inbound Uri used for routing. - /// - /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - Uri Uri { get; } + /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + Uri Uri { get; } - /// - /// Gets the URI decoded absolute path of the - /// - string AbsolutePathDecoded { get; } + /// + /// Gets the URI decoded absolute path of the + /// + string AbsolutePathDecoded { get; } - /// - /// Gets a value indicating the requested content. - /// - IPublishedContent? PublishedContent { get; } + /// + /// Gets a value indicating the requested content. + /// + IPublishedContent? PublishedContent { get; } - /// - /// Gets a value indicating whether the current published content has been obtained - /// from the initial published content following internal redirections exclusively. - /// - /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to - /// apply the internal redirect or not, when content is not the initial content. - bool IsInternalRedirect { get; } + /// + /// Gets a value indicating whether the current published content has been obtained + /// from the initial published content following internal redirections exclusively. + /// + /// + /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to + /// apply the internal redirect or not, when content is not the initial content. + /// + bool IsInternalRedirect { get; } - /// - /// Gets the template assigned to the request (if any) - /// - ITemplate? Template { get; } + /// + /// Gets the template assigned to the request (if any) + /// + ITemplate? Template { get; } - /// - /// Gets the content request's domain. - /// - /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, - /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg "http://example.com/". - DomainAndUri? Domain { get; } + /// + /// Gets the content request's domain. + /// + /// + /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, + /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg + /// "http://example.com/". + /// + DomainAndUri? Domain { get; } - /// - /// Gets the content request's culture. - /// - /// - /// This will get mapped to a CultureInfo eventually but CultureInfo are expensive to create so we want to leave that up to the - /// localization middleware to do. See https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs#L165. - /// - string? Culture { get; } + /// + /// Gets the content request's culture. + /// + /// + /// This will get mapped to a CultureInfo eventually but CultureInfo are expensive to create so we want to leave that + /// up to the + /// localization middleware to do. See + /// https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs#L165. + /// + string? Culture { get; } - /// - /// Gets the url to redirect to, when the content request triggers a redirect. - /// - string? RedirectUrl { get; } + /// + /// Gets the url to redirect to, when the content request triggers a redirect. + /// + string? RedirectUrl { get; } - /// - /// Gets the content request http response status code. - /// - /// Does not actually set the http response status code, only registers that the response - /// should use the specified code. The code will or will not be used, in due time. - int? ResponseStatusCode { get; } + /// + /// Gets the content request http response status code. + /// + /// + /// Does not actually set the http response status code, only registers that the response + /// should use the specified code. The code will or will not be used, in due time. + /// + int? ResponseStatusCode { get; } - /// - /// Gets a list of Extensions to append to the Response.Cache object. - /// - IReadOnlyList? CacheExtensions { get; } + /// + /// Gets a list of Extensions to append to the Response.Cache object. + /// + IReadOnlyList? CacheExtensions { get; } - /// - /// Gets a dictionary of Headers to append to the Response object. - /// - IReadOnlyDictionary? Headers { get; } + /// + /// Gets a dictionary of Headers to append to the Response object. + /// + IReadOnlyDictionary? Headers { get; } - /// - /// Gets a value indicating whether the no-cache value should be added to the Cache-Control header - /// - bool SetNoCacheHeader { get; } + /// + /// Gets a value indicating whether the no-cache value should be added to the Cache-Control header + /// + bool SetNoCacheHeader { get; } - /// - /// Gets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. - /// - /// - /// This is an uncommon API used for edge cases with complex routing and would be used - /// by developers to configure the request to disable collision checks in . - /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers since - /// collission checking only occurs in the back office which is launched by - /// for which events do not execute. - /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, https://issues.umbraco.org/issue/U4-10345 - /// but it's still unclear how this was used. - /// - bool IgnorePublishedContentCollisions { get; } - } + /// + /// Gets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. + /// + /// + /// + /// This is an uncommon API used for edge cases with complex routing and would be used + /// by developers to configure the request to disable collision checks in . + /// + /// + /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers + /// since + /// collission checking only occurs in the back office which is launched by + /// + /// for which events do not execute. + /// + /// + /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, + /// https://issues.umbraco.org/issue/U4-10345 + /// but it's still unclear how this was used. + /// + /// + bool IgnorePublishedContentCollisions { get; } } diff --git a/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs b/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs index e5a915d682..f6cdafee78 100644 --- a/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs +++ b/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs @@ -1,161 +1,175 @@ -using System; -using System.Collections.Generic; using System.Globalization; using System.Net; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Used by to route inbound requests to Umbraco content +/// +public interface IPublishedRequestBuilder { /// - /// Used by to route inbound requests to Umbraco content + /// Gets the cleaned up inbound Uri used for routing. /// - public interface IPublishedRequestBuilder - { - /// - /// Gets the cleaned up inbound Uri used for routing. - /// - /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - Uri Uri { get; } + /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + Uri Uri { get; } - /// - /// Gets the URI decoded absolute path of the - /// - string AbsolutePathDecoded { get; } + /// + /// Gets the URI decoded absolute path of the + /// + string AbsolutePathDecoded { get; } - /// - /// Gets the assigned (if any) - /// - DomainAndUri? Domain { get; } + /// + /// Gets the assigned (if any) + /// + DomainAndUri? Domain { get; } - /// - /// Gets the assigned (if any) - /// - string? Culture { get; } + /// + /// Gets the assigned (if any) + /// + string? Culture { get; } - /// - /// Gets a value indicating whether the current published content has been obtained - /// from the initial published content following internal redirections exclusively. - /// - /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to - /// apply the internal redirect or not, when content is not the initial content. - bool IsInternalRedirect { get; } + /// + /// Gets a value indicating whether the current published content has been obtained + /// from the initial published content following internal redirections exclusively. + /// + /// + /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to + /// apply the internal redirect or not, when content is not the initial content. + /// + bool IsInternalRedirect { get; } - /// - /// Gets the content request http response status code. - /// - int? ResponseStatusCode { get; } + /// + /// Gets the content request http response status code. + /// + int? ResponseStatusCode { get; } - /// - /// Gets the current assigned (if any) - /// - IPublishedContent? PublishedContent { get; } + /// + /// Gets the current assigned (if any) + /// + IPublishedContent? PublishedContent { get; } - /// - /// Gets the template assigned to the request (if any) - /// - ITemplate? Template { get; } + /// + /// Gets the template assigned to the request (if any) + /// + ITemplate? Template { get; } - /// - /// Builds the - /// - IPublishedRequest Build(); + /// + /// Builds the + /// + IPublishedRequest Build(); - /// - /// Sets the domain for the request which also sets the culture - /// - IPublishedRequestBuilder SetDomain(DomainAndUri domain); + /// + /// Sets the domain for the request which also sets the culture + /// + IPublishedRequestBuilder SetDomain(DomainAndUri domain); - /// - /// Sets the culture for the request - /// - IPublishedRequestBuilder SetCulture(string? culture); + /// + /// Sets the culture for the request + /// + IPublishedRequestBuilder SetCulture(string? culture); - /// - /// Sets the found for the request - /// - /// Setting the content clears the template and redirect - IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content); + /// + /// Sets the found for the request + /// + /// Setting the content clears the template and redirect + IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content); - /// - /// Sets the requested content, following an internal redirect. - /// - /// The requested content. - /// Since this sets the content, it will clear the template - IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content); + /// + /// Sets the requested content, following an internal redirect. + /// + /// The requested content. + /// Since this sets the content, it will clear the template + IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content); - /// - /// Tries to set the template to use to display the requested content. - /// - /// The alias of the template. - /// A value indicating whether a valid template with the specified alias was found. - /// - /// Successfully setting the template does refresh RenderingEngine. - /// If setting the template fails, then the previous template (if any) remains in place. - /// - bool TrySetTemplate(string alias); + /// + /// Tries to set the template to use to display the requested content. + /// + /// The alias of the template. + /// A value indicating whether a valid template with the specified alias was found. + /// + /// Successfully setting the template does refresh RenderingEngine. + /// If setting the template fails, then the previous template (if any) remains in place. + /// + bool TrySetTemplate(string alias); - /// - /// Sets the template to use to display the requested content. - /// - /// The template. - /// Setting the template does refresh RenderingEngine. - IPublishedRequestBuilder SetTemplate(ITemplate? template); + /// + /// Sets the template to use to display the requested content. + /// + /// The template. + /// Setting the template does refresh RenderingEngine. + IPublishedRequestBuilder SetTemplate(ITemplate? template); - /// - /// Indicates that the content request should trigger a permanent redirect (301). - /// - /// The url to redirect to. - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - IPublishedRequestBuilder SetRedirectPermanent(string url); + /// + /// Indicates that the content request should trigger a permanent redirect (301). + /// + /// The url to redirect to. + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + IPublishedRequestBuilder SetRedirectPermanent(string url); - /// - /// Indicates that the content request should trigger a redirect, with a specified status code. - /// - /// The url to redirect to. - /// The status code (300-308). - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect); + /// + /// Indicates that the content request should trigger a redirect, with a specified status code. + /// + /// The url to redirect to. + /// The status code (300-308). + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect); - /// - /// Sets the http response status code, along with an optional associated description. - /// - /// The http status code. - /// Does not actually set the http response status code and description, only registers that - /// the response should use the specified code and description. The code and description will or will - /// not be used, in due time. - IPublishedRequestBuilder SetResponseStatus(int code); + /// + /// Sets the http response status code, along with an optional associated description. + /// + /// The http status code. + /// + /// Does not actually set the http response status code and description, only registers that + /// the response should use the specified code and description. The code and description will or will + /// not be used, in due time. + /// + IPublishedRequestBuilder SetResponseStatus(int code); - /// - /// Sets the no-cache value to the Cache-Control header - /// - /// True to set the header, false to not set it - IPublishedRequestBuilder SetNoCacheHeader(bool setHeader); + /// + /// Sets the no-cache value to the Cache-Control header + /// + /// True to set the header, false to not set it + IPublishedRequestBuilder SetNoCacheHeader(bool setHeader); - /// - /// Sets a list of Extensions to append to the Response.Cache object. - /// - IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions); + /// + /// Sets a list of Extensions to append to the Response.Cache object. + /// + IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions); - /// - /// Sets a dictionary of Headers to append to the Response object. - /// - IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers); + /// + /// Sets a dictionary of Headers to append to the Response object. + /// + IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers); - /// - /// Can be called to configure the result to ignore URL collisions - /// - /// - /// This is an uncommon API used for edge cases with complex routing and would be used - /// by developers to configure the request to disable collision checks in . - /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers since - /// collission checking only occurs in the back office which is launched by - /// for which events do not execute. - /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, https://issues.umbraco.org/issue/U4-10345 - /// but it's still unclear how this was used. - /// - void IgnorePublishedContentCollisions(); - } + /// + /// Can be called to configure the result to ignore URL collisions + /// + /// + /// + /// This is an uncommon API used for edge cases with complex routing and would be used + /// by developers to configure the request to disable collision checks in . + /// + /// + /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers + /// since + /// collission checking only occurs in the back office which is launched by + /// + /// for which events do not execute. + /// + /// + /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, + /// https://issues.umbraco.org/issue/U4-10345 + /// but it's still unclear how this was used. + /// + /// + void IgnorePublishedContentCollisions(); } diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index a3c041768f..5434c46447 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -1,52 +1,49 @@ -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Routes requests. +/// +public interface IPublishedRouter { /// - /// Routes requests. + /// Creates a published request. /// - public interface IPublishedRouter - { - /// - /// Creates a published request. - /// - /// The current request Uri. - /// A published request builder. - Task CreateRequestAsync(Uri uri); + /// The current request Uri. + /// A published request builder. + Task CreateRequestAsync(Uri uri); - /// - /// Sends a through the routing pipeline and builds a result. - /// - /// The request. - /// The options. - /// The built instance. - Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options); + /// + /// Sends a through the routing pipeline and builds a result. + /// + /// The request. + /// The options. + /// The built instance. + Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options); - /// - /// Updates the request to use the specified item, or NULL - /// - /// The request. - /// - /// - /// A new based on values from the original - /// and with the re-routed values based on the passed in - /// - /// - /// This method is used for 2 cases: - /// - When the rendering content needs to change due to Public Access rules. - /// - When there is nothing to render due to circumstances such as no template files. In this case, NULL is used as the parameter. - /// - /// - /// This method is invoked when the pipeline decides it cannot render - /// the request, for whatever reason, and wants to force it to be re-routed - /// and rendered as if no document were found (404). - /// This occurs if there is no template found and route hijacking was not matched. - /// In that case it's the same as if there was no content which means even if there was - /// content matched we want to run the request through the last chance finders. - /// - /// - Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent); - } + /// + /// Updates the request to use the specified item, or NULL + /// + /// The request. + /// + /// + /// A new based on values from the original + /// and with the re-routed values based on the passed in + /// + /// + /// This method is used for 2 cases: + /// - When the rendering content needs to change due to Public Access rules. + /// - When there is nothing to render due to circumstances such as no template files. In this case, NULL is used as the parameter. + /// + /// + /// This method is invoked when the pipeline decides it cannot render + /// the request, for whatever reason, and wants to force it to be re-routed + /// and rendered as if no document were found (404). + /// This occurs if there is no template found and route hijacking was not matched. + /// In that case it's the same as if there was no content which means even if there was + /// content matched we want to run the request through the last chance finders. + /// + /// + Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent); } diff --git a/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs b/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs index fd52bc7805..598ad1b535 100644 --- a/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs +++ b/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs @@ -1,104 +1,109 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public interface IPublishedUrlProvider { - public interface IPublishedUrlProvider - { - /// - /// Gets or sets the provider url mode. - /// - UrlMode Mode { get; set; } + /// + /// Gets or sets the provider url mode. + /// + UrlMode Mode { get; set; } - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// The url mode. - /// A culture. - /// The current absolute url. - /// The url for the published content. - string GetUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); + /// + /// Gets the url of a published content. + /// + /// The published content identifier. + /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + string GetUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// The url mode. - /// A culture. - /// The current absolute url. - /// The url for the published content. - string GetUrl(int id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); + /// + /// Gets the url of a published content. + /// + /// The published content identifier. + /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + string GetUrl(int id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); - /// - /// Gets the url of a published content. - /// - /// The published content. - /// The url mode. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the published content is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns "#". - /// - string GetUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); + /// + /// Gets the url of a published content. + /// + /// The published content. + /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + /// + /// The url is absolute or relative depending on mode and on current. + /// + /// If the published content is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// If the provider is unable to provide a url, it returns "#". + /// + string GetUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); - string GetUrlFromRoute(int id, string? route, string? culture); + string GetUrlFromRoute(int id, string? route, string? culture); - /// - /// Gets the other urls of a published content. - /// - /// The published content id. - /// The other urls for the published content. - /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// The results depend on the current url. - /// - IEnumerable GetOtherUrls(int id); + /// + /// Gets the other urls of a published content. + /// + /// The published content id. + /// The other urls for the published content. + /// + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// The results depend on the current url. + /// + IEnumerable GetOtherUrls(int id); - /// - /// Gets the other urls of a published content. - /// - /// The published content id. - /// The current absolute url. - /// The other urls for the published content. - /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - IEnumerable GetOtherUrls(int id, Uri current); + /// + /// Gets the other urls of a published content. + /// + /// The published content id. + /// The current absolute url. + /// The other urls for the published content. + /// + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + IEnumerable GetOtherUrls(int id, Uri current); - /// - /// Gets the url of a media item. - /// - /// - /// - /// - /// - /// - /// - string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); + /// + /// Gets the url of a media item. + /// + /// + /// + /// + /// + /// + /// + string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); - /// - /// Gets the url of a media item. - /// - /// The published content. - /// The property alias to resolve the url from. - /// The url mode. - /// The variation language. - /// The current absolute url. - /// The url for the media. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns . - /// - string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); - } + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// If the provider is unable to provide a url, it returns . + /// + string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); } diff --git a/src/Umbraco.Core/Routing/ISiteDomainMapper.cs b/src/Umbraco.Core/Routing/ISiteDomainMapper.cs index e9ca34477c..93afe32d93 100644 --- a/src/Umbraco.Core/Routing/ISiteDomainMapper.cs +++ b/src/Umbraco.Core/Routing/ISiteDomainMapper.cs @@ -1,45 +1,47 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Routing +/// +/// Provides utilities to handle site domains. +/// +public interface ISiteDomainMapper { /// - /// Provides utilities to handle site domains. + /// Filters a list of DomainAndUri to pick one that best matches the current request. /// - public interface ISiteDomainMapper - { - /// - /// Filters a list of DomainAndUri to pick one that best matches the current request. - /// - /// The list of DomainAndUri to filter. - /// The Uri of the current request. - /// A culture. - /// The default culture. - /// The selected DomainAndUri. - /// - /// If the filter is invoked then is _not_ empty and - /// is _not_ null, and could not be - /// matched with anything in . - /// The may be null, but when non-null, it can be used - /// to help pick the best matches. - /// The filter _must_ return something else an exception will be thrown. - /// - DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, string? culture, string? defaultCulture); + /// The list of DomainAndUri to filter. + /// The Uri of the current request. + /// A culture. + /// The default culture. + /// The selected DomainAndUri. + /// + /// + /// If the filter is invoked then is _not_ empty and + /// is _not_ null, and could not be + /// matched with anything in . + /// + /// + /// The may be null, but when non-null, it can be used + /// to help pick the best matches. + /// + /// The filter _must_ return something else an exception will be thrown. + /// + DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, string? culture, string? defaultCulture); - /// - /// Filters a list of DomainAndUri to pick those that best matches the current request. - /// - /// The list of DomainAndUri to filter. - /// The Uri of the current request. - /// A value indicating whether to exclude the current/default domain. - /// A culture. - /// The default culture. - /// The selected DomainAndUri items. - /// - /// The filter must return something, even empty, else an exception will be thrown. - /// The may be null, but when non-null, it can be used - /// to help pick the best matches. - /// - IEnumerable MapDomains(IReadOnlyCollection domainAndUris, Uri current, bool excludeDefault, string? culture, string? defaultCulture); - } + /// + /// Filters a list of DomainAndUri to pick those that best matches the current request. + /// + /// The list of DomainAndUri to filter. + /// The Uri of the current request. + /// A value indicating whether to exclude the current/default domain. + /// A culture. + /// The default culture. + /// The selected DomainAndUri items. + /// + /// The filter must return something, even empty, else an exception will be thrown. + /// + /// The may be null, but when non-null, it can be used + /// to help pick the best matches. + /// + /// + IEnumerable MapDomains(IReadOnlyCollection domainAndUris, Uri current, bool excludeDefault, string? culture, string? defaultCulture); } diff --git a/src/Umbraco.Core/Routing/IUrlProvider.cs b/src/Umbraco.Core/Routing/IUrlProvider.cs index 0223b39c1d..38f28b3764 100644 --- a/src/Umbraco.Core/Routing/IUrlProvider.cs +++ b/src/Umbraco.Core/Routing/IUrlProvider.cs @@ -1,40 +1,41 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides URLs. +/// +public interface IUrlProvider { /// - /// Provides URLs. + /// Gets the URL of a published content. /// - public interface IUrlProvider - { - /// - /// Gets the URL of a published content. - /// - /// The published content. - /// The URL mode. - /// A culture. - /// The current absolute URL. - /// The URL for the published content. - /// - /// The URL is absolute or relative depending on mode and on current. - /// If the published content is multi-lingual, gets the URL for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a URL, it should return null. - /// - UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current); + /// The published content. + /// The URL mode. + /// A culture. + /// The current absolute URL. + /// The URL for the published content. + /// + /// The URL is absolute or relative depending on mode and on current. + /// + /// If the published content is multi-lingual, gets the URL for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// If the provider is unable to provide a URL, it should return null. + /// + UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current); - /// - /// Gets the other URLs of a published content. - /// - /// The published content id. - /// The current absolute URL. - /// The other URLs for the published content. - /// - /// Other URLs are those that GetUrl would not return in the current context, but would be valid - /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - IEnumerable GetOtherUrls(int id, Uri current); - } + /// + /// Gets the other URLs of a published content. + /// + /// The published content id. + /// The current absolute URL. + /// The other URLs for the published content. + /// + /// + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + IEnumerable GetOtherUrls(int id, Uri current); } diff --git a/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs b/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs index 264be41d60..85b864d717 100644 --- a/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs +++ b/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class MediaUrlProviderCollection : BuilderCollectionBase { - public class MediaUrlProviderCollection : BuilderCollectionBase + public MediaUrlProviderCollection(Func> items) + : base(items) { - public MediaUrlProviderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs b/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs index d778540e31..ba0a9b9fc2 100644 --- a/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs +++ b/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class MediaUrlProviderCollectionBuilder : OrderedCollectionBuilderBase { - public class MediaUrlProviderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override MediaUrlProviderCollectionBuilder This => this; - } + protected override MediaUrlProviderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Routing/PublishedRequest.cs b/src/Umbraco.Core/Routing/PublishedRequest.cs index 50328cbfdd..e3fc3818ef 100644 --- a/src/Umbraco.Core/Routing/PublishedRequest.cs +++ b/src/Umbraco.Core/Routing/PublishedRequest.cs @@ -1,70 +1,79 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class PublishedRequest : IPublishedRequest { - - public class PublishedRequest : IPublishedRequest + /// + /// Initializes a new instance of the class. + /// + public PublishedRequest( + Uri uri, + string absolutePathDecoded, + IPublishedContent? publishedContent, + bool isInternalRedirect, + ITemplate? template, + DomainAndUri? domain, + string? culture, + string? redirectUrl, + int? responseStatusCode, + IReadOnlyList? cacheExtensions, + IReadOnlyDictionary? headers, + bool setNoCacheHeader, + bool ignorePublishedContentCollisions) { - /// - /// Initializes a new instance of the class. - /// - public PublishedRequest(Uri uri, string absolutePathDecoded, IPublishedContent? publishedContent, bool isInternalRedirect, ITemplate? template, DomainAndUri? domain, string? culture, string? redirectUrl, int? responseStatusCode, IReadOnlyList? cacheExtensions, IReadOnlyDictionary? headers, bool setNoCacheHeader, bool ignorePublishedContentCollisions) - { - Uri = uri ?? throw new ArgumentNullException(nameof(uri)); - AbsolutePathDecoded = absolutePathDecoded ?? throw new ArgumentNullException(nameof(absolutePathDecoded)); - PublishedContent = publishedContent; - IsInternalRedirect = isInternalRedirect; - Template = template; - Domain = domain; - Culture = culture; - RedirectUrl = redirectUrl; - ResponseStatusCode = responseStatusCode; - CacheExtensions = cacheExtensions; - Headers = headers; - SetNoCacheHeader = setNoCacheHeader; - IgnorePublishedContentCollisions = ignorePublishedContentCollisions; - } - - /// - public Uri Uri { get; } - - /// - public string AbsolutePathDecoded { get; } - - /// - public bool IgnorePublishedContentCollisions { get; } - - /// - public IPublishedContent? PublishedContent { get; } - - /// - public bool IsInternalRedirect { get; } - - /// - public ITemplate? Template { get; } - - /// - public DomainAndUri? Domain { get; } - - /// - public string? Culture { get; } - - /// - public string? RedirectUrl { get; } - - /// - public int? ResponseStatusCode { get; } - - /// - public IReadOnlyList? CacheExtensions { get; } - - /// - public IReadOnlyDictionary? Headers { get; } - - /// - public bool SetNoCacheHeader { get; } + Uri = uri ?? throw new ArgumentNullException(nameof(uri)); + AbsolutePathDecoded = absolutePathDecoded ?? throw new ArgumentNullException(nameof(absolutePathDecoded)); + PublishedContent = publishedContent; + IsInternalRedirect = isInternalRedirect; + Template = template; + Domain = domain; + Culture = culture; + RedirectUrl = redirectUrl; + ResponseStatusCode = responseStatusCode; + CacheExtensions = cacheExtensions; + Headers = headers; + SetNoCacheHeader = setNoCacheHeader; + IgnorePublishedContentCollisions = ignorePublishedContentCollisions; } + + /// + public Uri Uri { get; } + + /// + public string AbsolutePathDecoded { get; } + + /// + public bool IgnorePublishedContentCollisions { get; } + + /// + public IPublishedContent? PublishedContent { get; } + + /// + public bool IsInternalRedirect { get; } + + /// + public ITemplate? Template { get; } + + /// + public DomainAndUri? Domain { get; } + + /// + public string? Culture { get; } + + /// + public string? RedirectUrl { get; } + + /// + public int? ResponseStatusCode { get; } + + /// + public IReadOnlyList? CacheExtensions { get; } + + /// + public IReadOnlyDictionary? Headers { get; } + + /// + public bool SetNoCacheHeader { get; } } diff --git a/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs b/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs index 128c81f605..180033dd33 100644 --- a/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs +++ b/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs @@ -1,204 +1,200 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class PublishedRequestBuilder : IPublishedRequestBuilder { - public class PublishedRequestBuilder : IPublishedRequestBuilder + private readonly IFileService _fileService; + private bool _cacheability; + private IReadOnlyList? _cacheExtensions; + private IReadOnlyDictionary? _headers; + private bool _ignorePublishedContentCollisions; + private IPublishedContent? _publishedContent; + private string? _redirectUrl; + private HttpStatusCode? _responseStatus; + + /// + /// Initializes a new instance of the class. + /// + public PublishedRequestBuilder(Uri uri, IFileService fileService) { - private readonly IFileService _fileService; - private IReadOnlyDictionary? _headers; - private bool _cacheability; - private IReadOnlyList? _cacheExtensions; - private string? _redirectUrl; - private HttpStatusCode? _responseStatus; - private IPublishedContent? _publishedContent; - private bool _ignorePublishedContentCollisions; + Uri = uri; + AbsolutePathDecoded = uri.GetAbsolutePathDecoded(); + _fileService = fileService; + } - /// - /// Initializes a new instance of the class. - /// - public PublishedRequestBuilder(Uri uri, IFileService fileService) + /// + public Uri Uri { get; } + + /// + public string AbsolutePathDecoded { get; } + + /// + public DomainAndUri? Domain { get; private set; } + + /// + public string? Culture { get; private set; } + + /// + public ITemplate? Template { get; private set; } + + /// + public bool IsInternalRedirect { get; private set; } + + /// + public int? ResponseStatusCode => _responseStatus.HasValue ? (int?)_responseStatus : null; + + /// + public IPublishedContent? PublishedContent + { + get => _publishedContent; + private set { - Uri = uri; - AbsolutePathDecoded = uri.GetAbsolutePathDecoded(); - _fileService = fileService; - } - - /// - public Uri Uri { get; } - - /// - public string AbsolutePathDecoded { get; } - - /// - public DomainAndUri? Domain { get; private set; } - - /// - public string? Culture { get; private set; } - - /// - public ITemplate? Template { get; private set; } - - /// - public bool IsInternalRedirect { get; private set; } - - /// - public int? ResponseStatusCode => _responseStatus.HasValue ? (int?)_responseStatus : null; - - /// - public IPublishedContent? PublishedContent - { - get => _publishedContent; - private set - { - _publishedContent = value; - IsInternalRedirect = false; - Template = null; - } - } - - /// - public IPublishedRequest Build() => new PublishedRequest( - Uri, - AbsolutePathDecoded, - PublishedContent, - IsInternalRedirect, - Template, - Domain, - Culture, - _redirectUrl, - _responseStatus.HasValue ? (int?)_responseStatus : null, - _cacheExtensions, - _headers, - _cacheability, - _ignorePublishedContentCollisions); - - /// - public IPublishedRequestBuilder SetNoCacheHeader(bool cacheability) - { - _cacheability = cacheability; - return this; - } - - /// - public IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions) - { - _cacheExtensions = cacheExtensions.ToList(); - return this; - } - - /// - public IPublishedRequestBuilder SetCulture(string? culture) - { - Culture = culture; - return this; - } - - /// - public IPublishedRequestBuilder SetDomain(DomainAndUri domain) - { - Domain = domain; - SetCulture(domain.Culture); - return this; - } - - /// - public IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers) - { - _headers = headers; - return this; - } - - /// - public IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content) - { - // unless a template has been set already by the finder, - // template should be null at that point. - - // redirecting to self - if (PublishedContent != null && content.Id == PublishedContent.Id) - { - // no need to set PublishedContent, we're done - IsInternalRedirect = true; - return this; - } - - // else - - // set published content - this resets the template, and sets IsInternalRedirect to false - PublishedContent = content; - IsInternalRedirect = true; - - return this; - } - - /// - public IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content) - { - PublishedContent = content; + _publishedContent = value; IsInternalRedirect = false; + Template = null; + } + } + + /// + public IPublishedRequest Build() => new PublishedRequest( + Uri, + AbsolutePathDecoded, + PublishedContent, + IsInternalRedirect, + Template, + Domain, + Culture, + _redirectUrl, + _responseStatus.HasValue ? (int?)_responseStatus : null, + _cacheExtensions, + _headers, + _cacheability, + _ignorePublishedContentCollisions); + + /// + public IPublishedRequestBuilder SetNoCacheHeader(bool cacheability) + { + _cacheability = cacheability; + return this; + } + + /// + public IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions) + { + _cacheExtensions = cacheExtensions.ToList(); + return this; + } + + /// + public IPublishedRequestBuilder SetCulture(string? culture) + { + Culture = culture; + return this; + } + + /// + public IPublishedRequestBuilder SetDomain(DomainAndUri domain) + { + Domain = domain; + SetCulture(domain.Culture); + return this; + } + + /// + public IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers) + { + _headers = headers; + return this; + } + + /// + public IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content) + { + // unless a template has been set already by the finder, + // template should be null at that point. + + // redirecting to self + if (PublishedContent != null && content.Id == PublishedContent.Id) + { + // no need to set PublishedContent, we're done + IsInternalRedirect = true; return this; } - /// - public IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect) + // else + + // set published content - this resets the template, and sets IsInternalRedirect to false + PublishedContent = content; + IsInternalRedirect = true; + + return this; + } + + /// + public IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content) + { + PublishedContent = content; + IsInternalRedirect = false; + return this; + } + + /// + public IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect) + { + _redirectUrl = url; + _responseStatus = (HttpStatusCode)status; + return this; + } + + /// + public IPublishedRequestBuilder SetRedirectPermanent(string url) + { + _redirectUrl = url; + _responseStatus = HttpStatusCode.Moved; + return this; + } + + /// + public IPublishedRequestBuilder SetResponseStatus(int code) + { + _responseStatus = (HttpStatusCode)code; + return this; + } + + /// + public IPublishedRequestBuilder SetTemplate(ITemplate? template) + { + Template = template; + return this; + } + + /// + public bool TrySetTemplate(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) { - _redirectUrl = url; - _responseStatus = (HttpStatusCode)status; - return this; - } - - /// - public IPublishedRequestBuilder SetRedirectPermanent(string url) - { - _redirectUrl = url; - _responseStatus = HttpStatusCode.Moved; - return this; - } - - /// - public IPublishedRequestBuilder SetResponseStatus(int code) - { - _responseStatus = (HttpStatusCode)code; - return this; - } - - /// - public IPublishedRequestBuilder SetTemplate(ITemplate? template) - { - Template = template; - return this; - } - - /// - public bool TrySetTemplate(string alias) - { - if (string.IsNullOrWhiteSpace(alias)) - { - Template = null; - return true; - } - - // NOTE - can we still get it with whitespaces in it due to old legacy bugs? - alias = alias.Replace(" ", string.Empty); - - ITemplate? model = _fileService.GetTemplate(alias); - if (model == null) - { - return false; - } - - Template = model; + Template = null; return true; } - /// - public void IgnorePublishedContentCollisions() => _ignorePublishedContentCollisions = true; + // NOTE - can we still get it with whitespaces in it due to old legacy bugs? + alias = alias.Replace(" ", string.Empty); + + ITemplate? model = _fileService.GetTemplate(alias); + if (model == null) + { + return false; + } + + Template = model; + return true; } + + /// + public void IgnorePublishedContentCollisions() => _ignorePublishedContentCollisions = true; } diff --git a/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs b/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs index 855bd53bde..6b9720e4ac 100644 --- a/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs +++ b/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs @@ -1,97 +1,102 @@ using System.Net; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public static class PublishedRequestExtensions { - - public static class PublishedRequestExtensions + /// + /// Gets the + /// + public static UmbracoRouteResult GetRouteResult(this IPublishedRequest publishedRequest) { - /// - /// Gets the - /// - public static UmbracoRouteResult GetRouteResult(this IPublishedRequest publishedRequest) + if (publishedRequest.IsRedirect()) { - if (publishedRequest.IsRedirect()) - { - return UmbracoRouteResult.Redirect; - } - - if (!publishedRequest.HasPublishedContent()) - { - return UmbracoRouteResult.NotFound; - } - - return UmbracoRouteResult.Success; + return UmbracoRouteResult.Redirect; } - /// - /// Gets a value indicating whether the request was successfully routed - /// - public static bool Success(this IPublishedRequest publishedRequest) - => !publishedRequest.IsRedirect() && publishedRequest.HasPublishedContent(); - - /// - /// Sets the response status to be 404 not found - /// - public static IPublishedRequestBuilder SetIs404(this IPublishedRequestBuilder publishedRequest) + if (!publishedRequest.HasPublishedContent()) { - publishedRequest.SetResponseStatus((int)HttpStatusCode.NotFound); - return publishedRequest; + return UmbracoRouteResult.NotFound; } - /// - /// Gets a value indicating whether the content request has a content. - /// - public static bool HasPublishedContent(this IPublishedRequestBuilder publishedRequest) => publishedRequest.PublishedContent != null; - - /// - /// Gets a value indicating whether the content request has a content. - /// - public static bool HasPublishedContent(this IPublishedRequest publishedRequest) => publishedRequest.PublishedContent != null; - - /// - /// Gets a value indicating whether the content request has a template. - /// - public static bool HasTemplate(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Template != null; - - /// - /// Gets a value indicating whether the content request has a template. - /// - public static bool HasTemplate(this IPublishedRequest publishedRequest) => publishedRequest.Template != null; - - /// - /// Gets the alias of the template to use to display the requested content. - /// - public static string? GetTemplateAlias(this IPublishedRequest publishedRequest) => publishedRequest.Template?.Alias; - - /// - /// Gets a value indicating whether the requested content could not be found. - /// - public static bool Is404(this IPublishedRequest publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.NotFound; - - /// - /// Gets a value indicating whether the content request triggers a redirect (permanent or not). - /// - public static bool IsRedirect(this IPublishedRequestBuilder publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; - - /// - /// Gets indicating whether the content request triggers a redirect (permanent or not). - /// - public static bool IsRedirect(this IPublishedRequest publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; - - /// - /// Gets a value indicating whether the redirect is permanent. - /// - public static bool IsRedirectPermanent(this IPublishedRequest publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; - - /// - /// Gets a value indicating whether the content request has a domain. - /// - public static bool HasDomain(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Domain != null; - - /// - /// Gets a value indicating whether the content request has a domain. - /// - public static bool HasDomain(this IPublishedRequest publishedRequest) => publishedRequest.Domain != null; - + return UmbracoRouteResult.Success; } + + /// + /// Gets a value indicating whether the request was successfully routed + /// + public static bool Success(this IPublishedRequest publishedRequest) + => !publishedRequest.IsRedirect() && publishedRequest.HasPublishedContent(); + + /// + /// Sets the response status to be 404 not found + /// + public static IPublishedRequestBuilder SetIs404(this IPublishedRequestBuilder publishedRequest) + { + publishedRequest.SetResponseStatus((int)HttpStatusCode.NotFound); + return publishedRequest; + } + + /// + /// Gets a value indicating whether the content request has a content. + /// + public static bool HasPublishedContent(this IPublishedRequestBuilder publishedRequest) => + publishedRequest.PublishedContent != null; + + /// + /// Gets a value indicating whether the content request has a content. + /// + public static bool HasPublishedContent(this IPublishedRequest publishedRequest) => + publishedRequest.PublishedContent != null; + + /// + /// Gets a value indicating whether the content request has a template. + /// + public static bool HasTemplate(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Template != null; + + /// + /// Gets a value indicating whether the content request has a template. + /// + public static bool HasTemplate(this IPublishedRequest publishedRequest) => publishedRequest.Template != null; + + /// + /// Gets the alias of the template to use to display the requested content. + /// + public static string? GetTemplateAlias(this IPublishedRequest publishedRequest) => publishedRequest.Template?.Alias; + + /// + /// Gets a value indicating whether the requested content could not be found. + /// + public static bool Is404(this IPublishedRequest publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.NotFound; + + /// + /// Gets a value indicating whether the content request triggers a redirect (permanent or not). + /// + public static bool IsRedirect(this IPublishedRequestBuilder publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; + + /// + /// Gets indicating whether the content request triggers a redirect (permanent or not). + /// + public static bool IsRedirect(this IPublishedRequest publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; + + /// + /// Gets a value indicating whether the redirect is permanent. + /// + public static bool IsRedirectPermanent(this IPublishedRequest publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; + + /// + /// Gets a value indicating whether the content request has a domain. + /// + public static bool HasDomain(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Domain != null; + + /// + /// Gets a value indicating whether the content request has a domain. + /// + public static bool HasDomain(this IPublishedRequest publishedRequest) => publishedRequest.Domain != null; } diff --git a/src/Umbraco.Core/Routing/PublishedRequestOld.cs b/src/Umbraco.Core/Routing/PublishedRequestOld.cs index 44a75aaccd..c7167971df 100644 --- a/src/Umbraco.Core/Routing/PublishedRequestOld.cs +++ b/src/Umbraco.Core/Routing/PublishedRequestOld.cs @@ -1,392 +1,412 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading; +using System.Globalization; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +// TODO: Kill this, but we need to port all of it's functionality +public class PublishedRequestOld // : IPublishedRequest { - // TODO: Kill this, but we need to port all of it's functionality - public class PublishedRequestOld // : IPublishedRequest + private readonly IPublishedRouter _publishedRouter; + private readonly WebRoutingSettings _webRoutingSettings; + private CultureInfo? _culture; + private DomainAndUri? _domain; + private bool _is404; + private IPublishedContent? _publishedContent; + + private bool _readonly; // after prepared + + /// + /// Initializes a new instance of the class. + /// + public PublishedRequestOld(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IOptions webRoutingSettings, Uri? uri = null) { - private readonly IPublishedRouter _publishedRouter; - private readonly WebRoutingSettings _webRoutingSettings; - - private bool _readonly; // after prepared - private bool _is404; - private DomainAndUri? _domain; - private CultureInfo? _culture; - private IPublishedContent? _publishedContent; - private IPublishedContent? _initialPublishedContent; // found by finders before 404, redirects, etc - - /// - /// Initializes a new instance of the class. - /// - public PublishedRequestOld(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IOptions webRoutingSettings, Uri? uri = null) - { - UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); - _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); - _webRoutingSettings = webRoutingSettings.Value; - Uri = uri ?? umbracoContext.CleanedUmbracoUrl; - } - - /// - /// Gets the UmbracoContext. - /// - public IUmbracoContext UmbracoContext { get; } - - /// - /// Gets or sets the cleaned up Uri used for routing. - /// - /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - public Uri Uri { get; } - - // utility for ensuring it is ok to set some properties - public void EnsureWriteable() - { - if (_readonly) - { - throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only."); - } - } - - public bool CacheabilityNoCache { get; set; } - - ///// - ///// Prepares the request. - ///// - //public void Prepare() - //{ - // _publishedRouter.PrepareRequest(this); - //} - - /// - /// Gets or sets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. - /// - public bool IgnorePublishedContentCollisions { get; set; } - - //#region Events - - ///// - ///// Triggers before the published content request is prepared. - ///// - ///// When the event triggers, no preparation has been done. It is still possible to - ///// modify the request's Uri property, for example to restore its original, public-facing value - ///// that might have been modified by an in-between equipment such as a load-balancer. - //public static event EventHandler Preparing; - - ///// - ///// Triggers once the published content request has been prepared, but before it is processed. - ///// - ///// When the event triggers, preparation is done ie domain, culture, document, template, - ///// rendering engine, etc. have been setup. It is then possible to change anything, before - ///// the request is actually processed and rendered by Umbraco. - //public static event EventHandler Prepared; - - ///// - ///// Triggers the Preparing event. - ///// - //public void OnPreparing() - //{ - // Preparing?.Invoke(this, EventArgs.Empty); - //} - - ///// - ///// Triggers the Prepared event. - ///// - //public void OnPrepared() - //{ - // Prepared?.Invoke(this, EventArgs.Empty); - - // if (HasPublishedContent == false) - // Is404 = true; // safety - - // _readonly = true; - //} - - //#endregion - - #region PublishedContent - - ///// - ///// Gets or sets the requested content. - ///// - ///// Setting the requested content clears Template. - //public IPublishedContent PublishedContent - //{ - // get { return _publishedContent; } - // set - // { - // EnsureWriteable(); - // _publishedContent = value; - // IsInternalRedirectPublishedContent = false; - // TemplateModel = null; - // } - //} - - /// - /// Sets the requested content, following an internal redirect. - /// - /// The requested content. - /// Depending on UmbracoSettings.InternalRedirectPreservesTemplate, will - /// preserve or reset the template, if any. - public void SetInternalRedirectPublishedContent(IPublishedContent content) - { - //if (content == null) - // throw new ArgumentNullException(nameof(content)); - //EnsureWriteable(); - - //// unless a template has been set already by the finder, - //// template should be null at that point. - - //// IsInternalRedirect if IsInitial, or already IsInternalRedirect - //var isInternalRedirect = IsInitialPublishedContent || IsInternalRedirectPublishedContent; - - //// redirecting to self - //if (content.Id == PublishedContent.Id) // neither can be null - //{ - // // no need to set PublishedContent, we're done - // IsInternalRedirectPublishedContent = isInternalRedirect; - // return; - //} - - //// else - - //// save - //var template = Template; - - //// set published content - this resets the template, and sets IsInternalRedirect to false - //PublishedContent = content; - //IsInternalRedirectPublishedContent = isInternalRedirect; - - //// must restore the template if it's an internal redirect & the config option is set - //if (isInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) - //{ - // // restore - // TemplateModel = template; - //} - } - - /// - /// Gets the initial requested content. - /// - /// The initial requested content is the content that was found by the finders, - /// before anything such as 404, redirect... took place. - public IPublishedContent? InitialPublishedContent => _initialPublishedContent; - - /// - /// Gets value indicating whether the current published content is the initial one. - /// - public bool IsInitialPublishedContent => _initialPublishedContent != null && _initialPublishedContent == _publishedContent; - - /// - /// Indicates that the current PublishedContent is the initial one. - /// - public void SetIsInitialPublishedContent() - { - EnsureWriteable(); - - // note: it can very well be null if the initial content was not found - _initialPublishedContent = _publishedContent; - IsInternalRedirectPublishedContent = false; - } - - /// - /// Gets or sets a value indicating whether the current published content has been obtained - /// from the initial published content following internal redirections exclusively. - /// - /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to - /// apply the internal redirect or not, when content is not the initial content. - public bool IsInternalRedirectPublishedContent { get; private set; } - - - #endregion - - /// - /// Gets or sets the template model to use to display the requested content. - /// - public ITemplate? Template { get; } - - /// - /// Gets the alias of the template to use to display the requested content. - /// - public string? TemplateAlias => Template?.Alias; - - - /// - /// Gets or sets the content request's domain. - /// - /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, - /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg "http://example.com/". - public DomainAndUri? Domain - { - get { return _domain; } - set - { - EnsureWriteable(); - _domain = value; - } - } - - /// - /// Gets a value indicating whether the content request has a domain. - /// - public bool HasDomain => Domain != null; - - /// - /// Gets or sets the content request's culture. - /// - public CultureInfo Culture - { - get { return _culture ?? Thread.CurrentThread.CurrentCulture; } - set - { - EnsureWriteable(); - _culture = value; - } - } - - // note: do we want to have an ordered list of alternate cultures, - // to allow for fallbacks when doing dictionary lookup and such? - - - #region Status - - /// - /// Gets or sets a value indicating whether the requested content could not be found. - /// - /// This is set in the PublishedContentRequestBuilder and can also be used in - /// custom content finders or Prepared event handlers, where we want to allow developers - /// to indicate a request is 404 but not to cancel it. - public bool Is404 - { - get { return _is404; } - set - { - EnsureWriteable(); - _is404 = value; - } - } - - /// - /// Gets a value indicating whether the content request triggers a redirect (permanent or not). - /// - public bool IsRedirect => string.IsNullOrWhiteSpace(RedirectUrl) == false; - - /// - /// Gets or sets a value indicating whether the redirect is permanent. - /// - public bool IsRedirectPermanent { get; private set; } - - /// - /// Gets or sets the URL to redirect to, when the content request triggers a redirect. - /// - public string? RedirectUrl { get; private set; } - - /// - /// Indicates that the content request should trigger a redirect (302). - /// - /// The URL to redirect to. - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - public void SetRedirect(string url) - { - EnsureWriteable(); - RedirectUrl = url; - IsRedirectPermanent = false; - } - - /// - /// Indicates that the content request should trigger a permanent redirect (301). - /// - /// The URL to redirect to. - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - public void SetRedirectPermanent(string url) - { - EnsureWriteable(); - RedirectUrl = url; - IsRedirectPermanent = true; - } - - /// - /// Indicates that the content request should trigger a redirect, with a specified status code. - /// - /// The URL to redirect to. - /// The status code (300-308). - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - public void SetRedirect(string url, int status) - { - EnsureWriteable(); - - if (status < 300 || status > 308) - throw new ArgumentOutOfRangeException(nameof(status), "Valid redirection status codes 300-308."); - - RedirectUrl = url; - IsRedirectPermanent = (status == 301 || status == 308); - if (status != 301 && status != 302) // default redirect statuses - ResponseStatusCode = status; - } - - /// - /// Gets or sets the content request http response status code. - /// - /// Does not actually set the http response status code, only registers that the response - /// should use the specified code. The code will or will not be used, in due time. - public int ResponseStatusCode { get; private set; } - - /// - /// Gets or sets the content request http response status description. - /// - /// Does not actually set the http response status description, only registers that the response - /// should use the specified description. The description will or will not be used, in due time. - public string? ResponseStatusDescription { get; private set; } - - /// - /// Sets the http response status code, along with an optional associated description. - /// - /// The http status code. - /// The description. - /// Does not actually set the http response status code and description, only registers that - /// the response should use the specified code and description. The code and description will or will - /// not be used, in due time. - public void SetResponseStatus(int code, string? description = null) - { - EnsureWriteable(); - - // .Status is deprecated - // .SubStatusCode is IIS 7+ internal, ignore - ResponseStatusCode = code; - ResponseStatusDescription = description; - } - - #endregion - - #region Response Cache - - /// - /// Gets or sets the System.Web.HttpCacheability - /// - // Note: we used to set a default value here but that would then be the default - // for ALL requests, we shouldn't overwrite it though if people are using [OutputCache] for example - // see: https://our.umbraco.com/forum/using-umbraco-and-getting-started/79715-output-cache-in-umbraco-752 - //public HttpCacheability Cacheability { get; set; } - - /// - /// Gets or sets a list of Extensions to append to the Response.Cache object. - /// - public List CacheExtensions { get; set; } = new List(); - - /// - /// Gets or sets a dictionary of Headers to append to the Response object. - /// - public Dictionary Headers { get; set; } = new Dictionary(); - - #endregion + UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); + _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); + _webRoutingSettings = webRoutingSettings.Value; + Uri = uri ?? umbracoContext.CleanedUmbracoUrl; } + + /// + /// Gets the UmbracoContext. + /// + public IUmbracoContext UmbracoContext { get; } + + /// + /// Gets or sets the cleaned up Uri used for routing. + /// + /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + public Uri Uri { get; } + + public bool CacheabilityNoCache { get; set; } + + ///// + ///// Prepares the request. + ///// + // public void Prepare() + // { + // _publishedRouter.PrepareRequest(this); + // } + + /// + /// Gets or sets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. + /// + public bool IgnorePublishedContentCollisions { get; set; } + + /// + /// Gets or sets the template model to use to display the requested content. + /// + public ITemplate? Template { get; } + + /// + /// Gets the alias of the template to use to display the requested content. + /// + public string? TemplateAlias => Template?.Alias; + + /// + /// Gets or sets the content request's domain. + /// + /// + /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, + /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg + /// "http://example.com/". + /// + public DomainAndUri? Domain + { + get => _domain; + set + { + EnsureWriteable(); + _domain = value; + } + } + + /// + /// Gets a value indicating whether the content request has a domain. + /// + public bool HasDomain => Domain != null; + + /// + /// Gets or sets the content request's culture. + /// + public CultureInfo Culture + { + get => _culture ?? Thread.CurrentThread.CurrentCulture; + set + { + EnsureWriteable(); + _culture = value; + } + } + + // utility for ensuring it is ok to set some properties + public void EnsureWriteable() + { + if (_readonly) + { + throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only."); + } + } + + // #region Events + + ///// + ///// Triggers before the published content request is prepared. + ///// + ///// When the event triggers, no preparation has been done. It is still possible to + ///// modify the request's Uri property, for example to restore its original, public-facing value + ///// that might have been modified by an in-between equipment such as a load-balancer. + // public static event EventHandler Preparing; + + ///// + ///// Triggers once the published content request has been prepared, but before it is processed. + ///// + ///// When the event triggers, preparation is done ie domain, culture, document, template, + ///// rendering engine, etc. have been setup. It is then possible to change anything, before + ///// the request is actually processed and rendered by Umbraco. + // public static event EventHandler Prepared; + + ///// + ///// Triggers the Preparing event. + ///// + // public void OnPreparing() + // { + // Preparing?.Invoke(this, EventArgs.Empty); + // } + + ///// + ///// Triggers the Prepared event. + ///// + // public void OnPrepared() + // { + // Prepared?.Invoke(this, EventArgs.Empty); + + // if (HasPublishedContent == false) + // Is404 = true; // safety + + // _readonly = true; + // } + + // #endregion + #region PublishedContent + + ///// + ///// Gets or sets the requested content. + ///// + ///// Setting the requested content clears Template. + // public IPublishedContent PublishedContent + // { + // get { return _publishedContent; } + // set + // { + // EnsureWriteable(); + // _publishedContent = value; + // IsInternalRedirectPublishedContent = false; + // TemplateModel = null; + // } + // } + + /// + /// Sets the requested content, following an internal redirect. + /// + /// The requested content. + /// + /// Depending on UmbracoSettings.InternalRedirectPreservesTemplate, will + /// preserve or reset the template, if any. + /// + public void SetInternalRedirectPublishedContent(IPublishedContent content) + { + // if (content == null) + // throw new ArgumentNullException(nameof(content)); + // EnsureWriteable(); + + //// unless a template has been set already by the finder, + //// template should be null at that point. + + //// IsInternalRedirect if IsInitial, or already IsInternalRedirect + // var isInternalRedirect = IsInitialPublishedContent || IsInternalRedirectPublishedContent; + + //// redirecting to self + // if (content.Id == PublishedContent.Id) // neither can be null + // { + // // no need to set PublishedContent, we're done + // IsInternalRedirectPublishedContent = isInternalRedirect; + // return; + // } + + //// else + + //// save + // var template = Template; + + //// set published content - this resets the template, and sets IsInternalRedirect to false + // PublishedContent = content; + // IsInternalRedirectPublishedContent = isInternalRedirect; + + //// must restore the template if it's an internal redirect & the config option is set + // if (isInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) + // { + // // restore + // TemplateModel = template; + // } + } + + /// + /// Gets the initial requested content. + /// + /// + /// The initial requested content is the content that was found by the finders, + /// before anything such as 404, redirect... took place. + /// + public IPublishedContent? InitialPublishedContent { get; private set; } + + /// + /// Gets value indicating whether the current published content is the initial one. + /// + public bool IsInitialPublishedContent => + InitialPublishedContent != null && InitialPublishedContent == _publishedContent; + + /// + /// Indicates that the current PublishedContent is the initial one. + /// + public void SetIsInitialPublishedContent() + { + EnsureWriteable(); + + // note: it can very well be null if the initial content was not found + InitialPublishedContent = _publishedContent; + IsInternalRedirectPublishedContent = false; + } + + /// + /// Gets or sets a value indicating whether the current published content has been obtained + /// from the initial published content following internal redirections exclusively. + /// + /// + /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to + /// apply the internal redirect or not, when content is not the initial content. + /// + public bool IsInternalRedirectPublishedContent { get; private set; } + + #endregion + + // note: do we want to have an ordered list of alternate cultures, + // to allow for fallbacks when doing dictionary lookup and such? + #region Status + + /// + /// Gets or sets a value indicating whether the requested content could not be found. + /// + /// + /// This is set in the PublishedContentRequestBuilder and can also be used in + /// custom content finders or Prepared event handlers, where we want to allow developers + /// to indicate a request is 404 but not to cancel it. + /// + public bool Is404 + { + get => _is404; + set + { + EnsureWriteable(); + _is404 = value; + } + } + + /// + /// Gets a value indicating whether the content request triggers a redirect (permanent or not). + /// + public bool IsRedirect => string.IsNullOrWhiteSpace(RedirectUrl) == false; + + /// + /// Gets or sets a value indicating whether the redirect is permanent. + /// + public bool IsRedirectPermanent { get; private set; } + + /// + /// Gets or sets the URL to redirect to, when the content request triggers a redirect. + /// + public string? RedirectUrl { get; private set; } + + /// + /// Indicates that the content request should trigger a redirect (302). + /// + /// The URL to redirect to. + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + public void SetRedirect(string url) + { + EnsureWriteable(); + RedirectUrl = url; + IsRedirectPermanent = false; + } + + /// + /// Indicates that the content request should trigger a permanent redirect (301). + /// + /// The URL to redirect to. + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + public void SetRedirectPermanent(string url) + { + EnsureWriteable(); + RedirectUrl = url; + IsRedirectPermanent = true; + } + + /// + /// Indicates that the content request should trigger a redirect, with a specified status code. + /// + /// The URL to redirect to. + /// The status code (300-308). + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + public void SetRedirect(string url, int status) + { + EnsureWriteable(); + + if (status < 300 || status > 308) + { + throw new ArgumentOutOfRangeException(nameof(status), "Valid redirection status codes 300-308."); + } + + RedirectUrl = url; + IsRedirectPermanent = status == 301 || status == 308; + + // default redirect statuses + if (status != 301 && status != 302) + { + ResponseStatusCode = status; + } + } + + /// + /// Gets or sets the content request http response status code. + /// + /// + /// Does not actually set the http response status code, only registers that the response + /// should use the specified code. The code will or will not be used, in due time. + /// + public int ResponseStatusCode { get; private set; } + + /// + /// Gets or sets the content request http response status description. + /// + /// + /// Does not actually set the http response status description, only registers that the response + /// should use the specified description. The description will or will not be used, in due time. + /// + public string? ResponseStatusDescription { get; private set; } + + /// + /// Sets the http response status code, along with an optional associated description. + /// + /// The http status code. + /// The description. + /// + /// Does not actually set the http response status code and description, only registers that + /// the response should use the specified code and description. The code and description will or will + /// not be used, in due time. + /// + public void SetResponseStatus(int code, string? description = null) + { + EnsureWriteable(); + + // .Status is deprecated + // .SubStatusCode is IIS 7+ internal, ignore + ResponseStatusCode = code; + ResponseStatusDescription = description; + } + + #endregion + + #region Response Cache + + // /// + // /// Gets or sets the System.Web.HttpCacheability + // /// + // Note: we used to set a default value here but that would then be the default + // for ALL requests, we shouldn't overwrite it though if people are using [OutputCache] for example + // see: https://our.umbraco.com/forum/using-umbraco-and-getting-started/79715-output-cache-in-umbraco-752 + // public HttpCacheability Cacheability { get; set; } + + /// + /// Gets or sets a list of Extensions to append to the Response.Cache object. + /// + public List CacheExtensions { get; set; } = new(); + + /// + /// Gets or sets a dictionary of Headers to append to the Response object. + /// + public Dictionary Headers { get; set; } = new(); + + #endregion } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 119f9980b4..63df65c6e0 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -1,8 +1,4 @@ -using System; using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -15,437 +11,462 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides the default implementation. +/// +public class PublishedRouter : IPublishedRouter { + private readonly ContentFinderCollection _contentFinders; + private readonly IContentLastChanceFinder _contentLastChanceFinder; + private readonly IContentTypeService _contentTypeService; + private readonly IEventAggregator _eventAggregator; + private readonly IFileService _fileService; + private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IPublishedValueFallback _publishedValueFallback; + private readonly IRequestAccessor _requestAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; + private WebRoutingSettings _webRoutingSettings; /// - /// Provides the default implementation. + /// Initializes a new instance of the class. /// - public class PublishedRouter : IPublishedRouter + public PublishedRouter( + IOptionsMonitor webRoutingSettings, + ContentFinderCollection contentFinders, + IContentLastChanceFinder contentLastChanceFinder, + IVariationContextAccessor variationContextAccessor, + IProfilingLogger proflog, + ILogger logger, + IPublishedUrlProvider publishedUrlProvider, + IRequestAccessor requestAccessor, + IPublishedValueFallback publishedValueFallback, + IFileService fileService, + IContentTypeService contentTypeService, + IUmbracoContextAccessor umbracoContextAccessor, + IEventAggregator eventAggregator) { - private WebRoutingSettings _webRoutingSettings; - private readonly ContentFinderCollection _contentFinders; - private readonly IContentLastChanceFinder _contentLastChanceFinder; - private readonly IProfilingLogger _profilingLogger; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly ILogger _logger; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IRequestAccessor _requestAccessor; - private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IFileService _fileService; - private readonly IContentTypeService _contentTypeService; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IEventAggregator _eventAggregator; + _webRoutingSettings = webRoutingSettings.CurrentValue ?? + throw new ArgumentNullException(nameof(webRoutingSettings)); + _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); + _contentLastChanceFinder = + contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); + _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); + _variationContextAccessor = variationContextAccessor ?? + throw new ArgumentNullException(nameof(variationContextAccessor)); + _logger = logger; + _publishedUrlProvider = publishedUrlProvider; + _requestAccessor = requestAccessor; + _publishedValueFallback = publishedValueFallback; + _fileService = fileService; + _contentTypeService = contentTypeService; + _umbracoContextAccessor = umbracoContextAccessor; + _eventAggregator = eventAggregator; + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } - /// - /// Initializes a new instance of the class. - /// - public PublishedRouter( - IOptionsMonitor webRoutingSettings, - ContentFinderCollection contentFinders, - IContentLastChanceFinder contentLastChanceFinder, - IVariationContextAccessor variationContextAccessor, - IProfilingLogger proflog, - ILogger logger, - IPublishedUrlProvider publishedUrlProvider, - IRequestAccessor requestAccessor, - IPublishedValueFallback publishedValueFallback, - IFileService fileService, - IContentTypeService contentTypeService, - IUmbracoContextAccessor umbracoContextAccessor, - IEventAggregator eventAggregator) + /// + public async Task CreateRequestAsync(Uri uri) + { + // trigger the Creating event - at that point the URL can be changed + // this is based on this old task here: https://issues.umbraco.org/issue/U4-7914 which was fulfiled by + // this PR https://github.com/umbraco/Umbraco-CMS/pull/1137 + // It's to do with proxies, quote: + + /* + "Thinking about another solution. + We already have an event, PublishedContentRequest.Prepared, which triggers once the request has been prepared and domain, content, template have been figured out -- but before it renders -- so ppl can change things before rendering. + Wondering whether we could have a event, PublishedContentRequest.Preparing, which would trigger before the request is prepared, and would let ppl change the value of the request's URI (which by default derives from the HttpContext request). + That way, if an in-between equipement changes the URI, you could replace it with the original, public-facing URI before we process the request, meaning you could register your HTTPS domain and it would work. And you would have to supply code for each equipment. Less magic in Core." + */ + + // but now we'll just have one event for creating so if people wish to change the URL here they can but nothing else + var creatingRequest = new CreatingRequestNotification(uri); + await _eventAggregator.PublishAsync(creatingRequest); + + var publishedRequestBuilder = new PublishedRequestBuilder(creatingRequest.Url, _fileService); + return publishedRequestBuilder; + } + + /// + public async Task RouteRequestAsync( + IPublishedRequestBuilder builder, + RouteRequestOptions options) + { + // outbound routing performs different/simpler logic + if (options.RouteDirection == RouteDirection.Outbound) { - _webRoutingSettings = webRoutingSettings.CurrentValue ?? throw new ArgumentNullException(nameof(webRoutingSettings)); - _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); - _contentLastChanceFinder = contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); - _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); - _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - _logger = logger; - _publishedUrlProvider = publishedUrlProvider; - _requestAccessor = requestAccessor; - _publishedValueFallback = publishedValueFallback; - _fileService = fileService; - _contentTypeService = contentTypeService; - _umbracoContextAccessor = umbracoContextAccessor; - _eventAggregator = eventAggregator; - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + return await TryRouteRequest(builder); } - /// - public async Task CreateRequestAsync(Uri uri) + // find domain + if (builder.Domain == null) { - // trigger the Creating event - at that point the URL can be changed - // this is based on this old task here: https://issues.umbraco.org/issue/U4-7914 which was fulfiled by - // this PR https://github.com/umbraco/Umbraco-CMS/pull/1137 - // It's to do with proxies, quote: - - /* - "Thinking about another solution. - We already have an event, PublishedContentRequest.Prepared, which triggers once the request has been prepared and domain, content, template have been figured out -- but before it renders -- so ppl can change things before rendering. - Wondering whether we could have a event, PublishedContentRequest.Preparing, which would trigger before the request is prepared, and would let ppl change the value of the request's URI (which by default derives from the HttpContext request). - That way, if an in-between equipement changes the URI, you could replace it with the original, public-facing URI before we process the request, meaning you could register your HTTPS domain and it would work. And you would have to supply code for each equipment. Less magic in Core." - */ - - // but now we'll just have one event for creating so if people wish to change the URL here they can but nothing else - var creatingRequest = new CreatingRequestNotification(uri); - await _eventAggregator.PublishAsync(creatingRequest); - - var publishedRequestBuilder = new PublishedRequestBuilder(creatingRequest.Url, _fileService); - return publishedRequestBuilder; + FindDomain(builder); } - private async Task TryRouteRequest(IPublishedRequestBuilder request) + await RouteRequestInternalAsync(builder); + + // complete the PCR and assign the remaining values + return BuildRequest(builder); + } + + /// + public async Task UpdateRequestAsync( + IPublishedRequest request, + IPublishedContent? publishedContent) + { + // store the original (if any) + IPublishedContent? content = request.PublishedContent; + + IPublishedRequestBuilder builder = new PublishedRequestBuilder(request.Uri, _fileService); + + // ensure we keep the previous domain and culture + if (request.Domain is not null) { - FindDomain(request); - - if (request.IsRedirect()) - { - return request.Build(); - } - - if (request.HasPublishedContent()) - { - return request.Build(); - } - - await FindPublishedContent(request); - - return request.Build(); + builder.SetDomain(request.Domain); } + builder.SetCulture(request.Culture); - private void SetVariationContext(string? culture) + // set to the new content (or null if specified) + builder.SetPublishedContent(publishedContent); + + // re-route + await RouteRequestInternalAsync(builder, true); + + // return if we are redirect + if (builder.IsRedirect()) { - VariationContext? variationContext = _variationContextAccessor.VariationContext; - if (variationContext != null && variationContext.Culture == culture) - { - return; - } - - _variationContextAccessor.VariationContext = new VariationContext(culture); - } - - /// - public async Task RouteRequestAsync(IPublishedRequestBuilder builder, RouteRequestOptions options) - { - // outbound routing performs different/simpler logic - if (options.RouteDirection == RouteDirection.Outbound) - { - return await TryRouteRequest(builder); - } - - // find domain - if (builder.Domain == null) - { - FindDomain(builder); - } - - await RouteRequestInternalAsync(builder); - - // complete the PCR and assign the remaining values return BuildRequest(builder); } - private async Task RouteRequestInternalAsync(IPublishedRequestBuilder builder) + // this will occur if publishedContent is null and the last chance finders also don't assign content + if (!builder.HasPublishedContent()) { - // if request builder was already flagged to redirect then return - // whoever called us is in charge of actually redirecting - if (builder.IsRedirect()) - { - return; - } - - // set the culture - SetVariationContext(builder.Culture); - - var foundContentByFinders = false; - - // Find the published content if it's not assigned. - // This could be manually assigned with a custom route handler, etc... - // which in turn could call this method - // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. - if (!builder.HasPublishedContent()) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", builder.Uri.AbsolutePath); - } - - // run the document finders - foundContentByFinders = await FindPublishedContent(builder); - } - - // if we are not a redirect - if (!builder.IsRedirect()) - { - // handle not-found, redirects, access... - await HandlePublishedContent(builder); - - // find a template - FindTemplate(builder, foundContentByFinders); - - // handle umbracoRedirect - FollowExternalRedirect(builder); - - // handle wildcard domains - HandleWildcardDomains(builder); - - // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain - SetVariationContext(builder.Culture); - } - - // trigger the routing request (used to be called Prepared) event - at that point it is still possible to change about anything - // even though the request might be flagged for redirection - we'll redirect _after_ the event - var routingRequest = new RoutingRequestNotification(builder); - await _eventAggregator.PublishAsync(routingRequest); - - // we don't take care of anything so if the content has changed, it's up to the user - // to find out the appropriate template + // means the engine could not find a proper document to handle 404 + // restore the saved content so we know it exists + builder.SetPublishedContent(content); } - /// - /// This method finalizes/builds the PCR with the values assigned. - /// - /// - /// Returns false if the request was not successfully configured - /// - /// - /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning their own values - /// but need to finalize it themselves. - /// - internal IPublishedRequest BuildRequest(IPublishedRequestBuilder builder) + return BuildRequest(builder); + } + + /// + /// This method finalizes/builds the PCR with the values assigned. + /// + /// + /// Returns false if the request was not successfully configured + /// + /// + /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning + /// their own values + /// but need to finalize it themselves. + /// + internal IPublishedRequest BuildRequest(IPublishedRequestBuilder builder) + { + IPublishedRequest result = builder.Build(); + + if (!builder.HasPublishedContent()) { - IPublishedRequest result = builder.Build(); - - if (!builder.HasPublishedContent()) - { - return result; - } - - // set the culture -- again, 'cos it might have changed in the event handler - SetVariationContext(result.Culture); - return result; } - /// - public async Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent) + // set the culture -- again, 'cos it might have changed in the event handler + SetVariationContext(result.Culture); + + return result; + } + + private async Task TryRouteRequest(IPublishedRequestBuilder request) + { + FindDomain(request); + + if (request.IsRedirect()) { - // store the original (if any) - IPublishedContent? content = request.PublishedContent; - - IPublishedRequestBuilder builder = new PublishedRequestBuilder(request.Uri, _fileService); - - // set to the new content (or null if specified) - builder.SetPublishedContent(publishedContent); - - // re-route - await RouteRequestInternalAsync(builder); - - // return if we are redirect - if (builder.IsRedirect()) - { - return BuildRequest(builder); - } - - // this will occur if publishedContent is null and the last chance finders also don't assign content - if (!builder.HasPublishedContent()) - { - // means the engine could not find a proper document to handle 404 - // restore the saved content so we know it exists - builder.SetPublishedContent(content); - } - - if (!builder.HasDomain()) - { - FindDomain(builder); - } - - return BuildRequest(builder); + return request.Build(); } - /// - /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. - /// - /// A value indicating whether a domain was found. - internal bool FindDomain(IPublishedRequestBuilder request) + if (request.HasPublishedContent()) { - const string tracePrefix = "FindDomain: "; - - // note - we are not handling schemes nor ports here. - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); - } - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IDomainCache? domainsCache = umbracoContext.PublishedSnapshot.Domains; - var domains = domainsCache?.GetAll(includeWildcards: false).ToList(); - - // determines whether a domain corresponds to a published document, since some - // domains may exist but on a document that has been unpublished - as a whole - or - // that is not published for the domain's culture - in which case the domain does - // not apply - bool IsPublishedContentDomain(Domain domain) - { - // just get it from content cache - optimize there, not here - IPublishedContent? domainDocument = umbracoContext.PublishedSnapshot.Content?.GetById(domain.ContentId); - - // not published - at all - if (domainDocument == null) - { - return false; - } - - // invariant - always published - if (!domainDocument.ContentType.VariesByCulture()) - { - return true; - } - - // variant, ensure that the culture corresponding to the domain's language is published - return domain.Culture is not null && domainDocument.Cultures.ContainsKey(domain.Culture); - } - - domains = domains?.Where(IsPublishedContentDomain).ToList(); - - var defaultCulture = domainsCache?.DefaultCulture; - - // try to find a domain matching the current request - DomainAndUri? domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); - - // handle domain - always has a contentId and a culture - if (domainAndUri != null) - { - // matching an existing domain - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Matches domain={Domain}, rootId={RootContentId}, culture={Culture}", tracePrefix, domainAndUri.Name, domainAndUri.ContentId, domainAndUri.Culture); - } - request.SetDomain(domainAndUri); - - // canonical? not implemented at the moment - // if (...) - // { - // _pcr.RedirectUrl = "..."; - // return true; - // } - } - else - { - // not matching any existing domain - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Matches no domain", tracePrefix); - } - - request.SetCulture(defaultCulture ?? CultureInfo.CurrentUICulture.Name); - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture); - } - - return request.Domain != null; + return request.Build(); } - /// - /// Looks for wildcard domains in the path and updates Culture accordingly. - /// - internal void HandleWildcardDomains(IPublishedRequestBuilder request) + await FindPublishedContent(request); + + return request.Build(); + } + + private void SetVariationContext(string? culture) + { + VariationContext? variationContext = _variationContextAccessor.VariationContext; + if (variationContext != null && variationContext.Culture == culture) { - const string tracePrefix = "HandleWildcardDomains: "; - - if (request.PublishedContent == null) - { - return; - } - - var nodePath = request.PublishedContent.Path; - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); - } - var rootNodeId = request.Domain != null ? request.Domain.ContentId : (int?)null; - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - Domain? domain = DomainUtilities.FindWildcardDomainInPath(umbracoContext.PublishedSnapshot.Domains?.GetAll(true), nodePath, rootNodeId); - - // always has a contentId and a culture - if (domain != null) - { - request.SetCulture(domain.Culture); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", tracePrefix, domain.ContentId, request.Culture); - } - } - else - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}No match.", tracePrefix); - } - } + return; } - internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo directory, string alias, string[] extensions) + _variationContextAccessor.VariationContext = new VariationContext(culture); + } + + private async Task RouteRequestInternalAsync(IPublishedRequestBuilder builder, bool skipContentFinders = false) + { + // if request builder was already flagged to redirect then return + // whoever called us is in charge of actually redirecting + if (builder.IsRedirect()) { - if (directory == null || directory.Exists == false) + return; + } + + // set the culture + SetVariationContext(builder.Culture); + + var foundContentByFinders = false; + + // Find the published content if it's not assigned. + // This could be manually assigned with a custom route handler, etc... + // which in turn could call this method + // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. + if (!builder.HasPublishedContent() && !skipContentFinders) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", builder.Uri.AbsolutePath); + } + + // run the document finders + foundContentByFinders = await FindPublishedContent(builder); + } + + // if we are not a redirect + if (!builder.IsRedirect()) + { + // handle not-found, redirects, access... + await HandlePublishedContent(builder); + + // find a template + FindTemplate(builder, foundContentByFinders); + + // handle umbracoRedirect + FollowExternalRedirect(builder); + + // handle wildcard domains + HandleWildcardDomains(builder); + + // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain + SetVariationContext(builder.Culture); + } + + // trigger the routing request (used to be called Prepared) event - at that point it is still possible to change about anything + // even though the request might be flagged for redirection - we'll redirect _after_ the event + var routingRequest = new RoutingRequestNotification(builder); + await _eventAggregator.PublishAsync(routingRequest); + + // we don't take care of anything so if the content has changed, it's up to the user + // to find out the appropriate template + } + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. + /// + /// A value indicating whether a domain was found. + internal bool FindDomain(IPublishedRequestBuilder request) + { + const string tracePrefix = "FindDomain: "; + + // note - we are not handling schemes nor ports here. + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); + } + + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IDomainCache? domainsCache = umbracoContext.PublishedSnapshot.Domains; + var domains = domainsCache?.GetAll(false).ToList(); + + // determines whether a domain corresponds to a published document, since some + // domains may exist but on a document that has been unpublished - as a whole - or + // that is not published for the domain's culture - in which case the domain does + // not apply + bool IsPublishedContentDomain(Domain domain) + { + // just get it from content cache - optimize there, not here + IPublishedContent? domainDocument = umbracoContext.PublishedSnapshot.Content?.GetById(domain.ContentId); + + // not published - at all + if (domainDocument == null) { return false; } - var pos = alias.IndexOf('/'); - if (pos > 0) + // invariant - always published + if (!domainDocument.ContentType.VariesByCulture()) { - // recurse - DirectoryInfo? subdir = directory.GetDirectories(alias.Substring(0, pos)).FirstOrDefault(); - alias = alias.Substring(pos + 1); - return subdir != null && FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); + return true; } - // look here - return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); + // variant, ensure that the culture corresponding to the domain's language is published + return domain.Culture is not null && domainDocument.Cultures.ContainsKey(domain.Culture); } - /// - /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. - /// - /// There is no finder collection. - internal async Task FindPublishedContent(IPublishedRequestBuilder request) - { - const string tracePrefix = "FindPublishedContent: "; + domains = domains?.Where(IsPublishedContentDomain).ToList(); - // look for the document - // the first successful finder, if any, will set this.PublishedContent, and may also set this.Template - // some finders may implement caching - DisposableTimer? profilingScope = null; - try + var defaultCulture = domainsCache?.DefaultCulture; + + // try to find a domain matching the current request + DomainAndUri? domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + + // handle domain - always has a contentId and a culture + if (domainAndUri != null) + { + // matching an existing domain + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - profilingScope = _profilingLogger.DebugDuration( + _logger.LogDebug( + "{TracePrefix}Matches domain={Domain}, rootId={RootContentId}, culture={Culture}", + tracePrefix, + domainAndUri.Name, + domainAndUri.ContentId, + domainAndUri.Culture); + } + + request.SetDomain(domainAndUri); + + // canonical? not implemented at the moment + // if (...) + // { + // _pcr.RedirectUrl = "..."; + // return true; + // } + } + else + { + // not matching any existing domain + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Matches no domain", tracePrefix); + } + + request.SetCulture(defaultCulture ?? CultureInfo.CurrentUICulture.Name); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture); + } + + return request.Domain != null; + } + + /// + /// Looks for wildcard domains in the path and updates Culture accordingly. + /// + internal void HandleWildcardDomains(IPublishedRequestBuilder request) + { + const string tracePrefix = "HandleWildcardDomains: "; + + if (request.PublishedContent == null) + { + return; + } + + var nodePath = request.PublishedContent.Path; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); + } + + var rootNodeId = request.Domain != null ? request.Domain.ContentId : (int?)null; + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + Domain? domain = + DomainUtilities.FindWildcardDomainInPath(umbracoContext.PublishedSnapshot.Domains?.GetAll(true), nodePath, rootNodeId); + + // always has a contentId and a culture + if (domain != null) + { + request.SetCulture(domain.Culture); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", + tracePrefix, + domain.ContentId, + request.Culture); + } + } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}No match.", tracePrefix); + } + } + } + + internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo? directory, string alias, string[] extensions) + { + if (directory == null || directory.Exists == false) + { + return false; + } + + var pos = alias.IndexOf('/'); + if (pos > 0) + { + // recurse + DirectoryInfo? subdir = directory.GetDirectories(alias.Substring(0, pos)).FirstOrDefault(); + alias = alias.Substring(pos + 1); + return subdir != null && FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); + } + + // look here + return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); + } + + /// + /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. + /// + /// There is no finder collection. + internal async Task FindPublishedContent(IPublishedRequestBuilder request) + { + const string tracePrefix = "FindPublishedContent: "; + + // look for the document + // the first successful finder, if any, will set this.PublishedContent, and may also set this.Template + // some finders may implement caching + DisposableTimer? profilingScope = null; + try + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + profilingScope = _profilingLogger.DebugDuration( $"{tracePrefix}Begin finders", $"{tracePrefix}End finders"); + } + + // iterate but return on first one that finds it + var found = false; + foreach (IContentFinder contentFinder in _contentFinders) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Finder {ContentFinderType}", contentFinder.GetType().FullName); } - // iterate but return on first one that finds it - var found = false; - foreach (var contentFinder in _contentFinders) + found = await contentFinder.TryFindContent(request); + if (found) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Finder {ContentFinderType}", contentFinder.GetType().FullName); - } - found = await contentFinder.TryFindContent(request); - if (found) - { - break; - } + break; } + } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( "Found? {Found}, Content: {PublishedContentId}, Template: {TemplateAlias}, Domain: {Domain}, Culture: {Culture}, StatusCode: {StatusCode}", found, request.HasPublishedContent() ? request.PublishedContent?.Id : "NULL", @@ -453,393 +474,431 @@ namespace Umbraco.Cms.Core.Routing request.HasDomain() ? request.Domain?.ToString() : "NULL", request.Culture ?? "NULL", request.ResponseStatusCode); - } + } - return found; - } - finally - { - profilingScope?.Dispose(); - } + return found; } - - /// - /// Handles the published content (if any). - /// - /// The request builder. - /// - /// Handles "not found", internal redirects ... - /// things that must be handled in one place because they can create loops - /// - private async Task HandlePublishedContent(IPublishedRequestBuilder request) + finally { - // because these might loop, we have to have some sort of infinite loop detection - int i = 0, j = 0; - const int maxLoop = 8; - do + profilingScope?.Dispose(); + } + } + + /// + /// Handles the published content (if any). + /// + /// The request builder. + /// + /// Handles "not found", internal redirects ... + /// things that must be handled in one place because they can create loops + /// + private async Task HandlePublishedContent(IPublishedRequestBuilder request) + { + // because these might loop, we have to have some sort of infinite loop detection + int i = 0, j = 0; + const int maxLoop = 8; + do + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + _logger.LogDebug("HandlePublishedContent: Loop {LoopCounter}", i); + } + + // handle not found + if (request.PublishedContent == null) + { + request.SetIs404(); + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("HandlePublishedContent: Loop {LoopCounter}", i); + _logger.LogDebug("HandlePublishedContent: No document, try last chance lookup"); } - // handle not found - if (request.PublishedContent == null) + // if it fails then give up, there isn't much more that we can do + if (await _contentLastChanceFinder.TryFindContent(request) == false) { - request.SetIs404(); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("HandlePublishedContent: No document, try last chance lookup"); + _logger.LogDebug("HandlePublishedContent: Failed to find a document, give up"); } - // if it fails then give up, there isn't much more that we can do - if (await _contentLastChanceFinder.TryFindContent(request) == false) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("HandlePublishedContent: Failed to find a document, give up"); - } - break; - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("HandlePublishedContent: Found a document"); - } - } - - // follow internal redirects as long as it's not running out of control ie infinite loop of some sort - j = 0; - while (FollowInternalRedirects(request) && j++ < maxLoop) - { } - - // we're running out of control - if (j == maxLoop) - { break; } - // loop while we don't have page, ie the redirect or access - // got us to nowhere and now we need to run the notFoundLookup again - // as long as it's not running out of control ie infinite loop of some sort - } while (request.PublishedContent == null && i++ < maxLoop); - - if (i == maxLoop || j == maxLoop) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); + _logger.LogDebug("HandlePublishedContent: Found a document"); } - request.SetPublishedContent(null); } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + + // follow internal redirects as long as it's not running out of control ie infinite loop of some sort + j = 0; + while (FollowInternalRedirects(request) && j++ < maxLoop) { - _logger.LogDebug("HandlePublishedContent: End"); + } + + // we're running out of control + if (j == maxLoop) + { + break; + } + + // loop while we don't have page, ie the redirect or access + // got us to nowhere and now we need to run the notFoundLookup again + // as long as it's not running out of control ie infinite loop of some sort + } + while (request.PublishedContent == null && i++ < maxLoop); + + if (i == maxLoop || j == maxLoop) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); + } + + request.SetPublishedContent(null); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("HandlePublishedContent: End"); + } + } + + /// + /// Follows internal redirections through the umbracoInternalRedirectId document property. + /// + /// The request builder. + /// A value indicating whether redirection took place and led to a new published document. + /// + /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. + /// As per legacy, if the redirect does not work, we just ignore it. + /// + private bool FollowInternalRedirects(IPublishedRequestBuilder request) + { + if (request.PublishedContent == null) + { + throw new InvalidOperationException("There is no PublishedContent."); + } + + // don't try to find a redirect if the property doesn't exist + if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) + { + return false; + } + + var redirect = false; + var valid = false; + IPublishedContent? internalRedirectNode = null; + var internalRedirectId = request.PublishedContent.Value( + _publishedValueFallback, + Constants.Conventions.Content.InternalRedirectId, + defaultValue: -1); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + if (internalRedirectId > 0) + { + // try and get the redirect node from a legacy integer ID + valid = true; + internalRedirectNode = umbracoContext.Content?.GetById(internalRedirectId); + } + else + { + GuidUdi? udiInternalRedirectId = request.PublishedContent.Value( + _publishedValueFallback, + Constants.Conventions.Content.InternalRedirectId); + if (udiInternalRedirectId is not null) + { + // try and get the redirect node from a UDI Guid + valid = true; + internalRedirectNode = umbracoContext.Content?.GetById(udiInternalRedirectId.Guid); } } - /// - /// Follows internal redirections through the umbracoInternalRedirectId document property. - /// - /// The request builder. - /// A value indicating whether redirection took place and led to a new published document. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - /// As per legacy, if the redirect does not work, we just ignore it. - /// - private bool FollowInternalRedirects(IPublishedRequestBuilder request) + if (valid == false) { - if (request.PublishedContent == null) + // bad redirect - log and display the current page (legacy behavior) + if (_logger.IsEnabled(LogLevel.Debug)) { - throw new InvalidOperationException("There is no PublishedContent."); - } - - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) - { - return false; - } - - var redirect = false; - var valid = false; - IPublishedContent? internalRedirectNode = null; - var internalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - - if (internalRedirectId > 0) - { - // try and get the redirect node from a legacy integer ID - valid = true; - internalRedirectNode = umbracoContext.Content?.GetById(internalRedirectId); - } - else - { - GuidUdi? udiInternalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.InternalRedirectId); - if (udiInternalRedirectId is not null) - { - // try and get the redirect node from a UDI Guid - valid = true; - internalRedirectNode = umbracoContext.Content?.GetById(udiInternalRedirectId.Guid); - } - } - - if (valid == false) - { - // bad redirect - log and display the current page (legacy behavior) - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( + _logger.LogDebug( "FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: value is not an int nor a GuidUdi.", request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId)?.GetSourceValue()); - } } - - if (internalRedirectNode == null) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( - "FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", - request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId)?.GetSourceValue()); - } - } - else if (internalRedirectId == request.PublishedContent.Id) - { - // redirect to self - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FollowInternalRedirects: Redirecting to self, ignore"); - } - } - else - { - // save since it will be cleared - ITemplate? template = request.Template; - - request.SetInternalRedirect(internalRedirectNode); // don't use .PublishedContent here - - // must restore the template if it's an internal redirect & the config option is set - if (request.IsInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) - { - // restore - request.SetTemplate(template); - } - - redirect = true; - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); - } - } - - return redirect; } - /// - /// Finds a template for the current node, if any. - /// - /// The request builder. - /// If the content was found by the finders, before anything such as 404, redirect... took place. - private void FindTemplate(IPublishedRequestBuilder request, bool contentFoundByFinders) + if (internalRedirectNode == null) { - // TODO: We've removed the event, might need to re-add? - // NOTE: at the moment there is only 1 way to find a template, and then ppl must - // use the Prepared event to change the template if they wish. Should we also - // implement an ITemplateFinder logic? - if (request.PublishedContent == null) + if (_logger.IsEnabled(LogLevel.Debug)) { - request.SetTemplate(null); + _logger.LogDebug( + "FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", + request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId)?.GetSourceValue()); + } + } + else if (internalRedirectId == request.PublishedContent.Id) + { + // redirect to self + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FollowInternalRedirects: Redirecting to self, ignore"); + } + } + else + { + // save since it will be cleared + ITemplate? template = request.Template; + + request.SetInternalRedirect(internalRedirectNode); // don't use .PublishedContent here + + // must restore the template if it's an internal redirect & the config option is set + if (request.IsInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) + { + // restore + request.SetTemplate(template); + } + + redirect = true; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); + } + } + + return redirect; + } + + /// + /// Finds a template for the current node, if any. + /// + /// The request builder. + /// + /// If the content was found by the finders, before anything such as 404, redirect... + /// took place. + /// + private void FindTemplate(IPublishedRequestBuilder request, bool contentFoundByFinders) + { + // TODO: We've removed the event, might need to re-add? + // NOTE: at the moment there is only 1 way to find a template, and then ppl must + // use the Prepared event to change the template if they wish. Should we also + // implement an ITemplateFinder logic? + if (request.PublishedContent == null) + { + request.SetTemplate(null); + return; + } + + // read the alternate template alias, from querystring, form, cookie or server vars, + // only if the published content is the initial once, else the alternate template + // does not apply + // + optionally, apply the alternate template on internal redirects + var useAltTemplate = contentFoundByFinders + || (_webRoutingSettings.InternalRedirectPreservesTemplate && request.IsInternalRedirect); + + var altTemplate = useAltTemplate + ? _requestAccessor.GetRequestValue(Constants.Conventions.Url.AltTemplate) + : null; + + if (string.IsNullOrWhiteSpace(altTemplate)) + { + // we don't have an alternate template specified. use the current one if there's one already, + // which can happen if a content lookup also set the template (LookupByNiceUrlAndTemplate...), + // else lookup the template id on the document then lookup the template with that id. + if (request.HasTemplate()) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FindTemplate: Has a template already, and no alternate template."); + } + return; } - // read the alternate template alias, from querystring, form, cookie or server vars, - // only if the published content is the initial once, else the alternate template - // does not apply - // + optionally, apply the alternate template on internal redirects - var useAltTemplate = contentFoundByFinders - || (_webRoutingSettings.InternalRedirectPreservesTemplate && request.IsInternalRedirect); - - var altTemplate = useAltTemplate - ? _requestAccessor.GetRequestValue(Constants.Conventions.Url.AltTemplate) - : null; - - if (string.IsNullOrWhiteSpace(altTemplate)) + // TODO: We need to limit altTemplate to only allow templates that are assigned to the current document type! + // if the template isn't assigned to the document type we should log a warning and return 404 + var templateId = request.PublishedContent.TemplateId; + ITemplate? template = GetTemplate(templateId); + request.SetTemplate(template); + if (template != null) { - // we don't have an alternate template specified. use the current one if there's one already, - // which can happen if a content lookup also set the template (LookupByNiceUrlAndTemplate...), - // else lookup the template id on the document then lookup the template with that id. - if (request.HasTemplate()) + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Has a template already, and no alternate template."); - } - return; - } - - // TODO: We need to limit altTemplate to only allow templates that are assigned to the current document type! - // if the template isn't assigned to the document type we should log a warning and return 404 - var templateId = request.PublishedContent.TemplateId; - ITemplate? template = GetTemplate(templateId); - request.SetTemplate(template); - if (template != null) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - } - } - else - { - _logger.LogWarning("FindTemplate: Could not find template with id {TemplateId}", templateId); + _logger.LogDebug( + "FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", + template.Id, + template.Alias); } } else { - // we have an alternate template specified. lookup the template with that alias - // this means the we override any template that a content lookup might have set - // so /path/to/page/template1?altTemplate=template2 will use template2 + _logger.LogWarning("FindTemplate: Could not find template with id {TemplateId}", templateId); + } + } + else + { + // we have an alternate template specified. lookup the template with that alias + // this means the we override any template that a content lookup might have set + // so /path/to/page/template1?altTemplate=template2 will use template2 - // ignore if the alias does not match - just trace - if (request.HasTemplate()) + // ignore if the alias does not match - just trace + if (request.HasTemplate()) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Has a template already, but also an alternative template."); - } - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); + _logger.LogDebug("FindTemplate: Has a template already, but also an alternative template."); } + } - // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings - if (request.PublishedContent.IsAllowedTemplate( + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); + } + + // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings + if (request.PublishedContent.IsAllowedTemplate( _fileService, _contentTypeService, _webRoutingSettings.DisableAlternativeTemplates, _webRoutingSettings.ValidateAlternativeTemplates, altTemplate)) - { - // allowed, use - ITemplate? template = _fileService.GetTemplate(altTemplate); + { + // allowed, use + ITemplate? template = _fileService.GetTemplate(altTemplate); - if (template != null) + if (template != null) + { + request.SetTemplate(template); + if (_logger.IsEnabled(LogLevel.Debug)) { - request.SetTemplate(template); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - } - } - else - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", altTemplate); - } + _logger.LogDebug( + "FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", + template.Id, + template.Alias); } } else { - _logger.LogWarning("FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", altTemplate, request.PublishedContent.Id); - // no allowed, back to default - var templateId = request.PublishedContent.TemplateId; - ITemplate? template = GetTemplate(templateId); - request.SetTemplate(template); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", template?.Id, template?.Alias); + _logger.LogDebug( + "FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", + altTemplate); } } } - - if (!request.HasTemplate()) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: No template was found."); - } - - // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true - // then reset _pcr.Document to null to force a 404. - // - // but: because we want to let MVC hijack routes even though no template is defined, we decide that - // a missing template is OK but the request will then be forwarded to MVC, which will need to take - // care of everything. - // - // so, don't set _pcr.Document to null here - } - } - - private ITemplate? GetTemplate(int? templateId) - { - if (templateId.HasValue == false || templateId.Value == default) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("GetTemplateModel: No template."); - } - return null; - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("GetTemplateModel: Get template id={TemplateId}", templateId); - } - - if (templateId == null) - { - throw new InvalidOperationException("The template is not set, the page cannot render."); - } - - ITemplate? template = _fileService.GetTemplate(templateId.Value); - if (template == null) - { - throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render."); - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - } - return template; - } - - /// - /// Follows external redirection through umbracoRedirect document property. - /// - /// As per legacy, if the redirect does not work, we just ignore it. - private void FollowExternalRedirect(IPublishedRequestBuilder request) - { - if (request.PublishedContent == null) - { - return; - } - - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Constants.Conventions.Content.Redirect) == false) - { - return; - } - - var redirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.Redirect, defaultValue: -1); - var redirectUrl = "#"; - if (redirectId > 0) - { - redirectUrl = _publishedUrlProvider.GetUrl(redirectId); - } else { - // might be a UDI instead of an int Id - GuidUdi? redirectUdi = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.Redirect); - if (redirectUdi is not null) + _logger.LogWarning( + "FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", + altTemplate, + request.PublishedContent.Id); + + // no allowed, back to default + var templateId = request.PublishedContent.TemplateId; + ITemplate? template = GetTemplate(templateId); + request.SetTemplate(template); + if (_logger.IsEnabled(LogLevel.Debug)) { - redirectUrl = _publishedUrlProvider.GetUrl(redirectUdi.Guid); + _logger.LogDebug( + "FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", + template?.Id, + template?.Alias); } } + } - if (redirectUrl != "#") + if (!request.HasTemplate()) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - request.SetRedirect(redirectUrl); + _logger.LogDebug("FindTemplate: No template was found."); } + + // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true + // then reset _pcr.Document to null to force a 404. + // + // but: because we want to let MVC hijack routes even though no template is defined, we decide that + // a missing template is OK but the request will then be forwarded to MVC, which will need to take + // care of everything. + // + // so, don't set _pcr.Document to null here + } + } + + private ITemplate? GetTemplate(int? templateId) + { + if (templateId.HasValue == false || templateId.Value == default) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("GetTemplateModel: No template."); + } + + return null; + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("GetTemplateModel: Get template id={TemplateId}", templateId); + } + + if (templateId == null) + { + throw new InvalidOperationException("The template is not set, the page cannot render."); + } + + ITemplate? template = _fileService.GetTemplate(templateId.Value); + if (template == null) + { + throw new InvalidOperationException("The template with Id " + templateId + + " does not exist, the page cannot render."); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); + } + + return template; + } + + /// + /// Follows external redirection through umbracoRedirect document property. + /// + /// As per legacy, if the redirect does not work, we just ignore it. + private void FollowExternalRedirect(IPublishedRequestBuilder request) + { + if (request.PublishedContent == null) + { + return; + } + + // don't try to find a redirect if the property doesn't exist + if (request.PublishedContent.HasProperty(Constants.Conventions.Content.Redirect) == false) + { + return; + } + + var redirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.Redirect, defaultValue: -1); + var redirectUrl = "#"; + if (redirectId > 0) + { + redirectUrl = _publishedUrlProvider.GetUrl(redirectId); + } + else + { + // might be a UDI instead of an int Id + GuidUdi? redirectUdi = + request.PublishedContent.Value( + _publishedValueFallback, + Constants.Conventions.Content.Redirect); + if (redirectUdi is not null) + { + redirectUrl = _publishedUrlProvider.GetUrl(redirectUdi.Guid); + } + } + + if (redirectUrl != "#") + { + request.SetRedirect(redirectUrl); } } } diff --git a/src/Umbraco.Core/Routing/RouteDirection.cs b/src/Umbraco.Core/Routing/RouteDirection.cs index 33dad7b081..7ba637c288 100644 --- a/src/Umbraco.Core/Routing/RouteDirection.cs +++ b/src/Umbraco.Core/Routing/RouteDirection.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// The direction of a route +/// +public enum RouteDirection { /// - /// The direction of a route + /// An inbound route used to map a URL to a content item /// - public enum RouteDirection - { - /// - /// An inbound route used to map a URL to a content item - /// - Inbound = 1, + Inbound = 1, - /// - /// An outbound route used to generate a URL for a content item - /// - Outbound = 2 - } + /// + /// An outbound route used to generate a URL for a content item + /// + Outbound = 2, } diff --git a/src/Umbraco.Core/Routing/RouteRequestOptions.cs b/src/Umbraco.Core/Routing/RouteRequestOptions.cs index 97792ebad3..960bf4bd36 100644 --- a/src/Umbraco.Core/Routing/RouteRequestOptions.cs +++ b/src/Umbraco.Core/Routing/RouteRequestOptions.cs @@ -1,29 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Routing +/// +/// Options for routing an Umbraco request +/// +public struct RouteRequestOptions : IEquatable { /// - /// Options for routing an Umbraco request + /// Initializes a new instance of the struct. /// - public struct RouteRequestOptions : IEquatable - { - /// - /// Initializes a new instance of the struct. - /// - public RouteRequestOptions(RouteDirection direction) => RouteDirection = direction; + public RouteRequestOptions(RouteDirection direction) => RouteDirection = direction; - /// - /// Gets the - /// - public RouteDirection RouteDirection { get; } + /// + /// Gets the + /// + public RouteDirection RouteDirection { get; } - /// - public override bool Equals(object? obj) => obj is RouteRequestOptions options && Equals(options); + /// + public override bool Equals(object? obj) => obj is RouteRequestOptions options && Equals(options); - /// - public bool Equals(RouteRequestOptions other) => RouteDirection == other.RouteDirection; + /// + public bool Equals(RouteRequestOptions other) => RouteDirection == other.RouteDirection; - /// - public override int GetHashCode() => 15391035 + RouteDirection.GetHashCode(); - } + /// + public override int GetHashCode() => 15391035 + RouteDirection.GetHashCode(); } diff --git a/src/Umbraco.Core/Routing/SiteDomainMapper.cs b/src/Umbraco.Core/Routing/SiteDomainMapper.cs index a74d4532e1..b8ae10c3aa 100644 --- a/src/Umbraco.Core/Routing/SiteDomainMapper.cs +++ b/src/Umbraco.Core/Routing/SiteDomainMapper.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; -using System.Threading; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Routing @@ -235,8 +231,7 @@ namespace Umbraco.Cms.Core.Routing #region Map domains /// - public virtual DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, - string? culture, string? defaultCulture) + public virtual DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, string? culture, string? defaultCulture) { var currentAuthority = current.GetLeftPart(UriPartial.Authority); Dictionary? qualifiedSites = GetQualifiedSites(current); @@ -245,8 +240,7 @@ namespace Umbraco.Cms.Core.Routing } /// - public virtual IEnumerable MapDomains(IReadOnlyCollection domainAndUris, - Uri current, bool excludeDefault, string? culture, string? defaultCulture) + public virtual IEnumerable MapDomains(IReadOnlyCollection domainAndUris, Uri current, bool excludeDefault, string? culture, string? defaultCulture) { // TODO: ignoring cultures entirely? @@ -277,8 +271,7 @@ namespace Umbraco.Cms.Core.Routing { // it is illegal to call MapDomain if domainAndUris is empty // also, domainAndUris should NOT contain current, hence the test on hinted - DomainAndUri? mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, - defaultCulture); // what GetUrl would get + DomainAndUri? mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, defaultCulture); // what GetUrl would get ret = ret.Where(d => d != mainDomain); } } @@ -368,16 +361,19 @@ namespace Umbraco.Cms.Core.Routing kvp => kvp.Value.Select(d => new Uri(UriUtilityCore.StartWithScheme(d, current.Scheme)) .GetLeftPart(UriPartial.Authority)) - .ToArray() - ); + .ToArray()); // .ToDictionary will evaluate and create the dictionary immediately // the new value is .ToArray so it will also be evaluated immediately // therefore it is safe to return and exit the configuration lock } - private DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, - Dictionary? qualifiedSites, string currentAuthority, string? culture, string? defaultCulture) + private DomainAndUri? MapDomain( + IReadOnlyCollection domainAndUris, + Dictionary? qualifiedSites, + string currentAuthority, + string? culture, + string? defaultCulture) { if (domainAndUris == null) { diff --git a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs index 5d298b811a..fe1e83d254 100644 --- a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs +++ b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs @@ -1,134 +1,129 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Utility for checking paths +/// +public class UmbracoRequestPaths { + private readonly string _apiMvcPath; + private readonly string _appPath; + private readonly string _backOfficeMvcPath; + private readonly string _backOfficePath; + private readonly List _defaultUmbPaths; + private readonly string _installPath; + private readonly string _mvcArea; + private readonly string _previewMvcPath; + private readonly string _surfaceMvcPath; + /// - /// Utility for checking paths + /// Initializes a new instance of the class. /// - public class UmbracoRequestPaths + public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment) { - private readonly string _backOfficePath; - private readonly string _mvcArea; - private readonly string _backOfficeMvcPath; - private readonly string _previewMvcPath; - private readonly string _surfaceMvcPath; - private readonly string _apiMvcPath; - private readonly string _installPath; - private readonly string _appPath; - private readonly List _defaultUmbPaths; + var applicationPath = hostingEnvironment.ApplicationVirtualPath; + _appPath = applicationPath.TrimStart(Constants.CharArrays.ForwardSlash); - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment) + _backOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment) + .EnsureStartsWith('/').TrimStart(_appPath.EnsureStartsWith('/')).EnsureStartsWith('/'); + + _mvcArea = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); + _defaultUmbPaths = new List { "/" + _mvcArea, "/" + _mvcArea + "/" }; + _backOfficeMvcPath = "/" + _mvcArea + "/BackOffice/"; + _previewMvcPath = "/" + _mvcArea + "/Preview/"; + _surfaceMvcPath = "/" + _mvcArea + "/Surface/"; + _apiMvcPath = "/" + _mvcArea + "/Api/"; + _installPath = hostingEnvironment.ToAbsolute(Constants.SystemDirectories.Install); + } + + /// + /// Checks if the current uri is a back office request + /// + /// + /// + /// There are some special routes we need to check to properly determine this: + /// + /// + /// These are def back office: + /// /Umbraco/BackOffice = back office + /// /Umbraco/Preview = back office + /// + /// + /// If it's not any of the above then we cannot determine if it's back office or front-end + /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the + /// backoffice + /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. + /// + /// + /// These are def front-end: + /// /Umbraco/Surface = front-end + /// /Umbraco/Api = front-end + /// But if we've got this far we'll just have to assume it's front-end anyways. + /// + /// + public bool IsBackOfficeRequest(string absPath) + { + var fullUrlPath = absPath.TrimStart(Constants.CharArrays.ForwardSlash); + var urlPath = fullUrlPath.TrimStart(_appPath).EnsureStartsWith('/'); + + // check if this is in the umbraco back office + var isUmbracoPath = urlPath.InvariantStartsWith(_backOfficePath); + + // if not, then def not back office + if (isUmbracoPath == false) { - var applicationPath = hostingEnvironment.ApplicationVirtualPath; - _appPath = applicationPath.TrimStart(Constants.CharArrays.ForwardSlash); - - _backOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment) - .EnsureStartsWith('/').TrimStart(_appPath.EnsureStartsWith('/')).EnsureStartsWith('/'); - - _mvcArea = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); - _defaultUmbPaths = new List { "/" + _mvcArea, "/" + _mvcArea + "/" }; - _backOfficeMvcPath = "/" + _mvcArea + "/BackOffice/"; - _previewMvcPath = "/" + _mvcArea + "/Preview/"; - _surfaceMvcPath = "/" + _mvcArea + "/Surface/"; - _apiMvcPath = "/" + _mvcArea + "/Api/"; - _installPath = hostingEnvironment.ToAbsolute(Constants.SystemDirectories.Install); + return false; } - /// - /// Checks if the current uri is a back office request - /// - /// - /// - /// There are some special routes we need to check to properly determine this: - /// - /// - /// These are def back office: - /// /Umbraco/BackOffice = back office - /// /Umbraco/Preview = back office - /// - /// - /// If it's not any of the above then we cannot determine if it's back office or front-end - /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the backoffice - /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. - /// - /// - /// These are def front-end: - /// /Umbraco/Surface = front-end - /// /Umbraco/Api = front-end - /// But if we've got this far we'll just have to assume it's front-end anyways. - /// - /// - public bool IsBackOfficeRequest(string absPath) + // if its the normal /umbraco path + if (_defaultUmbPaths.Any(x => urlPath.InvariantEquals(x))) { - var fullUrlPath = absPath.TrimStart(Constants.CharArrays.ForwardSlash); - var urlPath = fullUrlPath.TrimStart(_appPath).EnsureStartsWith('/'); - - // check if this is in the umbraco back office - var isUmbracoPath = urlPath.InvariantStartsWith(_backOfficePath); - - // if not, then def not back office - if (isUmbracoPath == false) - { - return false; - } - - // if its the normal /umbraco path - if (_defaultUmbPaths.Any(x => urlPath.InvariantEquals(x))) - { - return true; - } - - // check for special back office paths - if (urlPath.InvariantStartsWith(_backOfficeMvcPath) - || urlPath.InvariantStartsWith(_previewMvcPath)) - { - return true; - } - - // check for special front-end paths - if (urlPath.InvariantStartsWith(_surfaceMvcPath) - || urlPath.InvariantStartsWith(_apiMvcPath)) - { - return false; - } - - // if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by - // checking how many parts the route has, for example, all PluginController routes will be routed like - // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} - // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a - // plugin controller for the front-end. - if (urlPath.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries).Length >= 3) - { - return false; - } - - // if its anything else we can assume it's back office return true; } - /// - /// Checks if the current uri is an install request - /// - public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath); - - /// - /// Rudimentary check to see if it's not a server side request - /// - public bool IsClientSideRequest(string absPath) + // check for special back office paths + if (urlPath.InvariantStartsWith(_backOfficeMvcPath) + || urlPath.InvariantStartsWith(_previewMvcPath)) { - var ext = Path.GetExtension(absPath); - return !ext.IsNullOrWhiteSpace(); + return true; } + + // check for special front-end paths + if (urlPath.InvariantStartsWith(_surfaceMvcPath) + || urlPath.InvariantStartsWith(_apiMvcPath)) + { + return false; + } + + // if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by + // checking how many parts the route has, for example, all PluginController routes will be routed like + // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} + // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a + // plugin controller for the front-end. + if (urlPath.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries).Length >= 3) + { + return false; + } + + // if its anything else we can assume it's back office + return true; + } + + /// + /// Checks if the current uri is an install request + /// + public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath); + + /// + /// Rudimentary check to see if it's not a server side request + /// + public bool IsClientSideRequest(string absPath) + { + var ext = Path.GetExtension(absPath); + return !ext.IsNullOrWhiteSpace(); } } diff --git a/src/Umbraco.Core/Routing/UmbracoRouteResult.cs b/src/Umbraco.Core/Routing/UmbracoRouteResult.cs index d41c7ad7c3..67690e11e8 100644 --- a/src/Umbraco.Core/Routing/UmbracoRouteResult.cs +++ b/src/Umbraco.Core/Routing/UmbracoRouteResult.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public enum UmbracoRouteResult { - public enum UmbracoRouteResult - { - /// - /// Routing was successful and a content item was matched - /// - Success, + /// + /// Routing was successful and a content item was matched + /// + Success, - /// - /// A redirection took place - /// - Redirect, + /// + /// A redirection took place + /// + Redirect, - /// - /// Nothing matched - /// - NotFound - } + /// + /// Nothing matched + /// + NotFound, } diff --git a/src/Umbraco.Core/Routing/UriUtility.cs b/src/Umbraco.Core/Routing/UriUtility.cs index b973bdd068..fb59ada249 100644 --- a/src/Umbraco.Core/Routing/UriUtility.cs +++ b/src/Umbraco.Core/Routing/UriUtility.cs @@ -1,201 +1,230 @@ -using System; using System.Text; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public sealed class UriUtility { - public sealed class UriUtility + private static string? _appPath; + private static string? _appPathPrefix; + + public UriUtility(IHostingEnvironment hostingEnvironment) { - static string? _appPath; - static string? _appPathPrefix; - - public UriUtility(IHostingEnvironment hostingEnvironment) + if (hostingEnvironment is null) { - if (hostingEnvironment is null) throw new ArgumentNullException(nameof(hostingEnvironment)); - ResetAppDomainAppVirtualPath(hostingEnvironment); + throw new ArgumentNullException(nameof(hostingEnvironment)); } - // internal for unit testing only - internal void SetAppDomainAppVirtualPath(string appPath) + ResetAppDomainAppVirtualPath(hostingEnvironment); + } + + // will be "/" or "/foo" + public string? AppPath => _appPath; + + // will be "" or "/foo" + public string? AppPathPrefix => _appPathPrefix; + + // adds the virtual directory if any + // see also VirtualPathUtility.ToAbsolute + // TODO: Does this do anything differently than IHostingEnvironment.ToAbsolute? Seems it does less, maybe should be removed? + public string ToAbsolute(string url) + { + // return ResolveUrl(url); + url = url.TrimStart(Constants.CharArrays.Tilde); + return _appPathPrefix + url; + } + + // internal for unit testing only + internal void SetAppDomainAppVirtualPath(string appPath) + { + _appPath = appPath ?? "/"; + _appPathPrefix = _appPath; + if (_appPathPrefix == "/") { - _appPath = appPath ?? "/"; - _appPathPrefix = _appPath; - if (_appPathPrefix == "/") - _appPathPrefix = String.Empty; + _appPathPrefix = string.Empty; + } + } + + internal void ResetAppDomainAppVirtualPath(IHostingEnvironment hostingEnvironment) => + SetAppDomainAppVirtualPath(hostingEnvironment.ApplicationVirtualPath); + + // strips the virtual directory if any + // see also VirtualPathUtility.ToAppRelative + public string ToAppRelative(string virtualPath) + { + if (_appPathPrefix is not null && virtualPath.InvariantStartsWith(_appPathPrefix) + && (virtualPath.Length == _appPathPrefix.Length || + virtualPath[_appPathPrefix.Length] == '/')) + { + virtualPath = virtualPath[_appPathPrefix.Length..]; } - internal void ResetAppDomainAppVirtualPath(IHostingEnvironment hostingEnvironment) + if (virtualPath.Length == 0) { - SetAppDomainAppVirtualPath(hostingEnvironment.ApplicationVirtualPath); + virtualPath = "/"; } - // will be "/" or "/foo" - public string? AppPath => _appPath; + return virtualPath; + } - // will be "" or "/foo" - public string? AppPathPrefix => _appPathPrefix; + // maps an internal umbraco uri to a public uri + // ie with virtual directory, .aspx if required... + public Uri UriFromUmbraco(Uri uri, RequestHandlerSettings requestConfig) + { + var path = uri.GetSafeAbsolutePath(); - // adds the virtual directory if any - // see also VirtualPathUtility.ToAbsolute - // TODO: Does this do anything differently than IHostingEnvironment.ToAbsolute? Seems it does less, maybe should be removed? - public string ToAbsolute(string url) + if (path != "/" && requestConfig.AddTrailingSlash) { - //return ResolveUrl(url); - url = url.TrimStart(Constants.CharArrays.Tilde); - return _appPathPrefix + url; + path = path.EnsureEndsWith("/"); } - // strips the virtual directory if any - // see also VirtualPathUtility.ToAppRelative - public string ToAppRelative(string virtualPath) + path = ToAbsolute(path); + + return uri.Rewrite(path); + } + + // maps a media umbraco uri to a public uri + // ie with virtual directory - that is all for media + public Uri MediaUriFromUmbraco(Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + path = ToAbsolute(path); + return uri.Rewrite(path); + } + + // maps a public uri to an internal umbraco uri + // ie no virtual directory, no .aspx, lowercase... + public Uri UriToUmbraco(Uri uri) + { + // TODO: This is critical code that executes on every request, we should + // look into if all of this is necessary? not really sure we need ToLower? + + // note: no need to decode uri here because we're returning a uri + // so it will be re-encoded anyway + var path = uri.GetSafeAbsolutePath(); + + path = path.ToLower(); + path = ToAppRelative(path); // strip vdir if any + + if (path != "/") { - if (_appPathPrefix is not null && virtualPath.InvariantStartsWith(_appPathPrefix) - && (virtualPath.Length == _appPathPrefix.Length || virtualPath[_appPathPrefix.Length] == '/')) + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); + } + + return uri.Rewrite(path); + } + + #region ResolveUrl + + // http://www.codeproject.com/Articles/53460/ResolveUrl-in-ASP-NET-The-Perfect-Solution + // note + // if browsing http://example.com/sub/page1.aspx then + // ResolveUrl("page2.aspx") returns "/page2.aspx" + // Page.ResolveUrl("page2.aspx") returns "/sub/page2.aspx" (relative...) + public string ResolveUrl(string relativeUrl) + { + if (relativeUrl == null) + { + throw new ArgumentNullException("relativeUrl"); + } + + if (relativeUrl.Length == 0 || relativeUrl[0] == '/' || relativeUrl[0] == '\\') + { + return relativeUrl; + } + + var idxOfScheme = relativeUrl.IndexOf(@"://", StringComparison.Ordinal); + if (idxOfScheme != -1) + { + var idxOfQM = relativeUrl.IndexOf('?'); + if (idxOfQM == -1 || idxOfQM > idxOfScheme) { - virtualPath = virtualPath.Substring(_appPathPrefix.Length); - } - - if (virtualPath.Length == 0) - { - virtualPath = "/"; - } - - return virtualPath; - } - - // maps an internal umbraco uri to a public uri - // ie with virtual directory, .aspx if required... - public Uri UriFromUmbraco(Uri uri, RequestHandlerSettings requestConfig) - { - var path = uri.GetSafeAbsolutePath(); - - if (path != "/" && requestConfig.AddTrailingSlash) - path = path.EnsureEndsWith("/"); - - path = ToAbsolute(path); - - return uri.Rewrite(path); - } - - // maps a media umbraco uri to a public uri - // ie with virtual directory - that is all for media - public Uri MediaUriFromUmbraco(Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - path = ToAbsolute(path); - return uri.Rewrite(path); - } - - // maps a public uri to an internal umbraco uri - // ie no virtual directory, no .aspx, lowercase... - public Uri UriToUmbraco(Uri uri) - { - // TODO: This is critical code that executes on every request, we should - // look into if all of this is necessary? not really sure we need ToLower? - - // note: no need to decode uri here because we're returning a uri - // so it will be re-encoded anyway - var path = uri.GetSafeAbsolutePath(); - - path = path.ToLower(); - path = ToAppRelative(path); // strip vdir if any - - if (path != "/") - { - path = path.TrimEnd(Constants.CharArrays.ForwardSlash); - } - - return uri.Rewrite(path); - } - - #region ResolveUrl - - // http://www.codeproject.com/Articles/53460/ResolveUrl-in-ASP-NET-The-Perfect-Solution - // note - // if browsing http://example.com/sub/page1.aspx then - // ResolveUrl("page2.aspx") returns "/page2.aspx" - // Page.ResolveUrl("page2.aspx") returns "/sub/page2.aspx" (relative...) - // - public string ResolveUrl(string relativeUrl) - { - if (relativeUrl == null) throw new ArgumentNullException("relativeUrl"); - - if (relativeUrl.Length == 0 || relativeUrl[0] == '/' || relativeUrl[0] == '\\') return relativeUrl; - - int idxOfScheme = relativeUrl.IndexOf(@"://", StringComparison.Ordinal); - if (idxOfScheme != -1) - { - int idxOfQM = relativeUrl.IndexOf('?'); - if (idxOfQM == -1 || idxOfQM > idxOfScheme) return relativeUrl; } + } - StringBuilder sbUrl = new StringBuilder(); - sbUrl.Append(_appPathPrefix); - if (sbUrl.Length == 0 || sbUrl[sbUrl.Length - 1] != '/') sbUrl.Append('/'); + var sbUrl = new StringBuilder(); + sbUrl.Append(_appPathPrefix); + if (sbUrl.Length == 0 || sbUrl[^1] != '/') + { + sbUrl.Append('/'); + } - // found question mark already? query string, do not touch! - bool foundQM = false; - bool foundSlash; // the latest char was a slash? - if (relativeUrl.Length > 1 - && relativeUrl[0] == '~' - && (relativeUrl[1] == '/' || relativeUrl[1] == '\\')) + // found question mark already? query string, do not touch! + var foundQM = false; + bool foundSlash; // the latest char was a slash? + if (relativeUrl.Length > 1 + && relativeUrl[0] == '~' + && (relativeUrl[1] == '/' || relativeUrl[1] == '\\')) + { + relativeUrl = relativeUrl[2..]; + foundSlash = true; + } + else + { + foundSlash = false; + } + + foreach (var c in relativeUrl) + { + if (!foundQM) { - relativeUrl = relativeUrl.Substring(2); - foundSlash = true; - } - else foundSlash = false; - foreach (char c in relativeUrl) - { - if (!foundQM) + if (c == '?') { - if (c == '?') foundQM = true; - else + foundQM = true; + } + else + { + if (c == '/' || c == '\\') { - if (c == '/' || c == '\\') + if (foundSlash) { - if (foundSlash) continue; - else - { - sbUrl.Append('/'); - foundSlash = true; - continue; - } + continue; } - else if (foundSlash) foundSlash = false; + + sbUrl.Append('/'); + foundSlash = true; + continue; + } + + if (foundSlash) + { + foundSlash = false; } } - sbUrl.Append(c); } - return sbUrl.ToString(); + sbUrl.Append(c); } - #endregion + return sbUrl.ToString(); + } + #endregion - /// - /// Returns an full URL with the host, port, etc... - /// - /// An absolute path (i.e. starts with a '/' ) - /// - /// - /// - /// Based on http://stackoverflow.com/questions/3681052/get-absolute-url-from-relative-path-refactored-method - /// - internal Uri ToFullUrl(string absolutePath, Uri curentRequestUrl) + /// + /// Returns an full URL with the host, port, etc... + /// + /// An absolute path (i.e. starts with a '/' ) + /// + /// + /// + /// Based on http://stackoverflow.com/questions/3681052/get-absolute-url-from-relative-path-refactored-method + /// + internal Uri ToFullUrl(string absolutePath, Uri curentRequestUrl) + { + if (string.IsNullOrEmpty(absolutePath)) { - if (string.IsNullOrEmpty(absolutePath)) - throw new ArgumentNullException(nameof(absolutePath)); - - if (!absolutePath.StartsWith("/")) - throw new FormatException("The absolutePath specified does not start with a '/'"); - - return new Uri(absolutePath, UriKind.Relative).MakeAbsolute(curentRequestUrl); + throw new ArgumentNullException(nameof(absolutePath)); } + if (!absolutePath.StartsWith("/")) + { + throw new FormatException("The absolutePath specified does not start with a '/'"); + } + return new Uri(absolutePath, UriKind.Relative).MakeAbsolute(curentRequestUrl); } } diff --git a/src/Umbraco.Core/Routing/UrlInfo.cs b/src/Umbraco.Core/Routing/UrlInfo.cs index 3a5c277725..f5b208fb73 100644 --- a/src/Umbraco.Core/Routing/UrlInfo.cs +++ b/src/Umbraco.Core/Routing/UrlInfo.cs @@ -1,102 +1,117 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Represents infos for a URL. +/// +[DataContract(Name = "urlInfo", Namespace = "")] +public class UrlInfo : IEquatable { /// - /// Represents infos for a URL. + /// Initializes a new instance of the class. /// - [DataContract(Name = "urlInfo", Namespace = "")] - public class UrlInfo : IEquatable + public UrlInfo(string text, bool isUrl, string? culture) { - - /// - /// Creates a instance representing a true URL. - /// - public static UrlInfo Url(string text, string? culture = null) => new UrlInfo(text, true, culture); - - /// - /// Creates a instance representing a message. - /// - public static UrlInfo Message(string text, string? culture = null) => new UrlInfo(text, false, culture); - - /// - /// Initializes a new instance of the class. - /// - public UrlInfo(string text, bool isUrl, string? culture) + if (string.IsNullOrWhiteSpace(text)) { - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(text)); - IsUrl = isUrl; - Text = text; - Culture = culture; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(text)); } - /// - /// Gets the culture. - /// - [DataMember(Name = "culture")] - public string? Culture { get; } + IsUrl = isUrl; + Text = text; + Culture = culture; + } - /// - /// Gets a value indicating whether the URL is a true URL. - /// - /// Otherwise, it is a message. - [DataMember(Name = "isUrl")] - public bool IsUrl { get; } + /// + /// Gets the culture. + /// + [DataMember(Name = "culture")] + public string? Culture { get; } - /// - /// Gets the text, which is either the URL, or a message. - /// - [DataMember(Name = "text")] - public string Text { get; } + /// + /// Gets a value indicating whether the URL is a true URL. + /// + /// Otherwise, it is a message. + [DataMember(Name = "isUrl")] + public bool IsUrl { get; } - /// - /// Checks equality - /// - /// - /// - /// - /// Compare both culture and Text as invariant strings since URLs are not case sensitive, nor are culture names within Umbraco - /// - public bool Equals(UrlInfo? other) + /// + /// Gets the text, which is either the URL, or a message. + /// + [DataMember(Name = "text")] + public string Text { get; } + + public static bool operator ==(UrlInfo left, UrlInfo right) => Equals(left, right); + + /// + /// Creates a instance representing a true URL. + /// + public static UrlInfo Url(string text, string? culture = null) => new(text, true, culture); + + /// + /// Checks equality + /// + /// + /// + /// + /// Compare both culture and Text as invariant strings since URLs are not case sensitive, nor are culture names within + /// Umbraco + /// + public bool Equals(UrlInfo? other) + { + if (ReferenceEquals(null, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Culture, other.Culture, StringComparison.InvariantCultureIgnoreCase) && IsUrl == other.IsUrl && string.Equals(Text, other.Text, StringComparison.InvariantCultureIgnoreCase); + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((UrlInfo) obj); + return true; } - public override int GetHashCode() + return string.Equals(Culture, other.Culture, StringComparison.InvariantCultureIgnoreCase) && + IsUrl == other.IsUrl && string.Equals(Text, other.Text, StringComparison.InvariantCultureIgnoreCase); + } + + /// + /// Creates a instance representing a message. + /// + public static UrlInfo Message(string text, string? culture = null) => new(text, false, culture); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - unchecked - { - var hashCode = (Culture != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Culture) : 0); - hashCode = (hashCode * 397) ^ IsUrl.GetHashCode(); - hashCode = (hashCode * 397) ^ (Text != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Text) : 0); - return hashCode; - } + return false; } - public static bool operator ==(UrlInfo left, UrlInfo right) + if (ReferenceEquals(this, obj)) { - return Equals(left, right); + return true; } - public static bool operator !=(UrlInfo left, UrlInfo right) + if (obj.GetType() != GetType()) { - return !Equals(left, right); + return false; } - public override string ToString() + return Equals((UrlInfo)obj); + } + + public override int GetHashCode() + { + unchecked { - return Text; + var hashCode = Culture != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Culture) : 0; + hashCode = (hashCode * 397) ^ IsUrl.GetHashCode(); + hashCode = (hashCode * 397) ^ + (Text != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Text) : 0); + return hashCode; } } + + public static bool operator !=(UrlInfo left, UrlInfo right) => !Equals(left, right); + + public override string ToString() => Text; } diff --git a/src/Umbraco.Core/Routing/UrlProvider.cs b/src/Umbraco.Core/Routing/UrlProvider.cs index 11597159f7..97385a144b 100644 --- a/src/Umbraco.Core/Routing/UrlProvider.cs +++ b/src/Umbraco.Core/Routing/UrlProvider.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -51,17 +48,17 @@ namespace Umbraco.Cms.Core.Routing private IPublishedContent? GetDocument(int id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return umbracoContext.Content?.GetById(id); } private IPublishedContent? GetDocument(Guid id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return umbracoContext.Content?.GetById(id); } private IPublishedContent? GetMedia(Guid id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return umbracoContext.Media?.GetById(id); } @@ -104,36 +101,40 @@ namespace Umbraco.Cms.Core.Routing public string GetUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null) { if (content == null || content.ContentType.ItemType == PublishedItemType.Element) + { return "#"; + } if (mode == UrlMode.Default) + { mode = Mode; + } // this the ONLY place where we deal with default culture - IUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant // (but only for variant content of course) - if (content.ContentType.VariesByCulture()) + // We need to check all ancestors because urls are variant even for invariant content, if an ancestor is variant. + if (culture == null && content.AncestorsOrSelf().Any(x => x.ContentType.VariesByCulture())) { - if (culture == null) - culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } if (current == null) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); current = umbracoContext.CleanedUmbracoUrl; } - var url = _urlProviders.Select(provider => provider.GetUrl(content, mode, culture, current)) + UrlInfo? url = _urlProviders.Select(provider => provider.GetUrl(content, mode, culture, current)) .FirstOrDefault(u => u is not null); return url?.Text ?? "#"; // legacy wants this } public string GetUrlFromRoute(int id, string? route, string? culture) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var provider = _urlProviders.OfType().FirstOrDefault(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + DefaultUrlProvider? provider = _urlProviders.OfType().FirstOrDefault(); var url = provider == null ? route // what else? : provider.GetUrlFromRoute(route, umbracoContext, id, umbracoContext.CleanedUmbracoUrl, Mode, culture)?.Text; @@ -156,7 +157,7 @@ namespace Umbraco.Cms.Core.Routing /// public IEnumerable GetOtherUrls(int id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return GetOtherUrls(id, umbracoContext.CleanedUmbracoUrl); } @@ -204,18 +205,24 @@ namespace Umbraco.Cms.Core.Routing /// The URL is absolute or relative depending on mode and on current. /// If the media is multi-lingual, gets the URL for the specified culture or, /// when no culture is specified, the current culture. - /// If the provider is unable to provide a URL, it returns . + /// If the provider is unable to provide a URL, it returns . /// public string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null) { if (propertyAlias == null) + { throw new ArgumentNullException(nameof(propertyAlias)); + } if (content == null) - return ""; + { + return string.Empty; + } if (mode == UrlMode.Default) + { mode = Mode; + } // this the ONLY place where we deal with default culture - IMediaUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant @@ -223,21 +230,23 @@ namespace Umbraco.Cms.Core.Routing if (content.ContentType.VariesByCulture()) { if (culture == null) - culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + { + culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; + } } if (current == null) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); current = umbracoContext.CleanedUmbracoUrl; } - var url = _mediaUrlProviders.Select(provider => + UrlInfo? url = _mediaUrlProviders.Select(provider => provider.GetMediaUrl(content, propertyAlias, mode, culture, current)) .FirstOrDefault(u => u is not null); - return url?.Text ?? ""; + return url?.Text ?? string.Empty; } #endregion diff --git a/src/Umbraco.Core/Routing/UrlProviderCollection.cs b/src/Umbraco.Core/Routing/UrlProviderCollection.cs index c17417c83c..0acb75264d 100644 --- a/src/Umbraco.Core/Routing/UrlProviderCollection.cs +++ b/src/Umbraco.Core/Routing/UrlProviderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class UrlProviderCollection : BuilderCollectionBase { - public class UrlProviderCollection : BuilderCollectionBase + public UrlProviderCollection(Func> items) + : base(items) { - public UrlProviderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs b/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs index ca6f703c8b..fe975272dd 100644 --- a/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs +++ b/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class UrlProviderCollectionBuilder : OrderedCollectionBuilderBase { - public class UrlProviderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override UrlProviderCollectionBuilder This => this; - } + protected override UrlProviderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 5f28bee2b7..8e2a577f3a 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -6,250 +6,256 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UrlProviderExtensions { - public static class UrlProviderExtensions + /// + /// Gets the URLs of the content item. + /// + /// + /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list. + /// Contains all the URLs that we can figure out (based upon domains, etc). + /// + public static async Task> GetContentUrlsAsync( + this IContent content, + IPublishedRouter publishedRouter, + IUmbracoContext umbracoContext, + ILocalizationService localizationService, + ILocalizedTextService textService, + IContentService contentService, + IVariationContextAccessor variationContextAccessor, + ILogger logger, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider) { - /// - /// Gets the URLs of the content item. - /// - /// - /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list. - /// Contains all the URLs that we can figure out (based upon domains, etc). - /// - public static async Task> GetContentUrlsAsync( - this IContent content, - IPublishedRouter publishedRouter, - IUmbracoContext umbracoContext, - ILocalizationService localizationService, - ILocalizedTextService textService, - IContentService contentService, - IVariationContextAccessor variationContextAccessor, - ILogger logger, - UriUtility uriUtility, - IPublishedUrlProvider publishedUrlProvider) + ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(publishedRouter); + ArgumentNullException.ThrowIfNull(umbracoContext); + ArgumentNullException.ThrowIfNull(localizationService); + ArgumentNullException.ThrowIfNull(textService); + ArgumentNullException.ThrowIfNull(contentService); + ArgumentNullException.ThrowIfNull(variationContextAccessor); + ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(uriUtility); + ArgumentNullException.ThrowIfNull(publishedUrlProvider); + + var result = new List(); + + if (content.Published == false) { - ArgumentNullException.ThrowIfNull(content); - ArgumentNullException.ThrowIfNull(publishedRouter); - ArgumentNullException.ThrowIfNull(umbracoContext); - ArgumentNullException.ThrowIfNull(localizationService); - ArgumentNullException.ThrowIfNull(textService); - ArgumentNullException.ThrowIfNull(contentService); - ArgumentNullException.ThrowIfNull(variationContextAccessor); - ArgumentNullException.ThrowIfNull(logger); - ArgumentNullException.ThrowIfNull(uriUtility); - ArgumentNullException.ThrowIfNull(publishedUrlProvider); - - var result = new List(); - - if (content.Published == false) - { - result.Add(UrlInfo.Message(textService.Localize("content", "itemNotPublished"))); - return result; - } - - // build a list of URLs, for the back-office - // which will contain - // - the 'main' URLs, which is what .Url would return, for each culture - // - the 'other' URLs we know (based upon domains, etc) - // - // need to work through each installed culture: - // on invariant nodes, each culture returns the same URL segment but, - // we don't know if the branch to this content is invariant, so we need to ask - // for URLs for all cultures. - // and, not only for those assigned to domains in the branch, because we want - // to show what GetUrl() would return, for every culture. - var urls = new HashSet(); - var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList(); - - // get all URLs for all cultures - // in a HashSet, so de-duplicates too - foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider)) - { - urls.Add(cultureUrl); - } - - // return the real URLs first, then the messages - foreach (IGrouping urlGroup in urls.GroupBy(x => x.IsUrl).OrderByDescending(x => x.Key)) - { - // in some cases there will be the same URL for multiple cultures: - // * The entire branch is invariant - // * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed - if (urlGroup.Key) - { - result.AddRange(urlGroup.DistinctBy(x => x.Text, StringComparer.OrdinalIgnoreCase).OrderBy(x => x.Text).ThenBy(x => x.Culture)); - } - else - { - result.AddRange(urlGroup); - } - } - - // get the 'other' URLs - ie not what you'd get with GetUrl() but URLs that would route to the document, nevertheless. - // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. - foreach (UrlInfo otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture)) - { - // avoid duplicates - if (urls.Add(otherUrl)) - { - result.Add(otherUrl); - } - } - + result.Add(UrlInfo.Message(textService.Localize("content", "itemNotPublished"))); return result; } - /// - /// Tries to return a for each culture for the content while detecting collisions/errors - /// - private static async Task> GetContentUrlsByCultureAsync( - IContent content, - IEnumerable cultures, - IPublishedRouter publishedRouter, - IUmbracoContext umbracoContext, - IContentService contentService, - ILocalizedTextService textService, - IVariationContextAccessor variationContextAccessor, - ILogger logger, - UriUtility uriUtility, - IPublishedUrlProvider publishedUrlProvider) + // build a list of URLs, for the back-office + // which will contain + // - the 'main' URLs, which is what .Url would return, for each culture + // - the 'other' URLs we know (based upon domains, etc) + // + // need to work through each installed culture: + // on invariant nodes, each culture returns the same URL segment but, + // we don't know if the branch to this content is invariant, so we need to ask + // for URLs for all cultures. + // and, not only for those assigned to domains in the branch, because we want + // to show what GetUrl() would return, for every culture. + var urls = new HashSet(); + var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList(); + + // get all URLs for all cultures + // in a HashSet, so de-duplicates too + foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider)) { - var result = new List(); - - foreach (var culture in cultures) - { - // if content is variant, and culture is not published, skip - if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture)) - { - continue; - } - - // if it's variant and culture is published, or if it's invariant, proceed - string url; - try - { - url = publishedUrlProvider.GetUrl(content.Id, culture: culture); - } - catch (Exception ex) - { - logger.LogError(ex, "GetUrl exception."); - url = "#ex"; - } - - switch (url) - { - // deal with 'could not get the URL' - case "#": - result.Add(HandleCouldNotGetUrl(content, culture, contentService, textService)); - break; - - // deal with exceptions - case "#ex": - result.Add(UrlInfo.Message(textService.Localize("content", "getUrlException"), culture)); - break; - - // got a URL, deal with collisions, add URL - default: - // detect collisions, etc - Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); - if (hasCollision.Success && hasCollision.Result is not null) - { - result.Add(hasCollision.Result); - } - else - { - result.Add(UrlInfo.Url(url, culture)); - } - - break; - } - } - - return result; + urls.Add(cultureUrl); } - private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService) + // return the real URLs first, then the messages + foreach (IGrouping urlGroup in urls.GroupBy(x => x.IsUrl).OrderByDescending(x => x.Key)) { - // document has a published version yet its URL is "#" => a parent must be - // unpublished, walk up the tree until we find it, and report. - IContent? parent = content; - do + // in some cases there will be the same URL for multiple cultures: + // * The entire branch is invariant + // * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed + if (urlGroup.Key) { - parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null; + result.AddRange(urlGroup.DistinctBy(x => x.Text, StringComparer.OrdinalIgnoreCase).OrderBy(x => x.Text) + .ThenBy(x => x.Culture)); } - while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); - - if (parent == null) + else { - // oops, internal error - return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); + result.AddRange(urlGroup); } - - if (!parent.Published) - { - // totally not published - return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); - } - - // culture not published - return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }), culture); } - private static async Task> DetectCollisionAsync( - ILogger logger, - IContent content, - string url, - string culture, - IUmbracoContext umbracoContext, - IPublishedRouter publishedRouter, - ILocalizedTextService textService, - IVariationContextAccessor variationContextAccessor, - UriUtility uriUtility) + // get the 'other' URLs - ie not what you'd get with GetUrl() but URLs that would route to the document, nevertheless. + // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. + foreach (UrlInfo otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text) + .ThenBy(x => x.Culture)) { - // test for collisions on the 'main' URL - var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); - if (uri.IsAbsoluteUri == false) + // avoid duplicates + if (urls.Add(otherUrl)) { - uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl); + result.Add(otherUrl); + } + } + + return result; + } + + /// + /// Tries to return a for each culture for the content while detecting collisions/errors + /// + private static async Task> GetContentUrlsByCultureAsync( + IContent content, + IEnumerable cultures, + IPublishedRouter publishedRouter, + IUmbracoContext umbracoContext, + IContentService contentService, + ILocalizedTextService textService, + IVariationContextAccessor variationContextAccessor, + ILogger logger, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider) + { + var result = new List(); + + foreach (var culture in cultures) + { + // if content is variant, and culture is not published, skip + if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture)) + { + continue; } - uri = uriUtility.UriToUmbraco(uri); - IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri); - IPublishedRequest pcr = await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); - - if (!pcr.HasPublishedContent()) + // if it's variant and culture is published, or if it's invariant, proceed + string url; + try { - const string logMsg = nameof(DetectCollisionAsync) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; - logger.LogDebug(logMsg, url, uri, culture); - - var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); - return Attempt.Succeed(urlInfo); + url = publishedUrlProvider.GetUrl(content.Id, culture: culture); + } + catch (Exception ex) + { + logger.LogError(ex, "GetUrl exception."); + url = "#ex"; } - if (pcr.IgnorePublishedContentCollisions) + switch (url) { - return Attempt.Fail(); + // deal with 'could not get the URL' + case "#": + result.Add(HandleCouldNotGetUrl(content, culture, contentService, textService)); + break; + + // deal with exceptions + case "#ex": + result.Add(UrlInfo.Message(textService.Localize("content", "getUrlException"), culture)); + break; + + // got a URL, deal with collisions, add URL + default: + // detect collisions, etc + Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); + if (hasCollision.Success && hasCollision.Result is not null) + { + result.Add(hasCollision.Result); + } + else + { + result.Add(UrlInfo.Url(url, culture)); + } + + break; } + } - if (pcr.PublishedContent?.Id != content.Id) - { - IPublishedContent? o = pcr.PublishedContent; - var l = new List(); - while (o != null) - { - l.Add(o.Name(variationContextAccessor)!); - o = o.Parent; - } + return result; + } - l.Reverse(); - var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent?.Id + ")"; + private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService) + { + // document has a published version yet its URL is "#" => a parent must be + // unpublished, walk up the tree until we find it, and report. + IContent? parent = content; + do + { + parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null; + } + while (parent != null && parent.Published && + (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); - var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture); - return Attempt.Succeed(urlInfo); - } + if (parent == null) + { + // oops, internal error + return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); + } - // no collision + if (!parent.Published) + { + // totally not published + return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); + } + + // culture not published + return UrlInfo.Message( + textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }), + culture); + } + + private static async Task> DetectCollisionAsync( + ILogger logger, + IContent content, + string url, + string culture, + IUmbracoContext umbracoContext, + IPublishedRouter publishedRouter, + ILocalizedTextService textService, + IVariationContextAccessor variationContextAccessor, + UriUtility uriUtility) + { + // test for collisions on the 'main' URL + var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri == false) + { + uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl); + } + + uri = uriUtility.UriToUmbraco(uri); + IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri); + IPublishedRequest pcr = + await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); + + if (!pcr.HasPublishedContent()) + { + const string logMsg = nameof(DetectCollisionAsync) + + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + logger.LogDebug(logMsg, url, uri, culture); + + var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); + return Attempt.Succeed(urlInfo); + } + + if (pcr.IgnorePublishedContentCollisions) + { return Attempt.Fail(); } + + if (pcr.PublishedContent?.Id != content.Id) + { + IPublishedContent? o = pcr.PublishedContent; + var l = new List(); + while (o != null) + { + l.Add(o.Name(variationContextAccessor)!); + o = o.Parent; + } + + l.Reverse(); + var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent?.Id + ")"; + + var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture); + return Attempt.Succeed(urlInfo); + } + + // no collision + return Attempt.Fail(); } } diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs index a4da94ac79..7ecafff8a3 100644 --- a/src/Umbraco.Core/Routing/WebPath.cs +++ b/src/Umbraco.Core/Routing/WebPath.cs @@ -1,55 +1,53 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class WebPath { - public class WebPath + public const char PathSeparator = '/'; + + public static string Combine(params string[]? paths) { - public const char PathSeparator = '/'; - - public static string Combine(params string[]? paths) + if (paths == null) { - if (paths == null) - { - throw new ArgumentNullException(nameof(paths)); - } - - if (paths.Length == 0) - { - return string.Empty; - } - - var sb = new StringBuilder(); - - for (var index = 0; index < paths.Length; index++) - { - var path = paths[index]; - var start = 0; - var count = path.Length; - var isFirst = index == 0; - var isLast = index == paths.Length - 1; - - // don't trim start if it's the first - if (!isFirst && path[0] == PathSeparator) - { - start = 1; - } - - // always trim end - if (path[path.Length - 1] == PathSeparator) - { - count = path.Length - 1; - } - - sb.Append(path, start, count - start); - - if (!isLast) - { - sb.Append(PathSeparator); - } - } - - return sb.ToString(); + throw new ArgumentNullException(nameof(paths)); } + + if (paths.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(); + + for (var index = 0; index < paths.Length; index++) + { + var path = paths[index]; + var start = 0; + var count = path.Length; + var isFirst = index == 0; + var isLast = index == paths.Length - 1; + + // don't trim start if it's the first + if (!isFirst && path[0] == PathSeparator) + { + start = 1; + } + + // always trim end + if (path[^1] == PathSeparator) + { + count = path.Length - 1; + } + + sb.Append(path, start, count - start); + + if (!isLast) + { + sb.Append(PathSeparator); + } + } + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs index 6c45e4d969..8d7ec082be 100644 --- a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs +++ b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs @@ -5,31 +5,29 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +public class EssentialDirectoryCreator : INotificationHandler { - public class EssentialDirectoryCreator : INotificationHandler + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; + + public EssentialDirectoryCreator(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) { - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly GlobalSettings _globalSettings; + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + _globalSettings = globalSettings.Value; + } - public EssentialDirectoryCreator(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) - { - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - _globalSettings = globalSettings.Value; - } - - public void Handle(UmbracoApplicationStartingNotification notification) - { - // ensure we have some essential directories - // every other component can then initialize safely - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); - - } + public void Handle(UmbracoApplicationStartingNotification notification) + { + // ensure we have some essential directories + // every other component can then initialize safely + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); } } diff --git a/src/Umbraco.Core/Runtime/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs index 65c64857b3..59278e161c 100644 --- a/src/Umbraco.Core/Runtime/IMainDom.cs +++ b/src/Umbraco.Core/Runtime/IMainDom.cs @@ -1,39 +1,39 @@ -using System; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +/// +/// Represents the main AppDomain running for a given application. +/// +/// +/// There can be only one "main" AppDomain running for a given application at a time. +/// It is possible to register against the MainDom and be notified when it is released. +/// +public interface IMainDom { /// - /// Represents the main AppDomain running for a given application. + /// Gets a value indicating whether the current domain is the main domain. /// /// - /// There can be only one "main" AppDomain running for a given application at a time. - /// It is possible to register against the MainDom and be notified when it is released. + /// Acquire must be called first else this will always return false /// - public interface IMainDom - { - /// - /// Gets a value indicating whether the current domain is the main domain. - /// - /// - /// Acquire must be called first else this will always return false - /// - bool IsMainDom { get; } + bool IsMainDom { get; } - /// - /// Tries to acquire the MainDom, returns true if successful else false - /// - bool Acquire(IApplicationShutdownRegistry hostingEnvironment); + /// + /// Tries to acquire the MainDom, returns true if successful else false + /// + bool Acquire(IApplicationShutdownRegistry hostingEnvironment); - /// - /// Registers a resource that requires the current AppDomain to be the main domain to function. - /// - /// An action to execute when registering. - /// An action to execute before the AppDomain releases the main domain status. - /// An optional weight (lower goes first). - /// A value indicating whether it was possible to register. - /// If registering is successful, then the action - /// is guaranteed to execute before the AppDomain releases the main domain status. - bool Register(Action? install = null, Action? release = null, int weight = 100); - } + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute when registering. + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. + /// + /// If registering is successful, then the action + /// is guaranteed to execute before the AppDomain releases the main domain status. + /// + bool Register(Action? install = null, Action? release = null, int weight = 100); } diff --git a/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs index 5b8fb819e6..cbfbffac4c 100644 --- a/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs +++ b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +/// +/// Defines a class which can generate a distinct key for a MainDom boundary. +/// +public interface IMainDomKeyGenerator { /// - /// Defines a class which can generate a distinct key for a MainDom boundary. + /// Returns a key that signifies a MainDom boundary. /// - public interface IMainDomKeyGenerator - { - /// - /// Returns a key that signifies a MainDom boundary. - /// - string GenerateKey(); - } + string GenerateKey(); } diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs index b0b3394a01..7e58fa6533 100644 --- a/src/Umbraco.Core/Runtime/IMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -1,29 +1,25 @@ -using System; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Core.Runtime +/// +/// An application-wide distributed lock +/// +/// +/// Disposing releases the lock +/// +public interface IMainDomLock : IDisposable { /// - /// An application-wide distributed lock + /// Acquires an application-wide distributed lock /// - /// - /// Disposing releases the lock - /// - public interface IMainDomLock : IDisposable - { - /// - /// Acquires an application-wide distributed lock - /// - /// - /// - /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded - /// - Task AcquireLockAsync(int millisecondsTimeout); + /// + /// + /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded + /// + Task AcquireLockAsync(int millisecondsTimeout); - /// - /// Wait on a background thread to receive a signal from another AppDomain - /// - /// - Task ListenAsync(); - } + /// + /// Wait on a background thread to receive a signal from another AppDomain + /// + /// + Task ListenAsync(); } diff --git a/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs b/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs index 48ea6a5a48..b5c43cde4a 100644 --- a/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs +++ b/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +public interface IUmbracoBootPermissionChecker { - public interface IUmbracoBootPermissionChecker - { - void ThrowIfNotPermissions(); - } + void ThrowIfNotPermissions(); } diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 0198382b2a..83736914a2 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; @@ -27,7 +22,7 @@ namespace Umbraco.Cms.Core.Runtime private readonly IMainDomLock _mainDomLock; // our own lock for local consistency - private object _locko = new object(); + private object _locko = new(); private bool _isInitialized; // indicates whether... @@ -35,7 +30,7 @@ namespace Umbraco.Cms.Core.Runtime private volatile bool _signaled; // we have been signaled // actions to run before releasing the main domain - private readonly List> _callbacks = new List>(); + private readonly List> _callbacks = new(); private const int LockTimeoutMilliseconds = 40000; // 40 seconds @@ -114,14 +109,22 @@ namespace Umbraco.Cms.Core.Runtime lock (_locko) { _logger.LogDebug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source); - if (_signaled) return; - if (_isMainDom == false) return; // probably not needed + if (_signaled) + { + return; + } + + if (_isMainDom == false) + { + return; // probably not needed + } + _signaled = true; try { _logger.LogInformation("Stopping ({SignalSource})", source); - foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) + foreach (Action callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) { try { @@ -189,7 +192,8 @@ namespace Umbraco.Cms.Core.Runtime { // Listen for the signal from another AppDomain coming online to release the lock _listenTask = _mainDomLock.ListenAsync(); - _listenCompleteTask = _listenTask.ContinueWith(t => + _listenCompleteTask = _listenTask.ContinueWith( + t => { if (_listenTask.Exception != null) { @@ -201,7 +205,8 @@ namespace Umbraco.Cms.Core.Runtime } OnSignal("signal"); - }, TaskScheduler.Default); // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + }, + TaskScheduler.Default); // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html } catch (OperationCanceledException ex) { @@ -246,18 +251,18 @@ namespace Umbraco.Cms.Core.Runtime // This code added to correctly implement the disposable pattern. - private bool disposedValue = false; // To detect redundant calls + private bool _disposedValue; // To detect redundant calls protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposedValue) { if (disposing) { _mainDomLock.Dispose(); } - disposedValue = true; + _disposedValue = true; } } diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs index 5d2248906e..cdfd7b9305 100644 --- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -1,104 +1,100 @@ -using System; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +/// +/// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain +/// +public class MainDomSemaphoreLock : IMainDomLock { - /// - /// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain - /// - public class MainDomSemaphoreLock : IMainDomLock + private readonly ILogger _logger; + + // event wait handle used to notify current main domain that it should + // release the lock because a new domain wants to be the main domain + private readonly EventWaitHandle _signal; + private readonly SystemLock _systemLock; + private IDisposable? _lockRelease; + + public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment) { - private readonly SystemLock _systemLock; - - // event wait handle used to notify current main domain that it should - // release the lock because a new domain wants to be the main domain - private readonly EventWaitHandle _signal; - private readonly ILogger _logger; - private IDisposable? _lockRelease; - - public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - throw new PlatformNotSupportedException("MainDomSemaphoreLock is only supported on Windows."); - } - - var mainDomId = MainDom.GetMainDomId(hostingEnvironment); - var lockName = "UMBRACO-" + mainDomId + "-MAINDOM-LCK"; - _systemLock = new SystemLock(lockName); - - var eventName = "UMBRACO-" + mainDomId + "-MAINDOM-EVT"; - _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); - _logger = logger; + throw new PlatformNotSupportedException("MainDomSemaphoreLock is only supported on Windows."); } - // WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread - public Task ListenAsync() => _signal.WaitOneAsync(); + var mainDomId = MainDom.GetMainDomId(hostingEnvironment); + var lockName = "UMBRACO-" + mainDomId + "-MAINDOM-LCK"; + _systemLock = new SystemLock(lockName); - public Task AcquireLockAsync(int millisecondsTimeout) - { - // signal other instances that we want the lock, then wait on the lock, - // which may timeout, and this is accepted - see comments below - - // signal, then wait for the lock, then make sure the event is - // reset (maybe there was noone listening..) - _signal.Set(); - - // if more than 1 instance reach that point, one will get the lock - // and the other one will timeout, which is accepted - - // This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. - try - { - _lockRelease = _systemLock.Lock(millisecondsTimeout); - return Task.FromResult(true); - } - catch (TimeoutException ex) - { - _logger.LogError(ex.Message); - return Task.FromResult(false); - } - finally - { - // we need to reset the event, because otherwise we would end up - // signaling ourselves and committing suicide immediately. - // only 1 instance can reach that point, but other instances may - // have started and be trying to get the lock - they will timeout, - // which is accepted - - _signal.Reset(); - } - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - _lockRelease?.Dispose(); - _signal.Close(); - _signal.Dispose(); - } - - disposedValue = true; - } - } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - } - #endregion + var eventName = "UMBRACO-" + mainDomId + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _logger = logger; } + + // WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread + public Task ListenAsync() => _signal.WaitOneAsync(); + + public Task AcquireLockAsync(int millisecondsTimeout) + { + // signal other instances that we want the lock, then wait on the lock, + // which may timeout, and this is accepted - see comments below + + // signal, then wait for the lock, then make sure the event is + // reset (maybe there was noone listening..) + _signal.Set(); + + // if more than 1 instance reach that point, one will get the lock + // and the other one will timeout, which is accepted + + // This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. + try + { + _lockRelease = _systemLock.Lock(millisecondsTimeout); + return Task.FromResult(true); + } + catch (TimeoutException ex) + { + _logger.LogError(ex.Message); + return Task.FromResult(false); + } + finally + { + // we need to reset the event, because otherwise we would end up + // signaling ourselves and committing suicide immediately. + // only 1 instance can reach that point, but other instances may + // have started and be trying to get the lock - they will timeout, + // which is accepted + _signal.Reset(); + } + } + + #region IDisposable Support + + private bool disposedValue; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _lockRelease?.Dispose(); + _signal.Close(); + _signal.Dispose(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() => + + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + + #endregion } diff --git a/src/Umbraco.Core/RuntimeLevel.cs b/src/Umbraco.Core/RuntimeLevel.cs index d6687d4628..5b726045a9 100644 --- a/src/Umbraco.Core/RuntimeLevel.cs +++ b/src/Umbraco.Core/RuntimeLevel.cs @@ -1,40 +1,39 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Describes the levels in which the runtime can run. +/// +public enum RuntimeLevel { /// - /// Describes the levels in which the runtime can run. + /// The runtime has failed to boot and cannot run. /// - public enum RuntimeLevel - { - /// - /// The runtime has failed to boot and cannot run. - /// - BootFailed = -1, + BootFailed = -1, - /// - /// The level is unknown. - /// - Unknown = 0, + /// + /// The level is unknown. + /// + Unknown = 0, - /// - /// The runtime is booting. - /// - Boot = 1, + /// + /// The runtime is booting. + /// + Boot = 1, - /// - /// The runtime has detected that Umbraco is not installed at all, ie there is - /// no database, and is currently installing Umbraco. - /// - Install = 2, + /// + /// The runtime has detected that Umbraco is not installed at all, ie there is + /// no database, and is currently installing Umbraco. + /// + Install = 2, - /// - /// The runtime has detected an Umbraco install which needed to be upgraded, and - /// is currently upgrading Umbraco. - /// - Upgrade = 3, + /// + /// The runtime has detected an Umbraco install which needed to be upgraded, and + /// is currently upgrading Umbraco. + /// + Upgrade = 3, - /// - /// The runtime has detected an up-to-date Umbraco install and is running. - /// - Run = 100 - } + /// + /// The runtime has detected an up-to-date Umbraco install and is running. + /// + Run = 100, } diff --git a/src/Umbraco.Core/RuntimeLevelReason.cs b/src/Umbraco.Core/RuntimeLevelReason.cs index 94192c83b2..76f47e8a17 100644 --- a/src/Umbraco.Core/RuntimeLevelReason.cs +++ b/src/Umbraco.Core/RuntimeLevelReason.cs @@ -1,78 +1,77 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Describes the reason for the runtime level. +/// +public enum RuntimeLevelReason { /// - /// Describes the reason for the runtime level. + /// The reason is unknown. /// - public enum RuntimeLevelReason - { - /// - /// The reason is unknown. - /// - Unknown, + Unknown, - /// - /// The code version is lower than the version indicated in web.config, and - /// downgrading Umbraco is not supported. - /// - BootFailedCannotDowngrade, + /// + /// The code version is lower than the version indicated in web.config, and + /// downgrading Umbraco is not supported. + /// + BootFailedCannotDowngrade, - /// - /// The runtime cannot connect to the configured database. - /// - BootFailedCannotConnectToDatabase, + /// + /// The runtime cannot connect to the configured database. + /// + BootFailedCannotConnectToDatabase, - /// - /// The runtime can connect to the configured database, but it cannot - /// retrieve the migrations status. - /// - BootFailedCannotCheckUpgradeState, + /// + /// The runtime can connect to the configured database, but it cannot + /// retrieve the migrations status. + /// + BootFailedCannotCheckUpgradeState, - /// - /// An exception was thrown during boot. - /// - BootFailedOnException, + /// + /// An exception was thrown during boot. + /// + BootFailedOnException, - /// - /// Umbraco is not installed at all. - /// - InstallNoVersion, + /// + /// Umbraco is not installed at all. + /// + InstallNoVersion, - /// - /// A version is specified in web.config but the database is not configured. - /// - /// This is a weird state. - InstallNoDatabase, + /// + /// A version is specified in web.config but the database is not configured. + /// + /// This is a weird state. + InstallNoDatabase, - /// - /// A version is specified in web.config and a database is configured, but the - /// database is missing, and installing over a missing database has been enabled. - /// - InstallMissingDatabase, + /// + /// A version is specified in web.config and a database is configured, but the + /// database is missing, and installing over a missing database has been enabled. + /// + InstallMissingDatabase, - /// - /// A version is specified in web.config and a database is configured, but the - /// database is empty, and installing over an empty database has been enabled. - /// - InstallEmptyDatabase, + /// + /// A version is specified in web.config and a database is configured, but the + /// database is empty, and installing over an empty database has been enabled. + /// + InstallEmptyDatabase, - /// - /// Umbraco runs an old version. - /// - UpgradeOldVersion, + /// + /// Umbraco runs an old version. + /// + UpgradeOldVersion, - /// - /// Umbraco runs the current version but some migrations have not run. - /// - UpgradeMigrations, + /// + /// Umbraco runs the current version but some migrations have not run. + /// + UpgradeMigrations, - /// - /// Umbraco runs the current version but some package migrations have not run. - /// - UpgradePackageMigrations, + /// + /// Umbraco runs the current version but some package migrations have not run. + /// + UpgradePackageMigrations, - /// - /// Umbraco is running. - /// - Run - } + /// + /// Umbraco is running. + /// + Run, } diff --git a/src/Umbraco.Core/Scoping/ICoreScope.cs b/src/Umbraco.Core/Scoping/ICoreScope.cs index 8bb85ca29d..fe2a9489f3 100644 --- a/src/Umbraco.Core/Scoping/ICoreScope.cs +++ b/src/Umbraco.Core/Scoping/ICoreScope.cs @@ -1,57 +1,64 @@ -using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; namespace Umbraco.Cms.Core.Scoping; /// -/// Represents a scope. +/// Represents a scope. /// public interface ICoreScope : IDisposable, IInstanceIdentifiable { /// - /// Gets the scope notification publisher + /// Gets the distance from the root scope. + /// + /// + /// A zero represents a root scope, any value greater than zero represents a child scope. + /// + public int Depth => -1; + + /// + /// Gets the scope notification publisher /// IScopedNotificationPublisher Notifications { get; } /// - /// Gets the repositories cache mode. + /// Gets the repositories cache mode. /// RepositoryCacheMode RepositoryCacheMode { get; } /// - /// Gets the scope isolated cache. + /// Gets the scope isolated cache. /// IsolatedCaches IsolatedCaches { get; } /// - /// Completes the scope. + /// Completes the scope. /// /// A value indicating whether the scope has been successfully completed. /// Can return false if any child scope has not completed. bool Complete(); /// - /// Read-locks some lock objects. + /// Read-locks some lock objects. /// /// Array of lock object identifiers. void ReadLock(params int[] lockIds); /// - /// Write-locks some lock objects. + /// Write-locks some lock objects. /// /// Array of object identifiers. void WriteLock(params int[] lockIds); /// - /// Write-locks some lock objects. + /// Write-locks some lock objects. /// /// The database timeout in milliseconds /// The lock object identifier. void WriteLock(TimeSpan timeout, int lockId); /// - /// Read-locks some lock objects. + /// Read-locks some lock objects. /// /// The database timeout in milliseconds /// The lock object identifier. diff --git a/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs b/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs index d4fe496d38..792673453f 100644 --- a/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs @@ -2,49 +2,50 @@ using System.Data; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Scoping +namespace Umbraco.Cms.Core.Scoping; + +/// +/// Provides scopes. +/// +public interface ICoreScopeProvider { /// - /// Provides scopes. + /// Gets the scope context. /// - public interface ICoreScopeProvider - { - /// - /// Creates an ambient scope. - /// - /// The transaction isolation level. - /// The repositories cache mode. - /// An optional events dispatcher. - /// An optional notification publisher. - /// A value indicating whether to scope the filesystems. - /// A value indicating whether this scope should always be registered in the call context. - /// A value indicating whether this scope is auto-completed. - /// The created ambient scope. - /// - /// The created scope becomes the ambient scope. - /// If an ambient scope already exists, it becomes the parent of the created scope. - /// When the created scope is disposed, the parent scope becomes the ambient scope again. - /// Parameters must be specified on the outermost scope, or must be compatible with the parents. - /// Auto-completed scopes should be used for read-only operations ONLY. Do not use them if you do not - /// understand the associated issues, such as the scope being completed even though an exception is thrown. - /// - ICoreScope CreateCoreScope( - IsolationLevel isolationLevel = IsolationLevel.Unspecified, - RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher? eventDispatcher = null, - IScopedNotificationPublisher? scopedNotificationPublisher = null, - bool? scopeFileSystems = null, - bool callContext = false, - bool autoComplete = false); + IScopeContext? Context { get; } - /// - /// Gets the scope context. - /// - IScopeContext? Context { get; } + /// + /// Creates an ambient scope. + /// + /// The transaction isolation level. + /// The repositories cache mode. + /// An optional events dispatcher. + /// An optional notification publisher. + /// A value indicating whether to scope the filesystems. + /// A value indicating whether this scope should always be registered in the call context. + /// A value indicating whether this scope is auto-completed. + /// The created ambient scope. + /// + /// The created scope becomes the ambient scope. + /// If an ambient scope already exists, it becomes the parent of the created scope. + /// When the created scope is disposed, the parent scope becomes the ambient scope again. + /// Parameters must be specified on the outermost scope, or must be compatible with the parents. + /// + /// Auto-completed scopes should be used for read-only operations ONLY. Do not use them if you do not + /// understand the associated issues, such as the scope being completed even though an exception is thrown. + /// + /// + ICoreScope CreateCoreScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher? eventDispatcher = null, + IScopedNotificationPublisher? scopedNotificationPublisher = null, + bool? scopeFileSystems = null, + bool callContext = false, + bool autoComplete = false); - /// - /// Creates an instance of - /// - IQuery CreateQuery(); - } + /// + /// Creates an instance of + /// + IQuery CreateQuery(); } diff --git a/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs index 9d0bc9ceef..1942ecdc43 100644 --- a/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs +++ b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs @@ -1,16 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Scoping +/// +/// Exposes an instance unique identifier. +/// +public interface IInstanceIdentifiable { /// - /// Exposes an instance unique identifier. + /// Gets the instance unique identifier. /// - public interface IInstanceIdentifiable - { - /// - /// Gets the instance unique identifier. - /// - Guid InstanceId { get; } - int CreatedThreadId { get; } - } + Guid InstanceId { get; } + + int CreatedThreadId { get; } } diff --git a/src/Umbraco.Core/Scoping/IScopeContext.cs b/src/Umbraco.Core/Scoping/IScopeContext.cs index 7f1302911a..26f17b31b0 100644 --- a/src/Umbraco.Core/Scoping/IScopeContext.cs +++ b/src/Umbraco.Core/Scoping/IScopeContext.cs @@ -1,52 +1,53 @@ -using System; +namespace Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Scoping +/// +/// Represents a scope context. +/// +/// +/// A scope context can enlist objects that will be attached to the scope, and available +/// for the duration of the scope. In addition, it can enlist actions, that will run when the +/// scope is exiting, and after the database transaction has been committed. +/// +public interface IScopeContext : IInstanceIdentifiable { /// - /// Represents a scope context. + /// Enlists an action. /// - /// A scope context can enlist objects that will be attached to the scope, and available - /// for the duration of the scope. In addition, it can enlist actions, that will run when the - /// scope is exiting, and after the database transaction has been committed. - public interface IScopeContext : IInstanceIdentifiable - { - /// - /// Enlists an action. - /// - /// The action unique identifier. - /// The action. - /// The optional action priority (default is 100, lower runs first). - /// - /// It is ok to enlist multiple action with the same key but only the first one will run. - /// The action boolean parameter indicates whether the scope completed or not. - /// - void Enlist(string key, Action action, int priority = 100); + /// The action unique identifier. + /// The action. + /// The optional action priority (default is 100, lower runs first). + /// + /// It is ok to enlist multiple action with the same key but only the first one will run. + /// The action boolean parameter indicates whether the scope completed or not. + /// + void Enlist(string key, Action action, int priority = 100); - /// - /// Enlists an object and action. - /// - /// The type of the object. - /// The object unique identifier. - /// A function providing the object. - /// The optional action. - /// The optional action priority (default is 100, lower runs first). - /// The object. - /// - /// On the first time an object is enlisted with a given key, the object is actually - /// created. Next calls just return the existing object. It is ok to enlist multiple objects - /// and action with the same key but only the first one is used, the others are ignored. - /// The action boolean parameter indicates whether the scope completed or not. - /// - T? Enlist(string key, Func creator, Action? action = null, int priority = 100); + /// + /// Enlists an object and action. + /// + /// The type of the object. + /// The object unique identifier. + /// A function providing the object. + /// The optional action. + /// The optional action priority (default is 100, lower runs first). + /// The object. + /// + /// + /// On the first time an object is enlisted with a given key, the object is actually + /// created. Next calls just return the existing object. It is ok to enlist multiple objects + /// and action with the same key but only the first one is used, the others are ignored. + /// + /// The action boolean parameter indicates whether the scope completed or not. + /// + T? Enlist(string key, Func creator, Action? action = null, int priority = 100); - /// - /// Gets an enlisted object. - /// - /// The type of the object. - /// The object unique identifier. - /// The enlisted object, if any, else the default value. - T? GetEnlisted(string key); + /// + /// Gets an enlisted object. + /// + /// The type of the object. + /// The object unique identifier. + /// The enlisted object, if any, else the default value. + T? GetEnlisted(string key); - void ScopeExit(bool completed); - } + void ScopeExit(bool completed); } diff --git a/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs index 78c50b628f..75361726f3 100644 --- a/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs +++ b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs @@ -1,36 +1,35 @@ -namespace Umbraco.Cms.Core.Scoping +namespace Umbraco.Cms.Core.Scoping; + +/// +/// Specifies the cache mode of repositories. +/// +public enum RepositoryCacheMode { /// - /// Specifies the cache mode of repositories. + /// Unspecified. /// - public enum RepositoryCacheMode - { - /// - /// Unspecified. - /// - Unspecified = 0, + Unspecified = 0, - /// - /// Default, full L2 cache. - /// - Default = 1, + /// + /// Default, full L2 cache. + /// + Default = 1, - /// - /// Scoped cache. - /// - /// - /// Reads from, and writes to, a scope-local cache. - /// Upon scope completion, clears the global L2 cache. - /// - Scoped = 2, + /// + /// Scoped cache. + /// + /// + /// Reads from, and writes to, a scope-local cache. + /// Upon scope completion, clears the global L2 cache. + /// + Scoped = 2, - /// - /// No cache. - /// - /// - /// Bypasses caches entirely. - /// Upon scope completion, clears the global L2 cache. - /// - None = 3 - } + /// + /// No cache. + /// + /// + /// Bypasses caches entirely. + /// Upon scope completion, clears the global L2 cache. + /// + None = 3, } diff --git a/src/Umbraco.Core/Sections/ContentSection.cs b/src/Umbraco.Core/Sections/ContentSection.cs index 828adea295..f8d46747b1 100644 --- a/src/Umbraco.Core/Sections/ContentSection.cs +++ b/src/Umbraco.Core/Sections/ContentSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office content section - /// - public class ContentSection : ISection - { - /// - public string Alias => Constants.Applications.Content; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Content"; - } +/// +/// Defines the back office content section +/// +public class ContentSection : ISection +{ + /// + public string Alias => Constants.Applications.Content; + + /// + public string Name => "Content"; } diff --git a/src/Umbraco.Core/Sections/FormsSection.cs b/src/Umbraco.Core/Sections/FormsSection.cs index e0fd1085ee..3ac36e4732 100644 --- a/src/Umbraco.Core/Sections/FormsSection.cs +++ b/src/Umbraco.Core/Sections/FormsSection.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office media section +/// +public class FormsSection : ISection { - /// - /// Defines the back office media section - /// - public class FormsSection : ISection - { - public string Alias => Constants.Applications.Forms; - public string Name => "Forms"; - } + public string Alias => Constants.Applications.Forms; + + public string Name => "Forms"; } diff --git a/src/Umbraco.Core/Sections/ISection.cs b/src/Umbraco.Core/Sections/ISection.cs index bbd380f57e..565955dfe9 100644 --- a/src/Umbraco.Core/Sections/ISection.cs +++ b/src/Umbraco.Core/Sections/ISection.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines a back office section. +/// +public interface ISection { /// - /// Defines a back office section. + /// Gets the alias of the section. /// - public interface ISection - { - /// - /// Gets the alias of the section. - /// - string Alias { get; } + string Alias { get; } - /// - /// Gets the name of the section. - /// - string Name { get; } - } + /// + /// Gets the name of the section. + /// + string Name { get; } } diff --git a/src/Umbraco.Core/Sections/MediaSection.cs b/src/Umbraco.Core/Sections/MediaSection.cs index 8732556a28..f5fd0a79b7 100644 --- a/src/Umbraco.Core/Sections/MediaSection.cs +++ b/src/Umbraco.Core/Sections/MediaSection.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office media section +/// +public class MediaSection : ISection { - /// - /// Defines the back office media section - /// - public class MediaSection : ISection - { - public string Alias => Constants.Applications.Media; - public string Name => "Media"; - } + public string Alias => Constants.Applications.Media; + + public string Name => "Media"; } diff --git a/src/Umbraco.Core/Sections/MembersSection.cs b/src/Umbraco.Core/Sections/MembersSection.cs index 1edbf12604..a2e98ac871 100644 --- a/src/Umbraco.Core/Sections/MembersSection.cs +++ b/src/Umbraco.Core/Sections/MembersSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office members section - /// - public class MembersSection : ISection - { - /// - public string Alias => Constants.Applications.Members; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Members"; - } +/// +/// Defines the back office members section +/// +public class MembersSection : ISection +{ + /// + public string Alias => Constants.Applications.Members; + + /// + public string Name => "Members"; } diff --git a/src/Umbraco.Core/Sections/PackagesSection.cs b/src/Umbraco.Core/Sections/PackagesSection.cs index 4852c11397..d65acfccec 100644 --- a/src/Umbraco.Core/Sections/PackagesSection.cs +++ b/src/Umbraco.Core/Sections/PackagesSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office packages section - /// - public class PackagesSection : ISection - { - /// - public string Alias => Constants.Applications.Packages; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Packages"; - } +/// +/// Defines the back office packages section +/// +public class PackagesSection : ISection +{ + /// + public string Alias => Constants.Applications.Packages; + + /// + public string Name => "Packages"; } diff --git a/src/Umbraco.Core/Sections/SectionCollection.cs b/src/Umbraco.Core/Sections/SectionCollection.cs index 5ff0157d14..83169a390d 100644 --- a/src/Umbraco.Core/Sections/SectionCollection.cs +++ b/src/Umbraco.Core/Sections/SectionCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +public class SectionCollection : BuilderCollectionBase { - public class SectionCollection : BuilderCollectionBase + public SectionCollection(Func> items) + : base(items) { - public SectionCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs index 219d634261..7644b1cc8c 100644 --- a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs +++ b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs @@ -1,24 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +public class + SectionCollectionBuilder : OrderedCollectionBuilderBase { - public class SectionCollectionBuilder : OrderedCollectionBuilderBase + protected override SectionCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) { - protected override SectionCollectionBuilder This => this; + // get the manifest parser just-in-time - injecting it in the ctor would mean that + // simply getting the builder in order to configure the collection, would require + // its dependencies too, and that can create cycles or other oddities + IManifestParser manifestParser = factory.GetRequiredService(); - protected override IEnumerable CreateItems(IServiceProvider factory) - { - // get the manifest parser just-in-time - injecting it in the ctor would mean that - // simply getting the builder in order to configure the collection, would require - // its dependencies too, and that can create cycles or other oddities - var manifestParser = factory.GetRequiredService(); - - return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.Sections); - } + return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.Sections); } } diff --git a/src/Umbraco.Core/Sections/SettingsSection.cs b/src/Umbraco.Core/Sections/SettingsSection.cs index bc0a43cae1..3fe825c70d 100644 --- a/src/Umbraco.Core/Sections/SettingsSection.cs +++ b/src/Umbraco.Core/Sections/SettingsSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office settings section - /// - public class SettingsSection : ISection - { - /// - public string Alias => Constants.Applications.Settings; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Settings"; - } +/// +/// Defines the back office settings section +/// +public class SettingsSection : ISection +{ + /// + public string Alias => Constants.Applications.Settings; + + /// + public string Name => "Settings"; } diff --git a/src/Umbraco.Core/Sections/TranslationSection.cs b/src/Umbraco.Core/Sections/TranslationSection.cs index d739757e93..d11391c811 100644 --- a/src/Umbraco.Core/Sections/TranslationSection.cs +++ b/src/Umbraco.Core/Sections/TranslationSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office translation section - /// - public class TranslationSection : ISection - { - /// - public string Alias => Constants.Applications.Translation; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Translation"; - } +/// +/// Defines the back office translation section +/// +public class TranslationSection : ISection +{ + /// + public string Alias => Constants.Applications.Translation; + + /// + public string Name => "Translation"; } diff --git a/src/Umbraco.Core/Sections/UsersSection.cs b/src/Umbraco.Core/Sections/UsersSection.cs index 6969e9be3d..cea5047c81 100644 --- a/src/Umbraco.Core/Sections/UsersSection.cs +++ b/src/Umbraco.Core/Sections/UsersSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office users section - /// - public class UsersSection : ISection - { - /// - public string Alias => Constants.Applications.Users; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Users"; - } +/// +/// Defines the back office users section +/// +public class UsersSection : ISection +{ + /// + public string Alias => Constants.Applications.Users; + + /// + public string Name => "Users"; } diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 187d44b05d..2b8294e8db 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -4,32 +4,31 @@ using System.Globalization; using System.Security.Claims; using System.Security.Principal; -using System.Threading; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class AuthenticationExtensions { - public static class AuthenticationExtensions + /// + /// Ensures that the thread culture is set based on the back office user's culture + /// + public static void EnsureCulture(this IIdentity identity) { - /// - /// Ensures that the thread culture is set based on the back office user's culture - /// - public static void EnsureCulture(this IIdentity identity) + CultureInfo? culture = GetCulture(identity); + if (!(culture is null)) { - var culture = GetCulture(identity); - if (!(culture is null)) - { - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; - } - } - - public static CultureInfo? GetCulture(this IIdentity identity) - { - if (identity is ClaimsIdentity umbIdentity && umbIdentity.VerifyBackOfficeIdentity(out _) && umbIdentity.IsAuthenticated && umbIdentity.GetCultureString() is not null) - { - return CultureInfo.GetCultureInfo(umbIdentity.GetCultureString()!); - } - - return null; + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; } } + + public static CultureInfo? GetCulture(this IIdentity identity) + { + if (identity is ClaimsIdentity umbIdentity && umbIdentity.VerifyBackOfficeIdentity(out _) && + umbIdentity.IsAuthenticated && umbIdentity.GetCultureString() is not null) + { + return CultureInfo.GetCultureInfo(umbIdentity.GetCultureString()!); + } + + return null; + } } diff --git a/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs b/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs index c79fa87429..cece444588 100644 --- a/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs +++ b/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs @@ -1,22 +1,19 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public class BackOfficeExternalLoginProviderErrors { - public class BackOfficeExternalLoginProviderErrors + // required for deserialization + public BackOfficeExternalLoginProviderErrors() { - // required for deserialization - public BackOfficeExternalLoginProviderErrors() - { - } - - public BackOfficeExternalLoginProviderErrors(string? authenticationType, IEnumerable errors) - { - AuthenticationType = authenticationType; - Errors = errors ?? Enumerable.Empty(); - } - - public string? AuthenticationType { get; set; } - public IEnumerable? Errors { get; set; } } + + public BackOfficeExternalLoginProviderErrors(string? authenticationType, IEnumerable errors) + { + AuthenticationType = authenticationType; + Errors = errors ?? Enumerable.Empty(); + } + + public string? AuthenticationType { get; set; } + + public IEnumerable? Errors { get; set; } } diff --git a/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs b/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs index e4eacaf9d6..913f4c6dde 100644 --- a/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs +++ b/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Identity; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Identity options specifically for the back office identity implementation +/// +public class BackOfficeIdentityOptions : IdentityOptions { - /// - /// Identity options specifically for the back office identity implementation - /// - public class BackOfficeIdentityOptions : IdentityOptions - { - } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs index a59c1fb435..5466642a14 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// The result returned from the IBackOfficeUserPasswordChecker +/// +public enum BackOfficeUserPasswordCheckerResult { - /// - /// The result returned from the IBackOfficeUserPasswordChecker - /// - public enum BackOfficeUserPasswordCheckerResult - { - ValidCredentials, - InvalidCredentials, - FallbackToDefaultChecker - } + ValidCredentials, + InvalidCredentials, + FallbackToDefaultChecker, } diff --git a/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs index b9912a3911..4cb9e20dac 100644 --- a/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs +++ b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs @@ -1,91 +1,99 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Globalization; -using System.Linq; using System.Security.Claims; using System.Security.Principal; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ClaimsPrincipalExtensions { - public static class ClaimsPrincipalExtensions + public static bool IsBackOfficeAuthenticationType(this ClaimsIdentity? claimsIdentity) { - - public static bool IsBackOfficeAuthenticationType(this ClaimsIdentity claimsIdentity) + if (claimsIdentity is null) { - if (claimsIdentity is null) - { - return false; - } - - return claimsIdentity.IsAuthenticated && claimsIdentity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType; + return false; } - /// - /// This will return the current back office identity if the IPrincipal is the correct type and authenticated. - /// - /// - /// - public static ClaimsIdentity? GetUmbracoIdentity(this IPrincipal principal) - { - //If it's already a UmbracoBackOfficeIdentity - if (principal.Identity is ClaimsIdentity claimsIdentity - && claimsIdentity.IsBackOfficeAuthenticationType() - && claimsIdentity.VerifyBackOfficeIdentity(out var backOfficeIdentity)) - { - return backOfficeIdentity; - } - //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that - // We can have assigned more identities if it is a preview request. - if (principal is ClaimsPrincipal claimsPrincipal ) + return claimsIdentity.IsAuthenticated && + claimsIdentity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType; + } + + /// + /// This will return the current back office identity if the IPrincipal is the correct type and authenticated. + /// + /// + /// + public static ClaimsIdentity? GetUmbracoIdentity(this IPrincipal principal) + { + // If it's already a UmbracoBackOfficeIdentity + if (principal.Identity is ClaimsIdentity claimsIdentity + && claimsIdentity.IsBackOfficeAuthenticationType() + && claimsIdentity.VerifyBackOfficeIdentity(out ClaimsIdentity? backOfficeIdentity)) + { + return backOfficeIdentity; + } + + // Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that + // We can have assigned more identities if it is a preview request. + if (principal is ClaimsPrincipal claimsPrincipal) + { + ClaimsIdentity? identity = + claimsPrincipal.Identities.FirstOrDefault(x => x.IsBackOfficeAuthenticationType()); + if (identity is not null) { - var identity = claimsPrincipal.Identities.FirstOrDefault(x => x.IsBackOfficeAuthenticationType()); - if (identity is not null) + claimsIdentity = identity; + if (claimsIdentity.VerifyBackOfficeIdentity(out backOfficeIdentity)) { - claimsIdentity = identity; - if (claimsIdentity.VerifyBackOfficeIdentity(out backOfficeIdentity)) - { - return backOfficeIdentity; - } + return backOfficeIdentity; } } - - //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd - if (principal.Identity is ClaimsIdentity claimsIdentity2 - && claimsIdentity2.VerifyBackOfficeIdentity(out backOfficeIdentity)) - { - return backOfficeIdentity; - } - return null; } - /// - /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication - /// - /// - /// - public static double GetRemainingAuthSeconds(this IPrincipal user) => user.GetRemainingAuthSeconds(DateTimeOffset.UtcNow); - - /// - /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication - /// - /// - /// - /// - public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now) + // Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd + if (principal.Identity is ClaimsIdentity claimsIdentity2 + && claimsIdentity2.VerifyBackOfficeIdentity(out backOfficeIdentity)) { - var claimsPrincipal = user as ClaimsPrincipal; - if (claimsPrincipal == null) return 0; - - var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value; - if (ticketExpires.IsNullOrWhiteSpace()) return 0; - - var utcExpired = DateTimeOffset.Parse(ticketExpires!, null, DateTimeStyles.RoundtripKind); - - var secondsRemaining = utcExpired.Subtract(now).TotalSeconds; - return secondsRemaining; + return backOfficeIdentity; } + + return null; + } + + /// + /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig + /// authentication + /// + /// + /// + public static double GetRemainingAuthSeconds(this IPrincipal user) => + user.GetRemainingAuthSeconds(DateTimeOffset.UtcNow); + + /// + /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig + /// authentication + /// + /// + /// + /// + public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now) + { + if (user is not ClaimsPrincipal claimsPrincipal) + { + return 0; + } + + var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value; + if (ticketExpires.IsNullOrWhiteSpace()) + { + return 0; + } + + var utcExpired = DateTimeOffset.Parse(ticketExpires!, null, DateTimeStyles.RoundtripKind); + + var secondsRemaining = utcExpired.Subtract(now).TotalSeconds; + return secondsRemaining; } } diff --git a/src/Umbraco.Core/Security/ContentPermissions.cs b/src/Umbraco.Core/Security/ContentPermissions.cs index 73f9f4ccef..db27d100c6 100644 --- a/src/Umbraco.Core/Security/ContentPermissions.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -1,290 +1,356 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Checks user access to content +/// +public class ContentPermissions { + private readonly AppCaches _appCaches; - /// - /// Checks user access to content - /// - public class ContentPermissions + public enum ContentAccess { - private readonly IUserService _userService; - private readonly IContentService _contentService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; + Granted, + Denied, + NotFound, + } - public enum ContentAccess + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + + public ContentPermissions( + IUserService userService, + IContentService contentService, + IEntityService entityService, + AppCaches appCaches) + { + _userService = userService; + _contentService = contentService; + _entityService = entityService; + _appCaches = appCaches; + } + + public static bool HasPathAccess(string? path, int[]? startNodeIds, int recycleBinId) + { + if (string.IsNullOrWhiteSpace(path)) { - Granted, - Denied, - NotFound + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); } - public ContentPermissions( - IUserService userService, - IContentService contentService, - IEntityService entityService, - AppCaches appCaches) + // check for no access + if (startNodeIds is null || startNodeIds.Length == 0) { - _userService = userService; - _contentService = contentService; - _entityService = entityService; - _appCaches = appCaches; - } - - public ContentAccess CheckPermissions( - IContent content, - IUser user, - char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); - - public ContentAccess CheckPermissions( - IContent? content, - IUser? user, - IReadOnlyList permissionsToCheck) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (content == null) return ContentAccess.NotFound; - - var hasPathAccess = user.HasPathAccess(content, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(content.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - public ContentAccess CheckPermissions( - IUmbracoEntity entity, - IUser? user, - char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); - - public ContentAccess CheckPermissions( - IUmbracoEntity entity, - IUser? user, - IReadOnlyList permissionsToCheck) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (entity == null) return ContentAccess.NotFound; - - var hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - /// - /// Checks if the user has access to the specified node and permissions set - /// - /// - /// - /// - /// - /// The item resolved if one was found for the id - /// - /// - public ContentAccess CheckPermissions( - int nodeId, - IUser user, - out IUmbracoEntity? entity, - IReadOnlyList? permissionsToCheck = null) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - bool? hasPathAccess = null; - entity = null; - - if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); - else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); - - if (hasPathAccess.HasValue) - return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - - entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); - if (entity == null) return ContentAccess.NotFound; - hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - /// - /// Checks if the user has access to the specified node and permissions set - /// - /// - /// - /// - /// - /// - /// The item resolved if one was found for the id - /// - /// - public ContentAccess CheckPermissions( - int nodeId, - IUser? user, - out IContent? contentItem, - IReadOnlyList? permissionsToCheck = null) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - bool? hasPathAccess = null; - contentItem = null; - - if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); - else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); - - if (hasPathAccess.HasValue) - return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - - contentItem = _contentService.GetById(nodeId); - if (contentItem == null) return ContentAccess.NotFound; - hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) - { - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - //get the implicit/inherited permissions for the user for this path, - //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var permission = _userService.GetPermissionsForPath(user, path); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - public static bool HasPathAccess(string? path, int[]? startNodeIds, int recycleBinId) - { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - // check for no access - if (startNodeIds is null || startNodeIds.Length == 0) - return false; - - // check for root access - if (startNodeIds.Contains(Constants.System.Root)) - return true; - - var formattedPath = string.Concat(",", path, ","); - - // only users with root access have access to the recycle bin, - // if the above check didn't pass then access is denied - if (formattedPath.Contains(string.Concat(",", recycleBinId.ToString(CultureInfo.InvariantCulture), ","))) - return false; - - // check for a start node in the path - return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x.ToString(CultureInfo.InvariantCulture), ","))); - } - - public static bool IsInBranchOfStartNode(string path, int[]? startNodeIds, string[]? startNodePaths, out bool hasPathAccess) - { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - hasPathAccess = false; - - // check for no access - if (startNodeIds?.Length == 0) - return false; - - // check for root access - if (startNodeIds?.Contains(Constants.System.Root) ?? false) - { - hasPathAccess = true; - return true; - } - - //is it self? - var self = startNodePaths?.Any(x => x == path) ?? false; - if (self) - { - hasPathAccess = true; - return true; - } - - //is it ancestor? - var ancestor = startNodePaths?.Any(x => x.StartsWith(path)) ?? false; - if (ancestor) - { - //hasPathAccess = false; - return true; - } - - //is it descendant? - var descendant = startNodePaths?.Any(x => path.StartsWith(x)) ?? false; - if (descendant) - { - hasPathAccess = true; - return true; - } - return false; } + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + { + return true; + } + + var formattedPath = string.Concat(",", path, ","); + + // only users with root access have access to the recycle bin, + // if the above check didn't pass then access is denied + if (formattedPath.Contains(string.Concat(",", recycleBinId.ToString(CultureInfo.InvariantCulture), ","))) + { + return false; + } + + // check for a start node in the path + return startNodeIds.Any(x => + formattedPath.Contains(string.Concat(",", x.ToString(CultureInfo.InvariantCulture), ","))); + } + + public ContentAccess CheckPermissions( + IContent content, + IUser user, + char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); + + public ContentAccess CheckPermissions( + IContent? content, + IUser? user, + IReadOnlyList permissionsToCheck) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (content == null) + { + return ContentAccess.NotFound; + } + + var hasPathAccess = user.HasPathAccess(content, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(content.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + public ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser? user, + char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); + + public ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser? user, + IReadOnlyList permissionsToCheck) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (entity == null) + { + return ContentAccess.NotFound; + } + + var hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public ContentAccess CheckPermissions( + int nodeId, + IUser user, + out IUmbracoEntity? entity, + IReadOnlyList? permissionsToCheck = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } + + bool? hasPathAccess = null; + entity = null; + + if (nodeId == Constants.System.Root) + { + hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); + } + else if (nodeId == Constants.System.RecycleBinContent) + { + hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); + } + + if (hasPathAccess.HasValue) + { + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + } + + entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); + if (entity == null) + { + return ContentAccess.NotFound; + } + + hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public ContentAccess CheckPermissions( + int nodeId, + IUser? user, + out IContent? contentItem, + IReadOnlyList? permissionsToCheck = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } + + bool? hasPathAccess = null; + contentItem = null; + + if (nodeId == Constants.System.Root) + { + hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); + } + else if (nodeId == Constants.System.RecycleBinContent) + { + hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); + } + + if (hasPathAccess.HasValue) + { + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + } + + contentItem = _contentService.GetById(nodeId); + if (contentItem == null) + { + return ContentAccess.NotFound; + } + + hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) + { + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } + + // get the implicit/inherited permissions for the user for this path, + // if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + EntityPermissionSet permission = _userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + + return allowed; + } + + public static bool IsInBranchOfStartNode(string path, int[]? startNodeIds, string[]? startNodePaths, out bool hasPathAccess) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + } + + hasPathAccess = false; + + // check for no access + if (startNodeIds?.Length == 0) + { + return false; + } + + // check for root access + if (startNodeIds?.Contains(Constants.System.Root) ?? false) + { + hasPathAccess = true; + return true; + } + + // is it self? + var self = startNodePaths?.Any(x => x == path) ?? false; + if (self) + { + hasPathAccess = true; + return true; + } + + // is it ancestor? + var ancestor = startNodePaths?.Any(x => x.StartsWith(path)) ?? false; + if (ancestor) + { + // hasPathAccess = false; + return true; + } + + // is it descendant? + var descendant = startNodePaths?.Any(x => path.StartsWith(x)) ?? false; + if (descendant) + { + hasPathAccess = true; + return true; + } + + return false; } } diff --git a/src/Umbraco.Core/Security/ExternalLogin.cs b/src/Umbraco.Core/Security/ExternalLogin.cs index 631fe52b28..6eb3defc45 100644 --- a/src/Umbraco.Core/Security/ExternalLogin.cs +++ b/src/Umbraco.Core/Security/ExternalLogin.cs @@ -1,28 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +/// +public class ExternalLogin : IExternalLogin { + /// + /// Initializes a new instance of the class. + /// + public ExternalLogin(string loginProvider, string providerKey, string? userData = null) + { + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + ProviderKey = providerKey ?? throw new ArgumentNullException(nameof(providerKey)); + UserData = userData; + } /// - public class ExternalLogin : IExternalLogin - { - /// - /// Initializes a new instance of the class. - /// - public ExternalLogin(string loginProvider, string providerKey, string? userData = null) - { - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - ProviderKey = providerKey ?? throw new ArgumentNullException(nameof(providerKey)); - UserData = userData; - } + public string LoginProvider { get; } - /// - public string LoginProvider { get; } + /// + public string ProviderKey { get; } - /// - public string ProviderKey { get; } - - /// - public string? UserData { get; } - } + /// + public string? UserData { get; } } diff --git a/src/Umbraco.Core/Security/ExternalLoginToken.cs b/src/Umbraco.Core/Security/ExternalLoginToken.cs index 85089ddba6..df986d176f 100644 --- a/src/Umbraco.Core/Security/ExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/ExternalLoginToken.cs @@ -1,27 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +/// +public class ExternalLoginToken : IExternalLoginToken { - /// - public class ExternalLoginToken : IExternalLoginToken + /// + /// Initializes a new instance of the class. + /// + public ExternalLoginToken(string loginProvider, string name, string value) { - /// - /// Initializes a new instance of the class. - /// - public ExternalLoginToken(string loginProvider, string name, string value) - { - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); - } - - /// - public string LoginProvider { get; } - - /// - public string Name { get; } - - /// - public string Value { get; } + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); } + + /// + public string LoginProvider { get; } + + /// + public string Name { get; } + + /// + public string Value { get; } } diff --git a/src/Umbraco.Core/Security/IBackofficeSecurity.cs b/src/Umbraco.Core/Security/IBackofficeSecurity.cs index 3b3c956cd6..2de9104a95 100644 --- a/src/Umbraco.Core/Security/IBackofficeSecurity.cs +++ b/src/Umbraco.Core/Security/IBackofficeSecurity.cs @@ -1,44 +1,43 @@ using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IBackOfficeSecurity { - public interface IBackOfficeSecurity - { - /// - /// Gets the current user. - /// - /// The current user that has been authenticated for the request. - /// If authentication hasn't taken place this will be null. - // TODO: This is used a lot but most of it can be refactored to not use this at all since the IUser instance isn't - // needed in most cases. Where an IUser is required this could be an ext method on the ClaimsIdentity/ClaimsPrincipal that passes in - // an IUserService, like HttpContext.User.GetUmbracoUser(_userService); - // This one isn't as easy to remove as the others below. - IUser? CurrentUser { get; } + /// + /// Gets the current user. + /// + /// The current user that has been authenticated for the request. + /// If authentication hasn't taken place this will be null. + // TODO: This is used a lot but most of it can be refactored to not use this at all since the IUser instance isn't + // needed in most cases. Where an IUser is required this could be an ext method on the ClaimsIdentity/ClaimsPrincipal that passes in + // an IUserService, like HttpContext.User.GetUmbracoUser(_userService); + // This one isn't as easy to remove as the others below. + IUser? CurrentUser { get; } - /// - /// Gets the current user's id. - /// - /// The current user's Id that has been authenticated for the request. - /// If authentication hasn't taken place this will be unsuccessful. - // TODO: This should just be an extension method on ClaimsIdentity - Attempt GetUserId(); + /// + /// Gets the current user's id. + /// + /// The current user's Id that has been authenticated for the request. + /// If authentication hasn't taken place this will be unsuccessful. + // TODO: This should just be an extension method on ClaimsIdentity + Attempt GetUserId(); - /// - /// Checks if the specified user as access to the app - /// - /// - /// - /// - /// If authentication hasn't taken place this will be unsuccessful. - // TODO: Should be part of IBackOfficeUserManager - bool UserHasSectionAccess(string section, IUser user); + /// + /// Checks if the specified user as access to the app + /// + /// + /// + /// + /// If authentication hasn't taken place this will be unsuccessful. + // TODO: Should be part of IBackOfficeUserManager + bool UserHasSectionAccess(string section, IUser user); - /// - /// Ensures that a back office user is logged in - /// - /// - /// This does not force authentication, that must be done before calls to this are made. - // TODO: Should be removed, this should not be necessary - bool IsAuthenticated(); - } + /// + /// Ensures that a back office user is logged in + /// + /// + /// This does not force authentication, that must be done before calls to this are made. + // TODO: Should be removed, this should not be necessary + bool IsAuthenticated(); } diff --git a/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs b/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs index 7ef33ecdc6..11ed86971e 100644 --- a/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs +++ b/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IBackOfficeSecurityAccessor { - public interface IBackOfficeSecurityAccessor - { - IBackOfficeSecurity? BackOfficeSecurity { get; } - } + IBackOfficeSecurity? BackOfficeSecurity { get; } } diff --git a/src/Umbraco.Core/Security/IExternalLogin.cs b/src/Umbraco.Core/Security/IExternalLogin.cs index 0c09cecfc0..225b0390d3 100644 --- a/src/Umbraco.Core/Security/IExternalLogin.cs +++ b/src/Umbraco.Core/Security/IExternalLogin.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Used to persist external login data for a user +/// +public interface IExternalLogin { /// - /// Used to persist external login data for a user + /// Gets the login provider /// - public interface IExternalLogin - { - /// - /// Gets the login provider - /// - string LoginProvider { get; } + string LoginProvider { get; } - /// - /// Gets the provider key - /// - string ProviderKey { get; } + /// + /// Gets the provider key + /// + string ProviderKey { get; } - /// - /// Gets the user data - /// - string? UserData { get; } - } + /// + /// Gets the user data + /// + string? UserData { get; } } diff --git a/src/Umbraco.Core/Security/IExternalLoginToken.cs b/src/Umbraco.Core/Security/IExternalLoginToken.cs index b3fd4b64b2..a5dba5a17e 100644 --- a/src/Umbraco.Core/Security/IExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/IExternalLoginToken.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Used to persist an external login token for a user +/// +public interface IExternalLoginToken { /// - /// Used to persist an external login token for a user + /// Gets the login provider /// - public interface IExternalLoginToken - { - /// - /// Gets the login provider - /// - string LoginProvider { get; } + string LoginProvider { get; } - /// - /// Gets the name of the token - /// - string Name { get; } + /// + /// Gets the name of the token + /// + string Name { get; } - /// - /// Gets the value of the token - /// - string Value { get; } - } + /// + /// Gets the value of the token + /// + string Value { get; } } diff --git a/src/Umbraco.Core/Security/IHtmlSanitizer.cs b/src/Umbraco.Core/Security/IHtmlSanitizer.cs index 9bcfe405dd..3faf3cfd4d 100644 --- a/src/Umbraco.Core/Security/IHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/IHtmlSanitizer.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IHtmlSanitizer { - public interface IHtmlSanitizer - { - /// - /// Sanitizes HTML - /// - /// HTML to be sanitized - /// Sanitized HTML - string Sanitize(string html); - } + /// + /// Sanitizes HTML + /// + /// HTML to be sanitized + /// Sanitized HTML + string Sanitize(string html); } diff --git a/src/Umbraco.Core/Security/IIdentityUserLogin.cs b/src/Umbraco.Core/Security/IIdentityUserLogin.cs index c9eb64ceb3..51035b724c 100644 --- a/src/Umbraco.Core/Security/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IIdentityUserLogin.cs @@ -1,31 +1,30 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// An external login provider linked to a user +/// +/// The PK type for the user +public interface IIdentityUserLogin : IEntity, IRememberBeingDirty { /// - /// An external login provider linked to a user + /// Gets or sets the login provider for the login (i.e. Facebook, Google) /// - /// The PK type for the user - public interface IIdentityUserLogin : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the login provider for the login (i.e. Facebook, Google) - /// - string LoginProvider { get; set; } + string LoginProvider { get; set; } - /// - /// Gets or sets key representing the login for the provider - /// - string ProviderKey { get; set; } + /// + /// Gets or sets key representing the login for the provider + /// + string ProviderKey { get; set; } - /// - /// Gets or sets user or member key (Guid) for the user/member who owns this login - /// - string UserId { get; set; } // TODO: This should be able to be used by both users and members + /// + /// Gets or sets user or member key (Guid) for the user/member who owns this login + /// + string UserId { get; set; } // TODO: This should be able to be used by both users and members - /// - /// Gets or sets any arbitrary data for the user and external provider - /// - string? UserData { get; set; } - } + /// + /// Gets or sets any arbitrary data for the user and external provider + /// + string? UserData { get; set; } } diff --git a/src/Umbraco.Core/Security/IIdentityUserToken.cs b/src/Umbraco.Core/Security/IIdentityUserToken.cs index 0e7f22d72f..f2e17a19af 100644 --- a/src/Umbraco.Core/Security/IIdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IIdentityUserToken.cs @@ -1,30 +1,29 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// An external login provider token +/// +public interface IIdentityUserToken : IEntity { /// - /// An external login provider token + /// Gets or sets user Id for the user who owns this token /// - public interface IIdentityUserToken : IEntity - { - /// - /// Gets or sets user Id for the user who owns this token - /// - string? UserId { get; set; } + string? UserId { get; set; } - /// - /// Gets or sets the login provider for the login (i.e. Facebook, Google) - /// - string LoginProvider { get; set; } + /// + /// Gets or sets the login provider for the login (i.e. Facebook, Google) + /// + string LoginProvider { get; set; } - /// - /// Gets or sets the token name - /// - string Name { get; set; } + /// + /// Gets or sets the token name + /// + string Name { get; set; } - /// - /// Gets or set the token value - /// - string Value { get; set; } - } + /// + /// Gets or set the token value + /// + string Value { get; set; } } diff --git a/src/Umbraco.Core/Security/IPasswordHasher.cs b/src/Umbraco.Core/Security/IPasswordHasher.cs index c0d436048e..5f3345ea73 100644 --- a/src/Umbraco.Core/Security/IPasswordHasher.cs +++ b/src/Umbraco.Core/Security/IPasswordHasher.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IPasswordHasher { - public interface IPasswordHasher - { - /// - /// Hashes a password - /// - /// The password. - /// The password hashed. - string HashPassword(string password); - } + /// + /// Hashes a password + /// + /// The password. + /// The password hashed. + string HashPassword(string password); } diff --git a/src/Umbraco.Core/Security/IPublicAccessChecker.cs b/src/Umbraco.Core/Security/IPublicAccessChecker.cs index 6ec9eb7ade..d830d757f1 100644 --- a/src/Umbraco.Core/Security/IPublicAccessChecker.cs +++ b/src/Umbraco.Core/Security/IPublicAccessChecker.cs @@ -1,9 +1,6 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public interface IPublicAccessChecker { - public interface IPublicAccessChecker - { - Task HasMemberAccessToContentAsync(int publishedContentId); - } + Task HasMemberAccessToContentAsync(int publishedContentId); } diff --git a/src/Umbraco.Core/Security/ITwoFactorProvider.cs b/src/Umbraco.Core/Security/ITwoFactorProvider.cs index f0da6c314a..8d2b12b6f8 100644 --- a/src/Umbraco.Core/Security/ITwoFactorProvider.cs +++ b/src/Umbraco.Core/Security/ITwoFactorProvider.cs @@ -1,22 +1,15 @@ -using System; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public interface ITwoFactorProvider { - public interface ITwoFactorProvider - { - string ProviderName { get; } + string ProviderName { get; } - Task GetSetupDataAsync(Guid userOrMemberKey, string secret); - - bool ValidateTwoFactorPIN(string secret, string token); - - /// - /// - /// - /// Called to confirm the setup of two factor on the user. - bool ValidateTwoFactorSetup(string secret, string token); - } + Task GetSetupDataAsync(Guid userOrMemberKey, string secret); + bool ValidateTwoFactorPIN(string secret, string token); + /// + /// + /// Called to confirm the setup of two factor on the user. + bool ValidateTwoFactorSetup(string secret, string token); } diff --git a/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs index 225d46b268..83c11916b1 100644 --- a/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs @@ -1,88 +1,77 @@ -using System; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public enum AuditEvent { + AccountLocked, + AccountUnlocked, + ForgotPasswordRequested, + ForgotPasswordChangedSuccess, + LoginFailed, + LoginRequiresVerification, + LoginSucces, + LogoutSuccess, + PasswordChanged, + PasswordReset, + ResetAccessFailedCount, + SendingUserInvite, +} + +/// +/// This class is used by events raised from the BackofficeUserManager +/// +public class IdentityAuditEventArgs : EventArgs +{ + /// + /// Default constructor + /// + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUser, string affectedUsername) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + IpAddress = ipAddress; + Comment = comment; + PerformingUser = performingUser; + AffectedUsername = affectedUsername; + AffectedUser = affectedUser; + } + + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername) + : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername) + { + } /// - /// This class is used by events raised from the BackofficeUserManager + /// The action that got triggered from the audit event /// - public class IdentityAuditEventArgs : EventArgs - { - /// - /// The action that got triggered from the audit event - /// - public AuditEvent Action { get; private set; } + public AuditEvent Action { get; } - /// - /// Current date/time in UTC format - /// - public DateTime DateTimeUtc { get; private set; } + /// + /// Current date/time in UTC format + /// + public DateTime DateTimeUtc { get; } - /// - /// The source IP address of the user performing the action - /// - public string IpAddress { get; private set; } + /// + /// The source IP address of the user performing the action + /// + public string IpAddress { get; } - /// - /// The user affected by the event raised - /// - public string AffectedUser { get; private set; } + /// + /// The user affected by the event raised + /// + public string AffectedUser { get; } - /// - /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 - /// - public string PerformingUser { get; private set; } + /// + /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 + /// + public string PerformingUser { get; } - /// - /// An optional comment about the action being logged - /// - public string Comment { get; private set; } + /// + /// An optional comment about the action being logged + /// + public string Comment { get; } - /// - /// This property is always empty except in the LoginFailed event for an unknown user trying to login - /// - public string AffectedUsername { get; private set; } - - - /// - /// Default constructor - /// - /// - /// - /// - /// - /// - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUser, string affectedUsername) - { - DateTimeUtc = DateTime.UtcNow; - Action = action; - IpAddress = ipAddress; - Comment = comment; - PerformingUser = performingUser; - AffectedUsername = affectedUsername; - AffectedUser = affectedUser; - } - - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername) - : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername) - { - } - - } - - public enum AuditEvent - { - AccountLocked, - AccountUnlocked, - ForgotPasswordRequested, - ForgotPasswordChangedSuccess, - LoginFailed, - LoginRequiresVerification, - LoginSucces, - LogoutSuccess, - PasswordChanged, - PasswordReset, - ResetAccessFailedCount, - SendingUserInvite - } + /// + /// This property is always empty except in the LoginFailed event for an unknown user trying to login + /// + public string AffectedUsername { get; } } diff --git a/src/Umbraco.Core/Security/IdentityUserLogin.cs b/src/Umbraco.Core/Security/IdentityUserLogin.cs index 402660ead9..ca821811cc 100644 --- a/src/Umbraco.Core/Security/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IdentityUserLogin.cs @@ -1,46 +1,43 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Entity type for a user's login (i.e. Facebook, Google) +/// +public class IdentityUserLogin : EntityBase, IIdentityUserLogin { + /// + /// Initializes a new instance of the class. + /// + public IdentityUserLogin(string loginProvider, string providerKey, string userId) + { + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + } /// - /// Entity type for a user's login (i.e. Facebook, Google) + /// Initializes a new instance of the class. /// - public class IdentityUserLogin : EntityBase, IIdentityUserLogin + public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate) { - /// - /// Initializes a new instance of the class. - /// - public IdentityUserLogin(string loginProvider, string providerKey, string userId) - { - LoginProvider = loginProvider; - ProviderKey = providerKey; - UserId = userId; - } - - /// - /// Initializes a new instance of the class. - /// - public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate) - { - Id = id; - LoginProvider = loginProvider; - ProviderKey = providerKey; - UserId = userId; - CreateDate = createDate; - } - - /// - public string LoginProvider { get; set; } - - /// - public string ProviderKey { get; set; } - - /// - public string UserId { get; set; } - - /// - public string? UserData { get; set; } + Id = id; + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + CreateDate = createDate; } + + /// + public string LoginProvider { get; set; } + + /// + public string ProviderKey { get; set; } + + /// + public string UserId { get; set; } + + /// + public string? UserData { get; set; } } diff --git a/src/Umbraco.Core/Security/IdentityUserToken.cs b/src/Umbraco.Core/Security/IdentityUserToken.cs index 014001a3a9..f4fcd46ace 100644 --- a/src/Umbraco.Core/Security/IdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IdentityUserToken.cs @@ -1,44 +1,42 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public class IdentityUserToken : EntityBase, IIdentityUserToken { - public class IdentityUserToken : EntityBase, IIdentityUserToken + /// + /// Initializes a new instance of the class. + /// + public IdentityUserToken(string loginProvider, string? name, string? value, string? userId) { - /// - /// Initializes a new instance of the class. - /// - public IdentityUserToken(string loginProvider, string? name, string? value, string? userId) - { - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); - UserId = userId; - } - - /// - /// Initializes a new instance of the class. - /// - public IdentityUserToken(int id, string? loginProvider, string? name, string? value, string userId, DateTime createDate) - { - Id = id; - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); - UserId = userId; - CreateDate = createDate; - } - - /// - public string LoginProvider { get; set; } - - /// - public string Name { get; set; } - - /// - public string Value { get; set; } - - /// - public string? UserId { get; set; } + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + UserId = userId; } + + /// + /// Initializes a new instance of the class. + /// + public IdentityUserToken(int id, string? loginProvider, string? name, string? value, string userId, DateTime createDate) + { + Id = id; + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + UserId = userId; + CreateDate = createDate; + } + + /// + public string LoginProvider { get; set; } + + /// + public string Name { get; set; } + + /// + public string Value { get; set; } + + /// + public string? UserId { get; set; } } diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs index b8c7596b2d..3b53509240 100644 --- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs +++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs @@ -1,239 +1,241 @@ -using System; using System.ComponentModel; using System.Security.Cryptography; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Handles password hashing and formatting for legacy hashing algorithms. +/// +/// +/// Should probably be internal. +/// +public class LegacyPasswordSecurity { + public static string GenerateSalt() + { + var numArray = new byte[16]; + new RNGCryptoServiceProvider().GetBytes(numArray); + return Convert.ToBase64String(numArray); + } + + // TODO: Remove v11 + // Used for tests + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] + public string HashPasswordForStorage(string algorithmType, string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException("password cannot be empty", nameof(password)); + } + + var hashed = HashNewPassword(algorithmType, password, out string salt); + return FormatPasswordForStorage(algorithmType, hashed, salt); + } + + // TODO: Remove v11 + // Used for tests + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] + public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt) + { + if (!SupportHashAlgorithm(algorithmType)) + { + throw new InvalidOperationException($"{algorithmType} is not supported"); + } + + return salt + hashedPassword; + } /// - /// Handles password hashing and formatting for legacy hashing algorithms. + /// Verifies if the password matches the expected hash+salt of the stored password string /// - /// - /// Should probably be internal. - /// - public class LegacyPasswordSecurity + /// The hashing algorithm for the stored password. + /// The password. + /// The value of the password stored in a data store. + /// + public bool VerifyPassword(string algorithm, string password, string dbPassword) { - // TODO: Remove v11 - // Used for tests - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] - public string HashPasswordForStorage(string algorithmType, string password) + if (string.IsNullOrWhiteSpace(dbPassword)) { - if (string.IsNullOrWhiteSpace(password)) - throw new ArgumentException("password cannot be empty", nameof(password)); - - string salt; - var hashed = HashNewPassword(algorithmType, password, out salt); - return FormatPasswordForStorage(algorithmType, hashed, salt); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); } - // TODO: Remove v11 - // Used for tests - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] - public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt) + if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) { - if (!SupportHashAlgorithm(algorithmType)) - { - throw new InvalidOperationException($"{algorithmType} is not supported"); - } - - return salt + hashedPassword; + return false; } - /// - /// Verifies if the password matches the expected hash+salt of the stored password string - /// - /// The hashing algorithm for the stored password. - /// The password. - /// The value of the password stored in a data store. - /// - public bool VerifyPassword(string algorithm, string password, string dbPassword) + try { - if (string.IsNullOrWhiteSpace(dbPassword)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); - } - - if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) - { - return false; - } - - try - { - var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt); - var hashed = HashPassword(algorithm, password, salt); - return storedHashedPass == hashed; - } - catch (ArgumentOutOfRangeException) - { - //This can happen if the length of the password is wrong and a salt cannot be extracted. - return false; - } + var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt); + var hashed = HashPassword(algorithm, password, salt); + return storedHashedPass == hashed; } - - /// - /// Verify a legacy hashed password (HMACSHA1) - /// - public bool VerifyLegacyHashedPassword(string password, string dbPassword) + catch (ArgumentOutOfRangeException) { - var hashAlgorithm = new HMACSHA1 - { - //the legacy salt was actually the password :( - Key = Encoding.Unicode.GetBytes(password) - }; - - var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password))); - - return dbPassword == hashed; - } - - /// - /// Create a new password hash and a new salt - /// - /// The hashing algorithm for the password. - /// - /// - /// - // TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage - // TODO: Remove v11 - [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] - public string HashNewPassword(string algorithm, string newPassword, out string salt) - { - salt = GenerateSalt(); - return HashPassword(algorithm, newPassword, salt); - } - - /// - /// Parses out the hashed password and the salt from the stored password string value - /// - /// The hashing algorithm for the stored password. - /// - /// returns the salt - /// - public string ParseStoredHashPassword(string algorithm, string storedString, out string salt) - { - if (string.IsNullOrWhiteSpace(storedString)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); - } - - if (!SupportHashAlgorithm(algorithm)) - { - throw new InvalidOperationException($"{algorithm} is not supported"); - } - - var saltLen = GenerateSalt(); - salt = storedString.Substring(0, saltLen.Length); - return storedString.Substring(saltLen.Length); - } - - public static string GenerateSalt() - { - var numArray = new byte[16]; - new RNGCryptoServiceProvider().GetBytes(numArray); - return Convert.ToBase64String(numArray); - } - - /// - /// Hashes a password with a given salt - /// - /// The hashing algorithm for the password. - /// - /// - /// - private string HashPassword(string algorithmType, string pass, string salt) - { - if (!SupportHashAlgorithm(algorithmType)) - { - throw new InvalidOperationException($"{algorithmType} is not supported"); - } - - // This is the correct way to implement this (as per the sql membership provider) - - var bytes = Encoding.Unicode.GetBytes(pass); - var saltBytes = Convert.FromBase64String(salt); - byte[] inArray; - - using var hashAlgorithm = GetHashAlgorithm(algorithmType); - var algorithm = hashAlgorithm as KeyedHashAlgorithm; - if (algorithm != null) - { - var keyedHashAlgorithm = algorithm; - if (keyedHashAlgorithm.Key.Length == saltBytes.Length) - { - //if the salt bytes is the required key length for the algorithm, use it as-is - keyedHashAlgorithm.Key = saltBytes; - } - else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) - { - //if the salt bytes is too long for the required key length for the algorithm, reduce it - var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; - Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); - keyedHashAlgorithm.Key = numArray2; - } - else - { - //if the salt bytes is too short for the required key length for the algorithm, extend it - var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; - var dstOffset = 0; - while (dstOffset < numArray2.Length) - { - var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); - Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); - dstOffset += count; - } - keyedHashAlgorithm.Key = numArray2; - } - inArray = keyedHashAlgorithm.ComputeHash(bytes); - } - else - { - var buffer = new byte[saltBytes.Length + bytes.Length]; - Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); - Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); - inArray = hashAlgorithm.ComputeHash(buffer); - } - - return Convert.ToBase64String(inArray); - } - - /// - /// Return the hash algorithm to use based on the - /// - /// The hashing algorithm name. - /// - /// - private HashAlgorithm GetHashAlgorithm(string algorithm) - { - if (algorithm.IsNullOrWhiteSpace()) - throw new InvalidOperationException("No hash algorithm type specified"); - - var alg = HashAlgorithm.Create(algorithm); - if (alg == null) - throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved"); - - return alg; - } - - public bool SupportHashAlgorithm(string algorithm) - { - // This is for the v6-v8 hashing algorithm - if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName)) - { - return true; - } - - // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes) - if (algorithm.InvariantEquals("SHA1")) - { - return true; - } - + // This can happen if the length of the password is wrong and a salt cannot be extracted. return false; } } + + /// + /// Verify a legacy hashed password (HMACSHA1) + /// + public bool VerifyLegacyHashedPassword(string password, string dbPassword) + { + var hashAlgorithm = new HMACSHA1 + { + // the legacy salt was actually the password :( + Key = Encoding.Unicode.GetBytes(password), + }; + + var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password))); + + return dbPassword == hashed; + } + + /// + /// Create a new password hash and a new salt + /// + /// The hashing algorithm for the password. + /// + /// + /// + // TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage + // TODO: Remove v11 + [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] + public string HashNewPassword(string algorithm, string newPassword, out string salt) + { + salt = GenerateSalt(); + return HashPassword(algorithm, newPassword, salt); + } + + /// + /// Parses out the hashed password and the salt from the stored password string value + /// + /// The hashing algorithm for the stored password. + /// + /// returns the salt + /// + public string ParseStoredHashPassword(string algorithm, string storedString, out string salt) + { + if (string.IsNullOrWhiteSpace(storedString)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); + } + + if (!SupportHashAlgorithm(algorithm)) + { + throw new InvalidOperationException($"{algorithm} is not supported"); + } + + var saltLen = GenerateSalt(); + salt = storedString.Substring(0, saltLen.Length); + return storedString.Substring(saltLen.Length); + } + + public bool SupportHashAlgorithm(string algorithm) + { + // This is for the v6-v8 hashing algorithm + if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName)) + { + return true; + } + + // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes) + if (algorithm.InvariantEquals("SHA1")) + { + return true; + } + + return false; + } + + /// + /// Hashes a password with a given salt + /// + /// The hashing algorithm for the password. + /// + /// + /// + private string HashPassword(string algorithmType, string pass, string salt) + { + if (!SupportHashAlgorithm(algorithmType)) + { + throw new InvalidOperationException($"{algorithmType} is not supported"); + } + + // This is the correct way to implement this (as per the sql membership provider) + var bytes = Encoding.Unicode.GetBytes(pass); + var saltBytes = Convert.FromBase64String(salt); + byte[] inArray; + + using HashAlgorithm hashAlgorithm = GetHashAlgorithm(algorithmType); + if (hashAlgorithm is KeyedHashAlgorithm algorithm) + { + KeyedHashAlgorithm keyedHashAlgorithm = algorithm; + if (keyedHashAlgorithm.Key.Length == saltBytes.Length) + { + // if the salt bytes is the required key length for the algorithm, use it as-is + keyedHashAlgorithm.Key = saltBytes; + } + else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) + { + // if the salt bytes is too long for the required key length for the algorithm, reduce it + var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; + Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); + keyedHashAlgorithm.Key = numArray2; + } + else + { + // if the salt bytes is too short for the required key length for the algorithm, extend it + var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; + var dstOffset = 0; + while (dstOffset < numArray2.Length) + { + var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); + Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); + dstOffset += count; + } + + keyedHashAlgorithm.Key = numArray2; + } + + inArray = keyedHashAlgorithm.ComputeHash(bytes); + } + else + { + var buffer = new byte[saltBytes.Length + bytes.Length]; + Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); + Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); + inArray = hashAlgorithm.ComputeHash(buffer); + } + + return Convert.ToBase64String(inArray); + } + + /// + /// Return the hash algorithm to use based on the + /// + /// The hashing algorithm name. + /// + /// + private HashAlgorithm GetHashAlgorithm(string algorithm) + { + if (algorithm.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("No hash algorithm type specified"); + } + + var alg = HashAlgorithm.Create(algorithm); + if (alg == null) + { + throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved"); + } + + return alg; + } } diff --git a/src/Umbraco.Core/Security/MediaPermissions.cs b/src/Umbraco.Core/Security/MediaPermissions.cs index d30ab90af2..c46d32f565 100644 --- a/src/Umbraco.Core/Security/MediaPermissions.cs +++ b/src/Umbraco.Core/Security/MediaPermissions.cs @@ -1,77 +1,84 @@ -using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Checks user access to media +/// +public class MediaPermissions { - /// - /// Checks user access to media - /// - public class MediaPermissions + private readonly AppCaches _appCaches; + + public enum MediaAccess { - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; + Granted, + Denied, + NotFound, + } - public enum MediaAccess + private readonly IEntityService _entityService; + private readonly IMediaService _mediaService; + + public MediaPermissions(IMediaService mediaService, IEntityService entityService, AppCaches appCaches) + { + _mediaService = mediaService; + _entityService = entityService; + _appCaches = appCaches; + } + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// + public MediaAccess CheckPermissions(IUser? user, int nodeId, out IMedia? media) + { + if (user == null) { - Granted, - Denied, - NotFound + throw new ArgumentNullException(nameof(user)); } - public MediaPermissions(IMediaService mediaService, IEntityService entityService, AppCaches appCaches) + media = null; + + if (nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) { - _mediaService = mediaService; - _entityService = entityService; - _appCaches = appCaches; + media = _mediaService.GetById(nodeId); } - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - public MediaAccess CheckPermissions(IUser? user, int nodeId, out IMedia? media) + if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) { - if (user == null) throw new ArgumentNullException(nameof(user)); - - media = null; - - if (nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - media = _mediaService.GetById(nodeId); - } - - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - return MediaAccess.NotFound; - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasMediaRootAccess(_entityService, _appCaches) - : (nodeId == Constants.System.RecycleBinMedia) - ? user.HasMediaBinAccess(_entityService, _appCaches) - : user.HasPathAccess(media, _entityService, _appCaches); - - return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + return MediaAccess.NotFound; } - public MediaAccess CheckPermissions(IMedia? media, IUser? user) + var hasPathAccess = nodeId == Constants.System.Root + ? user.HasMediaRootAccess(_entityService, _appCaches) + : nodeId == Constants.System.RecycleBinMedia + ? user.HasMediaBinAccess(_entityService, _appCaches) + : user.HasPathAccess(media, _entityService, _appCaches); + + return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + } + + public MediaAccess CheckPermissions(IMedia? media, IUser? user) + { + if (user == null) { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (media == null) return MediaAccess.NotFound; - - var hasPathAccess = user.HasPathAccess(media, _entityService, _appCaches); - - return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + throw new ArgumentNullException(nameof(user)); } + + if (media == null) + { + return MediaAccess.NotFound; + } + + var hasPathAccess = user.HasPathAccess(media, _entityService, _appCaches); + + return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; } } diff --git a/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs index 2ada23631a..5892f786a7 100644 --- a/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs @@ -1,10 +1,6 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public class NoopHtmlSanitizer : IHtmlSanitizer { - public class NoopHtmlSanitizer : IHtmlSanitizer - { - public string Sanitize(string html) - { - return html; - } - } + public string Sanitize(string html) => html; } diff --git a/src/Umbraco.Core/Security/PasswordGenerator.cs b/src/Umbraco.Core/Security/PasswordGenerator.cs index 55a6ba1a51..7d55e0e39d 100644 --- a/src/Umbraco.Core/Security/PasswordGenerator.cs +++ b/src/Umbraco.Core/Security/PasswordGenerator.cs @@ -1,161 +1,212 @@ -using System; -using System.Linq; using System.Security.Cryptography; using Umbraco.Cms.Core.Configuration; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Generates a password +/// +/// +/// This uses logic copied from the old MembershipProvider.GeneratePassword logic +/// +public class PasswordGenerator { + private readonly IPasswordConfiguration _passwordConfiguration; + + public PasswordGenerator(IPasswordConfiguration passwordConfiguration) => + _passwordConfiguration = passwordConfiguration; + + public string GeneratePassword() + { + var password = PasswordStore.GeneratePassword( + _passwordConfiguration.RequiredLength, + _passwordConfiguration.GetMinNonAlphaNumericChars()); + + var passwordChars = password.ToCharArray(); + + if (_passwordConfiguration.RequireDigit && + passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) + { + password += Convert.ToChar(RandomNumberGenerator.GetInt32(48, 58)); // 0-9 + } + + if (_passwordConfiguration.RequireLowercase && + passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) + { + password += Convert.ToChar(RandomNumberGenerator.GetInt32(97, 123)); // a-z + } + + if (_passwordConfiguration.RequireUppercase && + passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) + { + password += Convert.ToChar(RandomNumberGenerator.GetInt32(65, 91)); // A-Z + } + + if (_passwordConfiguration.RequireNonLetterOrDigit && + passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) + { + password += Convert.ToChar(RandomNumberGenerator.GetInt32(33, 48)); // symbols !"#$%&'()*+,-./ + } + + return password; + } + /// - /// Generates a password + /// Internal class copied from ASP.NET Framework MembershipProvider /// /// - /// This uses logic copied from the old MembershipProvider.GeneratePassword logic + /// See https://stackoverflow.com/a/39855417/694494 + + /// https://github.com/Microsoft/referencesource/blob/master/System.Web/Security/Membership.cs /// - public class PasswordGenerator + private static class PasswordStore { - private readonly IPasswordConfiguration _passwordConfiguration; + private static readonly char[] Punctuations = "!@#$%^&*()_-+=[{]};:>|./?".ToCharArray(); + private static readonly char[] StartingChars = { '<', '&' }; - public PasswordGenerator(IPasswordConfiguration passwordConfiguration) + /// Generates a random password of the specified length. + /// A random password of the specified length. + /// + /// The number of characters in the generated password. The length must be between 1 and 128 + /// characters. + /// + /// + /// The minimum number of non-alphanumeric characters (such as @, #, !, %, + /// &, and so on) in the generated password. + /// + /// + /// is less than 1 or greater than 128 -or- + /// is less than 0 or greater than . + /// + public static string GeneratePassword(int length, int numberOfNonAlphanumericCharacters) { - _passwordConfiguration = passwordConfiguration; - } - public string GeneratePassword() - { - var password = PasswordStore.GeneratePassword( - _passwordConfiguration.RequiredLength, - _passwordConfiguration.GetMinNonAlphaNumericChars()); - - var random = new Random(); - - var passwordChars = password.ToCharArray(); - - if (_passwordConfiguration.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) - password += Convert.ToChar(random.Next(48, 58)); // 0-9 - - if (_passwordConfiguration.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) - password += Convert.ToChar(random.Next(97, 123)); // a-z - - if (_passwordConfiguration.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) - password += Convert.ToChar(random.Next(65, 91)); // A-Z - - if (_passwordConfiguration.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) - password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./ - - return password; - } - - /// - /// Internal class copied from ASP.NET Framework MembershipProvider - /// - /// - /// See https://stackoverflow.com/a/39855417/694494 + https://github.com/Microsoft/referencesource/blob/master/System.Web/Security/Membership.cs - /// - private static class PasswordStore - { - private static readonly char[] Punctuations = "!@#$%^&*()_-+=[{]};:>|./?".ToCharArray(); - private static readonly char[] StartingChars = new char[] { '<', '&' }; - /// Generates a random password of the specified length. - /// A random password of the specified length. - /// The number of characters in the generated password. The length must be between 1 and 128 characters. - /// The minimum number of non-alphanumeric characters (such as @, #, !, %, &, and so on) in the generated password. - /// - /// is less than 1 or greater than 128 -or- is less than 0 or greater than . - public static string GeneratePassword(int length, int numberOfNonAlphanumericCharacters) + if (length < 1 || length > 128) { - if (length < 1 || length > 128) - throw new ArgumentException("password length incorrect", nameof(length)); - if (numberOfNonAlphanumericCharacters > length || numberOfNonAlphanumericCharacters < 0) - throw new ArgumentException("min required non alphanumeric characters incorrect", nameof(numberOfNonAlphanumericCharacters)); - string s; - int matchIndex; - do - { - var data = new byte[length]; - var chArray = new char[length]; - var num1 = 0; - new RNGCryptoServiceProvider().GetBytes(data); - for (var index = 0; index < length; ++index) - { - var num2 = (int)data[index] % 87; - if (num2 < 10) - chArray[index] = (char)(48 + num2); - else if (num2 < 36) - chArray[index] = (char)(65 + num2 - 10); - else if (num2 < 62) - { - chArray[index] = (char)(97 + num2 - 36); - } - else - { - chArray[index] = Punctuations[num2 - 62]; - ++num1; - } - } - if (num1 < numberOfNonAlphanumericCharacters) - { - var random = new Random(); - for (var index1 = 0; index1 < numberOfNonAlphanumericCharacters - num1; ++index1) - { - int index2; - do - { - index2 = random.Next(0, length); - } - while (!char.IsLetterOrDigit(chArray[index2])); - chArray[index2] = Punctuations[random.Next(0, Punctuations.Length)]; - } - } - s = new string(chArray); - } - while (IsDangerousString(s, out matchIndex)); - return s; + throw new ArgumentException("password length incorrect", nameof(length)); } - private static bool IsDangerousString(string s, out int matchIndex) + if (numberOfNonAlphanumericCharacters > length || numberOfNonAlphanumericCharacters < 0) { - //bool inComment = false; - matchIndex = 0; + throw new ArgumentException( + "min required non alphanumeric characters incorrect", + nameof(numberOfNonAlphanumericCharacters)); + } - for (var i = 0; ;) + string s; + do + { + var data = new byte[length]; + var chArray = new char[length]; + var num1 = 0; + new RNGCryptoServiceProvider().GetBytes(data); + + for (var index = 0; index < length; ++index) + { + var num2 = data[index] % 87; + if (num2 < 10) + { + chArray[index] = (char)(48 + num2); + } + else if (num2 < 36) + { + chArray[index] = (char)(65 + num2 - 10); + } + else if (num2 < 62) + { + chArray[index] = (char)(97 + num2 - 36); + } + else + { + chArray[index] = Punctuations[num2 - 62]; + ++num1; + } + } + + if (num1 < numberOfNonAlphanumericCharacters) { - // Look for the start of one of our patterns - var n = s.IndexOfAny(StartingChars, i); - - // If not found, the string is safe - if (n < 0) return false; - - // If it's the last char, it's safe - if (n == s.Length - 1) return false; - - matchIndex = n; - - switch (s[n]) + for (var index1 = 0; index1 < numberOfNonAlphanumericCharacters - num1; ++index1) { - case '<': - // If the < is followed by a letter or '!', it's unsafe (looks like a tag or HTML comment) - if (IsAtoZ(s[n + 1]) || s[n + 1] == '!' || s[n + 1] == '/' || s[n + 1] == '?') return true; - break; - case '&': - // If the & is followed by a #, it's unsafe (e.g. S) - if (s[n + 1] == '#') return true; - break; + int index2; + do + { + index2 = RandomNumberGenerator.GetInt32(0, length); + } + while (!char.IsLetterOrDigit(chArray[index2])); + + chArray[index2] = Punctuations[RandomNumberGenerator.GetInt32(0, Punctuations.Length)]; } - - // Continue searching - i = n + 1; } + + s = new string(chArray); + } + while (IsDangerousString(s, out int matchIndex)); + + return s; + } + + private static bool IsDangerousString(string s, out int matchIndex) + { + // bool inComment = false; + matchIndex = 0; + + for (var i = 0; ;) + { + // Look for the start of one of our patterns + var n = s.IndexOfAny(StartingChars, i); + + // If not found, the string is safe + if (n < 0) + { + return false; + } + + // If it's the last char, it's safe + if (n == s.Length - 1) + { + return false; + } + + matchIndex = n; + + switch (s[n]) + { + case '<': + // If the < is followed by a letter or '!', it's unsafe (looks like a tag or HTML comment) + if (IsAtoZ(s[n + 1]) || s[n + 1] == '!' || s[n + 1] == '/' || s[n + 1] == '?') + { + return true; + } + + break; + case '&': + // If the & is followed by a #, it's unsafe (e.g. S) + if (s[n + 1] == '#') + { + return true; + } + + break; + } + + // Continue searching + i = n + 1; + } + } + + private static bool IsAtoZ(char c) + { + if (c >= 97 && c <= 122) + { + return true; } - private static bool IsAtoZ(char c) + if (c >= 65) { - if ((int)c >= 97 && (int)c <= 122) - return true; - if ((int)c >= 65) - return (int)c <= 90; - return false; + return c <= 90; } + + return false; } } } diff --git a/src/Umbraco.Core/Security/PublicAccessStatus.cs b/src/Umbraco.Core/Security/PublicAccessStatus.cs index b92c0ff57a..9026b11fd5 100644 --- a/src/Umbraco.Core/Security/PublicAccessStatus.cs +++ b/src/Umbraco.Core/Security/PublicAccessStatus.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public enum PublicAccessStatus { - public enum PublicAccessStatus - { - NotLoggedIn, - AccessDenied, - NotApproved, - LockedOut, - AccessAccepted - } + NotLoggedIn, + AccessDenied, + NotApproved, + LockedOut, + AccessAccepted, } diff --git a/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs b/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs index b6b6c241e4..0809f6c501 100644 --- a/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs +++ b/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs @@ -1,24 +1,18 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public class UpdateMemberProfileResult { - public class UpdateMemberProfileResult + private UpdateMemberProfileResult() { - private UpdateMemberProfileResult() - { - } - - public UpdateMemberProfileStatus Status { get; private set; } - - public string? ErrorMessage { get; private set; } - - public static UpdateMemberProfileResult Success() - { - return new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Success }; - } - - public static UpdateMemberProfileResult Error(string message) - { - return new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Error, ErrorMessage = message }; - } } + public UpdateMemberProfileStatus Status { get; private set; } + + public string? ErrorMessage { get; private set; } + + public static UpdateMemberProfileResult Success() => + new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Success }; + + public static UpdateMemberProfileResult Error(string message) => + new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Error, ErrorMessage = message }; } diff --git a/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs b/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs index df805d3096..74fb52e697 100644 --- a/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs +++ b/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public enum UpdateMemberProfileStatus { - public enum UpdateMemberProfileStatus - { - Success, - Error, - } + Success, + Error, } diff --git a/src/Umbraco.Core/Semver/Semver.cs b/src/Umbraco.Core/Semver/Semver.cs index 5a04553f1b..3c33f43087 100644 --- a/src/Umbraco.Core/Semver/Semver.cs +++ b/src/Umbraco.Core/Semver/Semver.cs @@ -1,5 +1,4 @@ -using System; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; #if !NETSTANDARD using System.Globalization; using System.Runtime.Serialization; @@ -30,8 +29,8 @@ THE SOFTWARE. namespace Umbraco.Cms.Core.Semver { /// - /// A semantic version implementation. - /// Conforms to v2.0.0 of http://semver.org/ + /// A semantic version implementation. + /// Conforms to v2.0.0 of http://semver.org/ /// #if NETSTANDARD public sealed class SemVersion : IComparable, IComparable @@ -40,8 +39,9 @@ namespace Umbraco.Cms.Core.Semver public sealed class SemVersion : IComparable, IComparable, ISerializable #endif { - static Regex parseEx = - new Regex(@"^(?\d+)" + + private static Regex parseEx = + new( + @"^(?\d+)" + @"(\.(?\d+))?" + @"(\.(?\d+))?" + @"(\-(?
[0-9A-Za-z\-\.]+))?" +
@@ -54,15 +54,19 @@ namespace Umbraco.Cms.Core.Semver
 
 #if !NETSTANDARD
         /// 
-        /// Initializes a new instance of the  class.
+        ///     Initializes a new instance of the  class.
         /// 
         /// 
         /// 
         /// 
         private SemVersion(SerializationInfo info, StreamingContext context)
         {
-            if (info == null) throw new ArgumentNullException("info");
-            var semVersion = Parse(info.GetString("SemVersion")!);
+            if (info == null)
+            {
+                throw new ArgumentNullException("info");
+            }
+
+            SemVersion semVersion = Parse(info.GetString("SemVersion")!);
             Major = semVersion.Major;
             Minor = semVersion.Minor;
             Patch = semVersion.Patch;
@@ -72,7 +76,7 @@ namespace Umbraco.Cms.Core.Semver
 #endif
 
         /// 
-        /// Initializes a new instance of the  class.
+        ///     Initializes a new instance of the  class.
         /// 
         /// The major version.
         /// The minor version.
@@ -81,46 +85,50 @@ namespace Umbraco.Cms.Core.Semver
         /// The build eg ("nightly.232").
         public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = "", string build = "")
         {
-            this.Major = major;
-            this.Minor = minor;
-            this.Patch = patch;
+            Major = major;
+            Minor = minor;
+            Patch = patch;
 
-            this.Prerelease = prerelease ?? "";
-            this.Build = build ?? "";
+            Prerelease = prerelease ?? string.Empty;
+            Build = build ?? string.Empty;
         }
 
         /// 
-        /// Initializes a new instance of the  class.
+        ///     Initializes a new instance of the  class.
         /// 
-        /// The  that is used to initialize
-        /// the Major, Minor, Patch and Build properties.
+        /// 
+        ///     The  that is used to initialize
+        ///     the Major, Minor, Patch and Build properties.
+        /// 
         public SemVersion(Version version)
         {
             if (version == null)
+            {
                 throw new ArgumentNullException("version");
+            }
 
-            this.Major = version.Major;
-            this.Minor = version.Minor;
+            Major = version.Major;
+            Minor = version.Minor;
 
             if (version.Revision >= 0)
             {
-                this.Patch = version.Revision;
+                Patch = version.Revision;
             }
 
-            this.Prerelease = String.Empty;
+            Prerelease = string.Empty;
 
             if (version.Build > 0)
             {
-                this.Build = version.Build.ToString();
+                Build = version.Build.ToString();
             }
             else
             {
-                this.Build = String.Empty;
+                Build = string.Empty;
             }
         }
 
         /// 
-        /// Parses the specified string to a semantic version.
+        ///     Parses the specified string to a semantic version.
         /// 
         /// The version string.
         /// If set to true minor and patch version are required, else they default to 0.
@@ -128,9 +136,11 @@ namespace Umbraco.Cms.Core.Semver
         /// When a invalid version string is passed.
         public static SemVersion Parse(string version, bool strict = false)
         {
-            var match = parseEx.Match(version);
+            Match match = parseEx.Match(version);
             if (!match.Success)
+            {
                 throw new ArgumentException("Invalid version.", "version");
+            }
 
 #if NETSTANDARD
             var major = int.Parse(match.Groups["major"].Value);
@@ -138,8 +148,8 @@ namespace Umbraco.Cms.Core.Semver
             var major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
 #endif
 
-            var minorMatch = match.Groups["minor"];
-            int minor = 0;
+            Group minorMatch = match.Groups["minor"];
+            var minor = 0;
             if (minorMatch.Success)
             {
 #if NETSTANDARD
@@ -153,8 +163,8 @@ namespace Umbraco.Cms.Core.Semver
                 throw new InvalidOperationException("Invalid version (no minor version given in strict mode)");
             }
 
-            var patchMatch = match.Groups["patch"];
-            int patch = 0;
+            Group patchMatch = match.Groups["patch"];
+            var patch = 0;
             if (patchMatch.Success)
             {
 #if NETSTANDARD
@@ -175,12 +185,14 @@ namespace Umbraco.Cms.Core.Semver
         }
 
         /// 
-        /// Parses the specified string to a semantic version.
+        ///     Parses the specified string to a semantic version.
         /// 
         /// The version string.
-        /// When the method returns, contains a SemVersion instance equivalent
-        /// to the version string passed in, if the version string was valid, or null if the
-        /// version string was not valid.
+        /// 
+        ///     When the method returns, contains a SemVersion instance equivalent
+        ///     to the version string passed in, if the version string was valid, or null if the
+        ///     version string was not valid.
+        /// 
         /// If set to true minor and patch version are required, else they default to 0.
         /// False when a invalid version string is passed, otherwise true.
         public static bool TryParse(string version, out SemVersion? semver, bool strict = false)
@@ -198,7 +210,7 @@ namespace Umbraco.Cms.Core.Semver
         }
 
         /// 
-        /// Tests the specified versions for equality.
+        ///     Tests the specified versions for equality.
         /// 
         /// The first version.
         /// The second version.
@@ -206,26 +218,34 @@ namespace Umbraco.Cms.Core.Semver
         public static bool Equals(SemVersion versionA, SemVersion versionB)
         {
             if (ReferenceEquals(versionA, null))
+            {
                 return ReferenceEquals(versionB, null);
+            }
+
             return versionA.Equals(versionB);
         }
 
         /// 
-        /// Compares the specified versions.
+        ///     Compares the specified versions.
         /// 
         /// The version to compare to.
         /// The version to compare against.
-        /// If versionA < versionB < 0, if versionA > versionB > 0,
-        /// if versionA is equal to versionB 0.
+        /// 
+        ///     If versionA < versionB < 0, if versionA > versionB > 0,
+        ///     if versionA is equal to versionB 0.
+        /// 
         public static int Compare(SemVersion versionA, SemVersion versionB)
         {
             if (ReferenceEquals(versionA, null))
+            {
                 return ReferenceEquals(versionB, null) ? 0 : -1;
+            }
+
             return versionA.CompareTo(versionB);
         }
 
         /// 
-        /// Make a copy of the current instance with optional altered fields.
+        ///     Make a copy of the current instance with optional altered fields.
         /// 
         /// The major version.
         /// The minor version.
@@ -233,194 +253,224 @@ namespace Umbraco.Cms.Core.Semver
         /// The prerelease text.
         /// The build text.
         /// The new version object.
-        public SemVersion Change(int? major = null, int? minor = null, int? patch = null,
-            string? prerelease = null, string? build = null)
-        {
-            return new SemVersion(
-                major ?? this.Major,
-                minor ?? this.Minor,
-                patch ?? this.Patch,
-                prerelease ?? this.Prerelease,
-                build ?? this.Build);
-        }
+        public SemVersion Change(int? major = null, int? minor = null, int? patch = null, string? prerelease = null, string? build = null) =>
+            new(
+                major ?? Major,
+                minor ?? Minor,
+                patch ?? Patch,
+                prerelease ?? Prerelease,
+                build ?? Build);
 
         /// 
-        /// Gets the major version.
+        ///     Gets the major version.
         /// 
         /// 
-        /// The major version.
+        ///     The major version.
         /// 
         public int Major { get; private set; }
 
         /// 
-        /// Gets the minor version.
+        ///     Gets the minor version.
         /// 
         /// 
-        /// The minor version.
+        ///     The minor version.
         /// 
         public int Minor { get; private set; }
 
         /// 
-        /// Gets the patch version.
+        ///     Gets the patch version.
         /// 
         /// 
-        /// The patch version.
+        ///     The patch version.
         /// 
         public int Patch { get; private set; }
 
         /// 
-        /// Gets the pre-release version.
+        ///     Gets the pre-release version.
         /// 
         /// 
-        /// The pre-release version.
+        ///     The pre-release version.
         /// 
         public string Prerelease { get; private set; }
 
         /// 
-        /// Gets the build version.
+        ///     Gets the build version.
         /// 
         /// 
-        /// The build version.
+        ///     The build version.
         /// 
         public string Build { get; private set; }
 
         /// 
-        /// Returns a  that represents this instance.
+        ///     Returns a  that represents this instance.
         /// 
         /// 
-        /// A  that represents this instance.
+        ///     A  that represents this instance.
         /// 
         public override string ToString()
         {
-            var version = "" + Major + "." + Minor + "." + Patch;
-            if (!String.IsNullOrEmpty(Prerelease))
+            var version = string.Empty + Major + "." + Minor + "." + Patch;
+            if (!string.IsNullOrEmpty(Prerelease))
+            {
                 version += "-" + Prerelease;
-            if (!String.IsNullOrEmpty(Build))
+            }
+
+            if (!string.IsNullOrEmpty(Build))
+            {
                 version += "+" + Build;
+            }
+
             return version;
         }
 
         /// 
-        /// Compares the current instance with another object of the same type and returns an integer that indicates
-        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
-        /// other object.
+        ///     Compares the current instance with another object of the same type and returns an integer that indicates
+        ///     whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+        ///     other object.
         /// 
         /// An object to compare with this instance.
         /// 
-        /// A value that indicates the relative order of the objects being compared.
-        /// The return value has these meanings: Value Meaning Less than zero
-        ///  This instance precedes  in the sort order.
-        ///  Zero This instance occurs in the same position in the sort order as . i
-        ///  Greater than zero This instance follows  in the sort order.
+        ///     A value that indicates the relative order of the objects being compared.
+        ///     The return value has these meanings: Value Meaning Less than zero
+        ///     This instance precedes  in the sort order.
+        ///     Zero This instance occurs in the same position in the sort order as . i
+        ///     Greater than zero This instance follows  in the sort order.
         /// 
-        public int CompareTo(object? obj)
-        {
-            return CompareTo((SemVersion?)obj);
-        }
+        public int CompareTo(object? obj) => CompareTo((SemVersion?)obj);
 
         /// 
-        /// Compares the current instance with another object of the same type and returns an integer that indicates
-        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
-        /// other object.
+        ///     Compares the current instance with another object of the same type and returns an integer that indicates
+        ///     whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+        ///     other object.
         /// 
         /// An object to compare with this instance.
         /// 
-        /// A value that indicates the relative order of the objects being compared.
-        /// The return value has these meanings: Value Meaning Less than zero
-        ///  This instance precedes  in the sort order.
-        ///  Zero This instance occurs in the same position in the sort order as . i
-        ///  Greater than zero This instance follows  in the sort order.
+        ///     A value that indicates the relative order of the objects being compared.
+        ///     The return value has these meanings: Value Meaning Less than zero
+        ///     This instance precedes  in the sort order.
+        ///     Zero This instance occurs in the same position in the sort order as . i
+        ///     Greater than zero This instance follows  in the sort order.
         /// 
         public int CompareTo(SemVersion? other)
         {
             if (ReferenceEquals(other, null))
+            {
                 return 1;
+            }
 
-            var r = this.CompareByPrecedence(other);
+            var r = CompareByPrecedence(other);
             if (r != 0)
+            {
                 return r;
+            }
 
-            r = CompareComponent(this.Build, other.Build);
+            r = CompareComponent(Build, other.Build);
             return r;
         }
 
         /// 
-        /// Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
+        ///     Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
         /// 
         /// The semantic version.
         /// true if the version precedence matches.
-        public bool PrecedenceMatches(SemVersion other)
-        {
-            return CompareByPrecedence(other) == 0;
-        }
+        public bool PrecedenceMatches(SemVersion other) => CompareByPrecedence(other) == 0;
 
         /// 
-        /// Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
+        ///     Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
         /// 
         /// The semantic version.
         /// 
-        /// A value that indicates the relative order of the objects being compared.
-        /// The return value has these meanings: Value Meaning Less than zero
-        ///  This instance precedes  in the version precedence.
-        ///  Zero This instance has the same precedence as . i
-        ///  Greater than zero This instance has creater precedence as .
+        ///     A value that indicates the relative order of the objects being compared.
+        ///     The return value has these meanings: Value Meaning Less than zero
+        ///     This instance precedes  in the version precedence.
+        ///     Zero This instance has the same precedence as . i
+        ///     Greater than zero This instance has creater precedence as .
         /// 
         public int CompareByPrecedence(SemVersion other)
         {
             if (ReferenceEquals(other, null))
+            {
                 return 1;
+            }
 
-            var r = this.Major.CompareTo(other.Major);
-            if (r != 0) return r;
+            var r = Major.CompareTo(other.Major);
+            if (r != 0)
+            {
+                return r;
+            }
 
-            r = this.Minor.CompareTo(other.Minor);
-            if (r != 0) return r;
+            r = Minor.CompareTo(other.Minor);
+            if (r != 0)
+            {
+                return r;
+            }
 
-            r = this.Patch.CompareTo(other.Patch);
-            if (r != 0) return r;
+            r = Patch.CompareTo(other.Patch);
+            if (r != 0)
+            {
+                return r;
+            }
 
-            r = CompareComponent(this.Prerelease, other.Prerelease, true);
+            r = CompareComponent(Prerelease, other.Prerelease, true);
             return r;
         }
 
-        static int CompareComponent(string a, string b, bool lower = false)
+        private static int CompareComponent(string a, string b, bool lower = false)
         {
-            var aEmpty = String.IsNullOrEmpty(a);
-            var bEmpty = String.IsNullOrEmpty(b);
+            var aEmpty = string.IsNullOrEmpty(a);
+            var bEmpty = string.IsNullOrEmpty(b);
             if (aEmpty && bEmpty)
+            {
                 return 0;
+            }
 
             if (aEmpty)
+            {
                 return lower ? 1 : -1;
+            }
+
             if (bEmpty)
+            {
                 return lower ? -1 : 1;
+            }
 
             var aComps = a.Split('.');
             var bComps = b.Split('.');
 
             var minLen = Math.Min(aComps.Length, bComps.Length);
-            for (int i = 0; i < minLen; i++)
+            for (var i = 0; i < minLen; i++)
             {
                 var ac = aComps[i];
                 var bc = bComps[i];
                 int anum, bnum;
-                var isanum = Int32.TryParse(ac, out anum);
-                var isbnum = Int32.TryParse(bc, out bnum);
+                var isanum = int.TryParse(ac, out anum);
+                var isbnum = int.TryParse(bc, out bnum);
                 int r;
                 if (isanum && isbnum)
                 {
                     r = anum.CompareTo(bnum);
-                    if (r != 0) return anum.CompareTo(bnum);
+                    if (r != 0)
+                    {
+                        return anum.CompareTo(bnum);
+                    }
                 }
                 else
                 {
                     if (isanum)
+                    {
                         return -1;
+                    }
+
                     if (isbnum)
+                    {
                         return 1;
-                    r = String.CompareOrdinal(ac, bc);
+                    }
+
+                    r = string.CompareOrdinal(ac, bc);
                     if (r != 0)
+                    {
                         return r;
+                    }
                 }
             }
 
@@ -428,44 +478,48 @@ namespace Umbraco.Cms.Core.Semver
         }
 
         /// 
-        /// Determines whether the specified  is equal to this instance.
+        ///     Determines whether the specified  is equal to this instance.
         /// 
         /// The  to compare with this instance.
         /// 
-        ///   true if the specified  is equal to this instance; otherwise, false.
+        ///     true if the specified  is equal to this instance; otherwise, false.
         /// 
         public override bool Equals(object? obj)
         {
             if (ReferenceEquals(obj, null))
+            {
                 return false;
+            }
 
             if (ReferenceEquals(this, obj))
+            {
                 return true;
+            }
 
             var other = (SemVersion)obj;
 
-            return this.Major == other.Major &&
-                this.Minor == other.Minor &&
-                this.Patch == other.Patch &&
-                string.Equals(this.Prerelease, other.Prerelease, StringComparison.Ordinal) &&
-                string.Equals(this.Build, other.Build, StringComparison.Ordinal);
+            return Major == other.Major &&
+                   Minor == other.Minor &&
+                   Patch == other.Patch &&
+                   string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal) &&
+                   string.Equals(Build, other.Build, StringComparison.Ordinal);
         }
 
         /// 
-        /// Returns a hash code for this instance.
+        ///     Returns a hash code for this instance.
         /// 
         /// 
-        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+        ///     A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
         /// 
         public override int GetHashCode()
         {
             unchecked
             {
-                int result = this.Major.GetHashCode();
-                result = result * 31 + this.Minor.GetHashCode();
-                result = result * 31 + this.Patch.GetHashCode();
-                result = result * 31 + this.Prerelease.GetHashCode();
-                result = result * 31 + this.Build.GetHashCode();
+                var result = Major.GetHashCode();
+                result = (result * 31) + Minor.GetHashCode();
+                result = (result * 31) + Patch.GetHashCode();
+                result = (result * 31) + Prerelease.GetHashCode();
+                result = (result * 31) + Build.GetHashCode();
                 return result;
             }
         }
@@ -474,85 +528,68 @@ namespace Umbraco.Cms.Core.Semver
         [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
         public void GetObjectData(SerializationInfo info, StreamingContext context)
         {
-            if (info == null) throw new ArgumentNullException("info");
+            if (info == null)
+            {
+                throw new ArgumentNullException("info");
+            }
+
             info.AddValue("SemVersion", ToString());
         }
 #endif
 
         /// 
-        /// Implicit conversion from string to SemVersion.
+        ///     Implicit conversion from string to SemVersion.
         /// 
         /// The semantic version.
         /// The SemVersion object.
-        public static implicit operator SemVersion(string version)
-        {
-            return SemVersion.Parse(version);
-        }
+        public static implicit operator SemVersion(string version) => Parse(version);
 
         /// 
-        /// The override of the equals operator.
+        ///     The override of the equals operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is equal to right true, else false.
-        public static bool operator ==(SemVersion left, SemVersion right)
-        {
-            return SemVersion.Equals(left, right);
-        }
+        public static bool operator ==(SemVersion left, SemVersion right) => Equals(left, right);
 
         /// 
-        /// The override of the un-equal operator.
+        ///     The override of the un-equal operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is not equal to right true, else false.
-        public static bool operator !=(SemVersion left, SemVersion right)
-        {
-            return !SemVersion.Equals(left, right);
-        }
+        public static bool operator !=(SemVersion left, SemVersion right) => !Equals(left, right);
 
         /// 
-        /// The override of the greater operator.
+        ///     The override of the greater operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is greater than right true, else false.
-        public static bool operator >(SemVersion left, SemVersion right)
-        {
-            return SemVersion.Compare(left, right) > 0;
-        }
+        public static bool operator >(SemVersion left, SemVersion right) => Compare(left, right) > 0;
 
         /// 
-        /// The override of the greater than or equal operator.
+        ///     The override of the greater than or equal operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is greater than or equal to right true, else false.
-        public static bool operator >=(SemVersion left, SemVersion right)
-        {
-            return left == right || left > right;
-        }
+        public static bool operator >=(SemVersion left, SemVersion right) => left == right || left > right;
 
         /// 
-        /// The override of the less operator.
+        ///     The override of the less operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is less than right true, else false.
-        public static bool operator <(SemVersion left, SemVersion right)
-        {
-            return SemVersion.Compare(left, right) < 0;
-        }
+        public static bool operator <(SemVersion left, SemVersion right) => Compare(left, right) < 0;
 
         /// 
-        /// The override of the less than or equal operator.
+        ///     The override of the less than or equal operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is less than or equal to right true, else false.
-        public static bool operator <=(SemVersion left, SemVersion right)
-        {
-            return left == right || left < right;
-        }
+        public static bool operator <=(SemVersion left, SemVersion right) => left == right || left < right;
     }
 }
diff --git a/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs b/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs
index dee2e4c5db..9a0429a75e 100644
--- a/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs
+++ b/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs
@@ -1,7 +1,5 @@
-namespace Umbraco.Cms.Core.Serialization
-{
-    public interface IConfigurationEditorJsonSerializer : IJsonSerializer
-    {
+namespace Umbraco.Cms.Core.Serialization;
 
-    }
+public interface IConfigurationEditorJsonSerializer : IJsonSerializer
+{
 }
diff --git a/src/Umbraco.Core/Serialization/IJsonSerializer.cs b/src/Umbraco.Core/Serialization/IJsonSerializer.cs
index 051055b564..5a31a2cf97 100644
--- a/src/Umbraco.Core/Serialization/IJsonSerializer.cs
+++ b/src/Umbraco.Core/Serialization/IJsonSerializer.cs
@@ -1,11 +1,10 @@
-namespace Umbraco.Cms.Core.Serialization
+namespace Umbraco.Cms.Core.Serialization;
+
+public interface IJsonSerializer
 {
-    public interface IJsonSerializer
-    {
-        string Serialize(object? input);
+    string Serialize(object? input);
 
-        T? Deserialize(string input);
+    T? Deserialize(string input);
 
-        T? DeserializeSubset(string input, string key);
-    }
+    T? DeserializeSubset(string input, string key);
 }
diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs
index f7560afa93..046c5fff3d 100644
--- a/src/Umbraco.Core/Services/AuditService.cs
+++ b/src/Umbraco.Core/Services/AuditService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -8,231 +5,295 @@ using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services.Implement
+namespace Umbraco.Cms.Core.Services.Implement;
+
+public sealed class AuditService : RepositoryService, IAuditService
 {
-    public sealed class AuditService : RepositoryService, IAuditService
+    private readonly IAuditEntryRepository _auditEntryRepository;
+    private readonly IAuditRepository _auditRepository;
+    private readonly Lazy _isAvailable;
+
+    public AuditService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IAuditRepository auditRepository,
+        IAuditEntryRepository auditEntryRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly Lazy _isAvailable;
-        private readonly IAuditRepository _auditRepository;
-        private readonly IAuditEntryRepository _auditEntryRepository;
+        _auditRepository = auditRepository;
+        _auditEntryRepository = auditEntryRepository;
+        _isAvailable = new Lazy(DetermineIsAvailable);
+    }
 
-        public AuditService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IAuditRepository auditRepository, IAuditEntryRepository auditEntryRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    public void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            _auditRepository = auditRepository;
-            _auditEntryRepository = auditEntryRepository;
-            _isAvailable = new Lazy(DetermineIsAvailable);
+            _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, comment, parameters));
+            scope.Complete();
+        }
+    }
+
+    public IEnumerable GetLogs(int objectId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IEnumerable result = _auditRepository.Get(Query().Where(x => x.Id == objectId));
+            scope.Complete();
+            return result;
+        }
+    }
+
+    public IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IEnumerable result = sinceDate.HasValue == false
+                ? _auditRepository.Get(type, Query().Where(x => x.UserId == userId))
+                : _auditRepository.Get(
+                    type,
+                    Query().Where(x => x.UserId == userId && x.CreateDate >= sinceDate.Value));
+            scope.Complete();
+            return result;
+        }
+    }
+
+    public IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IEnumerable result = sinceDate.HasValue == false
+                ? _auditRepository.Get(type, Query())
+                : _auditRepository.Get(type, Query().Where(x => x.CreateDate >= sinceDate.Value));
+            scope.Complete();
+            return result;
+        }
+    }
+
+    public void CleanLogs(int maximumAgeOfLogsInMinutes)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _auditRepository.CleanLogs(maximumAgeOfLogsInMinutes);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Returns paged items in the audit trail for a given entity
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    public IEnumerable GetPagedItemsByEntity(
+        int entityId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        public void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null)
+        if (pageSize <= 0)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, comment, parameters));
-                scope.Complete();
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        public IEnumerable GetLogs(int objectId)
+        if (entityId == Constants.System.Root || entityId <= 0)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var result = _auditRepository.Get(Query().Where(x => x.Id == objectId));
-                scope.Complete();
-                return result;
-            }
+            totalRecords = 0;
+            return Enumerable.Empty();
         }
 
-        public IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null)
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var result = sinceDate.HasValue == false
-                    ? _auditRepository.Get(type, Query().Where(x => x.UserId == userId))
-                    : _auditRepository.Get(type, Query().Where(x => x.UserId == userId && x.CreateDate >= sinceDate.Value));
-                scope.Complete();
-                return result;
-            }
+            IQuery query = Query().Where(x => x.Id == entityId);
+
+            return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
+        }
+    }
+
+    /// 
+    ///     Returns paged items in the audit trail for a given user
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    public IEnumerable GetPagedItemsByUser(
+        int userId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        public IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null)
+        if (pageSize <= 0)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var result = sinceDate.HasValue == false
-                    ? _auditRepository.Get(type, Query())
-                    : _auditRepository.Get(type, Query().Where(x => x.CreateDate >= sinceDate.Value));
-                scope.Complete();
-                return result;
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        public void CleanLogs(int maximumAgeOfLogsInMinutes)
+        if (userId < Constants.Security.SuperUserId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditRepository.CleanLogs(maximumAgeOfLogsInMinutes);
-                scope.Complete();
-            }
+            totalRecords = 0;
+            return Enumerable.Empty();
         }
 
-        /// 
-        /// Returns paged items in the audit trail for a given entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        public IEnumerable GetPagedItemsByEntity(int entityId, long pageIndex, int pageSize, out long totalRecords,
-            Direction orderDirection = Direction.Descending,
-            AuditType[]? auditTypeFilter = null,
-            IQuery? customFilter = null)
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            IQuery query = Query().Where(x => x.UserId == userId);
 
-            if (entityId == Cms.Core.Constants.System.Root || entityId <= 0)
-            {
-                totalRecords = 0;
-                return Enumerable.Empty();
-            }
+            return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
+        }
+    }
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.Id == entityId);
-
-                return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
-            }
+    /// 
+    public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails)
+    {
+        if (performingUserId < 0 && performingUserId != Constants.Security.SuperUserId)
+        {
+            throw new ArgumentOutOfRangeException(nameof(performingUserId));
         }
 
-        /// 
-        /// Returns paged items in the audit trail for a given user
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        public IEnumerable GetPagedItemsByUser(int userId, long pageIndex, int pageSize, out long totalRecords, Direction orderDirection = Direction.Descending, AuditType[]? auditTypeFilter = null, IQuery? customFilter = null)
+        if (string.IsNullOrWhiteSpace(perfomingDetails))
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
-
-            if (userId < Cms.Core.Constants.Security.SuperUserId)
-            {
-                totalRecords = 0;
-                return Enumerable.Empty();
-            }
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.UserId == userId);
-
-                return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
-            }
+            throw new ArgumentException("Value cannot be null or whitespace.", nameof(perfomingDetails));
         }
 
-        /// 
-        public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails)
+        if (string.IsNullOrWhiteSpace(eventType))
         {
-            if (performingUserId < 0 && performingUserId != Cms.Core.Constants.Security.SuperUserId) throw new ArgumentOutOfRangeException(nameof(performingUserId));
-            if (string.IsNullOrWhiteSpace(perfomingDetails)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(perfomingDetails));
-            if (string.IsNullOrWhiteSpace(eventType)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventType));
-            if (string.IsNullOrWhiteSpace(eventDetails)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventDetails));
+            throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventType));
+        }
 
-            //we need to truncate the data else we'll get SQL errors
-            affectedDetails = affectedDetails?.Substring(0, Math.Min(affectedDetails.Length, Constants.Audit.DetailsLength));
-            eventDetails = eventDetails.Substring(0, Math.Min(eventDetails.Length, Constants.Audit.DetailsLength));
+        if (string.IsNullOrWhiteSpace(eventDetails))
+        {
+            throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventDetails));
+        }
 
-            //validate the eventType - must contain a forward slash, no spaces, no special chars
-            var eventTypeParts = eventType.ToCharArray();
-            if (eventTypeParts.Contains('/') == false || eventTypeParts.All(c => char.IsLetterOrDigit(c) || c == '/' || c == '-') == false)
-                throw new ArgumentException(nameof(eventType) + " must contain only alphanumeric characters, hyphens and at least one '/' defining a category");
-            if (eventType.Length > Constants.Audit.EventTypeLength)
-                throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(eventType));
-            if (performingIp != null && performingIp.Length > Constants.Audit.IpLength)
-                throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(performingIp));
+        // we need to truncate the data else we'll get SQL errors
+        affectedDetails =
+            affectedDetails?[..Math.Min(affectedDetails.Length, Constants.Audit.DetailsLength)];
+        eventDetails = eventDetails[..Math.Min(eventDetails.Length, Constants.Audit.DetailsLength)];
 
-            var entry = new AuditEntry
-            {
-                PerformingUserId = performingUserId,
-                PerformingDetails = perfomingDetails,
-                PerformingIp = performingIp,
-                EventDateUtc = eventDateUtc,
-                AffectedUserId = affectedUserId,
-                AffectedDetails = affectedDetails,
-                EventType = eventType,
-                EventDetails = eventDetails,
-            };
+        // validate the eventType - must contain a forward slash, no spaces, no special chars
+        var eventTypeParts = eventType.ToCharArray();
+        if (eventTypeParts.Contains('/') == false ||
+            eventTypeParts.All(c => char.IsLetterOrDigit(c) || c == '/' || c == '-') == false)
+        {
+            throw new ArgumentException(nameof(eventType) +
+                                        " must contain only alphanumeric characters, hyphens and at least one '/' defining a category");
+        }
 
-            if (_isAvailable.Value == false) return entry;
+        if (eventType.Length > Constants.Audit.EventTypeLength)
+        {
+            throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(eventType));
+        }
 
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditEntryRepository.Save(entry);
-                scope.Complete();
-            }
+        if (performingIp != null && performingIp.Length > Constants.Audit.IpLength)
+        {
+            throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(performingIp));
+        }
 
+        var entry = new AuditEntry
+        {
+            PerformingUserId = performingUserId,
+            PerformingDetails = perfomingDetails,
+            PerformingIp = performingIp,
+            EventDateUtc = eventDateUtc,
+            AffectedUserId = affectedUserId,
+            AffectedDetails = affectedDetails,
+            EventType = eventType,
+            EventDetails = eventDetails,
+        };
+
+        if (_isAvailable.Value == false)
+        {
             return entry;
         }
 
-        // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
-        internal IEnumerable? GetAll()
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (_isAvailable.Value == false) return Enumerable.Empty();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.GetMany();
-            }
+            _auditEntryRepository.Save(entry);
+            scope.Complete();
         }
 
-        // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
-        internal IEnumerable GetPage(long pageIndex, int pageCount, out long records)
-        {
-            if (_isAvailable.Value == false)
-            {
-                records = 0;
-                return Enumerable.Empty();
-            }
+        return entry;
+    }
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.GetPage(pageIndex, pageCount, out records);
-            }
+    // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
+    internal IEnumerable? GetAll()
+    {
+        if (_isAvailable.Value == false)
+        {
+            return Enumerable.Empty();
         }
 
-        /// 
-        /// Determines whether the repository is available.
-        /// 
-        private bool DetermineIsAvailable()
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.IsAvailable();
-            }
+            return _auditEntryRepository.GetMany();
+        }
+    }
+
+    // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
+    internal IEnumerable GetPage(long pageIndex, int pageCount, out long records)
+    {
+        if (_isAvailable.Value == false)
+        {
+            records = 0;
+            return Enumerable.Empty();
+        }
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _auditEntryRepository.GetPage(pageIndex, pageCount, out records);
+        }
+    }
+
+    /// 
+    ///     Determines whether the repository is available.
+    /// 
+    private bool DetermineIsAvailable()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _auditEntryRepository.IsAvailable();
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/BasicAuthService.cs b/src/Umbraco.Core/Services/BasicAuthService.cs
index 3021768bfe..02f955bad6 100644
--- a/src/Umbraco.Core/Services/BasicAuthService.cs
+++ b/src/Umbraco.Core/Services/BasicAuthService.cs
@@ -1,48 +1,61 @@
-using System;
 using System.Net;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
 using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 
-namespace Umbraco.Cms.Core.Services.Implement
+namespace Umbraco.Cms.Core.Services.Implement;
+
+public class BasicAuthService : IBasicAuthService
 {
-    public class BasicAuthService : IBasicAuthService
-    {
-        private readonly IIpAddressUtilities _ipAddressUtilities;
-        private BasicAuthSettings _basicAuthSettings;
+    private readonly IIpAddressUtilities _ipAddressUtilities;
+    private BasicAuthSettings _basicAuthSettings;
 
-        // Scheduled for removal in v12
-        [Obsolete("Please use the contructor that takes an IIpadressUtilities instead")]
-        public BasicAuthService(IOptionsMonitor optionsMonitor)
+    // Scheduled for removal in v12
+    [Obsolete("Please use the contructor that takes an IIpadressUtilities instead")]
+    public BasicAuthService(IOptionsMonitor optionsMonitor)
         : this(optionsMonitor, StaticServiceProvider.Instance.GetRequiredService())
+    {
+        _basicAuthSettings = optionsMonitor.CurrentValue;
+
+        optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
+    }
+
+    public BasicAuthService(IOptionsMonitor optionsMonitor, IIpAddressUtilities ipAddressUtilities)
+    {
+        _ipAddressUtilities = ipAddressUtilities;
+        _basicAuthSettings = optionsMonitor.CurrentValue;
+
+        optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
+    }
+
+    public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled;
+    public bool IsRedirectToLoginPageEnabled() => _basicAuthSettings.RedirectToLoginPage;
+
+    public bool IsIpAllowListed(IPAddress clientIpAddress)
+    {
+        foreach (var allowedIpString in _basicAuthSettings.AllowedIPs)
         {
-            _basicAuthSettings = optionsMonitor.CurrentValue;
-
-            optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
-        }
-
-        public BasicAuthService(IOptionsMonitor optionsMonitor, IIpAddressUtilities ipAddressUtilities)
-        {
-            _ipAddressUtilities = ipAddressUtilities;
-            _basicAuthSettings = optionsMonitor.CurrentValue;
-
-            optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
-        }
-
-        public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled;
-
-        public bool IsIpAllowListed(IPAddress clientIpAddress)
-        {
-            foreach (var allowedIpString in _basicAuthSettings.AllowedIPs)
+            if (_ipAddressUtilities.IsAllowListed(clientIpAddress, allowedIpString))
             {
-                if (_ipAddressUtilities.IsAllowListed(clientIpAddress, allowedIpString))
-                {
-                    return true;
-                }
+                return true;
             }
+        }
 
+        return false;
+    }
+
+    public bool HasCorrectSharedSecret(IDictionary headers)
+    {
+        var headerName = _basicAuthSettings.SharedSecret.HeaderName;
+        var sharedSecret = _basicAuthSettings.SharedSecret.Value;
+
+        if (string.IsNullOrWhiteSpace(headerName) || string.IsNullOrWhiteSpace(sharedSecret))
+        {
             return false;
         }
+
+        return headers.TryGetValue(headerName, out StringValues value) && value.Equals(sharedSecret);
     }
 }
diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs
index f823406818..f3fe533373 100644
--- a/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs
+++ b/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs
@@ -1,21 +1,17 @@
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services.Changes
+namespace Umbraco.Cms.Core.Services.Changes;
+
+public class ContentTypeChange
+    where TItem : class, IContentTypeComposition
 {
-    public class ContentTypeChange
-        where TItem : class, IContentTypeComposition
+    public ContentTypeChange(TItem item, ContentTypeChangeTypes changeTypes)
     {
-        public ContentTypeChange(TItem item, ContentTypeChangeTypes changeTypes)
-        {
-            Item = item;
-            ChangeTypes = changeTypes;
-        }
-
-        public TItem Item { get; }
-
-        public ContentTypeChangeTypes ChangeTypes { get; set; }
+        Item = item;
+        ChangeTypes = changeTypes;
     }
 
+    public TItem Item { get; }
+
+    public ContentTypeChangeTypes ChangeTypes { get; set; }
 }
diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs
index 9489e52d42..d45a2267bc 100644
--- a/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs
+++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs
@@ -1,33 +1,21 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System.Collections.Generic;
-using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+public static class ContentTypeChangeExtensions
 {
-    public static class ContentTypeChangeExtensions
-    {
+    public static bool HasType(this ContentTypeChangeTypes change, ContentTypeChangeTypes type) =>
+        (change & type) != ContentTypeChangeTypes.None;
 
-        public static bool HasType(this ContentTypeChangeTypes change, ContentTypeChangeTypes type)
-        {
-            return (change & type) != ContentTypeChangeTypes.None;
-        }
+    public static bool HasTypesAll(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (change & types) == types;
 
-        public static bool HasTypesAll(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) == types;
-        }
+    public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (change & types) != ContentTypeChangeTypes.None;
 
-        public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) != ContentTypeChangeTypes.None;
-        }
-
-        public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) == ContentTypeChangeTypes.None;
-        }
-    }
+    public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (change & types) == ContentTypeChangeTypes.None;
 }
diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs
index cd4965dc2b..4346a278cc 100644
--- a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs
+++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs
@@ -1,30 +1,27 @@
-using System;
+namespace Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services.Changes
+[Flags]
+public enum ContentTypeChangeTypes : byte
 {
-    [Flags]
-    public enum ContentTypeChangeTypes : byte
-    {
-        None = 0,
+    None = 0,
 
-        /// 
-        /// Item type has been created, no impact
-        /// 
-        Create = 1,
+    /// 
+    ///     Item type has been created, no impact
+    /// 
+    Create = 1,
 
-        /// 
-        /// Content type changes impact only the Content type being saved
-        /// 
-        RefreshMain = 2,
+    /// 
+    ///     Content type changes impact only the Content type being saved
+    /// 
+    RefreshMain = 2,
 
-        /// 
-        /// Content type changes impacts the content type being saved and others used that are composed of it
-        /// 
-        RefreshOther = 4, // changed, other change
+    /// 
+    ///     Content type changes impacts the content type being saved and others used that are composed of it
+    /// 
+    RefreshOther = 4, // changed, other change
 
-        /// 
-        /// Content type was removed
-        /// 
-        Remove = 8
-    }
+    /// 
+    ///     Content type was removed
+    /// 
+    Remove = 8,
 }
diff --git a/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs b/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs
index 25bf48e55a..303461f48f 100644
--- a/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs
+++ b/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs
@@ -1,10 +1,9 @@
-namespace Umbraco.Cms.Core.Services.Changes
+namespace Umbraco.Cms.Core.Services.Changes;
+
+public enum DomainChangeTypes : byte
 {
-    public enum DomainChangeTypes : byte
-    {
-        None = 0,
-        RefreshAll = 1,
-        Refresh = 2,
-        Remove = 3
-    }
+    None = 0,
+    RefreshAll = 1,
+    Refresh = 2,
+    Remove = 3,
 }
diff --git a/src/Umbraco.Core/Services/Changes/TreeChange.cs b/src/Umbraco.Core/Services/Changes/TreeChange.cs
index f306a796cc..bb722dce24 100644
--- a/src/Umbraco.Core/Services/Changes/TreeChange.cs
+++ b/src/Umbraco.Core/Services/Changes/TreeChange.cs
@@ -1,36 +1,28 @@
-using System.Collections.Generic;
-using System.Linq;
+namespace Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services.Changes
+public class TreeChange
 {
-    public class TreeChange
+    public TreeChange(TItem changedItem, TreeChangeTypes changeTypes)
     {
-        public TreeChange(TItem changedItem, TreeChangeTypes changeTypes)
+        Item = changedItem;
+        ChangeTypes = changeTypes;
+    }
+
+    public TItem Item { get; }
+
+    public TreeChangeTypes ChangeTypes { get; }
+
+    public EventArgs ToEventArgs() => new EventArgs(this);
+
+    public class EventArgs : System.EventArgs
+    {
+        public EventArgs(IEnumerable> changes) => Changes = changes.ToArray();
+
+        public EventArgs(TreeChange change)
+            : this(new[] { change })
         {
-            Item = changedItem;
-            ChangeTypes = changeTypes;
         }
 
-        public TItem Item { get; }
-        public TreeChangeTypes ChangeTypes { get; }
-
-        public EventArgs ToEventArgs()
-        {
-            return new EventArgs(this);
-        }
-
-        public class EventArgs : System.EventArgs
-        {
-            public EventArgs(IEnumerable> changes)
-            {
-                Changes = changes.ToArray();
-            }
-
-            public EventArgs(TreeChange change)
-                : this(new[] { change })
-            { }
-
-            public IEnumerable> Changes { get; private set; }
-        }
+        public IEnumerable> Changes { get; }
     }
 }
diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs
index 5de6ae9847..1dc972eb7a 100644
--- a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs
+++ b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs
@@ -1,36 +1,23 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+public static class TreeChangeExtensions
 {
-    public static class TreeChangeExtensions
-    {
-        public static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes)
-        {
-            return new TreeChange.EventArgs(changes);
-        }
+    public static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes) =>
+        new TreeChange.EventArgs(changes);
 
-        public static bool HasType(this TreeChangeTypes change, TreeChangeTypes type)
-        {
-            return (change & type) != TreeChangeTypes.None;
-        }
+    public static bool HasType(this TreeChangeTypes change, TreeChangeTypes type) =>
+        (change & type) != TreeChangeTypes.None;
 
-        public static bool HasTypesAll(this TreeChangeTypes change, TreeChangeTypes types)
-        {
-            return (change & types) == types;
-        }
+    public static bool HasTypesAll(this TreeChangeTypes change, TreeChangeTypes types) => (change & types) == types;
 
-        public static bool HasTypesAny(this TreeChangeTypes change, TreeChangeTypes types)
-        {
-            return (change & types) != TreeChangeTypes.None;
-        }
+    public static bool HasTypesAny(this TreeChangeTypes change, TreeChangeTypes types) =>
+        (change & types) != TreeChangeTypes.None;
 
-        public static bool HasTypesNone(this TreeChangeTypes change, TreeChangeTypes types)
-        {
-            return (change & types) == TreeChangeTypes.None;
-        }
-    }
+    public static bool HasTypesNone(this TreeChangeTypes change, TreeChangeTypes types) =>
+        (change & types) == TreeChangeTypes.None;
 }
diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs
index 9ef231ac06..85db740a56 100644
--- a/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs
+++ b/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs
@@ -1,25 +1,22 @@
-using System;
+namespace Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services.Changes
+[Flags]
+public enum TreeChangeTypes : byte
 {
-    [Flags]
-    public enum TreeChangeTypes : byte
-    {
-        None = 0,
+    None = 0,
 
-        // all items have been refreshed
-        RefreshAll = 1,
+    // all items have been refreshed
+    RefreshAll = 1,
 
-        // an item node has been refreshed
-        // with only local impact
-        RefreshNode = 2,
+    // an item node has been refreshed
+    // with only local impact
+    RefreshNode = 2,
 
-        // an item node has been refreshed
-        // with branch impact
-        RefreshBranch = 4,
+    // an item node has been refreshed
+    // with branch impact
+    RefreshBranch = 4,
 
-        // an item node has been removed
-        // never to return
-        Remove = 8,
-    }
+    // an item node has been removed
+    // never to return
+    Remove = 8,
 }
diff --git a/src/Umbraco.Core/Services/ConsentService.cs b/src/Umbraco.Core/Services/ConsentService.cs
index d37e2e4d0f..d7bb7af13e 100644
--- a/src/Umbraco.Core/Services/ConsentService.cs
+++ b/src/Umbraco.Core/Services/ConsentService.cs
@@ -1,81 +1,113 @@
-using System;
-using System.Collections.Generic;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Implements .
+/// 
+internal class ConsentService : RepositoryService, IConsentService
 {
+    private readonly IConsentRepository _consentRepository;
+
     /// 
-    /// Implements .
+    ///     Initializes a new instance of the  class.
     /// 
-    internal class ConsentService : RepositoryService, IConsentService
+    public ConsentService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IConsentRepository consentRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _consentRepository = consentRepository;
+
+    /// 
+    public IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null)
     {
-        private readonly IConsentRepository _consentRepository;
-
-        /// 
-        /// Initializes a new instance of the  class.
-        /// 
-        public ConsentService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IConsentRepository consentRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        // prevent stupid states
+        var v = 0;
+        if ((state & ConsentState.Pending) > 0)
         {
-            _consentRepository = consentRepository;
+            v++;
         }
 
-        /// 
-        public IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null)
+        if ((state & ConsentState.Granted) > 0)
         {
-            // prevent stupid states
-            var v = 0;
-            if ((state & ConsentState.Pending) > 0) v++;
-            if ((state & ConsentState.Granted) > 0) v++;
-            if ((state & ConsentState.Revoked) > 0) v++;
-            if (v != 1)
-                throw new ArgumentException("Invalid state.", nameof(state));
-
-            var consent = new Consent
-            {
-                Current = true,
-                Source = source,
-                Context = context,
-                Action = action,
-                CreateDate = DateTime.Now,
-                State = state,
-                Comment = comment
-            };
-
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _consentRepository.ClearCurrent(source, context, action);
-                _consentRepository.Save(consent);
-                scope.Complete();
-            }
-
-            return consent;
+            v++;
         }
 
-        /// 
-        public IEnumerable LookupConsent(string? source = null, string? context = null, string? action = null,
-            bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false,
-            bool includeHistory = false)
+        if ((state & ConsentState.Revoked) > 0)
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
+            v++;
+        }
+
+        if (v != 1)
+        {
+            throw new ArgumentException("Invalid state.", nameof(state));
+        }
+
+        var consent = new Consent
+        {
+            Current = true,
+            Source = source,
+            Context = context,
+            Action = action,
+            CreateDate = DateTime.Now,
+            State = state,
+            Comment = comment,
+        };
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _consentRepository.ClearCurrent(source, context, action);
+            _consentRepository.Save(consent);
+            scope.Complete();
+        }
+
+        return consent;
+    }
+
+    /// 
+    public IEnumerable LookupConsent(
+        string? source = null,
+        string? context = null,
+        string? action = null,
+        bool sourceStartsWith = false,
+        bool contextStartsWith = false,
+        bool actionStartsWith = false,
+        bool includeHistory = false)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query();
+
+            if (string.IsNullOrWhiteSpace(source) == false)
             {
-                var query = Query();
-
-                if (string.IsNullOrWhiteSpace(source) == false)
-                    query = sourceStartsWith ? query.Where(x => x.Source!.StartsWith(source)) : query.Where(x => x.Source == source);
-                if (string.IsNullOrWhiteSpace(context) == false)
-                    query = contextStartsWith ? query.Where(x => x.Context!.StartsWith(context)) : query.Where(x => x.Context == context);
-                if (string.IsNullOrWhiteSpace(action) == false)
-                    query = actionStartsWith ? query.Where(x => x.Action!.StartsWith(action)) : query.Where(x => x.Action == action);
-                if (includeHistory == false)
-                    query = query.Where(x => x.Current);
-
-                return _consentRepository.Get(query);
+                query = sourceStartsWith
+                    ? query.Where(x => x.Source!.StartsWith(source))
+                    : query.Where(x => x.Source == source);
             }
+
+            if (string.IsNullOrWhiteSpace(context) == false)
+            {
+                query = contextStartsWith
+                    ? query.Where(x => x.Context!.StartsWith(context))
+                    : query.Where(x => x.Context == context);
+            }
+
+            if (string.IsNullOrWhiteSpace(action) == false)
+            {
+                query = actionStartsWith
+                    ? query.Where(x => x.Action!.StartsWith(action))
+                    : query.Where(x => x.Action == action);
+            }
+
+            if (includeHistory == false)
+            {
+                query = query.Where(x => x.Current);
+            }
+
+            return _consentRepository.Get(query);
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index a2fa13a346..63a05dcd6e 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -1,6 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Exceptions;
@@ -14,1485 +12,1541 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services.Changes;
 using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Web.Common.DependencyInjection;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Implements the content service.
+/// 
+public class ContentService : RepositoryService, IContentService
 {
-    /// 
-    ///     Implements the content service.
-    /// 
-    public class ContentService : RepositoryService, IContentService
+    private readonly IAuditRepository _auditRepository;
+    private readonly IContentTypeRepository _contentTypeRepository;
+    private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
+    private readonly IDocumentRepository _documentRepository;
+    private readonly IEntityRepository _entityRepository;
+    private readonly ILanguageRepository _languageRepository;
+    private readonly ILogger _logger;
+    private readonly Lazy _propertyValidationService;
+    private readonly IShortStringHelper _shortStringHelper;
+    private readonly ICultureImpactFactory _cultureImpactFactory;
+    private IQuery? _queryNotTrashed;
+
+    #region Constructors
+
+        public ContentService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDocumentRepository documentRepository,
+        IEntityRepository entityRepository,
+        IAuditRepository auditRepository,
+        IContentTypeRepository contentTypeRepository,
+        IDocumentBlueprintRepository documentBlueprintRepository,
+        ILanguageRepository languageRepository,
+        Lazy propertyValidationService,
+        IShortStringHelper shortStringHelper,
+        ICultureImpactFactory cultureImpactFactory)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IAuditRepository _auditRepository;
-        private readonly IContentTypeRepository _contentTypeRepository;
-        private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
-        private readonly IDocumentRepository _documentRepository;
-        private readonly IEntityRepository _entityRepository;
-        private readonly ILanguageRepository _languageRepository;
-        private readonly ILogger _logger;
-        private readonly Lazy _propertyValidationService;
-        private readonly IShortStringHelper _shortStringHelper;
-        private IQuery? _queryNotTrashed;
+        _documentRepository = documentRepository;
+        _entityRepository = entityRepository;
+        _auditRepository = auditRepository;
+        _contentTypeRepository = contentTypeRepository;
+        _documentBlueprintRepository = documentBlueprintRepository;
+        _languageRepository = languageRepository;
+        _propertyValidationService = propertyValidationService;
+        _shortStringHelper = shortStringHelper;
+            _cultureImpactFactory = cultureImpactFactory;
+        _logger = loggerFactory.CreateLogger();
+    }
 
-        #region Constructors
+    [Obsolete("Use constructor that takes ICultureImpactService as a parameter, scheduled for removal in V12")]
+    public ContentService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDocumentRepository documentRepository,
+        IEntityRepository entityRepository,
+        IAuditRepository auditRepository,
+        IContentTypeRepository contentTypeRepository,
+        IDocumentBlueprintRepository documentBlueprintRepository,
+        ILanguageRepository languageRepository,
+        Lazy propertyValidationService,
+        IShortStringHelper shortStringHelper)
+        : this(
+            provider,
+            loggerFactory,
+            eventMessagesFactory,
+            documentRepository,
+            entityRepository,
+            auditRepository,
+            contentTypeRepository,
+            documentBlueprintRepository,
+            languageRepository,
+            propertyValidationService,
+            shortStringHelper,
+            StaticServiceProvider.Instance.GetRequiredService())
+    {
+    }
 
-        public ContentService(ICoreScopeProvider provider, ILoggerFactory loggerFactory,
-            IEventMessagesFactory eventMessagesFactory,
-            IDocumentRepository documentRepository, IEntityRepository entityRepository,
-            IAuditRepository auditRepository,
-            IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository,
-            ILanguageRepository languageRepository,
-            Lazy propertyValidationService, IShortStringHelper shortStringHelper)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    #endregion
+
+    #region Static queries
+
+    // lazy-constructed because when the ctor runs, the query factory may not be ready
+    private IQuery QueryNotTrashed =>
+        _queryNotTrashed ??= Query().Where(x => x.Trashed == false);
+
+    #endregion
+
+    #region Rollback
+
+    public OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        // Get the current copy of the node
+        IContent? content = GetById(id);
+
+        // Get the version
+        IContent? version = GetVersion(versionId);
+
+        // Good old null checks
+        if (content == null || version == null || content.Trashed)
         {
-            _documentRepository = documentRepository;
-            _entityRepository = entityRepository;
-            _auditRepository = auditRepository;
-            _contentTypeRepository = contentTypeRepository;
-            _documentBlueprintRepository = documentBlueprintRepository;
-            _languageRepository = languageRepository;
-            _propertyValidationService = propertyValidationService;
-            _shortStringHelper = shortStringHelper;
-            _logger = loggerFactory.CreateLogger();
+            return new OperationResult(OperationResultType.FailedCannot, evtMsgs);
         }
 
-        #endregion
+        // Store the result of doing the save of content for the rollback
+        OperationResult rollbackSaveResult;
 
-        #region Static queries
-
-        // lazy-constructed because when the ctor runs, the query factory may not be ready
-
-        private IQuery QueryNotTrashed =>
-            _queryNotTrashed ??= Query().Where(x => x.Trashed == false);
-
-        #endregion
-
-        #region Rollback
-
-        public OperationResult Rollback(int id, int versionId, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            // Get the current copy of the node
-            IContent? content = GetById(id);
-
-            // Get the version
-            IContent? version = GetVersion(versionId);
-
-            // Good old null checks
-            if (content == null || version == null || content.Trashed)
+            var rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(rollingBackNotification))
             {
-                return new OperationResult(OperationResultType.FailedCannot, evtMsgs);
-            }
-
-            // Store the result of doing the save of content for the rollback
-            OperationResult rollbackSaveResult;
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(rollingBackNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(evtMsgs);
-                }
-
-                // Copy the changes from the version
-                content.CopyFrom(version, culture);
-
-                // Save the content for the rollback
-                rollbackSaveResult = Save(content, userId);
-
-                // Depending on the save result - is what we log & audit along with what we return
-                if (rollbackSaveResult.Success == false)
-                {
-                    // Log the error/warning
-                    _logger.LogError(
-                        "User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId,
-                        id, versionId);
-                }
-                else
-                {
-                    scope.Notifications.Publish(
-                        new ContentRolledBackNotification(content, evtMsgs).WithStateFrom(rollingBackNotification));
-
-                    // Logging & Audit message
-                    _logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'",
-                        userId, id, versionId);
-                    Audit(AuditType.RollBack, userId, id,
-                        $"Content '{content.Name}' was rolled back to version '{versionId}'");
-                }
-
                 scope.Complete();
+                return OperationResult.Cancel(evtMsgs);
             }
 
-            return rollbackSaveResult;
-        }
+            // Copy the changes from the version
+            content.CopyFrom(version, culture);
 
-        #endregion
+            // Save the content for the rollback
+            rollbackSaveResult = Save(content, userId);
 
-        #region Count
-
-        public int CountPublished(string? contentTypeAlias = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            // Depending on the save result - is what we log & audit along with what we return
+            if (rollbackSaveResult.Success == false)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountPublished(contentTypeAlias);
+                // Log the error/warning
+                _logger.LogError(
+                    "User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId);
             }
-        }
-
-        public int Count(string? contentTypeAlias = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            else
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Count(contentTypeAlias);
+                scope.Notifications.Publish(
+                    new ContentRolledBackNotification(content, evtMsgs).WithStateFrom(rollingBackNotification));
+
+                // Logging & Audit message
+                _logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId);
+                Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'");
             }
+
+            scope.Complete();
         }
 
-        public int CountChildren(int parentId, string? contentTypeAlias = null)
+        return rollbackSaveResult;
+    }
+
+    #endregion
+
+    #region Count
+
+    public int CountPublished(string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountPublished(contentTypeAlias);
+        }
+    }
+
+    public int Count(string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Count(contentTypeAlias);
+        }
+    }
+
+    public int CountChildren(int parentId, string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountChildren(parentId, contentTypeAlias);
+        }
+    }
+
+    public int CountDescendants(int parentId, string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountDescendants(parentId, contentTypeAlias);
+        }
+    }
+
+    #endregion
+
+    #region Permissions
+
+    /// 
+    ///     Used to bulk update the permissions set for a content item. This will replace all permissions
+    ///     assigned to an entity with a list of user id & permission pairs.
+    /// 
+    /// 
+    public void SetPermissions(EntityPermissionSet permissionSet)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.ReplaceContentPermissions(permissionSet);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Assigns a single permission to the current content item for the specified group ids
+    /// 
+    /// 
+    /// 
+    /// 
+    public void SetPermission(IContent entity, char permission, IEnumerable groupIds)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.AssignEntityPermission(entity, permission, groupIds);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Returns implicit/inherited permissions assigned to the content item for all user groups
+    /// 
+    /// 
+    /// 
+    public EntityPermissionCollection GetPermissions(IContent content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPermissionsForEntity(content.Id);
+        }
+    }
+
+    #endregion
+
+    #region Create
+
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Content should based on.
+    /// 
+    /// 
+    ///     Note that using this method will simply return a new IContent without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new content objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Content object
+    /// Id of Parent for the new Content
+    /// Alias of the 
+    /// Optional id of the user creating the content
+    /// 
+    ///     
+    /// 
+    public IContent Create(string name, Guid parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        IContent? parent = GetById(parentId);
+        return Create(name, parent, contentTypeAlias, userId);
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type.
+    /// 
+    /// 
+    ///     This method simply returns a new, non-persisted, IContent without any identity. It
+    ///     is intended as a shortcut to creating new content objects that does not invoke a save
+    ///     operation against the database.
+    /// 
+    /// The name of the content object.
+    /// The identifier of the parent, or -1.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent Create(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        IContentType contentType = GetContentType(contentTypeAlias);
+        return Create(name, parentId, contentType, userId);
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type.
+    /// 
+    /// 
+    ///     This method simply returns a new, non-persisted, IContent without any identity. It
+    ///     is intended as a shortcut to creating new content objects that does not invoke a save
+    ///     operation against the database.
+    /// 
+    /// The name of the content object.
+    /// The identifier of the parent, or -1.
+    /// The content type of the content
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId)
+    {
+        if (contentType is null)
+        {
+            throw new ArgumentException("Content type must be specified", nameof(contentType));
+        }
+
+        IContent? parent = parentId > 0 ? GetById(parentId) : null;
+        if (parentId > 0 && parent is null)
+        {
+            throw new ArgumentException("No content with that id.", nameof(parentId));
+        }
+
+        var content = new Content(name, parentId, contentType, userId);
+
+        return content;
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type, under a parent.
+    /// 
+    /// 
+    ///     This method simply returns a new, non-persisted, IContent without any identity. It
+    ///     is intended as a shortcut to creating new content objects that does not invoke a save
+    ///     operation against the database.
+    /// 
+    /// The name of the content object.
+    /// The parent content object.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent Create(string name, IContent? parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        if (parent == null)
+        {
+            throw new ArgumentNullException(nameof(parent));
+        }
+
+        IContentType contentType = GetContentType(contentTypeAlias);
+        if (contentType == null)
+        {
+            throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
+        }
+
+        var content = new Content(name, parent, contentType, userId);
+
+        return content;
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type.
+    /// 
+    /// This method returns a new, persisted, IContent with an identity.
+    /// The name of the content object.
+    /// The identifier of the parent, or -1.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // locking the content tree secures content types too
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            IContentType contentType = GetContentType(contentTypeAlias); // + locks
+            if (contentType == null)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountChildren(parentId, contentTypeAlias);
+                throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
             }
-        }
 
-        public int CountDescendants(int parentId, string? contentTypeAlias = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            IContent? parent = parentId > 0 ? GetById(parentId) : null; // + locks
+            if (parentId > 0 && parent == null)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountDescendants(parentId, contentTypeAlias);
-            }
-        }
-
-        #endregion
-
-        #region Permissions
-
-        /// 
-        ///     Used to bulk update the permissions set for a content item. This will replace all permissions
-        ///     assigned to an entity with a list of user id & permission pairs.
-        /// 
-        /// 
-        public void SetPermissions(EntityPermissionSet permissionSet)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.ReplaceContentPermissions(permissionSet);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Assigns a single permission to the current content item for the specified group ids
-        /// 
-        /// 
-        /// 
-        /// 
-        public void SetPermission(IContent entity, char permission, IEnumerable groupIds)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.AssignEntityPermission(entity, permission, groupIds);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Returns implicit/inherited permissions assigned to the content item for all user groups
-        /// 
-        /// 
-        /// 
-        public EntityPermissionCollection GetPermissions(IContent content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetPermissionsForEntity(content.Id);
-            }
-        }
-
-        #endregion
-
-        #region Create
-
-        /// 
-        ///     Creates an  object using the alias of the 
-        ///     that this Content should based on.
-        /// 
-        /// 
-        ///     Note that using this method will simply return a new IContent without any identity
-        ///     as it has not yet been persisted. It is intended as a shortcut to creating new content objects
-        ///     that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Content object
-        /// Id of Parent for the new Content
-        /// Alias of the 
-        /// Optional id of the user creating the content
-        /// 
-        ///     
-        /// 
-        public IContent Create(string name, Guid parentId, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
-        {
-            // TODO: what about culture?
-
-            IContent? parent = GetById(parentId);
-            return Create(name, parent, contentTypeAlias, userId);
-        }
-
-        /// 
-        ///     Creates an  object of a specified content type.
-        /// 
-        /// 
-        ///     This method simply returns a new, non-persisted, IContent without any identity. It
-        ///     is intended as a shortcut to creating new content objects that does not invoke a save
-        ///     operation against the database.
-        /// 
-        /// The name of the content object.
-        /// The identifier of the parent, or -1.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent Create(string name, int parentId, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
-        {
-            // TODO: what about culture?
-
-            IContentType contentType = GetContentType(contentTypeAlias);
-            return Create(name, parentId, contentType, userId);
-        }
-
-        /// 
-        ///     Creates an  object of a specified content type.
-        /// 
-        /// 
-        ///     This method simply returns a new, non-persisted, IContent without any identity. It
-        ///     is intended as a shortcut to creating new content objects that does not invoke a save
-        ///     operation against the database.
-        /// 
-        /// The name of the content object.
-        /// The identifier of the parent, or -1.
-        /// The content type of the content
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent Create(string name, int parentId, IContentType contentType,
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (contentType is null)
-            {
-                throw new ArgumentException("Content type must be specified", nameof(contentType));
+                throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
             }
 
-            IContent? parent = parentId > 0 ? GetById(parentId) : null;
-            if (parentId > 0 && parent is null)
-            {
-                throw new ArgumentException("No content with that id.", nameof(parentId));
-            }
+            Content content = parentId > 0
+                ? new Content(name, parent!, contentType, userId)
+                : new Content(name, parentId, contentType, userId);
 
-            var content = new Content(name, parentId, contentType, userId);
+            Save(content, userId);
 
             return content;
         }
+    }
 
-        /// 
-        ///     Creates an  object of a specified content type, under a parent.
-        /// 
-        /// 
-        ///     This method simply returns a new, non-persisted, IContent without any identity. It
-        ///     is intended as a shortcut to creating new content objects that does not invoke a save
-        ///     operation against the database.
-        /// 
-        /// The name of the content object.
-        /// The parent content object.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent Create(string name, IContent? parent, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
+    /// 
+    ///     Creates an  object of a specified content type, under a parent.
+    /// 
+    /// This method returns a new, persisted, IContent with an identity.
+    /// The name of the content object.
+    /// The parent content object.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        if (parent == null)
         {
-            // TODO: what about culture?
+            throw new ArgumentNullException(nameof(parent));
+        }
 
-            if (parent == null)
-            {
-                throw new ArgumentNullException(nameof(parent));
-            }
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // locking the content tree secures content types too
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            IContentType contentType = GetContentType(contentTypeAlias);
+            IContentType contentType = GetContentType(contentTypeAlias); // + locks
             if (contentType == null)
             {
-                throw new ArgumentException("No content type with that alias.",
-                    nameof(contentTypeAlias)); // causes rollback
+                throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
             }
 
             var content = new Content(name, parent, contentType, userId);
 
+            Save(content, userId);
+
             return content;
         }
+    }
 
-        /// 
-        ///     Creates an  object of a specified content type.
-        /// 
-        /// This method returns a new, persisted, IContent with an identity.
-        /// The name of the content object.
-        /// The identifier of the parent, or -1.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent CreateAndSave(string name, int parentId, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
+    #endregion
+
+    #region Get, Has, Is
+
+    /// 
+    ///     Gets an  object by Id
+    /// 
+    /// Id of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IContent? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            // TODO: what about culture?
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(id);
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // locking the content tree secures content types too
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                IContentType contentType = GetContentType(contentTypeAlias); // + locks
-                if (contentType == null)
-                {
-                    throw new ArgumentException("No content type with that alias.",
-                        nameof(contentTypeAlias)); // causes rollback
-                }
-
-                IContent? parent = parentId > 0 ? GetById(parentId) : null; // + locks
-                if (parentId > 0 && parent == null)
-                {
-                    throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
-                }
-
-                Content content = parentId > 0
-                    ? new Content(name, parent!, contentType, userId)
-                    : new Content(name, parentId, contentType, userId);
-
-                Save(content, userId);
-
-                return content;
-            }
+    /// 
+    ///     Gets an  object by Id
+    /// 
+    /// Ids of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        var idsA = ids.ToArray();
+        if (idsA.Length == 0)
+        {
+            return Enumerable.Empty();
         }
 
-        /// 
-        ///     Creates an  object of a specified content type, under a parent.
-        /// 
-        /// This method returns a new, persisted, IContent with an identity.
-        /// The name of the content object.
-        /// The parent content object.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            // TODO: what about culture?
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IEnumerable items = _documentRepository.GetMany(idsA);
+            var index = items.ToDictionary(x => x.Id, x => x);
+            return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
+        }
+    }
 
-            if (parent == null)
-            {
-                throw new ArgumentNullException(nameof(parent));
-            }
+    /// 
+    ///     Gets an  object by its 'UniqueId'
+    /// 
+    /// Guid key of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IContent? GetById(Guid key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(key);
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // locking the content tree secures content types too
-                scope.WriteLock(Constants.Locks.ContentTree);
+    /// 
+    public ContentScheduleCollection GetContentScheduleByContentId(int contentId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentSchedule(contentId);
+        }
+    }
 
-                IContentType contentType = GetContentType(contentTypeAlias); // + locks
-                if (contentType == null)
-                {
-                    throw new ArgumentException("No content type with that alias.",
-                        nameof(contentTypeAlias)); // causes rollback
-                }
+    /// 
+    public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.PersistContentSchedule(content, contentSchedule);
+        }
+    }
 
-                var content = new Content(name, parent, contentType, userId);
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt IContentServiceBase.Save(IEnumerable contents, int userId) =>
+        Attempt.Succeed(Save(contents, userId));
 
-                Save(content, userId);
-
-                return content;
-            }
+    /// 
+    ///     Gets  objects by Ids
+    /// 
+    /// Ids of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        Guid[] idsA = ids.ToArray();
+        if (idsA.Length == 0)
+        {
+            return Enumerable.Empty();
         }
 
-        #endregion
-
-        #region Get, Has, Is
-
-        /// 
-        ///     Gets an  object by Id
-        /// 
-        /// Id of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IContent? GetById(int id)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(id);
-            }
-        }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IEnumerable? items = _documentRepository.GetMany(idsA);
 
-        /// 
-        ///     Gets an  object by Id
-        /// 
-        /// Ids of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IEnumerable GetByIds(IEnumerable ids)
-        {
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0)
+            if (items is not null)
             {
-                return Enumerable.Empty();
-            }
+                var index = items.ToDictionary(x => x.Key, x => x);
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IEnumerable items = _documentRepository.GetMany(idsA);
-                var index = items.ToDictionary(x => x.Id, x => x);
                 return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
             }
+
+            return Enumerable.Empty();
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedOfType(
+        int contentTypeId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        ///     Gets an  object by its 'UniqueId'
-        /// 
-        /// Guid key of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IContent? GetById(Guid key)
+        if (pageSize <= 0)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(key);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        /// 
-        public ContentScheduleCollection GetContentScheduleByContentId(int contentId)
+        if (ordering == null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.ContentTree);
-                return _documentRepository.GetContentSchedule(contentId);
-            }
+            ordering = Ordering.By("sortOrder");
         }
 
-        /// 
-        public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.WriteLock(Cms.Core.Constants.Locks.ContentTree);
-                _documentRepository.PersistContentSchedule(content, contentSchedule);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPage(
+                Query()?.Where(x => x.ContentTypeId == contentTypeId),
+                pageIndex,
+                pageSize,
+                out totalRecords,
+                filter,
+                ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt IContentServiceBase.Save(IEnumerable contents, int userId) =>
-            Attempt.Succeed(Save(contents, userId));
-
-        /// 
-        ///     Gets  objects by Ids
-        /// 
-        /// Ids of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IEnumerable GetByIds(IEnumerable ids)
+        if (pageSize <= 0)
         {
-            Guid[] idsA = ids.ToArray();
-            if (idsA.Length == 0)
-            {
-                return Enumerable.Empty();
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
+        }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IEnumerable? items = _documentRepository.GetMany(idsA);
+        if (ordering == null)
+        {
+            ordering = Ordering.By("sortOrder");
+        }
 
-                if (items is not null)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPage(
+                Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)),
+                pageIndex,
+                pageSize,
+                out totalRecords,
+                filter,
+                ordering);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of  objects by Level
+    /// 
+    /// The level to retrieve Content from
+    /// An Enumerable list of  objects
+    /// Contrary to most methods, this method filters out trashed content items.
+    public IEnumerable GetByLevel(int level)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery? query = Query().Where(x => x.Level == level && x.Trashed == false);
+            return _documentRepository.Get(query);
+        }
+    }
+
+    /// 
+    ///     Gets a specific version of an  item.
+    /// 
+    /// Id of the version to retrieve
+    /// An  item
+    public IContent? GetVersion(int versionId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetVersion(versionId);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of an  objects versions by Id
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetVersions(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetAllVersions(id);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of an  objects versions by Id
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetVersionsSlim(int id, int skip, int take)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetAllVersionsSlim(id, skip, take);
+        }
+    }
+
+    /// 
+    ///     Gets a list of all version Ids for the given content item ordered so latest is first
+    /// 
+    /// 
+    /// The maximum number of rows to return
+    /// 
+    public IEnumerable GetVersionIds(int id, int maxRows)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _documentRepository.GetVersionIds(id, maxRows);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current content.
+    /// 
+    /// Id of the  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    public IEnumerable GetAncestors(int id)
+    {
+        // intentionally not locking
+        IContent? content = GetById(id);
+        if (content is null)
+        {
+            return Enumerable.Empty();
+        }
+
+        return GetAncestors(content);
+    }
+
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current content.
+    /// 
+    ///  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    public IEnumerable GetAncestors(IContent content)
+    {
+        // null check otherwise we get exceptions
+        if (content.Path.IsNullOrWhiteSpace())
+        {
+            return Enumerable.Empty();
+        }
+
+        var ids = content.GetAncestorIds()?.ToArray();
+        if (ids?.Any() == false)
+        {
+            return new List();
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetMany(ids!);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of published  objects by Parent Id
+    /// 
+    /// Id of the Parent to retrieve Children from
+    /// An Enumerable list of published  objects
+    public IEnumerable GetPublishedChildren(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery? query = Query().Where(x => x.ParentId == id && x.Published);
+            return _documentRepository.Get(query).OrderBy(x => x.SortOrder);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
+        }
+
+        if (pageSize <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
+        }
+
+        if (ordering == null)
+        {
+            ordering = Ordering.By("sortOrder");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+
+            IQuery? query = Query()?.Where(x => x.ParentId == id);
+            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
+    {
+        if (ordering == null)
+        {
+            ordering = Ordering.By("Path");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+
+            // if the id is System Root, then just get all
+            if (id != Constants.System.Root)
+            {
+                TreeEntityPath[] contentPath =
+                    _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray();
+                if (contentPath.Length == 0)
                 {
-                    var index = items.ToDictionary(x => x.Key, x => x);
-
-                    return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
+                    totalChildren = 0;
+                    return Enumerable.Empty();
                 }
 
-                return Enumerable.Empty();
+                return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
             }
-        }
 
-        /// 
-        public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize,
-            out long totalRecords
-            , IQuery? filter = null, Ordering? ordering = null)
+            return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
+        }
+    }
+
+    private IQuery? GetPagedDescendantQuery(string contentPath)
+    {
+        IQuery? query = Query();
+        if (!contentPath.IsNullOrWhiteSpace())
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetPage(
-                    Query()?.Where(x => x.ContentTypeId == contentTypeId),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
+            query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
         }
 
-        /// 
-        public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize,
-            out long totalRecords, IQuery? filter, Ordering? ordering = null)
+        return query;
+    }
+
+    private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize, out long totalChildren, IQuery? filter, Ordering? ordering)
+    {
+        if (pageIndex < 0)
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetPage(
-                    Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        ///     Gets a collection of  objects by Level
-        /// 
-        /// The level to retrieve Content from
-        /// An Enumerable list of  objects
-        /// Contrary to most methods, this method filters out trashed content items.
-        public IEnumerable GetByLevel(int level)
+        if (pageSize <= 0)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery? query = Query().Where(x => x.Level == level && x.Trashed == false);
-                return _documentRepository.Get(query);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        /// 
-        ///     Gets a specific version of an  item.
-        /// 
-        /// Id of the version to retrieve
-        /// An  item
-        public IContent? GetVersion(int versionId)
+        if (ordering == null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetVersion(versionId);
-            }
+            throw new ArgumentNullException(nameof(ordering));
         }
 
-        /// 
-        ///     Gets a collection of an  objects versions by Id
-        /// 
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetVersions(int id)
+        return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
+    }
+
+    /// 
+    ///     Gets the parent of the current content as an  item.
+    /// 
+    /// Id of the  to retrieve the parent from
+    /// Parent  object
+    public IContent? GetParent(int id)
+    {
+        // intentionally not locking
+        IContent? content = GetById(id);
+        return GetParent(content);
+    }
+
+    /// 
+    ///     Gets the parent of the current content as an  item.
+    /// 
+    ///  to retrieve the parent from
+    /// Parent  object
+    public IContent? GetParent(IContent? content)
+    {
+        if (content?.ParentId == Constants.System.Root || content?.ParentId == Constants.System.RecycleBinContent ||
+            content is null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetAllVersions(id);
-            }
+            return null;
         }
 
-        /// 
-        ///     Gets a collection of an  objects versions by Id
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetVersionsSlim(int id, int skip, int take)
+        return GetById(content.ParentId);
+    }
+
+    /// 
+    ///     Gets a collection of  objects, which reside at the first level / root
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetRootContent()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetAllVersionsSlim(id, skip, take);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery query = Query().Where(x => x.ParentId == Constants.System.Root);
+            return _documentRepository.Get(query);
         }
+    }
 
-        /// 
-        ///     Gets a list of all version Ids for the given content item ordered so latest is first
-        /// 
-        /// 
-        /// The maximum number of rows to return
-        /// 
-        public IEnumerable GetVersionIds(int id, int maxRows)
+    /// 
+    ///     Gets all published content items
+    /// 
+    /// 
+    internal IEnumerable GetAllPublished()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _documentRepository.GetVersionIds(id, maxRows);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(QueryNotTrashed);
         }
+    }
 
-        /// 
-        ///     Gets a collection of  objects, which are ancestors of the current content.
-        /// 
-        /// Id of the  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        public IEnumerable GetAncestors(int id)
+    /// 
+    public IEnumerable GetContentForExpiration(DateTime date)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            // intentionally not locking
-            IContent? content = GetById(id);
-            if (content is null)
-            {
-                return Enumerable.Empty();
-            }
-
-            return GetAncestors(content);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentForExpiration(date);
         }
+    }
 
-        /// 
-        ///     Gets a collection of  objects, which are ancestors of the current content.
-        /// 
-        ///  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        public IEnumerable GetAncestors(IContent content)
+    /// 
+    public IEnumerable GetContentForRelease(DateTime date)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            //null check otherwise we get exceptions
-            if (content.Path.IsNullOrWhiteSpace())
-            {
-                return Enumerable.Empty();
-            }
-
-            var ids = content.GetAncestorIds()?.ToArray();
-            if (ids?.Any() == false)
-            {
-                return new List();
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetMany(ids!);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentForRelease(date);
         }
+    }
 
-        /// 
-        ///     Gets a collection of published  objects by Parent Id
-        /// 
-        /// Id of the Parent to retrieve Children from
-        /// An Enumerable list of published  objects
-        public IEnumerable GetPublishedChildren(int id)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery? query = Query().Where(x => x.ParentId == id && x.Published);
-                return _documentRepository.Get(query).OrderBy(x => x.SortOrder);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-
-                IQuery? query = Query()?.Where(x => x.ParentId == id);
-                return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+    /// 
+    ///     Gets a collection of an  objects, which resides in the Recycle Bin
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
             if (ordering == null)
             {
                 ordering = Ordering.By("Path");
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery? query = Query()?
+                .Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix));
+            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
 
-                //if the id is System Root, then just get all
-                if (id != Constants.System.Root)
-                {
-                    TreeEntityPath[] contentPath =
-                        _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray();
-                    if (contentPath.Length == 0)
-                    {
-                        totalChildren = 0;
-                        return Enumerable.Empty();
-                    }
+    /// 
+    ///     Checks whether an  item has any children
+    /// 
+    /// Id of the 
+    /// True if the content has any children otherwise False
+    public bool HasChildren(int id) => CountChildren(id) > 0;
 
-                    return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize,
-                        out totalChildren, filter, ordering);
-                }
-
-                return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
-            }
+    /// 
+    ///     Checks if the passed in  can be published based on the ancestors publish state.
+    /// 
+    ///  to check if ancestors are published
+    /// True if the Content can be published, otherwise False
+    public bool IsPathPublishable(IContent content)
+    {
+        // fast
+        if (content.ParentId == Constants.System.Root)
+        {
+            return true; // root content is always publishable
         }
 
-        private IQuery? GetPagedDescendantQuery(string contentPath)
+        if (content.Trashed)
         {
-            IQuery? query = Query();
-            if (!contentPath.IsNullOrWhiteSpace())
-            {
-                query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
-            }
-
-            return query;
+            return false; // trashed content is never publishable
         }
 
-        private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize,
-            out long totalChildren,
-            IQuery? filter, Ordering? ordering)
+        // not trashed and has a parent: publishable if the parent is path-published
+        IContent? parent = GetById(content.ParentId);
+        return parent == null || IsPathPublished(parent);
+    }
+
+    public bool IsPathPublished(IContent? content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.IsPathPublished(content);
+        }
+    }
 
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
+    #endregion
 
-            if (ordering == null)
-            {
-                throw new ArgumentNullException(nameof(ordering));
-            }
+    #region Save, Publish, Unpublish
 
-            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
+    /// 
+    public OperationResult Save(IContent content, int? userId = null, ContentScheduleCollection? contentSchedule = null)
+    {
+        PublishedState publishedState = content.PublishedState;
+        if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+        {
+            throw new InvalidOperationException(
+                $"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method.");
         }
 
-        /// 
-        ///     Gets the parent of the current content as an  item.
-        /// 
-        /// Id of the  to retrieve the parent from
-        /// Parent  object
-        public IContent? GetParent(int id)
+        if (content.Name != null && content.Name.Length > 255)
         {
-            // intentionally not locking
-            IContent? content = GetById(id);
-            return GetParent(content);
+            throw new InvalidOperationException(
+                $"Content with the name {content.Name} cannot be more than 255 characters in length.");
         }
 
-        /// 
-        ///     Gets the parent of the current content as an  item.
-        /// 
-        ///  to retrieve the parent from
-        /// Parent  object
-        public IContent? GetParent(IContent? content)
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (content?.ParentId == Constants.System.Root || content?.ParentId == Constants.System.RecycleBinContent ||
-                content is null)
+            var savingNotification = new ContentSavingNotification(content, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                return null;
-            }
-
-            return GetById(content.ParentId);
-        }
-
-        /// 
-        ///     Gets a collection of  objects, which reside at the first level / root
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetRootContent()
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery query = Query().Where(x => x.ParentId == Constants.System.Root);
-                return _documentRepository.Get(query);
-            }
-        }
-
-        /// 
-        ///     Gets all published content items
-        /// 
-        /// 
-        internal IEnumerable GetAllPublished()
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(QueryNotTrashed);
-            }
-        }
-
-        /// 
-        public IEnumerable GetContentForExpiration(DateTime date)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetContentForExpiration(date);
-            }
-        }
-
-        /// 
-        public IEnumerable GetContentForRelease(DateTime date)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetContentForRelease(date);
-            }
-        }
-
-        /// 
-        ///     Gets a collection of an  objects, which resides in the Recycle Bin
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                if (ordering == null)
-                {
-                    ordering = Ordering.By("Path");
-                }
-
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery? query = Query()?
-                    .Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix));
-                return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        ///     Checks whether an  item has any children
-        /// 
-        /// Id of the 
-        /// True if the content has any children otherwise False
-        public bool HasChildren(int id) => CountChildren(id) > 0;
-
-        /// 
-        ///     Checks if the passed in  can be published based on the ancestors publish state.
-        /// 
-        ///  to check if ancestors are published
-        /// True if the Content can be published, otherwise False
-        public bool IsPathPublishable(IContent content)
-        {
-            // fast
-            if (content.ParentId == Constants.System.Root)
-            {
-                return true; // root content is always publishable
-            }
-
-            if (content.Trashed)
-            {
-                return false; // trashed content is never publishable
-            }
-
-            // not trashed and has a parent: publishable if the parent is path-published
-            IContent? parent = GetById(content.ParentId);
-            return parent == null || IsPathPublished(parent);
-        }
-
-        public bool IsPathPublished(IContent? content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.IsPathPublished(content);
-            }
-        }
-
-        #endregion
-
-        #region Save, Publish, Unpublish
-
-        /// 
-        public OperationResult Save(IContent content, int? userId = null,
-            ContentScheduleCollection? contentSchedule = null)
-        {
-            PublishedState publishedState = content.PublishedState;
-            if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
-            {
-                throw new InvalidOperationException(
-                    $"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method.");
-            }
-
-            if (content.Name != null && content.Name.Length > 255)
-            {
-                throw new InvalidOperationException(
-                    $"Content with the name {content.Name} cannot be more than 255 characters in length.");
-            }
-
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingNotification = new ContentSavingNotification(content, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                userId ??= Constants.Security.SuperUserId;
-
-                if (content.HasIdentity == false)
-                {
-                    content.CreatorId = userId.Value;
-                }
-
-                content.WriterId = userId.Value;
-
-                //track the cultures that have changed
-                List? culturesChanging = content.ContentType.VariesByCulture()
-                    ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
-                    : null;
-                // TODO: Currently there's no way to change track which variant properties have changed, we only have change
-                // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
-                // in this particular case, determining which cultures have changed works with the above with names since it will
-                // have always changed if it's been saved in the back office but that's not really fail safe.
-
-                _documentRepository.Save(content);
-
-                if (contentSchedule != null)
-                {
-                    _documentRepository.PersistContentSchedule(content, contentSchedule);
-                }
-
-                scope.Notifications.Publish(
-                    new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification));
-
-                // TODO: we had code here to FORCE that this event can never be suppressed. But that just doesn't make a ton of sense?!
-                // I understand that if its suppressed that the caches aren't updated, but that would be expected. If someone
-                // is supressing events then I think it's expected that nothing will happen. They are probably doing it for perf
-                // reasons like bulk import and in those cases we don't want this occuring.
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, eventMessages));
-
-                if (culturesChanging != null)
-                {
-                    var languages = _languageRepository.GetMany()?
-                        .Where(x => culturesChanging.InvariantContains(x.IsoCode))
-                        .Select(x => x.CultureName);
-                    if (languages is not null)
-                    {
-                        var langs = string.Join(", ", languages);
-                        Audit(AuditType.SaveVariant, userId.Value, content.Id, $"Saved languages: {langs}", langs);
-                    }
-                }
-                else
-                {
-                    Audit(AuditType.Save, userId.Value, content.Id);
-                }
-
                 scope.Complete();
+                return OperationResult.Cancel(eventMessages);
             }
 
-            return OperationResult.Succeed(eventMessages);
-        }
+            scope.WriteLock(Constants.Locks.ContentTree);
+            userId ??= Constants.Security.SuperUserId;
 
-        /// 
-        public OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            IContent[] contentsA = contents.ToArray();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            if (content.HasIdentity == false)
             {
-                var savingNotification = new ContentSavingNotification(contentsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                foreach (IContent content in contentsA)
-                {
-                    if (content.HasIdentity == false)
-                    {
-                        content.CreatorId = userId;
-                    }
-
-                    content.WriterId = userId;
-
-                    _documentRepository.Save(content);
-                }
-
-                scope.Notifications.Publish(
-                    new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification));
-                // TODO: See note above about supressing events
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages));
-
-                Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
-
-                scope.Complete();
+                content.CreatorId = userId.Value;
             }
 
-            return OperationResult.Succeed(eventMessages);
-        }
+            content.WriterId = userId.Value;
 
-        /// 
-        public PublishResult SaveAndPublish(IContent content, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            PublishedState publishedState = content.PublishedState;
-            if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
-            {
-                throw new InvalidOperationException(
-                    $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
-            }
-
-            // cannot accept invariant (null or empty) culture for variant content type
-            // cannot accept a specific culture for invariant content type (but '*' is ok)
-            if (content.ContentType.VariesByCulture())
-            {
-                if (culture.IsNullOrWhiteSpace())
-                {
-                    throw new NotSupportedException("Invariant culture is not supported by variant content types.");
-                }
-            }
-            else
-            {
-                if (!culture.IsNullOrWhiteSpace() && culture != "*")
-                {
-                    throw new NotSupportedException(
-                        $"Culture \"{culture}\" is not supported by invariant content types.");
-                }
-            }
-
-            if (content.Name != null && content.Name.Length > 255)
-            {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                // if culture is specific, first publish the invariant values, then publish the culture itself.
-                // if culture is '*', then publish them all (including variants)
-
-                //this will create the correct culture impact even if culture is * or null
-                var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
-
-                // publish the culture(s)
-                // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
-                content.PublishCulture(impact);
-
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
-        }
-
-        /// 
-        public PublishResult SaveAndPublish(IContent content, string[] cultures,
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
-
-            if (cultures == null)
-            {
-                throw new ArgumentNullException(nameof(cultures));
-            }
-
-            if (content.Name != null && content.Name.Length > 255)
-            {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                EventMessages evtMsgs = EventMessagesFactory.Get();
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                var varies = content.ContentType.VariesByCulture();
-
-                if (cultures.Length == 0 && !varies)
-                {
-                    //no cultures specified and doesn't vary, so publish it, else nothing to publish
-                    return SaveAndPublish(content, userId: userId);
-                }
-
-                if (cultures.Any(x => x == null || x == "*"))
-                {
-                    throw new InvalidOperationException(
-                        "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed");
-                }
-
-                IEnumerable impacts =
-                    cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x)));
-
-                // publish the culture(s)
-                // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
-                foreach (CultureImpact impact in impacts)
-                {
-                    content.PublishCulture(impact);
-                }
-
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
-        }
-
-        /// 
-        public PublishResult Unpublish(IContent content, string? culture = "*",
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
-
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            culture = culture?.NullOrWhiteSpaceAsNull();
-
-            PublishedState publishedState = content.PublishedState;
-            if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
-            {
-                throw new InvalidOperationException(
-                    $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
-            }
-
-            // cannot accept invariant (null or empty) culture for variant content type
-            // cannot accept a specific culture for invariant content type (but '*' is ok)
-            if (content.ContentType.VariesByCulture())
-            {
-                if (culture == null)
-                {
-                    throw new NotSupportedException("Invariant culture is not supported by variant content types.");
-                }
-            }
-            else
-            {
-                if (culture != null && culture != "*")
-                {
-                    throw new NotSupportedException(
-                        $"Culture \"{culture}\" is not supported by invariant content types.");
-                }
-            }
-
-            // if the content is not published, nothing to do
-            if (!content.Published)
-            {
-                return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                // all cultures = unpublish whole
-                if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
-                {
-                    // It's important to understand that when the document varies by culture but the "*" is used,
-                    // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected
-                    // because we don't want to actually unpublish every culture and then the document, we just want everything
-                    // to be non-routable so that when it's re-published all variants were as they were.
-
-                    content.PublishedState = PublishedState.Unpublishing;
-                    PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                        savingNotification.State, userId);
-                    scope.Complete();
-                    return result;
-                }
-                else
-                {
-                    // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will
-                    // essentially be re-publishing the document with the requested culture removed.
-                    // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished
-                    // and will then unpublish the document accordingly.
-                    // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist)
-                    var removed = content.UnpublishCulture(culture);
-
-                    //save and publish any changes
-                    PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                        savingNotification.State, userId);
-
-                    scope.Complete();
-
-                    // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures
-                    // were specified to be published which will be the case when removed is false. In that case
-                    // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before).
-                    if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed)
-                    {
-                        return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
-                    }
-
-                    return result;
-                }
-            }
-        }
-
-        /// 
-        ///     Saves a document and publishes/unpublishes any pending publishing changes made to the document.
-        /// 
-        /// 
-        ///     
-        ///         This MUST NOT be called from within this service, this used to be a public API and must only be used outside of
-        ///         this service.
-        ///         Internally in this service, calls must be made to CommitDocumentChangesInternal
-        ///     
-        ///     This is the underlying logic for both publishing and unpublishing any document
-        ///     
-        ///         Pending publishing/unpublishing changes on a document are made with calls to
-        ///          and
-        ///         .
-        ///     
-        ///     
-        ///         When publishing or unpublishing a single culture, or all cultures, use 
-        ///         and . But if the flexibility to both publish and unpublish in a single operation is
-        ///         required
-        ///         then this method needs to be used in combination with 
-        ///         and 
-        ///         on the content itself - this prepares the content, but does not commit anything - and then, invoke
-        ///          to actually commit the changes to the database.
-        ///     
-        ///     The document is *always* saved, even when publishing fails.
-        /// 
-        internal PublishResult CommitDocumentChanges(IContent content,
-            int userId = Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages evtMsgs = EventMessagesFactory.Get();
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
-        }
-
-        /// 
-        ///     Handles a lot of business logic cases for how the document should be persisted
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        ///     
-        ///         Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for
-        ///         pending scheduled publishing, etc... is dealt with in this method.
-        ///         There is quite a lot of cases to take into account along with logic that needs to deal with scheduled
-        ///         saving/publishing, branch saving/publishing, etc...
-        ///     
-        /// 
-        private PublishResult CommitDocumentChangesInternal(ICoreScope scope, IContent content,
-            EventMessages eventMessages, IReadOnlyCollection allLangs,
-            IDictionary? notificationState,
-            int userId = Constants.Security.SuperUserId,
-            bool branchOne = false, bool branchRoot = false)
-        {
-            if (scope == null)
-            {
-                throw new ArgumentNullException(nameof(scope));
-            }
-
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
-
-            if (eventMessages == null)
-            {
-                throw new ArgumentNullException(nameof(eventMessages));
-            }
-
-            PublishResult? publishResult = null;
-            PublishResult? unpublishResult = null;
-
-            // nothing set = republish it all
-            if (content.PublishedState != PublishedState.Publishing &&
-                content.PublishedState != PublishedState.Unpublishing)
-            {
-                content.PublishedState = PublishedState.Publishing;
-            }
-
-            // State here is either Publishing or Unpublishing
-            // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later
-            var publishing = content.PublishedState == PublishedState.Publishing;
-            var unpublishing = content.PublishedState == PublishedState.Unpublishing;
-
-            var variesByCulture = content.ContentType.VariesByCulture();
-
-            //track cultures that are being published, changed, unpublished
-            IReadOnlyList? culturesPublishing = null;
-            IReadOnlyList? culturesUnpublishing = null;
-            IReadOnlyList? culturesChanging = variesByCulture
+            // track the cultures that have changed
+            List? culturesChanging = content.ContentType.VariesByCulture()
                 ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
                 : null;
 
-            var isNew = !content.HasIdentity;
-            TreeChangeTypes changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
-            var previouslyPublished = content.HasIdentity && content.Published;
+            // TODO: Currently there's no way to change track which variant properties have changed, we only have change
+            // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
+            // in this particular case, determining which cultures have changed works with the above with names since it will
+            // have always changed if it's been saved in the back office but that's not really fail safe.
+            _documentRepository.Save(content);
 
-            //inline method to persist the document with the documentRepository since this logic could be called a couple times below
-            void SaveDocument(IContent c)
+            if (contentSchedule != null)
             {
-                // save, always
-                if (c.HasIdentity == false)
-                {
-                    c.CreatorId = userId;
-                }
-
-                c.WriterId = userId;
-
-                // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
-                _documentRepository.Save(c);
+                _documentRepository.PersistContentSchedule(content, contentSchedule);
             }
 
-            if (publishing)
+            scope.Notifications.Publish(
+                new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification));
+
+            // TODO: we had code here to FORCE that this event can never be suppressed. But that just doesn't make a ton of sense?!
+            // I understand that if its suppressed that the caches aren't updated, but that would be expected. If someone
+            // is supressing events then I think it's expected that nothing will happen. They are probably doing it for perf
+            // reasons like bulk import and in those cases we don't want this occuring.
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, eventMessages));
+
+            if (culturesChanging != null)
             {
-                //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo
-                culturesUnpublishing = content.GetCulturesUnpublishing();
-                culturesPublishing = variesByCulture
-                    ? content.PublishCultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
-                    : null;
-
-                // ensure that the document can be published, and publish handling events, business rules, etc
-                publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ !branchOne || branchRoot,
-                    culturesPublishing, culturesUnpublishing, eventMessages, allLangs, notificationState);
-                if (publishResult.Success)
+                IEnumerable? languages = _languageRepository.GetMany()?
+                    .Where(x => culturesChanging.InvariantContains(x.IsoCode))
+                    .Select(x => x.CultureName);
+                if (languages is not null)
                 {
-                    // note: StrategyPublish flips the PublishedState to Publishing!
-                    publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages);
+                    var langs = string.Join(", ", languages);
+                    Audit(AuditType.SaveVariant, userId.Value, content.Id, $"Saved languages: {langs}", langs);
+                }
+            }
+            else
+            {
+                Audit(AuditType.Save, userId.Value, content.Id);
+            }
 
-                    //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole
-                    if (publishResult.Result == PublishResultType.SuccessUnpublishCulture &&
-                        content.PublishCultureInfos?.Count == 0)
-                    {
-                        // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures
-                        // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that
-                        // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to
-                        // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can
-                        // mark the document for Unpublishing.
-                        SaveDocument(content);
+            scope.Complete();
+        }
 
-                        //set the flag to unpublish and continue
-                        unpublishing = content.Published; // if not published yet, nothing to do
-                    }
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    /// 
+    public OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        IContent[] contentsA = contents.ToArray();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new ContentSavingNotification(contentsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+            foreach (IContent content in contentsA)
+            {
+                if (content.HasIdentity == false)
+                {
+                    content.CreatorId = userId;
+                }
+
+                content.WriterId = userId;
+
+                _documentRepository.Save(content);
+            }
+
+            scope.Notifications.Publish(
+                new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification));
+
+            // TODO: See note above about supressing events
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages));
+
+            Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    /// 
+    public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        PublishedState publishedState = content.PublishedState;
+        if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+        {
+            throw new InvalidOperationException(
+                $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
+        }
+
+        // cannot accept invariant (null or empty) culture for variant content type
+        // cannot accept a specific culture for invariant content type (but '*' is ok)
+        if (content.ContentType.VariesByCulture())
+        {
+            if (culture.IsNullOrWhiteSpace())
+            {
+                throw new NotSupportedException("Invariant culture is not supported by variant content types.");
+            }
+        }
+        else
+        {
+            if (!culture.IsNullOrWhiteSpace() && culture != "*")
+            {
+                throw new NotSupportedException(
+                    $"Culture \"{culture}\" is not supported by invariant content types.");
+            }
+        }
+
+        if (content.Name != null && content.Name.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            // if culture is specific, first publish the invariant values, then publish the culture itself.
+            // if culture is '*', then publish them all (including variants)
+
+            // this will create the correct culture impact even if culture is * or null
+                var impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content);
+
+            // publish the culture(s)
+            // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
+            content.PublishCulture(impact);
+
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
+
+    /// 
+    public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId)
+    {
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
+
+        if (cultures == null)
+        {
+            throw new ArgumentNullException(nameof(cultures));
+        }
+
+        if (content.Name != null && content.Name.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            var varies = content.ContentType.VariesByCulture();
+
+            if (cultures.Length == 0 && !varies)
+            {
+                // No cultures specified and doesn't vary, so publish it, else nothing to publish
+                return SaveAndPublish(content, userId: userId);
+            }
+
+            if (cultures.Any(x => x == null || x == "*"))
+            {
+                throw new InvalidOperationException(
+                    "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed");
+            }
+
+            IEnumerable impacts =
+                    cultures.Select(x => _cultureImpactFactory.ImpactExplicit(x, IsDefaultCulture(allLangs, x)));
+
+            // publish the culture(s)
+            // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
+            foreach (CultureImpact impact in impacts)
+            {
+                content.PublishCulture(impact);
+            }
+
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
+
+    /// 
+    public PublishResult Unpublish(IContent content, string? culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
+
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        culture = culture?.NullOrWhiteSpaceAsNull();
+
+        PublishedState publishedState = content.PublishedState;
+        if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+        {
+            throw new InvalidOperationException(
+                $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
+        }
+
+        // cannot accept invariant (null or empty) culture for variant content type
+        // cannot accept a specific culture for invariant content type (but '*' is ok)
+        if (content.ContentType.VariesByCulture())
+        {
+            if (culture == null)
+            {
+                throw new NotSupportedException("Invariant culture is not supported by variant content types.");
+            }
+        }
+        else
+        {
+            if (culture != null && culture != "*")
+            {
+                throw new NotSupportedException(
+                    $"Culture \"{culture}\" is not supported by invariant content types.");
+            }
+        }
+
+        // if the content is not published, nothing to do
+        if (!content.Published)
+        {
+            return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            // all cultures = unpublish whole
+            if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
+            {
+                // It's important to understand that when the document varies by culture but the "*" is used,
+                // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected
+                // because we don't want to actually unpublish every culture and then the document, we just want everything
+                // to be non-routable so that when it's re-published all variants were as they were.
+                content.PublishedState = PublishedState.Unpublishing;
+                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+                scope.Complete();
+                return result;
+            }
+            else
+            {
+                // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will
+                // essentially be re-publishing the document with the requested culture removed.
+                // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished
+                // and will then unpublish the document accordingly.
+                // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist)
+                var removed = content.UnpublishCulture(culture);
+
+                // Save and publish any changes
+                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+
+                scope.Complete();
+
+                // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures
+                // were specified to be published which will be the case when removed is false. In that case
+                // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before).
+                if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed)
+                {
+                    return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
+                }
+
+                return result;
+            }
+        }
+    }
+
+    /// 
+    ///     Saves a document and publishes/unpublishes any pending publishing changes made to the document.
+    /// 
+    /// 
+    ///     
+    ///         This MUST NOT be called from within this service, this used to be a public API and must only be used outside of
+    ///         this service.
+    ///         Internally in this service, calls must be made to CommitDocumentChangesInternal
+    ///     
+    ///     This is the underlying logic for both publishing and unpublishing any document
+    ///     
+    ///         Pending publishing/unpublishing changes on a document are made with calls to
+    ///          and
+    ///         .
+    ///     
+    ///     
+    ///         When publishing or unpublishing a single culture, or all cultures, use 
+    ///         and . But if the flexibility to both publish and unpublish in a single operation is
+    ///         required
+    ///         then this method needs to be used in combination with 
+    ///         and 
+    ///         on the content itself - this prepares the content, but does not commit anything - and then, invoke
+    ///          to actually commit the changes to the database.
+    ///     
+    ///     The document is *always* saved, even when publishing fails.
+    /// 
+    internal PublishResult CommitDocumentChanges(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
+
+    /// 
+    ///     Handles a lot of business logic cases for how the document should be persisted
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     
+    ///         Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for
+    ///         pending scheduled publishing, etc... is dealt with in this method.
+    ///         There is quite a lot of cases to take into account along with logic that needs to deal with scheduled
+    ///         saving/publishing, branch saving/publishing, etc...
+    ///     
+    /// 
+    private PublishResult CommitDocumentChangesInternal(
+        ICoreScope scope,
+        IContent content,
+        EventMessages eventMessages,
+        IReadOnlyCollection allLangs,
+        IDictionary? notificationState,
+        int userId = Constants.Security.SuperUserId,
+        bool branchOne = false,
+        bool branchRoot = false)
+    {
+        if (scope == null)
+        {
+            throw new ArgumentNullException(nameof(scope));
+        }
+
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
+
+        if (eventMessages == null)
+        {
+            throw new ArgumentNullException(nameof(eventMessages));
+        }
+
+        PublishResult? publishResult = null;
+        PublishResult? unpublishResult = null;
+
+        // nothing set = republish it all
+        if (content.PublishedState != PublishedState.Publishing &&
+            content.PublishedState != PublishedState.Unpublishing)
+        {
+            content.PublishedState = PublishedState.Publishing;
+        }
+
+        // State here is either Publishing or Unpublishing
+        // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later
+        var publishing = content.PublishedState == PublishedState.Publishing;
+        var unpublishing = content.PublishedState == PublishedState.Unpublishing;
+
+        var variesByCulture = content.ContentType.VariesByCulture();
+
+        // Track cultures that are being published, changed, unpublished
+        IReadOnlyList? culturesPublishing = null;
+        IReadOnlyList? culturesUnpublishing = null;
+        IReadOnlyList? culturesChanging = variesByCulture
+            ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
+            : null;
+
+        var isNew = !content.HasIdentity;
+        TreeChangeTypes changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
+        var previouslyPublished = content.HasIdentity && content.Published;
+
+        // Inline method to persist the document with the documentRepository since this logic could be called a couple times below
+        void SaveDocument(IContent c)
+        {
+            // save, always
+            if (c.HasIdentity == false)
+            {
+                c.CreatorId = userId;
+            }
+
+            c.WriterId = userId;
+
+            // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
+            _documentRepository.Save(c);
+        }
+
+        if (publishing)
+        {
+            // Determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo
+            culturesUnpublishing = content.GetCulturesUnpublishing();
+            culturesPublishing = variesByCulture
+                ? content.PublishCultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
+                : null;
+
+            // ensure that the document can be published, and publish handling events, business rules, etc
+            publishResult = StrategyCanPublish(
+                scope,
+                content, /*checkPath:*/
+                !branchOne || branchRoot,
+                culturesPublishing,
+                culturesUnpublishing,
+                eventMessages,
+                allLangs,
+                notificationState);
+            if (publishResult.Success)
+            {
+                // note: StrategyPublish flips the PublishedState to Publishing!
+                publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages);
+
+                // Check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole
+                if (publishResult.Result == PublishResultType.SuccessUnpublishCulture &&
+                    content.PublishCultureInfos?.Count == 0)
+                {
+                    // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures
+                    // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that
+                    // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to
+                    // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can
+                    // mark the document for Unpublishing.
+                    SaveDocument(content);
+
+                    // Set the flag to unpublish and continue
+                    unpublishing = content.Published; // if not published yet, nothing to do
+                }
+            }
+            else
+            {
+                // in a branch, just give up
+                if (branchOne && !branchRoot)
+                {
+                    return publishResult;
+                }
+
+                // Check for mandatory culture missing, and then unpublish document as a whole
+                if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing)
+                {
+                    publishing = false;
+                    unpublishing = content.Published; // if not published yet, nothing to do
+
+                    // we may end up in a state where we won't publish nor unpublish
+                    // keep going, though, as we want to save anyways
+                }
+
+                // reset published state from temp values (publishing, unpublishing) to original value
+                // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
+                // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
+                // PublishState to anything other than Publishing or Unpublishing - which is precisely
+                // what we want to do here - throws
+                content.Published = content.Published;
+            }
+        }
+
+        // won't happen in a branch
+        if (unpublishing)
+        {
+            IContent? newest = GetById(content.Id); // ensure we have the newest version - in scope
+            if (content.VersionId != newest?.VersionId)
+            {
+                return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages, content);
+            }
+
+            if (content.Published)
+            {
+                // ensure that the document can be unpublished, and unpublish
+                // handling events, business rules, etc
+                // note: StrategyUnpublish flips the PublishedState to Unpublishing!
+                // note: This unpublishes the entire document (not different variants)
+                unpublishResult = StrategyCanUnpublish(scope, content, eventMessages);
+                if (unpublishResult.Success)
+                {
+                    unpublishResult = StrategyUnpublish(content, eventMessages);
                 }
                 else
                 {
-                    // in a branch, just give up
-                    if (branchOne && !branchRoot)
-                    {
-                        return publishResult;
-                    }
-
-                    //check for mandatory culture missing, and then unpublish document as a whole
-                    if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing)
-                    {
-                        publishing = false;
-                        unpublishing = content.Published; // if not published yet, nothing to do
-
-                        // we may end up in a state where we won't publish nor unpublish
-                        // keep going, though, as we want to save anyways
-                    }
-
                     // reset published state from temp values (publishing, unpublishing) to original value
                     // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
                     // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
@@ -1501,733 +1555,1852 @@ namespace Umbraco.Cms.Core.Services
                     content.Published = content.Published;
                 }
             }
-
-            if (unpublishing) // won't happen in a branch
+            else
             {
-                IContent? newest = GetById(content.Id); // ensure we have the newest version - in scope
-                if (content.VersionId != newest?.VersionId)
-                {
-                    return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages,
-                        content);
-                }
-
-                if (content.Published)
-                {
-                    // ensure that the document can be unpublished, and unpublish
-                    // handling events, business rules, etc
-                    // note: StrategyUnpublish flips the PublishedState to Unpublishing!
-                    // note: This unpublishes the entire document (not different variants)
-                    unpublishResult = StrategyCanUnpublish(scope, content, eventMessages);
-                    if (unpublishResult.Success)
-                    {
-                        unpublishResult = StrategyUnpublish(content, eventMessages);
-                    }
-                    else
-                    {
-                        // reset published state from temp values (publishing, unpublishing) to original value
-                        // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
-                        // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
-                        // PublishState to anything other than Publishing or Unpublishing - which is precisely
-                        // what we want to do here - throws
-                        content.Published = content.Published;
-                    }
-                }
-                else
-                {
-                    // already unpublished - optimistic concurrency collision, really,
-                    // and I am not sure at all what we should do, better die fast, else
-                    // we may end up corrupting the db
-                    throw new InvalidOperationException("Concurrency collision.");
-                }
+                // already unpublished - optimistic concurrency collision, really,
+                // and I am not sure at all what we should do, better die fast, else
+                // we may end up corrupting the db
+                throw new InvalidOperationException("Concurrency collision.");
             }
+        }
 
-            //Persist the document
-            SaveDocument(content);
+        // Persist the document
+        SaveDocument(content);
 
-            // raise the Saved event, always
-            scope.Notifications.Publish(
-                new ContentSavedNotification(content, eventMessages).WithState(notificationState));
+        // raise the Saved event, always
+        scope.Notifications.Publish(
+            new ContentSavedNotification(content, eventMessages).WithState(notificationState));
 
-            if (unpublishing) // we have tried to unpublish - won't happen in a branch
+        // we have tried to unpublish - won't happen in a branch
+        if (unpublishing)
+        {
+            // and succeeded, trigger events
+            if (unpublishResult?.Success ?? false)
             {
-                if (unpublishResult?.Success ?? false) // and succeeded, trigger events
+                // events and audit
+                scope.Notifications.Publish(
+                    new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
+                scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
+
+                if (culturesUnpublishing != null)
                 {
-                    // events and audit
-                    scope.Notifications.Publish(
-                        new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
-                    scope.Notifications.Publish(new ContentTreeChangeNotification(content,
-                        TreeChangeTypes.RefreshBranch, eventMessages));
+                    // This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
+                    var langs = string.Join(", ", allLangs
+                        .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
+                        .Select(x => x.CultureName));
+                    Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
 
-                    if (culturesUnpublishing != null)
+                    if (publishResult == null)
                     {
-                        // This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
-
-                        var langs = string.Join(", ", allLangs
-                            .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
-                            .Select(x => x.CultureName));
-                        Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
-
-                        if (publishResult == null)
-                        {
-                            throw new PanicException("publishResult == null - should not happen");
-                        }
-
-                        switch (publishResult.Result)
-                        {
-                            case PublishResultType.FailedPublishMandatoryCultureMissing:
-                                //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
-
-                                //log that the whole content item has been unpublished due to mandatory culture unpublished
-                                Audit(AuditType.Unpublish, userId, content.Id,
-                                    "Unpublished (mandatory language unpublished)");
-                                return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture,
-                                    eventMessages, content);
-                            case PublishResultType.SuccessUnpublishCulture:
-                                //occurs when the last culture is unpublished
-
-                                Audit(AuditType.Unpublish, userId, content.Id,
-                                    "Unpublished (last language unpublished)");
-                                return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, eventMessages,
-                                    content);
-                        }
-                    }
-
-                    Audit(AuditType.Unpublish, userId, content.Id);
-                    return new PublishResult(PublishResultType.SuccessUnpublish, eventMessages, content);
-                }
-
-                // or, failed
-                scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
-                return new PublishResult(PublishResultType.FailedUnpublish, eventMessages, content); // bah
-            }
-
-            if (publishing) // we have tried to publish
-            {
-                if (publishResult?.Success ?? false) // and succeeded, trigger events
-                {
-                    if (isNew == false && previouslyPublished == false)
-                    {
-                        changeType = TreeChangeTypes.RefreshBranch; // whole branch
-                    }
-                    else if (isNew == false && previouslyPublished)
-                    {
-                        changeType = TreeChangeTypes.RefreshNode; // single node
-                    }
-
-
-                    // invalidate the node/branch
-                    if (!branchOne) // for branches, handled by SaveAndPublishBranch
-                    {
-                        scope.Notifications.Publish(
-                            new ContentTreeChangeNotification(content, changeType, eventMessages));
-                        scope.Notifications.Publish(
-                            new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
-                    }
-
-                    // it was not published and now is... descendants that were 'published' (but
-                    // had an unpublished ancestor) are 're-published' ie not explicitly published
-                    // but back as 'published' nevertheless
-                    if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id))
-                    {
-                        IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray();
-                        scope.Notifications.Publish(
-                            new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState));
+                        throw new PanicException("publishResult == null - should not happen");
                     }
 
                     switch (publishResult.Result)
                     {
-                        case PublishResultType.SuccessPublish:
-                            Audit(AuditType.Publish, userId, content.Id);
-                            break;
-                        case PublishResultType.SuccessPublishCulture:
-                            if (culturesPublishing != null)
-                            {
-                                var langs = string.Join(", ", allLangs
-                                    .Where(x => culturesPublishing.InvariantContains(x.IsoCode))
-                                    .Select(x => x.CultureName));
-                                Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}",
-                                    langs);
-                            }
+                        case PublishResultType.FailedPublishMandatoryCultureMissing:
+                            // Occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
 
-                            break;
+                            // Log that the whole content item has been unpublished due to mandatory culture unpublished
+                            Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)");
+                            return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, eventMessages, content);
                         case PublishResultType.SuccessUnpublishCulture:
-                            if (culturesUnpublishing != null)
-                            {
-                                var langs = string.Join(", ", allLangs
-                                    .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
-                                    .Select(x => x.CultureName));
-                                Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}",
-                                    langs);
-                            }
-
-                            break;
+                            // Occurs when the last culture is unpublished
+                            Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)");
+                            return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, eventMessages, content);
                     }
-
-                    return publishResult;
                 }
-            }
 
-            // should not happen
-            if (branchOne && !branchRoot)
-            {
-                throw new PanicException("branchOne && !branchRoot - should not happen");
-            }
-
-            //if publishing didn't happen or if it has failed, we still need to log which cultures were saved
-            if (!branchOne && (publishResult == null || !publishResult.Success))
-            {
-                if (culturesChanging != null)
-                {
-                    var langs = string.Join(", ", allLangs
-                        .Where(x => culturesChanging.InvariantContains(x.IsoCode))
-                        .Select(x => x.CultureName));
-                    Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs);
-                }
-                else
-                {
-                    Audit(AuditType.Save, userId, content.Id);
-                }
+                Audit(AuditType.Unpublish, userId, content.Id);
+                return new PublishResult(PublishResultType.SuccessUnpublish, eventMessages, content);
             }
 
             // or, failed
             scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
-            return publishResult!;
+            return new PublishResult(PublishResultType.FailedUnpublish, eventMessages, content); // bah
         }
 
-        /// 
-        public IEnumerable PerformScheduledPublish(DateTime date)
+        // we have tried to publish
+        if (publishing)
         {
-            var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList());
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-            var results = new List();
-
-            PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
-            PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
-
-            return results;
-        }
-
-        private void PerformScheduledPublishingExpiration(DateTime date, List results,
-            EventMessages evtMsgs, Lazy> allLangs)
-        {
-            using ICoreScope scope = ScopeProvider.CreateCoreScope();
-
-            // do a fast read without any locks since this executes often to see if we even need to proceed
-            if (_documentRepository.HasContentForExpiration(date))
+            // and succeeded, trigger events
+            if (publishResult?.Success ?? false)
             {
-                // now take a write lock since we'll be updating
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                foreach (IContent d in _documentRepository.GetContentForExpiration(date))
+                if (isNew == false && previouslyPublished == false)
                 {
-                    ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
-                    if (d.ContentType.VariesByCulture())
-                    {
-                        //find which cultures have pending schedules
-                        var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Expire, date)
-                            .Select(x => x.Culture)
-                            .Distinct()
-                            .ToList();
-
-                        if (pendingCultures.Count == 0)
-                        {
-                            continue; //shouldn't happen but no point in processing this document if there's nothing there
-                        }
-
-                        var savingNotification = new ContentSavingNotification(d, evtMsgs);
-                        if (scope.Notifications.PublishCancelable(savingNotification))
-                        {
-                            results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
-                            continue;
-                        }
-
-                        foreach (var c in pendingCultures)
-                        {
-                            //Clear this schedule for this culture
-                            contentSchedule.Clear(c, ContentScheduleAction.Expire, date);
-                            //set the culture to be published
-                            d.UnpublishCulture(c);
-                        }
-
-                        _documentRepository.PersistContentSchedule(d, contentSchedule);
-                        PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value,
-                            savingNotification.State, d.WriterId);
-                        if (result.Success == false)
-                        {
-                            _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id,
-                                result.Result);
-                        }
-
-                        results.Add(result);
-                    }
-                    else
-                    {
-                        //Clear this schedule for this culture
-                        contentSchedule.Clear(ContentScheduleAction.Expire, date);
-                        _documentRepository.PersistContentSchedule(d, contentSchedule);
-                        PublishResult result = Unpublish(d, userId: d.WriterId);
-                        if (result.Success == false)
-                        {
-                            _logger.LogError(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.",
-                                d.Id, result.Result);
-                        }
-
-                        results.Add(result);
-                    }
+                    changeType = TreeChangeTypes.RefreshBranch; // whole branch
+                }
+                else if (isNew == false && previouslyPublished)
+                {
+                    changeType = TreeChangeTypes.RefreshNode; // single node
                 }
 
-                _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
-            }
+                // invalidate the node/branch
+                // for branches, handled by SaveAndPublishBranch
+                if (!branchOne)
+                {
+                    scope.Notifications.Publish(
+                        new ContentTreeChangeNotification(content, changeType, eventMessages));
+                    scope.Notifications.Publish(
+                        new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
+                }
 
-            scope.Complete();
+                // it was not published and now is... descendants that were 'published' (but
+                // had an unpublished ancestor) are 're-published' ie not explicitly published
+                // but back as 'published' nevertheless
+                if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id))
+                {
+                    IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray();
+                    scope.Notifications.Publish(
+                        new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState));
+                }
+
+                switch (publishResult.Result)
+                {
+                    case PublishResultType.SuccessPublish:
+                        Audit(AuditType.Publish, userId, content.Id);
+                        break;
+                    case PublishResultType.SuccessPublishCulture:
+                        if (culturesPublishing != null)
+                        {
+                            var langs = string.Join(", ", allLangs
+                                .Where(x => culturesPublishing.InvariantContains(x.IsoCode))
+                                .Select(x => x.CultureName));
+                            Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs);
+                        }
+
+                        break;
+                    case PublishResultType.SuccessUnpublishCulture:
+                        if (culturesUnpublishing != null)
+                        {
+                            var langs = string.Join(", ", allLangs
+                                .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
+                                .Select(x => x.CultureName));
+                            Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
+                        }
+
+                        break;
+                }
+
+                return publishResult;
+            }
         }
 
-        private void PerformScheduledPublishingRelease(DateTime date, List results,
-            EventMessages evtMsgs, Lazy> allLangs)
+        // should not happen
+        if (branchOne && !branchRoot)
         {
-            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            throw new PanicException("branchOne && !branchRoot - should not happen");
+        }
 
-            // do a fast read without any locks since this executes often to see if we even need to proceed
-            if (_documentRepository.HasContentForRelease(date))
+        // if publishing didn't happen or if it has failed, we still need to log which cultures were saved
+        if (!branchOne && (publishResult == null || !publishResult.Success))
+        {
+            if (culturesChanging != null)
             {
-                // now take a write lock since we'll be updating
-                scope.WriteLock(Constants.Locks.ContentTree);
+                var langs = string.Join(", ", allLangs
+                    .Where(x => culturesChanging.InvariantContains(x.IsoCode))
+                    .Select(x => x.CultureName));
+                Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs);
+            }
+            else
+            {
+                Audit(AuditType.Save, userId, content.Id);
+            }
+        }
 
-                foreach (IContent d in _documentRepository.GetContentForRelease(date))
+        // or, failed
+        scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
+        return publishResult!;
+    }
+
+    /// 
+    public IEnumerable PerformScheduledPublish(DateTime date)
+    {
+        var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList());
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+        var results = new List();
+
+        PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
+        PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
+
+        return results;
+    }
+
+    private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+
+        // do a fast read without any locks since this executes often to see if we even need to proceed
+        if (_documentRepository.HasContentForExpiration(date))
+        {
+            // now take a write lock since we'll be updating
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            foreach (IContent d in _documentRepository.GetContentForExpiration(date))
+            {
+                ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
+                if (d.ContentType.VariesByCulture())
                 {
-                    ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
-                    if (d.ContentType.VariesByCulture())
+                    // find which cultures have pending schedules
+                    var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Expire, date)
+                        .Select(x => x.Culture)
+                        .Distinct()
+                        .ToList();
+
+                    if (pendingCultures.Count == 0)
                     {
-                        //find which cultures have pending schedules
-                        var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date)
-                            .Select(x => x.Culture)
-                            .Distinct()
-                            .ToList();
+                        continue; // shouldn't happen but no point in processing this document if there's nothing there
+                    }
 
-                        if (pendingCultures.Count == 0)
-                        {
-                            continue; //shouldn't happen but no point in processing this document if there's nothing there
-                        }
+                    var savingNotification = new ContentSavingNotification(d, evtMsgs);
+                    if (scope.Notifications.PublishCancelable(savingNotification))
+                    {
+                        results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
+                        continue;
+                    }
 
-                        var savingNotification = new ContentSavingNotification(d, evtMsgs);
-                        if (scope.Notifications.PublishCancelable(savingNotification))
-                        {
-                            results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
-                            continue;
-                        }
+                    foreach (var c in pendingCultures)
+                    {
+                        // Clear this schedule for this culture
+                        contentSchedule.Clear(c, ContentScheduleAction.Expire, date);
 
-                        var publishing = true;
-                        foreach (var culture in pendingCultures)
-                        {
-                            //Clear this schedule for this culture
-                            contentSchedule.Clear(culture, ContentScheduleAction.Release, date);
+                        // set the culture to be published
+                        d.UnpublishCulture(c);
+                    }
 
-                            if (d.Trashed)
-                            {
-                                continue; // won't publish
-                            }
+                    _documentRepository.PersistContentSchedule(d, contentSchedule);
+                    PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
 
-                            //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
-                            IProperty[]? invalidProperties = null;
-                            var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture));
-                            var tryPublish = d.PublishCulture(impact) &&
-                                             _propertyValidationService.Value.IsPropertyDataValid(d,
-                                                 out invalidProperties, impact);
-                            if (invalidProperties != null && invalidProperties.Length > 0)
-                            {
-                                _logger.LogWarning(
-                                    "Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
-                                    d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
-                            }
+                    results.Add(result);
+                }
+                else
+                {
+                    // Clear this schedule for this culture
+                    contentSchedule.Clear(ContentScheduleAction.Expire, date);
+                    _documentRepository.PersistContentSchedule(d, contentSchedule);
+                    PublishResult result = Unpublish(d, userId: d.WriterId);
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
 
-                            publishing &= tryPublish; //set the culture to be published
-                            if (!publishing)
-                            {
-                                continue;
-                            }
-                        }
+                    results.Add(result);
+                }
+            }
 
-                        PublishResult result;
+            _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
+        }
+
+        scope.Complete();
+    }
+
+    private void PerformScheduledPublishingRelease(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+
+        // do a fast read without any locks since this executes often to see if we even need to proceed
+        if (_documentRepository.HasContentForRelease(date))
+        {
+            // now take a write lock since we'll be updating
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            foreach (IContent d in _documentRepository.GetContentForRelease(date))
+            {
+                ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
+                if (d.ContentType.VariesByCulture())
+                {
+                    // find which cultures have pending schedules
+                    var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date)
+                        .Select(x => x.Culture)
+                        .Distinct()
+                        .ToList();
+
+                    if (pendingCultures.Count == 0)
+                    {
+                        continue; // shouldn't happen but no point in processing this document if there's nothing there
+                    }
+
+                    var savingNotification = new ContentSavingNotification(d, evtMsgs);
+                    if (scope.Notifications.PublishCancelable(savingNotification))
+                    {
+                        results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
+                        continue;
+                    }
+
+                    var publishing = true;
+                    foreach (var culture in pendingCultures)
+                    {
+                        // Clear this schedule for this culture
+                        contentSchedule.Clear(culture, ContentScheduleAction.Release, date);
 
                         if (d.Trashed)
                         {
-                            result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
-                        }
-                        else if (!publishing)
-                        {
-                            result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
-                        }
-                        else
-                        {
-                            _documentRepository.PersistContentSchedule(d, contentSchedule);
-                            result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value,
-                                savingNotification.State, d.WriterId);
+                            continue; // won't publish
                         }
 
-                        if (result.Success == false)
+                        // publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
+                        IProperty[]? invalidProperties = null;
+                            var impact = _cultureImpactFactory.ImpactExplicit(culture, IsDefaultCulture(allLangs.Value, culture));
+                        var tryPublish = d.PublishCulture(impact) &&
+                                         _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
+                        if (invalidProperties != null && invalidProperties.Length > 0)
                         {
-                            _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id,
-                                result.Result);
+                            _logger.LogWarning(
+                                "Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
+                                d.Id,
+                                culture,
+                                string.Join(",", invalidProperties.Select(x => x.Alias)));
                         }
 
-                        results.Add(result);
+                        publishing &= tryPublish; // set the culture to be published
+                        if (!publishing)
+                        {
+                        }
+                    }
+
+                    PublishResult result;
+
+                    if (d.Trashed)
+                    {
+                        result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
+                    }
+                    else if (!publishing)
+                    {
+                        result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
                     }
                     else
                     {
-                        //Clear this schedule
-                        contentSchedule.Clear(ContentScheduleAction.Release, date);
-
-                        PublishResult? result = null;
-
-                        if (d.Trashed)
-                        {
-                            result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
-                        }
-                        else
-                        {
-                            _documentRepository.PersistContentSchedule(d, contentSchedule);
-                            result = SaveAndPublish(d, userId: d.WriterId);
-                        }
-
-                        if (result.Success == false)
-                        {
-                            _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id,
-                                result.Result);
-                        }
-
-                        results.Add(result);
+                        _documentRepository.PersistContentSchedule(d, contentSchedule);
+                        result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
                     }
-                }
 
-                _documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
+
+                    results.Add(result);
+                }
+                else
+                {
+                    // Clear this schedule
+                    contentSchedule.Clear(ContentScheduleAction.Release, date);
+
+                    PublishResult? result = null;
+
+                    if (d.Trashed)
+                    {
+                        result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
+                    }
+                    else
+                    {
+                        _documentRepository.PersistContentSchedule(d, contentSchedule);
+                        result = SaveAndPublish(d, userId: d.WriterId);
+                    }
+
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
+
+                    results.Add(result);
+                }
             }
 
-            scope.Complete();
+            _documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
         }
 
-        // utility 'PublishCultures' func used by SaveAndPublishBranch
-        private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish,
-            IReadOnlyCollection allLangs)
+        scope.Complete();
+    }
+
+    // utility 'PublishCultures' func used by SaveAndPublishBranch
+    private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs)
+    {
+        // TODO: Th is does not support being able to return invalid property details to bubble up to the UI
+
+        // variant content type - publish specified cultures
+        // invariant content type - publish only the invariant culture
+        if (content.ContentType.VariesByCulture())
         {
-            //TODO: This does not support being able to return invalid property details to bubble up to the UI
-
-            // variant content type - publish specified cultures
-            // invariant content type - publish only the invariant culture
-            if (content.ContentType.VariesByCulture())
+            return culturesToPublish.All(culture =>
             {
-                return culturesToPublish.All(culture =>
-                {
-                    var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
-                    return content.PublishCulture(impact) &&
-                           _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
-                });
-            }
-
-            return content.PublishCulture(CultureImpact.Invariant)
-                   && _propertyValidationService.Value.IsPropertyDataValid(content, out _, CultureImpact.Invariant);
+                    var impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content);
+                return content.PublishCulture(impact) &&
+                       _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
+            });
         }
 
-        // utility 'ShouldPublish' func used by SaveAndPublishBranch
-        private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c,
-            bool published, bool edited, bool isRoot, bool force)
+            return content.PublishCulture(_cultureImpactFactory.ImpactInvariant())
+                   && _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant());
+    }
+
+    // utility 'ShouldPublish' func used by SaveAndPublishBranch
+    private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, bool force)
+    {
+        // if published, republish
+        if (published)
         {
-            // if published, republish
-            if (published)
-            {
-                if (cultures == null)
-                {
-                    cultures = new HashSet(); // empty means 'already published'
-                }
-
-                if (edited)
-                {
-                    cultures.Add(c); //  means 'republish this culture'
-                }
-
-                return cultures;
-            }
-
-            // if not published, publish if force/root else do nothing
-            if (!force && !isRoot)
-            {
-                return cultures; // null means 'nothing to do'
-            }
-
             if (cultures == null)
             {
-                cultures = new HashSet();
+                cultures = new HashSet(); // empty means 'already published'
+            }
+
+            if (edited)
+            {
+                cultures.Add(c); //  means 'republish this culture'
             }
 
-            cultures.Add(c); //  means 'publish this culture'
             return cultures;
         }
 
-        /// 
-        public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
+        // if not published, publish if force/root else do nothing
+        if (!force && !isRoot)
         {
-            // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
-            // and not to == them, else we would be comparing references, and that is a bad thing
-
-            // determines whether the document is edited, and thus needs to be published,
-            // for the specified culture (it may be edited for other cultures and that
-            // should not trigger a publish).
-
-            // determines cultures to be published
-            // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
-            HashSet? ShouldPublish(IContent c)
-            {
-                var isRoot = c.Id == content.Id;
-                HashSet? culturesToPublish = null;
-
-                if (!c.ContentType.VariesByCulture()) // invariant content type
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot,
-                        force);
-                }
-
-                if (culture != "*") // variant content type, specific culture
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture,
-                        c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
-                }
-
-                // variant content type, all cultures
-                if (c.Published)
-                {
-                    // then some (and maybe all) cultures will be 'already published' (unless forcing),
-                    // others will have to 'republish this culture'
-                    foreach (var x in c.AvailableCultures)
-                    {
-                        SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x),
-                            c.IsCultureEdited(x), isRoot, force);
-                    }
-
-                    return culturesToPublish;
-                }
-
-                // if not published, publish if force/root else do nothing
-                return force || isRoot
-                    ? new HashSet {"*"} // "*" means 'publish all'
-                    : null; // null means 'nothing to do'
-            }
-
-            return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+            return cultures; // null means 'nothing to do'
         }
 
-        /// 
-        public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures,
-            int userId = Constants.Security.SuperUserId)
+        if (cultures == null)
         {
-            // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
-            // and not to == them, else we would be comparing references, and that is a bad thing
-
-            cultures = cultures ?? Array.Empty();
-
-            // determines cultures to be published
-            // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
-            HashSet? ShouldPublish(IContent c)
-            {
-                var isRoot = c.Id == content.Id;
-                HashSet? culturesToPublish = null;
-
-                if (!c.ContentType.VariesByCulture()) // invariant content type
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot,
-                        force);
-                }
-
-                // variant content type, specific cultures
-                if (c.Published)
-                {
-                    // then some (and maybe all) cultures will be 'already published' (unless forcing),
-                    // others will have to 'republish this culture'
-                    foreach (var x in cultures)
-                    {
-                        SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x),
-                            c.IsCultureEdited(x), isRoot, force);
-                    }
-
-                    return culturesToPublish;
-                }
-
-                // if not published, publish if force/root else do nothing
-                return force || isRoot
-                    ? new HashSet(cultures) // means 'publish specified cultures'
-                    : null; // null means 'nothing to do'
-            }
-
-            return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+            cultures = new HashSet();
         }
 
-        internal IEnumerable SaveAndPublishBranch(IContent document, bool force,
-            Func?> shouldPublish,
-            Func, IReadOnlyCollection, bool> publishCultures,
-            int userId = Constants.Security.SuperUserId)
+        cultures.Add(c); //  means 'publish this culture'
+        return cultures;
+    }
+
+    /// 
+    public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
+        // and not to == them, else we would be comparing references, and that is a bad thing
+
+        // determines whether the document is edited, and thus needs to be published,
+        // for the specified culture (it may be edited for other cultures and that
+        // should not trigger a publish).
+
+        // determines cultures to be published
+        // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
+        HashSet? ShouldPublish(IContent c)
         {
-            if (shouldPublish == null)
+            var isRoot = c.Id == content.Id;
+            HashSet? culturesToPublish = null;
+
+            // invariant content type
+            if (!c.ContentType.VariesByCulture())
             {
-                throw new ArgumentNullException(nameof(shouldPublish));
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
             }
 
-            if (publishCultures == null)
+            // variant content type, specific culture
+            if (culture != "*")
             {
-                throw new ArgumentNullException(nameof(publishCultures));
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
             }
 
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            var results = new List();
-            var publishedDocuments = new List();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            // variant content type, all cultures
+            if (c.Published)
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                if (!document.HasIdentity)
+                // then some (and maybe all) cultures will be 'already published' (unless forcing),
+                // others will have to 'republish this culture'
+                foreach (var x in c.AvailableCultures)
                 {
-                    throw new InvalidOperationException("Cannot not branch-publish a new document.");
+                    SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
                 }
 
-                PublishedState publishedState = document.PublishedState;
-                if (publishedState == PublishedState.Publishing)
+                return culturesToPublish;
+            }
+
+            // if not published, publish if force/root else do nothing
+            return force || isRoot
+                ? new HashSet { "*" } // "*" means 'publish all'
+                : null; // null means 'nothing to do'
+        }
+
+        return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+    }
+
+    /// 
+    public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId)
+    {
+        // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
+        // and not to == them, else we would be comparing references, and that is a bad thing
+        cultures = cultures ?? Array.Empty();
+
+        // determines cultures to be published
+        // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
+        HashSet? ShouldPublish(IContent c)
+        {
+            var isRoot = c.Id == content.Id;
+            HashSet? culturesToPublish = null;
+
+            // invariant content type
+            if (!c.ContentType.VariesByCulture())
+            {
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
+            }
+
+            // variant content type, specific cultures
+            if (c.Published)
+            {
+                // then some (and maybe all) cultures will be 'already published' (unless forcing),
+                // others will have to 'republish this culture'
+                foreach (var x in cultures)
                 {
-                    throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
+                    SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
                 }
 
-                // deal with the branch root - if it fails, abort
-                PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true,
-                    publishedDocuments, eventMessages, userId, allLangs);
-                if (result != null)
+                return culturesToPublish;
+            }
+
+            // if not published, publish if force/root else do nothing
+            return force || isRoot
+                ? new HashSet(cultures) // means 'publish specified cultures'
+                : null; // null means 'nothing to do'
+        }
+
+        return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+    }
+
+    internal IEnumerable SaveAndPublishBranch(
+        IContent document,
+        bool force,
+        Func?> shouldPublish,
+        Func, IReadOnlyCollection, bool> publishCultures,
+        int userId = Constants.Security.SuperUserId)
+    {
+        if (shouldPublish == null)
+        {
+            throw new ArgumentNullException(nameof(shouldPublish));
+        }
+
+        if (publishCultures == null)
+        {
+            throw new ArgumentNullException(nameof(publishCultures));
+        }
+
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        var results = new List();
+        var publishedDocuments = new List();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            if (!document.HasIdentity)
+            {
+                throw new InvalidOperationException("Cannot not branch-publish a new document.");
+            }
+
+            PublishedState publishedState = document.PublishedState;
+            if (publishedState == PublishedState.Publishing)
+            {
+                throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
+            }
+
+            // deal with the branch root - if it fails, abort
+            PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs);
+            if (result != null)
+            {
+                results.Add(result);
+                if (!result.Success)
                 {
-                    results.Add(result);
-                    if (!result.Success)
+                    return results;
+                }
+            }
+
+            // deal with descendants
+            // if one fails, abort its branch
+            var exclude = new HashSet();
+
+            int count;
+            var page = 0;
+            const int pageSize = 100;
+            do
+            {
+                count = 0;
+
+                // important to order by Path ASC so make it explicit in case defaults change
+                // ReSharper disable once RedundantArgumentDefaultValue
+                foreach (IContent d in GetPagedDescendants(document.Id, page, pageSize, out _, ordering: Ordering.By("Path", Direction.Ascending)))
+                {
+                    count++;
+
+                    // if parent is excluded, exclude child too
+                    if (exclude.Contains(d.ParentId))
                     {
-                        return results;
+                        exclude.Add(d.Id);
+                        continue;
                     }
-                }
 
-                // deal with descendants
-                // if one fails, abort its branch
-                var exclude = new HashSet();
-
-                int count;
-                var page = 0;
-                const int pageSize = 100;
-                do
-                {
-                    count = 0;
-                    // important to order by Path ASC so make it explicit in case defaults change
-                    // ReSharper disable once RedundantArgumentDefaultValue
-                    foreach (IContent d in GetPagedDescendants(document.Id, page, pageSize, out _,
-                                 ordering: Ordering.By("Path", Direction.Ascending)))
+                    // no need to check path here, parent has to be published here
+                    result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs);
+                    if (result != null)
                     {
-                        count++;
-
-                        // if parent is excluded, exclude child too
-                        if (exclude.Contains(d.ParentId))
+                        results.Add(result);
+                        if (result.Success)
                         {
-                            exclude.Add(d.Id);
                             continue;
                         }
-
-                        // no need to check path here, parent has to be published here
-                        result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false,
-                            publishedDocuments, eventMessages, userId, allLangs);
-                        if (result != null)
-                        {
-                            results.Add(result);
-                            if (result.Success)
-                            {
-                                continue;
-                            }
-                        }
-
-                        // if we could not publish the document, cut its branch
-                        exclude.Add(d.Id);
                     }
 
-                    page++;
-                } while (count > 0);
+                    // if we could not publish the document, cut its branch
+                    exclude.Add(d.Id);
+                }
 
-                Audit(AuditType.Publish, userId, document.Id, "Branch published");
-
-                // trigger events for the entire branch
-                // (SaveAndPublishBranchOne does *not* do it)
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages));
-                scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages));
-
-                scope.Complete();
+                page++;
             }
+            while (count > 0);
 
-            return results;
+            Audit(AuditType.Publish, userId, document.Id, "Branch published");
+
+            // trigger events for the entire branch
+            // (SaveAndPublishBranchOne does *not* do it)
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages));
+            scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages));
+
+            scope.Complete();
         }
 
-        // shouldPublish: a function determining whether the document has changes that need to be published
-        //  note - 'force' is handled by 'editing'
-        // publishValues: a function publishing values (using the appropriate PublishCulture calls)
-        private PublishResult? SaveAndPublishBranchItem(ICoreScope scope, IContent document,
-            Func?> shouldPublish,
-            Func, IReadOnlyCollection, bool> publishCultures,
-            bool isRoot,
-            ICollection publishedDocuments,
-            EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs)
+        return results;
+    }
+
+    // shouldPublish: a function determining whether the document has changes that need to be published
+    //  note - 'force' is handled by 'editing'
+    // publishValues: a function publishing values (using the appropriate PublishCulture calls)
+    private PublishResult? SaveAndPublishBranchItem(
+        ICoreScope scope,
+        IContent document,
+        Func?> shouldPublish,
+        Func, IReadOnlyCollection,
+            bool> publishCultures,
+        bool isRoot,
+        ICollection publishedDocuments,
+        EventMessages evtMsgs,
+        int userId,
+        IReadOnlyCollection allLangs)
+    {
+        HashSet? culturesToPublish = shouldPublish(document);
+
+        // null = do not include
+        if (culturesToPublish == null)
         {
-            HashSet? culturesToPublish = shouldPublish(document);
-            if (culturesToPublish == null) // null = do not include
+            return null;
+        }
+
+        // empty = already published
+        if (culturesToPublish.Count == 0)
+        {
+            return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
+        }
+
+        var savingNotification = new ContentSavingNotification(document, evtMsgs);
+        if (scope.Notifications.PublishCancelable(savingNotification))
+        {
+            return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
+        }
+
+        // publish & check if values are valid
+        if (!publishCultures(document, culturesToPublish, allLangs))
+        {
+            // TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
+            return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
+        }
+
+        PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, true, isRoot);
+        if (result.Success)
+        {
+            publishedDocuments.Add(document);
+        }
+
+        return result;
+    }
+
+    #endregion
+
+    #region Delete
+
+    /// 
+    public OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(content, eventMessages)))
             {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            // if it's not trashed yet, and published, we should unpublish
+            // but... Unpublishing event makes no sense (not going to cancel?) and no need to save
+            // just raise the event
+            if (content.Trashed == false && content.Published)
+            {
+                scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
+            }
+
+            DeleteLocked(scope, content, eventMessages);
+
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages));
+            Audit(AuditType.Delete, userId, content.Id);
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
+    {
+        void DoDelete(IContent c)
+        {
+            _documentRepository.Delete(c);
+            scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
+
+            // media files deleted by QueuingEventDispatcher
+        }
+
+        const int pageSize = 500;
+        var total = long.MaxValue;
+        while (total > 0)
+        {
+            // get descendants - ordered from deepest to shallowest
+            IEnumerable descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
+            foreach (IContent c in descendants)
+            {
+                DoDelete(c);
+            }
+        }
+
+        DoDelete(content);
+    }
+
+    // TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
+    // Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT,
+    // if that's not the case, then the file will never be deleted, because when we delete the content,
+    // the version referencing the file will not be there anymore. SO, we can leak files.
+
+    /// 
+    ///     Permanently deletes versions from an  object prior to a specific date.
+    ///     This method will never delete the latest version of a content item.
+    /// 
+    /// Id of the  object to delete versions from
+    /// Latest version date
+    /// Optional Id of the User deleting versions of a Content object
+    public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var deletingVersionsNotification =
+                new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
+            if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.DeleteVersions(id, versionDate);
+
+            scope.Notifications.Publish(
+                new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(
+                    deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version date)");
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Permanently deletes specific version(s) from an  object.
+    ///     This method will never delete the latest version of a content item.
+    /// 
+    /// Id of the  object to delete a version from
+    /// Id of the version to delete
+    /// Boolean indicating whether to delete versions prior to the versionId
+    /// Optional Id of the User deleting versions of a Content object
+    public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, versionId);
+            if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            if (deletePriorVersions)
+            {
+                IContent? content = GetVersion(versionId);
+                DeleteVersions(id, content?.UpdateDate ?? DateTime.Now, userId);
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+            IContent? c = _documentRepository.Get(id);
+
+            // don't delete the current or published version
+            if (c?.VersionId != versionId &&
+                c?.PublishedVersionId != versionId)
+            {
+                _documentRepository.DeleteVersion(versionId);
+            }
+
+            scope.Notifications.Publish(
+                new ContentDeletedVersionsNotification(id, evtMsgs, versionId).WithStateFrom(
+                    deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)");
+
+            scope.Complete();
+        }
+    }
+
+    #endregion
+
+    #region Move, RecycleBin
+
+    /// 
+    public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        var moves = new List<(IContent, string)>();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var originalPath = content.Path;
+            var moveEventInfo =
+                new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent);
+
+            var movingToRecycleBinNotification =
+                new ContentMovingToRecycleBinNotification(moveEventInfo, eventMessages);
+            if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
+            {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages); // causes rollback
+            }
+
+            // if it's published we may want to force-unpublish it - that would be backward-compatible... but...
+            // making a radical decision here: trashing is equivalent to moving under an unpublished node so
+            // it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted
+            // if (content.HasPublishedVersion)
+            // { }
+            PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true);
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
+
+            MoveEventInfo[] moveInfo = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
+
+            scope.Notifications.Publish(
+                new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom(
+                    movingToRecycleBinNotification));
+            Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    /// 
+    ///     Moves an  object to a new location by changing its parent id.
+    /// 
+    /// 
+    ///     If the  object is already published it will be
+    ///     published after being moved to its new location. Otherwise it'll just
+    ///     be saved with a new parent id.
+    /// 
+    /// The  to move
+    /// Id of the Content's new Parent
+    /// Optional Id of the User moving the Content
+    public void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
+    {
+        // if moving to the recycle bin then use the proper method
+        if (parentId == Constants.System.RecycleBinContent)
+        {
+            MoveToRecycleBin(content, userId);
+            return;
+        }
+
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        var moves = new List<(IContent, string)>();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            IContent? parent = parentId == Constants.System.Root ? null : GetById(parentId);
+            if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
+            {
+                throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
+            }
+
+            var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
+
+            var movingNotification = new ContentMovingNotification(moveEventInfo, eventMessages);
+            if (scope.Notifications.PublishCancelable(movingNotification))
+            {
+                scope.Complete();
+                return; // causes rollback
+            }
+
+            // if content was trashed, and since we're not moving to the recycle bin,
+            // indicate that the trashed status should be changed to false, else just
+            // leave it unchanged
+            var trashed = content.Trashed ? false : (bool?)null;
+
+            // if the content was trashed under another content, and so has a published version,
+            // it cannot move back as published but has to be unpublished first - that's for the
+            // root content, everything underneath will retain its published status
+            if (content.Trashed && content.Published)
+            {
+                // however, it had been masked when being trashed, so there's no need for
+                // any special event here - just change its state
+                content.PublishedState = PublishedState.Unpublishing;
+            }
+
+            PerformMoveLocked(content, parentId, parent, userId, moves, trashed);
+
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
+
+            // changes
+            MoveEventInfo[] moveInfo = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
+
+            scope.Notifications.Publish(
+                new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification));
+
+            Audit(AuditType.Move, userId, content.Id);
+
+            scope.Complete();
+        }
+    }
+
+    // MUST be called from within WriteLock
+    // trash indicates whether we are trashing, un-trashing, or not changing anything
+    private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash)
+    {
+        content.WriterId = userId;
+        content.ParentId = parentId;
+
+        // get the level delta (old pos to new pos)
+        // note that recycle bin (id:-20) level is 0!
+        var levelDelta = 1 - content.Level + (parent?.Level ?? 0);
+
+        var paths = new Dictionary();
+
+        moves.Add((content, content.Path)); // capture original path
+
+        // need to store the original path to lookup descendants based on it below
+        var originalPath = content.Path;
+
+        // these will be updated by the repo because we changed parentId
+        // content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
+        // content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId);
+        // content.Level += levelDelta;
+        PerformMoveContentLocked(content, userId, trash);
+
+        // if uow is not immediate, content.Path will be updated only when the UOW commits,
+        // and because we want it now, we have to calculate it by ourselves
+        // paths[content.Id] = content.Path;
+        paths[content.Id] =
+            (parent == null
+                ? parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString
+                : parent.Path) + "," + content.Id;
+
+        const int pageSize = 500;
+        IQuery? query = GetPagedDescendantQuery(originalPath);
+        long total;
+        do
+        {
+            // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
+            IEnumerable descendants =
+                GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path"));
+
+            foreach (IContent descendant in descendants)
+            {
+                moves.Add((descendant, descendant.Path)); // capture original path
+
+                // update path and level since we do not update parentId
+                descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
+                descendant.Level += levelDelta;
+                PerformMoveContentLocked(descendant, userId, trash);
+            }
+        }
+        while (total > pageSize);
+    }
+
+    private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
+    {
+        if (trash.HasValue)
+        {
+            ((ContentBase)content).Trashed = trash.Value;
+        }
+
+        content.WriterId = userId;
+        _documentRepository.Save(content);
+    }
+
+    /// 
+    ///     Empties the Recycle Bin by deleting all  that resides in the bin
+    /// 
+    public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId)
+    {
+        var deleted = new List();
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            // emptying the recycle bin means deleting whatever is in there - do it properly!
+            IQuery? query = Query().Where(x => x.ParentId == Constants.System.RecycleBinContent);
+            IContent[] contents = _documentRepository.Get(query).ToArray();
+
+            var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages);
+            if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
+            {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
+
+            if (contents is not null)
+            {
+                foreach (IContent content in contents)
+                {
+                    DeleteLocked(scope, content, eventMessages);
+                    deleted.Add(content);
+                }
+            }
+
+            scope.Notifications.Publish(
+                new ContentEmptiedRecycleBinNotification(deleted, eventMessages).WithStateFrom(
+                    emptyingRecycleBinNotification));
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(deleted, TreeChangeTypes.Remove, eventMessages));
+            Audit(AuditType.Delete, userId, Constants.System.RecycleBinContent, "Recycle bin emptied");
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    public bool RecycleBinSmells()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.RecycleBinSmells();
+        }
+    }
+
+    #endregion
+
+    #region Others
+
+    /// 
+    ///     Copies an  object by creating a new Content object of the same type and copies all data from
+    ///     the current
+    ///     to the new copy which is returned. Recursively copies all children.
+    /// 
+    /// The  to copy
+    /// Id of the Content's new Parent
+    /// Boolean indicating whether the copy should be related to the original
+    /// Optional Id of the User copying the Content
+    /// The newly created  object
+    public IContent? Copy(IContent content, int parentId, bool relateToOriginal, int userId = Constants.Security.SuperUserId) => Copy(content, parentId, relateToOriginal, true, userId);
+
+    /// 
+    ///     Copies an  object by creating a new Content object of the same type and copies all data from
+    ///     the current
+    ///     to the new copy which is returned.
+    /// 
+    /// The  to copy
+    /// Id of the Content's new Parent
+    /// Boolean indicating whether the copy should be related to the original
+    /// A value indicating whether to recursively copy children.
+    /// Optional Id of the User copying the Content
+    /// The newly created  object
+    public IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        IContent copy = content.DeepCloneWithResetIdentities();
+        copy.ParentId = parentId;
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            if (scope.Notifications.PublishCancelable(
+                    new ContentCopyingNotification(content, copy, parentId, eventMessages)))
+            {
+                scope.Complete();
                 return null;
             }
 
-            if (culturesToPublish.Count == 0) // empty = already published
+            // note - relateToOriginal is not managed here,
+            // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do
+            // meaning that the event has to trigger for every copied content including descendants
+            var copies = new List>();
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            // a copy is not published (but not really unpublishing either)
+            // update the create author and last edit author
+            if (copy.Published)
             {
-                return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
+                copy.Published = false;
             }
 
-            var savingNotification = new ContentSavingNotification(document, evtMsgs);
-            if (scope.Notifications.PublishCancelable(savingNotification))
+            copy.CreatorId = userId;
+            copy.WriterId = userId;
+
+            // get the current permissions, if there are any explicit ones they need to be copied
+            EntityPermissionCollection currentPermissions = GetPermissions(content);
+            currentPermissions.RemoveWhere(p => p.IsDefaultPermissions);
+
+            // save and flush because we need the ID for the recursive Copying events
+            _documentRepository.Save(copy);
+
+            // add permissions
+            if (currentPermissions.Count > 0)
             {
-                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
+                var permissionSet = new ContentPermissionSet(copy, currentPermissions);
+                _documentRepository.AddOrUpdatePermissions(permissionSet);
             }
 
-            // publish & check if values are valid
-            if (!publishCultures(document, culturesToPublish, allLangs))
+            // keep track of copies
+            copies.Add(Tuple.Create(content, copy));
+            var idmap = new Dictionary { [content.Id] = copy.Id };
+
+            // process descendants
+            if (recursive)
             {
-                //TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
+                const int pageSize = 500;
+                var page = 0;
+                var total = long.MaxValue;
+                while (page * pageSize < total)
+                {
+                    IEnumerable descendants =
+                        GetPagedDescendants(content.Id, page++, pageSize, out total);
+                    foreach (IContent descendant in descendants)
+                    {
+                        // if parent has not been copied, skip, else gets its copy id
+                        if (idmap.TryGetValue(descendant.ParentId, out parentId) == false)
+                        {
+                            continue;
+                        }
+
+                        IContent descendantCopy = descendant.DeepCloneWithResetIdentities();
+                        descendantCopy.ParentId = parentId;
+
+                        if (scope.Notifications.PublishCancelable(
+                                new ContentCopyingNotification(descendant, descendantCopy, parentId, eventMessages)))
+                        {
+                            continue;
+                        }
+
+                        // a copy is not published (but not really unpublishing either)
+                        // update the create author and last edit author
+                        if (descendantCopy.Published)
+                        {
+                            descendantCopy.Published = false;
+                        }
+
+                        descendantCopy.CreatorId = userId;
+                        descendantCopy.WriterId = userId;
+
+                        // save and flush (see above)
+                        _documentRepository.Save(descendantCopy);
+
+                        copies.Add(Tuple.Create(descendant, descendantCopy));
+                        idmap[descendant.Id] = descendantCopy.Id;
+                    }
+                }
             }
 
-            PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs,
-                savingNotification.State, userId, true, isRoot);
-            if (result.Success)
+            // not handling tags here, because
+            // - tags should be handled by the content repository
+            // - a copy is unpublished and therefore has no impact on tags in DB
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(copy, TreeChangeTypes.RefreshBranch, eventMessages));
+            foreach (Tuple x in copies)
             {
-                publishedDocuments.Add(document);
+                scope.Notifications.Publish(new ContentCopiedNotification(x.Item1, x.Item2, parentId, relateToOriginal, eventMessages));
             }
 
-            return result;
+            Audit(AuditType.Copy, userId, content.Id);
+
+            scope.Complete();
         }
 
-        #endregion
+        return copy;
+    }
 
-        #region Delete
-
-        /// 
-        public OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId)
+    /// 
+    ///     Sends an  to Publication, which executes handlers and events for the 'Send to Publication'
+    ///     action.
+    /// 
+    /// The  to send to publication
+    /// Optional Id of the User issuing the send to publication
+    /// True if sending publication was successful otherwise false
+    public bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId)
+    {
+        if (content is null)
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
+            return false;
+        }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(sendingToPublishNotification))
             {
-                if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(content, eventMessages)))
+                scope.Complete();
+                return false;
+            }
+
+            // track the cultures changing for auditing
+            var culturesChanging = content.ContentType.VariesByCulture()
+                ? string.Join(",", content.CultureInfos!.Values.Where(x => x.IsDirty()).Select(x => x.Culture))
+                : null;
+
+            // TODO: Currently there's no way to change track which variant properties have changed, we only have change
+            // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
+            // in this particular case, determining which cultures have changed works with the above with names since it will
+            // have always changed if it's been saved in the back office but that's not really fail safe.
+
+            // Save before raising event
+            OperationResult saveResult = Save(content, userId);
+
+            // always complete (but maybe return a failed status)
+            scope.Complete();
+
+            if (!saveResult.Success)
+            {
+                return saveResult.Success;
+            }
+
+            scope.Notifications.Publish(
+                new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification));
+
+            if (culturesChanging != null)
+            {
+                Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
+            }
+            else
+            {
+                Audit(AuditType.SendToPublish, content.WriterId, content.Id);
+            }
+
+            return saveResult.Success;
+        }
+    }
+
+    /// 
+    ///     Sorts a collection of  objects by updating the SortOrder according
+    ///     to the ordering of items in the passed in .
+    /// 
+    /// 
+    ///     Using this method will ensure that the Published-state is maintained upon sorting
+    ///     so the cache is updated accordingly - as needed.
+    /// 
+    /// 
+    /// 
+    /// Result indicating what action was taken when handling the command.
+    public OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        IContent[] itemsA = items.ToArray();
+        if (itemsA.Length == 0)
+        {
+            return new OperationResult(OperationResultType.NoOperation, evtMsgs);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
+            scope.Complete();
+            return ret;
+        }
+    }
+
+    /// 
+    ///     Sorts a collection of  objects by updating the SortOrder according
+    ///     to the ordering of items identified by the .
+    /// 
+    /// 
+    ///     Using this method will ensure that the Published-state is maintained upon sorting
+    ///     so the cache is updated accordingly - as needed.
+    /// 
+    /// 
+    /// 
+    /// Result indicating what action was taken when handling the command.
+    public OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        var idsA = ids?.ToArray();
+        if (idsA is null || idsA.Length == 0)
+        {
+            return new OperationResult(OperationResultType.NoOperation, evtMsgs);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            IContent[] itemsA = GetByIds(idsA).ToArray();
+
+            OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
+            scope.Complete();
+            return ret;
+        }
+    }
+
+    private OperationResult Sort(ICoreScope scope, IContent[] itemsA, int userId, EventMessages eventMessages)
+    {
+        var sortingNotification = new ContentSortingNotification(itemsA, eventMessages);
+        var savingNotification = new ContentSavingNotification(itemsA, eventMessages);
+
+        // raise cancelable sorting event
+        if (scope.Notifications.PublishCancelable(sortingNotification))
+        {
+            return OperationResult.Cancel(eventMessages);
+        }
+
+        // raise cancelable saving event
+        if (scope.Notifications.PublishCancelable(savingNotification))
+        {
+            return OperationResult.Cancel(eventMessages);
+        }
+
+        var published = new List();
+        var saved = new List();
+        var sortOrder = 0;
+
+        foreach (IContent content in itemsA)
+        {
+            // if the current sort order equals that of the content we don't
+            // need to update it, so just increment the sort order and continue.
+            if (content.SortOrder == sortOrder)
+            {
+                sortOrder++;
+                continue;
+            }
+
+            // else update
+            content.SortOrder = sortOrder++;
+            content.WriterId = userId;
+
+            // if it's published, register it, no point running StrategyPublish
+            // since we're not really publishing it and it cannot be cancelled etc
+            if (content.Published)
+            {
+                published.Add(content);
+            }
+
+            // save
+            saved.Add(content);
+            _documentRepository.Save(content);
+        }
+
+        // first saved, then sorted
+        scope.Notifications.Publish(
+            new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification));
+        scope.Notifications.Publish(
+            new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification));
+
+        scope.Notifications.Publish(
+            new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages));
+
+        if (published.Any())
+        {
+            scope.Notifications.Publish(new ContentPublishedNotification(published, eventMessages));
+        }
+
+        Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            ContentDataIntegrityReport report = _documentRepository.CheckDataIntegrity(options);
+
+            if (report.FixedIssues.Count > 0)
+            {
+                // The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
+                var root = new Content("root", -1, new ContentType(_shortStringHelper, -1)) { Id = -1, Key = Guid.Empty };
+                scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll, EventMessagesFactory.Get()));
+            }
+
+            return report;
+        }
+    }
+
+    #endregion
+
+    #region Internal Methods
+
+    /// 
+    ///     Gets a collection of  descendants by the first Parent.
+    /// 
+    ///  item to retrieve Descendants from
+    /// An Enumerable list of  objects
+    internal IEnumerable GetPublishedDescendants(IContent content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow!
+        }
+    }
+
+    internal IEnumerable GetPublishedDescendantsLocked(IContent content)
+    {
+        var pathMatch = content.Path + ",";
+        IQuery query = Query()
+            .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/);
+        IEnumerable contents = _documentRepository.Get(query);
+
+        // beware! contents contains all published version below content
+        // including those that are not directly published because below an unpublished content
+        // these must be filtered out here
+        var parents = new List { content.Id };
+        if (contents is not null)
+        {
+            foreach (IContent c in contents)
+            {
+                if (parents.Contains(c.ParentId))
                 {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
+                    yield return c;
+                    parents.Add(c.Id);
                 }
+            }
+        }
+    }
 
-                scope.WriteLock(Constants.Locks.ContentTree);
+    #endregion
 
+    #region Private Methods
+
+    private void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.Document.GetName(), message, parameters));
+
+    private bool IsDefaultCulture(IReadOnlyCollection? langs, string culture) =>
+        langs?.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)) ?? false;
+
+    private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) =>
+        langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture));
+
+    #endregion
+
+    #region Publishing Strategies
+
+    /// 
+    ///     Ensures that a document can be published
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    private PublishResult StrategyCanPublish(
+        ICoreScope scope,
+        IContent content,
+        bool checkPath,
+        IReadOnlyList? culturesPublishing,
+        IReadOnlyCollection? culturesUnpublishing,
+        EventMessages evtMsgs,
+        IReadOnlyCollection allLangs,
+        IDictionary? notificationState)
+    {
+        // raise Publishing notification
+        if (scope.Notifications.PublishCancelable(
+                new ContentPublishingNotification(content, evtMsgs).WithState(notificationState)))
+        {
+            _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled");
+            return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+        }
+
+        var variesByCulture = content.ContentType.VariesByCulture();
+
+        // If it's null it's invariant
+        CultureImpact[] impactsToPublish = culturesPublishing == null
+                ? new[] { _cultureImpactFactory.ImpactInvariant() }
+            : culturesPublishing.Select(x =>
+                _cultureImpactFactory.ImpactExplicit(
+                        x,
+                        allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory)))
+                    .ToArray();
+
+        // publish the culture(s)
+        if (!impactsToPublish.All(content.PublishCulture))
+        {
+            return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
+        }
+
+        // Validate the property values
+        IProperty[]? invalidProperties = null;
+        if (!impactsToPublish.All(x =>
+                _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x)))
+        {
+            return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
+            {
+                InvalidProperties = invalidProperties,
+            };
+        }
+
+        // Check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will
+        // be changed to Unpublished and any culture currently published will not be visible.
+        if (variesByCulture)
+        {
+            if (culturesPublishing == null)
+            {
+                throw new InvalidOperationException(
+                    "Internal error, variesByCulture but culturesPublishing is null.");
+            }
+
+            if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing?.Count == 0)
+            {
+                // no published cultures = cannot be published
+                // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case
+                // there will be nothing to publish/unpublish.
+                return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
+            }
+
+            // missing mandatory culture = cannot be published
+            IEnumerable mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
+            var mandatoryMissing = mandatoryCultures.Any(x =>
+                !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase));
+            if (mandatoryMissing)
+            {
+                return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content);
+            }
+
+            if (culturesPublishing.Count == 0 && culturesUnpublishing?.Count > 0)
+            {
+                return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
+            }
+        }
+
+        // ensure that the document has published values
+        // either because it is 'publishing' or because it already has a published version
+        if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0)
+        {
+            _logger.LogInformation(
+                "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                content.Name,
+                content.Id,
+                "document does not have published values");
+            return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
+        }
+
+        ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
+
+        // loop over each culture publishing - or string.Empty for invariant
+        foreach (var culture in culturesPublishing ?? new[] { string.Empty })
+        {
+            // ensure that the document status is correct
+            // note: culture will be string.Empty for invariant
+            switch (content.GetStatus(contentSchedule, culture))
+            {
+                case ContentStatus.Expired:
+                    if (!variesByCulture)
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired");
+                    }
+                    else
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document culture has expired");
+                    }
+
+                    return new PublishResult(
+                        !variesByCulture
+                            ? PublishResultType.FailedPublishHasExpired : PublishResultType.FailedPublishCultureHasExpired,
+                        evtMsgs,
+                        content);
+
+                case ContentStatus.AwaitingRelease:
+                    if (!variesByCulture)
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                            content.Name,
+                            content.Id,
+                            "document is awaiting release");
+                    }
+                    else
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}",
+                            content.Name,
+                            content.Id,
+                            culture,
+                            "document is culture awaiting release");
+                    }
+
+                    return new PublishResult(
+                        !variesByCulture
+                            ? PublishResultType.FailedPublishAwaitingRelease
+                            : PublishResultType.FailedPublishCultureAwaitingRelease,
+                        evtMsgs,
+                        content);
+
+                case ContentStatus.Trashed:
+                    _logger.LogInformation(
+                        "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                        content.Name,
+                        content.Id,
+                        "document is trashed");
+                    return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content);
+            }
+        }
+
+        if (checkPath)
+        {
+            // check if the content can be path-published
+            // root content can be published
+            // else check ancestors - we know we are not trashed
+            var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content));
+            if (!pathIsOk)
+            {
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                    content.Name,
+                    content.Id,
+                    "parent is not published");
+                return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content);
+            }
+        }
+
+        // If we are both publishing and unpublishing cultures, then return a mixed status
+        if (variesByCulture && culturesPublishing?.Count > 0 && culturesUnpublishing?.Count > 0)
+        {
+            return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
+        }
+
+        return new PublishResult(evtMsgs, content);
+    }
+
+    /// 
+    ///     Publishes a document
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     It is assumed that all publishing checks have passed before calling this method like
+    ///     
+    /// 
+    private PublishResult StrategyPublish(
+        IContent content,
+        IReadOnlyCollection? culturesPublishing,
+        IReadOnlyCollection? culturesUnpublishing,
+        EventMessages evtMsgs)
+    {
+        // change state to publishing
+        content.PublishedState = PublishedState.Publishing;
+
+        // if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result
+        if (content.ContentType.VariesByCulture())
+        {
+            if (content.Published && culturesUnpublishing?.Count == 0 && culturesPublishing?.Count == 0)
+            {
+                return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
+            }
+
+            if (culturesUnpublishing?.Count > 0)
+            {
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.",
+                    content.Name,
+                    content.Id,
+                    string.Join(",", culturesUnpublishing));
+            }
+
+            if (culturesPublishing?.Count > 0)
+            {
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.",
+                    content.Name,
+                    content.Id,
+                    string.Join(",", culturesPublishing));
+            }
+
+            if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count > 0)
+            {
+                return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
+            }
+
+            if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count == 0)
+            {
+                return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
+            }
+
+            return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
+        }
+
+        _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id);
+        return new PublishResult(evtMsgs, content);
+    }
+
+    /// 
+    ///     Ensures that a document can be unpublished
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    private PublishResult StrategyCanUnpublish(ICoreScope scope, IContent content, EventMessages evtMsgs)
+    {
+        // raise Unpublishing notification
+        if (scope.Notifications.PublishCancelable(new ContentUnpublishingNotification(content, evtMsgs)))
+        {
+            _logger.LogInformation(
+                "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id);
+            return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content);
+        }
+
+        return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
+    }
+
+    /// 
+    ///     Unpublishes a document
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     It is assumed that all unpublishing checks have passed before calling this method like
+    ///     
+    /// 
+    private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs)
+    {
+        var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
+
+        // TODO: What is this check?? we just created this attempt and of course it is Success?!
+        if (attempt.Success == false)
+        {
+            return attempt;
+        }
+
+        // if the document has any release dates set to before now,
+        // they should be removed so they don't interrupt an unpublish
+        // otherwise it would remain released == published
+        ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
+        IReadOnlyList pastReleases =
+            contentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now);
+        foreach (ContentSchedule p in pastReleases)
+        {
+            contentSchedule.Remove(p);
+        }
+
+        if (pastReleases.Count > 0)
+        {
+            _logger.LogInformation(
+                "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id);
+        }
+
+        _documentRepository.PersistContentSchedule(content, contentSchedule);
+
+        // change state to unpublishing
+        content.PublishedState = PublishedState.Unpublishing;
+
+        _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id);
+        return attempt;
+    }
+
+    #endregion
+
+    #region Content Types
+
+    /// 
+    ///     Deletes all content of specified type. All children of deleted content is moved to Recycle Bin.
+    /// 
+    /// 
+    ///     This needs extra care and attention as its potentially a dangerous and extensive operation.
+    ///     
+    ///         Deletes content items of the specified type, and only that type. Does *not* handle content types
+    ///         inheritance and compositions, which need to be managed outside of this method.
+    ///     
+    /// 
+    /// Id of the 
+    /// Optional Id of the user issuing the delete operation
+    public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: This currently this is called from the ContentTypeService but that needs to change,
+        // if we are deleting a content type, we should just delete the data and do this operation slightly differently.
+        // This method will recursively go lookup every content item, check if any of it's descendants are
+        // of a different type, move them to the recycle bin, then permanently delete the content items.
+        // The main problem with this is that for every content item being deleted, events are raised...
+        // which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
+        var changes = new List>();
+        var moves = new List<(IContent, string)>();
+        var contentTypeIdsA = contentTypeIds.ToArray();
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        // using an immediate uow here because we keep making changes with
+        // PerformMoveLocked and DeleteLocked that must be applied immediately,
+        // no point queuing operations
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            IQuery query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA);
+            IContent[] contents = _documentRepository.Get(query).ToArray();
+
+            if (contents is null)
+            {
+                return;
+            }
+
+            if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(contents, eventMessages)))
+            {
+                scope.Complete();
+                return;
+            }
+
+            // order by level, descending, so deepest first - that way, we cannot move
+            // a content of the deleted type, to the recycle bin (and then delete it...)
+            foreach (IContent content in contents.OrderByDescending(x => x.ParentId))
+            {
                 // if it's not trashed yet, and published, we should unpublish
                 // but... Unpublishing event makes no sense (not going to cancel?) and no need to save
                 // just raise the event
@@ -2236,1432 +3409,275 @@ namespace Umbraco.Cms.Core.Services
                     scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
                 }
 
+                // if current content has children, move them to trash
+                IContent c = content;
+                IQuery childQuery = Query().Where(x => x.ParentId == c.Id);
+                IEnumerable children = _documentRepository.Get(childQuery);
+                foreach (IContent child in children)
+                {
+                    // see MoveToRecycleBin
+                    PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true);
+                    changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch));
+                }
+
+                // delete content
+                // triggers the deleted event (and handles the files)
                 DeleteLocked(scope, content, eventMessages);
-
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages));
-                Audit(AuditType.Delete, userId, content.Id);
-
-                scope.Complete();
+                changes.Add(new TreeChange(content, TreeChangeTypes.Remove));
             }
 
-            return OperationResult.Succeed(eventMessages);
+            MoveEventInfo[] moveInfos = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
+            if (moveInfos.Length > 0)
+            {
+                scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(moveInfos, eventMessages));
+            }
+
+            scope.Notifications.Publish(new ContentTreeChangeNotification(changes, eventMessages));
+
+            Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete content of type {string.Join(",", contentTypeIdsA)}");
+
+            scope.Complete();
         }
-
-        private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
-        {
-            void DoDelete(IContent c)
-            {
-                _documentRepository.Delete(c);
-                scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
-
-                // media files deleted by QueuingEventDispatcher
-            }
-
-            const int pageSize = 500;
-            var total = long.MaxValue;
-            while (total > 0)
-            {
-                //get descendants - ordered from deepest to shallowest
-                IEnumerable descendants = GetPagedDescendants(content.Id, 0, pageSize, out total,
-                    ordering: Ordering.By("Path", Direction.Descending));
-                foreach (IContent c in descendants)
-                {
-                    DoDelete(c);
-                }
-            }
-
-            DoDelete(content);
-        }
-
-        //TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
-        // Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT,
-        // if that's not the case, then the file will never be deleted, because when we delete the content,
-        // the version referencing the file will not be there anymore. SO, we can leak files.
-
-        /// 
-        ///     Permanently deletes versions from an  object prior to a specific date.
-        ///     This method will never delete the latest version of a content item.
-        /// 
-        /// Id of the  object to delete versions from
-        /// Latest version date
-        /// Optional Id of the User deleting versions of a Content object
-        public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingVersionsNotification =
-                    new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.DeleteVersions(id, versionDate);
-
-                scope.Notifications.Publish(
-                    new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(
-                        deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version date)");
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Permanently deletes specific version(s) from an  object.
-        ///     This method will never delete the latest version of a content item.
-        /// 
-        /// Id of the  object to delete a version from
-        /// Id of the version to delete
-        /// Boolean indicating whether to delete versions prior to the versionId
-        /// Optional Id of the User deleting versions of a Content object
-        public void DeleteVersion(int id, int versionId, bool deletePriorVersions,
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, versionId);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (deletePriorVersions)
-                {
-                    IContent? content = GetVersion(versionId);
-                    DeleteVersions(id, content?.UpdateDate ?? DateTime.Now, userId);
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                IContent? c = _documentRepository.Get(id);
-                if (c?.VersionId != versionId &&
-                    c?.PublishedVersionId != versionId) // don't delete the current or published version
-                {
-                    _documentRepository.DeleteVersion(versionId);
-                }
-
-                scope.Notifications.Publish(
-                    new ContentDeletedVersionsNotification(id, evtMsgs, versionId).WithStateFrom(
-                        deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)");
-
-                scope.Complete();
-            }
-        }
-
-        #endregion
-
-        #region Move, RecycleBin
-
-        /// 
-        public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            var moves = new List<(IContent, string)>();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var originalPath = content.Path;
-                var moveEventInfo =
-                    new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent);
-
-                var movingToRecycleBinNotification =
-                    new ContentMovingToRecycleBinNotification(moveEventInfo, eventMessages);
-                if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages); // causes rollback
-                }
-
-                // if it's published we may want to force-unpublish it - that would be backward-compatible... but...
-                // making a radical decision here: trashing is equivalent to moving under an unpublished node so
-                // it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted
-                //if (content.HasPublishedVersion)
-                //{ }
-
-                PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true);
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
-
-                MoveEventInfo[] moveInfo = moves
-                    .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
-                    .ToArray();
-
-                scope.Notifications.Publish(
-                    new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom(
-                        movingToRecycleBinNotification));
-                Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
-
-                scope.Complete();
-            }
-
-            return OperationResult.Succeed(eventMessages);
-        }
-
-        /// 
-        ///     Moves an  object to a new location by changing its parent id.
-        /// 
-        /// 
-        ///     If the  object is already published it will be
-        ///     published after being moved to its new location. Otherwise it'll just
-        ///     be saved with a new parent id.
-        /// 
-        /// The  to move
-        /// Id of the Content's new Parent
-        /// Optional Id of the User moving the Content
-        public void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
-        {
-            // if moving to the recycle bin then use the proper method
-            if (parentId == Constants.System.RecycleBinContent)
-            {
-                MoveToRecycleBin(content, userId);
-                return;
-            }
-
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            var moves = new List<(IContent, string)>();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                IContent? parent = parentId == Constants.System.Root ? null : GetById(parentId);
-                if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
-                {
-                    throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
-                }
-
-                var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
-
-                var movingNotification = new ContentMovingNotification(moveEventInfo, eventMessages);
-                if (scope.Notifications.PublishCancelable(movingNotification))
-                {
-                    scope.Complete();
-                    return; // causes rollback
-                }
-
-                // if content was trashed, and since we're not moving to the recycle bin,
-                // indicate that the trashed status should be changed to false, else just
-                // leave it unchanged
-                var trashed = content.Trashed ? false : (bool?)null;
-
-                // if the content was trashed under another content, and so has a published version,
-                // it cannot move back as published but has to be unpublished first - that's for the
-                // root content, everything underneath will retain its published status
-                if (content.Trashed && content.Published)
-                {
-                    // however, it had been masked when being trashed, so there's no need for
-                    // any special event here - just change its state
-                    content.PublishedState = PublishedState.Unpublishing;
-                }
-
-                PerformMoveLocked(content, parentId, parent, userId, moves, trashed);
-
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
-
-                MoveEventInfo[] moveInfo = moves //changes
-                    .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
-                    .ToArray();
-
-                scope.Notifications.Publish(
-                    new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification));
-
-                Audit(AuditType.Move, userId, content.Id);
-
-                scope.Complete();
-            }
-        }
-
-        // MUST be called from within WriteLock
-        // trash indicates whether we are trashing, un-trashing, or not changing anything
-        private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId,
-            ICollection<(IContent, string)> moves,
-            bool? trash)
-        {
-            content.WriterId = userId;
-            content.ParentId = parentId;
-
-            // get the level delta (old pos to new pos)
-            // note that recycle bin (id:-20) level is 0!
-            var levelDelta = 1 - content.Level + (parent?.Level ?? 0);
-
-            var paths = new Dictionary();
-
-            moves.Add((content, content.Path)); // capture original path
-
-            //need to store the original path to lookup descendants based on it below
-            var originalPath = content.Path;
-
-            // these will be updated by the repo because we changed parentId
-            //content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
-            //content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId);
-            //content.Level += levelDelta;
-            PerformMoveContentLocked(content, userId, trash);
-
-            // if uow is not immediate, content.Path will be updated only when the UOW commits,
-            // and because we want it now, we have to calculate it by ourselves
-            //paths[content.Id] = content.Path;
-            paths[content.Id] =
-                (parent == null
-                    ? parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString
-                    : parent.Path) + "," + content.Id;
-
-            const int pageSize = 500;
-            IQuery? query = GetPagedDescendantQuery(originalPath);
-            long total;
-            do
-            {
-                // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
-                IEnumerable descendants =
-                    GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path"));
-
-                foreach (IContent descendant in descendants)
-                {
-                    moves.Add((descendant, descendant.Path)); // capture original path
-
-                    // update path and level since we do not update parentId
-                    descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
-                    descendant.Level += levelDelta;
-                    PerformMoveContentLocked(descendant, userId, trash);
-                }
-            } while (total > pageSize);
-        }
-
-        private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
-        {
-            if (trash.HasValue)
-            {
-                ((ContentBase)content).Trashed = trash.Value;
-            }
-
-            content.WriterId = userId;
-            _documentRepository.Save(content);
-        }
-
-        /// 
-        ///     Empties the Recycle Bin by deleting all  that resides in the bin
-        /// 
-        public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId)
-        {
-            var deleted = new List();
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                // emptying the recycle bin means deleting whatever is in there - do it properly!
-                IQuery? query = Query().Where(x => x.ParentId == Constants.System.RecycleBinContent);
-                IContent[] contents = _documentRepository.Get(query).ToArray();
-
-                var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages);
-                if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
-
-                if (contents is not null)
-                {
-                    foreach (IContent content in contents)
-                    {
-                        DeleteLocked(scope, content, eventMessages);
-                        deleted.Add(content);
-                    }
-                }
-
-                scope.Notifications.Publish(
-                    new ContentEmptiedRecycleBinNotification(deleted, eventMessages).WithStateFrom(
-                        emptyingRecycleBinNotification));
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(deleted, TreeChangeTypes.Remove, eventMessages));
-                Audit(AuditType.Delete, userId, Constants.System.RecycleBinContent, "Recycle bin emptied");
-
-                scope.Complete();
-            }
-
-            return OperationResult.Succeed(eventMessages);
-        }
-
-        public bool RecycleBinSmells()
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.RecycleBinSmells();
-            }
-        }
-
-        #endregion
-
-        #region Others
-
-        /// 
-        ///     Copies an  object by creating a new Content object of the same type and copies all data from
-        ///     the current
-        ///     to the new copy which is returned. Recursively copies all children.
-        /// 
-        /// The  to copy
-        /// Id of the Content's new Parent
-        /// Boolean indicating whether the copy should be related to the original
-        /// Optional Id of the User copying the Content
-        /// The newly created  object
-        public IContent? Copy(IContent content, int parentId, bool relateToOriginal,
-            int userId = Constants.Security.SuperUserId) => Copy(content, parentId, relateToOriginal, true, userId);
-
-        /// 
-        ///     Copies an  object by creating a new Content object of the same type and copies all data from
-        ///     the current
-        ///     to the new copy which is returned.
-        /// 
-        /// The  to copy
-        /// Id of the Content's new Parent
-        /// Boolean indicating whether the copy should be related to the original
-        /// A value indicating whether to recursively copy children.
-        /// Optional Id of the User copying the Content
-        /// The newly created  object
-        public IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive,
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            IContent copy = content.DeepCloneWithResetIdentities();
-            copy.ParentId = parentId;
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                if (scope.Notifications.PublishCancelable(
-                        new ContentCopyingNotification(content, copy, parentId, eventMessages)))
-                {
-                    scope.Complete();
-                    return null;
-                }
-
-                // note - relateToOriginal is not managed here,
-                // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do
-                // meaning that the event has to trigger for every copied content including descendants
-
-                var copies = new List>();
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                // a copy is not published (but not really unpublishing either)
-                // update the create author and last edit author
-                if (copy.Published)
-                {
-                    copy.Published = false;
-                }
-
-                copy.CreatorId = userId;
-                copy.WriterId = userId;
-
-                //get the current permissions, if there are any explicit ones they need to be copied
-                EntityPermissionCollection currentPermissions = GetPermissions(content);
-                currentPermissions.RemoveWhere(p => p.IsDefaultPermissions);
-
-                // save and flush because we need the ID for the recursive Copying events
-                _documentRepository.Save(copy);
-
-                //add permissions
-                if (currentPermissions.Count > 0)
-                {
-                    var permissionSet = new ContentPermissionSet(copy, currentPermissions);
-                    _documentRepository.AddOrUpdatePermissions(permissionSet);
-                }
-
-                // keep track of copies
-                copies.Add(Tuple.Create(content, copy));
-                var idmap = new Dictionary {[content.Id] = copy.Id};
-
-                if (recursive) // process descendants
-                {
-                    const int pageSize = 500;
-                    var page = 0;
-                    var total = long.MaxValue;
-                    while (page * pageSize < total)
-                    {
-                        IEnumerable descendants =
-                            GetPagedDescendants(content.Id, page++, pageSize, out total);
-                        foreach (IContent descendant in descendants)
-                        {
-                            // if parent has not been copied, skip, else gets its copy id
-                            if (idmap.TryGetValue(descendant.ParentId, out parentId) == false)
-                            {
-                                continue;
-                            }
-
-                            IContent descendantCopy = descendant.DeepCloneWithResetIdentities();
-                            descendantCopy.ParentId = parentId;
-
-                            if (scope.Notifications.PublishCancelable(
-                                    new ContentCopyingNotification(descendant, descendantCopy, parentId,
-                                        eventMessages)))
-                            {
-                                continue;
-                            }
-
-                            // a copy is not published (but not really unpublishing either)
-                            // update the create author and last edit author
-                            if (descendantCopy.Published)
-                            {
-                                descendantCopy.Published = false;
-                            }
-
-                            descendantCopy.CreatorId = userId;
-                            descendantCopy.WriterId = userId;
-
-                            // save and flush (see above)
-                            _documentRepository.Save(descendantCopy);
-
-                            copies.Add(Tuple.Create(descendant, descendantCopy));
-                            idmap[descendant.Id] = descendantCopy.Id;
-                        }
-                    }
-                }
-
-                // not handling tags here, because
-                // - tags should be handled by the content repository
-                // - a copy is unpublished and therefore has no impact on tags in DB
-
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(copy, TreeChangeTypes.RefreshBranch, eventMessages));
-                foreach (Tuple x in copies)
-                {
-                    scope.Notifications.Publish(new ContentCopiedNotification(x.Item1, x.Item2, parentId,
-                        relateToOriginal, eventMessages));
-                }
-
-                Audit(AuditType.Copy, userId, content.Id);
-
-                scope.Complete();
-            }
-
-            return copy;
-        }
-
-        /// 
-        ///     Sends an  to Publication, which executes handlers and events for the 'Send to Publication'
-        ///     action.
-        /// 
-        /// The  to send to publication
-        /// Optional Id of the User issuing the send to publication
-        /// True if sending publication was successful otherwise false
-        public bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId)
-        {
-            if (content is null)
-            {
-                return false;
-            }
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(sendingToPublishNotification))
-                {
-                    scope.Complete();
-                    return false;
-                }
-
-                //track the cultures changing for auditing
-                var culturesChanging = content.ContentType.VariesByCulture()
-                    ? string.Join(",", content.CultureInfos!.Values.Where(x => x.IsDirty()).Select(x => x.Culture))
-                    : null;
-
-                // TODO: Currently there's no way to change track which variant properties have changed, we only have change
-                // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
-                // in this particular case, determining which cultures have changed works with the above with names since it will
-                // have always changed if it's been saved in the back office but that's not really fail safe.
-
-                //Save before raising event
-                OperationResult saveResult = Save(content, userId);
-
-                // always complete (but maybe return a failed status)
-                scope.Complete();
-
-                if (!saveResult.Success)
-                {
-                    return saveResult.Success;
-                }
-
-                scope.Notifications.Publish(
-                    new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification));
-
-                if (culturesChanging != null)
-                {
-                    Audit(AuditType.SendToPublishVariant, userId, content.Id,
-                        $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
-                }
-                else
-                {
-                    Audit(AuditType.SendToPublish, content.WriterId, content.Id);
-                }
-
-                return saveResult.Success;
-            }
-        }
-
-        /// 
-        ///     Sorts a collection of  objects by updating the SortOrder according
-        ///     to the ordering of items in the passed in .
-        /// 
-        /// 
-        ///     Using this method will ensure that the Published-state is maintained upon sorting
-        ///     so the cache is updated accordingly - as needed.
-        /// 
-        /// 
-        /// 
-        /// Result indicating what action was taken when handling the command.
-        public OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            IContent[] itemsA = items.ToArray();
-            if (itemsA.Length == 0)
-            {
-                return new OperationResult(OperationResultType.NoOperation, evtMsgs);
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
-                scope.Complete();
-                return ret;
-            }
-        }
-
-        /// 
-        ///     Sorts a collection of  objects by updating the SortOrder according
-        ///     to the ordering of items identified by the .
-        /// 
-        /// 
-        ///     Using this method will ensure that the Published-state is maintained upon sorting
-        ///     so the cache is updated accordingly - as needed.
-        /// 
-        /// 
-        /// 
-        /// Result indicating what action was taken when handling the command.
-        public OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            var idsA = ids?.ToArray();
-            if (idsA is null || idsA.Length == 0)
-            {
-                return new OperationResult(OperationResultType.NoOperation, evtMsgs);
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                IContent[] itemsA = GetByIds(idsA).ToArray();
-
-                OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
-                scope.Complete();
-                return ret;
-            }
-        }
-
-        private OperationResult Sort(ICoreScope scope, IContent[] itemsA, int userId, EventMessages eventMessages)
-        {
-            var sortingNotification = new ContentSortingNotification(itemsA, eventMessages);
-            var savingNotification = new ContentSavingNotification(itemsA, eventMessages);
-
-            // raise cancelable sorting event
-            if (scope.Notifications.PublishCancelable(sortingNotification))
-            {
-                return OperationResult.Cancel(eventMessages);
-            }
-
-            // raise cancelable saving event
-            if (scope.Notifications.PublishCancelable(savingNotification))
-            {
-                return OperationResult.Cancel(eventMessages);
-            }
-
-            var published = new List();
-            var saved = new List();
-            var sortOrder = 0;
-
-            foreach (IContent content in itemsA)
-            {
-                // if the current sort order equals that of the content we don't
-                // need to update it, so just increment the sort order and continue.
-                if (content.SortOrder == sortOrder)
-                {
-                    sortOrder++;
-                    continue;
-                }
-
-                // else update
-                content.SortOrder = sortOrder++;
-                content.WriterId = userId;
-
-                // if it's published, register it, no point running StrategyPublish
-                // since we're not really publishing it and it cannot be cancelled etc
-                if (content.Published)
-                {
-                    published.Add(content);
-                }
-
-                // save
-                saved.Add(content);
-                _documentRepository.Save(content);
-            }
-
-            //first saved, then sorted
-            scope.Notifications.Publish(
-                new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification));
-            scope.Notifications.Publish(
-                new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification));
-
-            scope.Notifications.Publish(
-                new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages));
-
-            if (published.Any())
-            {
-                scope.Notifications.Publish(new ContentPublishedNotification(published, eventMessages));
-            }
-
-            Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
-            return OperationResult.Succeed(eventMessages);
-        }
-
-        public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                ContentDataIntegrityReport report = _documentRepository.CheckDataIntegrity(options);
-
-                if (report.FixedIssues.Count > 0)
-                {
-                    //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
-                    var root = new Content("root", -1, new ContentType(_shortStringHelper, -1))
-                    {
-                        Id = -1, Key = Guid.Empty
-                    };
-                    scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll,
-                        EventMessagesFactory.Get()));
-                }
-
-                return report;
-            }
-        }
-
-        #endregion
-
-        #region Internal Methods
-
-        /// 
-        ///     Gets a collection of  descendants by the first Parent.
-        /// 
-        ///  item to retrieve Descendants from
-        /// An Enumerable list of  objects
-        internal IEnumerable GetPublishedDescendants(IContent content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow!
-            }
-        }
-
-        internal IEnumerable GetPublishedDescendantsLocked(IContent content)
-        {
-            var pathMatch = content.Path + ",";
-            IQuery query = Query()
-                .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/);
-            IEnumerable contents = _documentRepository.Get(query);
-
-            // beware! contents contains all published version below content
-            // including those that are not directly published because below an unpublished content
-            // these must be filtered out here
-
-            var parents = new List {content.Id};
-            if (contents is not null)
-            {
-                foreach (IContent c in contents)
-                {
-                    if (parents.Contains(c.ParentId))
-                    {
-                        yield return c;
-                        parents.Add(c.Id);
-                    }
-                }
-            }
-        }
-
-        #endregion
-
-        #region Private Methods
-
-        private void Audit(AuditType type, int userId, int objectId, string? message = null,
-            string? parameters = null) =>
-            _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.Document.GetName(), message,
-                parameters));
-
-        private bool IsDefaultCulture(IReadOnlyCollection? langs, string culture) =>
-            langs?.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)) ?? false;
-
-        private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) =>
-            langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture));
-
-        #endregion
-
-        #region Publishing Strategies
-
-        /// 
-        ///     Ensures that a document can be published
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        private PublishResult StrategyCanPublish(ICoreScope scope, IContent content, bool checkPath,
-            IReadOnlyList? culturesPublishing,
-            IReadOnlyCollection? culturesUnpublishing, EventMessages evtMsgs,
-            IReadOnlyCollection allLangs, IDictionary? notificationState)
-        {
-            // raise Publishing notification
-            if (scope.Notifications.PublishCancelable(
-                    new ContentPublishingNotification(content, evtMsgs).WithState(notificationState)))
-            {
-                _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                    content.Name, content.Id, "publishing was cancelled");
-                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-            }
-
-            var variesByCulture = content.ContentType.VariesByCulture();
-
-            CultureImpact[] impactsToPublish = culturesPublishing == null
-                ? new[] {CultureImpact.Invariant} //if it's null it's invariant
-                : culturesPublishing.Select(x =>
-                    CultureImpact.Explicit(x,
-                        allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray();
-
-            // publish the culture(s)
-            if (!impactsToPublish.All(content.PublishCulture))
-            {
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
-            }
-
-            //validate the property values
-            IProperty[]? invalidProperties = null;
-            if (!impactsToPublish.All(x =>
-                    _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x)))
-            {
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
-                {
-                    InvalidProperties = invalidProperties
-                };
-            }
-
-            //Check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will
-            // be changed to Unpublished and any culture currently published will not be visible.
-            if (variesByCulture)
-            {
-                if (culturesPublishing == null)
-                {
-                    throw new InvalidOperationException(
-                        "Internal error, variesByCulture but culturesPublishing is null.");
-                }
-
-                if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing?.Count == 0)
-                {
-                    // no published cultures = cannot be published
-                    // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case
-                    // there will be nothing to publish/unpublish.
-                    return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
-                }
-
-
-                // missing mandatory culture = cannot be published
-                IEnumerable mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
-                var mandatoryMissing = mandatoryCultures.Any(x =>
-                    !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase));
-                if (mandatoryMissing)
-                {
-                    return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content);
-                }
-
-                if (culturesPublishing.Count == 0 && culturesUnpublishing?.Count > 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
-                }
-            }
-
-            // ensure that the document has published values
-            // either because it is 'publishing' or because it already has a published version
-            if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0)
-            {
-                _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                    content.Name, content.Id, "document does not have published values");
-                return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
-            }
-
-            ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
-            //loop over each culture publishing - or string.Empty for invariant
-            foreach (var culture in culturesPublishing ?? new[] {string.Empty})
-            {
-                // ensure that the document status is correct
-                // note: culture will be string.Empty for invariant
-                switch (content.GetStatus(contentSchedule, culture))
-                {
-                    case ContentStatus.Expired:
-                        if (!variesByCulture)
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name,
-                                content.Id, "document has expired");
-                        }
-                        else
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}",
-                                content.Name, content.Id, culture, "document culture has expired");
-                        }
-
-                        return new PublishResult(
-                            !variesByCulture
-                                ? PublishResultType.FailedPublishHasExpired
-                                : PublishResultType.FailedPublishCultureHasExpired, evtMsgs, content);
-
-                    case ContentStatus.AwaitingRelease:
-                        if (!variesByCulture)
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name,
-                                content.Id, "document is awaiting release");
-                        }
-                        else
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}",
-                                content.Name, content.Id, culture, "document is culture awaiting release");
-                        }
-
-                        return new PublishResult(
-                            !variesByCulture
-                                ? PublishResultType.FailedPublishAwaitingRelease
-                                : PublishResultType.FailedPublishCultureAwaitingRelease, evtMsgs, content);
-
-                    case ContentStatus.Trashed:
-                        _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                            content.Name, content.Id, "document is trashed");
-                        return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content);
-                }
-            }
-
-            if (checkPath)
-            {
-                // check if the content can be path-published
-                // root content can be published
-                // else check ancestors - we know we are not trashed
-                var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content));
-                if (!pathIsOk)
-                {
-                    _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                        content.Name, content.Id, "parent is not published");
-                    return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content);
-                }
-            }
-
-            //If we are both publishing and unpublishing cultures, then return a mixed status
-            if (variesByCulture && culturesPublishing?.Count > 0 && culturesUnpublishing?.Count > 0)
-            {
-                return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
-            }
-
-            return new PublishResult(evtMsgs, content);
-        }
-
-        /// 
-        ///     Publishes a document
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        ///     It is assumed that all publishing checks have passed before calling this method like
-        ///     
-        /// 
-        private PublishResult StrategyPublish(IContent content,
-            IReadOnlyCollection? culturesPublishing, IReadOnlyCollection? culturesUnpublishing,
-            EventMessages evtMsgs)
-        {
-            // change state to publishing
-            content.PublishedState = PublishedState.Publishing;
-
-            //if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result
-            if (content.ContentType.VariesByCulture())
-            {
-                if (content.Published && culturesUnpublishing?.Count == 0 && culturesPublishing?.Count == 0)
-                {
-                    return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
-                }
-
-                if (culturesUnpublishing?.Count > 0)
-                {
-                    _logger.LogInformation(
-                        "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.",
-                        content.Name, content.Id, string.Join(",", culturesUnpublishing));
-                }
-
-                if (culturesPublishing?.Count > 0)
-                {
-                    _logger.LogInformation(
-                        "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.",
-                        content.Name, content.Id, string.Join(",", culturesPublishing));
-                }
-
-                if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count > 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
-                }
-
-                if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count == 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
-                }
-
-                return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
-            }
-
-            _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name,
-                content.Id);
-            return new PublishResult(evtMsgs, content);
-        }
-
-        /// 
-        ///     Ensures that a document can be unpublished
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        private PublishResult StrategyCanUnpublish(ICoreScope scope, IContent content, EventMessages evtMsgs)
-        {
-            // raise Unpublishing notification
-            if (scope.Notifications.PublishCancelable(new ContentUnpublishingNotification(content, evtMsgs)))
-            {
-                _logger.LogInformation(
-                    "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.",
-                    content.Name, content.Id);
-                return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content);
-            }
-
-            return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
-        }
-
-        /// 
-        ///     Unpublishes a document
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        ///     It is assumed that all unpublishing checks have passed before calling this method like
-        ///     
-        /// 
-        private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs)
-        {
-            var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
-
-            //TODO: What is this check?? we just created this attempt and of course it is Success?!
-            if (attempt.Success == false)
-            {
-                return attempt;
-            }
-
-            // if the document has any release dates set to before now,
-            // they should be removed so they don't interrupt an unpublish
-            // otherwise it would remain released == published
-
-            var contentSchedule = _documentRepository.GetContentSchedule(content.Id);
-            var pastReleases = contentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now);
-            foreach (var p in pastReleases)
-                contentSchedule.Remove(p);
-
-            if (pastReleases.Count > 0)
-            {
-                _logger.LogInformation(
-                    "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.",
-                    content.Name, content.Id);
-            }
-
-            _documentRepository.PersistContentSchedule(content, contentSchedule);
-            // change state to unpublishing
-            content.PublishedState = PublishedState.Unpublishing;
-
-            _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name,
-                content.Id);
-            return attempt;
-        }
-
-        #endregion
-
-        #region Content Types
-
-        /// 
-        ///     Deletes all content of specified type. All children of deleted content is moved to Recycle Bin.
-        /// 
-        /// 
-        ///     This needs extra care and attention as its potentially a dangerous and extensive operation.
-        ///     
-        ///         Deletes content items of the specified type, and only that type. Does *not* handle content types
-        ///         inheritance and compositions, which need to be managed outside of this method.
-        ///     
-        /// 
-        /// Id of the 
-        /// Optional Id of the user issuing the delete operation
-        public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId)
-        {
-            // TODO: This currently this is called from the ContentTypeService but that needs to change,
-            // if we are deleting a content type, we should just delete the data and do this operation slightly differently.
-            // This method will recursively go lookup every content item, check if any of it's descendants are
-            // of a different type, move them to the recycle bin, then permanently delete the content items.
-            // The main problem with this is that for every content item being deleted, events are raised...
-            // which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
-
-            var changes = new List>();
-            var moves = new List<(IContent, string)>();
-            var contentTypeIdsA = contentTypeIds.ToArray();
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            // using an immediate uow here because we keep making changes with
-            // PerformMoveLocked and DeleteLocked that must be applied immediately,
-            // no point queuing operations
-            //
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                IQuery query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA);
-                IContent[] contents = _documentRepository.Get(query).ToArray();
-
-                if (contents is null)
-                {
-                    return;
-                }
-
-                if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(contents, eventMessages)))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                // order by level, descending, so deepest first - that way, we cannot move
-                // a content of the deleted type, to the recycle bin (and then delete it...)
-                foreach (IContent content in contents.OrderByDescending(x => x.ParentId))
-                {
-                    // if it's not trashed yet, and published, we should unpublish
-                    // but... Unpublishing event makes no sense (not going to cancel?) and no need to save
-                    // just raise the event
-                    if (content.Trashed == false && content.Published)
-                    {
-                        scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
-                    }
-
-                    // if current content has children, move them to trash
-                    IContent c = content;
-                    IQuery childQuery = Query().Where(x => x.ParentId == c.Id);
-                    IEnumerable children = _documentRepository.Get(childQuery);
-                    foreach (IContent child in children)
-                    {
-                        // see MoveToRecycleBin
-                        PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true);
-                        changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch));
-                    }
-
-                    // delete content
-                    // triggers the deleted event (and handles the files)
-                    DeleteLocked(scope, content, eventMessages);
-                    changes.Add(new TreeChange(content, TreeChangeTypes.Remove));
-                }
-
-                MoveEventInfo[] moveInfos = moves
-                    .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
-                    .ToArray();
-                if (moveInfos.Length > 0)
-                {
-                    scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(moveInfos, eventMessages));
-                }
-
-                scope.Notifications.Publish(new ContentTreeChangeNotification(changes, eventMessages));
-
-                Audit(AuditType.Delete, userId, Constants.System.Root,
-                    $"Delete content of type {string.Join(",", contentTypeIdsA)}");
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin.
-        /// 
-        /// This needs extra care and attention as its potentially a dangerous and extensive operation
-        /// Id of the 
-        /// Optional id of the user deleting the media
-        public void DeleteOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
-            DeleteOfTypes(new[] {contentTypeId}, userId);
-
-        private IContentType GetContentType(ICoreScope scope, string contentTypeAlias)
-        {
-            if (contentTypeAlias == null)
-            {
-                throw new ArgumentNullException(nameof(contentTypeAlias));
-            }
-
-            if (string.IsNullOrWhiteSpace(contentTypeAlias))
-            {
-                throw new ArgumentException("Value can't be empty or consist only of white-space characters.",
-                    nameof(contentTypeAlias));
-            }
-
-            scope.ReadLock(Constants.Locks.ContentTypes);
-
-            IQuery query = Query().Where(x => x.Alias == contentTypeAlias);
-            IContentType? contentType = _contentTypeRepository.Get(query).FirstOrDefault();
-
-            if (contentType == null)
-            {
-                throw new Exception(
-                    $"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback
-            }
-
-            return contentType;
-        }
-
-        private IContentType GetContentType(string contentTypeAlias)
-        {
-            if (contentTypeAlias == null)
-            {
-                throw new ArgumentNullException(nameof(contentTypeAlias));
-            }
-
-            if (string.IsNullOrWhiteSpace(contentTypeAlias))
-            {
-                throw new ArgumentException("Value can't be empty or consist only of white-space characters.",
-                    nameof(contentTypeAlias));
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return GetContentType(scope, contentTypeAlias);
-            }
-        }
-
-        #endregion
-
-        #region Blueprints
-
-        public IContent? GetBlueprintById(int id)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IContent? blueprint = _documentBlueprintRepository.Get(id);
-                if (blueprint != null)
-                {
-                    blueprint.Blueprint = true;
-                }
-
-                return blueprint;
-            }
-        }
-
-        public IContent? GetBlueprintById(Guid id)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IContent? blueprint = _documentBlueprintRepository.Get(id);
-                if (blueprint != null)
-                {
-                    blueprint.Blueprint = true;
-                }
-
-                return blueprint;
-            }
-        }
-
-        public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            //always ensure the blueprint is at the root
-            if (content.ParentId != -1)
-            {
-                content.ParentId = -1;
-            }
-
-            content.Blueprint = true;
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                if (content.HasIdentity == false)
-                {
-                    content.CreatorId = userId;
-                }
-
-                content.WriterId = userId;
-
-                _documentBlueprintRepository.Save(content);
-
-                Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id,
-                    $"Saved content template: {content.Name}");
-
-                scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
-
-                scope.Complete();
-            }
-        }
-
-        public void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentBlueprintRepository.Delete(content);
-                scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs));
-                scope.Complete();
-            }
-        }
-
-        private static readonly string?[] ArrayOfOneNullString = {null};
-
-        public IContent CreateContentFromBlueprint(IContent blueprint, string name,
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (blueprint == null)
-            {
-                throw new ArgumentNullException(nameof(blueprint));
-            }
-
-            IContentType contentType = GetContentType(blueprint.ContentType.Alias);
-            var content = new Content(name, -1, contentType);
-            content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id);
-
-            content.CreatorId = userId;
-            content.WriterId = userId;
-
-            IEnumerable cultures = ArrayOfOneNullString;
-            if (blueprint.CultureInfos?.Count > 0)
-            {
-                cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
-                using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-                {
-                    if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(),
-                            out ContentCultureInfos defaultCulture))
-                    {
-                        defaultCulture.Name = name;
-                    }
-
-                    scope.Complete();
-                }
-            }
-
-            DateTime now = DateTime.Now;
-            foreach (var culture in cultures)
-            {
-                foreach (IProperty property in blueprint.Properties)
-                {
-                    var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null;
-                    content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture);
-                }
-
-                if (!string.IsNullOrEmpty(culture))
-                {
-                    content.SetCultureInfo(culture, blueprint.GetCultureName(culture), now);
-                }
-            }
-
-            return content;
-        }
-
-        public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                IQuery query = Query();
-                if (contentTypeId.Length > 0)
-                {
-                    query.Where(x => contentTypeId.Contains(x.ContentTypeId));
-                }
-
-                return _documentBlueprintRepository.Get(query).Select(x =>
-                {
-                    x.Blueprint = true;
-                    return x;
-                });
-            }
-        }
-
-        public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds,
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var contentTypeIdsA = contentTypeIds.ToArray();
-                IQuery query = Query();
-                if (contentTypeIdsA.Length > 0)
-                {
-                    query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId));
-                }
-
-                IContent[]? blueprints = _documentBlueprintRepository.Get(query)?.Select(x =>
-                {
-                    x.Blueprint = true;
-                    return x;
-                }).ToArray();
-
-                if (blueprints is not null)
-                {
-                    foreach (IContent blueprint in blueprints)
-                    {
-                        _documentBlueprintRepository.Delete(blueprint);
-                    }
-
-                    scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
-                    scope.Complete();
-                }
-            }
-        }
-
-        public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
-            DeleteBlueprintsOfTypes(new[] {contentTypeId}, userId);
-
-        #endregion
     }
+
+    /// 
+    ///     Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin.
+    /// 
+    /// This needs extra care and attention as its potentially a dangerous and extensive operation
+    /// Id of the 
+    /// Optional id of the user deleting the media
+    public void DeleteOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
+        DeleteOfTypes(new[] { contentTypeId }, userId);
+
+    private IContentType GetContentType(ICoreScope scope, string contentTypeAlias)
+    {
+        if (contentTypeAlias == null)
+        {
+            throw new ArgumentNullException(nameof(contentTypeAlias));
+        }
+
+        if (string.IsNullOrWhiteSpace(contentTypeAlias))
+        {
+            throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias));
+        }
+
+        scope.ReadLock(Constants.Locks.ContentTypes);
+
+        IQuery query = Query().Where(x => x.Alias == contentTypeAlias);
+        IContentType? contentType = _contentTypeRepository.Get(query).FirstOrDefault();
+
+        if (contentType == null)
+        {
+            throw new Exception(
+                $"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback
+        }
+
+        return contentType;
+    }
+
+    private IContentType GetContentType(string contentTypeAlias)
+    {
+        if (contentTypeAlias == null)
+        {
+            throw new ArgumentNullException(nameof(contentTypeAlias));
+        }
+
+        if (string.IsNullOrWhiteSpace(contentTypeAlias))
+        {
+            throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias));
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return GetContentType(scope, contentTypeAlias);
+        }
+    }
+
+    #endregion
+
+    #region Blueprints
+
+    public IContent? GetBlueprintById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IContent? blueprint = _documentBlueprintRepository.Get(id);
+            if (blueprint != null)
+            {
+                blueprint.Blueprint = true;
+            }
+
+            return blueprint;
+        }
+    }
+
+    public IContent? GetBlueprintById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IContent? blueprint = _documentBlueprintRepository.Get(id);
+            if (blueprint != null)
+            {
+                blueprint.Blueprint = true;
+            }
+
+            return blueprint;
+        }
+    }
+
+    public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        // always ensure the blueprint is at the root
+        if (content.ParentId != -1)
+        {
+            content.ParentId = -1;
+        }
+
+        content.Blueprint = true;
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            if (content.HasIdentity == false)
+            {
+                content.CreatorId = userId;
+            }
+
+            content.WriterId = userId;
+
+            _documentBlueprintRepository.Save(content);
+
+            Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id, $"Saved content template: {content.Name}");
+
+            scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
+
+            scope.Complete();
+        }
+    }
+
+    public void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentBlueprintRepository.Delete(content);
+            scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs));
+            scope.Complete();
+        }
+    }
+
+    private static readonly string?[] ArrayOfOneNullString = { null };
+
+    public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
+    {
+        if (blueprint == null)
+        {
+            throw new ArgumentNullException(nameof(blueprint));
+        }
+
+        IContentType contentType = GetContentType(blueprint.ContentType.Alias);
+        var content = new Content(name, -1, contentType);
+        content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id);
+
+        content.CreatorId = userId;
+        content.WriterId = userId;
+
+        IEnumerable cultures = ArrayOfOneNullString;
+        if (blueprint.CultureInfos?.Count > 0)
+        {
+            cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            {
+                if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
+                {
+                    defaultCulture.Name = name;
+                }
+
+                scope.Complete();
+            }
+        }
+
+        DateTime now = DateTime.Now;
+        foreach (var culture in cultures)
+        {
+            foreach (IProperty property in blueprint.Properties)
+            {
+                var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null;
+                content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture);
+            }
+
+            if (!string.IsNullOrEmpty(culture))
+            {
+                content.SetCultureInfo(culture, blueprint.GetCultureName(culture), now);
+            }
+        }
+
+        return content;
+    }
+
+    public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query();
+            if (contentTypeId.Length > 0)
+            {
+                query.Where(x => contentTypeId.Contains(x.ContentTypeId));
+            }
+
+            return _documentBlueprintRepository.Get(query).Select(x =>
+            {
+                x.Blueprint = true;
+                return x;
+            });
+        }
+    }
+
+    public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var contentTypeIdsA = contentTypeIds.ToArray();
+            IQuery query = Query();
+            if (contentTypeIdsA.Length > 0)
+            {
+                query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId));
+            }
+
+            IContent[]? blueprints = _documentBlueprintRepository.Get(query)?.Select(x =>
+            {
+                x.Blueprint = true;
+                return x;
+            }).ToArray();
+
+            if (blueprints is not null)
+            {
+                foreach (IContent blueprint in blueprints)
+                {
+                    _documentBlueprintRepository.Delete(blueprint);
+                }
+
+                scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
+                scope.Complete();
+            }
+        }
+    }
+
+    public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
+        DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
index 726c5b4435..b042612b1a 100644
--- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
@@ -1,102 +1,106 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Text.RegularExpressions;
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+/// 
+///     Content service extension methods
+/// 
+public static class ContentServiceExtensions
 {
-    /// 
-    /// Content service extension methods
-    /// 
-    public static class ContentServiceExtensions
+    #region RTE Anchor values
+
+    private static readonly Regex AnchorRegex = new("", RegexOptions.Compiled);
+
+    public static IEnumerable? GetByIds(this IContentService contentService, IEnumerable ids)
     {
-        #region RTE Anchor values
-
-        private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled);
-
-        public static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string? culture = "*")
+        var guids = new List();
+        foreach (Udi udi in ids)
         {
-            var result = new List();
-            var content = contentService.GetById(id);
-
-            if (content is not null)
+            if (udi is not GuidUdi guidUdi)
             {
-                foreach (var contentProperty in content.Properties)
+                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                    " which is required by content");
+            }
+
+            guids.Add(guidUdi);
+        }
+
+        return contentService.GetByIds(guids.Select(x => x.Guid));
+    }
+
+    /// 
+    ///     Method to create an IContent object based on the Udi of a parent
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        if (parentId is not GuidUdi guidUdi)
+        {
+            throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                " which is required by content");
+        }
+
+        IContent? parent = contentService.GetById(guidUdi.Guid);
+        return contentService.Create(name, parent, contentTypeAlias, userId);
+    }
+
+    /// 
+    ///     Remove all permissions for this user for all nodes
+    /// 
+    /// 
+    /// 
+    public static void RemoveContentPermissions(this IContentService contentService, int contentId) =>
+        contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection()));
+
+    public static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string? culture = "*")
+    {
+        var result = new List();
+        IContent? content = contentService.GetById(id);
+
+        if (content is not null)
+        {
+            foreach (IProperty contentProperty in content.Properties)
+            {
+                if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases
+                        .TinyMce))
                 {
-                    if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce))
+                    var value = contentProperty.GetValue(culture)?.ToString();
+                    if (!string.IsNullOrEmpty(value))
                     {
-                        var value = contentProperty.GetValue(culture)?.ToString();
-                        if (!string.IsNullOrEmpty(value))
-                        {
-                            result.AddRange(contentService.GetAnchorValuesFromRTEContent(value));
-                        }
+                        result.AddRange(contentService.GetAnchorValuesFromRTEContent(value));
                     }
                 }
             }
-
-            return result;
         }
 
-
-        public static IEnumerable GetAnchorValuesFromRTEContent(this IContentService contentService, string rteContent)
-        {
-            var result = new List();
-            var matches = AnchorRegex.Matches(rteContent);
-            foreach (Match match in matches)
-            {
-                result.Add(match.Value.Split(Constants.CharArrays.DoubleQuote)[1]);
-            }
-            return result;
-        }
-        #endregion
-
-        public static IEnumerable? GetByIds(this IContentService contentService, IEnumerable ids)
-        {
-            var guids = new List();
-            foreach (var udi in ids)
-            {
-                var guidUdi = udi as GuidUdi;
-                if (guidUdi is null)
-                    throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
-                guids.Add(guidUdi);
-            }
-
-            return contentService.GetByIds(guids.Select(x => x.Guid));
-        }
-
-        /// 
-        /// Method to create an IContent object based on the Udi of a parent
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
-        {
-            var guidUdi = parentId as GuidUdi;
-            if (guidUdi is null)
-                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
-            var parent = contentService.GetById(guidUdi.Guid);
-            return contentService.Create(name, parent, contentTypeAlias, userId);
-        }
-
-        /// 
-        /// Remove all permissions for this user for all nodes
-        /// 
-        /// 
-        /// 
-        public static void RemoveContentPermissions(this IContentService contentService, int contentId)
-        {
-            contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection()));
-        }
+        return result;
     }
+
+    public static IEnumerable GetAnchorValuesFromRTEContent(
+        this IContentService contentService,
+        string rteContent)
+    {
+        var result = new List();
+        MatchCollection matches = AnchorRegex.Matches(rteContent);
+        foreach (Match match in matches)
+        {
+            result.Add(match.Value.Split(Constants.CharArrays.DoubleQuote)[1]);
+        }
+
+        return result;
+    }
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs b/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs
index b493460876..36a790b9f6 100644
--- a/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs
+++ b/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs
@@ -1,42 +1,50 @@
-using System;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class ContentTypeBaseServiceProvider : IContentTypeBaseServiceProvider
 {
-    public class ContentTypeBaseServiceProvider : IContentTypeBaseServiceProvider
+    private readonly IContentTypeService _contentTypeService;
+    private readonly IMediaTypeService _mediaTypeService;
+    private readonly IMemberTypeService _memberTypeService;
+
+    public ContentTypeBaseServiceProvider(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService)
     {
-        private readonly IContentTypeService _contentTypeService;
-        private readonly IMediaTypeService _mediaTypeService;
-        private readonly IMemberTypeService _memberTypeService;
+        _contentTypeService = contentTypeService;
+        _mediaTypeService = mediaTypeService;
+        _memberTypeService = memberTypeService;
+    }
 
-        public ContentTypeBaseServiceProvider(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService)
+    public IContentTypeBaseService For(IContentBase contentBase)
+    {
+        if (contentBase == null)
         {
-            _contentTypeService = contentTypeService;
-            _mediaTypeService = mediaTypeService;
-            _memberTypeService = memberTypeService;
+            throw new ArgumentNullException(nameof(contentBase));
         }
 
-        public IContentTypeBaseService For(IContentBase contentBase)
+        switch (contentBase)
         {
-            if (contentBase == null) throw new ArgumentNullException(nameof(contentBase));
-            switch (contentBase)
-            {
-                case IContent _:
-                    return  _contentTypeService;
-                case IMedia _:
-                    return   _mediaTypeService;
-                case IMember _:
-                    return  _memberTypeService;
-                default:
-                    throw new ArgumentException($"Invalid contentBase type: {contentBase.GetType().FullName}" , nameof(contentBase));
-            }
-        }
-
-        // note: this should be a default interface method with C# 8
-        public IContentTypeComposition? GetContentTypeOf(IContentBase contentBase)
-        {
-            if (contentBase == null) throw new ArgumentNullException(nameof(contentBase));
-            return For(contentBase).Get(contentBase.ContentTypeId);
+            case IContent _:
+                return _contentTypeService;
+            case IMedia _:
+                return _mediaTypeService;
+            case IMember _:
+                return _memberTypeService;
+            default:
+                throw new ArgumentException(
+                    $"Invalid contentBase type: {contentBase.GetType().FullName}",
+                    nameof(contentBase));
         }
     }
+
+    // note: this should be a default interface method with C# 8
+    public IContentTypeComposition? GetContentTypeOf(IContentBase contentBase)
+    {
+        if (contentBase == null)
+        {
+            throw new ArgumentNullException(nameof(contentBase));
+        }
+
+        return For(contentBase).Get(contentBase.ContentTypeId);
+    }
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs
index 8f7316d913..39adcf0daf 100644
--- a/src/Umbraco.Core/Services/ContentTypeService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -9,125 +6,138 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the ContentType Service, which is an easy access to operations involving 
+/// 
+public class ContentTypeService : ContentTypeServiceBase, IContentTypeService
 {
+    public ContentTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IContentService contentService,
+        IContentTypeRepository repository,
+        IAuditRepository auditRepository,
+        IDocumentTypeContainerRepository entityContainerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(provider, loggerFactory, eventMessagesFactory, repository, auditRepository, entityContainerRepository, entityRepository, eventAggregator) =>
+        ContentService = contentService;
+
+    // beware! order is important to avoid deadlocks
+    protected override int[] ReadLockIds { get; } = { Constants.Locks.ContentTypes };
+
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.ContentTree, Constants.Locks.ContentTypes };
+
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.DocumentType;
+
+    private IContentService ContentService { get; }
+
     /// 
-    /// Represents the ContentType Service, which is an easy access to operations involving 
+    ///     Gets all property type aliases across content, media and member types.
     /// 
-    public class ContentTypeService : ContentTypeServiceBase, IContentTypeService
+    /// All property type aliases.
+    /// Beware! Works across content, media and member types.
+    public IEnumerable GetAllPropertyTypeAliases()
     {
-        public ContentTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IContentService contentService,
-            IContentTypeRepository repository, IAuditRepository auditRepository, IDocumentTypeContainerRepository entityContainerRepository, IEntityRepository entityRepository,
-            IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory, repository, auditRepository, entityContainerRepository, entityRepository, eventAggregator)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            ContentService = contentService;
-        }
-
-        // beware! order is important to avoid deadlocks
-        protected override int[] ReadLockIds { get; } = { Cms.Core.Constants.Locks.ContentTypes };
-        protected override int[] WriteLockIds { get; } = { Cms.Core.Constants.Locks.ContentTree, Cms.Core.Constants.Locks.ContentTypes };
-
-        private IContentService ContentService { get; }
-
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.DocumentType;
-
-        #region Notifications
-
-        protected override SavingNotification GetSavingNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeSavingNotification(item, eventMessages);
-
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeSavingNotification(items, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeSavedNotification(item, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeSavedNotification(items, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeDeletingNotification(item, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeDeletingNotification(items, eventMessages);
-
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeDeletedNotification(items, eventMessages);
-
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new ContentTypeMovingNotification(moveInfo, eventMessages);
-
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new ContentTypeMovedNotification(moveInfo, eventMessages);
-
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new ContentTypeChangedNotification(changes, eventMessages);
-
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new ContentTypeRefreshedNotification(changes, eventMessages);
-
-        #endregion
-
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var typeIdsA = typeIds.ToArray();
-                ContentService.DeleteOfTypes(typeIdsA);
-                ContentService.DeleteBlueprintsOfTypes(typeIdsA);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Gets all property type aliases across content, media and member types.
-        /// 
-        /// All property type aliases.
-        /// Beware! Works across content, media and member types.
-        public IEnumerable GetAllPropertyTypeAliases()
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // that one is special because it works across content, media and member types
-                scope.ReadLock(new[] { Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes });
-                return Repository.GetAllPropertyTypeAliases();
-            }
-        }
-
-        /// 
-        /// Gets all content type aliases across content, media and member types.
-        /// 
-        /// Optional object types guid to restrict to content, and/or media, and/or member types.
-        /// All content type aliases.
-        /// Beware! Works across content, media and member types.
-        public IEnumerable GetAllContentTypeAliases(params Guid[] guids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // that one is special because it works across content, media and member types
-                scope.ReadLock(new[] { Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes });
-                return Repository.GetAllContentTypeAliases(guids);
-            }
-        }
-
-        /// 
-        /// Gets all content type id for aliases across content, media and member types.
-        /// 
-        /// Aliases to look for.
-        /// All content type ids.
-        /// Beware! Works across content, media and member types.
-        public IEnumerable GetAllContentTypeIds(string[] aliases)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // that one is special because it works across content, media and member types
-                scope.ReadLock(new[] { Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes });
-                return Repository.GetAllContentTypeIds(aliases);
-            }
+            // that one is special because it works across content, media and member types
+            scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes);
+            return Repository.GetAllPropertyTypeAliases();
         }
     }
+
+    /// 
+    ///     Gets all content type aliases across content, media and member types.
+    /// 
+    /// Optional object types guid to restrict to content, and/or media, and/or member types.
+    /// All content type aliases.
+    /// Beware! Works across content, media and member types.
+    public IEnumerable GetAllContentTypeAliases(params Guid[] guids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // that one is special because it works across content, media and member types
+            scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes);
+            return Repository.GetAllContentTypeAliases(guids);
+        }
+    }
+
+    /// 
+    ///     Gets all content type id for aliases across content, media and member types.
+    /// 
+    /// Aliases to look for.
+    /// All content type ids.
+    /// Beware! Works across content, media and member types.
+    public IEnumerable GetAllContentTypeIds(string[] aliases)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // that one is special because it works across content, media and member types
+            scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes);
+            return Repository.GetAllContentTypeIds(aliases);
+        }
+    }
+
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var typeIdsA = typeIds.ToArray();
+            ContentService.DeleteOfTypes(typeIdsA);
+            ContentService.DeleteBlueprintsOfTypes(typeIdsA);
+            scope.Complete();
+        }
+    }
+
+    #region Notifications
+
+    protected override SavingNotification GetSavingNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeSavingNotification(item, eventMessages);
+
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeSavingNotification(items, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeSavedNotification(item, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeSavedNotification(items, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeDeletingNotification(item, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeDeletingNotification(items, eventMessages);
+
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeDeletedNotification(items, eventMessages);
+
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new ContentTypeMovingNotification(moveInfo, eventMessages);
+
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new ContentTypeMovedNotification(moveInfo, eventMessages);
+
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new ContentTypeChangedNotification(changes, eventMessages);
+
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new ContentTypeRefreshedNotification(changes, eventMessages);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
index 1e97e02dca..7549cd849c 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
@@ -2,12 +2,12 @@ using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public abstract class ContentTypeServiceBase : RepositoryService
 {
-    public abstract class ContentTypeServiceBase : RepositoryService
+    protected ContentTypeServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        protected ContentTypeServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory)
-            : base(provider, loggerFactory, eventMessagesFactory)
-        { }
     }
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
index d97021dce0..98a7195fbf 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -5,1080 +5,1113 @@ using Umbraco.Cms.Core.Exceptions;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services.Changes;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeBaseService
+    where TRepository : IContentTypeRepositoryBase
+    where TItem : class, IContentTypeComposition
 {
-    public abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeBaseService
-        where TRepository : IContentTypeRepositoryBase
-        where TItem : class, IContentTypeComposition
+    private readonly IAuditRepository _auditRepository;
+    private readonly IEntityContainerRepository _containerRepository;
+    private readonly IEntityRepository _entityRepository;
+    private readonly IEventAggregator _eventAggregator;
+
+    protected ContentTypeServiceBase(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        TRepository repository,
+        IAuditRepository auditRepository,
+        IEntityContainerRepository containerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IAuditRepository _auditRepository;
-        private readonly IEntityContainerRepository _containerRepository;
-        private readonly IEntityRepository _entityRepository;
-        private readonly IEventAggregator _eventAggregator;
+        Repository = repository;
+        _auditRepository = auditRepository;
+        _containerRepository = containerRepository;
+        _entityRepository = entityRepository;
+        _eventAggregator = eventAggregator;
+    }
 
-        protected ContentTypeServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            TRepository repository, IAuditRepository auditRepository, IEntityContainerRepository containerRepository, IEntityRepository entityRepository,
-            IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    protected TRepository Repository { get; }
+    protected abstract int[] WriteLockIds { get; }
+    protected abstract int[] ReadLockIds { get; }
+
+    #region Notifications
+
+    protected abstract SavingNotification GetSavingNotification(TItem item, EventMessages eventMessages);
+    protected abstract SavingNotification GetSavingNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract SavedNotification GetSavedNotification(TItem item, EventMessages eventMessages);
+    protected abstract SavedNotification GetSavedNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract DeletingNotification GetDeletingNotification(TItem item, EventMessages eventMessages);
+    protected abstract DeletingNotification GetDeletingNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract DeletedNotification GetDeletedNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract MovingNotification GetMovingNotification(MoveEventInfo moveInfo, EventMessages eventMessages);
+
+    protected abstract MovedNotification GetMovedNotification(IEnumerable> moveInfo, EventMessages eventMessages);
+
+    protected abstract ContentTypeChangeNotification GetContentTypeChangedNotification(IEnumerable> changes, EventMessages eventMessages);
+
+    // This notification is identical to GetTypeChangeNotification, however it needs to be a different notification type because it's published within the transaction
+    /// The purpose of this notification being published within the transaction is so that listeners can perform database
+    /// operations from within the same transaction and guarantee data consistency so that if anything goes wrong
+    /// the entire transaction can be rolled back. This is used by Nucache.
+    protected abstract ContentTypeRefreshNotification GetContentTypeRefreshedNotification(IEnumerable> changes, EventMessages eventMessages);
+
+    #endregion
+
+    #region Validation
+
+    public Attempt ValidateComposition(TItem? compo)
+    {
+        try
         {
-            Repository = repository;
-            _auditRepository = auditRepository;
-            _containerRepository = containerRepository;
-            _entityRepository = entityRepository;
-            _eventAggregator = eventAggregator;
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            {
+                scope.ReadLock(ReadLockIds);
+                ValidateLocked(compo!);
+            }
+
+            return Attempt.Succeed();
+        }
+        catch (InvalidCompositionException ex)
+        {
+            return Attempt.Fail(ex.PropertyTypeAliases, ex);
+        }
+    }
+
+    protected void ValidateLocked(TItem compositionContentType)
+    {
+        // performs business-level validation of the composition
+        // should ensure that it is absolutely safe to save the composition
+
+        // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors)
+        // but that cannot be used (conflict with descendants)
+
+        IContentTypeComposition[] allContentTypes = Repository.GetMany(new int[0]).Cast().ToArray();
+
+        IEnumerable compositionAliases = compositionContentType.CompositionAliases();
+        IEnumerable compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y)));
+        var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias).ToArray();
+        var propertyGroupAliases = compositionContentType.PropertyGroups.ToDictionary(x => x.Alias, x => x.Type, StringComparer.InvariantCultureIgnoreCase);
+        IEnumerable indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id));
+        var comparer = new DelegateEqualityComparer((x, y) => x?.Id == y?.Id, x => x.Id);
+        var dependencies = new HashSet(compositions, comparer);
+
+        var stack = new Stack();
+        foreach (IContentTypeComposition indirectReference in indirectReferences)
+        {
+            stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively
         }
 
-        protected TRepository Repository { get; }
-        protected abstract int[] WriteLockIds { get; }
-        protected abstract int[] ReadLockIds { get; }
-
-        #region Notifications
-
-        protected abstract SavingNotification GetSavingNotification(TItem item, EventMessages eventMessages);
-        protected abstract SavingNotification GetSavingNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract SavedNotification GetSavedNotification(TItem item, EventMessages eventMessages);
-        protected abstract SavedNotification GetSavedNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract DeletingNotification GetDeletingNotification(TItem item, EventMessages eventMessages);
-        protected abstract DeletingNotification GetDeletingNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract DeletedNotification GetDeletedNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract MovingNotification GetMovingNotification(MoveEventInfo moveInfo, EventMessages eventMessages);
-
-        protected abstract MovedNotification GetMovedNotification(IEnumerable> moveInfo, EventMessages eventMessages);
-
-        protected abstract ContentTypeChangeNotification GetContentTypeChangedNotification(IEnumerable> changes, EventMessages eventMessages);
-
-        // This notification is identical to GetTypeChangeNotification, however it needs to be a different notification type because it's published within the transaction
-        /// The purpose of this notification being published within the transaction is so that listeners can perform database
-        /// operations from within the same transaction and guarantee data consistency so that if anything goes wrong
-        /// the entire transaction can be rolled back. This is used by Nucache.
-        protected abstract ContentTypeRefreshNotification GetContentTypeRefreshedNotification(IEnumerable> changes, EventMessages eventMessages);
-
-        #endregion
-
-        #region Validation
-
-        public Attempt ValidateComposition(TItem? compo)
+        while (stack.Count > 0)
         {
-            try
+            IContentTypeComposition indirectReference = stack.Pop();
+            dependencies.Add(indirectReference);
+
+            // get all compositions for the current indirect reference
+            IEnumerable directReferences = indirectReference.ContentTypeComposition;
+            foreach (IContentTypeComposition directReference in directReferences)
             {
-                using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+                if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias))
                 {
-                    scope.ReadLock(ReadLockIds);
-                    ValidateLocked(compo!);
-                }
-                return Attempt.Succeed();
-            }
-            catch (InvalidCompositionException ex)
-            {
-                return Attempt.Fail(ex.PropertyTypeAliases, ex);
-            }
-        }
-
-        protected void ValidateLocked(TItem compositionContentType)
-        {
-            // performs business-level validation of the composition
-            // should ensure that it is absolutely safe to save the composition
-
-            // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors)
-            // but that cannot be used (conflict with descendants)
-
-            var allContentTypes = Repository.GetMany(new int[0]).Cast().ToArray();
-
-            var compositionAliases = compositionContentType.CompositionAliases();
-            var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y)));
-            var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias).ToArray();
-            var propertyGroupAliases = compositionContentType.PropertyGroups.ToDictionary(x => x.Alias, x => x.Type, StringComparer.InvariantCultureIgnoreCase);
-            var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id));
-            var comparer = new DelegateEqualityComparer((x, y) => x?.Id == y?.Id, x => x.Id);
-            var dependencies = new HashSet(compositions, comparer);
-
-            var stack = new Stack();
-            foreach (var indirectReference in indirectReferences)
-                stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively
-
-            while (stack.Count > 0)
-            {
-                var indirectReference = stack.Pop();
-                dependencies.Add(indirectReference);
-
-                // get all compositions for the current indirect reference
-                var directReferences = indirectReference.ContentTypeComposition;
-                foreach (var directReference in directReferences)
-                {
-                    if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias))
-                        continue;
-
-                    dependencies.Add(directReference);
-
-                    // a direct reference has compositions of its own - these also need to be taken into account
-                    var directReferenceGraph = directReference.CompositionAliases();
-                    foreach (var c in allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))))
-                        dependencies.Add(c);
+                    continue;
                 }
 
-                // recursive lookup of indirect references
-                foreach (var c in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)))
-                    stack.Push(c);
+                dependencies.Add(directReference);
+
+                // a direct reference has compositions of its own - these also need to be taken into account
+                IEnumerable directReferenceGraph = directReference.CompositionAliases();
+                foreach (IContentTypeComposition c in allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))))
+                {
+                    dependencies.Add(c);
+                }
             }
 
-            var duplicatePropertyTypeAliases = new List();
-            var invalidPropertyGroupAliases = new List();
-
-            foreach (var dependency in dependencies)
+            // recursive lookup of indirect references
+            foreach (IContentTypeComposition c in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)))
             {
-                if (dependency.Id == compositionContentType.Id)
-                    continue;
-
-                var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
-                if (contentTypeDependency == null)
-                    continue;
-
-                duplicatePropertyTypeAliases.AddRange(contentTypeDependency.PropertyTypes.Select(x => x.Alias).Intersect(propertyTypeAliases, StringComparer.InvariantCultureIgnoreCase));
-                invalidPropertyGroupAliases.AddRange(contentTypeDependency.PropertyGroups.Where(x => propertyGroupAliases.TryGetValue(x.Alias, out var type) && type != x.Type).Select(x => x.Alias));
-            }
-
-            if (duplicatePropertyTypeAliases.Count > 0 || invalidPropertyGroupAliases.Count > 0)
-
-            {
-                throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases.Distinct().ToArray(), invalidPropertyGroupAliases.Distinct().ToArray());
+                stack.Push(c);
             }
         }
 
-        #endregion
+        var duplicatePropertyTypeAliases = new List();
+        var invalidPropertyGroupAliases = new List();
 
-        #region Composition
-
-        internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes)
+        foreach (IContentTypeComposition dependency in dependencies)
         {
-            // find all content types impacted by the changes,
-            // - content type alias changed
-            // - content type property removed, or alias changed
-            // - content type composition removed (not testing if composition had properties...)
-            // - content type variation changed
-            // - property type variation changed
-            //
-            // because these are the changes that would impact the raw content data
-
-            // note
-            // this is meant to run *after* uow.Commit() so must use WasPropertyDirty() everywhere
-            // instead of IsPropertyDirty() since dirty properties have been reset already
-
-            var changes = new List>();
-
-            foreach (var contentType in contentTypes)
+            if (dependency.Id == compositionContentType.Id)
             {
-                var dirty = (IRememberBeingDirty)contentType;
+                continue;
+            }
 
-                // skip new content types
+            IContentTypeComposition? contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
+            if (contentTypeDependency == null)
+            {
+                continue;
+            }
+
+            duplicatePropertyTypeAliases.AddRange(contentTypeDependency.PropertyTypes.Select(x => x.Alias).Intersect(propertyTypeAliases, StringComparer.InvariantCultureIgnoreCase));
+            invalidPropertyGroupAliases.AddRange(contentTypeDependency.PropertyGroups.Where(x => propertyGroupAliases.TryGetValue(x.Alias, out PropertyGroupType type) && type != x.Type).Select(x => x.Alias));
+        }
+
+        if (duplicatePropertyTypeAliases.Count > 0 || invalidPropertyGroupAliases.Count > 0)
+
+        {
+            throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases.Distinct().ToArray(), invalidPropertyGroupAliases.Distinct().ToArray());
+        }
+    }
+
+    #endregion
+
+    #region Composition
+
+    internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes)
+    {
+        // find all content types impacted by the changes,
+        // - content type alias changed
+        // - content type property removed, or alias changed
+        // - content type composition removed (not testing if composition had properties...)
+        // - content type variation changed
+        // - property type variation changed
+        //
+        // because these are the changes that would impact the raw content data
+
+        // note
+        // this is meant to run *after* uow.Commit() so must use WasPropertyDirty() everywhere
+        // instead of IsPropertyDirty() since dirty properties have been reset already
+
+        var changes = new List>();
+
+        foreach (TItem contentType in contentTypes)
+        {
+            var dirty = (IRememberBeingDirty)contentType;
+
+            // skip new content types
+            // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly
+            var isNewContentType = dirty.WasPropertyDirty("Id");
+            if (isNewContentType)
+            {
+                AddChange(changes, contentType, ContentTypeChangeTypes.Create);
+                continue;
+            }
+
+            // alias change?
+            var hasAliasChanged = dirty.WasPropertyDirty("Alias");
+
+            // existing property alias change?
+            var hasAnyPropertyChangedAlias = contentType.PropertyTypes.Any(propertyType =>
+            {
+                // skip new properties
                 // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly
-                var isNewContentType = dirty.WasPropertyDirty("Id");
-                if (isNewContentType)
+                var isNewProperty = propertyType.WasPropertyDirty("Id");
+                if (isNewProperty)
                 {
-                    AddChange(changes, contentType, ContentTypeChangeTypes.Create);
-                    continue;
+                    return false;
                 }
 
                 // alias change?
-                var hasAliasChanged = dirty.WasPropertyDirty("Alias");
+                return propertyType.WasPropertyDirty("Alias");
+            });
 
-                // existing property alias change?
-                var hasAnyPropertyChangedAlias = contentType.PropertyTypes.Any(propertyType =>
+            // removed properties?
+            var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
+
+            // removed compositions?
+            var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
+
+            // variation changed?
+            var hasContentTypeVariationChanged = dirty.WasPropertyDirty("Variations");
+
+            // property variation change?
+            var hasAnyPropertyVariationChanged = contentType.WasPropertyTypeVariationChanged();
+
+            // main impact on properties?
+            var hasPropertyMainImpact = hasContentTypeVariationChanged || hasAnyPropertyVariationChanged
+                                                                       || hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias;
+
+            if (hasAliasChanged || hasPropertyMainImpact)
+            {
+                // add that one, as a main change
+                AddChange(changes, contentType, ContentTypeChangeTypes.RefreshMain);
+
+                if (hasPropertyMainImpact)
                 {
-                    // skip new properties
-                    // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly
-                    var isNewProperty = propertyType.WasPropertyDirty("Id");
-                    if (isNewProperty) return false;
-
-                    // alias change?
-                    return propertyType.WasPropertyDirty("Alias");
-                });
-
-                // removed properties?
-                var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
-
-                // removed compositions?
-                var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
-
-                // variation changed?
-                var hasContentTypeVariationChanged = dirty.WasPropertyDirty("Variations");
-
-                // property variation change?
-                var hasAnyPropertyVariationChanged = contentType.WasPropertyTypeVariationChanged();
-
-                // main impact on properties?
-                var hasPropertyMainImpact = hasContentTypeVariationChanged || hasAnyPropertyVariationChanged
-                    || hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias;
-
-                if (hasAliasChanged || hasPropertyMainImpact)
-                {
-                    // add that one, as a main change
-                    AddChange(changes, contentType, ContentTypeChangeTypes.RefreshMain);
-
-                    if (hasPropertyMainImpact)
-                        foreach (var c in GetComposedOf(contentType.Id))
-                            AddChange(changes, c, ContentTypeChangeTypes.RefreshMain);
-                }
-                else
-                {
-                    // add that one, as an other change
-                    AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther);
-                }
-            }
-
-            return changes;
-        }
-
-        // ensures changes contains no duplicates
-        private static void AddChange(ICollection> changes, TItem contentType, ContentTypeChangeTypes changeTypes)
-        {
-            var change = changes.FirstOrDefault(x => x.Item == contentType);
-            if (change == null)
-            {
-                changes.Add(new ContentTypeChange(contentType, changeTypes));
-                return;
-            }
-            change.ChangeTypes |= changeTypes;
-        }
-
-        #endregion
-
-        #region Get, Has, Is, Count
-
-        IContentTypeComposition? IContentTypeBaseService.Get(int id)
-        {
-            return Get(id);
-        }
-
-        public TItem? Get(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(id);
-            }
-        }
-
-        public TItem? Get(string alias)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(alias);
-            }
-        }
-
-        public TItem? Get(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(id);
-            }
-        }
-
-        public IEnumerable GetAll(params int[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.GetMany(ids);
-            }
-        }
-
-        public IEnumerable GetAll(IEnumerable? ids)
-        {
-            if (ids is null)
-            {
-                return Enumerable.Empty();
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.GetMany(ids.ToArray());
-            }
-        }
-
-        public IEnumerable GetChildren(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var query = Query().Where(x => x.ParentId == id);
-                return Repository.Get(query);
-            }
-        }
-
-        public IEnumerable GetChildren(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var found = Get(id);
-                if (found == null) return Enumerable.Empty();
-                var query = Query().Where(x => x.ParentId == found.Id);
-                return Repository.Get(query);
-            }
-        }
-
-        public bool HasChildren(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var query = Query().Where(x => x.ParentId == id);
-                var count = Repository.Count(query);
-                return count > 0;
-            }
-        }
-
-        public bool HasChildren(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var found = Get(id);
-                if (found == null) return false;
-                var query = Query().Where(x => x.ParentId == found.Id);
-                var count = Repository.Count(query);
-                return count > 0;
-            }
-        }
-
-        /// 
-        /// Given the path of a content item, this will return true if the content item exists underneath a list view content item
-        /// 
-        /// 
-        /// 
-        public bool HasContainerInPath(string contentPath)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // can use same repo for both content and media
-                return Repository.HasContainerInPath(contentPath);
-            }
-        }
-
-        public bool HasContainerInPath(params int[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // can use same repo for both content and media
-                return Repository.HasContainerInPath(ids);
-            }
-        }
-
-        public IEnumerable GetDescendants(int id, bool andSelf)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-
-                var descendants = new List();
-                if (andSelf)
-                {
-                    var self = Repository.Get(id);
-                    if (self is not null)
+                    foreach (TItem c in GetComposedOf(contentType.Id))
                     {
-                        descendants.Add(self);
+                        AddChange(changes, c, ContentTypeChangeTypes.RefreshMain);
                     }
                 }
-                var ids = new Stack();
-                ids.Push(id);
-
-                while (ids.Count > 0)
-                {
-                    var i = ids.Pop();
-                    var query = Query().Where(x => x.ParentId == i);
-                    var result = Repository.Get(query).ToArray();
-
-                    if (result is not null)
-                    {
-                        foreach (var c in result)
-                        {
-                            descendants.Add(c);
-                            ids.Push(c.Id);
-                        }
-                    }
-                }
-
-                return descendants.ToArray();
-            }
-        }
-
-        public IEnumerable GetComposedOf(int id, IEnumerable all)
-        {
-            return all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id));
-
-        }
-
-        public IEnumerable GetComposedOf(int id)
-        {
-            // GetAll is cheap, repository has a full dataset cache policy
-            // TODO: still, because it uses the cache, race conditions!
-            var allContentTypes = GetAll(Array.Empty());
-            return GetComposedOf(id, allContentTypes);
-        }
-
-        public int Count()
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Count(Query());
-            }
-        }
-
-        public bool HasContentNodes(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.HasContentNodes(id);
-            }
-        }
-
-        #endregion
-
-        #region Save
-
-        public void Save(TItem? item, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            if (item is null)
-            {
-                return;
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                SavingNotification savingNotification = GetSavingNotification(item, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(item.Name))
-                {
-                    throw new ArgumentException("Cannot save item with empty name.");
-                }
-
-                if (item.Name != null && item.Name.Length > 255)
-                {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // validate the DAG transform, within the lock
-                ValidateLocked(item); // throws if invalid
-
-                item.CreatorId = userId;
-                if (item.Description == string.Empty)
-                {
-                    item.Description = null;
-                }
-
-                Repository.Save(item); // also updates content/media/member items
-
-                // figure out impacted content types
-                ContentTypeChange[] changes = ComposeContentTypeChanges(item).ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                SavedNotification savedNotification = GetSavedNotification(item, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
-
-                Audit(AuditType.Save, userId, item.Id);
-                scope.Complete();
-            }
-        }
-
-        public void Save(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            TItem[] itemsA = items.ToArray();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                SavingNotification savingNotification = GetSavingNotification(itemsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // all-or-nothing, validate them all first
-                foreach (TItem contentType in itemsA)
-                {
-                    ValidateLocked(contentType); // throws if invalid
-                }
-                foreach (TItem contentType in itemsA)
-                {
-                    contentType.CreatorId = userId;
-                    if (contentType.Description == string.Empty)
-                    {
-                        contentType.Description = null;
-                    }
-
-                    Repository.Save(contentType);
-                }
-
-                // figure out impacted content types
-                ContentTypeChange[] changes = ComposeContentTypeChanges(itemsA).ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages)); ;
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                SavedNotification savedNotification = GetSavedNotification(itemsA, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
-
-                Audit(AuditType.Save, userId, -1);
-                scope.Complete();
-            }
-        }
-
-        #endregion
-
-        #region Delete
-
-        public void Delete(TItem item, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                DeletingNotification deletingNotification = GetDeletingNotification(item, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // all descendants are going to be deleted
-                TItem[] descendantsAndSelf = GetDescendants(item.Id, true)
-                    .ToArray();
-                TItem[] deleted = descendantsAndSelf;
-
-                // all impacted (through composition) probably lose some properties
-                // don't try to be too clever here, just report them all
-                // do this before anything is deleted
-                TItem[] changed = descendantsAndSelf.SelectMany(xx => GetComposedOf(xx.Id))
-                    .Distinct()
-                    .Except(descendantsAndSelf)
-                    .ToArray();
-
-                // delete content
-                DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id));
-
-                // Next find all other document types that have a reference to this content type
-                IEnumerable referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes?.Any(p=>p.Id.Value==item.Id) ?? false);
-                foreach (TItem reference in referenceToAllowedContentTypes)
-                {
-                    reference.AllowedContentTypes = reference.AllowedContentTypes?.Where(p => p.Id.Value != item.Id);
-                    var changedRef = new List>() { new ContentTypeChange(reference, ContentTypeChangeTypes.RefreshMain) };
-                    // Fire change event
-                    scope.Notifications.Publish(GetContentTypeChangedNotification(changedRef, eventMessages));
-                }
-
-                // finally delete the content type
-                // - recursively deletes all descendants
-                // - deletes all associated property data
-                //  (contents of any descendant type have been deleted but
-                //   contents of any composed (impacted) type remain but
-                //   need to have their property data cleared)
-                Repository.Delete(item);
-
-                ContentTypeChange[] changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
-                    .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
-                    .ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
-
-                Audit(AuditType.Delete, userId, item.Id);
-                scope.Complete();
-            }
-        }
-
-        public void Delete(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            TItem[] itemsA = items.ToArray();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                DeletingNotification deletingNotification = GetDeletingNotification(itemsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // all descendants are going to be deleted
-                TItem[] allDescendantsAndSelf = itemsA.SelectMany(xx => GetDescendants(xx.Id, true)).DistinctBy(x => x.Id).ToArray();
-                TItem[] deleted = allDescendantsAndSelf;
-
-                // all impacted (through composition) probably lose some properties
-                // don't try to be too clever here, just report them all
-                // do this before anything is deleted
-                TItem[] changed = allDescendantsAndSelf.SelectMany(x => GetComposedOf(x.Id))
-                    .Distinct()
-                    .Except(allDescendantsAndSelf)
-                    .ToArray();
-
-                // delete content
-                DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id));
-
-                // finally delete the content types
-                // (see notes in overload)
-                foreach (TItem item in itemsA)
-                {
-                    Repository.Delete(item);
-                }
-
-                ContentTypeChange[] changes = allDescendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
-                    .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
-                    .ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
-
-                Audit(AuditType.Delete, userId, -1);
-                scope.Complete();
-            }
-        }
-
-        protected abstract void DeleteItemsOfTypes(IEnumerable typeIds);
-
-        #endregion
-
-        #region Copy
-
-        public TItem Copy(TItem original, string alias, string name, int parentId = -1)
-        {
-            TItem? parent = null;
-            if (parentId > 0)
-            {
-                parent = Get(parentId);
-                if (parent == null)
-                {
-                    throw new InvalidOperationException("Could not find parent with id " + parentId);
-                }
-            }
-            return Copy(original, alias, name, parent);
-        }
-
-        public TItem Copy(TItem original, string alias, string name, TItem? parent)
-        {
-            if (original == null) throw new ArgumentNullException(nameof(original));
-            if (alias == null) throw new ArgumentNullException(nameof(alias));
-            if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias));
-            if (parent != null && parent.HasIdentity == false) throw new InvalidOperationException("Parent must have an identity.");
-
-            // this is illegal
-            //var originalb = (ContentTypeCompositionBase)original;
-            // but we *know* it has to be a ContentTypeCompositionBase anyways
-            var originalb = (ContentTypeCompositionBase) (object) original;
-            var clone = (TItem) (object) originalb.DeepCloneWithResetIdentities(alias);
-
-            clone.Name = name;
-
-            //remove all composition that is not it's current alias
-            var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList();
-            foreach (var a in compositionAliases)
-            {
-                clone.RemoveContentType(a);
-            }
-
-            //if a parent is specified set it's composition and parent
-            if (parent != null)
-            {
-                //add a new parent composition
-                clone.AddContentType(parent);
-                clone.ParentId = parent.Id;
             }
             else
             {
-                //set to root
-                clone.ParentId = -1;
+                // add that one, as an other change
+                AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther);
             }
-
-            Save(clone);
-            return clone;
         }
 
-        public Attempt?> Copy(TItem copying, int containerId)
+        return changes;
+    }
+
+    // ensures changes contains no duplicates
+    private static void AddChange(ICollection> changes, TItem contentType, ContentTypeChangeTypes changeTypes)
+    {
+        ContentTypeChange? change = changes.FirstOrDefault(x => x.Item == contentType);
+        if (change == null)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-
-            TItem copy;
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(WriteLockIds);
-
-                try
-                {
-                    if (containerId > 0)
-                    {
-                        var container = _containerRepository?.Get(containerId);
-                        if (container == null)
-                            throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
-                    }
-                    var alias = Repository.GetUniqueAlias(copying.Alias);
-
-                    // this is illegal
-                    //var copyingb = (ContentTypeCompositionBase) copying;
-                    // but we *know* it has to be a ContentTypeCompositionBase anyways
-                    var copyingb = (ContentTypeCompositionBase) (object)copying;
-                    copy = (TItem) (object) copyingb.DeepCloneWithResetIdentities(alias);
-
-                    copy.Name = copy.Name + " (copy)"; // might not be unique
-
-                    // if it has a parent, and the parent is a content type, unplug composition
-                    // all other compositions remain in place in the copied content type
-                    if (copy.ParentId > 0)
-                    {
-                        var parent = Repository.Get(copy.ParentId);
-                        if (parent != null)
-                            copy.RemoveContentType(parent.Alias);
-                    }
-
-                    copy.ParentId = containerId;
-                    Repository.Save(copy);
-                    scope.Complete();
-                }
-                catch (DataOperationException ex)
-                {
-                    return OperationResult.Attempt.Fail(ex.Operation, evtMsgs); // causes rollback
-                }
-            }
-
-            return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs, copy);
+            changes.Add(new ContentTypeChange(contentType, changeTypes));
+            return;
         }
 
-        #endregion
+        change.ChangeTypes |= changeTypes;
+    }
 
-        #region Move
+    #endregion
 
-        public Attempt?> Move(TItem moving, int containerId)
+    #region Get, Has, Is, Count
+
+    IContentTypeComposition? IContentTypeBaseService.Get(int id)
+    {
+        return Get(id);
+    }
+
+    public TItem? Get(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(id);
+    }
+
+    public TItem? Get(string alias)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(alias);
+    }
+
+    public TItem? Get(Guid id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(id);
+    }
+
+    public IEnumerable GetAll(params int[] ids)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.GetMany(ids);
+    }
+
+    public IEnumerable GetAll(IEnumerable? ids)
+    {
+        if (ids is null)
+        {
+            return Enumerable.Empty();
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+
+        {
+            scope.ReadLock(ReadLockIds);
+            return Repository.GetMany(ids.ToArray());
+        }
+    }
+
+    public IEnumerable GetChildren(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        IQuery query = Query().Where(x => x.ParentId == id);
+        return Repository.Get(query);
+    }
+
+    public IEnumerable GetChildren(Guid id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        TItem? found = Get(id);
+        if (found == null)
+        {
+            return Enumerable.Empty();
+        }
+
+        IQuery query = Query().Where(x => x.ParentId == found.Id);
+        return Repository.Get(query);
+    }
+
+    public bool HasChildren(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        IQuery query = Query().Where(x => x.ParentId == id);
+        var count = Repository.Count(query);
+        return count > 0;
+    }
+
+    public bool HasChildren(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(ReadLockIds);
+            TItem? found = Get(id);
+            if (found == null)
+            {
+                return false;
+            }
+
+            IQuery query = Query().Where(x => x.ParentId == found.Id);
+            var count = Repository.Count(query);
+            return count > 0;
+        }
+    }
+
+    /// 
+    /// Given the path of a content item, this will return true if the content item exists underneath a list view content item
+    /// 
+    /// 
+    /// 
+    public bool HasContainerInPath(string contentPath)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        // can use same repo for both content and media
+        return Repository.HasContainerInPath(contentPath);
+    }
+
+    public bool HasContainerInPath(params int[] ids)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        // can use same repo for both content and media
+        return Repository.HasContainerInPath(ids);
+    }
+
+    public IEnumerable GetDescendants(int id, bool andSelf)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+
+        var descendants = new List();
+        if (andSelf)
+        {
+            TItem? self = Repository.Get(id);
+            if (self is not null)
+            {
+                descendants.Add(self);
+            }
+        }
+
+        var ids = new Stack();
+        ids.Push(id);
+
+        while (ids.Count > 0)
+        {
+            var i = ids.Pop();
+            IQuery query = Query().Where(x => x.ParentId == i);
+            TItem[]? result = Repository.Get(query).ToArray();
+
+            if (result is not null)
+            {
+                foreach (TItem c in result)
+                {
+                    descendants.Add(c);
+                    ids.Push(c.Id);
+                }
+            }
+        }
+
+        return descendants.ToArray();
+    }
+
+    public IEnumerable GetComposedOf(int id, IEnumerable all) =>
+        all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id));
+
+    public IEnumerable GetComposedOf(int id)
+    {
+        // GetAll is cheap, repository has a full dataset cache policy
+        // TODO: still, because it uses the cache, race conditions!
+        IEnumerable allContentTypes = GetAll(Array.Empty());
+        return GetComposedOf(id, allContentTypes);
+    }
+
+    public int Count()
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Count(Query());
+    }
+
+    public bool HasContentNodes(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.HasContentNodes(id);
+    }
+
+    #endregion
+
+    #region Save
+
+    public void Save(TItem? item, int userId = Constants.Security.SuperUserId)
+    {
+        if (item is null)
+        {
+            return;
+        }
+
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        SavingNotification savingNotification = GetSavingNotification(item, eventMessages);
+        if (scope.Notifications.PublishCancelable(savingNotification))
+        {
+            scope.Complete();
+            return;
+        }
+
+        if (string.IsNullOrWhiteSpace(item.Name))
+        {
+            throw new ArgumentException("Cannot save item with empty name.");
+        }
+
+        if (item.Name != null && item.Name.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
+
+        scope.WriteLock(WriteLockIds);
+
+        // validate the DAG transform, within the lock
+        ValidateLocked(item); // throws if invalid
+
+        item.CreatorId = userId;
+        if (item.Description == string.Empty)
+        {
+            item.Description = null;
+        }
+
+        Repository.Save(item); // also updates content/media/member items
+
+        // figure out impacted content types
+        ContentTypeChange[] changes = ComposeContentTypeChanges(item).ToArray();
+
+        // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+        _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+        scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+        SavedNotification savedNotification = GetSavedNotification(item, eventMessages);
+        savedNotification.WithStateFrom(savingNotification);
+        scope.Notifications.Publish(savedNotification);
+
+        Audit(AuditType.Save, userId, item.Id);
+        scope.Complete();
+    }
+
+    public void Save(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        TItem[] itemsA = items.ToArray();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
-
-            var moveInfo = new List>();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            SavingNotification savingNotification = GetSavingNotification(itemsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var moveEventInfo = new MoveEventInfo(moving, moving.Path, containerId);
-                MovingNotification movingNotification = GetMovingNotification(moveEventInfo, eventMessages);
-                if (scope.Notifications.PublishCancelable(movingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages);
-                }
-
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                try
-                {
-                    EntityContainer? container = null;
-                    if (containerId > 0)
-                    {
-                        container = _containerRepository?.Get(containerId);
-                        if (container == null)
-                            throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
-                    }
-                    moveInfo.AddRange(Repository.Move(moving, container!));
-                    scope.Complete();
-                }
-                catch (DataOperationException ex)
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(ex.Operation, eventMessages);
-                }
-
-                // note: not raising any Changed event here because moving a content type under another container
-                // has no impact on the published content types - would be entirely different if we were to support
-                // moving a content type under another content type.
-                MovedNotification movedNotification = GetMovedNotification(moveInfo, eventMessages);
-                movedNotification.WithStateFrom(movingNotification);
-                scope.Notifications.Publish(movedNotification);
+                scope.Complete();
+                return;
             }
 
-            return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages);
+            scope.WriteLock(WriteLockIds);
+
+            // all-or-nothing, validate them all first
+            foreach (TItem contentType in itemsA)
+            {
+                ValidateLocked(contentType); // throws if invalid
+            }
+            foreach (TItem contentType in itemsA)
+            {
+                contentType.CreatorId = userId;
+                if (contentType.Description == string.Empty)
+                {
+                    contentType.Description = null;
+                }
+
+                Repository.Save(contentType);
+            }
+
+            // figure out impacted content types
+            ContentTypeChange[] changes = ComposeContentTypeChanges(itemsA).ToArray();
+
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+            SavedNotification savedNotification = GetSavedNotification(itemsA, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+
+            Audit(AuditType.Save, userId, -1);
+            scope.Complete();
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Containers
+    #region Delete
 
-        protected abstract Guid ContainedObjectType { get; }
-
-        protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType);
-
-        public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
+    public void Delete(TItem item, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            DeletingNotification deletingNotification = GetDeletingNotification(item, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                try
-                {
-                    var container = new EntityContainer(ContainedObjectType)
-                    {
-                        Name = name,
-                        ParentId = parentId,
-                        CreatorId = userId,
-                        Key = key
-                    };
-
-                    var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
-                    if (scope.Notifications.PublishCancelable(savingNotification))
-                    {
-                        scope.Complete();
-                        return OperationResult.Attempt.Cancel(eventMessages, container);
-                    }
-
-                    _containerRepository?.Save(container);
-                    scope.Complete();
-
-                    var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
-                    savedNotification.WithStateFrom(savingNotification);
-                    scope.Notifications.Publish(savedNotification);
-                    // TODO: Audit trail ?
-
-                    return OperationResult.Attempt.Succeed(eventMessages, container);
-                }
-                catch (Exception ex)
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(OperationResultType.FailedCancelledByEvent, eventMessages, ex);
-                }
+                scope.Complete();
+                return;
             }
-        }
 
-        public Attempt SaveContainer(EntityContainer container, int userId = Cms.Core.Constants.Security.SuperUserId)
+            scope.WriteLock(WriteLockIds);
+
+            // all descendants are going to be deleted
+            TItem[] descendantsAndSelf = GetDescendants(item.Id, true)
+                .ToArray();
+            TItem[] deleted = descendantsAndSelf;
+
+            // all impacted (through composition) probably lose some properties
+            // don't try to be too clever here, just report them all
+            // do this before anything is deleted
+            TItem[] changed = descendantsAndSelf.SelectMany(xx => GetComposedOf(xx.Id))
+                .Distinct()
+                .Except(descendantsAndSelf)
+                .ToArray();
+
+            // delete content
+            DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id));
+
+            // Next find all other document types that have a reference to this content type
+            IEnumerable referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes?.Any(p=>p.Id.Value==item.Id) ?? false);
+            foreach (TItem reference in referenceToAllowedContentTypes)
+            {
+                reference.AllowedContentTypes = reference.AllowedContentTypes?.Where(p => p.Id.Value != item.Id);
+                var changedRef = new List>() { new ContentTypeChange(reference, ContentTypeChangeTypes.RefreshMain) };
+                // Fire change event
+                scope.Notifications.Publish(GetContentTypeChangedNotification(changedRef, eventMessages));
+            }
+
+            // finally delete the content type
+            // - recursively deletes all descendants
+            // - deletes all associated property data
+            //  (contents of any descendant type have been deleted but
+            //   contents of any composed (impacted) type remain but
+            //   need to have their property data cleared)
+            Repository.Delete(item);
+
+            ContentTypeChange[] changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
+                .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
+                .ToArray();
+
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+            DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
+            deletedNotification.WithStateFrom(deletingNotification);
+            scope.Notifications.Publish(deletedNotification);
+
+            Audit(AuditType.Delete, userId, item.Id);
+            scope.Complete();
+        }
+    }
+
+    public void Delete(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        TItem[] itemsA = items.ToArray();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
-
-            Guid containerObjectType = ContainerObjectType;
-            if (container.ContainerObjectType != containerObjectType)
+            DeletingNotification deletingNotification = GetDeletingNotification(itemsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                var ex = new InvalidOperationException("Not a container of the proper type.");
-                return OperationResult.Attempt.Fail(eventMessages, ex);
+                scope.Complete();
+                return;
             }
 
-            if (container.HasIdentity && container.IsPropertyDirty("ParentId"))
+            scope.WriteLock(WriteLockIds);
+
+            // all descendants are going to be deleted
+            TItem[] allDescendantsAndSelf = itemsA.SelectMany(xx => GetDescendants(xx.Id, true)).DistinctBy(x => x.Id).ToArray();
+            TItem[] deleted = allDescendantsAndSelf;
+
+            // all impacted (through composition) probably lose some properties
+            // don't try to be too clever here, just report them all
+            // do this before anything is deleted
+            TItem[] changed = allDescendantsAndSelf.SelectMany(x => GetComposedOf(x.Id))
+                .Distinct()
+                .Except(allDescendantsAndSelf)
+                .ToArray();
+
+            // delete content
+            DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id));
+
+            // finally delete the content types
+            // (see notes in overload)
+            foreach (TItem item in itemsA)
             {
-                var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead.");
-                return OperationResult.Attempt.Fail(eventMessages, ex);
+                Repository.Delete(item);
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            ContentTypeChange[] changes = allDescendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
+                .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
+                .ToArray();
+
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+            DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
+            deletedNotification.WithStateFrom(deletingNotification);
+            scope.Notifications.Publish(deletedNotification);
+
+            Audit(AuditType.Delete, userId, -1);
+            scope.Complete();
+        }
+    }
+
+    protected abstract void DeleteItemsOfTypes(IEnumerable typeIds);
+
+    #endregion
+
+    #region Copy
+
+    public TItem Copy(TItem original, string alias, string name, int parentId = -1)
+    {
+        TItem? parent = null;
+        if (parentId > 0)
+        {
+            parent = Get(parentId);
+            if (parent == null)
             {
-                var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
+                throw new InvalidOperationException("Could not find parent with id " + parentId);
+            }
+        }
+        return Copy(original, alias, name, parent);
+    }
+
+    public TItem Copy(TItem original, string alias, string name, TItem? parent)
+    {
+        if (original == null)
+        {
+            throw new ArgumentNullException(nameof(original));
+        }
+
+        if (alias == null)
+        {
+            throw new ArgumentNullException(nameof(alias));
+        }
+
+        if (string.IsNullOrWhiteSpace(alias))
+        {
+            throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias));
+        }
+
+        if (parent != null && parent.HasIdentity == false)
+        {
+            throw new InvalidOperationException("Parent must have an identity.");
+        }
+
+        // this is illegal
+        //var originalb = (ContentTypeCompositionBase)original;
+        // but we *know* it has to be a ContentTypeCompositionBase anyways
+        var originalb = (ContentTypeCompositionBase) (object) original;
+        var clone = (TItem) (object) originalb.DeepCloneWithResetIdentities(alias);
+
+        clone.Name = name;
+
+        //remove all composition that is not it's current alias
+        var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList();
+        foreach (var a in compositionAliases)
+        {
+            clone.RemoveContentType(a);
+        }
+
+        //if a parent is specified set it's composition and parent
+        if (parent != null)
+        {
+            //add a new parent composition
+            clone.AddContentType(parent);
+            clone.ParentId = parent.Id;
+        }
+        else
+        {
+            //set to root
+            clone.ParentId = -1;
+        }
+
+        Save(clone);
+        return clone;
+    }
+
+    public Attempt?> Copy(TItem copying, int containerId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        TItem copy;
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(WriteLockIds);
+
+            try
+            {
+                if (containerId > 0)
+                {
+                    EntityContainer? container = _containerRepository?.Get(containerId);
+                    if (container == null)
+                    {
+                        throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                    }
+                }
+
+                var alias = Repository.GetUniqueAlias(copying.Alias);
+
+                // this is illegal
+                //var copyingb = (ContentTypeCompositionBase) copying;
+                // but we *know* it has to be a ContentTypeCompositionBase anyways
+                var copyingb = (ContentTypeCompositionBase) (object)copying;
+                copy = (TItem) (object) copyingb.DeepCloneWithResetIdentities(alias);
+
+                copy.Name = copy.Name + " (copy)"; // might not be unique
+
+                // if it has a parent, and the parent is a content type, unplug composition
+                // all other compositions remain in place in the copied content type
+                if (copy.ParentId > 0)
+                {
+                    TItem? parent = Repository.Get(copy.ParentId);
+                    if (parent != null)
+                    {
+                        copy.RemoveContentType(parent.Alias);
+                    }
+                }
+
+                copy.ParentId = containerId;
+
+                SavingNotification savingNotification = GetSavingNotification(copy, eventMessages);
                 if (scope.Notifications.PublishCancelable(savingNotification))
                 {
                     scope.Complete();
-                    return OperationResult.Attempt.Cancel(eventMessages);
+                    return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages, copy);
                 }
 
-                scope.WriteLock(WriteLockIds); // also for containers
+                Repository.Save(copy);
+
+                ContentTypeChange[] changes = ComposeContentTypeChanges(copy).ToArray();
+
+                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+                SavedNotification savedNotification = GetSavedNotification(copy, eventMessages);
+                savedNotification.WithStateFrom(savingNotification);
+                scope.Notifications.Publish(savedNotification);
+
+                scope.Complete();
+            }
+            catch (DataOperationException ex)
+            {
+                return OperationResult.Attempt.Fail(ex.Operation, eventMessages); // causes rollback
+            }
+        }
+
+        return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages, copy);
+    }
+
+    #endregion
+
+    #region Move
+
+    public Attempt?> Move(TItem moving, int containerId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        var moveInfo = new List>();
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var moveEventInfo = new MoveEventInfo(moving, moving.Path, containerId);
+            MovingNotification movingNotification = GetMovingNotification(moveEventInfo, eventMessages);
+            if (scope.Notifications.PublishCancelable(movingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages);
+            }
+
+            scope.WriteLock(WriteLockIds); // also for containers
+
+            try
+            {
+                EntityContainer? container = null;
+                if (containerId > 0)
+                {
+                    container = _containerRepository?.Get(containerId);
+                    if (container == null)
+                    {
+                        throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                    }
+                }
+                moveInfo.AddRange(Repository.Move(moving, container!));
+                scope.Complete();
+            }
+            catch (DataOperationException ex)
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Fail(ex.Operation, eventMessages);
+            }
+
+            // note: not raising any Changed event here because moving a content type under another container
+            // has no impact on the published content types - would be entirely different if we were to support
+            // moving a content type under another content type.
+            MovedNotification movedNotification = GetMovedNotification(moveInfo, eventMessages);
+            movedNotification.WithStateFrom(movingNotification);
+            scope.Notifications.Publish(movedNotification);
+        }
+
+        return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages);
+    }
+
+    #endregion
+
+    #region Containers
+
+    protected abstract Guid ContainedObjectType { get; }
+
+    protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType);
+
+    public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+        scope.WriteLock(WriteLockIds); // also for containers
+
+        try
+        {
+            var container = new EntityContainer(ContainedObjectType)
+            {
+                Name = name,
+                ParentId = parentId,
+                CreatorId = userId,
+                Key = key
+            };
+
+            var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages, container);
+            }
+
+            _containerRepository?.Save(container);
+            scope.Complete();
+
+            var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+            // TODO: Audit trail ?
+
+            return OperationResult.Attempt.Succeed(eventMessages, container);
+        }
+        catch (Exception ex)
+        {
+            scope.Complete();
+            return OperationResult.Attempt.Fail(OperationResultType.FailedCancelledByEvent, eventMessages, ex);
+        }
+    }
+
+    public Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        Guid containerObjectType = ContainerObjectType;
+        if (container.ContainerObjectType != containerObjectType)
+        {
+            var ex = new InvalidOperationException("Not a container of the proper type.");
+            return OperationResult.Attempt.Fail(eventMessages, ex);
+        }
+
+        if (container.HasIdentity && container.IsPropertyDirty("ParentId"))
+        {
+            var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead.");
+            return OperationResult.Attempt.Fail(eventMessages, ex);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages);
+            }
+
+            scope.WriteLock(WriteLockIds); // also for containers
+
+            _containerRepository?.Save(container);
+            scope.Complete();
+
+            var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+        }
+
+        // TODO: Audit trail ?
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
+
+    public EntityContainer? GetContainer(int containerId)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.Get(containerId);
+    }
+
+    public EntityContainer? GetContainer(Guid containerId)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.Get(containerId);
+    }
+
+    public IEnumerable GetContainers(int[] containerIds)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.GetMany(containerIds);
+    }
+
+    public IEnumerable GetContainers(TItem item)
+    {
+        var ancestorIds = item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
+            .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt) ? asInt : int.MinValue)
+            .Where(x => x != int.MinValue && x != item.Id)
+            .ToArray();
+
+        return GetContainers(ancestorIds);
+    }
+
+    public IEnumerable GetContainers(string name, int level)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.Get(name, level);
+    }
+
+    public Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+        scope.WriteLock(WriteLockIds); // also for containers
+
+        EntityContainer? container = _containerRepository?.Get(containerId);
+        if (container == null)
+        {
+            return OperationResult.Attempt.NoOperation(eventMessages);
+        }
+
+        // 'container' here does not know about its children, so we need
+        // to get it again from the entity repository, as a light entity
+        IEntitySlim? entity = _entityRepository.Get(container.Id);
+        if (entity?.HasChildren ?? false)
+        {
+            scope.Complete();
+            return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, eventMessages));
+        }
+
+        var deletingNotification = new EntityContainerDeletingNotification(container, eventMessages);
+        if (scope.Notifications.PublishCancelable(deletingNotification))
+        {
+            scope.Complete();
+            return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages));
+        }
+
+        _containerRepository?.Delete(container);
+        scope.Complete();
+
+        var deletedNotification = new EntityContainerDeletedNotification(container, eventMessages);
+        deletedNotification.WithStateFrom(deletingNotification);
+        scope.Notifications.Publish(deletedNotification);
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+        // TODO: Audit trail ?
+    }
+
+    public Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(WriteLockIds); // also for containers
+
+            try
+            {
+                EntityContainer? container = _containerRepository?.Get(id);
+
+                //throw if null, this will be caught by the catch and a failed returned
+                if (container == null)
+                {
+                    throw new InvalidOperationException("No container found with id " + id);
+                }
+
+                container.Name = name;
+
+                var renamingNotification = new EntityContainerRenamingNotification(container, eventMessages);
+                if (scope.Notifications.PublishCancelable(renamingNotification))
+                {
+                    scope.Complete();
+                    return OperationResult.Attempt.Cancel(eventMessages);
+                }
 
                 _containerRepository?.Save(container);
                 scope.Complete();
 
-                var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
+                var renamedNotification = new EntityContainerRenamedNotification(container, eventMessages);
+                renamedNotification.WithStateFrom(renamingNotification);
+                scope.Notifications.Publish(renamedNotification);
+
+                return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, container);
             }
-
-            // TODO: Audit trail ?
-
-            return OperationResult.Attempt.Succeed(eventMessages);
-        }
-
-        public EntityContainer? GetContainer(int containerId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            catch (Exception ex)
             {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.Get(containerId);
+                return OperationResult.Attempt.Fail(eventMessages, ex);
             }
         }
-
-        public EntityContainer? GetContainer(Guid containerId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.Get(containerId);
-            }
-        }
-
-        public IEnumerable GetContainers(int[] containerIds)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.GetMany(containerIds);
-            }
-        }
-
-        public IEnumerable GetContainers(TItem item)
-        {
-            var ancestorIds = item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt) ? asInt : int.MinValue)
-                .Where(x => x != int.MinValue && x != item.Id)
-                .ToArray();
-
-            return GetContainers(ancestorIds);
-        }
-
-        public IEnumerable GetContainers(string name, int level)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.Get(name, level);
-            }
-        }
-
-        public Attempt DeleteContainer(int containerId, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                EntityContainer? container = _containerRepository?.Get(containerId);
-                if (container == null)
-                {
-                    return OperationResult.Attempt.NoOperation(eventMessages);
-                }
-
-                // 'container' here does not know about its children, so we need
-                // to get it again from the entity repository, as a light entity
-                IEntitySlim? entity = _entityRepository.Get(container.Id);
-                if (entity?.HasChildren ?? false)
-                {
-                    scope.Complete();
-                    return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, eventMessages));
-                }
-
-                var deletingNotification = new EntityContainerDeletingNotification(container, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages));
-                }
-
-                _containerRepository?.Delete(container);
-                scope.Complete();
-
-                var deletedNotification = new EntityContainerDeletedNotification(container, eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
-
-                return OperationResult.Attempt.Succeed(eventMessages);
-                // TODO: Audit trail ?
-            }
-        }
-
-        public Attempt?> RenameContainer(int id, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                try
-                {
-                    EntityContainer? container = _containerRepository?.Get(id);
-
-                    //throw if null, this will be caught by the catch and a failed returned
-                    if (container == null)
-                    {
-                        throw new InvalidOperationException("No container found with id " + id);
-                    }
-
-                    container.Name = name;
-
-                    var renamingNotification = new EntityContainerRenamingNotification(container, eventMessages);
-                    if (scope.Notifications.PublishCancelable(renamingNotification))
-                    {
-                        scope.Complete();
-                        return OperationResult.Attempt.Cancel(eventMessages);
-                    }
-
-                    _containerRepository?.Save(container);
-                    scope.Complete();
-
-                    var renamedNotification = new EntityContainerRenamedNotification(container, eventMessages);
-                    renamedNotification.WithStateFrom(renamingNotification);
-                    scope.Notifications.Publish(renamedNotification);
-
-                    return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, container);
-                }
-                catch (Exception ex)
-                {
-                    return OperationResult.Attempt.Fail(eventMessages, ex);
-                }
-            }
-        }
-
-        #endregion
-
-        #region Audit
-
-        private void Audit(AuditType type, int userId, int objectId)
-        {
-            _auditRepository.Save(new AuditItem(objectId, type, userId,
-                ObjectTypes.GetUmbracoObjectType(ContainedObjectType).GetName()));
-        }
-
-        #endregion
-
-
     }
+
+    #endregion
+
+    #region Audit
+
+    private void Audit(AuditType type, int userId, int objectId)
+    {
+        _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetUmbracoObjectType(ContainedObjectType).GetName()));
+    }
+
+    #endregion
+
+
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
index 6c30b24b67..5ae8da3a12 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
@@ -1,188 +1,207 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
-{
-    public static class ContentTypeServiceExtensions
-    {
-        /// 
-        /// Gets all of the element types (e.g. content types that have been marked as an element type).
-        /// 
-        /// The content type service.
-        /// Returns all the element types.
-        public static IEnumerable GetAllElementTypes(this IContentTypeService contentTypeService)
-        {
-            if (contentTypeService == null)
-            {
-                return Enumerable.Empty();
-            }
+namespace Umbraco.Extensions;
 
-            return contentTypeService.GetAll().Where(x => x.IsElement);
+public static class ContentTypeServiceExtensions
+{
+    /// 
+    ///     Gets all of the element types (e.g. content types that have been marked as an element type).
+    /// 
+    /// The content type service.
+    /// Returns all the element types.
+    public static IEnumerable GetAllElementTypes(this IContentTypeService contentTypeService)
+    {
+        if (contentTypeService == null)
+        {
+            return Enumerable.Empty();
         }
 
-        /// 
-        /// Returns the available composite content types for a given content type
-        /// 
-        /// 
-        /// 
-        /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out
-        /// along with any content types that have matching property types that are included in the filtered content types
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out.
-        /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot
-        /// be looked up via the db, they need to be passed in.
-        /// 
-        /// Whether the composite content types should be applicable for an element type
-        /// 
-        public static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService,
-            IContentTypeComposition? source,
-            IContentTypeComposition[] allContentTypes,
-            string[]? filterContentTypes = null,
-            string[]? filterPropertyTypes = null,
-            bool isElement = false)
+        return contentTypeService.GetAll().Where(x => x.IsElement);
+    }
+
+    /// 
+    ///     Returns the available composite content types for a given content type
+    /// 
+    /// 
+    /// 
+    ///     This is normally an empty list but if additional content type aliases are passed in, any content types containing
+    ///     those aliases will be filtered out
+    ///     along with any content types that have matching property types that are included in the filtered content types
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This is normally an empty list but if additional property type aliases are passed in, any content types that have
+    ///     these aliases will be filtered out.
+    ///     This is required because in the case of creating/modifying a content type because new property types being added to
+    ///     it are not yet persisted so cannot
+    ///     be looked up via the db, they need to be passed in.
+    /// 
+    /// Whether the composite content types should be applicable for an element type
+    /// 
+    public static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(
+        this IContentTypeService ctService,
+        IContentTypeComposition? source,
+        IContentTypeComposition[] allContentTypes,
+        string[]? filterContentTypes = null,
+        string[]? filterPropertyTypes = null,
+        bool isElement = false)
+    {
+        filterContentTypes = filterContentTypes == null
+            ? Array.Empty()
+            : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+
+        filterPropertyTypes = filterPropertyTypes == null
+            ? Array.Empty()
+            : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+
+        // create the full list of property types to use as the filter
+        // this is the combination of all property type aliases found in the content types passed in for the filter
+        // as well as the specific property types passed in for the filter
+        filterPropertyTypes = allContentTypes
+            .Where(c => filterContentTypes.InvariantContains(c.Alias))
+            .SelectMany(c => c.PropertyTypes)
+            .Select(c => c.Alias)
+            .Union(filterPropertyTypes)
+            .ToArray();
+
+        var sourceId = source?.Id ?? 0;
+
+        // find out if any content type uses this content type
+        IContentTypeComposition[] isUsing =
+            allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
+        if (isUsing.Length > 0)
         {
-            filterContentTypes = filterContentTypes == null
-                ? Array.Empty()
-                : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+            // if already in use a composition, do not allow any composited types
+            return new ContentTypeAvailableCompositionsResults();
+        }
 
-            filterPropertyTypes = filterPropertyTypes == null
-                ? Array.Empty()
-                : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+        // if it is not used then composition is possible
+        // hashset guarantees uniqueness on Id
+        var list = new HashSet(new DelegateEqualityComparer(
+            (x, y) => x?.Id == y?.Id,
+            x => x.Id));
 
-            //create the full list of property types to use as the filter
-            //this is the combination of all property type aliases found in the content types passed in for the filter
-            //as well as the specific property types passed in for the filter
-            filterPropertyTypes = allContentTypes
-                    .Where(c => filterContentTypes.InvariantContains(c.Alias))
-                    .SelectMany(c => c.PropertyTypes)
-                    .Select(c => c.Alias)
-                    .Union(filterPropertyTypes)
-                    .ToArray();
+        // usable types are those that are top-level
+        // do not allow element types to be composed by non-element types as this will break the model generation in ModelsBuilder
+        IContentTypeComposition[] usableContentTypes = allContentTypes
+            .Where(x => x.ContentTypeComposition.Any() == false && (isElement == false || x.IsElement)).ToArray();
+        foreach (IContentTypeComposition x in usableContentTypes)
+        {
+            list.Add(x);
+        }
 
-            var sourceId = source?.Id ?? 0;
+        // indirect types are those that we use, directly or indirectly
+        IContentTypeComposition[] indirectContentTypes = GetDirectOrIndirect(source).ToArray();
+        foreach (IContentTypeComposition x in indirectContentTypes)
+        {
+            list.Add(x);
+        }
 
-            // find out if any content type uses this content type
-            var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
-            if (isUsing.Length > 0)
+        // At this point we have a list of content types that 'could' be compositions
+
+        // now we'll filter this list based on the filters requested
+        var filtered = list
+            .Where(x =>
             {
-                //if already in use a composition, do not allow any composited types
-                return new ContentTypeAvailableCompositionsResults();
-            }
+                // need to filter any content types that are included in this list
+                return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false;
+            })
+            .Where(x =>
+            {
+                // need to filter any content types that have matching property aliases that are included in this list
+                // ensure that we don't return if there's any overlapping property aliases from the filtered ones specified
+                return filterPropertyTypes.Intersect(
+                    x.PropertyTypes.Select(p => p.Alias),
+                    StringComparer.InvariantCultureIgnoreCase).Any() == false;
+            })
+            .OrderBy(x => x.Name)
+            .ToList();
 
-            // if it is not used then composition is possible
-            // hashset guarantees uniqueness on Id
-            var list = new HashSet(new DelegateEqualityComparer(
-                (x, y) => x?.Id == y?.Id,
-                x => x.Id));
+        // get ancestor ids - we will filter all ancestors
+        IContentTypeComposition[] ancestors = GetAncestors(source, allContentTypes);
+        var ancestorIds = ancestors.Select(x => x.Id).ToArray();
 
-            // usable types are those that are top-level
-            // do not allow element types to be composed by non-element types as this will break the model generation in ModelsBuilder
-            var usableContentTypes = allContentTypes
-                .Where(x => x.ContentTypeComposition.Any() == false && (isElement == false || x.IsElement)).ToArray();
-            foreach (var x in usableContentTypes)
-                list.Add(x);
+        // now we can create our result based on what is still available and the ancestors
+        var result = list
 
-            // indirect types are those that we use, directly or indirectly
-            var indirectContentTypes = GetDirectOrIndirect(source).ToArray();
-            foreach (var x in indirectContentTypes)
-                list.Add(x);
-
-            //At this point we have a list of content types that 'could' be compositions
-
-            //now we'll filter this list based on the filters requested
-            var filtered = list
-                .Where(x =>
-                {
-                    //need to filter any content types that are included in this list
-                    return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false;
-                })
-                .Where(x =>
-                {
-                    //need to filter any content types that have matching property aliases that are included in this list
-                    //ensure that we don't return if there's any overlapping property aliases from the filtered ones specified
-                    return filterPropertyTypes.Intersect(
-                        x.PropertyTypes.Select(p => p.Alias),
-                        StringComparer.InvariantCultureIgnoreCase).Any() == false;
-                })
-                .OrderBy(x => x.Name)
-                .ToList();
-
-            //get ancestor ids - we will filter all ancestors
-            var ancestors = GetAncestors(source, allContentTypes);
-            var ancestorIds = ancestors.Select(x => x.Id).ToArray();
-
-            //now we can create our result based on what is still available and the ancestors
-            var result = list
-                //not itself
-                .Where(x => x.Id != sourceId)
-                .OrderBy(x => x.Name)
-                .Select(composition => filtered.Contains(composition)
+            // not itself
+            .Where(x => x.Id != sourceId)
+            .OrderBy(x => x.Name)
+            .Select(composition => filtered.Contains(composition)
                 ? new ContentTypeAvailableCompositionsResult(composition, ancestorIds.Contains(composition.Id) == false)
                 : new ContentTypeAvailableCompositionsResult(composition, false)).ToList();
 
-            return new ContentTypeAvailableCompositionsResults(ancestors, result);
-        }
+        return new ContentTypeAvailableCompositionsResults(ancestors, result);
+    }
 
-
-        private static IContentTypeComposition[] GetAncestors(IContentTypeComposition? ctype, IContentTypeComposition[] allContentTypes)
+    private static IContentTypeComposition[] GetAncestors(
+        IContentTypeComposition? ctype,
+        IContentTypeComposition[] allContentTypes)
+    {
+        if (ctype == null)
         {
-            if (ctype == null) return new IContentTypeComposition[] {};
-            var ancestors = new List();
-            var parentId = ctype.ParentId;
-            while (parentId > 0)
-            {
-                var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId);
-                if (parent != null)
-                {
-                    ancestors.Add(parent);
-                    parentId = parent.ParentId;
-                }
-                else
-                {
-                    parentId = -1;
-                }
-            }
-            return ancestors.ToArray();
+            return new IContentTypeComposition[] { };
         }
 
-        /// 
-        /// Get those that we use directly
-        /// 
-        /// 
-        /// 
-        private static IEnumerable GetDirectOrIndirect(IContentTypeComposition? ctype)
+        var ancestors = new List();
+        var parentId = ctype.ParentId;
+        while (parentId > 0)
         {
-            if (ctype == null) return Enumerable.Empty();
-
-            // hashset guarantees uniqueness on Id
-            var all = new HashSet(new DelegateEqualityComparer(
-                (x, y) => x?.Id == y?.Id,
-                x => x.Id));
-
-            var stack = new Stack();
-
-            foreach (var x in ctype.ContentTypeComposition)
-                stack.Push(x);
-
-            while (stack.Count > 0)
+            IContentTypeComposition? parent = allContentTypes.FirstOrDefault(x => x.Id == parentId);
+            if (parent != null)
             {
-                var x = stack.Pop();
-                all.Add(x);
-                foreach (var y in x.ContentTypeComposition)
-                    stack.Push(y);
+                ancestors.Add(parent);
+                parentId = parent.ParentId;
+            }
+            else
+            {
+                parentId = -1;
             }
-
-            return all;
         }
+
+        return ancestors.ToArray();
+    }
+
+    /// 
+    ///     Get those that we use directly
+    /// 
+    /// 
+    /// 
+    private static IEnumerable GetDirectOrIndirect(IContentTypeComposition? ctype)
+    {
+        if (ctype == null)
+        {
+            return Enumerable.Empty();
+        }
+
+        // hashset guarantees uniqueness on Id
+        var all = new HashSet(new DelegateEqualityComparer(
+            (x, y) => x?.Id == y?.Id,
+            x => x.Id));
+
+        var stack = new Stack();
+
+        foreach (IContentTypeComposition x in ctype.ContentTypeComposition)
+        {
+            stack.Push(x);
+        }
+
+        while (stack.Count > 0)
+        {
+            IContentTypeComposition x = stack.Pop();
+            all.Add(x);
+            foreach (IContentTypeComposition y in x.ContentTypeComposition)
+            {
+                stack.Push(y);
+            }
+        }
+
+        return all;
     }
 }
diff --git a/src/Umbraco.Core/Services/ContentVersionService.cs b/src/Umbraco.Core/Services/ContentVersionService.cs
index 9e32bab762..24443a3957 100644
--- a/src/Umbraco.Core/Services/ContentVersionService.cs
+++ b/src/Umbraco.Core/Services/ContentVersionService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -10,191 +7,194 @@ using Umbraco.Cms.Core.Scoping;
 using Umbraco.Extensions;
 
 // ReSharper disable once CheckNamespace
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+internal class ContentVersionService : IContentVersionService
 {
-    internal class ContentVersionService : IContentVersionService
+    private readonly IAuditRepository _auditRepository;
+    private readonly IContentVersionCleanupPolicy _contentVersionCleanupPolicy;
+    private readonly IDocumentVersionRepository _documentVersionRepository;
+    private readonly IEventMessagesFactory _eventMessagesFactory;
+    private readonly ILanguageRepository _languageRepository;
+    private readonly ILogger _logger;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public ContentVersionService(
+        ILogger logger,
+        IDocumentVersionRepository documentVersionRepository,
+        IContentVersionCleanupPolicy contentVersionCleanupPolicy,
+        ICoreScopeProvider scopeProvider,
+        IEventMessagesFactory eventMessagesFactory,
+        IAuditRepository auditRepository,
+        ILanguageRepository languageRepository)
     {
-        private readonly ILogger _logger;
-        private readonly IDocumentVersionRepository _documentVersionRepository;
-        private readonly IContentVersionCleanupPolicy _contentVersionCleanupPolicy;
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IEventMessagesFactory _eventMessagesFactory;
-        private readonly IAuditRepository _auditRepository;
-        private readonly ILanguageRepository _languageRepository;
+        _logger = logger;
+        _documentVersionRepository = documentVersionRepository;
+        _contentVersionCleanupPolicy = contentVersionCleanupPolicy;
+        _scopeProvider = scopeProvider;
+        _eventMessagesFactory = eventMessagesFactory;
+        _auditRepository = auditRepository;
+        _languageRepository = languageRepository;
+    }
 
-        public ContentVersionService(
-            ILogger logger,
-            IDocumentVersionRepository documentVersionRepository,
-            IContentVersionCleanupPolicy contentVersionCleanupPolicy,
-            ICoreScopeProvider scopeProvider,
-            IEventMessagesFactory eventMessagesFactory,
-            IAuditRepository auditRepository,
-            ILanguageRepository languageRepository)
+    /// 
+    public IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate) =>
+
+        // Media - ignored
+        // Members - ignored
+        CleanupDocumentVersions(asAtDate);
+
+    /// 
+    public IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null)
+    {
+        if (pageIndex < 0)
         {
-            _logger = logger;
-            _documentVersionRepository = documentVersionRepository;
-            _contentVersionCleanupPolicy = contentVersionCleanupPolicy;
-            _scopeProvider = scopeProvider;
-            _eventMessagesFactory = eventMessagesFactory;
-            _auditRepository = auditRepository;
-            _languageRepository = languageRepository;
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        public IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate)
+        if (pageSize <= 0)
         {
-            // Media - ignored
-            // Members - ignored
-            return CleanupDocumentVersions(asAtDate);
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        private IReadOnlyCollection CleanupDocumentVersions(DateTime asAtDate)
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            List versionsToDelete;
+            var languageId = _languageRepository.GetIdByIsoCode(culture, true);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentVersionRepository.GetPagedItemsByContentId(contentId, pageIndex, pageSize, out totalRecords, languageId);
+        }
+    }
 
-            /* Why so many scopes?
-             *
-             * We could just work out the set to delete at SQL infra level which was the original plan, however we agreed that really we should fire
-             * ContentService.DeletingVersions so people can hook & cancel if required.
-             *
-             * On first time run of cleanup on a site with a lot of history there may be a lot of historic ContentVersions to remove e.g. 200K for our.umbraco.com.
-             * If we weren't supporting SQL CE we could do TVP, or use temp tables to bulk delete with joins to our list of version ids to nuke.
-             * (much nicer, we can kill 100k in sub second time-frames).
-             *
-             * However we are supporting SQL CE, so the easiest thing to do is use the Umbraco InGroupsOf helper to create a query with 2K args of version
-             * ids to delete at a time.
-             *
-             * This is already done at the repository level, however if we only had a single scope at service level we're still locking
-             * the ContentVersions table (and other related tables) for a couple of minutes which makes the back office unusable.
-             *
-             * As a quick fix, we can also use InGroupsOf at service level, create a scope per group to give other connections a chance
-             * to grab the locks and execute their queries.
-             *
-             * This makes the back office a tiny bit sluggish during first run but it is usable for loading tree and publishing content.
-             *
-             * There are optimizations we can do, we could add a bulk delete for SqlServerSyntaxProvider which differs in implementation
-             * and fallback to this naive approach only for SQL CE, however we agreed it is not worth the effort as this is a one time pain,
-             * subsequent runs shouldn't have huge numbers of versions to cleanup.
-             *
-             * tl;dr lots of scopes to enable other connections to use the DB whilst we work.
-             */
-            using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+    /// 
+    public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentVersionRepository.SetPreventCleanup(versionId, preventCleanup);
+
+            ContentVersionMeta? version = _documentVersionRepository.Get(versionId);
+
+            if (version is null)
             {
-                IReadOnlyCollection? allHistoricVersions = _documentVersionRepository.GetDocumentVersionsEligibleForCleanup();
-
-                if (allHistoricVersions is null)
-                {
-                    return Array.Empty();
-                }
-                _logger.LogDebug("Discovered {count} candidate(s) for ContentVersion cleanup", allHistoricVersions.Count);
-                versionsToDelete = new List(allHistoricVersions.Count);
-
-                IEnumerable filteredContentVersions = _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions);
-
-                foreach (ContentVersionMeta version in filteredContentVersions)
-                {
-                    EventMessages messages = _eventMessagesFactory.Get();
-
-                    if (scope.Notifications.PublishCancelable(new ContentDeletingVersionsNotification(version.ContentId, messages, version.VersionId)))
-                    {
-                        _logger.LogDebug("Delete cancelled for ContentVersion [{versionId}]", version.VersionId);
-                        continue;
-                    }
-
-                    versionsToDelete.Add(version);
-                }
+                return;
             }
 
-            if (!versionsToDelete.Any())
+            AuditType auditType = preventCleanup
+                ? AuditType.ContentVersionPreventCleanup
+                : AuditType.ContentVersionEnableCleanup;
+
+            var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
+
+            Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
+        }
+    }
+
+    private IReadOnlyCollection CleanupDocumentVersions(DateTime asAtDate)
+    {
+        List versionsToDelete;
+
+        /* Why so many scopes?
+         *
+         * We could just work out the set to delete at SQL infra level which was the original plan, however we agreed that really we should fire
+         * ContentService.DeletingVersions so people can hook & cancel if required.
+         *
+         * On first time run of cleanup on a site with a lot of history there may be a lot of historic ContentVersions to remove e.g. 200K for our.umbraco.com.
+         * If we weren't supporting SQL CE we could do TVP, or use temp tables to bulk delete with joins to our list of version ids to nuke.
+         * (much nicer, we can kill 100k in sub second time-frames).
+         *
+         * However we are supporting SQL CE, so the easiest thing to do is use the Umbraco InGroupsOf helper to create a query with 2K args of version
+         * ids to delete at a time.
+         *
+         * This is already done at the repository level, however if we only had a single scope at service level we're still locking
+         * the ContentVersions table (and other related tables) for a couple of minutes which makes the back office unusable.
+         *
+         * As a quick fix, we can also use InGroupsOf at service level, create a scope per group to give other connections a chance
+         * to grab the locks and execute their queries.
+         *
+         * This makes the back office a tiny bit sluggish during first run but it is usable for loading tree and publishing content.
+         *
+         * There are optimizations we can do, we could add a bulk delete for SqlServerSyntaxProvider which differs in implementation
+         * and fallback to this naive approach only for SQL CE, however we agreed it is not worth the effort as this is a one time pain,
+         * subsequent runs shouldn't have huge numbers of versions to cleanup.
+         *
+         * tl;dr lots of scopes to enable other connections to use the DB whilst we work.
+         */
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IReadOnlyCollection? allHistoricVersions =
+                _documentVersionRepository.GetDocumentVersionsEligibleForCleanup();
+
+            if (allHistoricVersions is null)
             {
-                _logger.LogDebug("No remaining ContentVersions for cleanup");
                 return Array.Empty();
             }
 
-            _logger.LogDebug("Removing {count} ContentVersion(s)", versionsToDelete.Count);
+            _logger.LogDebug("Discovered {count} candidate(s) for ContentVersion cleanup", allHistoricVersions.Count);
+            versionsToDelete = new List(allHistoricVersions.Count);
 
-            foreach (IEnumerable group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount))
+            IEnumerable filteredContentVersions =
+                _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions);
+
+            foreach (ContentVersionMeta version in filteredContentVersions)
             {
-                using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+                EventMessages messages = _eventMessagesFactory.Get();
+
+                if (scope.Notifications.PublishCancelable(
+                        new ContentDeletingVersionsNotification(version.ContentId, messages, version.VersionId)))
                 {
-                    scope.WriteLock(Constants.Locks.ContentTree);
-                    var groupEnumerated = group.ToList();
-                    _documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId));
-
-                    foreach (ContentVersionMeta version in groupEnumerated)
-                    {
-                        EventMessages messages = _eventMessagesFactory.Get();
-
-                        scope.Notifications.Publish(new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
-                    }
+                    _logger.LogDebug("Delete cancelled for ContentVersion [{versionId}]", version.VersionId);
+                    continue;
                 }
-            }
 
-            using (_scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy");
+                versionsToDelete.Add(version);
             }
-
-            return versionsToDelete;
         }
 
-        /// 
-        public IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null)
+        if (!versionsToDelete.Any())
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var languageId = _languageRepository.GetIdByIsoCode(culture, throwOnNotFound: true);
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentVersionRepository.GetPagedItemsByContentId(contentId, pageIndex, pageSize, out totalRecords, languageId);
-            }
+            _logger.LogDebug("No remaining ContentVersions for cleanup");
+            return Array.Empty();
         }
 
-        /// 
-        public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1)
+        _logger.LogDebug("Removing {count} ContentVersion(s)", versionsToDelete.Count);
+
+        foreach (IEnumerable group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount))
         {
             using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
             {
                 scope.WriteLock(Constants.Locks.ContentTree);
-                _documentVersionRepository.SetPreventCleanup(versionId, preventCleanup);
+                var groupEnumerated = group.ToList();
+                _documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId));
 
-                ContentVersionMeta? version = _documentVersionRepository.Get(versionId);
-
-                if (version is null)
+                foreach (ContentVersionMeta version in groupEnumerated)
                 {
-                    return;
+                    EventMessages messages = _eventMessagesFactory.Get();
+
+                    scope.Notifications.Publish(
+                        new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
                 }
-
-                AuditType auditType = preventCleanup
-                    ? AuditType.ContentVersionPreventCleanup
-                    : AuditType.ContentVersionEnableCleanup;
-
-                var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
-
-                Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
             }
         }
 
-        private void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null)
+        using (_scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            var entry = new AuditItem(
-                objectId,
-                type,
-                userId,
-                UmbracoObjectTypes.Document.GetName(),
-                message,
-                parameters);
-
-            _auditRepository.Save(entry);
+            Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy");
         }
+
+        return versionsToDelete;
+    }
+
+    private void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null)
+    {
+        var entry = new AuditItem(
+            objectId,
+            type,
+            userId,
+            UmbracoObjectTypes.Document.GetName(),
+            message,
+            parameters);
+
+        _auditRepository.Save(entry);
     }
 }
diff --git a/src/Umbraco.Core/Services/CultureImpactFactory.cs b/src/Umbraco.Core/Services/CultureImpactFactory.cs
new file mode 100644
index 0000000000..c520f95d0e
--- /dev/null
+++ b/src/Umbraco.Core/Services/CultureImpactFactory.cs
@@ -0,0 +1,171 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.Services;
+
+public class CultureImpactFactory : ICultureImpactFactory
+{
+    private SecuritySettings _securitySettings;
+
+    public CultureImpactFactory(IOptionsMonitor securitySettings)
+    {
+        _securitySettings = securitySettings.CurrentValue;
+
+        securitySettings.OnChange(x => _securitySettings = x);
+    }
+
+    /// 
+    public CultureImpact? Create(string? culture, bool isDefault, IContent content)
+    {
+        TryCreate(culture, isDefault, content.ContentType.Variations, true, _securitySettings.AllowEditInvariantFromNonDefault, out CultureImpact? impact);
+
+        return impact;
+    }
+
+    /// 
+    public CultureImpact ImpactAll() => CultureImpact.All;
+
+    /// 
+    public CultureImpact ImpactInvariant() => CultureImpact.Invariant;
+
+    /// 
+    public CultureImpact ImpactExplicit(string? culture, bool isDefault)
+    {
+        if (culture is null)
+        {
+            throw new ArgumentException("Culture  is not explicit.");
+        }
+
+        if (string.IsNullOrWhiteSpace(culture))
+        {
+            throw new ArgumentException("Culture \"\" is not explicit.");
+        }
+
+        if (culture == "*")
+        {
+            throw new ArgumentException("Culture \"*\" is not explicit.");
+        }
+
+        return new CultureImpact(culture, isDefault, _securitySettings.AllowEditInvariantFromNonDefault);
+    }
+
+    /// 
+    public string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures, string? defaultCulture)
+    {
+        if (content is null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
+
+        if (savingCultures is null)
+        {
+            throw new ArgumentNullException(nameof(savingCultures));
+        }
+
+        if (savingCultures.Length == 0)
+        {
+            throw new ArgumentException(nameof(savingCultures));
+        }
+
+        var cultureForInvariantErrors = savingCultures.Any(x => x.InvariantEquals(defaultCulture))
+            // The default culture is being flagged for saving so use it
+            ? defaultCulture
+            // If the content has no published version, we need to affiliate validation with the first variant being saved.
+            // If the content has a published version we will not affiliate the validation with any culture (null)
+            : !content.Published ? savingCultures[0] : null;
+
+        return cultureForInvariantErrors;
+    }
+
+    /// 
+    /// Tries to create an impact instance representing the impact of a culture set,
+    /// in the context of a content item variation.
+    /// 
+    /// The culture code.
+    /// A value indicating whether the culture is the default culture.
+    /// A content variation.
+    /// A value indicating whether to throw if the impact cannot be created.
+    /// A value indicating if publishing invariant properties from non-default language.
+    /// The impact if it could be created, otherwise null.
+    /// A value indicating whether the impact could be created.
+    /// 
+    /// Validates that the culture is compatible with the variation.
+    /// 
+    internal bool TryCreate(string? culture, bool isDefault, ContentVariation variation, bool throwOnFail, bool editInvariantFromNonDefault, out CultureImpact? impact)
+    {
+        impact = null;
+
+        // if culture is invariant...
+        if (culture is null)
+        {
+            // ... then variation must not vary by culture ...
+            if (variation.VariesByCulture())
+            {
+                if (throwOnFail)
+                {
+                    throw new InvalidOperationException("The invariant culture is not compatible with a varying variation.");
+                }
+
+                return false;
+            }
+
+            // ... and it cannot be default
+            if (isDefault)
+            {
+                if (throwOnFail)
+                {
+                    throw new InvalidOperationException("The invariant culture can not be the default culture.");
+                }
+
+                return false;
+            }
+
+            impact = ImpactInvariant();
+            return true;
+        }
+
+        // if culture is 'all'...
+        if (culture == "*")
+        {
+            // ... it cannot be default
+            if (isDefault)
+            {
+                if (throwOnFail)
+                    throw new InvalidOperationException("The 'all' culture can not be the default culture.");
+                return false;
+            }
+
+            // if variation does not vary by culture, then impact is invariant
+            impact = variation.VariesByCulture() ? ImpactAll() : ImpactInvariant();
+            return true;
+        }
+
+        // neither null nor "*" - cannot be the empty string
+        if (culture.IsNullOrWhiteSpace())
+        {
+            if (throwOnFail)
+            {
+                throw new ArgumentException("Cannot be the empty string.", nameof(culture));
+            }
+
+            return false;
+        }
+
+        // if culture is specific, then variation must vary
+        if (variation.VariesByCulture() is false)
+        {
+            if (throwOnFail)
+            {
+                throw new InvalidOperationException($"The variant culture {culture} is not compatible with an invariant variation.");
+            }
+
+            return false;
+        }
+
+        // return specific impact
+        impact = new CultureImpact(culture, isDefault, editInvariantFromNonDefault);
+        return true;
+    }
+}
diff --git a/src/Umbraco.Core/Services/DashboardService.cs b/src/Umbraco.Core/Services/DashboardService.cs
index 203ce64984..f5ddb30557 100644
--- a/src/Umbraco.Core/Services/DashboardService.cs
+++ b/src/Umbraco.Core/Services/DashboardService.cs
@@ -1,145 +1,161 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core.Dashboards;
 using Umbraco.Cms.Core.Models.ContentEditing;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     A utility class for determine dashboard security
+/// 
+public class DashboardService : IDashboardService
 {
-    /// 
-    /// A utility class for determine dashboard security
-    /// 
-    public class DashboardService : IDashboardService
+    private readonly DashboardCollection _dashboardCollection;
+
+    private readonly ILocalizedTextService _localizedText;
+
+    // TODO: Unit test all this!!! :/
+    private readonly ISectionService _sectionService;
+
+    public DashboardService(ISectionService sectionService, DashboardCollection dashboardCollection, ILocalizedTextService localizedText)
     {
-        // TODO: Unit test all this!!! :/
+        _sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
+        _dashboardCollection = dashboardCollection ?? throw new ArgumentNullException(nameof(dashboardCollection));
+        _localizedText = localizedText ?? throw new ArgumentNullException(nameof(localizedText));
+    }
 
-        private readonly ISectionService _sectionService;
-        private readonly DashboardCollection _dashboardCollection;
-        private readonly ILocalizedTextService _localizedText;
+    /// 
+    public IEnumerable> GetDashboards(string section, IUser? currentUser)
+    {
+        var tabs = new List>();
+        var tabId = 0;
 
-        public DashboardService(ISectionService sectionService, DashboardCollection dashboardCollection, ILocalizedTextService localizedText)
+        foreach (IDashboard dashboard in _dashboardCollection.Where(x => x.Sections.InvariantContains(section)))
         {
-            _sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
-            _dashboardCollection = dashboardCollection ?? throw new ArgumentNullException(nameof(dashboardCollection));
-            _localizedText = localizedText ?? throw new ArgumentNullException(nameof(localizedText));
-        }
-
-
-        /// 
-        public IEnumerable> GetDashboards(string section, IUser? currentUser)
-        {
-            var tabs = new List>();
-            var tabId = 0;
-
-            foreach (var dashboard in _dashboardCollection.Where(x => x.Sections.InvariantContains(section)))
+            // validate access
+            if (currentUser is null || !CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules))
             {
-                // validate access
-                if (currentUser is null || !CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules))
-                    continue;
-
-                if (dashboard.View?.InvariantEndsWith(".ascx") ?? false)
-                    throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
-
-                var dashboards = new List { dashboard };
-                tabs.Add(new Tab
-                {
-                    Id = tabId++,
-                    Label = _localizedText.Localize("dashboardTabs", dashboard.Alias),
-                    Alias = dashboard.Alias,
-                    Properties = dashboards
-                });
+                continue;
             }
 
-            return tabs;
-        }
-
-        /// 
-        public IDictionary>> GetDashboards(IUser? currentUser)
-        {
-            return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
-        }
-
-        private bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable rules)
-        {
-            if (user.Id == Constants.Security.SuperUserId)
-                return true;
-
-            var (denyRules, grantRules, grantBySectionRules) = GroupRules(rules);
-
-            var hasAccess = true;
-            string[]? assignedUserGroups = null;
-
-            // if there are no grant rules, then access is granted by default, unless denied
-            // otherwise, grant rules determine if access can be granted at all
-            if (grantBySectionRules.Length > 0 || grantRules.Length > 0)
+            if (dashboard.View?.InvariantEndsWith(".ascx") ?? false)
             {
-                hasAccess = false;
+                throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
+            }
 
-                // check if this item has any grant-by-section arguments.
-                // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far)
-                if (grantBySectionRules.Length > 0)
+            var dashboards = new List { dashboard };
+            tabs.Add(new Tab
+            {
+                Id = tabId++,
+                Label = _localizedText.Localize("dashboardTabs", dashboard.Alias),
+                Alias = dashboard.Alias,
+                Properties = dashboards,
+            });
+        }
+
+        return tabs;
+    }
+
+    /// 
+    public IDictionary>> GetDashboards(IUser? currentUser) => _sectionService
+        .GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
+
+    private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable rules)
+    {
+        IAccessRule[]? denyRules = null, grantRules = null, grantBySectionRules = null;
+
+        IEnumerable> groupedRules = rules.GroupBy(x => x.Type);
+        foreach (IGrouping group in groupedRules)
+        {
+            IAccessRule[] a = group.ToArray();
+            switch (group.Key)
+            {
+                case AccessRuleType.Deny:
+                    denyRules = a;
+                    break;
+                case AccessRuleType.Grant:
+                    grantRules = a;
+                    break;
+                case AccessRuleType.GrantBySection:
+                    grantBySectionRules = a;
+                    break;
+                default:
+                    throw new NotSupportedException($"The '{group.Key}'-AccessRuleType is not supported.");
+            }
+        }
+
+        return (denyRules ?? Array.Empty(), grantRules ?? Array.Empty(),
+            grantBySectionRules ?? Array.Empty());
+    }
+
+    private bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable rules)
+    {
+        if (user.Id == Constants.Security.SuperUserId)
+        {
+            return true;
+        }
+
+        (IAccessRule[] denyRules, IAccessRule[] grantRules, IAccessRule[] grantBySectionRules) = GroupRules(rules);
+
+        var hasAccess = true;
+        string[]? assignedUserGroups = null;
+
+        // if there are no grant rules, then access is granted by default, unless denied
+        // otherwise, grant rules determine if access can be granted at all
+        if (grantBySectionRules.Length > 0 || grantRules.Length > 0)
+        {
+            hasAccess = false;
+
+            // check if this item has any grant-by-section arguments.
+            // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far)
+            if (grantBySectionRules.Length > 0)
+            {
+                var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray();
+                var wantedSections = grantBySectionRules.SelectMany(g =>
+                    g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ??
+                    Array.Empty()).ToArray();
+
+                if (wantedSections.Intersect(allowedSections).Any())
                 {
-                    var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray();
-                    var wantedSections = grantBySectionRules.SelectMany(g => g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()).ToArray();
-
-                    if (wantedSections.Intersect(allowedSections).Any())
-                        hasAccess = true;
-                }
-
-                // if not already granted access, check if this item as any grant arguments.
-                // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far)
-                if (hasAccess == false && grantRules.Any())
-                {
-                    assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray();
-                    var wantedUserGroups = grantRules.SelectMany(g => g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()).ToArray();
-
-                    if (wantedUserGroups.Intersect(assignedUserGroups).Any())
-                        hasAccess = true;
+                    hasAccess = true;
                 }
             }
 
-            // No need to check denyRules if there aren't any, just return current state
-            if (denyRules.Length == 0)
-                return hasAccess;
+            // if not already granted access, check if this item as any grant arguments.
+            // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far)
+            if (hasAccess == false && grantRules.Any())
+            {
+                assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray();
+                var wantedUserGroups = grantRules.SelectMany(g =>
+                    g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ??
+                    Array.Empty()).ToArray();
 
-            // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will
-            // be denied to see it no matter what
-            assignedUserGroups = assignedUserGroups ?? user.Groups.Select(x => x.Alias).ToArray();
-            var deniedUserGroups = denyRules.SelectMany(g => g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()).ToArray();
-
-            if (deniedUserGroups.Intersect(assignedUserGroups).Any())
-                hasAccess = false;
+                if (wantedUserGroups.Intersect(assignedUserGroups).Any())
+                {
+                    hasAccess = true;
+                }
+            }
+        }
 
+        // No need to check denyRules if there aren't any, just return current state
+        if (denyRules.Length == 0)
+        {
             return hasAccess;
         }
 
-        private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable rules)
+        // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will
+        // be denied to see it no matter what
+        assignedUserGroups ??= user.Groups.Select(x => x.Alias).ToArray();
+        var deniedUserGroups = denyRules.SelectMany(g =>
+                g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ??
+                Array.Empty())
+            .ToArray();
+
+        if (deniedUserGroups.Intersect(assignedUserGroups).Any())
         {
-            IAccessRule[]? denyRules = null, grantRules = null, grantBySectionRules = null;
-
-            var groupedRules = rules.GroupBy(x => x.Type);
-            foreach (var group in groupedRules)
-            {
-                var a = group.ToArray();
-                switch (group.Key)
-                {
-                    case AccessRuleType.Deny:
-                        denyRules = a;
-                        break;
-                    case AccessRuleType.Grant:
-                        grantRules = a;
-                        break;
-                    case AccessRuleType.GrantBySection:
-                        grantBySectionRules = a;
-                        break;
-                    default:
-                        throw new NotSupportedException($"The '{group.Key.ToString()}'-AccessRuleType is not supported.");
-                }
-            }
-
-            return (denyRules ?? Array.Empty(), grantRules ?? Array.Empty(), grantBySectionRules ?? Array.Empty());
+            hasAccess = false;
         }
+
+        return hasAccess;
     }
 }
diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs
index 5c9d5847ed..1fdbb4a79b 100644
--- a/src/Umbraco.Core/Services/DataTypeService.cs
+++ b/src/Umbraco.Core/Services/DataTypeService.cs
@@ -1,13 +1,12 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Exceptions;
 using Umbraco.Cms.Core.IO;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.PropertyEditors;
 using Umbraco.Cms.Core.Scoping;
@@ -39,10 +38,17 @@ namespace Umbraco.Cms.Core.Services.Implement
         [Obsolete("Please use constructor that takes an ")]
         public DataTypeService(
             IDataValueEditorFactory dataValueEditorFactory,
-            ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IDataTypeRepository dataTypeRepository, IDataTypeContainerRepository dataTypeContainerRepository,
-            IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository,
-            IIOHelper ioHelper, ILocalizedTextService localizedTextService, ILocalizationService localizationService,
+            ICoreScopeProvider provider,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IDataTypeRepository dataTypeRepository,
+            IDataTypeContainerRepository dataTypeContainerRepository,
+            IAuditRepository auditRepository,
+            IEntityRepository entityRepository,
+            IContentTypeRepository contentTypeRepository,
+            IIOHelper ioHelper,
+            ILocalizedTextService localizedTextService,
+            ILocalizationService localizationService,
             IShortStringHelper shortStringHelper,
             IJsonSerializer jsonSerializer)
             : this(
@@ -77,10 +83,17 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public DataTypeService(
             IDataValueEditorFactory dataValueEditorFactory,
-            ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IDataTypeRepository dataTypeRepository, IDataTypeContainerRepository dataTypeContainerRepository,
-            IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository,
-            IIOHelper ioHelper, ILocalizedTextService localizedTextService, ILocalizationService localizationService,
+            ICoreScopeProvider provider,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IDataTypeRepository dataTypeRepository,
+            IDataTypeContainerRepository dataTypeContainerRepository,
+            IAuditRepository auditRepository,
+            IEntityRepository entityRepository,
+            IContentTypeRepository contentTypeRepository,
+            IIOHelper ioHelper,
+            ILocalizedTextService localizedTextService,
+            ILocalizationService localizationService,
             IShortStringHelper shortStringHelper,
             IJsonSerializer jsonSerializer,
             IEditorConfigurationParser editorConfigurationParser)
@@ -102,14 +115,14 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         #region Containers
 
-        public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 try
                 {
-                    var container = new EntityContainer(Cms.Core.Constants.ObjectTypes.DataType)
+                    var container = new EntityContainer(Constants.ObjectTypes.DataType)
                     {
                         Name = name,
                         ParentId = parentId,
@@ -142,26 +155,20 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public EntityContainer? GetContainer(int containerId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.Get(containerId);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.Get(containerId);
         }
 
         public EntityContainer? GetContainer(Guid containerId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.Get(containerId);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.Get(containerId);
         }
 
         public IEnumerable GetContainers(string name, int level)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.Get(name, level);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.Get(name, level);
         }
 
         public IEnumerable GetContainers(IDataType dataType)
@@ -169,7 +176,7 @@ namespace Umbraco.Cms.Core.Services.Implement
             var ancestorIds = dataType.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
                 .Select(x =>
                 {
-                    var asInt = x.TryConvertTo();
+                    Attempt asInt = x.TryConvertTo();
                     return asInt.Success ? asInt.Result : int.MinValue;
                 })
                 .Where(x => x != int.MinValue && x != dataType.Id)
@@ -180,19 +187,17 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public IEnumerable GetContainers(int[] containerIds)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.GetMany(containerIds);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.GetMany(containerIds);
         }
 
-        public Attempt SaveContainer(EntityContainer container, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            if (container.ContainedObjectType != Cms.Core.Constants.ObjectTypes.DataType)
+            if (container.ContainedObjectType != Constants.ObjectTypes.DataType)
             {
-                var ex = new InvalidOperationException("Not a " + Cms.Core.Constants.ObjectTypes.DataType + " container.");
+                var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataType + " container.");
                 return OperationResult.Attempt.Fail(evtMsgs, ex);
             }
 
@@ -202,7 +207,7 @@ namespace Umbraco.Cms.Core.Services.Implement
                 return OperationResult.Attempt.Fail(evtMsgs, ex);
             }
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 var savingEntityContainerNotification = new EntityContainerSavingNotification(container, evtMsgs);
                 if (scope.Notifications.PublishCancelable(savingEntityContainerNotification))
@@ -221,17 +226,20 @@ namespace Umbraco.Cms.Core.Services.Implement
             return OperationResult.Attempt.Succeed(evtMsgs);
         }
 
-        public Attempt DeleteContainer(int containerId, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                var container = _dataTypeContainerRepository.Get(containerId);
-                if (container == null) return OperationResult.Attempt.NoOperation(evtMsgs);
+                EntityContainer? container = _dataTypeContainerRepository.Get(containerId);
+                if (container == null)
+                {
+                    return OperationResult.Attempt.NoOperation(evtMsgs);
+                }
 
                 // 'container' here does not know about its children, so we need
                 // to get it again from the entity repository, as a light entity
-                var entity = _entityRepository.Get(container.Id);
+                IEntitySlim? entity = _entityRepository.Get(container.Id);
                 if (entity?.HasChildren ?? false)
                 {
                     scope.Complete();
@@ -255,18 +263,20 @@ namespace Umbraco.Cms.Core.Services.Implement
             return OperationResult.Attempt.Succeed(evtMsgs);
         }
 
-        public Attempt?> RenameContainer(int id, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 try
                 {
-                    var container = _dataTypeContainerRepository.Get(id);
+                    EntityContainer? container = _dataTypeContainerRepository.Get(id);
 
                     //throw if null, this will be caught by the catch and a failed returned
                     if (container == null)
+                    {
                         throw new InvalidOperationException("No container found with id " + id);
+                    }
 
                     container.Name = name;
 
@@ -300,12 +310,10 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         public IDataType? GetDataType(string name)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var dataType = _dataTypeRepository.Get(Query().Where(x => x.Name == name))?.FirstOrDefault();
-                ConvertMissingEditorOfDataTypeToLabel(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IDataType? dataType = _dataTypeRepository.Get(Query().Where(x => x.Name == name))?.FirstOrDefault();
+            ConvertMissingEditorOfDataTypeToLabel(dataType);
+            return dataType;
         }
 
         /// 
@@ -315,12 +323,10 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         public IDataType? GetDataType(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var dataType = _dataTypeRepository.Get(id);
-                ConvertMissingEditorOfDataTypeToLabel(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IDataType? dataType = _dataTypeRepository.Get(id);
+            ConvertMissingEditorOfDataTypeToLabel(dataType);
+            return dataType;
         }
 
         /// 
@@ -330,13 +336,11 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         public IDataType? GetDataType(Guid id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.Key == id);
-                var dataType = _dataTypeRepository.Get(query).FirstOrDefault();
-                ConvertMissingEditorOfDataTypeToLabel(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery query = Query().Where(x => x.Key == id);
+            IDataType? dataType = _dataTypeRepository.Get(query).FirstOrDefault();
+            ConvertMissingEditorOfDataTypeToLabel(dataType);
+            return dataType;
         }
 
         /// 
@@ -346,13 +350,11 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// Collection of  objects with a matching control id
         public IEnumerable GetByEditorAlias(string propertyEditorAlias)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.EditorAlias == propertyEditorAlias);
-                var dataType = _dataTypeRepository.Get(query);
-                ConvertMissingEditorsOfDataTypesToLabels(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery query = Query().Where(x => x.EditorAlias == propertyEditorAlias);
+            IEnumerable dataType = _dataTypeRepository.Get(query).ToArray();
+            ConvertMissingEditorsOfDataTypesToLabels(dataType);
+            return dataType;
         }
 
         /// 
@@ -362,13 +364,11 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// An enumerable list of  objects
         public IEnumerable GetAll(params int[] ids)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var dataTypes = _dataTypeRepository.GetMany(ids);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IEnumerable dataTypes = _dataTypeRepository.GetMany(ids).ToArray();
 
-                ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
-                return dataTypes;
-            }
+            ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
+            return dataTypes;
         }
 
         private void ConvertMissingEditorOfDataTypeToLabel(IDataType? dataType)
@@ -385,9 +385,9 @@ namespace Umbraco.Cms.Core.Services.Implement
         {
             // Any data types that don't have an associated editor are created of a specific type.
             // We convert them to labels to make clear to the user why the data type cannot be used.
-            var dataTypesWithMissingEditors = dataTypes
+            IEnumerable dataTypesWithMissingEditors = dataTypes
                 .Where(x => x.Editor is MissingPropertyEditor);
-            foreach (var dataType in dataTypesWithMissingEditors)
+            foreach (IDataType dataType in dataTypesWithMissingEditors)
             {
                 dataType.Editor = new LabelPropertyEditor(_dataValueEditorFactory, _ioHelper, _editorConfigurationParser);
             }
@@ -395,10 +395,10 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public Attempt?> Move(IDataType toMove, int parentId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
             var moveInfo = new List>();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId);
 
@@ -416,7 +416,9 @@ namespace Umbraco.Cms.Core.Services.Implement
                     {
                         container = _dataTypeContainerRepository.Get(parentId);
                         if (container == null)
+                        {
                             throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                        }
                     }
                     moveInfo.AddRange(_dataTypeRepository.Move(toMove, container));
 
@@ -439,39 +441,37 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         ///  to save
         /// Id of the user issuing the save
-        public void Save(IDataType dataType, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void Save(IDataType dataType, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
             dataType.CreatorId = userId;
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var saveEventArgs = new SaveEventArgs(dataType);
+
+            var savingDataTypeNotification = new DataTypeSavingNotification(dataType, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
             {
-                var saveEventArgs = new SaveEventArgs(dataType);
-
-                var savingDataTypeNotification = new DataTypeSavingNotification(dataType, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(dataType.Name))
-                {
-                    throw new ArgumentException("Cannot save datatype with empty name.");
-                }
-
-                if (dataType.Name != null && dataType.Name.Length > 255)
-                {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-                }
-
-                _dataTypeRepository.Save(dataType);
-
-                scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification));
-
-                Audit(AuditType.Save, userId, dataType.Id);
                 scope.Complete();
+                return;
             }
+
+            if (string.IsNullOrWhiteSpace(dataType.Name))
+            {
+                throw new ArgumentException("Cannot save datatype with empty name.");
+            }
+
+            if (dataType.Name != null && dataType.Name.Length > 255)
+            {
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+            }
+
+            _dataTypeRepository.Save(dataType);
+
+            scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification));
+
+            Audit(AuditType.Save, userId, dataType.Id);
+            scope.Complete();
         }
 
         /// 
@@ -481,30 +481,28 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// Id of the user issuing the save
         public void Save(IEnumerable dataTypeDefinitions, int userId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            var dataTypeDefinitionsA = dataTypeDefinitions.ToArray();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            IDataType[] dataTypeDefinitionsA = dataTypeDefinitions.ToArray();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var savingDataTypeNotification = new DataTypeSavingNotification(dataTypeDefinitions, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
             {
-                var savingDataTypeNotification = new DataTypeSavingNotification(dataTypeDefinitions, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                foreach (var dataTypeDefinition in dataTypeDefinitionsA)
-                {
-                    dataTypeDefinition.CreatorId = userId;
-                    _dataTypeRepository.Save(dataTypeDefinition);
-                }
-
-                scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification));
-
-                Audit(AuditType.Save, userId, -1);
-
                 scope.Complete();
+                return;
             }
+
+            foreach (IDataType dataTypeDefinition in dataTypeDefinitionsA)
+            {
+                dataTypeDefinition.CreatorId = userId;
+                _dataTypeRepository.Save(dataTypeDefinition);
+            }
+
+            scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification));
+
+            Audit(AuditType.Save, userId, -1);
+
+            scope.Complete();
         }
 
         /// 
@@ -516,64 +514,60 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         ///  to delete
         /// Optional Id of the user issuing the deletion
-        public void Delete(IDataType dataType, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var deletingDataTypeNotification = new DataTypeDeletingNotification(dataType, evtMsgs);
+            if (scope.Notifications.PublishCancelable(deletingDataTypeNotification))
             {
-                var deletingDataTypeNotification = new DataTypeDeletingNotification(dataType, evtMsgs);
-                if (scope.Notifications.PublishCancelable(deletingDataTypeNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+                scope.Complete();
+                return;
+            }
 
-                // find ContentTypes using this IDataTypeDefinition on a PropertyType, and delete
-                // TODO: media and members?!
-                // TODO: non-group properties?!
-                var query = Query().Where(x => x.DataTypeId == dataType.Id);
-                var contentTypes = _contentTypeRepository.GetByQuery(query);
-                foreach (var contentType in contentTypes)
+            // find ContentTypes using this IDataTypeDefinition on a PropertyType, and delete
+            // TODO: media and members?!
+            // TODO: non-group properties?!
+            IQuery query = Query().Where(x => x.DataTypeId == dataType.Id);
+            IEnumerable contentTypes = _contentTypeRepository.GetByQuery(query);
+            foreach (IContentType contentType in contentTypes)
+            {
+                foreach (PropertyGroup propertyGroup in contentType.PropertyGroups)
                 {
-                    foreach (var propertyGroup in contentType.PropertyGroups)
+                    var types = propertyGroup.PropertyTypes?.Where(x => x.DataTypeId == dataType.Id).ToList();
+                    if (types is not null)
                     {
-                        var types = propertyGroup.PropertyTypes?.Where(x => x.DataTypeId == dataType.Id).ToList();
-                        if (types is not null)
+                        foreach (IPropertyType propertyType in types)
                         {
-                            foreach (var propertyType in types)
-                            {
-                                propertyGroup.PropertyTypes?.Remove(propertyType);
-                            }
+                            propertyGroup.PropertyTypes?.Remove(propertyType);
                         }
                     }
-
-                    // so... we are modifying content types here. the service will trigger Deleted event,
-                    // which will propagate to DataTypeCacheRefresher which will clear almost every cache
-                    // there is to clear... and in addition published snapshot caches will clear themselves too, so
-                    // this is probably safe although it looks... weird.
-                    //
-                    // what IS weird is that a content type is losing a property and we do NOT raise any
-                    // content type event... so ppl better listen on the data type events too.
-
-                    _contentTypeRepository.Save(contentType);
                 }
 
-                _dataTypeRepository.Delete(dataType);
+                // so... we are modifying content types here. the service will trigger Deleted event,
+                // which will propagate to DataTypeCacheRefresher which will clear almost every cache
+                // there is to clear... and in addition published snapshot caches will clear themselves too, so
+                // this is probably safe although it looks... weird.
+                //
+                // what IS weird is that a content type is losing a property and we do NOT raise any
+                // content type event... so ppl better listen on the data type events too.
 
-                scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification));
-
-                Audit(AuditType.Delete, userId, dataType.Id);
-
-                scope.Complete();
+                _contentTypeRepository.Save(contentType);
             }
+
+            _dataTypeRepository.Delete(dataType);
+
+            scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification));
+
+            Audit(AuditType.Delete, userId, dataType.Id);
+
+            scope.Complete();
         }
 
         public IReadOnlyDictionary> GetReferences(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete:true))
-            {
-                return _dataTypeRepository.FindUsages(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true);
+            return _dataTypeRepository.FindUsages(id);
         }
 
         private void Audit(AuditType type, int userId, int objectId)
diff --git a/src/Umbraco.Core/Services/DataTypeUsageService.cs b/src/Umbraco.Core/Services/DataTypeUsageService.cs
new file mode 100644
index 0000000000..1a6182037e
--- /dev/null
+++ b/src/Umbraco.Core/Services/DataTypeUsageService.cs
@@ -0,0 +1,25 @@
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+
+namespace Umbraco.Cms.Core.Services;
+
+public class DataTypeUsageService : IDataTypeUsageService
+{
+    private readonly IDataTypeUsageRepository _dataTypeUsageRepository;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public DataTypeUsageService(
+        IDataTypeUsageRepository dataTypeUsageRepository,
+        ICoreScopeProvider scopeProvider)
+    {
+        _dataTypeUsageRepository = dataTypeUsageRepository;
+        _scopeProvider = scopeProvider;
+    }
+
+    /// 
+    public bool HasSavedValues(int dataTypeId)
+    {
+        using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+        return _dataTypeUsageRepository.HasSavedValues(dataTypeId);
+    }
+}
diff --git a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs
index 312b939ec5..476a2ddd47 100644
--- a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs
@@ -1,21 +1,25 @@
-using System;
+using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.PropertyEditors;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+public static class DateTypeServiceExtensions
 {
-    public static class DateTypeServiceExtensions
+    public static bool IsDataTypeIgnoringUserStartNodes(this IDataTypeService dataTypeService, Guid key)
     {
-        public static bool IsDataTypeIgnoringUserStartNodes(this IDataTypeService dataTypeService, Guid key)
+        if (DataTypeExtensions.IsBuildInDataType(key))
         {
-            if (DataTypeExtensions.IsBuildInDataType(key)) return false; //built in ones can never be ignoring start nodes
-
-            var dataType = dataTypeService.GetDataType(key);
-
-            if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig)
-                return ignoreStartNodesConfig.IgnoreUserStartNodes;
-
-            return false;
+            return false; // built in ones can never be ignoring start nodes
         }
+
+        IDataType? dataType = dataTypeService.GetDataType(key);
+
+        if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig)
+        {
+            return ignoreStartNodesConfig.IgnoreUserStartNodes;
+        }
+
+        return false;
     }
 }
diff --git a/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs
index 810106e0ba..f51858fa5b 100644
--- a/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs
+++ b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Options;
 using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Core.Models;
@@ -8,93 +5,92 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using ContentVersionCleanupPolicySettings = Umbraco.Cms.Core.Models.ContentVersionCleanupPolicySettings;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy
 {
-    public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy
+    private readonly IOptions _contentSettings;
+    private readonly IDocumentVersionRepository _documentVersionRepository;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public DefaultContentVersionCleanupPolicy(
+        IOptions contentSettings,
+        ICoreScopeProvider scopeProvider,
+        IDocumentVersionRepository documentVersionRepository)
     {
-        private readonly IOptions _contentSettings;
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IDocumentVersionRepository _documentVersionRepository;
+        _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
+        _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
+        _documentVersionRepository = documentVersionRepository ??
+                                     throw new ArgumentNullException(nameof(documentVersionRepository));
+    }
 
-        public DefaultContentVersionCleanupPolicy(IOptions contentSettings, ICoreScopeProvider scopeProvider, IDocumentVersionRepository documentVersionRepository)
+    public IEnumerable Apply(DateTime asAtDate, IEnumerable items)
+    {
+        // Note: Not checking global enable flag, that's handled in the scheduled job.
+        // If this method is called and policy is globally disabled someone has chosen to run in code.
+        Configuration.Models.ContentVersionCleanupPolicySettings globalPolicy =
+            _contentSettings.Value.ContentVersionCleanupPolicy;
+
+        var theRest = new List();
+
+        using (_scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
-            _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
-            _documentVersionRepository = documentVersionRepository ?? throw new ArgumentNullException(nameof(documentVersionRepository));
-        }
+            var policyOverrides = _documentVersionRepository.GetCleanupPolicies()?
+                .ToDictionary(x => x.ContentTypeId);
 
-        public IEnumerable Apply(DateTime asAtDate, IEnumerable items)
-        {
-            // Note: Not checking global enable flag, that's handled in the scheduled job.
-            // If this method is called and policy is globally disabled someone has chosen to run in code.
-
-            var globalPolicy = _contentSettings.Value.ContentVersionCleanupPolicy;
-
-            var theRest = new List();
-
-            using(_scopeProvider.CreateCoreScope(autoComplete: true))
+            foreach (ContentVersionMeta version in items)
             {
-                var policyOverrides = _documentVersionRepository.GetCleanupPolicies()?
-                    .ToDictionary(x => x.ContentTypeId);
+                TimeSpan age = asAtDate - version.VersionDate;
 
-                foreach (var version in items)
+                ContentVersionCleanupPolicySettings? overrides = GetOverridePolicy(version, policyOverrides);
+
+                var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays;
+                var keepLatest = overrides?.KeepLatestVersionPerDayForDays ??
+                                 globalPolicy.KeepLatestVersionPerDayForDays;
+                var preventCleanup = overrides?.PreventCleanup ?? false;
+
+                if (preventCleanup)
                 {
-                    var age = asAtDate - version.VersionDate;
-
-                    var overrides = GetOverridePolicy(version, policyOverrides);
-
-                    var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays!;
-                    var keepLatest = overrides?.KeepLatestVersionPerDayForDays ?? globalPolicy.KeepLatestVersionPerDayForDays;
-                    var preventCleanup = overrides?.PreventCleanup ?? false;
-
-                    if (preventCleanup)
-                    {
-                        continue;
-                    }
-
-                    if (age.TotalDays <= keepAll)
-                    {
-                        continue;
-                    }
-
-                    if (age.TotalDays > keepLatest)
-                    {
-
-                        yield return version;
-                        continue;
-                    }
-
-                    theRest.Add(version);
+                    continue;
                 }
 
-                var grouped = theRest.GroupBy(x => new
+                if (age.TotalDays <= keepAll)
                 {
-                    x.ContentId,
-                    x.VersionDate.Date
-                });
+                    continue;
+                }
 
-                foreach (var group in grouped)
+                if (age.TotalDays > keepLatest)
                 {
-                    foreach (var version in group.OrderByDescending(x => x.VersionId).Skip(1))
-                    {
-                        yield return version;
-                    }
+                    yield return version;
+                    continue;
+                }
+
+                theRest.Add(version);
+            }
+
+            var grouped = theRest.GroupBy(x => new { x.ContentId, x.VersionDate.Date });
+
+            foreach (var group in grouped)
+            {
+                foreach (ContentVersionMeta version in group.OrderByDescending(x => x.VersionId).Skip(1))
+                {
+                    yield return version;
                 }
             }
         }
-
-        private ContentVersionCleanupPolicySettings? GetOverridePolicy(
-            ContentVersionMeta version,
-            IDictionary? overrides)
-        {
-            if (overrides is null)
-            {
-                return null;
-            }
-
-            _ = overrides.TryGetValue(version.ContentTypeId, out var value);
-
-            return value;
-        }
+    }
+
+    private ContentVersionCleanupPolicySettings? GetOverridePolicy(
+        ContentVersionMeta version,
+        IDictionary? overrides)
+    {
+        if (overrides is null)
+        {
+            return null;
+        }
+
+        _ = overrides.TryGetValue(version.ContentTypeId, out ContentVersionCleanupPolicySettings? value);
+
+        return value;
     }
 }
diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs
index b319f0fc42..38f27bb94c 100644
--- a/src/Umbraco.Core/Services/DomainService.cs
+++ b/src/Umbraco.Core/Services/DomainService.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -6,100 +5,102 @@ using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class DomainService : RepositoryService, IDomainService
 {
-    public class DomainService : RepositoryService, IDomainService
+    private readonly IDomainRepository _domainRepository;
+
+    public DomainService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDomainRepository domainRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _domainRepository = domainRepository;
+
+    public bool Exists(string domainName)
     {
-        private readonly IDomainRepository _domainRepository;
-
-        public DomainService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IDomainRepository domainRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _domainRepository = domainRepository;
-        }
-
-        public bool Exists(string domainName)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.Exists(domainName);
-            }
-        }
-
-        public Attempt Delete(IDomain domain)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingNotification = new DomainDeletingNotification(domain, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Cancel(eventMessages);
-                }
-
-                _domainRepository.Delete(domain);
-                scope.Complete();
-
-                scope.Notifications.Publish(new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification));
-            }
-
-            return OperationResult.Attempt.Succeed(eventMessages);
-        }
-
-        public IDomain? GetByName(string name)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetByName(name);
-            }
-        }
-
-        public IDomain? GetById(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.Get(id);
-            }
-        }
-
-        public IEnumerable GetAll(bool includeWildcards)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetAll(includeWildcards);
-            }
-        }
-
-        public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetAssignedDomains(contentId, includeWildcards);
-            }
-        }
-
-        public Attempt Save(IDomain domainEntity)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingNotification = new DomainSavingNotification(domainEntity, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Cancel(eventMessages);
-                }
-
-                _domainRepository.Save(domainEntity);
-                scope.Complete();
-                scope.Notifications.Publish(new DomainSavedNotification(domainEntity, eventMessages).WithStateFrom(savingNotification));
-            }
-
-            return OperationResult.Attempt.Succeed(eventMessages);
+            return _domainRepository.Exists(domainName);
         }
     }
+
+    public Attempt Delete(IDomain domain)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var deletingNotification = new DomainDeletingNotification(domain, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages);
+            }
+
+            _domainRepository.Delete(domain);
+            scope.Complete();
+
+            scope.Notifications.Publish(
+                new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification));
+        }
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
+
+    public IDomain? GetByName(string name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.GetByName(name);
+        }
+    }
+
+    public IDomain? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.Get(id);
+        }
+    }
+
+    public IEnumerable GetAll(bool includeWildcards)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.GetAll(includeWildcards);
+        }
+    }
+
+    public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.GetAssignedDomains(contentId, includeWildcards);
+        }
+    }
+
+    public Attempt Save(IDomain domainEntity)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new DomainSavingNotification(domainEntity, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages);
+            }
+
+            _domainRepository.Save(domainEntity);
+            scope.Complete();
+            scope.Notifications.Publish(
+                new DomainSavedNotification(domainEntity, eventMessages).WithStateFrom(savingNotification));
+        }
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
 }
diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs
index 5fa7ed24f7..591fa17909 100644
--- a/src/Umbraco.Core/Services/EntityService.cs
+++ b/src/Umbraco.Core/Services/EntityService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Linq.Expressions;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
@@ -11,472 +8,519 @@ using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class EntityService : RepositoryService, IEntityService
 {
-    public class EntityService : RepositoryService, IEntityService
+    private readonly IEntityRepository _entityRepository;
+    private readonly IIdKeyMap _idKeyMap;
+    private readonly Dictionary _objectTypes;
+    private IQuery? _queryRootEntity;
+
+    public EntityService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IIdKeyMap idKeyMap,
+        IEntityRepository entityRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IEntityRepository _entityRepository;
-        private readonly Dictionary _objectTypes;
-        private IQuery? _queryRootEntity;
-        private readonly IIdKeyMap _idKeyMap;
+        _idKeyMap = idKeyMap;
+        _entityRepository = entityRepository;
 
-        public EntityService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IIdKeyMap idKeyMap, IEntityRepository entityRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        _objectTypes = new Dictionary
         {
-            _idKeyMap = idKeyMap;
-            _entityRepository = entityRepository;
+            { typeof(IDataType).FullName!, UmbracoObjectTypes.DataType },
+            { typeof(IContent).FullName!, UmbracoObjectTypes.Document },
+            { typeof(IContentType).FullName!, UmbracoObjectTypes.DocumentType },
+            { typeof(IMedia).FullName!, UmbracoObjectTypes.Media },
+            { typeof(IMediaType).FullName!, UmbracoObjectTypes.MediaType },
+            { typeof(IMember).FullName!, UmbracoObjectTypes.Member },
+            { typeof(IMemberType).FullName!, UmbracoObjectTypes.MemberType },
+        };
+    }
 
-            _objectTypes = new Dictionary
-            {
-                { typeof (IDataType).FullName!, UmbracoObjectTypes.DataType },
-                { typeof (IContent).FullName!, UmbracoObjectTypes.Document },
-                { typeof (IContentType).FullName!, UmbracoObjectTypes.DocumentType },
-                { typeof (IMedia).FullName!, UmbracoObjectTypes.Media },
-                { typeof (IMediaType).FullName!, UmbracoObjectTypes.MediaType },
-                { typeof (IMember).FullName!, UmbracoObjectTypes.Member },
-                { typeof (IMemberType).FullName!, UmbracoObjectTypes.MemberType },
-            };
-        }
+    #region Static Queries
 
-        #region Static Queries
+    // lazy-constructed because when the ctor runs, the query factory may not be ready
+    private IQuery QueryRootEntity => _queryRootEntity ??= Query()
+                                                          .Where(x => x.ParentId == -1);
 
-        // lazy-constructed because when the ctor runs, the query factory may not be ready
-        private IQuery QueryRootEntity => _queryRootEntity
-            ?? (_queryRootEntity = Query().Where(x => x.ParentId == -1));
+    #endregion
 
-        #endregion
-
-        // gets the object type, throws if not supported
-        private UmbracoObjectTypes GetObjectType(Type ?type)
+    /// 
+    public IEntitySlim? Get(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out var objType))
-                throw new NotSupportedException($"Type \"{type?.FullName ?? ""}\" is not supported here.");
-            return objType;
-        }
-
-        /// 
-        public IEntitySlim? Get(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id);
-            }
-        }
-
-        /// 
-        public IEntitySlim? Get(Guid key)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key);
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? Get(int id, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? Get(int id)
-            where T : IUmbracoEntity
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id);
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? Get(Guid key)
-            where T : IUmbracoEntity
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key);
-            }
-        }
-
-        /// 
-        public bool Exists(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Exists(id);
-            }
-        }
-
-        /// 
-        public bool Exists(Guid key)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Exists(key);
-            }
-        }
-
-
-        /// 
-        public virtual IEnumerable GetAll() where T : IUmbracoEntity
-            => GetAll(Array.Empty());
-
-        /// 
-        public virtual IEnumerable GetAll(params int[] ids)
-            where T : IUmbracoEntity
-        {
-            var entityType = typeof (T);
-            var objectType = GetObjectType(entityType);
-            var objectTypeId = objectType.GetGuid();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectTypeId, ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(UmbracoObjectTypes objectType)
-            => GetAll(objectType, Array.Empty());
-
-        /// 
-        public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids)
-        {
-            var entityType = objectType.GetClrType();
-            if (entityType == null)
-                throw new NotSupportedException($"Type \"{objectType}\" is not supported here.");
-
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType.GetGuid(), ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(Guid objectType)
-            => GetAll(objectType, Array.Empty());
-
-        /// 
-        public virtual IEnumerable GetAll(Guid objectType, params int[] ids)
-        {
-            var entityType = ObjectTypes.GetClrType(objectType);
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType, ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(params Guid[] keys)
-            where T : IUmbracoEntity
-        {
-            var entityType = typeof (T);
-            var objectType = GetObjectType(entityType);
-            var objectTypeId = objectType.GetGuid();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectTypeId, keys);
-            }
-        }
-
-        /// 
-        public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType.GetGuid(), keys);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys)
-        {
-            var entityType = ObjectTypes.GetClrType(objectType);
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType, keys);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetRootEntities(UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetByQuery(QueryRootEntity, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? GetParent(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
-                    return null;
-                return _entityRepository.Get(entity.ParentId);
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
-                    return null;
-                return _entityRepository.Get(entity.ParentId, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetChildren(int parentId)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == parentId);
-                return _entityRepository.GetByQuery(query);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == parentId);
-                return _entityRepository.GetByQuery(query, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetDescendants(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                var pathMatch = entity?.Path + ",";
-                var query = Query().Where(x => x.Path.StartsWith(pathMatch) && x.Id != id);
-                return _entityRepository.GetByQuery(query);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                if (entity is null)
-                {
-                    return Enumerable.Empty();
-                }
-                var query = Query().Where(x => x.Path.StartsWith(entity.Path) && x.Id != id);
-                return _entityRepository.GetByQuery(query, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedChildren(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == id && x.Trashed == false);
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var objectTypeGuid = objectType.GetGuid();
-                var query = Query();
-
-                if (id != Cms.Core.Constants.System.Root)
-                {
-                    // lookup the path so we can use it in the prefix query below
-                    var paths = _entityRepository.GetAllPaths(objectTypeGuid, id).ToArray();
-                    if (paths.Length == 0)
-                    {
-                        totalRecords = 0;
-                        return Enumerable.Empty();
-                    }
-                    var path = paths[0].Path;
-                    query.Where(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar));
-                }
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            totalRecords = 0;
-
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0)
-                return Enumerable.Empty();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var objectTypeGuid = objectType.GetGuid();
-                var query = Query();
-
-                if (idsA.All(x => x != Cms.Core.Constants.System.Root))
-                {
-                    var paths = _entityRepository.GetAllPaths(objectTypeGuid, idsA).ToArray();
-                    if (paths.Length == 0)
-                    {
-                        totalRecords = 0;
-                        return Enumerable.Empty();
-                    }
-                    var clauses = new List>>();
-                    foreach (var id in idsA)
-                    {
-                        // if the id is root then don't add any clauses
-                        if (id == Cms.Core.Constants.System.Root) continue;
-
-                        var entityPath = paths.FirstOrDefault(x => x.Id == id);
-                        if (entityPath == null) continue;
-
-                        var path = entityPath.Path;
-                        var qid = id;
-                        clauses.Add(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) || x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar));
-                    }
-                    query.WhereAny(clauses);
-                }
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null, bool includeTrashed = true)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query();
-                if (includeTrashed == false)
-                    query.Where(x => x.Trashed == false);
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetObjectType(id);
-            }
-        }
-
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(Guid key)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetObjectType(key);
-            }
-        }
-
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity)
-        {
-            return entity is IEntitySlim light
-                ? ObjectTypes.GetUmbracoObjectType(light.NodeObjectType)
-                : GetObjectType(entity.Id);
-        }
-
-        /// 
-        public virtual Type? GetEntityType(int id)
-        {
-            var objectType = GetObjectType(id);
-            return objectType.GetClrType();
-        }
-
-        /// 
-        public Attempt GetId(Guid key, UmbracoObjectTypes objectType)
-        {
-            return _idKeyMap.GetIdForKey(key, objectType);
-        }
-
-        /// 
-        public Attempt GetId(Udi udi)
-        {
-            return _idKeyMap.GetIdForUdi(udi);
-        }
-
-        /// 
-        public Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType)
-        {
-            return _idKeyMap.GetKeyForId(id, umbracoObjectType);
-        }
-
-        /// 
-        public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAllPaths(objectType.GetGuid(), ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAllPaths(objectType.GetGuid(), keys);
-            }
-        }
-
-        /// 
-        public int ReserveId(Guid key)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.ReserveId(key);
-            }
+            return _entityRepository.Get(id);
         }
     }
+
+    /// 
+    public IEntitySlim? Get(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(key);
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? Get(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(id, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(key, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? Get(int id)
+        where T : IUmbracoEntity
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(id);
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? Get(Guid key)
+        where T : IUmbracoEntity
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(key);
+        }
+    }
+
+    /// 
+    public bool Exists(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Exists(id);
+        }
+    }
+
+    /// 
+    public bool Exists(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Exists(key);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll()
+        where T : IUmbracoEntity
+        => GetAll(Array.Empty());
+
+    /// 
+    public virtual IEnumerable GetAll(params int[] ids)
+        where T : IUmbracoEntity
+    {
+        Type entityType = typeof(T);
+        UmbracoObjectTypes objectType = GetObjectType(entityType);
+        Guid objectTypeId = objectType.GetGuid();
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectTypeId, ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(UmbracoObjectTypes objectType)
+        => GetAll(objectType, Array.Empty());
+
+    /// 
+    public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids)
+    {
+        Type? entityType = objectType.GetClrType();
+        if (entityType == null)
+        {
+            throw new NotSupportedException($"Type \"{objectType}\" is not supported here.");
+        }
+
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType.GetGuid(), ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(Guid objectType)
+        => GetAll(objectType, Array.Empty());
+
+    /// 
+    public virtual IEnumerable GetAll(Guid objectType, params int[] ids)
+    {
+        Type? entityType = ObjectTypes.GetClrType(objectType);
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType, ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(params Guid[] keys)
+        where T : IUmbracoEntity
+    {
+        Type entityType = typeof(T);
+        UmbracoObjectTypes objectType = GetObjectType(entityType);
+        Guid objectTypeId = objectType.GetGuid();
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectTypeId, keys);
+        }
+    }
+
+    /// 
+    public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys)
+    {
+        Type? entityType = objectType.GetClrType();
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType.GetGuid(), keys);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys)
+    {
+        Type? entityType = ObjectTypes.GetClrType(objectType);
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType, keys);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetRootEntities(UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetByQuery(QueryRootEntity, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? GetParent(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
+            {
+                return null;
+            }
+
+            return _entityRepository.Get(entity.ParentId);
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
+            {
+                return null;
+            }
+
+            return _entityRepository.Get(entity.ParentId, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetChildren(int parentId)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query().Where(x => x.ParentId == parentId);
+            return _entityRepository.GetByQuery(query);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query().Where(x => x.ParentId == parentId);
+            return _entityRepository.GetByQuery(query, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetDescendants(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            var pathMatch = entity?.Path + ",";
+            IQuery query = Query()
+                .Where(x => x.Path.StartsWith(pathMatch) && x.Id != id);
+            return _entityRepository.GetByQuery(query);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null)
+            {
+                return Enumerable.Empty();
+            }
+
+            IQuery query = Query()
+                .Where(x => x.Path.StartsWith(entity.Path) && x.Id != id);
+            return _entityRepository.GetByQuery(query, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedChildren(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false);
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            Guid objectTypeGuid = objectType.GetGuid();
+            IQuery query = Query();
+
+            if (id != Constants.System.Root)
+            {
+                // lookup the path so we can use it in the prefix query below
+                TreeEntityPath[] paths = _entityRepository.GetAllPaths(objectTypeGuid, id).ToArray();
+                if (paths.Length == 0)
+                {
+                    totalRecords = 0;
+                    return Enumerable.Empty();
+                }
+
+                var path = paths[0].Path;
+                query.Where(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar));
+            }
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(
+        IEnumerable ids,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        totalRecords = 0;
+
+        var idsA = ids.ToArray();
+        if (idsA.Length == 0)
+        {
+            return Enumerable.Empty();
+        }
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            Guid objectTypeGuid = objectType.GetGuid();
+            IQuery query = Query();
+
+            if (idsA.All(x => x != Constants.System.Root))
+            {
+                TreeEntityPath[] paths = _entityRepository.GetAllPaths(objectTypeGuid, idsA).ToArray();
+                if (paths.Length == 0)
+                {
+                    totalRecords = 0;
+                    return Enumerable.Empty();
+                }
+
+                var clauses = new List>>();
+                foreach (var id in idsA)
+                {
+                    // if the id is root then don't add any clauses
+                    if (id == Constants.System.Root)
+                    {
+                        continue;
+                    }
+
+                    TreeEntityPath? entityPath = paths.FirstOrDefault(x => x.Id == id);
+                    if (entityPath == null)
+                    {
+                        continue;
+                    }
+
+                    var path = entityPath.Path;
+                    var qid = id;
+                    clauses.Add(x =>
+                        x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) ||
+                        x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar));
+                }
+
+                query.WhereAny(clauses);
+            }
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null,
+        bool includeTrashed = true)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query();
+            if (includeTrashed == false)
+            {
+                query.Where(x => x.Trashed == false);
+            }
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetObjectType(id);
+        }
+    }
+
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(Guid key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetObjectType(key);
+        }
+    }
+
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) =>
+        entity is IEntitySlim light
+            ? ObjectTypes.GetUmbracoObjectType(light.NodeObjectType)
+            : GetObjectType(entity.Id);
+
+    /// 
+    public virtual Type? GetEntityType(int id)
+    {
+        UmbracoObjectTypes objectType = GetObjectType(id);
+        return objectType.GetClrType();
+    }
+
+    /// 
+    public Attempt GetId(Guid key, UmbracoObjectTypes objectType) => _idKeyMap.GetIdForKey(key, objectType);
+
+    /// 
+    public Attempt GetId(Udi udi) => _idKeyMap.GetIdForUdi(udi);
+
+    /// 
+    public Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType) =>
+        _idKeyMap.GetKeyForId(id, umbracoObjectType);
+
+    /// 
+    public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids)
+    {
+        Type? entityType = objectType.GetClrType();
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAllPaths(objectType.GetGuid(), ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys)
+    {
+        Type? entityType = objectType.GetClrType();
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAllPaths(objectType.GetGuid(), keys);
+        }
+    }
+
+    /// 
+    public int ReserveId(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.ReserveId(key);
+        }
+    }
+
+    // gets the object type, throws if not supported
+    private UmbracoObjectTypes GetObjectType(Type? type)
+    {
+        if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out UmbracoObjectTypes objType))
+        {
+            throw new NotSupportedException($"Type \"{type?.FullName ?? ""}\" is not supported here.");
+        }
+
+        return objType;
+    }
 }
diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs
index 00f9dc2a18..60ad1f10ba 100644
--- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs
+++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Net;
 using System.Xml.Linq;
 using Umbraco.Cms.Core.Models;
@@ -11,685 +8,745 @@ using Umbraco.Cms.Core.Serialization;
 using Umbraco.Cms.Core.Strings;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Serializes entities to XML
+/// 
+internal class EntityXmlSerializer : IEntityXmlSerializer
 {
-    /// 
-    /// Serializes entities to XML
-    /// 
-    internal class EntityXmlSerializer : IEntityXmlSerializer
+    private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
+    private readonly IContentService _contentService;
+    private readonly IContentTypeService _contentTypeService;
+    private readonly IDataTypeService _dataTypeService;
+    private readonly ILocalizationService _localizationService;
+    private readonly IMediaService _mediaService;
+    private readonly PropertyEditorCollection _propertyEditors;
+    private readonly IShortStringHelper _shortStringHelper;
+    private readonly UrlSegmentProviderCollection _urlSegmentProviders;
+    private readonly IUserService _userService;
+
+    public EntityXmlSerializer(
+        IContentService contentService,
+        IMediaService mediaService,
+        IDataTypeService dataTypeService,
+        IUserService userService,
+        ILocalizationService localizationService,
+        IContentTypeService contentTypeService,
+        UrlSegmentProviderCollection urlSegmentProviders,
+        IShortStringHelper shortStringHelper,
+        PropertyEditorCollection propertyEditors,
+        IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
     {
-        private readonly IContentTypeService _contentTypeService;
-        private readonly IMediaService _mediaService;
-        private readonly IContentService _contentService;
-        private readonly IDataTypeService _dataTypeService;
-        private readonly IUserService _userService;
-        private readonly ILocalizationService _localizationService;
-        private readonly UrlSegmentProviderCollection _urlSegmentProviders;
-        private readonly IShortStringHelper _shortStringHelper;
-        private readonly PropertyEditorCollection _propertyEditors;
-        private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
+        _contentTypeService = contentTypeService;
+        _mediaService = mediaService;
+        _contentService = contentService;
+        _dataTypeService = dataTypeService;
+        _userService = userService;
+        _localizationService = localizationService;
+        _urlSegmentProviders = urlSegmentProviders;
+        _shortStringHelper = shortStringHelper;
+        _propertyEditors = propertyEditors;
+        _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
+    }
 
-        public EntityXmlSerializer(
-            IContentService contentService,
-            IMediaService mediaService,
-            IDataTypeService dataTypeService,
-            IUserService userService,
-            ILocalizationService localizationService,
-            IContentTypeService contentTypeService,
-            UrlSegmentProviderCollection urlSegmentProviders,
-            IShortStringHelper shortStringHelper,
-            PropertyEditorCollection propertyEditors,
-            IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
+    /// 
+    ///     Exports an IContent item as an XElement.
+    /// 
+    public XElement Serialize(
+        IContent content,
+        bool published,
+        bool withDescendants = false) // TODO: take care of usage! only used for the packager
+    {
+        if (content == null)
         {
-            _contentTypeService = contentTypeService;
-            _mediaService = mediaService;
-            _contentService = contentService;
-            _dataTypeService = dataTypeService;
-            _userService = userService;
-            _localizationService = localizationService;
-            _urlSegmentProviders = urlSegmentProviders;
-            _shortStringHelper = shortStringHelper;
-            _propertyEditors = propertyEditors;
-            _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
+            throw new ArgumentNullException(nameof(content));
         }
 
-        /// 
-        /// Exports an IContent item as an XElement.
-        /// 
-        public XElement Serialize(IContent content,
-            bool published,
-            bool withDescendants = false) // TODO: take care of usage! only used for the packager
+        var nodeName = content.ContentType.Alias.ToSafeAlias(_shortStringHelper);
+
+        XElement xml = SerializeContentBase(content, content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders), nodeName, published);
+
+        xml.Add(new XAttribute("nodeType", content.ContentType.Id));
+        xml.Add(new XAttribute("nodeTypeAlias", content.ContentType.Alias));
+
+        xml.Add(new XAttribute("creatorName", content.GetCreatorProfile(_userService)?.Name ?? "??"));
+
+        // xml.Add(new XAttribute("creatorID", content.CreatorId));
+        xml.Add(new XAttribute("writerName", content.GetWriterProfile(_userService)?.Name ?? "??"));
+        xml.Add(new XAttribute("writerID", content.WriterId));
+
+        xml.Add(new XAttribute("template", content.TemplateId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty));
+
+        xml.Add(new XAttribute("isPublished", content.Published));
+
+        if (withDescendants)
         {
-            if (content == null) throw new ArgumentNullException(nameof(content));
-
-            var nodeName = content.ContentType.Alias.ToSafeAlias(_shortStringHelper);
-
-            var xml = SerializeContentBase(content, content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders), nodeName, published);
-
-            xml.Add(new XAttribute("nodeType", content.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", content.ContentType.Alias));
-
-            xml.Add(new XAttribute("creatorName", content.GetCreatorProfile(_userService)?.Name ?? "??"));
-            //xml.Add(new XAttribute("creatorID", content.CreatorId));
-            xml.Add(new XAttribute("writerName", content.GetWriterProfile(_userService)?.Name ?? "??"));
-            xml.Add(new XAttribute("writerID", content.WriterId));
-
-            xml.Add(new XAttribute("template", content.TemplateId?.ToString(CultureInfo.InvariantCulture) ?? ""));
-
-            xml.Add(new XAttribute("isPublished", content.Published));
-
-            if (withDescendants)
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
             {
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while(page * pageSize < total)
+                IEnumerable children =
+                    _contentService.GetPagedChildren(content.Id, page++, pageSize, out total);
+                SerializeChildren(children, xml, published);
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports an IMedia item as an XElement.
+    /// 
+    public XElement Serialize(
+        IMedia media,
+        bool withDescendants = false,
+        Action? onMediaItemSerialized = null)
+    {
+        if (_mediaService == null)
+        {
+            throw new ArgumentNullException(nameof(_mediaService));
+        }
+
+        if (_dataTypeService == null)
+        {
+            throw new ArgumentNullException(nameof(_dataTypeService));
+        }
+
+        if (_userService == null)
+        {
+            throw new ArgumentNullException(nameof(_userService));
+        }
+
+        if (_localizationService == null)
+        {
+            throw new ArgumentNullException(nameof(_localizationService));
+        }
+
+        if (media == null)
+        {
+            throw new ArgumentNullException(nameof(media));
+        }
+
+        if (_urlSegmentProviders == null)
+        {
+            throw new ArgumentNullException(nameof(_urlSegmentProviders));
+        }
+
+        var nodeName = media.ContentType.Alias.ToSafeAlias(_shortStringHelper);
+
+        const bool published = false; // always false for media
+        var urlValue = media.GetUrlSegment(_shortStringHelper, _urlSegmentProviders);
+        XElement xml = SerializeContentBase(media, urlValue, nodeName, published);
+
+        xml.Add(new XAttribute("nodeType", media.ContentType.Id));
+        xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias));
+
+        // xml.Add(new XAttribute("creatorName", media.GetCreatorProfile(userService).Name));
+        // xml.Add(new XAttribute("creatorID", media.CreatorId));
+        xml.Add(new XAttribute("writerName", media.GetWriterProfile(_userService)?.Name ?? string.Empty));
+        xml.Add(new XAttribute("writerID", media.WriterId));
+        xml.Add(new XAttribute("udi", media.GetUdi()));
+
+        // xml.Add(new XAttribute("template", 0)); // no template for media
+        onMediaItemSerialized?.Invoke(media, xml);
+
+        if (withDescendants)
+        {
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
+            {
+                IEnumerable children = _mediaService.GetPagedChildren(media.Id, page++, pageSize, out total);
+                SerializeChildren(children, xml, onMediaItemSerialized);
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports an IMember item as an XElement.
+    /// 
+    public XElement Serialize(IMember member)
+    {
+        var nodeName = member.ContentType.Alias.ToSafeAlias(_shortStringHelper);
+
+        const bool published = false; // always false for member
+        XElement xml = SerializeContentBase(member, string.Empty, nodeName, published);
+
+        xml.Add(new XAttribute("nodeType", member.ContentType.Id));
+        xml.Add(new XAttribute("nodeTypeAlias", member.ContentType.Alias));
+
+        // what about writer/creator/version?
+        xml.Add(new XAttribute("loginName", member.Username));
+        xml.Add(new XAttribute("email", member.Email));
+        xml.Add(new XAttribute("icon", member.ContentType.Icon!));
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of Data Types
+    /// 
+    /// List of data types to export
+    ///  containing the xml representation of the IDataTypeDefinition objects
+    public XElement Serialize(IEnumerable dataTypeDefinitions)
+    {
+        var container = new XElement("DataTypes");
+        foreach (IDataType dataTypeDefinition in dataTypeDefinitions)
+        {
+            container.Add(Serialize(dataTypeDefinition));
+        }
+
+        return container;
+    }
+
+    public XElement Serialize(IDataType dataType)
+    {
+        var xml = new XElement("DataType");
+        xml.Add(new XAttribute("Name", dataType.Name!));
+
+        // The 'ID' when exporting is actually the property editor alias (in pre v7 it was the IDataType GUID id)
+        xml.Add(new XAttribute("Id", dataType.EditorAlias));
+        xml.Add(new XAttribute("Definition", dataType.Key));
+        xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString()));
+        xml.Add(new XAttribute("Configuration", _configurationEditorJsonSerializer.Serialize(dataType.Configuration)));
+
+        var folderNames = string.Empty;
+        var folderKeys = string.Empty;
+        if (dataType.Level != 1)
+        {
+            // get URL encoded folder names
+            IOrderedEnumerable folders = _dataTypeService.GetContainers(dataType)
+                .OrderBy(x => x.Level);
+
+            folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
+            folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
+        }
+
+        if (string.IsNullOrWhiteSpace(folderNames) == false)
+        {
+            xml.Add(new XAttribute("Folders", folderNames));
+            xml.Add(new XAttribute("FolderKeys", folderKeys));
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of dictionary items to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem objects
+    public XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true)
+    {
+        var xml = new XElement("DictionaryItems");
+        foreach (IDictionaryItem item in dictionaryItem)
+        {
+            xml.Add(Serialize(item, includeChildren));
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a single  item to xml as an 
+    /// 
+    /// Dictionary Item to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem object
+    public XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren)
+    {
+        XElement xml = Serialize(dictionaryItem);
+
+        if (includeChildren)
+        {
+            IEnumerable? children = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key);
+            if (children is not null)
+            {
+                foreach (IDictionaryItem child in children)
                 {
-                    var children = _contentService.GetPagedChildren(content.Id, page++, pageSize, out total);
-                    SerializeChildren(children, xml, published);
-                }
-
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports an IMedia item as an XElement.
-        /// 
-        public XElement Serialize(
-            IMedia media,
-            bool withDescendants = false,
-            Action? onMediaItemSerialized = null)
-        {
-            if (_mediaService == null) throw new ArgumentNullException(nameof(_mediaService));
-            if (_dataTypeService == null) throw new ArgumentNullException(nameof(_dataTypeService));
-            if (_userService == null) throw new ArgumentNullException(nameof(_userService));
-            if (_localizationService == null) throw new ArgumentNullException(nameof(_localizationService));
-            if (media == null) throw new ArgumentNullException(nameof(media));
-            if (_urlSegmentProviders == null) throw new ArgumentNullException(nameof(_urlSegmentProviders));
-
-            var nodeName = media.ContentType.Alias.ToSafeAlias(_shortStringHelper);
-
-            const bool published = false; // always false for media
-            string? urlValue = media.GetUrlSegment(_shortStringHelper, _urlSegmentProviders);
-            XElement xml = SerializeContentBase(media, urlValue, nodeName, published);
-
-
-            xml.Add(new XAttribute("nodeType", media.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias));
-
-            //xml.Add(new XAttribute("creatorName", media.GetCreatorProfile(userService).Name));
-            //xml.Add(new XAttribute("creatorID", media.CreatorId));
-            xml.Add(new XAttribute("writerName", media.GetWriterProfile(_userService)?.Name ?? string.Empty));
-            xml.Add(new XAttribute("writerID", media.WriterId));
-            xml.Add(new XAttribute("udi", media.GetUdi()));
-
-            //xml.Add(new XAttribute("template", 0)); // no template for media
-
-            onMediaItemSerialized?.Invoke(media, xml);
-
-            if (withDescendants)
-            {
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while (page * pageSize < total)
-                {
-                    var children = _mediaService.GetPagedChildren(media.Id, page++, pageSize, out total);
-                    SerializeChildren(children, xml, onMediaItemSerialized);
-                }
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports an IMember item as an XElement.
-        /// 
-        public XElement Serialize(IMember member)
-        {
-            var nodeName = member.ContentType.Alias.ToSafeAlias(_shortStringHelper);
-
-            const bool published = false; // always false for member
-            var xml = SerializeContentBase(member, "", nodeName, published);
-
-            xml.Add(new XAttribute("nodeType", member.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", member.ContentType.Alias));
-
-            // what about writer/creator/version?
-
-            xml.Add(new XAttribute("loginName", member.Username!));
-            xml.Add(new XAttribute("email", member.Email!));
-            xml.Add(new XAttribute("icon", member.ContentType.Icon!));
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of Data Types
-        /// 
-        /// List of data types to export
-        ///  containing the xml representation of the IDataTypeDefinition objects
-        public XElement Serialize(IEnumerable dataTypeDefinitions)
-        {
-            var container = new XElement("DataTypes");
-            foreach (var dataTypeDefinition in dataTypeDefinitions)
-            {
-                container.Add(Serialize(dataTypeDefinition));
-            }
-            return container;
-        }
-
-        public XElement Serialize(IDataType dataType)
-        {
-            var xml = new XElement("DataType");
-            xml.Add(new XAttribute("Name", dataType.Name!));
-            //The 'ID' when exporting is actually the property editor alias (in pre v7 it was the IDataType GUID id)
-            xml.Add(new XAttribute("Id", dataType.EditorAlias));
-            xml.Add(new XAttribute("Definition", dataType.Key));
-            xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString()));
-            xml.Add(new XAttribute("Configuration", _configurationEditorJsonSerializer.Serialize(dataType.Configuration)));
-
-            var folderNames = string.Empty;
-            var folderKeys = string.Empty;
-            if (dataType.Level != 1)
-            {
-                //get URL encoded folder names
-                IOrderedEnumerable folders = _dataTypeService.GetContainers(dataType)
-                    .OrderBy(x => x.Level);
-
-                folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
-                folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
-            }
-
-            if (string.IsNullOrWhiteSpace(folderNames) == false)
-            {
-                xml.Add(new XAttribute("Folders", folderNames));
-                xml.Add(new XAttribute("FolderKeys", folderKeys));
-            }
-
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of dictionary items to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem objects
-        public XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true)
-        {
-            var xml = new XElement("DictionaryItems");
-            foreach (var item in dictionaryItem)
-            {
-                xml.Add(Serialize(item, includeChildren));
-            }
-            return xml;
-        }
-
-        /// 
-        /// Exports a single  item to xml as an 
-        /// 
-        /// Dictionary Item to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem object
-        public XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren)
-        {
-            var xml = Serialize(dictionaryItem);
-
-            if (includeChildren)
-            {
-                var children = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key);
-                if (children is not null)
-                {
-                    foreach (var child in children)
-                    {
-                        xml.Add(Serialize(child, true));
-                    }
-                }
-            }
-
-            return xml;
-        }
-
-        private XElement Serialize(IDictionaryItem dictionaryItem)
-        {
-            var xml = new XElement("DictionaryItem",
-                new XAttribute("Key", dictionaryItem.Key),
-                new XAttribute("Name", dictionaryItem.ItemKey));
-
-            foreach (IDictionaryTranslation translation in dictionaryItem.Translations!)
-            {
-                xml.Add(new XElement("Value",
-                    new XAttribute("LanguageId", translation.Language!.Id),
-                    new XAttribute("LanguageCultureAlias", translation.Language.IsoCode),
-                    new XCData(translation.Value!)));
-            }
-
-            return xml;
-        }
-
-        public XElement Serialize(IStylesheet stylesheet, bool includeProperties)
-        {
-            var xml = new XElement("Stylesheet",
-                new XElement("Name", stylesheet.Alias),
-                new XElement("FileName", stylesheet.Path),
-                new XElement("Content", new XCData(stylesheet.Content!)));
-
-            if (!includeProperties)
-            {
-                return xml;
-            }
-
-            var props = new XElement("Properties");
-            xml.Add(props);
-
-            if (stylesheet.Properties is not null)
-            {
-                foreach (var prop in stylesheet.Properties)
-                {
-                    props.Add(new XElement("Property",
-                        new XElement("Name", prop.Name),
-                        new XElement("Alias", prop.Alias),
-                        new XElement("Value", prop.Value)));
-                }
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of Languages to export
-        ///  containing the xml representation of the ILanguage objects
-        public XElement Serialize(IEnumerable languages)
-        {
-            var xml = new XElement("Languages");
-            foreach (var language in languages)
-            {
-                xml.Add(Serialize(language));
-            }
-            return xml;
-        }
-
-        public XElement Serialize(ILanguage language)
-        {
-            var xml = new XElement("Language",
-                new XAttribute("Id", language.Id),
-                new XAttribute("CultureAlias", language.IsoCode),
-                new XAttribute("FriendlyName", language.CultureName!));
-
-            return xml;
-        }
-
-        public XElement Serialize(ITemplate template)
-        {
-            var xml = new XElement("Template");
-            xml.Add(new XElement("Name", template.Name));
-            xml.Add(new XElement("Key", template.Key));
-            xml.Add(new XElement("Alias", template.Alias));
-            xml.Add(new XElement("Design", new XCData(template.Content!)));
-
-            if (template is Template concreteTemplate && concreteTemplate.MasterTemplateId != null)
-            {
-                if (concreteTemplate.MasterTemplateId.IsValueCreated &&
-                    concreteTemplate.MasterTemplateId.Value != default)
-                {
-                    xml.Add(new XElement("Master", concreteTemplate.MasterTemplateId.ToString()));
-                    xml.Add(new XElement("MasterAlias", concreteTemplate.MasterTemplateAlias));
-                }
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// 
-        /// 
-        public XElement Serialize(IEnumerable templates)
-        {
-            var xml = new XElement("Templates");
-            foreach (var item in templates)
-            {
-                xml.Add(Serialize(item));
-            }
-            return xml;
-        }
-
-
-        public XElement Serialize(IMediaType mediaType)
-        {
-            var info = new XElement("Info",
-                                    new XElement("Name", mediaType.Name),
-                                    new XElement("Alias", mediaType.Alias),
-                                    new XElement("Key", mediaType.Key),
-                                    new XElement("Icon", mediaType.Icon),
-                                    new XElement("Thumbnail", mediaType.Thumbnail),
-                                    new XElement("Description", mediaType.Description),
-                                    new XElement("AllowAtRoot", mediaType.AllowedAsRoot.ToString()));
-
-            var masterContentType = mediaType.CompositionAliases().FirstOrDefault();
-            if (masterContentType != null)
-            {
-                info.Add(new XElement("Master", masterContentType));
-            }
-
-            var structure = new XElement("Structure");
-            if (mediaType.AllowedContentTypes is not null)
-            {
-                foreach (var allowedType in mediaType.AllowedContentTypes)
-                {
-                    structure.Add(new XElement("MediaType", allowedType.Alias));
-                }
-            }
-
-            var genericProperties = new XElement("GenericProperties", SerializePropertyTypes(mediaType.PropertyTypes, mediaType.PropertyGroups)); // actually, all of them
-
-            var tabs = new XElement("Tabs", SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups
-
-            var xml = new XElement("MediaType",
-                                   info,
-                                   structure,
-                                   genericProperties,
-                                   tabs);
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// Macros to export
-        ///  containing the xml representation of the IMacro objects
-        public XElement Serialize(IEnumerable macros)
-        {
-            var xml = new XElement("Macros");
-            foreach (var item in macros)
-            {
-                xml.Add(Serialize(item));
-            }
-            return xml;
-        }
-
-        public XElement Serialize(IMacro macro)
-        {
-            var xml = new XElement("macro");
-            xml.Add(new XElement("name", macro.Name));
-            xml.Add(new XElement("key", macro.Key));
-            xml.Add(new XElement("alias", macro.Alias));
-            xml.Add(new XElement("macroSource", macro.MacroSource));
-            xml.Add(new XElement("useInEditor", macro.UseInEditor.ToString()));
-            xml.Add(new XElement("dontRender", macro.DontRender.ToString()));
-            xml.Add(new XElement("refreshRate", macro.CacheDuration.ToString(CultureInfo.InvariantCulture)));
-            xml.Add(new XElement("cacheByMember", macro.CacheByMember.ToString()));
-            xml.Add(new XElement("cacheByPage", macro.CacheByPage.ToString()));
-
-            var properties = new XElement("properties");
-            foreach (var property in macro.Properties)
-            {
-                properties.Add(new XElement("property",
-                    new XAttribute("key", property.Key),
-                    new XAttribute("name", property.Name!),
-                    new XAttribute("alias", property.Alias),
-                    new XAttribute("sortOrder", property.SortOrder),
-                    new XAttribute("propertyType", property.EditorAlias)));
-            }
-            xml.Add(properties);
-
-            return xml;
-        }
-
-        public XElement Serialize(IContentType contentType)
-        {
-            var info = new XElement("Info",
-                                    new XElement("Name", contentType.Name),
-                                    new XElement("Alias", contentType.Alias),
-                                    new XElement("Key", contentType.Key),
-                                    new XElement("Icon", contentType.Icon),
-                                    new XElement("Thumbnail", contentType.Thumbnail),
-                                    new XElement("Description", contentType.Description),
-                                    new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()),
-                                    new XElement("IsListView", contentType.IsContainer.ToString()),
-                                    new XElement("IsElement", contentType.IsElement.ToString()),
-                                    new XElement("Variations", contentType.Variations.ToString()));
-
-            var masterContentType = contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId);
-            if (masterContentType != null)
-            {
-                info.Add(new XElement("Master", masterContentType.Alias));
-            }
-
-            var compositionsElement = new XElement("Compositions");
-            var compositions = contentType.ContentTypeComposition;
-            foreach (var composition in compositions)
-            {
-                compositionsElement.Add(new XElement("Composition", composition.Alias));
-            }
-            info.Add(compositionsElement);
-
-            var allowedTemplates = new XElement("AllowedTemplates");
-            if (contentType.AllowedTemplates is not null)
-            {
-                foreach (var template in contentType.AllowedTemplates)
-                {
-                    allowedTemplates.Add(new XElement("Template", template.Alias));
-                }
-            }
-
-            info.Add(allowedTemplates);
-
-            if (contentType.DefaultTemplate != null && contentType.DefaultTemplate.Id != 0)
-            {
-                info.Add(new XElement("DefaultTemplate", contentType.DefaultTemplate.Alias));
-            }
-            else
-            {
-                info.Add(new XElement("DefaultTemplate", ""));
-            }
-
-            var structure = new XElement("Structure");
-            if (contentType.AllowedContentTypes is not null)
-            {
-                foreach (var allowedType in contentType.AllowedContentTypes)
-                {
-                    structure.Add(new XElement("DocumentType", allowedType.Alias));
-                }
-            }
-
-            var genericProperties = new XElement("GenericProperties", SerializePropertyTypes(contentType.PropertyTypes, contentType.PropertyGroups)); // actually, all of them
-
-            var tabs = new XElement("Tabs", SerializePropertyGroups(contentType.PropertyGroups)); // TODO Rename to PropertyGroups
-
-            var xml = new XElement("DocumentType",
-                info,
-                structure,
-                genericProperties,
-                tabs);
-
-            if (contentType is IContentType withCleanup && withCleanup.HistoryCleanup is not null)
-            {
-                xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup));
-            }
-
-            var folderNames = string.Empty;
-            var folderKeys = string.Empty;
-            //don't add folders if this is a child doc type
-            if (contentType.Level != 1 && masterContentType == null)
-            {
-                //get URL encoded folder names
-                IOrderedEnumerable folders = _contentTypeService.GetContainers(contentType)
-                    .OrderBy(x => x.Level);
-
-                folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
-                folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
-            }
-
-            if (string.IsNullOrWhiteSpace(folderNames) == false)
-            {
-                xml.Add(new XAttribute("Folders", folderNames));
-                xml.Add(new XAttribute("FolderKeys", folderKeys));
-            }
-
-
-            return xml;
-        }
-
-        private IEnumerable SerializePropertyTypes(IEnumerable propertyTypes, IEnumerable propertyGroups)
-        {
-            foreach (var propertyType in propertyTypes)
-            {
-                var definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
-
-                var propertyGroup = propertyType.PropertyGroupId == null // true generic property
-                    ? null
-                    : propertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
-
-                XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
-                genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
-
-                yield return genericProperty;
-            }
-        }
-
-        private IEnumerable SerializePropertyGroups(IEnumerable propertyGroups)
-        {
-            foreach (var propertyGroup in propertyGroups)
-            {
-                yield return new XElement("Tab", // TODO Rename to PropertyGroup
-                    new XElement("Id", propertyGroup.Id),
-                    new XElement("Key", propertyGroup.Key),
-                    new XElement("Type", propertyGroup.Type.ToString()),
-                    new XElement("Caption", propertyGroup.Name), // TODO Rename to Name (same in PackageDataInstallation)
-                    new XElement("Alias", propertyGroup.Alias),
-                    new XElement("SortOrder", propertyGroup.SortOrder));
-            }
-        }
-
-        private XElement SerializePropertyType(IPropertyType propertyType, IDataType? definition, PropertyGroup? propertyGroup)
-            => new XElement("GenericProperty",
-                    new XElement("Name", propertyType.Name),
-                    new XElement("Alias", propertyType.Alias),
-                    new XElement("Key", propertyType.Key),
-                    new XElement("Type", propertyType.PropertyEditorAlias),
-                    definition is not null ? new XElement("Definition", definition.Key) : null,
-                    propertyGroup is not null ? new XElement("Tab", propertyGroup.Name, new XAttribute("Alias", propertyGroup.Alias)) : null, // TODO Replace with PropertyGroupAlias
-                    new XElement("SortOrder", propertyType.SortOrder),
-                    new XElement("Mandatory", propertyType.Mandatory.ToString()),
-                    new XElement("LabelOnTop", propertyType.LabelOnTop.ToString()),
-                    propertyType.MandatoryMessage != null ? new XElement("MandatoryMessage", propertyType.MandatoryMessage) : null,
-                    propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null,
-                    propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null,
-                    propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null);
-
-        private XElement SerializeCleanupPolicy(HistoryCleanup cleanupPolicy)
-        {
-            if (cleanupPolicy == null)
-            {
-                throw new ArgumentNullException(nameof(cleanupPolicy));
-            }
-
-            var element = new XElement("HistoryCleanupPolicy",
-                new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup));
-
-            if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue)
-            {
-                element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays));
-            }
-
-            if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue)
-            {
-                element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays));
-            }
-
-            return element;
-        }
-
-        // exports an IContentBase (IContent, IMedia or IMember) as an XElement.
-        private XElement SerializeContentBase(IContentBase contentBase, string? urlValue, string nodeName, bool published)
-        {
-            var xml = new XElement(nodeName,
-                new XAttribute("id", contentBase.Id.ToInvariantString()),
-                new XAttribute("key", contentBase.Key),
-                new XAttribute("parentID", (contentBase.Level > 1 ? contentBase.ParentId : -1).ToInvariantString()),
-                new XAttribute("level", contentBase.Level),
-                new XAttribute("creatorID", contentBase.CreatorId.ToInvariantString()),
-                new XAttribute("sortOrder", contentBase.SortOrder),
-                new XAttribute("createDate", contentBase.CreateDate.ToString("s")),
-                new XAttribute("updateDate", contentBase.UpdateDate.ToString("s")),
-                new XAttribute("nodeName", contentBase.Name!),
-                new XAttribute("urlName", urlValue!),
-                new XAttribute("path", contentBase.Path),
-                new XAttribute("isDoc", ""));
-
-
-            // Add culture specific node names
-            foreach (var culture in contentBase.AvailableCultures)
-            {
-                xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture)!));
-            }
-
-            foreach (var property in contentBase.Properties)
-                xml.Add(SerializeProperty(property, published));
-
-            return xml;
-        }
-
-        // exports a property as XElements.
-        private IEnumerable SerializeProperty(IProperty property, bool published)
-        {
-            var propertyType = property.PropertyType;
-
-            // get the property editor for this property and let it convert it to the xml structure
-            var propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
-            return propertyEditor == null
-                ? Array.Empty()
-                : propertyEditor.GetValueEditor().ConvertDbToXml(property, published);
-        }
-
-        // exports an IContent item descendants.
-        private void SerializeChildren(IEnumerable children, XElement xml, bool published)
-        {
-            foreach (var child in children)
-            {
-                // add the child xml
-                var childXml = Serialize(child, published);
-                xml.Add(childXml);
-
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while(page * pageSize < total)
-                {
-                    var grandChildren = _contentService.GetPagedChildren(child.Id, page++, pageSize, out total);
-                    // recurse
-                    SerializeChildren(grandChildren, childXml, published);
+                    xml.Add(Serialize(child, true));
                 }
             }
         }
 
-        // exports an IMedia item descendants.
-        private void SerializeChildren(IEnumerable children, XElement xml, Action? onMediaItemSerialized)
-        {
-            foreach (var child in children)
-            {
-                // add the child xml
-                var childXml = Serialize(child, onMediaItemSerialized: onMediaItemSerialized);
-                xml.Add(childXml);
+        return xml;
+    }
 
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while (page * pageSize < total)
-                {
-                    var grandChildren = _mediaService.GetPagedChildren(child.Id, page++, pageSize, out total);
-                    // recurse
-                    SerializeChildren(grandChildren, childXml, onMediaItemSerialized);
-                }
+    public XElement Serialize(IStylesheet stylesheet, bool includeProperties)
+    {
+        var xml = new XElement(
+            "Stylesheet",
+            new XElement("Name", stylesheet.Alias),
+            new XElement("FileName", stylesheet.Path),
+            new XElement("Content", new XCData(stylesheet.Content!)));
+
+        if (!includeProperties)
+        {
+            return xml;
+        }
+
+        var props = new XElement("Properties");
+        xml.Add(props);
+
+        if (stylesheet.Properties is not null)
+        {
+            foreach (IStylesheetProperty prop in stylesheet.Properties)
+            {
+                props.Add(new XElement(
+                    "Property",
+                    new XElement("Name", prop.Name),
+                    new XElement("Alias", prop.Alias),
+                    new XElement("Value", prop.Value)));
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of Languages to export
+    ///  containing the xml representation of the ILanguage objects
+    public XElement Serialize(IEnumerable languages)
+    {
+        var xml = new XElement("Languages");
+        foreach (ILanguage language in languages)
+        {
+            xml.Add(Serialize(language));
+        }
+
+        return xml;
+    }
+
+    public XElement Serialize(ILanguage language)
+    {
+        var xml = new XElement(
+            "Language",
+            new XAttribute("Id", language.Id),
+            new XAttribute("CultureAlias", language.IsoCode),
+            new XAttribute("FriendlyName", language.CultureName));
+
+        return xml;
+    }
+
+    public XElement Serialize(ITemplate template)
+    {
+        var xml = new XElement("Template");
+        xml.Add(new XElement("Name", template.Name));
+        xml.Add(new XElement("Key", template.Key));
+        xml.Add(new XElement("Alias", template.Alias));
+        xml.Add(new XElement("Design", new XCData(template.Content!)));
+
+        if (template is Template concreteTemplate && concreteTemplate.MasterTemplateId != null)
+        {
+            if (concreteTemplate.MasterTemplateId.IsValueCreated &&
+                concreteTemplate.MasterTemplateId.Value != default)
+            {
+                xml.Add(new XElement("Master", concreteTemplate.MasterTemplateId.ToString()));
+                xml.Add(new XElement("MasterAlias", concreteTemplate.MasterTemplateAlias));
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// 
+    /// 
+    public XElement Serialize(IEnumerable templates)
+    {
+        var xml = new XElement("Templates");
+        foreach (ITemplate item in templates)
+        {
+            xml.Add(Serialize(item));
+        }
+
+        return xml;
+    }
+
+    public XElement Serialize(IMediaType mediaType)
+    {
+        var info = new XElement(
+            "Info",
+            new XElement("Name", mediaType.Name),
+            new XElement("Alias", mediaType.Alias),
+            new XElement("Key", mediaType.Key),
+            new XElement("Icon", mediaType.Icon),
+            new XElement("Thumbnail", mediaType.Thumbnail),
+            new XElement("Description", mediaType.Description),
+            new XElement("AllowAtRoot", mediaType.AllowedAsRoot.ToString()));
+
+        var masterContentType = mediaType.CompositionAliases().FirstOrDefault();
+        if (masterContentType != null)
+        {
+            info.Add(new XElement("Master", masterContentType));
+        }
+
+        var structure = new XElement("Structure");
+        if (mediaType.AllowedContentTypes is not null)
+        {
+            foreach (ContentTypeSort allowedType in mediaType.AllowedContentTypes)
+            {
+                structure.Add(new XElement("MediaType", allowedType.Alias));
+            }
+        }
+
+        var genericProperties = new XElement(
+            "GenericProperties",
+            SerializePropertyTypes(mediaType.PropertyTypes, mediaType.PropertyGroups)); // actually, all of them
+
+        var tabs = new XElement(
+            "Tabs",
+            SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups
+
+        var xml = new XElement(
+            "MediaType",
+            info,
+            structure,
+            genericProperties,
+            tabs);
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// Macros to export
+    ///  containing the xml representation of the IMacro objects
+    public XElement Serialize(IEnumerable macros)
+    {
+        var xml = new XElement("Macros");
+        foreach (IMacro item in macros)
+        {
+            xml.Add(Serialize(item));
+        }
+
+        return xml;
+    }
+
+    public XElement Serialize(IMacro macro)
+    {
+        var xml = new XElement("macro");
+        xml.Add(new XElement("name", macro.Name));
+        xml.Add(new XElement("key", macro.Key));
+        xml.Add(new XElement("alias", macro.Alias));
+        xml.Add(new XElement("macroSource", macro.MacroSource));
+        xml.Add(new XElement("useInEditor", macro.UseInEditor.ToString()));
+        xml.Add(new XElement("dontRender", macro.DontRender.ToString()));
+        xml.Add(new XElement("refreshRate", macro.CacheDuration.ToString(CultureInfo.InvariantCulture)));
+        xml.Add(new XElement("cacheByMember", macro.CacheByMember.ToString()));
+        xml.Add(new XElement("cacheByPage", macro.CacheByPage.ToString()));
+
+        var properties = new XElement("properties");
+        foreach (IMacroProperty property in macro.Properties)
+        {
+            properties.Add(new XElement(
+                "property",
+                new XAttribute("key", property.Key),
+                new XAttribute("name", property.Name!),
+                new XAttribute("alias", property.Alias),
+                new XAttribute("sortOrder", property.SortOrder),
+                new XAttribute("propertyType", property.EditorAlias)));
+        }
+
+        xml.Add(properties);
+
+        return xml;
+    }
+
+    public XElement Serialize(IContentType contentType)
+    {
+        var info = new XElement(
+            "Info",
+            new XElement("Name", contentType.Name),
+            new XElement("Alias", contentType.Alias),
+            new XElement("Key", contentType.Key),
+            new XElement("Icon", contentType.Icon),
+            new XElement("Thumbnail", contentType.Thumbnail),
+            new XElement("Description", contentType.Description),
+            new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()),
+            new XElement("IsListView", contentType.IsContainer.ToString()),
+            new XElement("IsElement", contentType.IsElement.ToString()),
+            new XElement("Variations", contentType.Variations.ToString()));
+
+        IContentTypeComposition? masterContentType =
+            contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId);
+        if (masterContentType != null)
+        {
+            info.Add(new XElement("Master", masterContentType.Alias));
+        }
+
+        var compositionsElement = new XElement("Compositions");
+        IEnumerable compositions = contentType.ContentTypeComposition;
+        foreach (IContentTypeComposition composition in compositions)
+        {
+            compositionsElement.Add(new XElement("Composition", composition.Alias));
+        }
+
+        info.Add(compositionsElement);
+
+        var allowedTemplates = new XElement("AllowedTemplates");
+        if (contentType.AllowedTemplates is not null)
+        {
+            foreach (ITemplate template in contentType.AllowedTemplates)
+            {
+                allowedTemplates.Add(new XElement("Template", template.Alias));
+            }
+        }
+
+        info.Add(allowedTemplates);
+
+        if (contentType.DefaultTemplate != null && contentType.DefaultTemplate.Id != 0)
+        {
+            info.Add(new XElement("DefaultTemplate", contentType.DefaultTemplate.Alias));
+        }
+        else
+        {
+            info.Add(new XElement("DefaultTemplate", string.Empty));
+        }
+
+        var structure = new XElement("Structure");
+        if (contentType.AllowedContentTypes is not null)
+        {
+            foreach (ContentTypeSort allowedType in contentType.AllowedContentTypes)
+            {
+                structure.Add(new XElement("DocumentType", allowedType.Alias));
+            }
+        }
+
+        var genericProperties = new XElement(
+            "GenericProperties",
+            SerializePropertyTypes(contentType.PropertyTypes, contentType.PropertyGroups)); // actually, all of them
+
+        var tabs = new XElement(
+            "Tabs",
+            SerializePropertyGroups(contentType.PropertyGroups)); // TODO Rename to PropertyGroups
+
+        var xml = new XElement(
+            "DocumentType",
+            info,
+            structure,
+            genericProperties,
+            tabs);
+
+        if (contentType is IContentType withCleanup && withCleanup.HistoryCleanup is not null)
+        {
+            xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup));
+        }
+
+        var folderNames = string.Empty;
+        var folderKeys = string.Empty;
+
+        // don't add folders if this is a child doc type
+        if (contentType.Level != 1 && masterContentType == null)
+        {
+            // get URL encoded folder names
+            IOrderedEnumerable folders = _contentTypeService.GetContainers(contentType)
+                .OrderBy(x => x.Level);
+
+            folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
+            folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
+        }
+
+        if (string.IsNullOrWhiteSpace(folderNames) == false)
+        {
+            xml.Add(new XAttribute("Folders", folderNames));
+            xml.Add(new XAttribute("FolderKeys", folderKeys));
+        }
+
+        return xml;
+    }
+
+    private XElement Serialize(IDictionaryItem dictionaryItem)
+    {
+        var xml = new XElement(
+            "DictionaryItem",
+            new XAttribute("Key", dictionaryItem.Key),
+            new XAttribute("Name", dictionaryItem.ItemKey));
+
+        foreach (IDictionaryTranslation translation in dictionaryItem.Translations)
+        {
+            xml.Add(new XElement(
+                "Value",
+                new XAttribute("LanguageId", translation.Language!.Id),
+                new XAttribute("LanguageCultureAlias", translation.Language.IsoCode),
+                new XCData(translation.Value)));
+        }
+
+        return xml;
+    }
+
+    private IEnumerable SerializePropertyTypes(
+        IEnumerable propertyTypes,
+        IEnumerable propertyGroups)
+    {
+        foreach (IPropertyType propertyType in propertyTypes)
+        {
+            IDataType? definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
+
+            PropertyGroup? propertyGroup = propertyType.PropertyGroupId == null // true generic property
+                ? null
+                : propertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
+
+            XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
+            genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
+
+            yield return genericProperty;
+        }
+    }
+
+    private IEnumerable SerializePropertyGroups(IEnumerable propertyGroups)
+    {
+        foreach (PropertyGroup propertyGroup in propertyGroups)
+        {
+            yield return new XElement(
+                "Tab", // TODO Rename to PropertyGroup
+                new XElement("Id", propertyGroup.Id),
+                new XElement("Key", propertyGroup.Key),
+                new XElement("Type", propertyGroup.Type.ToString()),
+                new XElement("Caption", propertyGroup.Name), // TODO Rename to Name (same in PackageDataInstallation)
+                new XElement("Alias", propertyGroup.Alias),
+                new XElement("SortOrder", propertyGroup.SortOrder));
+        }
+    }
+
+    private XElement SerializePropertyType(IPropertyType propertyType, IDataType? definition, PropertyGroup? propertyGroup)
+        => new(
+            "GenericProperty",
+            new XElement("Name", propertyType.Name),
+            new XElement("Alias", propertyType.Alias),
+            new XElement("Key", propertyType.Key),
+            new XElement("Type", propertyType.PropertyEditorAlias),
+            definition is not null ? new XElement("Definition", definition.Key) : null,
+            propertyGroup is not null ? new XElement("Tab", propertyGroup.Name, new XAttribute("Alias", propertyGroup.Alias)) : null, // TODO Replace with PropertyGroupAlias
+            new XElement("SortOrder", propertyType.SortOrder),
+            new XElement("Mandatory", propertyType.Mandatory.ToString()),
+            new XElement("LabelOnTop", propertyType.LabelOnTop.ToString()),
+            propertyType.MandatoryMessage != null ? new XElement("MandatoryMessage", propertyType.MandatoryMessage) : null,
+            propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null,
+            propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null,
+            propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null);
+
+    private XElement SerializeCleanupPolicy(HistoryCleanup cleanupPolicy)
+    {
+        if (cleanupPolicy == null)
+        {
+            throw new ArgumentNullException(nameof(cleanupPolicy));
+        }
+
+        var element = new XElement(
+            "HistoryCleanupPolicy",
+            new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup));
+
+        if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue)
+        {
+            element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays));
+        }
+
+        if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue)
+        {
+            element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays));
+        }
+
+        return element;
+    }
+
+    // exports an IContentBase (IContent, IMedia or IMember) as an XElement.
+    private XElement SerializeContentBase(IContentBase contentBase, string? urlValue, string nodeName, bool published)
+    {
+        var xml = new XElement(
+            nodeName,
+            new XAttribute("id", contentBase.Id.ToInvariantString()),
+            new XAttribute("key", contentBase.Key),
+            new XAttribute("parentID", (contentBase.Level > 1 ? contentBase.ParentId : -1).ToInvariantString()),
+            new XAttribute("level", contentBase.Level),
+            new XAttribute("creatorID", contentBase.CreatorId.ToInvariantString()),
+            new XAttribute("sortOrder", contentBase.SortOrder),
+            new XAttribute("createDate", contentBase.CreateDate.ToString("s")),
+            new XAttribute("updateDate", contentBase.UpdateDate.ToString("s")),
+            new XAttribute("nodeName", contentBase.Name!),
+            new XAttribute("urlName", urlValue!),
+            new XAttribute("path", contentBase.Path),
+            new XAttribute("isDoc", string.Empty));
+
+        // Add culture specific node names
+        foreach (var culture in contentBase.AvailableCultures)
+        {
+            xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture)!));
+        }
+
+        foreach (IProperty property in contentBase.Properties)
+        {
+            xml.Add(SerializeProperty(property, published));
+        }
+
+        return xml;
+    }
+
+    // exports a property as XElements.
+    private IEnumerable SerializeProperty(IProperty property, bool published)
+    {
+        IPropertyType propertyType = property.PropertyType;
+
+        // get the property editor for this property and let it convert it to the xml structure
+        IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
+        return propertyEditor == null
+            ? Array.Empty()
+            : propertyEditor.GetValueEditor().ConvertDbToXml(property, published);
+    }
+
+    // exports an IContent item descendants.
+    private void SerializeChildren(IEnumerable children, XElement xml, bool published)
+    {
+        foreach (IContent child in children)
+        {
+            // add the child xml
+            XElement childXml = Serialize(child, published);
+            xml.Add(childXml);
+
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
+            {
+                IEnumerable grandChildren =
+                    _contentService.GetPagedChildren(child.Id, page++, pageSize, out total);
+
+                // recurse
+                SerializeChildren(grandChildren, childXml, published);
+            }
+        }
+    }
+
+    // exports an IMedia item descendants.
+    private void SerializeChildren(IEnumerable children, XElement xml, Action? onMediaItemSerialized)
+    {
+        foreach (IMedia child in children)
+        {
+            // add the child xml
+            XElement childXml = Serialize(child, onMediaItemSerialized: onMediaItemSerialized);
+            xml.Add(childXml);
+
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
+            {
+                IEnumerable grandChildren =
+                    _mediaService.GetPagedChildren(child.Id, page++, pageSize, out total);
+
+                // recurse
+                SerializeChildren(grandChildren, childXml, onMediaItemSerialized);
             }
         }
     }
diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs
index 9fba54ed16..061e0b93aa 100644
--- a/src/Umbraco.Core/Services/ExternalLoginService.cs
+++ b/src/Umbraco.Core/Services/ExternalLoginService.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
@@ -11,78 +7,77 @@ using Umbraco.Cms.Core.Security;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class ExternalLoginService : RepositoryService, IExternalLoginWithKeyService
 {
-    public class ExternalLoginService : RepositoryService, IExternalLoginWithKeyService
+    private readonly IExternalLoginWithKeyRepository _externalLoginRepository;
+
+    public ExternalLoginService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IExternalLoginWithKeyRepository externalLoginRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _externalLoginRepository = externalLoginRepository;
+
+    public IEnumerable Find(string loginProvider, string providerKey)
     {
-        private readonly IExternalLoginWithKeyRepository _externalLoginRepository;
-
-        public ExternalLoginService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IExternalLoginWithKeyRepository externalLoginRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _externalLoginRepository = externalLoginRepository;
-        }
-
-        /// 
-        public IEnumerable GetExternalLogins(Guid userOrMemberKey)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
-                    .ToList();
-            }
-        }
-
-        /// 
-        public IEnumerable GetExternalLoginTokens(Guid userOrMemberKey)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
-                    .ToList();
-            }
-        }
-
-        /// 
-        public IEnumerable Find(string loginProvider, string providerKey)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query()
+            return _externalLoginRepository.Get(Query()
                     .Where(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider))
-                    .ToList();
-            }
+                .ToList();
         }
+    }
 
-        /// 
-        public void Save(Guid userOrMemberKey, IEnumerable logins)
+    /// 
+    public IEnumerable GetExternalLogins(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.Save(userOrMemberKey, logins);
-                scope.Complete();
-            }
+            return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
+                .ToList();
         }
+    }
 
-        /// 
-        public void Save(Guid userOrMemberKey, IEnumerable tokens)
+    /// 
+    public IEnumerable GetExternalLoginTokens(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.Save(userOrMemberKey, tokens);
-                scope.Complete();
-            }
+            return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
+                .ToList();
         }
+    }
 
-        /// 
-        public void DeleteUserLogins(Guid userOrMemberKey)
+    /// 
+    public void Save(Guid userOrMemberKey, IEnumerable logins)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.DeleteUserLogins(userOrMemberKey);
-                scope.Complete();
-            }
+            _externalLoginRepository.Save(userOrMemberKey, logins);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void Save(Guid userOrMemberKey, IEnumerable tokens)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _externalLoginRepository.Save(userOrMemberKey, tokens);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteUserLogins(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _externalLoginRepository.DeleteUserLogins(userOrMemberKey);
+            scope.Complete();
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs
index d692765620..758df3d102 100644
--- a/src/Umbraco.Core/Services/FileService.cs
+++ b/src/Umbraco.Core/Services/FileService.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
 using System.Text.RegularExpressions;
 using Microsoft.Extensions.FileProviders;
 using Microsoft.Extensions.Logging;
@@ -16,1002 +12,1038 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Strings;
 using Umbraco.Extensions;
+using File = System.IO.File;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the File Service, which is an easy access to operations involving  objects like
+///     Scripts, Stylesheets and Templates
+/// 
+public class FileService : RepositoryService, IFileService
 {
-    /// 
-    /// Represents the File Service, which is an easy access to operations involving  objects like Scripts, Stylesheets and Templates
-    /// 
-    public class FileService : RepositoryService, IFileService
+    private const string PartialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage";
+    private const string PartialViewMacroHeader = "@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage";
+    private readonly IAuditRepository _auditRepository;
+    private readonly GlobalSettings _globalSettings;
+    private readonly IHostingEnvironment _hostingEnvironment;
+    private readonly IPartialViewMacroRepository _partialViewMacroRepository;
+    private readonly IPartialViewRepository _partialViewRepository;
+    private readonly IScriptRepository _scriptRepository;
+    private readonly IShortStringHelper _shortStringHelper;
+    private readonly IStylesheetRepository _stylesheetRepository;
+    private readonly ITemplateRepository _templateRepository;
+
+    public FileService(
+        ICoreScopeProvider uowProvider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IStylesheetRepository stylesheetRepository,
+        IScriptRepository scriptRepository,
+        ITemplateRepository templateRepository,
+        IPartialViewRepository partialViewRepository,
+        IPartialViewMacroRepository partialViewMacroRepository,
+        IAuditRepository auditRepository,
+        IShortStringHelper shortStringHelper,
+        IOptions globalSettings,
+        IHostingEnvironment hostingEnvironment)
+        : base(uowProvider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IStylesheetRepository _stylesheetRepository;
-        private readonly IScriptRepository _scriptRepository;
-        private readonly ITemplateRepository _templateRepository;
-        private readonly IPartialViewRepository _partialViewRepository;
-        private readonly IPartialViewMacroRepository _partialViewMacroRepository;
-        private readonly IAuditRepository _auditRepository;
-        private readonly IShortStringHelper _shortStringHelper;
-        private readonly GlobalSettings _globalSettings;
-        private readonly IHostingEnvironment _hostingEnvironment;
+        _stylesheetRepository = stylesheetRepository;
+        _scriptRepository = scriptRepository;
+        _templateRepository = templateRepository;
+        _partialViewRepository = partialViewRepository;
+        _partialViewMacroRepository = partialViewMacroRepository;
+        _auditRepository = auditRepository;
+        _shortStringHelper = shortStringHelper;
+        _globalSettings = globalSettings.Value;
+        _hostingEnvironment = hostingEnvironment;
+    }
 
-        private const string PartialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage";
-        private const string PartialViewMacroHeader = "@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage";
+    #region Stylesheets
 
-        public FileService(ICoreScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IStylesheetRepository stylesheetRepository, IScriptRepository scriptRepository, ITemplateRepository templateRepository,
-            IPartialViewRepository partialViewRepository, IPartialViewMacroRepository partialViewMacroRepository,
-            IAuditRepository auditRepository, IShortStringHelper shortStringHelper, IOptions globalSettings, IHostingEnvironment hostingEnvironment)
-            : base(uowProvider, loggerFactory, eventMessagesFactory)
+    /// 
+    public IEnumerable GetStylesheets(params string[] paths)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _stylesheetRepository = stylesheetRepository;
-            _scriptRepository = scriptRepository;
-            _templateRepository = templateRepository;
-            _partialViewRepository = partialViewRepository;
-            _partialViewMacroRepository = partialViewMacroRepository;
-            _auditRepository = auditRepository;
-            _shortStringHelper = shortStringHelper;
-            _globalSettings = globalSettings.Value;
-            _hostingEnvironment = hostingEnvironment;
+            return _stylesheetRepository.GetMany(paths);
+        }
+    }
+
+    private void Audit(AuditType type, int userId, int objectId, string? entityType) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, entityType));
+
+    /// 
+    public IStylesheet? GetStylesheet(string? path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _stylesheetRepository.Get(path);
+        }
+    }
+
+    /// 
+    public void SaveStylesheet(IStylesheet? stylesheet, int? userId = null)
+    {
+        if (stylesheet is null)
+        {
+            return;
         }
 
-        #region Stylesheets
-
-        /// 
-        public IEnumerable GetStylesheets(params string[] paths)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetMany(paths);
-            }
-        }
-
-        /// 
-        public IStylesheet? GetStylesheet(string? path)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.Get(path);
-            }
-        }
-
-        /// 
-        public void SaveStylesheet(IStylesheet? stylesheet, int? userId = null)
-        {
-            if (stylesheet is null)
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new StylesheetSavingNotification(stylesheet, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
+                scope.Complete();
                 return;
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            userId ??= Constants.Security.SuperUserId;
+            _stylesheetRepository.Save(stylesheet);
+            scope.Notifications.Publish(
+                new StylesheetSavedNotification(stylesheet, eventMessages).WithStateFrom(savingNotification));
+            Audit(AuditType.Save, userId.Value, -1, "Stylesheet");
 
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteStylesheet(string path, int? userId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IStylesheet? stylesheet = _stylesheetRepository.Get(path);
+            if (stylesheet == null)
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new StylesheetSavingNotification(stylesheet, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                _stylesheetRepository.Save(stylesheet);
-                scope.Notifications.Publish(new StylesheetSavedNotification(stylesheet, eventMessages).WithStateFrom(savingNotification));
-                Audit(AuditType.Save, userId.Value, -1, "Stylesheet");
-
                 scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteStylesheet(string path, int? userId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                IStylesheet? stylesheet = _stylesheetRepository.Get(path);
-                if (stylesheet == null)
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new StylesheetDeletingNotification(stylesheet, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return; // causes rollback
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                _stylesheetRepository.Delete(stylesheet);
-
-                scope.Notifications.Publish(new StylesheetDeletedNotification(stylesheet, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId.Value, -1, "Stylesheet");
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void CreateStyleSheetFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteStyleSheetFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public Stream GetStylesheetFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetStylesheetFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetStylesheetFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Scripts
-
-        /// 
-        public IEnumerable GetScripts(params string[] names)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetMany(names);
-            }
-        }
-
-        /// 
-        public IScript? GetScript(string? name)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.Get(name);
-            }
-        }
-
-        /// 
-        public void SaveScript(IScript? script, int? userId)
-        {
-            if (userId is null)
-            {
-                userId = Constants.Security.SuperUserId;
-            }
-            if (script is null)
-            {
                 return;
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new ScriptSavingNotification(script, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _scriptRepository.Save(script);
-                scope.Notifications.Publish(new ScriptSavedNotification(script, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, userId.Value, -1, "Script");
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteScript(string path, int? userId = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                IScript? script = _scriptRepository.Get(path);
-                if (script == null)
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new ScriptDeletingNotification(script, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                _scriptRepository.Delete(script);
-                scope.Notifications.Publish(new ScriptDeletedNotification(script, eventMessages).WithStateFrom(deletingNotification));
-
-                Audit(AuditType.Delete, userId.Value, -1, "Script");
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void CreateScriptFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _scriptRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteScriptFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _scriptRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public Stream GetScriptFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetScriptFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _scriptRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetScriptFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Templates
-
-        /// 
-        /// Creates a template for a content type
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The template created
-        /// 
-        public Attempt?> CreateTemplateForContentType(string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId)
-        {
-            var template = new Template(_shortStringHelper, contentTypeName,
-                //NOTE: We are NOT passing in the content type alias here, we want to use it's name since we don't
-                // want to save template file names as camelCase, the Template ctor will clean the alias as
-                // `alias.ToCleanString(CleanStringType.UnderscoreAlias)` which has been the default.
-                // This fixes: http://issues.umbraco.org/issue/U4-7953
-                contentTypeName);
-
             EventMessages eventMessages = EventMessagesFactory.Get();
-
-            if (contentTypeAlias != null && contentTypeAlias.Length > 255)
+            var deletingNotification = new StylesheetDeletingNotification(stylesheet, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-            }
-
-            // check that the template hasn't been created on disk before creating the content type
-            // if it exists, set the new template content to the existing file content
-            string? content = GetViewContent(contentTypeAlias);
-            if (content != null)
-            {
-                template.Content = content;
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingEvent = new TemplateSavingNotification(template, eventMessages, true, contentTypeAlias!);
-                if (scope.Notifications.PublishCancelable(savingEvent))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(OperationResultType.FailedCancelledByEvent, eventMessages, template);
-                }
-
-                _templateRepository.Save(template);
-                scope.Notifications.Publish(new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingEvent));
-
-                Audit(AuditType.Save, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template));
                 scope.Complete();
+                return; // causes rollback
             }
 
-            return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, template);
+            userId ??= Constants.Security.SuperUserId;
+            _stylesheetRepository.Delete(stylesheet);
+
+            scope.Notifications.Publish(
+                new StylesheetDeletedNotification(stylesheet, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId.Value, -1, "Stylesheet");
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void CreateStyleSheetFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _stylesheetRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteStyleSheetFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _stylesheetRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetStylesheetFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _stylesheetRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    /// 
+    public void SetStylesheetFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _stylesheetRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetStylesheetFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _stylesheetRepository.GetFileSize(filepath);
+        }
+    }
+
+    #endregion
+
+    #region Scripts
+
+    /// 
+    public IEnumerable GetScripts(params string[] names)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.GetMany(names);
+        }
+    }
+
+    /// 
+    public IScript? GetScript(string? name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.Get(name);
+        }
+    }
+
+    /// 
+    public void SaveScript(IScript? script, int? userId)
+    {
+        if (userId is null)
+        {
+            userId = Constants.Security.SuperUserId;
         }
 
-        /// 
-        /// Create a new template, setting the content if a view exists in the filesystem
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public ITemplate CreateTemplateWithIdentity(string? name, string? alias, string? content, ITemplate? masterTemplate = null, int userId = Constants.Security.SuperUserId)
+        if (script is null)
         {
-            if (name == null)
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name));
-            }
-
-            if (name.Length > 255)
-            {
-                throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
-            }
-
-            // file might already be on disk, if so grab the content to avoid overwriting
-            var template = new Template(_shortStringHelper, name, alias)
-            {
-                Content = GetViewContent(alias) ?? content
-            };
-
-            if (masterTemplate != null)
-            {
-                template.SetMasterTemplate(masterTemplate);
-            }
-
-            SaveTemplate(template, userId);
-
-            return template;
+            return;
         }
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetTemplates(params string[] aliases)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new ScriptSavingNotification(script, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                return _templateRepository.GetAll(aliases).OrderBy(x => x.Name);
+                scope.Complete();
+                return;
             }
+
+            _scriptRepository.Save(script);
+            scope.Notifications.Publish(
+                new ScriptSavedNotification(script, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, userId.Value, -1, "Script");
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteScript(string path, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IScript? script = _scriptRepository.Get(path);
+            if (script == null)
+            {
+                scope.Complete();
+                return;
+            }
+
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new ScriptDeletingNotification(script, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            userId ??= Constants.Security.SuperUserId;
+            _scriptRepository.Delete(script);
+            scope.Notifications.Publish(
+                new ScriptDeletedNotification(script, eventMessages).WithStateFrom(deletingNotification));
+
+            Audit(AuditType.Delete, userId.Value, -1, "Script");
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void CreateScriptFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _scriptRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteScriptFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _scriptRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetScriptFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    /// 
+    public void SetScriptFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _scriptRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetScriptFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.GetFileSize(filepath);
+        }
+    }
+
+    #endregion
+
+    #region Templates
+
+    /// 
+    ///     Creates a template for a content type
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     The template created
+    /// 
+    public Attempt?> CreateTemplateForContentType(
+        string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId)
+    {
+        var template = new Template(_shortStringHelper, contentTypeName,
+
+            // NOTE: We are NOT passing in the content type alias here, we want to use it's name since we don't
+            // want to save template file names as camelCase, the Template ctor will clean the alias as
+            // `alias.ToCleanString(CleanStringType.UnderscoreAlias)` which has been the default.
+            // This fixes: http://issues.umbraco.org/issue/U4-7953
+            contentTypeName);
+
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        if (contentTypeAlias != null && contentTypeAlias.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
         }
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetTemplates(int masterTemplateId)
+        // check that the template hasn't been created on disk before creating the content type
+        // if it exists, set the new template content to the existing file content
+        var content = GetViewContent(contentTypeAlias);
+        if (content != null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetChildren(masterTemplateId).OrderBy(x => x.Name);
-            }
+            template.Content = content;
         }
 
-        /// 
-        /// Gets a  object by its alias.
-        /// 
-        /// The alias of the template.
-        /// The  object matching the alias, or null.
-        public ITemplate? GetTemplate(string? alias)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            var savingEvent = new TemplateSavingNotification(template, eventMessages, true, contentTypeAlias!);
+            if (scope.Notifications.PublishCancelable(savingEvent))
             {
-                return _templateRepository.Get(alias);
+                scope.Complete();
+                return OperationResult.Attempt.Fail(
+                    OperationResultType.FailedCancelledByEvent, eventMessages, template);
             }
+
+            _templateRepository.Save(template);
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingEvent));
+
+            Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
         }
 
-        /// 
-        /// Gets a  object by its identifier.
-        /// 
-        /// The identifier of the template.
-        /// The  object matching the identifier, or null.
-        public ITemplate? GetTemplate(int id)
+        return OperationResult.Attempt.Succeed(
+            OperationResultType.Success,
+            eventMessages,
+            template);
+    }
+
+    /// 
+    ///     Create a new template, setting the content if a view exists in the filesystem
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public ITemplate CreateTemplateWithIdentity(
+        string? name,
+        string? alias,
+        string? content,
+        ITemplate? masterTemplate = null,
+        int userId = Constants.Security.SuperUserId)
+    {
+        if (name == null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.Get(id);
-            }
+            throw new ArgumentNullException(nameof(name));
         }
 
-        /// 
-        /// Gets a  object by its guid identifier.
-        /// 
-        /// The guid identifier of the template.
-        /// The  object matching the identifier, or null.
-        public ITemplate? GetTemplate(Guid id)
+        if (string.IsNullOrWhiteSpace(name))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                IQuery? query = Query().Where(x => x.Key == id);
-                return _templateRepository.Get(query)?.SingleOrDefault();
-            }
+            throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name));
         }
 
-        /// 
-        /// Gets the template descendants
-        /// 
-        /// 
-        /// 
-        public IEnumerable GetTemplateDescendants(int masterTemplateId)
+        if (name.Length > 255)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetDescendants(masterTemplateId);
-            }
+            throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
         }
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// 
-        public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId)
+        // file might already be on disk, if so grab the content to avoid overwriting
+        var template = new Template(_shortStringHelper, name, alias) { Content = GetViewContent(alias) ?? content };
+
+        if (masterTemplate != null)
         {
+            template.SetMasterTemplate(masterTemplate);
+        }
+
+        SaveTemplate(template, userId);
+
+        return template;
+    }
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetTemplates(params string[] aliases)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetAll(aliases).OrderBy(x => x.Name);
+        }
+    }
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetTemplates(int masterTemplateId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetChildren(masterTemplateId).OrderBy(x => x.Name);
+        }
+    }
+
+    /// 
+    ///     Gets a  object by its alias.
+    /// 
+    /// The alias of the template.
+    /// The  object matching the alias, or null.
+    public ITemplate? GetTemplate(string? alias)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.Get(alias);
+        }
+    }
+
+    /// 
+    ///     Gets a  object by its identifier.
+    /// 
+    /// The identifier of the template.
+    /// The  object matching the identifier, or null.
+    public ITemplate? GetTemplate(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.Get(id);
+        }
+    }
+
+    /// 
+    ///     Gets a  object by its guid identifier.
+    /// 
+    /// The guid identifier of the template.
+    /// The  object matching the identifier, or null.
+    public ITemplate? GetTemplate(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery? query = Query().Where(x => x.Key == id);
+            return _templateRepository.Get(query)?.SingleOrDefault();
+        }
+    }
+
+    /// 
+    ///     Gets the template descendants
+    /// 
+    /// 
+    /// 
+    public IEnumerable GetTemplateDescendants(int masterTemplateId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetDescendants(masterTemplateId);
+        }
+    }
+
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// 
+    public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId)
+    {
+        if (template == null)
+        {
+            throw new ArgumentNullException(nameof(template));
+        }
+
+        if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255)
+        {
+            throw new InvalidOperationException(
+                "Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length.");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new TemplateSavingNotification(template, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            _templateRepository.Save(template);
+
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Saves a collection of  objects
+    /// 
+    /// List of  to save
+    /// Optional id of the user
+    public void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId)
+    {
+        ITemplate[] templatesA = templates.ToArray();
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new TemplateSavingNotification(templatesA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            foreach (ITemplate template in templatesA)
+            {
+                _templateRepository.Save(template);
+            }
+
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(templatesA, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, userId, -1, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Deletes a template by its alias
+    /// 
+    /// Alias of the  to delete
+    /// 
+    public void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            ITemplate? template = _templateRepository.Get(alias);
             if (template == null)
             {
-                throw new ArgumentNullException(nameof(template));
-            }
-
-            if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255)
-            {
-                throw new InvalidOperationException("Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length.");
-            }
-
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new TemplateSavingNotification(template, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _templateRepository.Save(template);
-
-                scope.Notifications.Publish(new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
                 scope.Complete();
+                return;
             }
-        }
 
-        /// 
-        /// Saves a collection of  objects
-        /// 
-        /// List of  to save
-        /// Optional id of the user
-        public void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId)
-        {
-            ITemplate[] templatesA = templates.ToArray();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new TemplateDeletingNotification(template, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new TemplateSavingNotification(templatesA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                foreach (ITemplate template in templatesA)
-                {
-                    _templateRepository.Save(template);
-                }
-
-                scope.Notifications.Publish(new TemplateSavedNotification(templatesA, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, userId, -1, UmbracoObjectTypes.Template.GetName());
                 scope.Complete();
+                return;
             }
+
+            _templateRepository.Delete(template);
+
+            scope.Notifications.Publish(
+                new TemplateDeletedNotification(template, eventMessages).WithStateFrom(deletingNotification));
+
+            Audit(AuditType.Delete, userId, template.Id, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetTemplateFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    private string? GetViewContent(string? fileName)
+    {
+        if (fileName.IsNullOrWhiteSpace())
+        {
+            throw new ArgumentNullException(nameof(fileName));
         }
 
-        /// 
-        /// Deletes a template by its alias
-        /// 
-        /// Alias of the  to delete
-        /// 
-        public void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId)
+        if (!fileName!.EndsWith(".cshtml"))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                ITemplate? template = _templateRepository.Get(alias);
-                if (template == null)
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new TemplateDeletingNotification(template, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _templateRepository.Delete(template);
-
-                scope.Notifications.Publish(new TemplateDeletedNotification(template, eventMessages).WithStateFrom(deletingNotification));
-
-                Audit(AuditType.Delete, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template));
-                scope.Complete();
-            }
+            fileName = $"{fileName}.cshtml";
         }
 
-        private string? GetViewContent(string? fileName)
+        Stream fs = _templateRepository.GetFileContentStream(fileName);
+
+        using (var view = new StreamReader(fs))
         {
-            if (fileName.IsNullOrWhiteSpace())
-            {
-                throw new ArgumentNullException(nameof(fileName));
-            }
+            return view.ReadToEnd().Trim();
+        }
+    }
 
-            if (!fileName!.EndsWith(".cshtml"))
-            {
-                fileName = $"{fileName}.cshtml";
-            }
+    /// 
+    public void SetTemplateFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _templateRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
 
-            Stream fs = _templateRepository.GetFileContentStream(fileName);
+    /// 
+    public long GetTemplateFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetFileSize(filepath);
+        }
+    }
 
-            using (var view = new StreamReader(fs))
-            {
-                return view.ReadToEnd().Trim();
-            }
+    #endregion
+
+    #region Partial Views
+
+    public IEnumerable GetPartialViewSnippetNames(params string[] filterNames)
+    {
+        var snippetProvider =
+            new EmbeddedFileProvider(GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
+
+        var files = snippetProvider.GetDirectoryContents(string.Empty)
+            .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml"))
+            .Select(x => Path.GetFileNameWithoutExtension(x.Name))
+            .Except(filterNames, StringComparer.InvariantCultureIgnoreCase)
+            .ToArray();
+
+        // Ensure the ones that are called 'Empty' are at the top
+        var empty = files.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false)
+            .OrderBy(x => x?.Length)
+            .ToArray();
+
+        return empty.Union(files.Except(empty)).WhereNotNull();
+    }
+
+    public void DeletePartialViewFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    public void DeletePartialViewMacroFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewMacroRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    public IEnumerable GetPartialViews(params string[] names)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.GetMany(names);
+        }
+    }
+
+    public IPartialView? GetPartialView(string path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.Get(path);
+        }
+    }
+
+    public IPartialView? GetPartialViewMacro(string path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewMacroRepository.Get(path);
+        }
+    }
+
+    public Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
+        CreatePartialViewMacro(partialView, PartialViewType.PartialView, snippetName, userId);
+
+    public Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
+        CreatePartialViewMacro(partialView, PartialViewType.PartialViewMacro, snippetName, userId);
+
+    public bool DeletePartialView(string path, int? userId = null) =>
+        DeletePartialViewMacro(path, PartialViewType.PartialView, userId);
+
+    private Attempt CreatePartialViewMacro(
+        IPartialView partialView,
+        PartialViewType partialViewType,
+        string? snippetName = null,
+        int? userId = Constants.Security.SuperUserId)
+    {
+        string partialViewHeader;
+        switch (partialViewType)
+        {
+            case PartialViewType.PartialView:
+                partialViewHeader = PartialViewHeader;
+                break;
+            case PartialViewType.PartialViewMacro:
+                partialViewHeader = PartialViewMacroHeader;
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
         }
 
-        /// 
-        public Stream GetTemplateFileContentStream(string filepath)
+        string? partialViewContent = null;
+        if (snippetName.IsNullOrWhiteSpace() == false)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetTemplateFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _templateRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetTemplateFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Partial Views
-
-        public IEnumerable GetPartialViewSnippetNames(params string[] filterNames)
-        {
-            var snippetProvider =
-                new EmbeddedFileProvider(this.GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
-
-            var files = snippetProvider.GetDirectoryContents(string.Empty)
-                .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml"))
-                .Select(x => Path.GetFileNameWithoutExtension(x.Name))
-                .Except(filterNames, StringComparer.InvariantCultureIgnoreCase)
-                .ToArray();
-
-            //Ensure the ones that are called 'Empty' are at the top
-            var empty = files.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false)
-                .OrderBy(x => x?.Length)
-                .ToArray();
-
-            return empty.Union(files.Except(empty)).WhereNotNull();
-        }
-
-        public void DeletePartialViewFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        public void DeletePartialViewMacroFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        public IEnumerable GetPartialViews(params string[] names)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetMany(names);
-            }
-        }
-
-        public IPartialView? GetPartialView(string path)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.Get(path);
-            }
-        }
-
-        public IPartialView? GetPartialViewMacro(string path)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.Get(path);
-            }
-        }
-
-        public Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
-            CreatePartialViewMacro(partialView, PartialViewType.PartialView, snippetName, userId);
-
-        public Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
-            CreatePartialViewMacro(partialView, PartialViewType.PartialViewMacro, snippetName, userId);
-
-        private Attempt CreatePartialViewMacro(IPartialView partialView, PartialViewType partialViewType, string? snippetName = null, int? userId = Constants.Security.SuperUserId)
-        {
-            string partialViewHeader;
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    partialViewHeader = PartialViewHeader;
-                    break;
-                case PartialViewType.PartialViewMacro:
-                    partialViewHeader = PartialViewMacroHeader;
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(partialViewType));
-            }
-
-            string? partialViewContent = null;
-            if (snippetName.IsNullOrWhiteSpace() == false)
-            {
-                //create the file
-                Attempt snippetPathAttempt = TryGetSnippetPath(snippetName);
-                if (snippetPathAttempt.Success == false)
-                {
-                    throw new InvalidOperationException("Could not load snippet with name " + snippetName);
-                }
-
-                using (var snippetFile = new StreamReader(System.IO.File.OpenRead(snippetPathAttempt.Result!)))
-                {
-                    var snippetContent = snippetFile.ReadToEnd().Trim();
-
-                    //strip the @inherits if it's there
-                    snippetContent = StripPartialViewHeader(snippetContent);
-
-                    //Update Model.Content. to be Model. when used as PartialView
-                    if(partialViewType == PartialViewType.PartialView)
-                    {
-                        snippetContent = snippetContent.Replace("Model.Content.", "Model.");
-                    }
-
-                    partialViewContent = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
-                }
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var creatingNotification = new PartialViewCreatingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(creatingNotification))
-                {
-                    scope.Complete();
-                    return Attempt.Fail();
-                }
-
-                IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
-                if (partialViewContent != null)
-                {
-                    partialView.Content = partialViewContent;
-                }
-
-                repository.Save(partialView);
-
-                scope.Notifications.Publish(new PartialViewCreatedNotification(partialView, eventMessages).WithStateFrom(creatingNotification));
-
-                Audit(AuditType.Save, userId!.Value, -1, partialViewType.ToString());
-
-                scope.Complete();
-            }
-
-            return Attempt.Succeed(partialView);
-        }
-
-        public bool DeletePartialView(string path, int? userId = null) =>
-            DeletePartialViewMacro(path, PartialViewType.PartialView, userId);
-
-        public bool DeletePartialViewMacro(string path, int? userId = null) =>
-            DeletePartialViewMacro(path, PartialViewType.PartialViewMacro, userId);
-
-        private bool DeletePartialViewMacro(string path, PartialViewType partialViewType, int? userId = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-
-                IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
-                IPartialView? partialView = repository.Get(path);
-                if (partialView == null)
-                {
-                    scope.Complete();
-                    return true;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new PartialViewDeletingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return false;
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                repository.Delete(partialView);
-                scope.Notifications.Publish(new PartialViewDeletedNotification(partialView, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId.Value, -1, partialViewType.ToString());
-
-                scope.Complete();
-            }
-
-            return true;
-        }
-
-        public Attempt SavePartialView(IPartialView partialView, int? userId = null) =>
-            SavePartialView(partialView, PartialViewType.PartialView, userId);
-
-        public Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null) =>
-            SavePartialView(partialView, PartialViewType.PartialViewMacro, userId);
-
-        private Attempt SavePartialView(IPartialView partialView, PartialViewType partialViewType, int? userId = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new PartialViewSavingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return Attempt.Fail();
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
-                repository.Save(partialView);
-
-                Audit(AuditType.Save, userId.Value, -1, partialViewType.ToString());
-                scope.Notifications.Publish(new PartialViewSavedNotification(partialView, eventMessages).WithStateFrom(savingNotification));
-
-                scope.Complete();
-            }
-
-            return Attempt.Succeed(partialView);
-        }
-
-        internal string StripPartialViewHeader(string contents)
-        {
-            var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline);
-            return headerMatch.Replace(contents, string.Empty);
-        }
-
-        internal Attempt TryGetSnippetPath(string? fileName)
-        {
-            if (fileName?.EndsWith(".cshtml") == false)
-            {
-                fileName += ".cshtml";
-            }
-
-            var snippetPath = _hostingEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.Umbraco}/PartialViewMacros/Templates/{fileName}");
-            return System.IO.File.Exists(snippetPath)
-                ? Attempt.Succeed(snippetPath)
-                : Attempt.Fail();
-        }
-
-        public void CreatePartialViewFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        public void CreatePartialViewMacroFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        private IPartialViewRepository GetPartialViewRepository(PartialViewType partialViewType)
-        {
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    return _partialViewRepository;
-                case PartialViewType.PartialViewMacro:
-                    return _partialViewMacroRepository;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(partialViewType));
-            }
-        }
-
-        /// 
-        public Stream GetPartialViewFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetPartialViewFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetPartialViewFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetFileSize(filepath);
-            }
-        }
-
-        /// 
-        public Stream GetPartialViewMacroFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetPartialViewMacroFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetPartialViewMacroFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Snippets
-
-        public string GetPartialViewSnippetContent(string snippetName) => GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialView);
-
-        public string GetPartialViewMacroSnippetContent(string snippetName) => GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialViewMacro);
-
-        private string GetPartialViewMacroSnippetContent(string snippetName, PartialViewType partialViewType)
-        {
-            if (snippetName.IsNullOrWhiteSpace())
-            {
-                throw new ArgumentNullException(nameof(snippetName));
-            }
-
-            string partialViewHeader;
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    partialViewHeader = PartialViewHeader;
-                    break;
-                case PartialViewType.PartialViewMacro:
-                    partialViewHeader = PartialViewMacroHeader;
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(partialViewType));
-            }
-
-            var snippetProvider =
-                new EmbeddedFileProvider(this.GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
-
-            var file = snippetProvider.GetDirectoryContents(string.Empty).FirstOrDefault(x=>x.Exists && x.Name.Equals(snippetName + ".cshtml"));
-
-            // Try and get the snippet path
-            if (file is null)
+            // create the file
+            Attempt snippetPathAttempt = TryGetSnippetPath(snippetName);
+            if (snippetPathAttempt.Success == false)
             {
                 throw new InvalidOperationException("Could not load snippet with name " + snippetName);
             }
 
-            using (var snippetFile = new StreamReader(file.CreateReadStream()))
+            using (var snippetFile = new StreamReader(File.OpenRead(snippetPathAttempt.Result!)))
             {
                 var snippetContent = snippetFile.ReadToEnd().Trim();
 
-                //strip the @inherits if it's there
+                // strip the @inherits if it's there
                 snippetContent = StripPartialViewHeader(snippetContent);
 
-                //Update Model.Content to be Model when used as PartialView
+                // Update Model.Content. to be Model. when used as PartialView
                 if (partialViewType == PartialViewType.PartialView)
                 {
-                    snippetContent = snippetContent
-                        .Replace("Model.Content.", "Model.")
-                        .Replace("(Model.Content)", "(Model)");
+                    snippetContent = snippetContent.Replace("Model.Content.", "Model.");
                 }
 
-                var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
-                return content;
+                partialViewContent = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
             }
         }
 
-        #endregion
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var creatingNotification = new PartialViewCreatingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(creatingNotification))
+            {
+                scope.Complete();
+                return Attempt.Fail();
+            }
 
-        private void Audit(AuditType type, int userId, int objectId, string? entityType) => _auditRepository.Save(new AuditItem(objectId, type, userId, entityType));
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            if (partialViewContent != null)
+            {
+                partialView.Content = partialViewContent;
+            }
 
-        // TODO: Method to change name and/or alias of view template
+            repository.Save(partialView);
+
+            scope.Notifications.Publish(
+                new PartialViewCreatedNotification(partialView, eventMessages).WithStateFrom(creatingNotification));
+
+            Audit(AuditType.Save, userId!.Value, -1, partialViewType.ToString());
+
+            scope.Complete();
+        }
+
+        return Attempt.Succeed(partialView);
     }
+
+    public bool DeletePartialViewMacro(string path, int? userId = null) =>
+        DeletePartialViewMacro(path, PartialViewType.PartialViewMacro, userId);
+
+    public Attempt SavePartialView(IPartialView partialView, int? userId = null) =>
+        SavePartialView(partialView, PartialViewType.PartialView, userId);
+
+    private bool DeletePartialViewMacro(string path, PartialViewType partialViewType, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            IPartialView? partialView = repository.Get(path);
+            if (partialView == null)
+            {
+                scope.Complete();
+                return true;
+            }
+
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new PartialViewDeletingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return false;
+            }
+
+            userId ??= Constants.Security.SuperUserId;
+            repository.Delete(partialView);
+            scope.Notifications.Publish(
+                new PartialViewDeletedNotification(partialView, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId.Value, -1, partialViewType.ToString());
+
+            scope.Complete();
+        }
+
+        return true;
+    }
+
+    public Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null) =>
+        SavePartialView(partialView, PartialViewType.PartialViewMacro, userId);
+
+    public void CreatePartialViewFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    internal string StripPartialViewHeader(string contents)
+    {
+        var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline);
+        return headerMatch.Replace(contents, string.Empty);
+    }
+
+    private Attempt SavePartialView(IPartialView partialView, PartialViewType partialViewType, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new PartialViewSavingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return Attempt.Fail();
+            }
+
+            userId ??= Constants.Security.SuperUserId;
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            repository.Save(partialView);
+
+            Audit(AuditType.Save, userId.Value, -1, partialViewType.ToString());
+            scope.Notifications.Publish(
+                new PartialViewSavedNotification(partialView, eventMessages).WithStateFrom(savingNotification));
+
+            scope.Complete();
+        }
+
+        return Attempt.Succeed(partialView);
+    }
+
+    internal Attempt TryGetSnippetPath(string? fileName)
+    {
+        if (fileName?.EndsWith(".cshtml") == false)
+        {
+            fileName += ".cshtml";
+        }
+
+        var snippetPath =
+            _hostingEnvironment.MapPathContentRoot(
+                $"{Constants.SystemDirectories.Umbraco}/PartialViewMacros/Templates/{fileName}");
+        return File.Exists(snippetPath)
+            ? Attempt.Succeed(snippetPath)
+            : Attempt.Fail();
+    }
+
+    public void CreatePartialViewMacroFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewMacroRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetPartialViewFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    private IPartialViewRepository GetPartialViewRepository(PartialViewType partialViewType)
+    {
+        switch (partialViewType)
+        {
+            case PartialViewType.PartialView:
+                return _partialViewRepository;
+            case PartialViewType.PartialViewMacro:
+                return _partialViewMacroRepository;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
+        }
+    }
+
+    /// 
+    public void SetPartialViewFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetPartialViewFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.GetFileSize(filepath);
+        }
+    }
+
+    /// 
+    public Stream GetPartialViewMacroFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewMacroRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    /// 
+    public void SetPartialViewMacroFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewMacroRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetPartialViewMacroFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewMacroRepository.GetFileSize(filepath);
+        }
+    }
+
+    #endregion
+
+    #region Snippets
+
+    public string GetPartialViewSnippetContent(string snippetName) =>
+        GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialView);
+
+    public string GetPartialViewMacroSnippetContent(string snippetName) =>
+        GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialViewMacro);
+
+    private string GetPartialViewMacroSnippetContent(string snippetName, PartialViewType partialViewType)
+    {
+        if (snippetName.IsNullOrWhiteSpace())
+        {
+            throw new ArgumentNullException(nameof(snippetName));
+        }
+
+        string partialViewHeader;
+        switch (partialViewType)
+        {
+            case PartialViewType.PartialView:
+                partialViewHeader = PartialViewHeader;
+                break;
+            case PartialViewType.PartialViewMacro:
+                partialViewHeader = PartialViewMacroHeader;
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
+        }
+
+        var snippetProvider =
+            new EmbeddedFileProvider(GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
+
+        IFileInfo? file = snippetProvider.GetDirectoryContents(string.Empty)
+            .FirstOrDefault(x => x.Exists && x.Name.Equals(snippetName + ".cshtml"));
+
+        // Try and get the snippet path
+        if (file is null)
+        {
+            throw new InvalidOperationException("Could not load snippet with name " + snippetName);
+        }
+
+        using (var snippetFile = new StreamReader(file.CreateReadStream()))
+        {
+            var snippetContent = snippetFile.ReadToEnd().Trim();
+
+            // strip the @inherits if it's there
+            snippetContent = StripPartialViewHeader(snippetContent);
+
+            // Update Model.Content to be Model when used as PartialView
+            if (partialViewType == PartialViewType.PartialView)
+            {
+                snippetContent = snippetContent
+                    .Replace("Model.Content.", "Model.")
+                    .Replace("(Model.Content)", "(Model)");
+            }
+
+            var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
+            return content;
+        }
+    }
+
+    #endregion
+
+    // TODO: Method to change name and/or alias of view template
 }
diff --git a/src/Umbraco.Core/Services/IAuditService.cs b/src/Umbraco.Core/Services/IAuditService.cs
index df816960a3..f58da53174 100644
--- a/src/Umbraco.Core/Services/IAuditService.cs
+++ b/src/Umbraco.Core/Services/IAuditService.cs
@@ -1,86 +1,104 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents a service for handling audit.
+/// 
+public interface IAuditService : IService
 {
+    void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null);
+
+    IEnumerable GetLogs(int objectId);
+
+    IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null);
+
+    IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null);
+
+    void CleanLogs(int maximumAgeOfLogsInMinutes);
+
     /// 
-    /// Represents a service for handling audit.
+    ///     Returns paged items in the audit trail for a given entity
     /// 
-    public interface IAuditService : IService
-    {
-        void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null);
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    IEnumerable GetPagedItemsByEntity(
+        int entityId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null);
 
-        IEnumerable GetLogs(int objectId);
-        IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null);
-        IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null);
-        void CleanLogs(int maximumAgeOfLogsInMinutes);
+    /// 
+    ///     Returns paged items in the audit trail for a given user
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    IEnumerable GetPagedItemsByUser(
+        int userId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null);
 
-        /// 
-        /// Returns paged items in the audit trail for a given entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        IEnumerable GetPagedItemsByEntity(int entityId, long pageIndex, int pageSize, out long totalRecords,
-            Direction orderDirection = Direction.Descending,
-            AuditType[]? auditTypeFilter = null,
-            IQuery? customFilter = null);
-
-        /// 
-        /// Returns paged items in the audit trail for a given user
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        IEnumerable GetPagedItemsByUser(int userId, long pageIndex, int pageSize, out long totalRecords,
-            Direction orderDirection = Direction.Descending,
-            AuditType[]? auditTypeFilter = null,
-            IQuery? customFilter = null);
-
-        /// 
-        /// Writes an audit entry for an audited event.
-        /// 
-        /// The identifier of the user triggering the audited event.
-        /// Free-form details about the user triggering the audited event.
-        /// The IP address or the request triggering the audited event.
-        /// The date and time of the audited event.
-        /// The identifier of the user affected by the audited event.
-        /// Free-form details about the entity affected by the audited event.
-        /// 
-        /// The type of the audited event - must contain only alphanumeric chars and hyphens with forward slashes separating categories.
-        /// 
-        /// The eventType will generally be formatted like: {application}/{entity-type}/{category}/{sub-category}
-        /// Example: umbraco/user/sign-in/failed
-        /// 
-        /// 
-        /// Free-form details about the audited event.
-        IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string affectedDetails, string eventType, string eventDetails);
-
-    }
+    /// 
+    ///     Writes an audit entry for an audited event.
+    /// 
+    /// The identifier of the user triggering the audited event.
+    /// Free-form details about the user triggering the audited event.
+    /// The IP address or the request triggering the audited event.
+    /// The date and time of the audited event.
+    /// The identifier of the user affected by the audited event.
+    /// Free-form details about the entity affected by the audited event.
+    /// 
+    ///     The type of the audited event - must contain only alphanumeric chars and hyphens with forward slashes separating
+    ///     categories.
+    ///     
+    ///         The eventType will generally be formatted like: {application}/{entity-type}/{category}/{sub-category}
+    ///         Example: umbraco/user/sign-in/failed
+    ///     
+    /// 
+    /// Free-form details about the audited event.
+    IAuditEntry Write(
+        int performingUserId,
+        string perfomingDetails,
+        string performingIp,
+        DateTime eventDateUtc,
+        int affectedUserId,
+        string affectedDetails,
+        string eventType,
+        string eventDetails);
 }
diff --git a/src/Umbraco.Core/Services/IBasicAuthService.cs b/src/Umbraco.Core/Services/IBasicAuthService.cs
index 84173a629a..c371376f85 100644
--- a/src/Umbraco.Core/Services/IBasicAuthService.cs
+++ b/src/Umbraco.Core/Services/IBasicAuthService.cs
@@ -1,10 +1,13 @@
 using System.Net;
+using Microsoft.Extensions.Primitives;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IBasicAuthService
 {
-    public interface IBasicAuthService
-    {
-        bool IsBasicAuthEnabled();
-        bool IsIpAllowListed(IPAddress clientIpAddress);
-    }
+    bool IsBasicAuthEnabled();
+    bool IsIpAllowListed(IPAddress clientIpAddress);
+    bool HasCorrectSharedSecret(IDictionary headers) => false;
+
+    bool IsRedirectToLoginPageEnabled() => false;
 }
diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs
index c884b8bed8..0b71bde66d 100644
--- a/src/Umbraco.Core/Services/ICacheInstructionService.cs
+++ b/src/Umbraco.Core/Services/ICacheInstructionService.cs
@@ -1,53 +1,50 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
 using Umbraco.Cms.Core.Cache;
 using Umbraco.Cms.Core.Sync;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface ICacheInstructionService
 {
-    public interface ICacheInstructionService
-    {
-        /// 
-        /// Checks to see if a cold boot is required, either because instructions exist and none have been synced or
-        /// because the last recorded synced instruction can't be found in the database.
-        /// 
-        bool IsColdBootRequired(int lastId);
+    /// 
+    ///     Checks to see if a cold boot is required, either because instructions exist and none have been synced or
+    ///     because the last recorded synced instruction can't be found in the database.
+    /// 
+    bool IsColdBootRequired(int lastId);
 
-        /// 
-        /// Checks to see if the number of pending instructions are over the configured limit.
-        /// 
-        bool IsInstructionCountOverLimit(int lastId, int limit, out int count);
+    /// 
+    ///     Checks to see if the number of pending instructions are over the configured limit.
+    /// 
+    bool IsInstructionCountOverLimit(int lastId, int limit, out int count);
 
-        /// 
-        /// Gets the most recent cache instruction record Id.
-        /// 
-        /// 
-        int GetMaxInstructionId();
+    /// 
+    ///     Gets the most recent cache instruction record Id.
+    /// 
+    /// 
+    int GetMaxInstructionId();
 
-        /// 
-        /// Creates a cache instruction record from a set of individual instructions and saves it.
-        /// 
-        void DeliverInstructions(IEnumerable instructions, string localIdentity);
+    /// 
+    ///     Creates a cache instruction record from a set of individual instructions and saves it.
+    /// 
+    void DeliverInstructions(IEnumerable instructions, string localIdentity);
 
-        /// 
-        /// Creates one or more cache instruction records based on the configured batch size from a set of individual instructions and saves them.
-        /// 
-        void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity);
+    /// 
+    ///     Creates one or more cache instruction records based on the configured batch size from a set of individual
+    ///     instructions and saves them.
+    /// 
+    void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity);
 
-        /// 
-        /// Processes and then prunes pending database cache instructions.
-        /// 
-        /// Flag indicating if process is shutting now and operations should exit.
-        /// Local identity of the executing AppDomain.
-        /// Date of last prune operation.
-        /// Id of the latest processed instruction
-        ProcessInstructionsResult ProcessInstructions(
-            CacheRefresherCollection cacheRefreshers,
-            ServerRole serverRole,
-            CancellationToken cancellationToken,
-            string localIdentity,
-            DateTime lastPruned,
-            int lastId);
-    }
+    /// 
+    ///     Processes and then prunes pending database cache instructions.
+    /// 
+    /// Flag indicating if process is shutting now and operations should exit.
+    /// Local identity of the executing AppDomain.
+    /// Date of last prune operation.
+    /// Id of the latest processed instruction
+    ProcessInstructionsResult ProcessInstructions(
+        CacheRefresherCollection cacheRefreshers,
+        ServerRole serverRole,
+        CancellationToken cancellationToken,
+        string localIdentity,
+        DateTime lastPruned,
+        int lastId);
 }
diff --git a/src/Umbraco.Core/Services/IConflictingRouteService.cs b/src/Umbraco.Core/Services/IConflictingRouteService.cs
index 04d81d7f88..fe044362b7 100644
--- a/src/Umbraco.Core/Services/IConflictingRouteService.cs
+++ b/src/Umbraco.Core/Services/IConflictingRouteService.cs
@@ -1,7 +1,6 @@
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IConflictingRouteService
 {
-    public interface IConflictingRouteService
-    {
-        public bool HasConflictingRoutes(out string controllerName);
-    }
+    public bool HasConflictingRoutes(out string controllerName);
 }
diff --git a/src/Umbraco.Core/Services/IConsentService.cs b/src/Umbraco.Core/Services/IConsentService.cs
index d191caebe2..dc04008503 100644
--- a/src/Umbraco.Core/Services/IConsentService.cs
+++ b/src/Umbraco.Core/Services/IConsentService.cs
@@ -1,45 +1,52 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     A service for handling lawful data processing requirements
+/// 
+/// 
+///     
+///         Consent can be given or revoked or changed via the  method, which
+///         creates a new  entity to track the consent. Revoking a consent is performed by
+///         registering a revoked consent.
+///     
+///     A consent can be revoked, by registering a revoked consent, but cannot be deleted.
+///     
+///         Getter methods return the current state of a consent, i.e. the latest 
+///         entity that was created.
+///     
+/// 
+public interface IConsentService : IService
 {
     /// 
-    /// A service for handling lawful data processing requirements
+    ///     Registers consent.
     /// 
-    /// 
-    /// Consent can be given or revoked or changed via the  method, which
-    /// creates a new  entity to track the consent. Revoking a consent is performed by
-    /// registering a revoked consent.
-    /// A consent can be revoked, by registering a revoked consent, but cannot be deleted.
-    /// Getter methods return the current state of a consent, i.e. the latest 
-    /// entity that was created.
-    /// 
-    public interface IConsentService : IService
-    {
-        /// 
-        /// Registers consent.
-        /// 
-        /// The source, i.e. whoever is consenting.
-        /// 
-        /// 
-        /// The state of the consent.
-        /// Additional free text.
-        /// The corresponding consent entity.
-        IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null);
+    /// The source, i.e. whoever is consenting.
+    /// 
+    /// 
+    /// The state of the consent.
+    /// Additional free text.
+    /// The corresponding consent entity.
+    IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null);
 
-        /// 
-        /// Retrieves consents.
-        /// 
-        /// The optional source.
-        /// The optional context.
-        /// The optional action.
-        /// Determines whether  is a start pattern.
-        /// Determines whether  is a start pattern.
-        /// Determines whether  is a start pattern.
-        /// Determines whether to include the history of consents.
-        /// Consents matching the parameters.
-        IEnumerable LookupConsent(string? source = null, string? context = null, string? action = null,
-            bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false,
-            bool includeHistory = false);
-    }
+    /// 
+    ///     Retrieves consents.
+    /// 
+    /// The optional source.
+    /// The optional context.
+    /// The optional action.
+    /// Determines whether  is a start pattern.
+    /// Determines whether  is a start pattern.
+    /// Determines whether  is a start pattern.
+    /// Determines whether to include the history of consents.
+    /// Consents matching the parameters.
+    IEnumerable LookupConsent(
+        string? source = null,
+        string? context = null,
+        string? action = null,
+        bool sourceStartsWith = false,
+        bool contextStartsWith = false,
+        bool actionStartsWith = false,
+        bool includeHistory = false);
 }
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index 93d51da757..1eb2db83bf 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -1,544 +1,559 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the ContentService, which is an easy access to operations involving 
+/// 
+public interface IContentService : IContentServiceBase
 {
+    #region Rollback
+
     /// 
-    /// Defines the ContentService, which is an easy access to operations involving 
+    ///     Rolls back the content to a specific version.
     /// 
-    public interface IContentService : IContentServiceBase
-    {
-        #region Blueprints
-
-        /// 
-        /// Gets a blueprint.
-        /// 
-        IContent? GetBlueprintById(int id);
-
-        /// 
-        /// Gets a blueprint.
-        /// 
-        IContent? GetBlueprintById(Guid id);
-
-        /// 
-        /// Gets blueprints for a content type.
-        /// 
-        IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeId);
-
-        /// 
-        /// Saves a blueprint.
-        /// 
-        void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes a blueprint.
-        /// 
-        void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a new content item from a blueprint.
-        /// 
-        IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes blueprints for a content type.
-        /// 
-        void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes blueprints for content types.
-        /// 
-        void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Get, Count Documents
-
-        /// 
-        /// Gets a document.
-        /// 
-        IContent? GetById(int id);
-
-        /// 
-        /// Gets a document.
-        /// 
-        IContent? GetById(Guid key);
-
-        /// 
-        /// Gets publish/unpublish schedule for a content node.
-        /// 
-        /// Id of the Content to load schedule for
-        /// 
-        ContentScheduleCollection GetContentScheduleByContentId(int contentId);
-
-        /// 
-        /// Persists publish/unpublish schedule for a content node.
-        /// 
-        /// 
-        /// 
-        void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule);
-
-        /// 
-        /// Gets documents.
-        /// 
-        IEnumerable GetByIds(IEnumerable ids);
-
-        /// 
-        /// Gets documents.
-        /// 
-        IEnumerable GetByIds(IEnumerable ids);
-
-        /// 
-        /// Gets documents at a given level.
-        /// 
-        IEnumerable GetByLevel(int level);
-
-        /// 
-        /// Gets the parent of a document.
-        /// 
-        IContent? GetParent(int id);
-
-        /// 
-        /// Gets the parent of a document.
-        /// 
-        IContent? GetParent(IContent content);
-
-        /// 
-        /// Gets ancestor documents of a document.
-        /// 
-        IEnumerable GetAncestors(int id);
-
-        /// 
-        /// Gets ancestor documents of a document.
-        /// 
-        IEnumerable GetAncestors(IContent content);
-
-        /// 
-        /// Gets all versions of a document.
-        /// 
-        /// Versions are ordered with current first, then most recent first.
-        IEnumerable GetVersions(int id);
-
-        /// 
-        /// Gets all versions of a document.
-        /// 
-        /// Versions are ordered with current first, then most recent first.
-        IEnumerable GetVersionsSlim(int id, int skip, int take);
-
-        /// 
-        /// Gets top versions of a document.
-        /// 
-        /// Versions are ordered with current first, then most recent first.
-        IEnumerable GetVersionIds(int id, int topRows);
-
-        /// 
-        /// Gets a version of a document.
-        /// 
-        IContent? GetVersion(int versionId);
-
-        /// 
-        /// Gets root-level documents.
-        /// 
-        IEnumerable GetRootContent();
-
-        /// 
-        /// Gets documents having an expiration date before (lower than, or equal to) a specified date.
-        /// 
-        /// An Enumerable list of  objects
-        /// 
-        /// The content returned from this method may be culture variant, in which case the resulting  should be queried
-        /// for which culture(s) have been scheduled.
-        /// 
-        IEnumerable GetContentForExpiration(DateTime date);
-
-        /// 
-        /// Gets documents having a release date before (lower than, or equal to) a specified date.
-        /// 
-        /// An Enumerable list of  objects
-        /// 
-        /// The content returned from this method may be culture variant, in which case the resulting  should be queried
-        /// for which culture(s) have been scheduled.
-        /// 
-        IEnumerable GetContentForRelease(DateTime date);
-
-        /// 
-        /// Gets documents in the recycle bin.
-        /// 
-        IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
-
-        /// 
-        /// Gets child documents of a parent.
-        /// 
-        /// The parent identifier.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Query filter.
-        /// Ordering infos.
-        IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
-
-        /// 
-        /// Gets descendant documents of a given parent.
-        /// 
-        /// The parent identifier.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Query filter.
-        /// Ordering infos.
-        IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
-
-        /// 
-        /// Gets paged documents of a content
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords,
-            IQuery filter, Ordering? ordering = null);
-
-        /// 
-        /// Gets paged documents for specified content types
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter, Ordering? ordering = null);
-
-        /// 
-        /// Counts documents of a given document type.
-        /// 
-        int Count(string? documentTypeAlias = null);
-
-        /// 
-        /// Counts published documents of a given document type.
-        /// 
-        int CountPublished(string? documentTypeAlias = null);
-
-        /// 
-        /// Counts child documents of a given parent, of a given document type.
-        /// 
-        int CountChildren(int parentId, string? documentTypeAlias = null);
-
-        /// 
-        /// Counts descendant documents of a given parent, of a given document type.
-        /// 
-        int CountDescendants(int parentId, string? documentTypeAlias = null);
-
-        /// 
-        /// Gets a value indicating whether a document has children.
-        /// 
-        bool HasChildren(int id);
-
-        #endregion
-
-        #region Save, Delete Document
-
-        /// 
-        /// Saves a document.
-        /// 
-        OperationResult Save(IContent content, int? userId = null, ContentScheduleCollection? contentSchedule = null);
-
-        /// 
-        /// Saves documents.
-        /// 
-        // TODO: why only 1 result not 1 per content?!
-        OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes a document.
-        /// 
-        /// 
-        /// This method will also delete associated media files, child content and possibly associated domains.
-        /// This method entirely clears the content from the database.
-        /// 
-        OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes all documents of a given document type.
-        /// 
-        /// 
-        /// All non-deleted descendants of the deleted documents are moved to the recycle bin.
-        /// This operation is potentially dangerous and expensive.
-        /// 
-        void DeleteOfType(int documentTypeId, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes all documents of given document types.
-        /// 
-        /// 
-        /// All non-deleted descendants of the deleted documents are moved to the recycle bin.
-        /// This operation is potentially dangerous and expensive.
-        /// 
-        void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes versions of a document prior to a given date.
-        /// 
-        void DeleteVersions(int id, DateTime date, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes a version of a document.
-        /// 
-        void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Move, Copy, Sort Document
-
-        /// 
-        /// Moves a document under a new parent.
-        /// 
-        void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Copies a document.
-        /// 
-        /// 
-        /// Recursively copies all children.
-        /// 
-        IContent? Copy(IContent content, int parentId, bool relateToOriginal, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Copies a document.
-        /// 
-        /// 
-        /// Optionally recursively copies all children.
-        /// 
-        IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Moves a document to the recycle bin.
-        /// 
-        OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Empties the Recycle Bin by deleting all  that resides in the bin
-        /// 
-        /// Optional Id of the User emptying the Recycle Bin
-        OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Returns true if there is any content in the recycle bin
-        /// 
-        bool RecycleBinSmells();
-
-        /// 
-        /// Sorts documents.
-        /// 
-        OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Sorts documents.
-        /// 
-        OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Publish Document
-
-        /// 
-        /// Saves and publishes a document.
-        /// 
-        /// 
-        /// By default, publishes all variations of the document, but it is possible to specify a culture to be published.
-        /// When a culture is being published, it includes all varying values along with all invariant values.
-        /// The document is *always* saved, even when publishing fails.
-        /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
-        /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty.
-        /// 
-        /// The document to publish.
-        /// The culture to publish.
-        /// The identifier of the user performing the action.
-        PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Saves and publishes a document.
-        /// 
-        /// 
-        /// By default, publishes all variations of the document, but it is possible to specify a culture to be published.
-        /// When a culture is being published, it includes all varying values along with all invariant values.
-        /// The document is *always* saved, even when publishing fails.
-        /// 
-        /// The document to publish.
-        /// The cultures to publish.
-        /// The identifier of the user performing the action.
-        PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Saves and publishes a document branch.
-        /// 
-        /// The root document.
-        /// A value indicating whether to force-publish documents that are not already published.
-        /// A culture, or "*" for all cultures.
-        /// The identifier of the user performing the operation.
-        /// 
-        /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more
-        /// than one culture, see the other overloads of this method.
-        /// The  parameter determines which documents are published. When false,
-        /// only those documents that are already published, are republished. When true, all documents are
-        /// published. The root of the branch is always published, regardless of .
-        /// 
-        IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Saves and publishes a document branch.
-        /// 
-        /// The root document.
-        /// A value indicating whether to force-publish documents that are not already published.
-        /// The cultures to publish.
-        /// The identifier of the user performing the operation.
-        /// 
-        /// The  parameter determines which documents are published. When false,
-        /// only those documents that are already published, are republished. When true, all documents are
-        /// published. The root of the branch is always published, regardless of .
-        /// 
-        IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
-
-        ///// 
-        ///// Saves and publishes a document branch.
-        ///// 
-        ///// The root document.
-        ///// A value indicating whether to force-publish documents that are not already published.
-        ///// A function determining cultures to publish.
-        ///// A function publishing cultures.
-        ///// The identifier of the user performing the operation.
-        ///// 
-        ///// The  parameter determines which documents are published. When false,
-        ///// only those documents that are already published, are republished. When true, all documents are
-        ///// published. The root of the branch is always published, regardless of .
-        ///// The  parameter is a function which determines whether a document has
-        ///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
-        ///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
-        ///// cultures may trigger an unwanted republish.
-        ///// The  parameter is a function to execute to publish cultures, on
-        ///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
-        ///// whether the cultures could be published.
-        ///// 
-        //IEnumerable SaveAndPublishBranch(IContent content, bool force,
-        //    Func> shouldPublish,
-        //    Func, bool> publishCultures,
-        //    int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Unpublishes a document.
-        /// 
-        /// 
-        /// By default, unpublishes the document as a whole, but it is possible to specify a culture to be
-        /// unpublished. Depending on whether that culture is mandatory, and other cultures remain published,
-        /// the document as a whole may or may not remain published.
-        /// If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor
-        /// empty. If the content type is invariant, then culture can be either '*' or null or empty.
-        /// 
-        PublishResult Unpublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Gets a value indicating whether a document is path-publishable.
-        /// 
-        /// A document is path-publishable when all its ancestors are published.
-        bool IsPathPublishable(IContent content);
-
-        /// 
-        /// Gets a value indicating whether a document is path-published.
-        /// 
-        /// A document is path-published when all its ancestors, and the document itself, are published.
-        bool IsPathPublished(IContent content);
-
-        /// 
-        /// Saves a document and raises the "sent to publication" events.
-        /// 
-        bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Publishes and unpublishes scheduled documents.
-        /// 
-        IEnumerable PerformScheduledPublish(DateTime date);
-
-        #endregion
-
-        #region Permissions
-
-        /// 
-        /// Gets permissions assigned to a document.
-        /// 
-        EntityPermissionCollection GetPermissions(IContent content);
-
-        /// 
-        /// Sets the permission of a document.
-        /// 
-        /// Replaces all permissions with the new set of permissions.
-        void SetPermissions(EntityPermissionSet permissionSet);
-
-        /// 
-        /// Assigns a permission to a document.
-        /// 
-        /// Adds the permission to existing permissions.
-        void SetPermission(IContent entity, char permission, IEnumerable groupIds);
-
-        #endregion
-
-        #region Create
-
-        /// 
-        /// Creates a document.
-        /// 
-        IContent Create(string name, Guid parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a document.
-        /// 
-        IContent Create(string name, int parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a document
-        /// 
-        IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a document.
-        /// 
-        IContent Create(string name, IContent? parent, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates and saves a document.
-        /// 
-        IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates and saves a document.
-        /// 
-        IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Rollback
-
-        /// 
-        /// Rolls back the content to a specific version.
-        /// 
-        /// The id of the content node.
-        /// The version id to roll back to.
-        /// An optional culture to roll back.
-        /// The identifier of the user who is performing the roll back.
-        /// 
-        /// When no culture is specified, all cultures are rolled back.
-        /// 
-        OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-    }
+    /// The id of the content node.
+    /// The version id to roll back to.
+    /// An optional culture to roll back.
+    /// The identifier of the user who is performing the roll back.
+    /// 
+    ///     When no culture is specified, all cultures are rolled back.
+    /// 
+    OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Blueprints
+
+    /// 
+    ///     Gets a blueprint.
+    /// 
+    IContent? GetBlueprintById(int id);
+
+    /// 
+    ///     Gets a blueprint.
+    /// 
+    IContent? GetBlueprintById(Guid id);
+
+    /// 
+    ///     Gets blueprints for a content type.
+    /// 
+    IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeId);
+
+    /// 
+    ///     Saves a blueprint.
+    /// 
+    void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes a blueprint.
+    /// 
+    void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a new content item from a blueprint.
+    /// 
+    IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes blueprints for a content type.
+    /// 
+    void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes blueprints for content types.
+    /// 
+    void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Get, Count Documents
+
+    /// 
+    ///     Gets a document.
+    /// 
+    IContent? GetById(int id);
+
+    new
+
+    /// 
+    ///     Gets a document.
+    /// 
+    IContent? GetById(Guid key);
+
+    /// 
+    ///     Gets publish/unpublish schedule for a content node.
+    /// 
+    /// Id of the Content to load schedule for
+    /// 
+    ///     
+    /// 
+    ContentScheduleCollection GetContentScheduleByContentId(int contentId);
+
+    /// 
+    ///     Persists publish/unpublish schedule for a content node.
+    /// 
+    /// 
+    /// 
+    void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule);
+
+    /// 
+    ///     Gets documents.
+    /// 
+    IEnumerable GetByIds(IEnumerable ids);
+
+    /// 
+    ///     Gets documents.
+    /// 
+    IEnumerable GetByIds(IEnumerable ids);
+
+    /// 
+    ///     Gets documents at a given level.
+    /// 
+    IEnumerable GetByLevel(int level);
+
+    /// 
+    ///     Gets the parent of a document.
+    /// 
+    IContent? GetParent(int id);
+
+    /// 
+    ///     Gets the parent of a document.
+    /// 
+    IContent? GetParent(IContent content);
+
+    /// 
+    ///     Gets ancestor documents of a document.
+    /// 
+    IEnumerable GetAncestors(int id);
+
+    /// 
+    ///     Gets ancestor documents of a document.
+    /// 
+    IEnumerable GetAncestors(IContent content);
+
+    /// 
+    ///     Gets all versions of a document.
+    /// 
+    /// Versions are ordered with current first, then most recent first.
+    IEnumerable GetVersions(int id);
+
+    /// 
+    ///     Gets all versions of a document.
+    /// 
+    /// Versions are ordered with current first, then most recent first.
+    IEnumerable GetVersionsSlim(int id, int skip, int take);
+
+    /// 
+    ///     Gets top versions of a document.
+    /// 
+    /// Versions are ordered with current first, then most recent first.
+    IEnumerable GetVersionIds(int id, int topRows);
+
+    /// 
+    ///     Gets a version of a document.
+    /// 
+    IContent? GetVersion(int versionId);
+
+    /// 
+    ///     Gets root-level documents.
+    /// 
+    IEnumerable GetRootContent();
+
+    /// 
+    ///     Gets documents having an expiration date before (lower than, or equal to) a specified date.
+    /// 
+    /// An Enumerable list of  objects
+    /// 
+    ///     The content returned from this method may be culture variant, in which case the resulting
+    ///      should be queried
+    ///     for which culture(s) have been scheduled.
+    /// 
+    IEnumerable GetContentForExpiration(DateTime date);
+
+    /// 
+    ///     Gets documents having a release date before (lower than, or equal to) a specified date.
+    /// 
+    /// An Enumerable list of  objects
+    /// 
+    ///     The content returned from this method may be culture variant, in which case the resulting
+    ///      should be queried
+    ///     for which culture(s) have been scheduled.
+    /// 
+    IEnumerable GetContentForRelease(DateTime date);
+
+    /// 
+    ///     Gets documents in the recycle bin.
+    /// 
+    IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
+
+    /// 
+    ///     Gets child documents of a parent.
+    /// 
+    /// The parent identifier.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Query filter.
+    /// Ordering infos.
+    IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
+
+    /// 
+    ///     Gets descendant documents of a given parent.
+    /// 
+    /// The parent identifier.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Query filter.
+    /// Ordering infos.
+    IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
+
+    /// 
+    ///     Gets paged documents of a content
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering? ordering = null);
+
+    /// 
+    ///     Gets paged documents for specified content types
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering = null);
+
+    /// 
+    ///     Counts documents of a given document type.
+    /// 
+    int Count(string? documentTypeAlias = null);
+
+    /// 
+    ///     Counts published documents of a given document type.
+    /// 
+    int CountPublished(string? documentTypeAlias = null);
+
+    /// 
+    ///     Counts child documents of a given parent, of a given document type.
+    /// 
+    int CountChildren(int parentId, string? documentTypeAlias = null);
+
+    /// 
+    ///     Counts descendant documents of a given parent, of a given document type.
+    /// 
+    int CountDescendants(int parentId, string? documentTypeAlias = null);
+
+    /// 
+    ///     Gets a value indicating whether a document has children.
+    /// 
+    bool HasChildren(int id);
+
+    #endregion
+
+    #region Save, Delete Document
+
+    /// 
+    ///     Saves a document.
+    /// 
+    OperationResult Save(IContent content, int? userId = null, ContentScheduleCollection? contentSchedule = null);
+
+    /// 
+    ///     Saves documents.
+    /// 
+    // TODO: why only 1 result not 1 per content?!
+    OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes a document.
+    /// 
+    /// 
+    ///     This method will also delete associated media files, child content and possibly associated domains.
+    ///     This method entirely clears the content from the database.
+    /// 
+    OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes all documents of a given document type.
+    /// 
+    /// 
+    ///     All non-deleted descendants of the deleted documents are moved to the recycle bin.
+    ///     This operation is potentially dangerous and expensive.
+    /// 
+    void DeleteOfType(int documentTypeId, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes all documents of given document types.
+    /// 
+    /// 
+    ///     All non-deleted descendants of the deleted documents are moved to the recycle bin.
+    ///     This operation is potentially dangerous and expensive.
+    /// 
+    void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes versions of a document prior to a given date.
+    /// 
+    void DeleteVersions(int id, DateTime date, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes a version of a document.
+    /// 
+    void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Move, Copy, Sort Document
+
+    /// 
+    ///     Moves a document under a new parent.
+    /// 
+    void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Copies a document.
+    /// 
+    /// 
+    ///     Recursively copies all children.
+    /// 
+    IContent? Copy(IContent content, int parentId, bool relateToOriginal, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Copies a document.
+    /// 
+    /// 
+    ///     Optionally recursively copies all children.
+    /// 
+    IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Moves a document to the recycle bin.
+    /// 
+    OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Empties the Recycle Bin by deleting all  that resides in the bin
+    /// 
+    /// Optional Id of the User emptying the Recycle Bin
+    OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Returns true if there is any content in the recycle bin
+    /// 
+    bool RecycleBinSmells();
+
+    /// 
+    ///     Sorts documents.
+    /// 
+    OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Sorts documents.
+    /// 
+    OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Publish Document
+
+    /// 
+    ///     Saves and publishes a document.
+    /// 
+    /// 
+    ///     
+    ///         By default, publishes all variations of the document, but it is possible to specify a culture to be
+    ///         published.
+    ///     
+    ///     When a culture is being published, it includes all varying values along with all invariant values.
+    ///     The document is *always* saved, even when publishing fails.
+    ///     
+    ///         If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
+    ///         'empty'. If the content type is invariant, then culture can be either '*' or null or empty.
+    ///     
+    /// 
+    /// The document to publish.
+    /// The culture to publish.
+    /// The identifier of the user performing the action.
+    PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves and publishes a document.
+    /// 
+    /// 
+    ///     
+    ///         By default, publishes all variations of the document, but it is possible to specify a culture to be
+    ///         published.
+    ///     
+    ///     When a culture is being published, it includes all varying values along with all invariant values.
+    ///     The document is *always* saved, even when publishing fails.
+    /// 
+    /// The document to publish.
+    /// The cultures to publish.
+    /// The identifier of the user performing the action.
+    PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves and publishes a document branch.
+    /// 
+    /// The root document.
+    /// A value indicating whether to force-publish documents that are not already published.
+    /// A culture, or "*" for all cultures.
+    /// The identifier of the user performing the operation.
+    /// 
+    ///     
+    ///         Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more
+    ///         than one culture, see the other overloads of this method.
+    ///     
+    ///     
+    ///         The  parameter determines which documents are published. When false,
+    ///         only those documents that are already published, are republished. When true, all documents are
+    ///         published. The root of the branch is always published, regardless of .
+    ///     
+    /// 
+    IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves and publishes a document branch.
+    /// 
+    /// The root document.
+    /// A value indicating whether to force-publish documents that are not already published.
+    /// The cultures to publish.
+    /// The identifier of the user performing the operation.
+    /// 
+    ///     
+    ///         The  parameter determines which documents are published. When false,
+    ///         only those documents that are already published, are republished. When true, all documents are
+    ///         published. The root of the branch is always published, regardless of .
+    ///     
+    /// 
+    IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
+
+    ///// 
+    ///// Saves and publishes a document branch.
+    ///// 
+    ///// The root document.
+    ///// A value indicating whether to force-publish documents that are not already published.
+    ///// A function determining cultures to publish.
+    ///// A function publishing cultures.
+    ///// The identifier of the user performing the operation.
+    ///// 
+    ///// The  parameter determines which documents are published. When false,
+    ///// only those documents that are already published, are republished. When true, all documents are
+    ///// published. The root of the branch is always published, regardless of .
+    ///// The  parameter is a function which determines whether a document has
+    ///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
+    ///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
+    ///// cultures may trigger an unwanted republish.
+    ///// The  parameter is a function to execute to publish cultures, on
+    ///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
+    ///// whether the cultures could be published.
+    ///// 
+    // IEnumerable SaveAndPublishBranch(IContent content, bool force,
+    //    Func> shouldPublish,
+    //    Func, bool> publishCultures,
+    //    int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Unpublishes a document.
+    /// 
+    /// 
+    ///     
+    ///         By default, unpublishes the document as a whole, but it is possible to specify a culture to be
+    ///         unpublished. Depending on whether that culture is mandatory, and other cultures remain published,
+    ///         the document as a whole may or may not remain published.
+    ///     
+    ///     
+    ///         If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor
+    ///         empty. If the content type is invariant, then culture can be either '*' or null or empty.
+    ///     
+    /// 
+    PublishResult Unpublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Gets a value indicating whether a document is path-publishable.
+    /// 
+    /// A document is path-publishable when all its ancestors are published.
+    bool IsPathPublishable(IContent content);
+
+    /// 
+    ///     Gets a value indicating whether a document is path-published.
+    /// 
+    /// A document is path-published when all its ancestors, and the document itself, are published.
+    bool IsPathPublished(IContent content);
+
+    /// 
+    ///     Saves a document and raises the "sent to publication" events.
+    /// 
+    bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Publishes and unpublishes scheduled documents.
+    /// 
+    IEnumerable PerformScheduledPublish(DateTime date);
+
+    #endregion
+
+    #region Permissions
+
+    /// 
+    ///     Gets permissions assigned to a document.
+    /// 
+    EntityPermissionCollection GetPermissions(IContent content);
+
+    /// 
+    ///     Sets the permission of a document.
+    /// 
+    /// Replaces all permissions with the new set of permissions.
+    void SetPermissions(EntityPermissionSet permissionSet);
+
+    /// 
+    ///     Assigns a permission to a document.
+    /// 
+    /// Adds the permission to existing permissions.
+    void SetPermission(IContent entity, char permission, IEnumerable groupIds);
+
+    #endregion
+
+    #region Create
+
+    /// 
+    ///     Creates a document.
+    /// 
+    IContent Create(string name, Guid parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a document.
+    /// 
+    IContent Create(string name, int parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a document
+    /// 
+    IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a document.
+    /// 
+    IContent Create(string name, IContent? parent, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates and saves a document.
+    /// 
+    IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates and saves a document.
+    /// 
+    IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs
index 1916fb49c4..1e07da7d8f 100644
--- a/src/Umbraco.Core/Services/IContentServiceBase.cs
+++ b/src/Umbraco.Core/Services/IContentServiceBase.cs
@@ -1,25 +1,23 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
-{
-    public interface IContentServiceBase : IContentServiceBase
-        where TItem: class, IContentBase
-    {
-        TItem? GetById(Guid key);
-        Attempt Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
-    }
+namespace Umbraco.Cms.Core.Services;
 
-    /// 
-    /// Placeholder for sharing logic between the content, media (and member) services
-    /// TODO: Start sharing the logic!
-    /// 
-    public interface IContentServiceBase : IService
-    {
-        /// 
-        /// Checks/fixes the data integrity of node paths/levels stored in the database
-        /// 
-        ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
-    }
+public interface IContentServiceBase : IContentServiceBase
+    where TItem : class, IContentBase
+{
+    TItem? GetById(Guid key);
+
+    Attempt Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
+}
+
+/// 
+///     Placeholder for sharing logic between the content, media (and member) services
+///     TODO: Start sharing the logic!
+/// 
+public interface IContentServiceBase : IService
+{
+    /// 
+    ///     Checks/fixes the data integrity of node paths/levels stored in the database
+    /// 
+    ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
 }
diff --git a/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs b/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs
index 4b6a78850c..be8cef8fd1 100644
--- a/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs
+++ b/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs
@@ -1,27 +1,30 @@
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Provides the  corresponding to an  object.
+/// 
+public interface IContentTypeBaseServiceProvider
 {
     /// 
-    /// Provides the  corresponding to an  object.
+    ///     Gets the content type service base managing types for the specified content base.
     /// 
-    public interface IContentTypeBaseServiceProvider
-    {
-        /// 
-        /// Gets the content type service base managing types for the specified content base.
-        /// 
-        /// 
-        /// If  is an , this returns the
-        /// , and if it's an , this returns
-        /// the , etc.
-        /// Services are returned as  and can be used
-        /// to retrieve the content / media / whatever type as .
-        /// 
-        IContentTypeBaseService For(IContentBase contentBase);
+    /// 
+    ///     
+    ///         If  is an , this returns the
+    ///         , and if it's an , this returns
+    ///         the , etc.
+    ///     
+    ///     
+    ///         Services are returned as  and can be used
+    ///         to retrieve the content / media / whatever type as .
+    ///     
+    /// 
+    IContentTypeBaseService For(IContentBase contentBase);
 
-        /// 
-        /// Gets the content type of an  object.
-        /// 
-        IContentTypeComposition? GetContentTypeOf(IContentBase contentBase);
-    }
+    /// 
+    ///     Gets the content type of an  object.
+    /// 
+    IContentTypeComposition? GetContentTypeOf(IContentBase contentBase);
 }
diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs
index 4b34baa869..d38139349b 100644
--- a/src/Umbraco.Core/Services/IContentTypeService.cs
+++ b/src/Umbraco.Core/Services/IContentTypeService.cs
@@ -1,35 +1,32 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Manages  objects.
+/// 
+public interface IContentTypeService : IContentTypeBaseService
 {
     /// 
-    /// Manages  objects.
+    ///     Gets all property type aliases.
     /// 
-    public interface IContentTypeService : IContentTypeBaseService
-    {
-        /// 
-        /// Gets all property type aliases.
-        /// 
-        /// 
-        IEnumerable GetAllPropertyTypeAliases();
+    /// 
+    IEnumerable GetAllPropertyTypeAliases();
 
-        /// 
-        /// Gets all content type aliases
-        /// 
-        /// 
-        /// If this list is empty, it will return all content type aliases for media, members and content, otherwise
-        /// it will only return content type aliases for the object types specified
-        /// 
-        /// 
-        IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes);
+    /// 
+    ///     Gets all content type aliases
+    /// 
+    /// 
+    ///     If this list is empty, it will return all content type aliases for media, members and content, otherwise
+    ///     it will only return content type aliases for the object types specified
+    /// 
+    /// 
+    IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes);
 
-        /// 
-        /// Returns all content type Ids for the aliases given
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAllContentTypeIds(string[] aliases);
-    }
+    /// 
+    ///     Returns all content type Ids for the aliases given
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAllContentTypeIds(string[] aliases);
 }
diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
index 5614d87bf3..8e67c78a20 100644
--- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
+++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
@@ -1,96 +1,112 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Provides a common base interface for .
+/// 
+public interface IContentTypeBaseService
 {
     /// 
-    /// Provides a common base interface for .
+    ///     Gets a content type.
     /// 
-    public interface IContentTypeBaseService
-    {
-        /// 
-        /// Gets a content type.
-        /// 
-        IContentTypeComposition? Get(int id);
-    }
+    IContentTypeComposition? Get(int id);
+}
+
+/// 
+///     Provides a common base interface for ,  and
+///     .
+/// 
+/// The type of the item.
+public interface IContentTypeBaseService : IContentTypeBaseService, IService
+    where TItem : IContentTypeComposition
+{
+    /// 
+    ///     Gets a content type.
+    /// 
+    new TItem? Get(int id);
 
     /// 
-    /// Provides a common base interface for ,  and .
+    ///     Gets a content type.
     /// 
-    /// The type of the item.
-    public interface IContentTypeBaseService : IContentTypeBaseService, IService
-        where TItem : IContentTypeComposition
-    {
-        /// 
-        /// Gets a content type.
-        /// 
-        new TItem? Get(int id);
+    TItem? Get(Guid key);
 
-        /// 
-        /// Gets a content type.
-        /// 
-        TItem? Get(Guid key);
+    /// 
+    ///     Gets a content type.
+    /// 
+    TItem? Get(string alias);
 
-        /// 
-        /// Gets a content type.
-        /// 
-        TItem? Get(string alias);
+    int Count();
 
-        int Count();
+    /// 
+    ///     Returns true or false depending on whether content nodes have been created based on the provided content type id.
+    /// 
+    bool HasContentNodes(int id);
 
-        /// 
-        /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
-        /// 
-        bool HasContentNodes(int id);
+    IEnumerable GetAll(params int[] ids);
 
-        IEnumerable GetAll(params int[] ids);
-        IEnumerable GetAll(IEnumerable? ids);
+    IEnumerable GetAll(IEnumerable? ids);
 
-        IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis
-        IEnumerable GetComposedOf(int id); // composition axis
+    IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis
 
-        IEnumerable GetChildren(int id);
-        IEnumerable GetChildren(Guid id);
+    IEnumerable GetComposedOf(int id); // composition axis
 
-        bool HasChildren(int id);
-        bool HasChildren(Guid id);
+    IEnumerable GetChildren(int id);
 
-        void Save(TItem? item, int userId = Constants.Security.SuperUserId);
-        void Save(IEnumerable items, int userId = Constants.Security.SuperUserId);
-        void Delete(TItem item, int userId = Constants.Security.SuperUserId);
-        void Delete(IEnumerable item, int userId = Constants.Security.SuperUserId);
+    IEnumerable GetChildren(Guid id);
 
+    bool HasChildren(int id);
 
-        Attempt ValidateComposition(TItem? compo);
+    bool HasChildren(Guid id);
 
-        /// 
-        /// Given the path of a content item, this will return true if the content item exists underneath a list view content item
-        /// 
-        /// 
-        /// 
-        bool HasContainerInPath(string contentPath);
+    void Save(TItem? item, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets a value indicating whether there is a list view content item in the path.
-        /// 
-        /// 
-        /// 
-        bool HasContainerInPath(params int[] ids);
+    void Save(IEnumerable items, int userId = Constants.Security.SuperUserId);
 
-        Attempt?> CreateContainer(int parentContainerId, Guid key, string name, int userId = Constants.Security.SuperUserId);
-        Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
-        EntityContainer? GetContainer(int containerId);
-        EntityContainer? GetContainer(Guid containerId);
-        IEnumerable GetContainers(int[] containerIds);
-        IEnumerable GetContainers(TItem contentType);
-        IEnumerable GetContainers(string folderName, int level);
-        Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
-        Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
+    void Delete(TItem item, int userId = Constants.Security.SuperUserId);
 
-        Attempt?> Move(TItem moving, int containerId);
-        Attempt?> Copy(TItem copying, int containerId);
-        TItem Copy(TItem original, string alias, string name, int parentId = -1);
-        TItem Copy(TItem original, string alias, string name, TItem parent);
-    }
+    void Delete(IEnumerable item, int userId = Constants.Security.SuperUserId);
+
+    Attempt ValidateComposition(TItem? compo);
+
+    /// 
+    ///     Given the path of a content item, this will return true if the content item exists underneath a list view content
+    ///     item
+    /// 
+    /// 
+    /// 
+    bool HasContainerInPath(string contentPath);
+
+    /// 
+    ///     Gets a value indicating whether there is a list view content item in the path.
+    /// 
+    /// 
+    /// 
+    bool HasContainerInPath(params int[] ids);
+
+    Attempt?> CreateContainer(int parentContainerId, Guid key, string name, int userId = Constants.Security.SuperUserId);
+
+    Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
+
+    EntityContainer? GetContainer(int containerId);
+
+    EntityContainer? GetContainer(Guid containerId);
+
+    IEnumerable GetContainers(int[] containerIds);
+
+    IEnumerable GetContainers(TItem contentType);
+
+    IEnumerable GetContainers(string folderName, int level);
+
+    Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
+
+    Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
+
+    Attempt?> Move(TItem moving, int containerId);
+
+    Attempt?> Copy(TItem copying, int containerId);
+
+    TItem Copy(TItem original, string alias, string name, int parentId = -1);
+
+    TItem Copy(TItem original, string alias, string name, TItem parent);
 }
diff --git a/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
index 86e2988307..d9cbcc0cda 100644
--- a/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
+++ b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
@@ -1,17 +1,14 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Used to filter historic content versions for cleanup.
+/// 
+public interface IContentVersionCleanupPolicy
 {
     /// 
-    /// Used to filter historic content versions for cleanup.
+    ///     Filters a set of candidates historic content versions for cleanup according to policy settings.
     /// 
-    public interface IContentVersionCleanupPolicy
-    {
-        /// 
-        /// Filters a set of candidates historic content versions for cleanup according to policy settings.
-        /// 
-        IEnumerable Apply(DateTime asAtDate, IEnumerable items);
-    }
+    IEnumerable Apply(DateTime asAtDate, IEnumerable items);
 }
diff --git a/src/Umbraco.Core/Services/IContentVersionService.cs b/src/Umbraco.Core/Services/IContentVersionService.cs
index d0f203b2ef..e0d518f52a 100644
--- a/src/Umbraco.Core/Services/IContentVersionService.cs
+++ b/src/Umbraco.Core/Services/IContentVersionService.cs
@@ -1,25 +1,22 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IContentVersionService
 {
-    public interface IContentVersionService
-    {
-        /// 
-        /// Removes historic content versions according to a policy.
-        /// 
-        IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate);
+    /// 
+    ///     Removes historic content versions according to a policy.
+    /// 
+    IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate);
 
-        /// 
-        /// Gets paginated content versions for given content id paginated.
-        /// 
-        /// Thrown when  is invalid.
-        IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null);
+    /// 
+    ///     Gets paginated content versions for given content id paginated.
+    /// 
+    /// Thrown when  is invalid.
+    IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null);
 
-        /// 
-        /// Updates preventCleanup value for given content version.
-        /// 
-        void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1);
-    }
+    /// 
+    ///     Updates preventCleanup value for given content version.
+    /// 
+    void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1);
 }
diff --git a/src/Umbraco.Core/Services/ICultureImpactFactory.cs b/src/Umbraco.Core/Services/ICultureImpactFactory.cs
new file mode 100644
index 0000000000..986b2f1aed
--- /dev/null
+++ b/src/Umbraco.Core/Services/ICultureImpactFactory.cs
@@ -0,0 +1,41 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services;
+
+public interface ICultureImpactFactory
+{
+    /// 
+    /// Creates an impact instance representing the impact of a culture set,
+    /// in the context of a content item variation.
+    /// 
+    /// The culture code.
+    /// A value indicating whether the culture is the default culture.
+    /// The content item.
+    /// 
+    /// Validates that the culture is compatible with the variation.
+    /// 
+    CultureImpact? Create(string culture, bool isDefault, IContent content);
+
+    /// 
+    /// Gets the impact of 'all' cultures (including the invariant culture).
+    /// 
+    CultureImpact ImpactAll();
+
+    /// 
+    /// Gets the impact of the invariant culture.
+    /// 
+    CultureImpact ImpactInvariant();
+
+    /// 
+    /// Creates an impact instance representing the impact of a specific culture.
+    /// 
+    /// The culture code.
+    /// A value indicating whether the culture is the default culture.
+    CultureImpact ImpactExplicit(string? culture, bool isDefault);
+
+    /// 
+    /// Utility method to return the culture used for invariant property errors based on what cultures are being actively saved,
+    /// the default culture and the state of the current content item
+    /// 
+    string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures, string? defaultCulture);
+}
diff --git a/src/Umbraco.Core/Services/IDashboardService.cs b/src/Umbraco.Core/Services/IDashboardService.cs
index 70e3410627..2792b142fe 100644
--- a/src/Umbraco.Core/Services/IDashboardService.cs
+++ b/src/Umbraco.Core/Services/IDashboardService.cs
@@ -1,27 +1,24 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Dashboards;
 using Umbraco.Cms.Core.Models.ContentEditing;
 using Umbraco.Cms.Core.Models.Membership;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IDashboardService
 {
-    public interface IDashboardService
-    {
-        /// 
-        /// Gets dashboard for a specific section/application
-        /// For a specific backoffice user
-        /// 
-        /// 
-        /// 
-        /// 
-        IEnumerable> GetDashboards(string section, IUser? currentUser);
+    /// 
+    ///     Gets dashboard for a specific section/application
+    ///     For a specific backoffice user
+    /// 
+    /// 
+    /// 
+    /// 
+    IEnumerable> GetDashboards(string section, IUser? currentUser);
 
-        /// 
-        /// Gets all dashboards, organized by section, for a user.
-        /// 
-        /// 
-        /// 
-        IDictionary>> GetDashboards(IUser? currentUser);
-
-    }
+    /// 
+    ///     Gets all dashboards, organized by section, for a user.
+    /// 
+    /// 
+    /// 
+    IDictionary>> GetDashboards(IUser? currentUser);
 }
diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs
index 898b24355e..effb4573b4 100644
--- a/src/Umbraco.Core/Services/IDataTypeService.cs
+++ b/src/Umbraco.Core/Services/IDataTypeService.cs
@@ -1,92 +1,103 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the DataType Service, which is an easy access to operations involving 
+/// 
+public interface IDataTypeService : IService
 {
+    /// 
+    ///     Returns a dictionary of content type s and the property type aliases that use a
+    ///     
+    /// 
+    /// 
+    /// 
+    IReadOnlyDictionary> GetReferences(int id);
+
+    Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId);
+
+    Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
+
+    EntityContainer? GetContainer(int containerId);
+
+    EntityContainer? GetContainer(Guid containerId);
+
+    IEnumerable GetContainers(string folderName, int level);
+
+    IEnumerable GetContainers(IDataType dataType);
+
+    IEnumerable GetContainers(int[] containerIds);
+
+    Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
+
+    Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
 
     /// 
-    /// Defines the DataType Service, which is an easy access to operations involving 
+    ///     Gets a  by its Name
     /// 
-    public interface IDataTypeService : IService
-    {
-        /// 
-        /// Returns a dictionary of content type s and the property type aliases that use a 
-        /// 
-        /// 
-        /// 
-        IReadOnlyDictionary> GetReferences(int id);
+    /// Name of the 
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(string name);
 
-        Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId);
-        Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
-        EntityContainer? GetContainer(int containerId);
-        EntityContainer? GetContainer(Guid containerId);
-        IEnumerable GetContainers(string folderName, int level);
-        IEnumerable GetContainers(IDataType dataType);
-        IEnumerable GetContainers(int[] containerIds);
-        Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
-        Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(int id);
 
-        /// 
-        /// Gets a  by its Name
-        /// 
-        /// Name of the 
-        /// 
-        IDataType? GetDataType(string name);
+    /// 
+    ///     Gets a  by its unique guid Id
+    /// 
+    /// Unique guid Id of the DataType
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(Guid id);
 
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// 
-        IDataType? GetDataType(int id);
+    /// 
+    ///     Gets all  objects or those with the ids passed in
+    /// 
+    /// Optional array of Ids
+    /// An enumerable list of  objects
+    IEnumerable GetAll(params int[] ids);
 
-        /// 
-        /// Gets a  by its unique guid Id
-        /// 
-        /// Unique guid Id of the DataType
-        /// 
-        IDataType? GetDataType(Guid id);
+    /// 
+    ///     Saves an 
+    /// 
+    ///  to save
+    /// Id of the user issuing the save
+    void Save(IDataType dataType, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets all  objects or those with the ids passed in
-        /// 
-        /// Optional array of Ids
-        /// An enumerable list of  objects
-        IEnumerable GetAll(params int[] ids);
+    /// 
+    ///     Saves a collection of 
+    /// 
+    ///  to save
+    /// Id of the user issuing the save
+    void Save(IEnumerable dataTypeDefinitions, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves an 
-        /// 
-        ///  to save
-        /// Id of the user issuing the save
-        void Save(IDataType dataType, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes an 
+    /// 
+    /// 
+    ///     Please note that deleting a  will remove
+    ///     all the  data that references this .
+    /// 
+    ///  to delete
+    /// Id of the user issuing the deletion
+    void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a collection of 
-        /// 
-        ///  to save
-        /// Id of the user issuing the save
-        void Save(IEnumerable dataTypeDefinitions, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  by its control Id
+    /// 
+    /// Alias of the property editor
+    /// Collection of  objects with a matching control id
+    IEnumerable GetByEditorAlias(string propertyEditorAlias);
 
-        /// 
-        /// Deletes an 
-        /// 
-        /// 
-        /// Please note that deleting a  will remove
-        /// all the  data that references this .
-        /// 
-        ///  to delete
-        /// Id of the user issuing the deletion
-        void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Gets a  by its control Id
-        /// 
-        /// Alias of the property editor
-        /// Collection of  objects with a matching control id
-        IEnumerable GetByEditorAlias(string propertyEditorAlias);
-
-        Attempt?> Move(IDataType toMove, int parentId);
-    }
+    Attempt?> Move(IDataType toMove, int parentId);
 }
diff --git a/src/Umbraco.Core/Services/IDataTypeUsageService.cs b/src/Umbraco.Core/Services/IDataTypeUsageService.cs
new file mode 100644
index 0000000000..2514c9bef9
--- /dev/null
+++ b/src/Umbraco.Core/Services/IDataTypeUsageService.cs
@@ -0,0 +1,11 @@
+namespace Umbraco.Cms.Core.Services;
+
+public interface IDataTypeUsageService
+{
+    /// 
+    /// Checks if there are any saved property values using a given data type.
+    /// 
+    /// The ID of the data type to check.
+    /// True if there are any property values using the data type, otherwise false.
+    bool HasSavedValues(int dataTypeId);
+}
diff --git a/src/Umbraco.Core/Services/IDomainService.cs b/src/Umbraco.Core/Services/IDomainService.cs
index 952eaecfde..54a006ecb1 100644
--- a/src/Umbraco.Core/Services/IDomainService.cs
+++ b/src/Umbraco.Core/Services/IDomainService.cs
@@ -1,16 +1,20 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IDomainService : IService
 {
-    public interface IDomainService : IService
-    {
-        bool Exists(string domainName);
-        Attempt Delete(IDomain domain);
-        IDomain? GetByName(string name);
-        IDomain? GetById(int id);
-        IEnumerable GetAll(bool includeWildcards);
-        IEnumerable GetAssignedDomains(int contentId, bool includeWildcards);
-        Attempt Save(IDomain domainEntity);
-    }
+    bool Exists(string domainName);
+
+    Attempt Delete(IDomain domain);
+
+    IDomain? GetByName(string name);
+
+    IDomain? GetById(int id);
+
+    IEnumerable GetAll(bool includeWildcards);
+
+    IEnumerable GetAssignedDomains(int contentId, bool includeWildcards);
+
+    Attempt Save(IDomain domainEntity);
 }
diff --git a/src/Umbraco.Core/Services/IEditorConfigurationParser.cs b/src/Umbraco.Core/Services/IEditorConfigurationParser.cs
index 8dc1210d11..1a37045490 100644
--- a/src/Umbraco.Core/Services/IEditorConfigurationParser.cs
+++ b/src/Umbraco.Core/Services/IEditorConfigurationParser.cs
@@ -1,11 +1,12 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.PropertyEditors;
 
 namespace Umbraco.Cms.Core.Services;
 
 public interface IEditorConfigurationParser
 {
-    TConfiguration? ParseFromConfigurationEditor(IDictionary? editorValues, IEnumerable fields);
+    TConfiguration? ParseFromConfigurationEditor(
+        IDictionary? editorValues,
+        IEnumerable fields);
 
     Dictionary ParseToConfigurationEditor(TConfiguration? configuration);
 }
diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs
index 66298aba1d..74a416a8fe 100644
--- a/src/Umbraco.Core/Services/IEntityService.cs
+++ b/src/Umbraco.Core/Services/IEntityService.cs
@@ -1,252 +1,279 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IEntityService
 {
-    public interface IEntityService
-    {
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The identifier of the entity.
-        IEntitySlim? Get(int id);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The identifier of the entity.
+    IEntitySlim? Get(int id);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The unique key of the entity.
-        IEntitySlim? Get(Guid key);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The unique key of the entity.
+    IEntitySlim? Get(Guid key);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the entity.
-        IEntitySlim? Get(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the entity.
+    IEntitySlim? Get(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The unique key of the entity.
-        /// The object type of the entity.
-        IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The unique key of the entity.
+    /// The object type of the entity.
+    IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The type used to determine the object type of the entity.
-        /// The identifier of the entity.
-        IEntitySlim? Get(int id) where T : IUmbracoEntity;
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The type used to determine the object type of the entity.
+    /// The identifier of the entity.
+    IEntitySlim? Get(int id)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The type used to determine the object type of the entity.
-        /// The unique key of the entity.
-        IEntitySlim? Get(Guid key) where T : IUmbracoEntity;
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The type used to determine the object type of the entity.
+    /// The unique key of the entity.
+    IEntitySlim? Get(Guid key)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Determines whether an entity exists.
-        /// 
-        /// The identifier of the entity.
-        bool Exists(int id);
+    /// 
+    ///     Determines whether an entity exists.
+    /// 
+    /// The identifier of the entity.
+    bool Exists(int id);
 
-        /// 
-        /// Determines whether an entity exists.
-        /// 
-        /// The unique key of the entity.
-        bool Exists(Guid key);
+    /// 
+    ///     Determines whether an entity exists.
+    /// 
+    /// The unique key of the entity.
+    bool Exists(Guid key);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The type used to determine the object type of the entities.
-        IEnumerable GetAll() where T : IUmbracoEntity;
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The type used to determine the object type of the entities.
+    IEnumerable GetAll()
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The type used to determine the object type of the entities.
-        /// The identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(params int[] ids) where T : IUmbracoEntity;
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The type used to determine the object type of the entities.
+    /// The identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(params int[] ids)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        IEnumerable GetAll(UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    IEnumerable GetAll(UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        IEnumerable GetAll(Guid objectType);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    IEnumerable GetAll(Guid objectType);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(Guid objectType, params int[] ids);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(Guid objectType, params int[] ids);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The type used to determine the object type of the entities.
-        /// The unique identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(params Guid[] keys) where T : IUmbracoEntity;
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The type used to determine the object type of the entities.
+    /// The unique identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(params Guid[] keys)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The unique identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The unique identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The unique identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(Guid objectType, params Guid[] keys);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The unique identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(Guid objectType, params Guid[] keys);
 
-        /// 
-        /// Gets entities at root.
-        /// 
-        /// The object type of the entities.
-        IEnumerable GetRootEntities(UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets entities at root.
+    /// 
+    /// The object type of the entities.
+    IEnumerable GetRootEntities(UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the parent of an entity.
-        /// 
-        /// The identifier of the entity.
-        IEntitySlim? GetParent(int id);
+    /// 
+    ///     Gets the parent of an entity.
+    /// 
+    /// The identifier of the entity.
+    IEntitySlim? GetParent(int id);
 
-        /// 
-        /// Gets the parent of an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the parent.
-        IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the parent of an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the parent.
+    IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the children of an entity.
-        /// 
-        /// The identifier of the entity.
-        IEnumerable GetChildren(int id);
+    /// 
+    ///     Gets the children of an entity.
+    /// 
+    /// The identifier of the entity.
+    IEnumerable GetChildren(int id);
 
-        /// 
-        /// Gets the children of an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the children.
-        IEnumerable GetChildren(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the children of an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the children.
+    IEnumerable GetChildren(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the descendants of an entity.
-        /// 
-        /// The identifier of the entity.
-        IEnumerable GetDescendants(int id);
+    /// 
+    ///     Gets the descendants of an entity.
+    /// 
+    /// The identifier of the entity.
+    IEnumerable GetDescendants(int id);
 
-        /// 
-        /// Gets the descendants of an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the descendants.
-        IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the descendants of an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the descendants.
+    IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets children of an entity.
-        /// 
-        IEnumerable GetPagedChildren(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets children of an entity.
+    /// 
+    IEnumerable GetPagedChildren(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets descendants of an entity.
-        /// 
-        IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets descendants of an entity.
+    /// 
+    IEnumerable GetPagedDescendants(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets descendants of entities.
-        /// 
-        IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets descendants of entities.
+    /// 
+    IEnumerable GetPagedDescendants(
+        IEnumerable ids,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        // TODO: Do we really need this? why not just pass in -1
-        /// 
-        /// Gets descendants of root.
-        /// 
-        IEnumerable GetPagedDescendants(UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null, bool includeTrashed = true);
+    // TODO: Do we really need this? why not just pass in -1
 
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(int id);
+    /// 
+    ///     Gets descendants of root.
+    /// 
+    IEnumerable GetPagedDescendants(
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null,
+        bool includeTrashed = true);
 
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(Guid key);
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(int id);
 
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(IUmbracoEntity entity);
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(Guid key);
 
-        /// 
-        /// Gets the CLR type of an entity.
-        /// 
-        Type? GetEntityType(int id);
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(IUmbracoEntity entity);
 
-        /// 
-        /// Gets the integer identifier corresponding to a unique Guid identifier.
-        /// 
-        Attempt GetId(Guid key, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the CLR type of an entity.
+    /// 
+    Type? GetEntityType(int id);
 
-        /// 
-        /// Gets the integer identifier corresponding to a Udi.
-        /// 
-        Attempt GetId(Udi udi);
+    /// 
+    ///     Gets the integer identifier corresponding to a unique Guid identifier.
+    /// 
+    Attempt GetId(Guid key, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the unique Guid identifier corresponding to an integer identifier.
-        /// 
-        Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType);
+    /// 
+    ///     Gets the integer identifier corresponding to a Udi.
+    /// 
+    Attempt GetId(Udi udi);
 
-        /// 
-        /// Gets paths for entities.
-        /// 
-        IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids);
+    /// 
+    ///     Gets the unique Guid identifier corresponding to an integer identifier.
+    /// 
+    Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType);
 
-        /// 
-        /// Gets paths for entities.
-        /// 
-        IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys);
+    /// 
+    ///     Gets paths for entities.
+    /// 
+    IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids);
 
-        /// 
-        /// Reserves an identifier for a key.
-        /// 
-        /// They key.
-        /// The identifier.
-        /// When a new content or a media is saved with the key, it will have the reserved identifier.
-        int ReserveId(Guid key);
-    }
+    /// 
+    ///     Gets paths for entities.
+    /// 
+    IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys);
+
+    /// 
+    ///     Reserves an identifier for a key.
+    /// 
+    /// They key.
+    /// The identifier.
+    /// When a new content or a media is saved with the key, it will have the reserved identifier.
+    int ReserveId(Guid key);
 }
diff --git a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs
index fd68a9dfca..5ada7ab5b6 100644
--- a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs
+++ b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs
@@ -1,90 +1,90 @@
-using System;
-using System.Collections.Generic;
 using System.Xml.Linq;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Serializes entities to XML
+/// 
+public interface IEntityXmlSerializer
 {
     /// 
-    /// Serializes entities to XML
+    ///     Exports an IContent item as an XElement.
     /// 
-    public interface IEntityXmlSerializer
-    {
-        /// 
-        /// Exports an IContent item as an XElement.
-        /// 
-        XElement Serialize(IContent content,
-                bool published,
-                bool withDescendants = false) // TODO: take care of usage! only used for the packager
-            ;
+    XElement Serialize(
+        IContent content,
+        bool published,
+        bool withDescendants = false) // TODO: take care of usage! only used for the packager
+        ;
 
-        /// 
-        /// Exports an IMedia item as an XElement.
-        /// 
-        XElement Serialize(
-            IMedia media,
-            bool withDescendants = false,
-            Action? onMediaItemSerialized = null);
+    /// 
+    ///     Exports an IMedia item as an XElement.
+    /// 
+    XElement Serialize(
+        IMedia media,
+        bool withDescendants = false,
+        Action? onMediaItemSerialized = null);
 
-        /// 
-        /// Exports an IMember item as an XElement.
-        /// 
-        XElement Serialize(IMember member);
+    /// 
+    ///     Exports an IMember item as an XElement.
+    /// 
+    XElement Serialize(IMember member);
 
-        /// 
-        /// Exports a list of Data Types
-        /// 
-        /// List of data types to export
-        ///  containing the xml representation of the IDataTypeDefinition objects
-        XElement Serialize(IEnumerable dataTypeDefinitions);
+    /// 
+    ///     Exports a list of Data Types
+    /// 
+    /// List of data types to export
+    ///  containing the xml representation of the IDataTypeDefinition objects
+    XElement Serialize(IEnumerable dataTypeDefinitions);
 
-        XElement Serialize(IDataType dataType);
+    XElement Serialize(IDataType dataType);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of dictionary items to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem objects
-        XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true);
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of dictionary items to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem objects
+    XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true);
 
-        /// 
-        /// Exports a single  item to xml as an 
-        /// 
-        /// Dictionary Item to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem object
-        XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren);
+    /// 
+    ///     Exports a single  item to xml as an 
+    /// 
+    /// Dictionary Item to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem object
+    XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren);
 
-        XElement Serialize(IStylesheet stylesheet, bool includeProperties);
+    XElement Serialize(IStylesheet stylesheet, bool includeProperties);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of Languages to export
-        ///  containing the xml representation of the ILanguage objects
-        XElement Serialize(IEnumerable languages);
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of Languages to export
+    ///  containing the xml representation of the ILanguage objects
+    XElement Serialize(IEnumerable languages);
 
-        XElement Serialize(ILanguage language);
-        XElement Serialize(ITemplate template);
+    XElement Serialize(ILanguage language);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// 
-        /// 
-        XElement Serialize(IEnumerable templates);
+    XElement Serialize(ITemplate template);
 
-        XElement Serialize(IMediaType mediaType);
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// 
+    /// 
+    XElement Serialize(IEnumerable templates);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// Macros to export
-        ///  containing the xml representation of the IMacro objects
-        XElement Serialize(IEnumerable macros);
+    XElement Serialize(IMediaType mediaType);
 
-        XElement Serialize(IMacro macro);
-        XElement Serialize(IContentType contentType);
-    }
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// Macros to export
+    ///  containing the xml representation of the IMacro objects
+    XElement Serialize(IEnumerable macros);
+
+    XElement Serialize(IMacro macro);
+
+    XElement Serialize(IContentType contentType);
 }
diff --git a/src/Umbraco.Core/Services/IExamineIndexCountService.cs b/src/Umbraco.Core/Services/IExamineIndexCountService.cs
index 05c5f7d554..8d85e17e04 100644
--- a/src/Umbraco.Core/Services/IExamineIndexCountService.cs
+++ b/src/Umbraco.Core/Services/IExamineIndexCountService.cs
@@ -1,7 +1,6 @@
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IExamineIndexCountService
 {
-    public interface IExamineIndexCountService
-    {
-        public int GetCount();
-    }
+    public int GetCount();
 }
diff --git a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
index bc31f54f8b..54f827c899 100644
--- a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
+++ b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
@@ -1,54 +1,51 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Security;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IExternalLoginWithKeyService : IService
 {
-    public interface IExternalLoginWithKeyService : IService
-    {
-        /// 
-        /// Returns all user logins assigned
-        /// 
-        IEnumerable GetExternalLogins(Guid userOrMemberKey);
+    /// 
+    ///     Returns all user logins assigned
+    /// 
+    IEnumerable GetExternalLogins(Guid userOrMemberKey);
 
-        /// 
-        /// Returns all user login tokens assigned
-        /// 
-        IEnumerable GetExternalLoginTokens(Guid userOrMemberKey);
+    /// 
+    ///     Returns all user login tokens assigned
+    /// 
+    IEnumerable GetExternalLoginTokens(Guid userOrMemberKey);
 
-        /// 
-        /// Returns all logins matching the login info - generally there should only be one but in some cases
-        /// there might be more than one depending on if an administrator has been editing/removing members
-        /// 
-        IEnumerable Find(string loginProvider, string providerKey);
+    /// 
+    ///     Returns all logins matching the login info - generally there should only be one but in some cases
+    ///     there might be more than one depending on if an administrator has been editing/removing members
+    /// 
+    IEnumerable Find(string loginProvider, string providerKey);
 
-        /// 
-        /// Saves the external logins associated with the user
-        /// 
-        /// 
-        /// The user or member key associated with the logins
-        /// 
-        /// 
-        /// 
-        /// This will replace all external login provider information for the user
-        /// 
-        void Save(Guid userOrMemberKey, IEnumerable logins);
+    /// 
+    ///     Saves the external logins associated with the user
+    /// 
+    /// 
+    ///     The user or member key associated with the logins
+    /// 
+    /// 
+    /// 
+    ///     This will replace all external login provider information for the user
+    /// 
+    void Save(Guid userOrMemberKey, IEnumerable logins);
 
-        /// 
-        /// Saves the external login tokens associated with the user
-        /// 
-        /// 
-        /// The user or member key associated with the logins
-        /// 
-        /// 
-        /// 
-        /// This will replace all external login tokens for the user
-        /// 
-        void Save(Guid userOrMemberKey,IEnumerable tokens);
+    /// 
+    ///     Saves the external login tokens associated with the user
+    /// 
+    /// 
+    ///     The user or member key associated with the logins
+    /// 
+    /// 
+    /// 
+    ///     This will replace all external login tokens for the user
+    /// 
+    void Save(Guid userOrMemberKey, IEnumerable tokens);
 
-        /// 
-        /// Deletes all user logins - normally used when a member is deleted
-        /// 
-        void DeleteUserLogins(Guid userOrMemberKey);
-    }
+    /// 
+    ///     Deletes all user logins - normally used when a member is deleted
+    /// 
+    void DeleteUserLogins(Guid userOrMemberKey);
 }
diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs
index 6cbc06208c..53d7d004b2 100644
--- a/src/Umbraco.Core/Services/IFileService.cs
+++ b/src/Umbraco.Core/Services/IFileService.cs
@@ -1,307 +1,321 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the File Service, which is an easy access to operations involving  objects like
+///     Scripts, Stylesheets and Templates
+/// 
+public interface IFileService : IService
 {
+    [Obsolete("Please use SnippetCollection.GetPartialViewSnippetNames() or SnippetCollection.GetPartialViewMacroSnippetNames() instead. Scheduled for removal in V12.")]
+    IEnumerable GetPartialViewSnippetNames(params string[] filterNames);
+
+    void CreatePartialViewFolder(string folderPath);
+
+    void CreatePartialViewMacroFolder(string folderPath);
+
+    void DeletePartialViewFolder(string folderPath);
+
+    void DeletePartialViewMacroFolder(string folderPath);
+
     /// 
-    /// Defines the File Service, which is an easy access to operations involving  objects like Scripts, Stylesheets and Templates
+    ///     Gets a list of all  objects
     /// 
-    public interface IFileService : IService
-    {
-        IEnumerable GetPartialViewSnippetNames(params string[] filterNames);
-        void CreatePartialViewFolder(string folderPath);
-        void CreatePartialViewMacroFolder(string folderPath);
-        void DeletePartialViewFolder(string folderPath);
-        void DeletePartialViewMacroFolder(string folderPath);
+    /// An enumerable list of  objects
+    IEnumerable GetPartialViews(params string[] names);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetPartialViews(params string[] names);
+    IPartialView? GetPartialView(string path);
 
-        IPartialView? GetPartialView(string path);
-        IPartialView? GetPartialViewMacro(string path);
-        Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
-        Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
-        bool DeletePartialView(string path, int? userId = null);
-        bool DeletePartialViewMacro(string path, int? userId = null);
-        Attempt SavePartialView(IPartialView partialView, int? userId = null);
-        Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null);
+    IPartialView? GetPartialViewMacro(string path);
 
-        /// 
-        /// Gets the content of a partial view as a stream.
-        /// 
-        /// The filesystem path to the partial view.
-        /// The content of the partial view.
-        Stream GetPartialViewFileContentStream(string filepath);
+    Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Sets the content of a partial view.
-        /// 
-        /// The filesystem path to the partial view.
-        /// The content of the partial view.
-        void SetPartialViewFileContent(string filepath, Stream content);
+    Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the size of a partial view.
-        /// 
-        /// The filesystem path to the partial view.
-        /// The size of the partial view.
-        long GetPartialViewFileSize(string filepath);
+    bool DeletePartialView(string path, int? userId = null);
 
-        /// 
-        /// Gets the content of a macro partial view as a stream.
-        /// 
-        /// The filesystem path to the macro partial view.
-        /// The content of the macro partial view.
-        Stream GetPartialViewMacroFileContentStream(string filepath);
+    bool DeletePartialViewMacro(string path, int? userId = null);
 
-        /// 
-        /// Sets the content of a macro partial view.
-        /// 
-        /// The filesystem path to the macro partial view.
-        /// The content of the macro partial view.
-        void SetPartialViewMacroFileContent(string filepath, Stream content);
+    Attempt SavePartialView(IPartialView partialView, int? userId = null);
 
-        /// 
-        /// Gets the size of a macro partial view.
-        /// 
-        /// The filesystem path to the macro partial view.
-        /// The size of the macro partial view.
-        long GetPartialViewMacroFileSize(string filepath);
+    Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetStylesheets(params string[] paths);
+    /// 
+    ///     Gets the content of a partial view as a stream.
+    /// 
+    /// The filesystem path to the partial view.
+    /// The content of the partial view.
+    Stream GetPartialViewFileContentStream(string filepath);
 
-        /// 
-        /// Gets a  object by its name
-        /// 
-        /// Path of the stylesheet incl. extension
-        /// A  object
-        IStylesheet? GetStylesheet(string? path);
+    /// 
+    ///     Sets the content of a partial view.
+    /// 
+    /// The filesystem path to the partial view.
+    /// The content of the partial view.
+    void SetPartialViewFileContent(string filepath, Stream content);
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the stylesheet
-        void SaveStylesheet(IStylesheet? stylesheet, int? userId = null);
+    /// 
+    ///     Gets the size of a partial view.
+    /// 
+    /// The filesystem path to the partial view.
+    /// The size of the partial view.
+    long GetPartialViewFileSize(string filepath);
 
-        /// 
-        /// Deletes a stylesheet by its name
-        /// 
-        /// Name incl. extension of the Stylesheet to delete
-        /// Optional id of the user deleting the stylesheet
-        void DeleteStylesheet(string path, int? userId = null);
+    /// 
+    ///     Gets the content of a macro partial view as a stream.
+    /// 
+    /// The filesystem path to the macro partial view.
+    /// The content of the macro partial view.
+    Stream GetPartialViewMacroFileContentStream(string filepath);
 
-        /// 
-        /// Creates a folder for style sheets
-        /// 
-        /// 
-        /// 
-        void CreateStyleSheetFolder(string folderPath);
+    /// 
+    ///     Sets the content of a macro partial view.
+    /// 
+    /// The filesystem path to the macro partial view.
+    /// The content of the macro partial view.
+    void SetPartialViewMacroFileContent(string filepath, Stream content);
 
-        /// 
-        /// Deletes a folder for style sheets
-        /// 
-        /// 
-        void DeleteStyleSheetFolder(string folderPath);
+    /// 
+    ///     Gets the size of a macro partial view.
+    /// 
+    /// The filesystem path to the macro partial view.
+    /// The size of the macro partial view.
+    long GetPartialViewMacroFileSize(string filepath);
 
-        /// 
-        /// Gets the content of a stylesheet as a stream.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The content of the stylesheet.
-        Stream GetStylesheetFileContentStream(string filepath);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetStylesheets(params string[] paths);
 
-        /// 
-        /// Sets the content of a stylesheet.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The content of the stylesheet.
-        void SetStylesheetFileContent(string filepath, Stream content);
+    /// 
+    ///     Gets a  object by its name
+    /// 
+    /// Path of the stylesheet incl. extension
+    /// A  object
+    IStylesheet? GetStylesheet(string? path);
 
-        /// 
-        /// Gets the size of a stylesheet.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The size of the stylesheet.
-        long GetStylesheetFileSize(string filepath);
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the stylesheet
+    void SaveStylesheet(IStylesheet? stylesheet, int? userId = null);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetScripts(params string[] names);
+    /// 
+    ///     Deletes a stylesheet by its name
+    /// 
+    /// Name incl. extension of the Stylesheet to delete
+    /// Optional id of the user deleting the stylesheet
+    void DeleteStylesheet(string path, int? userId = null);
 
-        /// 
-        /// Gets a  object by its name
-        /// 
-        /// Name of the script incl. extension
-        /// A  object
-        IScript? GetScript(string? name);
+    /// 
+    ///     Creates a folder for style sheets
+    /// 
+    /// 
+    /// 
+    void CreateStyleSheetFolder(string folderPath);
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the script
-        void SaveScript(IScript? script, int? userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes a folder for style sheets
+    /// 
+    /// 
+    void DeleteStyleSheetFolder(string folderPath);
 
-        /// 
-        /// Deletes a script by its name
-        /// 
-        /// Name incl. extension of the Script to delete
-        /// Optional id of the user deleting the script
-        void DeleteScript(string path, int? userId = null);
+    /// 
+    ///     Gets the content of a stylesheet as a stream.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The content of the stylesheet.
+    Stream GetStylesheetFileContentStream(string filepath);
 
-        /// 
-        /// Creates a folder for scripts
-        /// 
-        /// 
-        /// 
-        void CreateScriptFolder(string folderPath);
+    /// 
+    ///     Sets the content of a stylesheet.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The content of the stylesheet.
+    void SetStylesheetFileContent(string filepath, Stream content);
 
-        /// 
-        /// Deletes a folder for scripts
-        /// 
-        /// 
-        void DeleteScriptFolder(string folderPath);
+    /// 
+    ///     Gets the size of a stylesheet.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The size of the stylesheet.
+    long GetStylesheetFileSize(string filepath);
 
-        /// 
-        /// Gets the content of a script file as a stream.
-        /// 
-        /// The filesystem path to the script.
-        /// The content of the script file.
-        Stream GetScriptFileContentStream(string filepath);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetScripts(params string[] names);
 
-        /// 
-        /// Sets the content of a script file.
-        /// 
-        /// The filesystem path to the script.
-        /// The content of the script file.
-        void SetScriptFileContent(string filepath, Stream content);
+    /// 
+    ///     Gets a  object by its name
+    /// 
+    /// Name of the script incl. extension
+    /// A  object
+    IScript? GetScript(string? name);
 
-        /// 
-        /// Gets the size of a script file.
-        /// 
-        /// The filesystem path to the script file.
-        /// The size of the script file.
-        long GetScriptFileSize(string filepath);
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the script
+    void SaveScript(IScript? script, int? userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetTemplates(params string[] aliases);
+    /// 
+    ///     Deletes a script by its name
+    /// 
+    /// Name incl. extension of the Script to delete
+    /// Optional id of the user deleting the script
+    void DeleteScript(string path, int? userId = null);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetTemplates(int masterTemplateId);
+    /// 
+    ///     Creates a folder for scripts
+    /// 
+    /// 
+    /// 
+    void CreateScriptFolder(string folderPath);
 
-        /// 
-        /// Gets a  object by its alias.
-        /// 
-        /// The alias of the template.
-        /// The  object matching the alias, or null.
-        ITemplate? GetTemplate(string? alias);
+    /// 
+    ///     Deletes a folder for scripts
+    /// 
+    /// 
+    void DeleteScriptFolder(string folderPath);
 
-        /// 
-        /// Gets a  object by its identifier.
-        /// 
-        /// The identifier of the template.
-        /// The  object matching the identifier, or null.
-        ITemplate? GetTemplate(int id);
+    /// 
+    ///     Gets the content of a script file as a stream.
+    /// 
+    /// The filesystem path to the script.
+    /// The content of the script file.
+    Stream GetScriptFileContentStream(string filepath);
 
-        /// 
-        /// Gets a  object by its guid identifier.
-        /// 
-        /// The guid identifier of the template.
-        /// The  object matching the identifier, or null.
-        ITemplate? GetTemplate(Guid id);
+    /// 
+    ///     Sets the content of a script file.
+    /// 
+    /// The filesystem path to the script.
+    /// The content of the script file.
+    void SetScriptFileContent(string filepath, Stream content);
 
-        /// 
-        /// Gets the template descendants
-        /// 
-        /// 
-        /// 
-        IEnumerable GetTemplateDescendants(int masterTemplateId);
+    /// 
+    ///     Gets the size of a script file.
+    /// 
+    /// The filesystem path to the script file.
+    /// The size of the script file.
+    long GetScriptFileSize(string filepath);
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the template
-        void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetTemplates(params string[] aliases);
 
-        /// 
-        /// Creates a template for a content type
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The template created
-        /// 
-        Attempt?> CreateTemplateForContentType(string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetTemplates(int masterTemplateId);
 
-        ITemplate CreateTemplateWithIdentity(string? name, string? alias, string? content, ITemplate? masterTemplate = null, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  object by its alias.
+    /// 
+    /// The alias of the template.
+    /// The  object matching the alias, or null.
+    ITemplate? GetTemplate(string? alias);
 
-        /// 
-        /// Deletes a template by its alias
-        /// 
-        /// Alias of the  to delete
-        /// Optional id of the user deleting the template
-        void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  object by its identifier.
+    /// 
+    /// The identifier of the template.
+    /// The  object matching the identifier, or null.
+    ITemplate? GetTemplate(int id);
 
-        /// 
-        /// Saves a collection of  objects
-        /// 
-        /// List of  to save
-        /// Optional id of the user
-        void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  object by its guid identifier.
+    /// 
+    /// The guid identifier of the template.
+    /// The  object matching the identifier, or null.
+    ITemplate? GetTemplate(Guid id);
 
-        /// 
-        /// Gets the content of a template as a stream.
-        /// 
-        /// The filesystem path to the template.
-        /// The content of the template.
-        Stream GetTemplateFileContentStream(string filepath);
+    /// 
+    ///     Gets the template descendants
+    /// 
+    /// 
+    /// 
+    IEnumerable GetTemplateDescendants(int masterTemplateId);
 
-        /// 
-        /// Sets the content of a template.
-        /// 
-        /// The filesystem path to the template.
-        /// The content of the template.
-        void SetTemplateFileContent(string filepath, Stream content);
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the template
+    void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the size of a template.
-        /// 
-        /// The filesystem path to the template.
-        /// The size of the template.
-        long GetTemplateFileSize(string filepath);
+    /// 
+    ///     Creates a template for a content type
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     The template created
+    /// 
+    Attempt?> CreateTemplateForContentType(
+        string contentTypeAlias,
+        string? contentTypeName,
+        int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the content of a macro partial view snippet as a string
-        /// 
-        /// The name of the snippet
-        /// 
-        string GetPartialViewMacroSnippetContent(string snippetName);
+    ITemplate CreateTemplateWithIdentity(string? name, string? alias, string? content, ITemplate? masterTemplate = null, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the content of a partial view snippet as a string.
-        /// 
-        /// The name of the snippet
-        /// The content of the partial view.
-        string GetPartialViewSnippetContent(string snippetName);
-    }
+    /// 
+    ///     Deletes a template by its alias
+    /// 
+    /// Alias of the  to delete
+    /// Optional id of the user deleting the template
+    void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves a collection of  objects
+    /// 
+    /// List of  to save
+    /// Optional id of the user
+    void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Gets the content of a template as a stream.
+    /// 
+    /// The filesystem path to the template.
+    /// The content of the template.
+    Stream GetTemplateFileContentStream(string filepath);
+
+    /// 
+    ///     Sets the content of a template.
+    /// 
+    /// The filesystem path to the template.
+    /// The content of the template.
+    void SetTemplateFileContent(string filepath, Stream content);
+
+    /// 
+    ///     Gets the size of a template.
+    /// 
+    /// The filesystem path to the template.
+    /// The size of the template.
+    long GetTemplateFileSize(string filepath);
+
+    /// 
+    ///     Gets the content of a macro partial view snippet as a string
+    /// 
+    /// The name of the snippet
+    /// 
+    [Obsolete("Please use SnippetCollection.GetPartialViewMacroSnippetContent instead. Scheduled for removal in V12.")]
+    string GetPartialViewMacroSnippetContent(string snippetName);
+
+    /// 
+    ///     Gets the content of a partial view snippet as a string.
+    /// 
+    /// The name of the snippet
+    /// The content of the partial view.
+    [Obsolete("Please use SnippetCollection.GetPartialViewSnippetContent instead. Scheduled for removal in V12.")]
+    string GetPartialViewSnippetContent(string snippetName);
 }
diff --git a/src/Umbraco.Core/Services/IIconService.cs b/src/Umbraco.Core/Services/IIconService.cs
index 0b215c481c..8aff7e8920 100644
--- a/src/Umbraco.Core/Services/IIconService.cs
+++ b/src/Umbraco.Core/Services/IIconService.cs
@@ -1,23 +1,19 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
-{
-    public interface IIconService
-    {
-        /// 
-        /// Gets the svg string for the icon name found at the global icons path
-        /// 
-        /// 
-        /// 
-        IconModel? GetIcon(string iconName);
+namespace Umbraco.Cms.Core.Services;
 
-        /// 
-        /// Gets a list of all svg icons found at at the global icons path.
-        /// 
-        /// 
-        IReadOnlyDictionary? GetIcons();
-    }
+public interface IIconService
+{
+    /// 
+    ///     Gets the svg string for the icon name found at the global icons path
+    /// 
+    /// 
+    /// 
+    IconModel? GetIcon(string iconName);
+
+    /// 
+    ///     Gets a list of all svg icons found at at the global icons path.
+    /// 
+    /// 
+    IReadOnlyDictionary? GetIcons();
 }
diff --git a/src/Umbraco.Core/Services/IIdKeyMap.cs b/src/Umbraco.Core/Services/IIdKeyMap.cs
index 199ee23813..e85095d41f 100644
--- a/src/Umbraco.Core/Services/IIdKeyMap.cs
+++ b/src/Umbraco.Core/Services/IIdKeyMap.cs
@@ -1,16 +1,20 @@
-using System;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IIdKeyMap
 {
-    public interface IIdKeyMap
-    {
-        Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType);
-        Attempt GetIdForUdi(Udi udi);
-        Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType);
-        Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType);
-        void ClearCache();
-        void ClearCache(int id);
-        void ClearCache(Guid key);
-    }
+    Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType);
+
+    Attempt GetIdForUdi(Udi udi);
+
+    Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType);
+
+    Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType);
+
+    void ClearCache();
+
+    void ClearCache(int id);
+
+    void ClearCache(Guid key);
 }
diff --git a/src/Umbraco.Core/Services/IInstallationService.cs b/src/Umbraco.Core/Services/IInstallationService.cs
index 5b1d28cccc..688c6298bd 100644
--- a/src/Umbraco.Core/Services/IInstallationService.cs
+++ b/src/Umbraco.Core/Services/IInstallationService.cs
@@ -1,9 +1,6 @@
-using System.Threading.Tasks;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+public interface IInstallationService
 {
-    public interface IInstallationService
-    {
-        Task LogInstall(InstallLog installLog);
-    }
+    Task LogInstall(InstallLog installLog);
 }
diff --git a/src/Umbraco.Core/Services/IIpAddressUtilities.cs b/src/Umbraco.Core/Services/IIpAddressUtilities.cs
index 7c68bcfa9f..f6c3717244 100644
--- a/src/Umbraco.Core/Services/IIpAddressUtilities.cs
+++ b/src/Umbraco.Core/Services/IIpAddressUtilities.cs
@@ -1,4 +1,4 @@
-using System.Net;
+using System.Net;
 
 namespace Umbraco.Cms.Core.Services;
 
diff --git a/src/Umbraco.Core/Services/IKeyValueService.cs b/src/Umbraco.Core/Services/IKeyValueService.cs
index 1ebf6e9728..97316911c4 100644
--- a/src/Umbraco.Core/Services/IKeyValueService.cs
+++ b/src/Umbraco.Core/Services/IKeyValueService.cs
@@ -1,45 +1,45 @@
-using System.Collections;
-using System.Collections.Generic;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+/// 
+///     Manages the simplified key/value store.
+/// 
+public interface IKeyValueService
 {
     /// 
-    /// Manages the simplified key/value store.
+    ///     Gets a value.
     /// 
-    public interface IKeyValueService
-    {
-        /// 
-        /// Gets a value.
-        /// 
-        /// Returns null if no value was found for the key.
-        string? GetValue(string key);
+    /// Returns null if no value was found for the key.
+    string? GetValue(string key);
 
-        /// 
-        /// Returns key/value pairs for all keys with the specified prefix.
-        /// 
-        /// 
-        /// 
-        IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix);
+    /// 
+    ///     Returns key/value pairs for all keys with the specified prefix.
+    /// 
+    /// 
+    /// 
+    IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix);
 
-        /// 
-        /// Sets a value.
-        /// 
-        void SetValue(string key, string value);
+    /// 
+    ///     Sets a value.
+    /// 
+    void SetValue(string key, string value);
 
-        /// 
-        /// Sets a value.
-        /// 
-        /// Sets the value to  if the value is ,
-        /// and returns true; otherwise throws an exception. In other words, ensures that the value has not changed
-        /// before setting it.
-        void SetValue(string key, string originValue, string newValue);
+    /// 
+    ///     Sets a value.
+    /// 
+    /// 
+    ///     Sets the value to  if the value is ,
+    ///     and returns true; otherwise throws an exception. In other words, ensures that the value has not changed
+    ///     before setting it.
+    /// 
+    void SetValue(string key, string originValue, string newValue);
 
-        /// 
-        /// Tries to set a value.
-        /// 
-        /// Sets the value to  if the value is ,
-        /// and returns true; otherwise returns false. In other words, ensures that the value has not changed
-        /// before setting it.
-        bool TrySetValue(string key, string originValue, string newValue);
-    }
+    /// 
+    ///     Tries to set a value.
+    /// 
+    /// 
+    ///     Sets the value to  if the value is ,
+    ///     and returns true; otherwise returns false. In other words, ensures that the value has not changed
+    ///     before setting it.
+    /// 
+    bool TrySetValue(string key, string originValue, string newValue);
 }
diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs
index eca2a8e070..7a1b1b6fd1 100644
--- a/src/Umbraco.Core/Services/ILocalizationService.cs
+++ b/src/Umbraco.Core/Services/ILocalizationService.cs
@@ -1,171 +1,178 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the Localization Service, which is an easy access to operations involving Languages and Dictionary
+/// 
+public interface ILocalizationService : IService
 {
+    // Possible to-do list:
+    // Import DictionaryItem (?)
+    // RemoveByLanguage (translations)
+    // Add/Set Text (Insert/Update)
+    // Remove Text (in translation)
+
     /// 
-    /// Defines the Localization Service, which is an easy access to operations involving Languages and Dictionary
+    ///     Adds or updates a translation for a dictionary item and language
     /// 
-    public interface ILocalizationService : IService
-    {
-        //Possible to-do list:
-        //Import DictionaryItem (?)
-        //RemoveByLanguage (translations)
-        //Add/Set Text (Insert/Update)
-        //Remove Text (in translation)
+    /// 
+    /// 
+    /// 
+    /// 
+    void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value);
 
-        /// 
-        /// Adds or updates a translation for a dictionary item and language
-        /// 
-        /// 
-        /// 
-        /// 
-        void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value);
+    /// 
+    ///     Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null);
 
-        /// 
-        /// Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null);
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    IDictionaryItem? GetDictionaryItemById(int id);
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        IDictionaryItem? GetDictionaryItemById(int id);
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    IDictionaryItem? GetDictionaryItemById(Guid id);
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        IDictionaryItem? GetDictionaryItemById(Guid id);
+    /// 
+    ///     Gets a  by its key
+    /// 
+    /// Key of the 
+    /// 
+    ///     
+    /// 
+    IDictionaryItem? GetDictionaryItemByKey(string key);
 
-        /// 
-        /// Gets a  by its key
-        /// 
-        /// Key of the 
-        /// 
-        IDictionaryItem? GetDictionaryItemByKey(string key);
+    /// 
+    ///     Gets a list of children for a 
+    /// 
+    /// Id of the parent
+    /// An enumerable list of  objects
+    IEnumerable GetDictionaryItemChildren(Guid parentId);
 
-        /// 
-        /// Gets a list of children for a 
-        /// 
-        /// Id of the parent
-        /// An enumerable list of  objects
-        IEnumerable GetDictionaryItemChildren(Guid parentId);
+    /// 
+    ///     Gets a list of descendants for a 
+    /// 
+    /// Id of the parent, null will return all dictionary items
+    /// An enumerable list of  objects
+    IEnumerable GetDictionaryItemDescendants(Guid? parentId);
 
-        /// 
-        /// Gets a list of descendants for a 
-        /// 
-        /// Id of the parent, null will return all dictionary items
-        /// An enumerable list of  objects
-        IEnumerable GetDictionaryItemDescendants(Guid? parentId);
+    /// 
+    ///     Gets the root/top  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetRootDictionaryItems();
 
-        /// 
-        /// Gets the root/top  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetRootDictionaryItems();
+    /// 
+    ///     Checks if a  with given key exists
+    /// 
+    /// Key of the 
+    /// True if a  exists, otherwise false
+    bool DictionaryItemExists(string key);
 
-        /// 
-        /// Checks if a  with given key exists
-        /// 
-        /// Key of the 
-        /// True if a  exists, otherwise false
-        bool DictionaryItemExists(string key);
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the dictionary item
+    void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the dictionary item
-        void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes a  object and its related translations
+    ///     as well as its children.
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the dictionary item
+    void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes a  object and its related translations
-        /// as well as its children.
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the dictionary item
-        void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  by its id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    ILanguage? GetLanguageById(int id);
 
-        /// 
-        /// Gets a  by its id
-        /// 
-        /// Id of the 
-        /// 
-        ILanguage? GetLanguageById(int id);
+    /// 
+    ///     Gets a  by its iso code
+    /// 
+    /// Iso Code of the language (ie. en-US)
+    /// 
+    ///     
+    /// 
+    ILanguage? GetLanguageByIsoCode(string? isoCode);
 
-        /// 
-        /// Gets a  by its iso code
-        /// 
-        /// Iso Code of the language (ie. en-US)
-        /// 
-        ILanguage? GetLanguageByIsoCode(string? isoCode);
+    /// 
+    ///     Gets a language identifier from its ISO code.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    int? GetLanguageIdByIsoCode(string isoCode);
 
-        /// 
-        /// Gets a language identifier from its ISO code.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        int? GetLanguageIdByIsoCode(string isoCode);
+    /// 
+    ///     Gets a language ISO code from its identifier.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    string? GetLanguageIsoCodeById(int id);
 
-        /// 
-        /// Gets a language ISO code from its identifier.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        string? GetLanguageIsoCodeById(int id);
+    /// 
+    ///     Gets the default language ISO code.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    string GetDefaultLanguageIsoCode();
 
-        /// 
-        /// Gets the default language ISO code.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        string GetDefaultLanguageIsoCode();
+    /// 
+    ///     Gets the default language identifier.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    int? GetDefaultLanguageId();
 
-        /// 
-        /// Gets the default language identifier.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        int? GetDefaultLanguageId();
+    /// 
+    ///     Gets all available languages
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetAllLanguages();
 
-        /// 
-        /// Gets all available languages
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetAllLanguages();
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the language
+    void Save(ILanguage language, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the language
-        void Save(ILanguage language, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes a  by removing it and its usages from the db
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the language
+    void Delete(ILanguage language, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes a  by removing it and its usages from the db
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the language
-        void Delete(ILanguage language, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Gets the full dictionary key map.
-        /// 
-        /// The full dictionary key map.
-        Dictionary GetDictionaryItemKeyMap();
-    }
+    /// 
+    ///     Gets the full dictionary key map.
+    /// 
+    /// The full dictionary key map.
+    Dictionary GetDictionaryItemKeyMap();
 }
diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs
index c49a4e6b2f..23e3888ea0 100644
--- a/src/Umbraco.Core/Services/ILocalizedTextService.cs
+++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs
@@ -1,62 +1,62 @@
-using System.Collections.Generic;
 using System.Globalization;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+// TODO: This needs to be merged into one interface in v9, but better yet
+// the Localize method should just the based on area + alias and we should remove
+// the one with the 'key' (the concatenated area/alias) to ensure that we never use that again.
+
+/// 
+///     The entry point to localize any key in the text storage source for a given culture
+/// 
+/// 
+///     This class is created to be as simple as possible so that it can be replaced very easily,
+///     all other methods are extension methods that simply call the one underlying method in this class
+/// 
+public interface ILocalizedTextService
 {
-    // TODO: This needs to be merged into one interface in v9, but better yet
-    // the Localize method should just the based on area + alias and we should remove
-    // the one with the 'key' (the concatenated area/alias) to ensure that we never use that again.
+    /// 
+    ///     Localize a key with variables
+    /// 
+    /// 
+    /// 
+    /// 
+    /// This can be null
+    /// 
+    string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null);
 
     /// 
-    /// The entry point to localize any key in the text storage source for a given culture
+    ///     Returns all key/values in storage for the given culture
     /// 
+    /// 
+    IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture);
+
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    /// 
+    IDictionary GetAllStoredValues(CultureInfo culture);
+
+    /// 
+    ///     Returns a list of all currently supported cultures
+    /// 
+    /// 
+    IEnumerable GetSupportedCultures();
+
+    /// 
+    ///     Tries to resolve a full 4 letter culture from a 2 letter culture name
+    /// 
+    /// 
+    ///     The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be
+    ///     returned
+    /// 
+    /// 
     /// 
-    /// This class is created to be as simple as possible so that it can be replaced very easily,
-    /// all other methods are extension methods that simply call the one underlying method in this class
+    ///     TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since
+    ///     that
+    ///     is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this
+    ///     attempts
+    ///     to resolve the full culture if possible.
     /// 
-    public interface ILocalizedTextService
-    {
-        /// 
-        /// Localize a key with variables
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This can be null
-        /// 
-        string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null);
-
-
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture);
-
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        IDictionary GetAllStoredValues(CultureInfo culture);
-
-        /// 
-        /// Returns a list of all currently supported cultures
-        /// 
-        /// 
-        IEnumerable GetSupportedCultures();
-
-        /// 
-        /// Tries to resolve a full 4 letter culture from a 2 letter culture name
-        /// 
-        /// 
-        /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned
-        /// 
-        /// 
-        /// 
-        /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that
-        /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts
-        /// to resolve the full culture if possible.
-        /// 
-        CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture);
-    }
+    CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture);
 }
diff --git a/src/Umbraco.Core/Services/IMacroService.cs b/src/Umbraco.Core/Services/IMacroService.cs
index c5d553c99a..ef99248727 100644
--- a/src/Umbraco.Core/Services/IMacroService.cs
+++ b/src/Umbraco.Core/Services/IMacroService.cs
@@ -1,64 +1,60 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the MacroService, which is an easy access to operations involving 
+/// 
+public interface IMacroService : IService
 {
     /// 
-    /// Defines the MacroService, which is an easy access to operations involving 
+    ///     Gets an  object by its alias
     /// 
-    public interface IMacroService : IService
-    {
+    /// Alias to retrieve an  for
+    /// An  object
+    IMacro? GetByAlias(string alias);
 
-        /// 
-        /// Gets an  object by its alias
-        /// 
-        /// Alias to retrieve an  for
-        /// An  object
-        IMacro? GetByAlias(string alias);
+    IEnumerable GetAll();
 
-        IEnumerable GetAll();
+    IEnumerable GetAll(params int[] ids);
 
-        IEnumerable GetAll(params int[] ids);
+    IEnumerable GetAll(params Guid[] ids);
 
-        IEnumerable GetAll(params Guid[] ids);
+    IMacro? GetById(int id);
 
-        IMacro? GetById(int id);
+    IMacro? GetById(Guid id);
 
-        IMacro? GetById(Guid id);
+    /// 
+    ///     Deletes an 
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the macro
+    void Delete(IMacro macro, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes an 
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the macro
-        void Delete(IMacro macro, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Saves an 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the macro
+    void Save(IMacro macro, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves an 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the macro
-        void Save(IMacro macro, int userId = Constants.Security.SuperUserId);
+    ///// 
+    ///// Gets a list all available  plugins
+    ///// 
+    ///// An enumerable list of  objects
+    // IEnumerable GetMacroPropertyTypes();
 
-        ///// 
-        ///// Gets a list all available  plugins
-        ///// 
-        ///// An enumerable list of  objects
-        //IEnumerable GetMacroPropertyTypes();
+    ///// 
+    ///// Gets an  by its alias
+    ///// 
+    ///// Alias to retrieve an  for
+    ///// An  object
+    //IMacroPropertyType GetMacroPropertyTypeByAlias(string alias);
 
-        ///// 
-        ///// Gets an  by its alias
-        ///// 
-        ///// Alias to retrieve an  for
-        ///// An  object
-        //IMacroPropertyType GetMacroPropertyTypeByAlias(string alias);
-
-        /// 
-        /// Gets a list of available  objects by alias.
-        /// 
-        /// Optional array of aliases to limit the results
-        /// An enumerable list of  objects
-        IEnumerable GetAll(params string[] aliases);
-    }
+    /// 
+    /// Gets a list of available  objects by alias.
+    /// 
+    /// Optional array of aliases to limit the results
+    /// An enumerable list of  objects
+    IEnumerable GetAll(params string[] aliases);
 }
diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs
index fe14bdda0f..86440b1119 100644
--- a/src/Umbraco.Core/Services/IMediaService.cs
+++ b/src/Umbraco.Core/Services/IMediaService.cs
@@ -1,363 +1,387 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the Media Service, which is an easy access to operations involving 
+/// 
+public interface IMediaService : IContentServiceBase
 {
-        /// 
-    /// Defines the Media Service, which is an easy access to operations involving 
+    int CountNotTrashed(string? contentTypeAlias = null);
+
+    int Count(string? mediaTypeAlias = null);
+
+    int CountChildren(int parentId, string? mediaTypeAlias = null);
+
+    int CountDescendants(int parentId, string? mediaTypeAlias = null);
+
+    IEnumerable GetByIds(IEnumerable ids);
+
+    IEnumerable GetByIds(IEnumerable ids);
+
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
     /// 
-    public interface IMediaService : IContentServiceBase
-    {
-        int CountNotTrashed(string? contentTypeAlias = null);
-        int Count(string? mediaTypeAlias = null);
-        int CountChildren(int parentId, string? mediaTypeAlias = null);
-        int CountDescendants(int parentId, string? mediaTypeAlias = null);
+    /// 
+    ///     Note that using this method will simply return a new IMedia without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new media objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Media object
+    /// Id of Parent for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        IEnumerable GetByIds(IEnumerable ids);
-        IEnumerable GetByIds(IEnumerable ids);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     Note that using this method will simply return a new IMedia without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new media objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Media object
+    /// Id of Parent for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// Note that using this method will simply return a new IMedia without any identity
-        /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects
-        /// that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Media object
-        /// Id of Parent for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     Note that using this method will simply return a new IMedia without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new media objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Media object
+    /// Parent  for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// Note that using this method will simply return a new IMedia without any identity
-        /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects
-        /// that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Media object
-        /// Id of Parent for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets an  object by Id
+    /// 
+    /// Id of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    IMedia? GetById(int id);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// Note that using this method will simply return a new IMedia without any identity
-        /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects
-        /// that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Media object
-        /// Parent  for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a collection of  objects by Parent Id
+    /// 
+    /// Id of the Parent to retrieve Children from
+    /// Page number
+    /// Page size
+    /// Total records query would return without paging
+    /// Field to order by
+    /// Direction to order by
+    /// Flag to indicate when ordering by system field
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
 
-        /// 
-        /// Gets an  object by Id
-        /// 
-        /// Id of the Content to retrieve
-        /// 
-        IMedia? GetById(int id);
+    /// 
+    ///     Gets a collection of  objects by Parent Id
+    /// 
+    /// Id of the Parent to retrieve Descendants from
+    /// Page number
+    /// Page size
+    /// Total records query would return without paging
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
 
-        /// 
-        /// Gets a collection of  objects by Parent Id
-        /// 
-        /// Id of the Parent to retrieve Children from
-        /// Page number
-        /// Page size
-        /// Total records query would return without paging
-        /// Field to order by
-        /// Direction to order by
-        /// Flag to indicate when ordering by system field
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets paged documents of a content
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
 
-        /// 
-        /// Gets a collection of  objects by Parent Id
-        /// 
-        /// Id of the Parent to retrieve Descendants from
-        /// Page number
-        /// Page size
-        /// Total records query would return without paging
-        /// 
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets paged documents for specified content types
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfTypes(
+        int[] contentTypeIds,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets paged documents of a content
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets a collection of  objects, which reside at the first level / root
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetRootMedia();
 
-        /// 
-        /// Gets paged documents for specified content types
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets a collection of an  objects, which resides in the Recycle Bin
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetPagedMediaInRecycleBin(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets a collection of  objects, which reside at the first level / root
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetRootMedia();
+    /// 
+    ///     Moves an  object to a new location
+    /// 
+    /// The  to move
+    /// Id of the Media's new Parent
+    /// Id of the User moving the Media
+    /// True if moving succeeded, otherwise False
+    Attempt Move(IMedia media, int parentId, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets a collection of an  objects, which resides in the Recycle Bin
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Deletes an  object by moving it to the Recycle Bin
+    /// 
+    /// The  to delete
+    /// Id of the User deleting the Media
+    Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Moves an  object to a new location
-        /// 
-        /// The  to move
-        /// Id of the Media's new Parent
-        /// Id of the User moving the Media
-        /// True if moving succeeded, otherwise False
-        Attempt Move(IMedia media, int parentId, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Empties the Recycle Bin by deleting all  that resides in the bin
+    /// 
+    /// Optional Id of the User emptying the Recycle Bin
+    OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes an  object by moving it to the Recycle Bin
-        /// 
-        /// The  to delete
-        /// Id of the User deleting the Media
-        Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Returns true if there is any media in the recycle bin
+    /// 
+    bool RecycleBinSmells();
 
-        /// 
-        /// Empties the Recycle Bin by deleting all  that resides in the bin
-        /// 
-        /// Optional Id of the User emptying the Recycle Bin
-        OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes all media of specified type. All children of deleted media is moved to Recycle Bin.
+    /// 
+    /// This needs extra care and attention as its potentially a dangerous and extensive operation
+    /// Id of the 
+    /// Optional Id of the user deleting Media
+    void DeleteMediaOfType(int mediaTypeId, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Returns true if there is any media in the recycle bin
-        /// 
-        bool RecycleBinSmells();
+    /// 
+    ///     Deletes all media of the specified types. All Descendants of deleted media that is not of these types is moved to
+    ///     Recycle Bin.
+    /// 
+    /// This needs extra care and attention as its potentially a dangerous and extensive operation
+    /// Ids of the s
+    /// Optional Id of the user issuing the delete operation
+    void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin.
-        /// 
-        /// This needs extra care and attention as its potentially a dangerous and extensive operation
-        /// Id of the 
-        /// Optional Id of the user deleting Media
-        void DeleteMediaOfType(int mediaTypeId, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Permanently deletes an  object
+    /// 
+    /// 
+    ///     Please note that this method will completely remove the Media from the database,
+    ///     but current not from the file system.
+    /// 
+    /// The  to delete
+    /// Id of the User deleting the Media
+    Attempt Delete(IMedia media, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes all media of the specified types. All Descendants of deleted media that is not of these types is moved to Recycle Bin.
-        /// 
-        /// This needs extra care and attention as its potentially a dangerous and extensive operation
-        /// Ids of the s
-        /// Optional Id of the user issuing the delete operation
-        void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Saves a single  object
+    /// 
+    /// The  to save
+    /// Id of the User saving the Media
+    Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Permanently deletes an  object
-        /// 
-        /// 
-        /// Please note that this method will completely remove the Media from the database,
-        /// but current not from the file system.
-        /// 
-        /// The  to delete
-        /// Id of the User deleting the Media
-        Attempt Delete(IMedia media, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Saves a collection of  objects
+    /// 
+    /// Collection of  to save
+    /// Id of the User saving the Media
+    Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a single  object
-        /// 
-        /// The  to save
-        /// Id of the User saving the Media
-        Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets an  object by its 'UniqueId'
+    /// 
+    /// Guid key of the Media to retrieve
+    /// 
+    ///     
+    /// 
+    IMedia? GetById(Guid key);
 
-        /// 
-        /// Saves a collection of  objects
-        /// 
-        /// Collection of  to save
-        /// Id of the User saving the Media
-        Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a collection of  objects by Level
+    /// 
+    /// The level to retrieve Media from
+    /// An Enumerable list of  objects
+    IEnumerable? GetByLevel(int level);
 
-        /// 
-        /// Gets an  object by its 'UniqueId'
-        /// 
-        /// Guid key of the Media to retrieve
-        /// 
-        IMedia? GetById(Guid key);
+    /// 
+    ///     Gets a specific version of an  item.
+    /// 
+    /// Id of the version to retrieve
+    /// An  item
+    IMedia? GetVersion(int versionId);
 
-        /// 
-        /// Gets a collection of  objects by Level
-        /// 
-        /// The level to retrieve Media from
-        /// An Enumerable list of  objects
-        IEnumerable? GetByLevel(int level);
+    /// 
+    ///     Gets a collection of an  objects versions by Id
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetVersions(int id);
 
-        /// 
-        /// Gets a specific version of an  item.
-        /// 
-        /// Id of the version to retrieve
-        /// An  item
-        IMedia? GetVersion(int versionId);
+    /// 
+    ///     Checks whether an  item has any children
+    /// 
+    /// Id of the 
+    /// True if the media has any children otherwise False
+    bool HasChildren(int id);
 
-        /// 
-        /// Gets a collection of an  objects versions by Id
-        /// 
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetVersions(int id);
+    /// 
+    ///     Permanently deletes versions from an  object prior to a specific date.
+    /// 
+    /// Id of the  object to delete versions from
+    /// Latest version date
+    /// Optional Id of the User deleting versions of a Content object
+    void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Checks whether an  item has any children
-        /// 
-        /// Id of the 
-        /// True if the media has any children otherwise False
-        bool HasChildren(int id);
+    /// 
+    ///     Permanently deletes specific version(s) from an  object.
+    /// 
+    /// Id of the  object to delete a version from
+    /// Id of the version to delete
+    /// Boolean indicating whether to delete versions prior to the versionId
+    /// Optional Id of the User deleting versions of a Content object
+    void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Permanently deletes versions from an  object prior to a specific date.
-        /// 
-        /// Id of the  object to delete versions from
-        /// Latest version date
-        /// Optional Id of the User deleting versions of a Content object
-        void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets an  object from the path stored in the 'umbracoFile' property.
+    /// 
+    /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg)
+    /// 
+    ///     
+    /// 
+    IMedia? GetMediaByPath(string mediaPath);
 
-        /// 
-        /// Permanently deletes specific version(s) from an  object.
-        /// 
-        /// Id of the  object to delete a version from
-        /// Id of the version to delete
-        /// Boolean indicating whether to delete versions prior to the versionId
-        /// Optional Id of the User deleting versions of a Content object
-        void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current media.
+    /// 
+    /// Id of the  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    IEnumerable GetAncestors(int id);
 
-        /// 
-        /// Gets an  object from the path stored in the 'umbracoFile' property.
-        /// 
-        /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg)
-        /// 
-        IMedia? GetMediaByPath(string mediaPath);
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current media.
+    /// 
+    ///  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    IEnumerable GetAncestors(IMedia media);
 
-        /// 
-        /// Gets a collection of  objects, which are ancestors of the current media.
-        /// 
-        /// Id of the  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        IEnumerable GetAncestors(int id);
+    /// 
+    ///     Gets the parent of the current media as an  item.
+    /// 
+    /// Id of the  to retrieve the parent from
+    /// Parent  object
+    IMedia? GetParent(int id);
 
-        /// 
-        /// Gets a collection of  objects, which are ancestors of the current media.
-        /// 
-        ///  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        IEnumerable GetAncestors(IMedia media);
+    /// 
+    ///     Gets the parent of the current media as an  item.
+    /// 
+    ///  to retrieve the parent from
+    /// Parent  object
+    IMedia? GetParent(IMedia media);
 
-        /// 
-        /// Gets the parent of the current media as an  item.
-        /// 
-        /// Id of the  to retrieve the parent from
-        /// Parent  object
-        IMedia? GetParent(int id);
+    /// 
+    ///     Sorts a collection of  objects by updating the SortOrder according
+    ///     to the ordering of items in the passed in .
+    /// 
+    /// 
+    /// 
+    /// True if sorting succeeded, otherwise False
+    bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the parent of the current media as an  item.
-        /// 
-        ///  to retrieve the parent from
-        /// Parent  object
-        IMedia? GetParent(IMedia media);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     This method returns an  object that has been persisted to the database
+    ///     and therefor has an identity.
+    /// 
+    /// Name of the Media object
+    /// Parent  for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Sorts a collection of  objects by updating the SortOrder according
-        /// to the ordering of items in the passed in .
-        /// 
-        /// 
-        /// 
-        /// True if sorting succeeded, otherwise False
-        bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     This method returns an  object that has been persisted to the database
+    ///     and therefor has an identity.
+    /// 
+    /// Name of the Media object
+    /// Id of Parent for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// This method returns an  object that has been persisted to the database
-        /// and therefor has an identity.
-        /// 
-        /// Name of the Media object
-        /// Parent  for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets the content of a media as a stream.
+    /// 
+    /// The filesystem path to the media.
+    /// The content of the media.
+    Stream GetMediaFileContentStream(string filepath);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// This method returns an  object that has been persisted to the database
-        /// and therefor has an identity.
-        /// 
-        /// Name of the Media object
-        /// Id of Parent for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Sets the content of a media.
+    /// 
+    /// The filesystem path to the media.
+    /// The content of the media.
+    void SetMediaFileContent(string filepath, Stream content);
 
-        /// 
-        /// Gets the content of a media as a stream.
-        /// 
-        /// The filesystem path to the media.
-        /// The content of the media.
-        Stream GetMediaFileContentStream(string filepath);
+    /// 
+    ///     Deletes a media file.
+    /// 
+    /// The filesystem path to the media.
+    void DeleteMediaFile(string filepath);
 
-        /// 
-        /// Sets the content of a media.
-        /// 
-        /// The filesystem path to the media.
-        /// The content of the media.
-        void SetMediaFileContent(string filepath, Stream content);
-
-        /// 
-        /// Deletes a media file.
-        /// 
-        /// The filesystem path to the media.
-        void DeleteMediaFile(string filepath);
-
-        /// 
-        /// Gets the size of a media.
-        /// 
-        /// The filesystem path to the media.
-        /// The size of the media.
-        long GetMediaFileSize(string filepath);
-    }
+    /// 
+    ///     Gets the size of a media.
+    /// 
+    /// The filesystem path to the media.
+    /// The size of the media.
+    long GetMediaFileSize(string filepath);
 }
diff --git a/src/Umbraco.Core/Services/IMediaTypeService.cs b/src/Umbraco.Core/Services/IMediaTypeService.cs
index e00d86613b..a00b9ae5c6 100644
--- a/src/Umbraco.Core/Services/IMediaTypeService.cs
+++ b/src/Umbraco.Core/Services/IMediaTypeService.cs
@@ -1,10 +1,10 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Manages  objects.
+/// 
+public interface IMediaTypeService : IContentTypeBaseService
 {
-    /// 
-    /// Manages  objects.
-    /// 
-    public interface IMediaTypeService : IContentTypeBaseService
-    { }
 }
diff --git a/src/Umbraco.Core/Services/IMemberGroupService.cs b/src/Umbraco.Core/Services/IMemberGroupService.cs
index 9b8c4a8d53..24cc6845ad 100644
--- a/src/Umbraco.Core/Services/IMemberGroupService.cs
+++ b/src/Umbraco.Core/Services/IMemberGroupService.cs
@@ -1,17 +1,20 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IMemberGroupService : IService
 {
-    public interface IMemberGroupService : IService
-    {
-        IEnumerable GetAll();
-        IMemberGroup? GetById(int id);
-        IMemberGroup? GetById(Guid id);
-        IEnumerable GetByIds(IEnumerable ids);
-        IMemberGroup? GetByName(string? name);
-        void Save(IMemberGroup memberGroup);
-        void Delete(IMemberGroup memberGroup);
-    }
+    IEnumerable GetAll();
+
+    IMemberGroup? GetById(int id);
+
+    IMemberGroup? GetById(Guid id);
+
+    IEnumerable GetByIds(IEnumerable ids);
+
+    IMemberGroup? GetByName(string? name);
+
+    void Save(IMemberGroup memberGroup);
+
+    void Delete(IMemberGroup memberGroup);
 }
diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs
index d6e0480091..ec600efab7 100644
--- a/src/Umbraco.Core/Services/IMemberService.cs
+++ b/src/Umbraco.Core/Services/IMemberService.cs
@@ -1,206 +1,280 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the MemberService, which is an easy access to operations involving (umbraco) members.
+/// 
+public interface IMemberService : IMembershipMemberService
 {
     /// 
-    /// Defines the MemberService, which is an easy access to operations involving (umbraco) members.
+    ///     Gets a list of paged  objects
     /// 
-    public interface IMemberService : IMembershipMemberService
-    {
-        /// 
-        /// Gets a list of paged  objects
-        /// 
-        /// An  can be of type  
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// Field to order by
-        /// Direction to order by
-        /// 
-        /// Search text filter
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, string? memberTypeAlias = null, string filter = "");
+    /// An  can be of type  
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// Field to order by
+    /// Direction to order by
+    /// 
+    /// Search text filter
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        string? memberTypeAlias = null,
+        string filter = "");
 
-        /// 
-        /// Gets a list of paged  objects
-        /// 
-        /// An  can be of type  
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// Field to order by
-        /// Direction to order by
-        /// Flag to indicate when ordering by system field
-        /// 
-        /// Search text filter
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, bool orderBySystemField, string? memberTypeAlias, string filter);
+    /// 
+    ///     Gets a list of paged  objects
+    /// 
+    /// An  can be of type  
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// Field to order by
+    /// Direction to order by
+    /// Flag to indicate when ordering by system field
+    /// 
+    /// Search text filter
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        bool orderBySystemField,
+        string? memberTypeAlias,
+        string filter);
 
-        /// 
-        /// Creates an  object without persisting it
-        /// 
-        /// This method is convenient for when you need to add properties to a new Member
-        /// before persisting it in order to limit the amount of times its saved.
-        /// Also note that the returned  will not have an Id until its saved.
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// Alias of the MemberType the Member should be based on
-        /// 
-        IMember CreateMember(string username, string email, string name, string memberTypeAlias);
+    /// 
+    ///     Creates an  object without persisting it
+    /// 
+    /// 
+    ///     This method is convenient for when you need to add properties to a new Member
+    ///     before persisting it in order to limit the amount of times its saved.
+    ///     Also note that the returned  will not have an Id until its saved.
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// Alias of the MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMember(string username, string email, string name, string memberTypeAlias);
 
-        /// 
-        /// Creates an  object without persisting it
-        /// 
-        /// This method is convenient for when you need to add properties to a new Member
-        /// before persisting it in order to limit the amount of times its saved.
-        /// Also note that the returned  will not have an Id until its saved.
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// MemberType the Member should be based on
-        /// 
-        IMember CreateMember(string username, string email, string name, IMemberType memberType);
+    /// 
+    ///     Creates an  object without persisting it
+    /// 
+    /// 
+    ///     This method is convenient for when you need to add properties to a new Member
+    ///     before persisting it in order to limit the amount of times its saved.
+    ///     Also note that the returned  will not have an Id until its saved.
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMember(string username, string email, string name, IMemberType memberType);
 
-        /// 
-        /// Creates and persists a Member
-        /// 
-        /// Using this method will persist the Member object before its returned
-        /// meaning that it will have an Id available (unlike the CreateMember method)
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// Alias of the MemberType the Member should be based on
-        /// 
-        IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias);
+    /// 
+    ///     Creates and persists a Member
+    /// 
+    /// 
+    ///     Using this method will persist the Member object before its returned
+    ///     meaning that it will have an Id available (unlike the CreateMember method)
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// Alias of the MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias);
 
-        /// 
-        /// Creates and persists a Member
-        /// 
-        /// Using this method will persist the Member object before its returned
-        /// meaning that it will have an Id available (unlike the CreateMember method)
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// MemberType the Member should be based on
-        /// 
-        IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
+    /// 
+    ///     Creates and persists a Member
+    /// 
+    /// 
+    ///     Using this method will persist the Member object before its returned
+    ///     meaning that it will have an Id available (unlike the CreateMember method)
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
 
-        /// 
-        /// Gets the count of Members by an optional MemberType alias
-        /// 
-        /// If no alias is supplied then the count for all Member will be returned
-        /// Optional alias for the MemberType when counting number of Members
-        ///  with number of Members
-        int Count(string? memberTypeAlias = null);
+    /// 
+    ///     Gets the count of Members by an optional MemberType alias
+    /// 
+    /// If no alias is supplied then the count for all Member will be returned
+    /// Optional alias for the MemberType when counting number of Members
+    ///  with number of Members
+    int Count(string? memberTypeAlias = null);
 
-        /// 
-        /// Checks if a Member with the id exists
-        /// 
-        /// Id of the Member
-        /// True if the Member exists otherwise False
-        bool Exists(int id);
+    /// 
+    ///     Checks if a Member with the id exists
+    /// 
+    /// Id of the Member
+    /// True if the Member exists otherwise False
+    bool Exists(int id);
 
-        /// 
-        /// Gets a Member by the unique key
-        /// 
-        /// The guid key corresponds to the unique id in the database
-        /// and the user id in the membership provider.
-        ///  Id
-        /// 
-        IMember? GetByKey(Guid id);
+    /// 
+    ///     Gets a Member by the unique key
+    /// 
+    /// 
+    ///     The guid key corresponds to the unique id in the database
+    ///     and the user id in the membership provider.
+    /// 
+    ///  Id
+    /// 
+    ///     
+    /// 
+    IMember? GetByKey(Guid id);
 
-        /// 
-        /// Gets a Member by its integer id
-        /// 
-        ///  Id
-        /// 
-        IMember? GetById(int id);
+    /// 
+    ///     Gets a Member by its integer id
+    /// 
+    ///  Id
+    /// 
+    ///     
+    /// 
+    IMember? GetById(int id);
 
-        /// 
-        /// Gets all Members for the specified MemberType alias
-        /// 
-        /// Alias of the MemberType
-        /// 
-        IEnumerable GetMembersByMemberType(string memberTypeAlias);
+    /// 
+    ///     Gets all Members for the specified MemberType alias
+    /// 
+    /// Alias of the MemberType
+    /// 
+    ///     
+    /// 
+    IEnumerable GetMembersByMemberType(string memberTypeAlias);
 
-        /// 
-        /// Gets all Members for the MemberType id
-        /// 
-        /// Id of the MemberType
-        /// 
-        IEnumerable GetMembersByMemberType(int memberTypeId);
+    /// 
+    ///     Gets all Members for the MemberType id
+    /// 
+    /// Id of the MemberType
+    /// 
+    ///     
+    /// 
+    IEnumerable GetMembersByMemberType(int memberTypeId);
 
-        /// 
-        /// Gets all Members within the specified MemberGroup name
-        /// 
-        /// Name of the MemberGroup
-        /// 
-        IEnumerable GetMembersByGroup(string memberGroupName);
+    /// 
+    ///     Gets all Members within the specified MemberGroup name
+    /// 
+    /// Name of the MemberGroup
+    /// 
+    ///     
+    /// 
+    IEnumerable GetMembersByGroup(string memberGroupName);
 
-        /// 
-        /// Gets all Members with the ids specified
-        /// 
-        /// If no Ids are specified all Members will be retrieved
-        /// Optional list of Member Ids
-        /// 
-        IEnumerable GetAllMembers(params int[] ids);
+    /// 
+    ///     Gets all Members with the ids specified
+    /// 
+    /// If no Ids are specified all Members will be retrieved
+    /// Optional list of Member Ids
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAllMembers(params int[] ids);
 
-        /// 
-        /// Delete Members of the specified MemberType id
-        /// 
-        /// Id of the MemberType
-        void DeleteMembersOfType(int memberTypeId);
+    /// 
+    ///     Delete Members of the specified MemberType id
+    /// 
+    /// Id of the MemberType
+    void DeleteMembersOfType(int memberTypeId);
 
-        /// 
-        /// Finds Members based on their display name
-        /// 
-        /// Display name to match
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
+    /// 
+    ///     Finds Members based on their display name
+    /// 
+    /// Display name to match
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable FindMembersByDisplayName(
+        string displayNameToMatch,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact);
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(
+        string propertyTypeAlias,
+        string value,
+        StringPropertyMatchType matchType = StringPropertyMatchType.Exact);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, bool value);
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, bool value);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
-    }
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
 }
diff --git a/src/Umbraco.Core/Services/IMemberTypeService.cs b/src/Umbraco.Core/Services/IMemberTypeService.cs
index 4a52438d5e..6a70e620a1 100644
--- a/src/Umbraco.Core/Services/IMemberTypeService.cs
+++ b/src/Umbraco.Core/Services/IMemberTypeService.cs
@@ -1,12 +1,11 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Manages  objects.
+/// 
+public interface IMemberTypeService : IContentTypeBaseService
 {
-    /// 
-    /// Manages  objects.
-    /// 
-    public interface IMemberTypeService : IContentTypeBaseService
-    {
-        string GetDefault();
-    }
+    string GetDefault();
 }
diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs
index 0971738a99..553441f572 100644
--- a/src/Umbraco.Core/Services/IMembershipMemberService.cs
+++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs
@@ -1,159 +1,190 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines part of the MemberService, which is specific to methods used by the membership provider.
+/// 
+/// 
+///     Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider
+///     implementation.
+/// 
+public interface IMembershipMemberService : IMembershipMemberService, IMembershipRoleService
 {
     /// 
-    /// Defines part of the MemberService, which is specific to methods used by the membership provider.
+    ///     Creates and persists a new Member
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    ///  which the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType);
+}
+
+/// 
+///     Defines part of the UserService/MemberService, which is specific to methods used by the membership provider.
+///     The generic type is restricted to . The implementation of this interface  uses
+///     either  for the MemberService or  for the UserService.
+/// 
+/// 
+///     Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider
+///     implementation.
+/// 
+public interface IMembershipMemberService : IService
+    where T : class, IMembershipUser
+{
+    /// 
+    ///     Gets the total number of Members or Users based on the count type
     /// 
     /// 
-    /// Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider implementation.
+    ///     The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any
+    ///     members
+    ///     that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact
+    ///     science
+    ///     but that is how MS have made theirs so we'll follow that principal.
     /// 
-    public interface IMembershipMemberService : IMembershipMemberService, IMembershipRoleService
-    {
-        /// 
-        /// Creates and persists a new Member
-        /// 
-        /// Username of the Member to create
-        /// Email of the Member to create
-        ///  which the Member should be based on
-        /// 
-        IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType);
-    }
+    ///  to count by
+    ///  with number of Members or Users for passed in type
+    int GetCount(MemberCountType countType);
 
     /// 
-    /// Defines part of the UserService/MemberService, which is specific to methods used by the membership provider.
-    /// The generic type is restricted to . The implementation of this interface  uses
-    /// either  for the MemberService or  for the UserService.
+    ///     Checks if a Member with the username exists
     /// 
-    /// 
-    /// Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider implementation.
-    /// 
-    public interface IMembershipMemberService : IService
-        where T : class, IMembershipUser
-    {
-        /// 
-        /// Gets the total number of Members or Users based on the count type
-        /// 
-        /// 
-        /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members
-        /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science
-        /// but that is how MS have made theirs so we'll follow that principal.
-        /// 
-        ///  to count by
-        ///  with number of Members or Users for passed in type
-        int GetCount(MemberCountType countType);
+    /// Username to check
+    /// True if the Member exists otherwise False
+    bool Exists(string username);
 
-        /// 
-        /// Checks if a Member with the username exists
-        /// 
-        /// Username to check
-        /// True if the Member exists otherwise False
-        bool Exists(string username);
+    /// 
+    ///     Creates and persists a new 
+    /// 
+    /// An  can be of type  or 
+    /// Username of the  to create
+    /// Email of the  to create
+    /// 
+    ///     This value should be the encoded/encrypted/hashed value for the password that will be
+    ///     stored in the database
+    /// 
+    /// Alias of the Type
+    /// 
+    ///     
+    /// 
+    T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias);
 
-        /// 
-        /// Creates and persists a new 
-        /// 
-        /// An  can be of type  or 
-        /// Username of the  to create
-        /// Email of the  to create
-        /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database
-        /// Alias of the Type
-        /// 
-        T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias);
+    /// 
+    ///     Creates and persists a new 
+    /// 
+    /// An  can be of type  or 
+    /// Username of the  to create
+    /// Email of the  to create
+    /// 
+    ///     This value should be the encoded/encrypted/hashed value for the password that will be
+    ///     stored in the database
+    /// 
+    /// Alias of the Type
+    /// IsApproved of the  to create
+    /// 
+    ///     
+    /// 
+    T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved);
 
-        /// 
-        /// Creates and persists a new 
-        /// 
-        /// An  can be of type  or 
-        /// Username of the  to create
-        /// Email of the  to create
-        /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database
-        /// Alias of the Type
-        /// IsApproved of the  to create
-        /// 
-        T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved);
+    /// 
+    ///     Gets an  by its provider key
+    /// 
+    /// An  can be of type  or 
+    /// Id to use for retrieval
+    /// 
+    ///     
+    /// 
+    T? GetByProviderKey(object id);
 
-        /// 
-        /// Gets an  by its provider key
-        /// 
-        /// An  can be of type  or 
-        /// Id to use for retrieval
-        /// 
-        T? GetByProviderKey(object id);
+    /// 
+    ///     Get an  by email
+    /// 
+    /// An  can be of type  or 
+    /// Email to use for retrieval
+    /// 
+    ///     
+    /// 
+    T? GetByEmail(string email);
 
-        /// 
-        /// Get an  by email
-        /// 
-        /// An  can be of type  or 
-        /// Email to use for retrieval
-        /// 
-        T? GetByEmail(string email);
+    /// 
+    ///     Get an  by username
+    /// 
+    /// An  can be of type  or 
+    /// Username to use for retrieval
+    /// 
+    ///     
+    /// 
+    T? GetByUsername(string? username);
 
-        /// 
-        /// Get an  by username
-        /// 
-        /// An  can be of type  or 
-        /// Username to use for retrieval
-        /// 
-        T? GetByUsername(string? username);
+    /// 
+    ///     Deletes an 
+    /// 
+    /// An  can be of type  or 
+    ///  or  to Delete
+    void Delete(T membershipUser);
 
-        /// 
-        /// Deletes an 
-        /// 
-        /// An  can be of type  or 
-        ///  or  to Delete
-        void Delete(T membershipUser);
+    /// 
+    ///     Saves an 
+    /// 
+    /// An  can be of type  or 
+    ///  or  to Save
+    void Save(T entity);
 
-        /// 
-        /// Saves an 
-        /// 
-        /// An  can be of type  or 
-        ///  or  to Save
-        void Save(T entity);
+    /// 
+    ///     Saves a list of  objects
+    /// 
+    /// An  can be of type  or 
+    ///  to save
+    void Save(IEnumerable entities);
 
-        /// 
-        /// Saves a list of  objects
-        /// 
-        /// An  can be of type  or 
-        ///  to save
-        void Save(IEnumerable entities);
+    /// 
+    ///     Finds a list of  objects by a partial email string
+    /// 
+    /// An  can be of type  or 
+    /// Partial email string to match
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        /// 
-        /// Finds a list of  objects by a partial email string
-        /// 
-        /// An  can be of type  or 
-        /// Partial email string to match
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
+    /// 
+    ///     Finds a list of  objects by a partial username
+    /// 
+    /// An  can be of type  or 
+    /// Partial username to match
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        /// 
-        /// Finds a list of  objects by a partial username
-        /// 
-        /// An  can be of type  or 
-        /// Partial username to match
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
-
-        /// 
-        /// Gets a list of paged  objects
-        /// 
-        /// An  can be of type  or 
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords);
-    }
+    /// 
+    ///     Gets a list of paged  objects
+    /// 
+    /// An  can be of type  or 
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords);
 }
diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs
index 5c62a84973..538ae4fb8c 100644
--- a/src/Umbraco.Core/Services/IMembershipRoleService.cs
+++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs
@@ -1,52 +1,49 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IMembershipRoleService
+    where T : class, IMembershipUser
 {
-    public interface IMembershipRoleService
-        where T : class, IMembershipUser
-    {
-        void AddRole(string roleName);
+    void AddRole(string roleName);
 
-        IEnumerable GetAllRoles();
+    IEnumerable GetAllRoles();
 
-        IEnumerable GetAllRoles(int memberId);
+    IEnumerable GetAllRoles(int memberId);
 
-        IEnumerable GetAllRoles(string username);
+    IEnumerable GetAllRoles(string username);
 
-        IEnumerable GetAllRolesIds();
+    IEnumerable GetAllRolesIds();
 
-        IEnumerable GetAllRolesIds(int memberId);
+    IEnumerable GetAllRolesIds(int memberId);
 
-        IEnumerable GetAllRolesIds(string username);
+    IEnumerable GetAllRolesIds(string username);
 
-        IEnumerable GetMembersInRole(string roleName);
+    IEnumerable GetMembersInRole(string roleName);
 
-        IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
+    IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        bool DeleteRole(string roleName, bool throwIfBeingUsed);
+    bool DeleteRole(string roleName, bool throwIfBeingUsed);
 
-        void AssignRole(string username, string roleName);
+    void AssignRole(string username, string roleName);
 
-        void AssignRoles(string[] usernames, string[] roleNames);
+    void AssignRoles(string[] usernames, string[] roleNames);
 
-        void DissociateRole(string username, string roleName);
+    void DissociateRole(string username, string roleName);
 
-        void DissociateRoles(string[] usernames, string[] roleNames);
+    void DissociateRoles(string[] usernames, string[] roleNames);
 
-        void AssignRole(int memberId, string roleName);
+    void AssignRole(int memberId, string roleName);
 
-        void AssignRoles(int[] memberIds, string[] roleNames);
+    void AssignRoles(int[] memberIds, string[] roleNames);
 
-        void DissociateRole(int memberId, string roleName);
+    void DissociateRole(int memberId, string roleName);
 
-        void DissociateRoles(int[] memberIds, string[] roleNames);
+    void DissociateRoles(int[] memberIds, string[] roleNames);
 
-        void ReplaceRoles(string[] usernames, string[] roleNames);
+    void ReplaceRoles(string[] usernames, string[] roleNames);
 
-        void ReplaceRoles(int[] memberIds, string[] roleNames);
-
-    }
+    void ReplaceRoles(int[] memberIds, string[] roleNames);
 }
diff --git a/src/Umbraco.Core/Services/IMembershipUserService.cs b/src/Umbraco.Core/Services/IMembershipUserService.cs
index a2aca2821e..7a8dc2023f 100644
--- a/src/Umbraco.Core/Services/IMembershipUserService.cs
+++ b/src/Umbraco.Core/Services/IMembershipUserService.cs
@@ -1,24 +1,27 @@
-using Umbraco.Cms.Core.Models.Membership;
+using Umbraco.Cms.Core.Models.Membership;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines part of the UserService, which is specific to methods used by the membership provider.
+/// 
+/// 
+///     Idea is to have this is an isolated interface so that it can be easily 'replaced' in the membership provider impl.
+/// 
+public interface IMembershipUserService : IMembershipMemberService
 {
     /// 
-    /// Defines part of the UserService, which is specific to methods used by the membership provider.
+    ///     Creates and persists a new User
     /// 
     /// 
-    /// Idea is to have this is an isolated interface so that it can be easily 'replaced' in the membership provider impl.
+    ///     The user will be saved in the database and returned with an Id.
+    ///     This method is convenient when you need to perform operations, which needs the
+    ///     Id of the user once its been created.
     /// 
-    public interface IMembershipUserService : IMembershipMemberService
-    {
-        /// 
-        /// Creates and persists a new User
-        /// 
-        /// The user will be saved in the database and returned with an Id.
-        /// This method is convenient when you need to perform operations, which needs the
-        /// Id of the user once its been created.
-        /// Username of the User to create
-        /// Email of the User to create
-        /// 
-        IUser CreateUserWithIdentity(string username, string email);
-    }
+    /// Username of the User to create
+    /// Email of the User to create
+    /// 
+    ///     
+    /// 
+    IUser CreateUserWithIdentity(string username, string email);
 }
diff --git a/src/Umbraco.Core/Services/IMetricsConsentService.cs b/src/Umbraco.Core/Services/IMetricsConsentService.cs
index e55cfd71d0..72f3ebe873 100644
--- a/src/Umbraco.Core/Services/IMetricsConsentService.cs
+++ b/src/Umbraco.Core/Services/IMetricsConsentService.cs
@@ -1,11 +1,10 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IMetricsConsentService
 {
-    public interface IMetricsConsentService
-    {
-        TelemetryLevel GetConsentLevel();
+    TelemetryLevel GetConsentLevel();
 
-        void SetConsentLevel(TelemetryLevel telemetryLevel);
-    }
+    void SetConsentLevel(TelemetryLevel telemetryLevel);
 }
diff --git a/src/Umbraco.Core/Services/INodeCountService.cs b/src/Umbraco.Core/Services/INodeCountService.cs
index 50d91c1512..d442a7199f 100644
--- a/src/Umbraco.Core/Services/INodeCountService.cs
+++ b/src/Umbraco.Core/Services/INodeCountService.cs
@@ -1,10 +1,8 @@
-using System;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+public interface INodeCountService
 {
-    public interface INodeCountService
-    {
-        int GetNodeCount(Guid nodeType);
-        int GetMediaCount();
-    }
+    int GetNodeCount(Guid nodeType);
+
+    int GetMediaCount();
 }
diff --git a/src/Umbraco.Core/Services/INotificationService.cs b/src/Umbraco.Core/Services/INotificationService.cs
index cf65b1aa67..8472333d19 100644
--- a/src/Umbraco.Core/Services/INotificationService.cs
+++ b/src/Umbraco.Core/Services/INotificationService.cs
@@ -1,89 +1,92 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Models.Membership;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface INotificationService : IService
 {
-    public interface INotificationService : IService
-    {
-        /// 
-        /// Sends the notifications for the specified user regarding the specified nodes and action.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        void SendNotifications(IUser operatingUser, IEnumerable entities, string? action, string? actionName, Uri siteUri,
-                               Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
-                               Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody);
+    /// 
+    ///     Sends the notifications for the specified user regarding the specified nodes and action.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    void SendNotifications(
+        IUser operatingUser,
+        IEnumerable entities,
+        string? action,
+        string? actionName,
+        Uri siteUri,
+        Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
+        Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody);
 
-        /// 
-        /// Gets the notifications for the user
-        /// 
-        /// 
-        /// 
-        IEnumerable? GetUserNotifications(IUser user);
+    /// 
+    ///     Gets the notifications for the user
+    /// 
+    /// 
+    /// 
+    IEnumerable? GetUserNotifications(IUser user);
 
-        /// 
-        /// Gets the notifications for the user based on the specified node path
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors)
-        /// 
-        IEnumerable? GetUserNotifications(IUser? user, string path);
+    /// 
+    ///     Gets the notifications for the user based on the specified node path
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     Notifications are inherited from the parent so any child node will also have notifications assigned based on it's
+    ///     parent (ancestors)
+    /// 
+    IEnumerable? GetUserNotifications(IUser? user, string path);
 
-        /// 
-        /// Returns the notifications for an entity
-        /// 
-        /// 
-        /// 
-        IEnumerable? GetEntityNotifications(IEntity entity);
+    /// 
+    ///     Returns the notifications for an entity
+    /// 
+    /// 
+    /// 
+    IEnumerable? GetEntityNotifications(IEntity entity);
 
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        void DeleteNotifications(IEntity entity);
+    /// 
+    ///     Deletes notifications by entity
+    /// 
+    /// 
+    void DeleteNotifications(IEntity entity);
 
-        /// 
-        /// Deletes notifications by user
-        /// 
-        /// 
-        void DeleteNotifications(IUser user);
+    /// 
+    ///     Deletes notifications by user
+    /// 
+    /// 
+    void DeleteNotifications(IUser user);
 
-        /// 
-        /// Delete notifications by user and entity
-        /// 
-        /// 
-        /// 
-        void DeleteNotifications(IUser user, IEntity entity);
+    /// 
+    ///     Delete notifications by user and entity
+    /// 
+    /// 
+    /// 
+    void DeleteNotifications(IUser user, IEntity entity);
 
-        /// 
-        /// Sets the specific notifications for the user and entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This performs a full replace
-        /// 
-        IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions);
+    /// 
+    ///     Sets the specific notifications for the user and entity
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This performs a full replace
+    /// 
+    IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions);
 
-        /// 
-        /// Creates a new notification
-        /// 
-        /// 
-        /// 
-        /// The action letter - note: this is a string for future compatibility
-        /// 
-        Notification CreateNotification(IUser user, IEntity entity, string action);
-    }
+    /// 
+    ///     Creates a new notification
+    /// 
+    /// 
+    /// 
+    /// The action letter - note: this is a string for future compatibility
+    /// 
+    Notification CreateNotification(IUser user, IEntity entity, string action);
 }
diff --git a/src/Umbraco.Core/Services/IPackagingService.cs b/src/Umbraco.Core/Services/IPackagingService.cs
index 8429898354..40f39628be 100644
--- a/src/Umbraco.Core/Services/IPackagingService.cs
+++ b/src/Umbraco.Core/Services/IPackagingService.cs
@@ -1,63 +1,59 @@
-using System.Collections.Generic;
-using System.IO;
 using System.Xml.Linq;
 using Umbraco.Cms.Core.Models.Packaging;
 using Umbraco.Cms.Core.Packaging;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IPackagingService : IService
 {
-    public interface IPackagingService : IService
-    {
-        /// 
-        /// Returns a  result from an umbraco package file (zip)
-        /// 
-        /// 
-        /// 
-        CompiledPackage GetCompiledPackageInfo(XDocument packageXml);
+    /// 
+    ///     Returns a  result from an umbraco package file (zip)
+    /// 
+    /// 
+    /// 
+    CompiledPackage GetCompiledPackageInfo(XDocument packageXml);
 
-        /// 
-        /// Installs the data, entities, objects contained in an umbraco package file (zip)
-        /// 
-        /// 
-        /// 
-        InstallationSummary InstallCompiledPackageData(FileInfo packageXmlFile, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Installs the data, entities, objects contained in an umbraco package file (zip)
+    /// 
+    /// 
+    /// 
+    InstallationSummary InstallCompiledPackageData(FileInfo packageXmlFile, int userId = Constants.Security.SuperUserId);
 
-        InstallationSummary InstallCompiledPackageData(XDocument? packageXml, int userId = Constants.Security.SuperUserId);
+    InstallationSummary InstallCompiledPackageData(XDocument? packageXml, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Returns the advertised installed packages
-        /// 
-        /// 
-        IEnumerable GetAllInstalledPackages();
+    /// 
+    ///     Returns the advertised installed packages
+    /// 
+    /// 
+    IEnumerable GetAllInstalledPackages();
 
-        InstalledPackage? GetInstalledPackageByName(string packageName);
+    InstalledPackage? GetInstalledPackageByName(string packageName);
 
-        /// 
-        /// Returns the created packages
-        /// 
-        /// 
-        IEnumerable GetAllCreatedPackages();
+    /// 
+    ///     Returns the created packages
+    /// 
+    /// 
+    IEnumerable GetAllCreatedPackages();
 
-        /// 
-        /// Returns a created package by id
-        /// 
-        /// 
-        /// 
-        PackageDefinition? GetCreatedPackageById(int id);
+    /// 
+    ///     Returns a created package by id
+    /// 
+    /// 
+    /// 
+    PackageDefinition? GetCreatedPackageById(int id);
 
-        void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
+    void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Persists a package definition to storage
-        /// 
-        /// 
-        bool SaveCreatedPackage(PackageDefinition definition);
+    /// 
+    ///     Persists a package definition to storage
+    /// 
+    /// 
+    bool SaveCreatedPackage(PackageDefinition definition);
 
-        /// 
-        /// Creates the package file and returns it's physical path
-        /// 
-        /// 
-        string ExportCreatedPackage(PackageDefinition definition);
-
-    }
+    /// 
+    ///     Creates the package file and returns it's physical path
+    /// 
+    /// 
+    string ExportCreatedPackage(PackageDefinition definition);
 }
diff --git a/src/Umbraco.Core/Services/IPropertyTypeUsageService.cs b/src/Umbraco.Core/Services/IPropertyTypeUsageService.cs
new file mode 100644
index 0000000000..231e535cc4
--- /dev/null
+++ b/src/Umbraco.Core/Services/IPropertyTypeUsageService.cs
@@ -0,0 +1,11 @@
+namespace Umbraco.Cms.Core.Services;
+
+public interface IPropertyTypeUsageService
+{
+    /// 
+    /// Checks if a property type has any saved property values associated with it.
+    /// 
+    /// The alias of the property type to check.
+    /// True if the property type has any property values, otherwise false.
+    bool HasSavedPropertyValues(string propertyTypeAlias);
+}
diff --git a/src/Umbraco.Core/Services/IPropertyValidationService.cs b/src/Umbraco.Core/Services/IPropertyValidationService.cs
index c2b8824340..e854d0f7f5 100644
--- a/src/Umbraco.Core/Services/IPropertyValidationService.cs
+++ b/src/Umbraco.Core/Services/IPropertyValidationService.cs
@@ -1,39 +1,37 @@
-using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.PropertyEditors;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IPropertyValidationService
 {
-    public interface IPropertyValidationService
-    {
-        /// 
-        /// Validates the content item's properties pass validation rules
-        /// 
-        bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact);
+    /// 
+    ///     Validates the content item's properties pass validation rules
+    /// 
+    bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact);
 
-        /// 
-        /// Gets a value indicating whether the property has valid values.
-        /// 
-        bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*");
+    /// 
+    ///     Gets a value indicating whether the property has valid values.
+    /// 
+    bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*");
 
-        /// 
-        /// Validates a property value.
-        /// 
-        IEnumerable ValidatePropertyValue(
-            IDataEditor editor,
-            IDataType dataType,
-            object? postedValue,
-            bool isRequired,
-            string? validationRegExp,
-            string? isRequiredMessage,
-            string? validationRegExpMessage);
+    /// 
+    ///     Validates a property value.
+    /// 
+    IEnumerable ValidatePropertyValue(
+        IDataEditor editor,
+        IDataType dataType,
+        object? postedValue,
+        bool isRequired,
+        string? validationRegExp,
+        string? isRequiredMessage,
+        string? validationRegExpMessage);
 
-        /// 
-        /// Validates a property value.
-        /// 
-        IEnumerable ValidatePropertyValue(
-            IPropertyType propertyType,
-            object? postedValue);
-    }
+    /// 
+    ///     Validates a property value.
+    /// 
+    IEnumerable ValidatePropertyValue(
+        IPropertyType propertyType,
+        object? postedValue);
 }
diff --git a/src/Umbraco.Core/Services/IPublicAccessService.cs b/src/Umbraco.Core/Services/IPublicAccessService.cs
index 96d8ca5d1b..fb4f080e03 100644
--- a/src/Umbraco.Core/Services/IPublicAccessService.cs
+++ b/src/Umbraco.Core/Services/IPublicAccessService.cs
@@ -1,73 +1,69 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IPublicAccessService : IService
 {
-    public interface IPublicAccessService : IService
-    {
+    /// 
+    ///     Gets all defined entries and associated rules
+    /// 
+    /// 
+    IEnumerable GetAll();
 
-        /// 
-        /// Gets all defined entries and associated rules
-        /// 
-        /// 
-        IEnumerable GetAll();
+    /// 
+    ///     Gets the entry defined for the content item's path
+    /// 
+    /// 
+    /// Returns null if no entry is found
+    PublicAccessEntry? GetEntryForContent(IContent content);
 
-        /// 
-        /// Gets the entry defined for the content item's path
-        /// 
-        /// 
-        /// Returns null if no entry is found
-        PublicAccessEntry? GetEntryForContent(IContent content);
+    /// 
+    ///     Gets the entry defined for the content item based on a content path
+    /// 
+    /// 
+    /// Returns null if no entry is found
+    PublicAccessEntry? GetEntryForContent(string contentPath);
 
-        /// 
-        /// Gets the entry defined for the content item based on a content path
-        /// 
-        /// 
-        /// Returns null if no entry is found
-        PublicAccessEntry? GetEntryForContent(string contentPath);
+    /// 
+    ///     Returns true if the content has an entry for it's path
+    /// 
+    /// 
+    /// 
+    Attempt IsProtected(IContent content);
 
-        /// 
-        /// Returns true if the content has an entry for it's path
-        /// 
-        /// 
-        /// 
-        Attempt IsProtected(IContent content);
+    /// 
+    ///     Returns true if the content has an entry based on a content path
+    /// 
+    /// 
+    /// 
+    Attempt IsProtected(string contentPath);
 
-        /// 
-        /// Returns true if the content has an entry based on a content path
-        /// 
-        /// 
-        /// 
-        Attempt IsProtected(string contentPath);
+    /// 
+    ///     Adds a rule if the entry doesn't already exist
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt?> AddRule(IContent content, string ruleType, string ruleValue);
 
-        /// 
-        /// Adds a rule if the entry doesn't already exist
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt?> AddRule(IContent content, string ruleType, string ruleValue);
+    /// 
+    ///     Removes a rule
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt RemoveRule(IContent content, string ruleType, string ruleValue);
 
-        /// 
-        /// Removes a rule
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt RemoveRule(IContent content, string ruleType, string ruleValue);
+    /// 
+    ///     Saves the entry
+    /// 
+    /// 
+    Attempt Save(PublicAccessEntry entry);
 
-        /// 
-        /// Saves the entry
-        /// 
-        /// 
-        Attempt Save(PublicAccessEntry entry);
-
-        /// 
-        /// Deletes the entry and all associated rules
-        /// 
-        /// 
-        Attempt Delete(PublicAccessEntry entry);
-
-    }
+    /// 
+    ///     Deletes the entry and all associated rules
+    /// 
+    /// 
+    Attempt Delete(PublicAccessEntry entry);
 }
diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs
index 3c061db466..f1897053f0 100644
--- a/src/Umbraco.Core/Services/IRedirectUrlService.cs
+++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs
@@ -1,95 +1,100 @@
-using System;
-using System.Collections.Generic;
+using System.Threading.Tasks;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+/// 
+public interface IRedirectUrlService : IService
 {
     /// 
-    ///
+    ///     Registers a redirect URL.
     /// 
-    public interface IRedirectUrlService : IService
-    {
-        /// 
-        /// Registers a redirect URL.
-        /// 
-        /// The Umbraco URL route.
-        /// The content unique key.
-        /// The culture.
-        /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo.
-        void Register(string url, Guid contentKey, string? culture = null);
+    /// The Umbraco URL route.
+    /// The content unique key.
+    /// The culture.
+    /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo.
+    void Register(string url, Guid contentKey, string? culture = null);
+
+    /// 
+    ///     Deletes all redirect URLs for a given content.
+    /// 
+    /// The content unique key.
+    void DeleteContentRedirectUrls(Guid contentKey);
+
+    /// 
+    ///     Deletes a redirect URL.
+    /// 
+    /// The redirect URL to delete.
+    void Delete(IRedirectUrl redirectUrl);
+
+    /// 
+    ///     Deletes a redirect URL.
+    /// 
+    /// The redirect URL identifier.
+    void Delete(Guid id);
+
+    /// 
+    ///     Deletes all redirect URLs.
+    /// 
+    void DeleteAll();
+
+    /// 
+    ///     Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
+    /// 
+    /// The Umbraco redirect URL route.
+    /// The most recent redirect URLs corresponding to the route.
+    IRedirectUrl? GetMostRecentRedirectUrl(string url);
+
+    /// 
+    ///     Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
+    /// 
+    /// The Umbraco redirect URL route.
+    /// The culture of the request.
+    /// The most recent redirect URLs corresponding to the route.
+    IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture);
+
+    /// 
+    /// Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
+    /// 
+    /// The Umbraco redirect URL route.
+    /// The culture of the request.
+    /// The most recent redirect URLs corresponding to the route.
+    Task GetMostRecentRedirectUrlAsync(string url, string? culture) => Task.FromResult(GetMostRecentRedirectUrl(url, culture));
 
         /// 
-        /// Deletes all redirect URLs for a given content.
-        /// 
-        /// The content unique key.
-        void DeleteContentRedirectUrls(Guid contentKey);
+    ///     Gets all redirect URLs for a content item.
+    /// 
+    /// The content unique key.
+    /// All redirect URLs for the content item.
+    IEnumerable GetContentRedirectUrls(Guid contentKey);
 
-        /// 
-        /// Deletes a redirect URL.
-        /// 
-        /// The redirect URL to delete.
-        void Delete(IRedirectUrl redirectUrl);
+    /// 
+    ///     Gets all redirect URLs.
+    /// 
+    /// The page index.
+    /// The page size.
+    /// The total count of redirect URLs.
+    /// The redirect URLs.
+    IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total);
 
-        /// 
-        /// Deletes a redirect URL.
-        /// 
-        /// The redirect URL identifier.
-        void Delete(Guid id);
+    /// 
+    ///     Gets all redirect URLs below a given content item.
+    /// 
+    /// The content unique identifier.
+    /// The page index.
+    /// The page size.
+    /// The total count of redirect URLs.
+    /// The redirect URLs.
+    IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total);
 
-        /// 
-        /// Deletes all redirect URLs.
-        /// 
-        void DeleteAll();
-
-        /// 
-        /// Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
-        /// 
-        /// The Umbraco redirect URL route.
-        /// The most recent redirect URLs corresponding to the route.
-        IRedirectUrl? GetMostRecentRedirectUrl(string url);
-
-        /// 
-        /// Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
-        /// 
-        /// The Umbraco redirect URL route.
-        /// The culture of the request.
-        /// The most recent redirect URLs corresponding to the route.
-        IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture);
-
-        /// 
-        /// Gets all redirect URLs for a content item.
-        /// 
-        /// The content unique key.
-        /// All redirect URLs for the content item.
-        IEnumerable GetContentRedirectUrls(Guid contentKey);
-
-        /// 
-        /// Gets all redirect URLs.
-        /// 
-        /// The page index.
-        /// The page size.
-        /// The total count of redirect URLs.
-        /// The redirect URLs.
-        IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total);
-
-        /// 
-        /// Gets all redirect URLs below a given content item.
-        /// 
-        /// The content unique identifier.
-        /// The page index.
-        /// The page size.
-        /// The total count of redirect URLs.
-        /// The redirect URLs.
-        IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total);
-
-        /// 
-        /// Searches for all redirect URLs that contain a given search term in their URL property.
-        /// 
-        /// The term to search for.
-        /// The page index.
-        /// The page size.
-        /// The total count of redirect URLs.
-        /// The redirect URLs.
-        IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total);
-    }
+    /// 
+    ///     Searches for all redirect URLs that contain a given search term in their URL property.
+    /// 
+    /// The term to search for.
+    /// The page index.
+    /// The page size.
+    /// The total count of redirect URLs.
+    /// The redirect URLs.
+    IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total);
 }
diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs
index a0825611f7..6f8fa9b75a 100644
--- a/src/Umbraco.Core/Services/IRelationService.cs
+++ b/src/Umbraco.Core/Services/IRelationService.cs
@@ -1,357 +1,353 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IRelationService : IService
 {
-    public interface IRelationService : IService
-    {
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// A  object
-        IRelation? GetById(int id);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// A  object
+    IRelation? GetById(int id);
 
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// A  object
-        IRelationType? GetRelationTypeById(int id);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// A  object
+    IRelationType? GetRelationTypeById(int id);
 
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// A  object
-        IRelationType? GetRelationTypeById(Guid id);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// A  object
+    IRelationType? GetRelationTypeById(Guid id);
 
-        /// 
-        /// Gets a  by its Alias
-        /// 
-        /// Alias of the 
-        /// A  object
-        IRelationType? GetRelationTypeByAlias(string alias);
+    /// 
+    ///     Gets a  by its Alias
+    /// 
+    /// Alias of the 
+    /// A  object
+    IRelationType? GetRelationTypeByAlias(string alias);
 
-        /// 
-        /// Gets all  objects
-        /// 
-        /// Optional array of integer ids to return relations for
-        /// An enumerable list of  objects
-        IEnumerable GetAllRelations(params int[] ids);
+    /// 
+    ///     Gets all  objects
+    /// 
+    /// Optional array of integer ids to return relations for
+    /// An enumerable list of  objects
+    IEnumerable GetAllRelations(params int[] ids);
 
-        /// 
-        /// Gets all  objects by their 
-        /// 
-        ///  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetAllRelationsByRelationType(IRelationType relationType);
+    /// 
+    ///     Gets all  objects by their 
+    /// 
+    ///  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetAllRelationsByRelationType(IRelationType relationType);
 
-        /// 
-        /// Gets all  objects by their 's Id
-        /// 
-        /// Id of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetAllRelationsByRelationType(int relationTypeId);
+    /// 
+    ///     Gets all  objects by their 's Id
+    /// 
+    /// Id of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetAllRelationsByRelationType(int relationTypeId);
 
-        /// 
-        /// Gets all  objects
-        /// 
-        /// Optional array of integer ids to return relationtypes for
-        /// An enumerable list of  objects
-        IEnumerable GetAllRelationTypes(params int[] ids);
+    /// 
+    ///     Gets all  objects
+    /// 
+    /// Optional array of integer ids to return relationtypes for
+    /// An enumerable list of  objects
+    IEnumerable GetAllRelationTypes(params int[] ids);
 
-        /// 
-        /// Gets a list of  objects by their parent Id
-        /// 
-        /// Id of the parent to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetByParentId(int id);
+    /// 
+    ///     Gets a list of  objects by their parent Id
+    /// 
+    /// Id of the parent to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetByParentId(int id);
 
-        /// 
-        /// Gets a list of  objects by their parent Id
-        /// 
-        /// Id of the parent to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable? GetByParentId(int id, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their parent Id
+    /// 
+    /// Id of the parent to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable? GetByParentId(int id, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their parent entity
-        /// 
-        /// Parent Entity to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetByParent(IUmbracoEntity parent);
+    /// 
+    ///     Gets a list of  objects by their parent entity
+    /// 
+    /// Parent Entity to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetByParent(IUmbracoEntity parent);
 
-        /// 
-        /// Gets a list of  objects by their parent entity
-        /// 
-        /// Parent Entity to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their parent entity
+    /// 
+    /// Parent Entity to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their child Id
-        /// 
-        /// Id of the child to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByChildId(int id);
+    /// 
+    ///     Gets a list of  objects by their child Id
+    /// 
+    /// Id of the child to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByChildId(int id);
 
-        /// 
-        /// Gets a list of  objects by their child Id
-        /// 
-        /// Id of the child to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable GetByChildId(int id, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their child Id
+    /// 
+    /// Id of the child to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable GetByChildId(int id, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their child Entity
-        /// 
-        /// Child Entity to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByChild(IUmbracoEntity child);
+    /// 
+    ///     Gets a list of  objects by their child Entity
+    /// 
+    /// Child Entity to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByChild(IUmbracoEntity child);
 
-        /// 
-        /// Gets a list of  objects by their child Entity
-        /// 
-        /// Child Entity to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their child Entity
+    /// 
+    /// Child Entity to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their child or parent Id.
-        /// Using this method will get you all relations regards of it being a child or parent relation.
-        /// 
-        /// Id of the child or parent to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByParentOrChildId(int id);
+    /// 
+    ///     Gets a list of  objects by their child or parent Id.
+    ///     Using this method will get you all relations regards of it being a child or parent relation.
+    /// 
+    /// Id of the child or parent to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByParentOrChildId(int id);
 
-        IEnumerable GetByParentOrChildId(int id, string relationTypeAlias);
+    IEnumerable GetByParentOrChildId(int id, string relationTypeAlias);
 
-        /// 
-        /// Gets a relation by the unique combination of parentId, childId and relationType.
-        /// 
-        /// The id of the parent item.
-        /// The id of the child item.
-        /// The RelationType.
-        /// The relation or null
-        IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType);
+    /// 
+    ///     Gets a relation by the unique combination of parentId, childId and relationType.
+    /// 
+    /// The id of the parent item.
+    /// The id of the child item.
+    /// The RelationType.
+    /// The relation or null
+    IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType);
 
-        /// 
-        /// Gets a list of  objects by the Name of the 
-        /// 
-        /// Name of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByRelationTypeName(string relationTypeName);
+    /// 
+    ///     Gets a list of  objects by the Name of the 
+    /// 
+    /// Name of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByRelationTypeName(string relationTypeName);
 
-        /// 
-        /// Gets a list of  objects by the Alias of the 
-        /// 
-        /// Alias of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByRelationTypeAlias(string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by the Alias of the 
+    /// 
+    /// Alias of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByRelationTypeAlias(string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by the Id of the 
-        /// 
-        /// Id of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetByRelationTypeId(int relationTypeId);
+    /// 
+    ///     Gets a list of  objects by the Id of the 
+    /// 
+    /// Id of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetByRelationTypeId(int relationTypeId);
 
-        /// 
-        /// Gets a paged result of 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null);
+    /// 
+    ///     Gets a paged result of 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null);
 
-        /// 
-        /// Gets the Child object from a Relation as an 
-        /// 
-        /// Relation to retrieve child object from
-        /// An 
-        IUmbracoEntity? GetChildEntityFromRelation(IRelation relation);
+    /// 
+    ///     Gets the Child object from a Relation as an 
+    /// 
+    /// Relation to retrieve child object from
+    /// An 
+    IUmbracoEntity? GetChildEntityFromRelation(IRelation relation);
 
-        /// 
-        /// Gets the Parent object from a Relation as an 
-        /// 
-        /// Relation to retrieve parent object from
-        /// An 
-        IUmbracoEntity? GetParentEntityFromRelation(IRelation relation);
+    /// 
+    ///     Gets the Parent object from a Relation as an 
+    /// 
+    /// Relation to retrieve parent object from
+    /// An 
+    IUmbracoEntity? GetParentEntityFromRelation(IRelation relation);
 
-        /// 
-        /// Gets the Parent and Child objects from a Relation as a "/> with .
-        /// 
-        /// Relation to retrieve parent and child object from
-        /// Returns a Tuple with Parent (item1) and Child (item2)
-        Tuple? GetEntitiesFromRelation(IRelation relation);
+    /// 
+    ///     Gets the Parent and Child objects from a Relation as a "/> with .
+    /// 
+    /// Relation to retrieve parent and child object from
+    /// Returns a Tuple with Parent (item1) and Child (item2)
+    Tuple? GetEntitiesFromRelation(IRelation relation);
 
-        /// 
-        /// Gets the Child objects from a list of Relations as a list of  objects.
-        /// 
-        /// List of relations to retrieve child objects from
-        /// An enumerable list of 
-        IEnumerable GetChildEntitiesFromRelations(IEnumerable relations);
+    /// 
+    ///     Gets the Child objects from a list of Relations as a list of  objects.
+    /// 
+    /// List of relations to retrieve child objects from
+    /// An enumerable list of 
+    IEnumerable GetChildEntitiesFromRelations(IEnumerable relations);
 
-        /// 
-        /// Gets the Parent objects from a list of Relations as a list of  objects.
-        /// 
-        /// List of relations to retrieve parent objects from
-        /// An enumerable list of 
-        IEnumerable GetParentEntitiesFromRelations(IEnumerable relations);
+    /// 
+    ///     Gets the Parent objects from a list of Relations as a list of  objects.
+    /// 
+    /// List of relations to retrieve parent objects from
+    /// An enumerable list of 
+    IEnumerable GetParentEntitiesFromRelations(IEnumerable relations);
 
-        /// 
-        /// Returns paged parent entities for a related child id
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// An enumerable list of 
-        IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
+    /// 
+    ///     Returns paged parent entities for a related child id
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// An enumerable list of 
+    IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
 
-        /// 
-        /// Returns paged child entities for a related parent id
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// An enumerable list of 
-        IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
+    /// 
+    ///     Returns paged child entities for a related parent id
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// An enumerable list of 
+    IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
 
-        /// 
-        /// Gets the Parent and Child objects from a list of Relations as a list of  objects.
-        /// 
-        /// List of relations to retrieve parent and child objects from
-        /// An enumerable list of  with 
-        IEnumerable> GetEntitiesFromRelations(IEnumerable relations);
+    /// 
+    ///     Gets the Parent and Child objects from a list of Relations as a list of  objects.
+    /// 
+    /// List of relations to retrieve parent and child objects from
+    /// An enumerable list of  with 
+    IEnumerable> GetEntitiesFromRelations(IEnumerable relations);
 
-        /// 
-        /// Relates two objects by their entity Ids.
-        /// 
-        /// Id of the parent
-        /// Id of the child
-        /// The type of relation to create
-        /// The created 
-        IRelation Relate(int parentId, int childId, IRelationType relationType);
+    /// 
+    ///     Relates two objects by their entity Ids.
+    /// 
+    /// Id of the parent
+    /// Id of the child
+    /// The type of relation to create
+    /// The created 
+    IRelation Relate(int parentId, int childId, IRelationType relationType);
 
-        /// 
-        /// Relates two objects that are based on the  interface.
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// The type of relation to create
-        /// The created 
-        IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType);
+    /// 
+    ///     Relates two objects that are based on the  interface.
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// The type of relation to create
+    /// The created 
+    IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType);
 
-        /// 
-        /// Relates two objects by their entity Ids.
-        /// 
-        /// Id of the parent
-        /// Id of the child
-        /// Alias of the type of relation to create
-        /// The created 
-        IRelation Relate(int parentId, int childId, string relationTypeAlias);
+    /// 
+    ///     Relates two objects by their entity Ids.
+    /// 
+    /// Id of the parent
+    /// Id of the child
+    /// Alias of the type of relation to create
+    /// The created 
+    IRelation Relate(int parentId, int childId, string relationTypeAlias);
 
-        /// 
-        /// Relates two objects that are based on the  interface.
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// Alias of the type of relation to create
-        /// The created 
-        IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
+    /// 
+    ///     Relates two objects that are based on the  interface.
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// Alias of the type of relation to create
+    /// The created 
+    IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
 
-        /// 
-        /// Checks whether any relations exists for the passed in .
-        /// 
-        ///  to check for relations
-        /// Returns True if any relations exists for the given , otherwise False
-        bool HasRelations(IRelationType relationType);
+    /// 
+    ///     Checks whether any relations exists for the passed in .
+    /// 
+    ///  to check for relations
+    /// 
+    ///     Returns True if any relations exists for the given , otherwise False
+    /// 
+    bool HasRelations(IRelationType relationType);
 
-        /// 
-        /// Checks whether any relations exists for the passed in Id.
-        /// 
-        /// Id of an object to check relations for
-        /// Returns True if any relations exists with the given Id, otherwise False
-        bool IsRelated(int id);
+    /// 
+    ///     Checks whether any relations exists for the passed in Id.
+    /// 
+    /// Id of an object to check relations for
+    /// Returns True if any relations exists with the given Id, otherwise False
+    bool IsRelated(int id);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Id of the Parent relation
-        /// Id of the Child relation
-        /// Returns True if any relations exists with the given Ids, otherwise False
-        bool AreRelated(int parentId, int childId);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Id of the Parent relation
+    /// Id of the Child relation
+    /// Returns True if any relations exists with the given Ids, otherwise False
+    bool AreRelated(int parentId, int childId);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// Returns True if any relations exist between the entities, otherwise False
-        bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// Returns True if any relations exist between the entities, otherwise False
+    bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// Alias of the type of relation to create
-        /// Returns True if any relations exist between the entities, otherwise False
-        bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// Alias of the type of relation to create
+    /// Returns True if any relations exist between the entities, otherwise False
+    bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Id of the Parent relation
-        /// Id of the Child relation
-        /// Alias of the type of relation to create
-        /// Returns True if any relations exist between the entities, otherwise False
-        bool AreRelated(int parentId, int childId, string relationTypeAlias);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Id of the Parent relation
+    /// Id of the Child relation
+    /// Alias of the type of relation to create
+    /// Returns True if any relations exist between the entities, otherwise False
+    bool AreRelated(int parentId, int childId, string relationTypeAlias);
 
-        /// 
-        /// Saves a 
-        /// 
-        /// Relation to save
-        void Save(IRelation relation);
+    /// 
+    ///     Saves a 
+    /// 
+    /// Relation to save
+    void Save(IRelation relation);
 
-        void Save(IEnumerable relations);
+    void Save(IEnumerable relations);
 
-        /// 
-        /// Saves a 
-        /// 
-        /// RelationType to Save
-        void Save(IRelationType relationType);
+    /// 
+    ///     Saves a 
+    /// 
+    /// RelationType to Save
+    void Save(IRelationType relationType);
 
-        /// 
-        /// Deletes a 
-        /// 
-        /// Relation to Delete
-        void Delete(IRelation relation);
+    /// 
+    ///     Deletes a 
+    /// 
+    /// Relation to Delete
+    void Delete(IRelation relation);
 
-        /// 
-        /// Deletes a 
-        /// 
-        /// RelationType to Delete
-        void Delete(IRelationType relationType);
+    /// 
+    ///     Deletes a 
+    /// 
+    /// RelationType to Delete
+    void Delete(IRelationType relationType);
 
-        /// 
-        /// Deletes all  objects based on the passed in 
-        /// 
-        ///  to Delete Relations for
-        void DeleteRelationsOfType(IRelationType relationType);
-
-
-
-    }
+    /// 
+    ///     Deletes all  objects based on the passed in 
+    /// 
+    ///  to Delete Relations for
+    void DeleteRelationsOfType(IRelationType relationType);
 }
diff --git a/src/Umbraco.Core/Services/IRuntime.cs b/src/Umbraco.Core/Services/IRuntime.cs
index caa430ce1f..53ac51f585 100644
--- a/src/Umbraco.Core/Services/IRuntime.cs
+++ b/src/Umbraco.Core/Services/IRuntime.cs
@@ -1,22 +1,19 @@
-using System.Threading;
-using System.Threading.Tasks;
 using Microsoft.Extensions.Hosting;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the Umbraco runtime.
+/// 
+public interface IRuntime : IHostedService
 {
     /// 
-    /// Defines the Umbraco runtime.
+    ///     Gets the runtime state.
     /// 
-    public interface IRuntime : IHostedService
-    {
-        /// 
-        /// Gets the runtime state.
-        /// 
-        IRuntimeState State { get; }
+    IRuntimeState State { get; }
 
-        /// 
-        /// Stops and Starts the runtime using the original cancellation token.
-        /// 
-        Task RestartAsync();
-    }
+    /// 
+    ///     Stops and Starts the runtime using the original cancellation token.
+    /// 
+    Task RestartAsync();
 }
diff --git a/src/Umbraco.Core/Services/IRuntimeState.cs b/src/Umbraco.Core/Services/IRuntimeState.cs
index 3c765a0748..a576671010 100644
--- a/src/Umbraco.Core/Services/IRuntimeState.cs
+++ b/src/Umbraco.Core/Services/IRuntimeState.cs
@@ -1,65 +1,62 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Exceptions;
 using Umbraco.Cms.Core.Semver;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the state of the Umbraco runtime.
+/// 
+public interface IRuntimeState
 {
     /// 
-    /// Represents the state of the Umbraco runtime.
+    ///     Gets the version of the executing code.
     /// 
-    public interface IRuntimeState
-    {
-        /// 
-        /// Gets the version of the executing code.
-        /// 
-        Version Version { get; }
+    Version Version { get; }
 
-        /// 
-        /// Gets the version comment of the executing code.
-        /// 
-        string VersionComment { get; }
+    /// 
+    ///     Gets the version comment of the executing code.
+    /// 
+    string VersionComment { get; }
 
-        /// 
-        /// Gets the semantic version of the executing code.
-        /// 
-        SemVersion SemanticVersion { get; }
+    /// 
+    ///     Gets the semantic version of the executing code.
+    /// 
+    SemVersion SemanticVersion { get; }
 
-        /// 
-        /// Gets the runtime level of execution.
-        /// 
-        RuntimeLevel Level { get; }
+    /// 
+    ///     Gets the runtime level of execution.
+    /// 
+    RuntimeLevel Level { get; }
 
-        /// 
-        /// Gets the reason for the runtime level of execution.
-        /// 
-        RuntimeLevelReason Reason { get; }
+    /// 
+    ///     Gets the reason for the runtime level of execution.
+    /// 
+    RuntimeLevelReason Reason { get; }
 
-        /// 
-        /// Gets the current migration state.
-        /// 
-        string? CurrentMigrationState { get; }
+    /// 
+    ///     Gets the current migration state.
+    /// 
+    string? CurrentMigrationState { get; }
 
-        /// 
-        /// Gets the final migration state.
-        /// 
-        string? FinalMigrationState { get; }
+    /// 
+    ///     Gets the final migration state.
+    /// 
+    string? FinalMigrationState { get; }
 
-        /// 
-        /// Gets the exception that caused the boot to fail.
-        /// 
-        BootFailedException? BootFailedException { get; }
+    /// 
+    ///     Gets the exception that caused the boot to fail.
+    /// 
+    BootFailedException? BootFailedException { get; }
 
-        /// 
-        /// Determines the runtime level.
-        /// 
-        void DetermineRuntimeLevel();
+    /// 
+    ///     Returns any state data that was collected during startup
+    /// 
+    IReadOnlyDictionary StartupState { get; }
 
-        void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception? bootFailedException = null);
+    /// 
+    ///     Determines the runtime level.
+    /// 
+    void DetermineRuntimeLevel();
 
-        /// 
-        /// Returns any state data that was collected during startup
-        /// 
-        IReadOnlyDictionary StartupState { get; }
-    }
+    void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception? bootFailedException = null);
 }
diff --git a/src/Umbraco.Core/Services/ISectionService.cs b/src/Umbraco.Core/Services/ISectionService.cs
index ded733963b..515896cafc 100644
--- a/src/Umbraco.Core/Services/ISectionService.cs
+++ b/src/Umbraco.Core/Services/ISectionService.cs
@@ -1,27 +1,25 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Sections;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface ISectionService
 {
-    public interface ISectionService
-    {
-        /// 
-        /// The cache storage for all applications
-        /// 
-        IEnumerable GetSections();
+    /// 
+    ///     The cache storage for all applications
+    /// 
+    IEnumerable GetSections();
 
-        /// 
-        /// Get the user group's allowed sections
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAllowedSections(int userId);
+    /// 
+    ///     Get the user group's allowed sections
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAllowedSections(int userId);
 
-        /// 
-        /// Gets the application by its alias.
-        /// 
-        /// The application alias.
-        /// 
-        ISection? GetByAlias(string appAlias);
-    }
+    /// 
+    ///     Gets the application by its alias.
+    /// 
+    /// The application alias.
+    /// 
+    ISection? GetByAlias(string appAlias);
 }
diff --git a/src/Umbraco.Core/Services/IServerRegistrationService.cs b/src/Umbraco.Core/Services/IServerRegistrationService.cs
index e469de9a06..4a08492079 100644
--- a/src/Umbraco.Core/Services/IServerRegistrationService.cs
+++ b/src/Umbraco.Core/Services/IServerRegistrationService.cs
@@ -1,57 +1,58 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Sync;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IServerRegistrationService
 {
-    public interface IServerRegistrationService
-    {
-        /// 
-        /// Touches a server to mark it as active; deactivate stale servers.
-        /// 
-        /// The server URL.
-        /// The time after which a server is considered stale.
-        void TouchServer(string serverAddress, TimeSpan staleTimeout);
+    /// 
+    ///     Touches a server to mark it as active; deactivate stale servers.
+    /// 
+    /// The server URL.
+    /// The time after which a server is considered stale.
+    void TouchServer(string serverAddress, TimeSpan staleTimeout);
 
-        /// 
-        /// Deactivates a server.
-        /// 
-        /// The server unique identity.
-        void DeactiveServer(string serverIdentity);
+    /// 
+    ///     Deactivates a server.
+    /// 
+    /// The server unique identity.
+    void DeactiveServer(string serverIdentity);
 
-        /// 
-        /// Deactivates stale servers.
-        /// 
-        /// The time after which a server is considered stale.
-        void DeactiveStaleServers(TimeSpan staleTimeout);
+    /// 
+    ///     Deactivates stale servers.
+    /// 
+    /// The time after which a server is considered stale.
+    void DeactiveStaleServers(TimeSpan staleTimeout);
 
-        /// 
-        /// Return all active servers.
-        /// 
-        /// A value indicating whether to force-refresh the cache.
-        /// All active servers.
-        /// By default this method will rely on the repository's cache, which is updated each
-        /// time the current server is touched, and the period depends on the configuration. Use the
-        ///  parameter to force a cache refresh and reload active servers
-        /// from the database.
-        IEnumerable? GetActiveServers(bool refresh = false);
+    /// 
+    ///     Return all active servers.
+    /// 
+    /// A value indicating whether to force-refresh the cache.
+    /// All active servers.
+    /// 
+    ///     By default this method will rely on the repository's cache, which is updated each
+    ///     time the current server is touched, and the period depends on the configuration. Use the
+    ///      parameter to force a cache refresh and reload active servers
+    ///     from the database.
+    /// 
+    IEnumerable? GetActiveServers(bool refresh = false);
 
-        /// 
-        /// Return all servers (active and inactive).
-        /// 
-        /// A value indicating whether to force-refresh the cache.
-        /// All servers.
-        /// By default this method will rely on the repository's cache, which is updated each
-        /// time the current server is touched, and the period depends on the configuration. Use the
-        ///  parameter to force a cache refresh and reload all servers
-        /// from the database.
-        IEnumerable GetServers(bool refresh = false);
+    /// 
+    ///     Return all servers (active and inactive).
+    /// 
+    /// A value indicating whether to force-refresh the cache.
+    /// All servers.
+    /// 
+    ///     By default this method will rely on the repository's cache, which is updated each
+    ///     time the current server is touched, and the period depends on the configuration. Use the
+    ///      parameter to force a cache refresh and reload all servers
+    ///     from the database.
+    /// 
+    IEnumerable GetServers(bool refresh = false);
 
-        /// 
-        /// Gets the role of the current server.
-        /// 
-        /// The role of the current server.
-        ServerRole GetCurrentServerRole();
-    }
+    /// 
+    ///     Gets the role of the current server.
+    /// 
+    /// The role of the current server.
+    ServerRole GetCurrentServerRole();
 }
diff --git a/src/Umbraco.Core/Services/IService.cs b/src/Umbraco.Core/Services/IService.cs
index 6ca00a8dbe..3147b34a56 100644
--- a/src/Umbraco.Core/Services/IService.cs
+++ b/src/Umbraco.Core/Services/IService.cs
@@ -1,10 +1,8 @@
-namespace Umbraco.Cms.Core.Services
-{
-    /// 
-    /// Marker interface for services, which is used to store difference services in a list or dictionary
-    /// 
-    public interface IService
-    {
+namespace Umbraco.Cms.Core.Services;
 
-    }
+/// 
+///     Marker interface for services, which is used to store difference services in a list or dictionary
+/// 
+public interface IService
+{
 }
diff --git a/src/Umbraco.Core/Services/ITagService.cs b/src/Umbraco.Core/Services/ITagService.cs
index 70c4ba81b6..5e2f164a35 100644
--- a/src/Umbraco.Core/Services/ITagService.cs
+++ b/src/Umbraco.Core/Services/ITagService.cs
@@ -1,98 +1,95 @@
-using System;
-using System.Collections.Generic;
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Tag service to query for tags in the tags db table. The tags returned are only relevant for published content &
+///     saved media or members
+/// 
+/// 
+///     If there is unpublished content with tags, those tags will not be contained.
+///     This service does not contain methods to query for content, media or members based on tags, those methods will be added
+///     to the content, media and member services respectively.
+/// 
+public interface ITagService : IService
 {
     /// 
-    /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members
+    ///     Gets a tagged entity.
     /// 
-    /// 
-    /// If there is unpublished content with tags, those tags will not be contained.
-    ///
-    /// This service does not contain methods to query for content, media or members based on tags, those methods will be added
-    /// to the content, media and member services respectively.
-    /// 
-    public interface ITagService : IService
-    {
-        /// 
-        /// Gets a tagged entity.
-        /// 
-        TaggedEntity? GetTaggedEntityById(int id);
+    TaggedEntity? GetTaggedEntityById(int id);
 
-        /// 
-        /// Gets a tagged entity.
-        /// 
-        TaggedEntity? GetTaggedEntityByKey(Guid key);
+    /// 
+    ///     Gets a tagged entity.
+    /// 
+    TaggedEntity? GetTaggedEntityByKey(Guid key);
 
-        /// 
-        /// Gets all documents tagged with any tag in the specified group.
-        /// 
-        IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null);
+    /// 
+    ///     Gets all documents tagged with any tag in the specified group.
+    /// 
+    IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null);
 
-        /// 
-        /// Gets all documents tagged with the specified tag.
-        /// 
-        IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all documents tagged with the specified tag.
+    /// 
+    IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all media tagged with any tag in the specified group.
-        /// 
-        IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null);
+    /// 
+    ///     Gets all media tagged with any tag in the specified group.
+    /// 
+    IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null);
 
-        /// 
-        /// Gets all media tagged with the specified tag.
-        /// 
-        IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all media tagged with the specified tag.
+    /// 
+    IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all members tagged with any tag in the specified group.
-        /// 
-        IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null);
+    /// 
+    ///     Gets all members tagged with any tag in the specified group.
+    /// 
+    IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null);
 
-        /// 
-        /// Gets all members tagged with the specified tag.
-        /// 
-        IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all members tagged with the specified tag.
+    /// 
+    IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags.
-        /// 
-        IEnumerable GetAllTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags.
+    /// 
+    IEnumerable GetAllTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all document tags.
-        /// 
-        IEnumerable GetAllContentTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all document tags.
+    /// 
+    IEnumerable GetAllContentTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all media tags.
-        /// 
-        IEnumerable GetAllMediaTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all media tags.
+    /// 
+    IEnumerable GetAllMediaTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all member tags.
-        /// 
-        IEnumerable GetAllMemberTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all member tags.
+    /// 
+    IEnumerable GetAllMemberTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity via a property.
-        /// 
-        IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags attached to an entity via a property.
+    /// 
+    IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity.
-        /// 
-        IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags attached to an entity.
+    /// 
+    IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity via a property.
-        /// 
-        IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags attached to an entity via a property.
+    /// 
+    IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity.
-        /// 
-        IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null);
-    }
+    /// 
+    ///     Gets all tags attached to an entity.
+    /// 
+    IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null);
 }
diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
index dea99c0f6d..16b953c35a 100644
--- a/src/Umbraco.Core/Services/ITrackedReferencesService.cs
+++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
@@ -1,38 +1,46 @@
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface ITrackedReferencesService
 {
-    public interface ITrackedReferencesService
-    {
-        /// 
-        /// Gets a paged result of items which are in relation with the current item.
-        /// Basically, shows the items which depend on the current item.
-        /// 
-        /// The identifier of the entity to retrieve relations for.
-        /// The page index.
-        /// The page size.
-        /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
-        /// A paged result of  objects.
-        PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+    /// 
+    ///     Gets a paged result of items which are in relation with the current item.
+    ///     Basically, shows the items which depend on the current item.
+    /// 
+    /// The identifier of the entity to retrieve relations for.
+    /// The page index.
+    /// The page size.
+    /// 
+    ///     A boolean indicating whether to filter only the RelationTypes which are
+    ///     dependencies (isDependency field is set to true).
+    /// 
+    /// A paged result of  objects.
+    PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency);
 
-        /// 
-        /// Gets a paged result of the descending items that have any references, given a parent id.
-        /// 
-        /// The unique identifier of the parent to retrieve descendants for.
-        /// The page index.
-        /// The page size.
-        /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
-        /// A paged result of  objects.
-        PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+    /// 
+    ///     Gets a paged result of the descending items that have any references, given a parent id.
+    /// 
+    /// The unique identifier of the parent to retrieve descendants for.
+    /// The page index.
+    /// The page size.
+    /// 
+    ///     A boolean indicating whether to filter only the RelationTypes which are
+    ///     dependencies (isDependency field is set to true).
+    /// 
+    /// A paged result of  objects.
+    PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency);
 
-        /// 
-        /// Gets a paged result of items used in any kind of relation from selected integer ids.
-        /// 
-        /// The identifiers of the entities to check for relations.
-        /// The page index.
-        /// The page size.
-        /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
-        /// A paged result of  objects.
-        PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
-    }
+    /// 
+    ///     Gets a paged result of items used in any kind of relation from selected integer ids.
+    /// 
+    /// The identifiers of the entities to check for relations.
+    /// The page index.
+    /// The page size.
+    /// 
+    ///     A boolean indicating whether to filter only the RelationTypes which are
+    ///     dependencies (isDependency field is set to true).
+    /// 
+    /// A paged result of  objects.
+    PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
 }
diff --git a/src/Umbraco.Core/Services/ITreeService.cs b/src/Umbraco.Core/Services/ITreeService.cs
index b67e36e15b..d61fca066a 100644
--- a/src/Umbraco.Core/Services/ITreeService.cs
+++ b/src/Umbraco.Core/Services/ITreeService.cs
@@ -1,32 +1,30 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Trees;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents a service which manages section trees.
+/// 
+public interface ITreeService
 {
     /// 
-    /// Represents a service which manages section trees.
+    ///     Gets a tree.
     /// 
-    public interface ITreeService
-    {
-        /// 
-        /// Gets a tree.
-        /// 
-        /// The tree alias.
-        Tree? GetByAlias(string treeAlias);
+    /// The tree alias.
+    Tree? GetByAlias(string treeAlias);
 
-        /// 
-        /// Gets all trees.
-        /// 
-        IEnumerable GetAll(TreeUse use = TreeUse.Main);
+    /// 
+    ///     Gets all trees.
+    /// 
+    IEnumerable GetAll(TreeUse use = TreeUse.Main);
 
-        /// 
-        /// Gets all trees for a section.
-        /// 
-        IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main);
+    /// 
+    ///     Gets all trees for a section.
+    /// 
+    IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main);
 
-        /// 
-        /// Gets all trees for a section, grouped.
-        /// 
-        IDictionary> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main);
-    }
+    /// 
+    ///     Gets all trees for a section, grouped.
+    /// 
+    IDictionary> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main);
 }
diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
index a577dcedf7..0a0cc751d5 100644
--- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
+++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
@@ -1,71 +1,68 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Service handling 2FA logins.
+/// 
+public interface ITwoFactorLoginService : IService
 {
     /// 
-    /// Service handling 2FA logins.
+    ///     Deletes all user logins - normally used when a member is deleted.
     /// 
-    public interface ITwoFactorLoginService : IService
-    {
-        /// 
-        /// Deletes all user logins - normally used when a member is deleted.
-        /// 
-        Task DeleteUserLoginsAsync(Guid userOrMemberKey);
+    Task DeleteUserLoginsAsync(Guid userOrMemberKey);
 
-        /// 
-        /// Checks whether 2FA is enabled for the user or member with the specified key.
-        /// 
-        Task IsTwoFactorEnabledAsync(Guid userOrMemberKey);
+    /// 
+    ///     Checks whether 2FA is enabled for the user or member with the specified key.
+    /// 
+    Task IsTwoFactorEnabledAsync(Guid userOrMemberKey);
 
-        /// 
-        /// Gets the secret for user or member and a specific provider.
-        /// 
-        Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName);
+    /// 
+    ///     Gets the secret for user or member and a specific provider.
+    /// 
+    Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName);
 
-        /// 
-        /// Gets the setup info for a specific user or member and a specific provider.
-        /// 
-        /// 
-        /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by the provider.
-        /// 
-        Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName);
+    /// 
+    ///     Gets the setup info for a specific user or member and a specific provider.
+    /// 
+    /// 
+    ///     The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by
+    ///     the provider.
+    /// 
+    Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName);
 
-        /// 
-        /// Gets all registered providers names.
-        /// 
-        IEnumerable GetAllProviderNames();
+    /// 
+    ///     Gets all registered providers names.
+    /// 
+    IEnumerable GetAllProviderNames();
 
-        /// 
-        /// Disables the 2FA provider with the specified provider name for the specified user or member.
-        /// 
-        Task DisableAsync(Guid userOrMemberKey, string providerName);
+    /// 
+    ///     Disables the 2FA provider with the specified provider name for the specified user or member.
+    /// 
+    Task DisableAsync(Guid userOrMemberKey, string providerName);
 
-        /// 
-        /// Validates the setup of the provider using the secret and code.
-        /// 
-        bool ValidateTwoFactorSetup(string providerName, string secret, string code);
+    /// 
+    ///     Validates the setup of the provider using the secret and code.
+    /// 
+    bool ValidateTwoFactorSetup(string providerName, string secret, string code);
 
-        /// 
-        /// Saves the 2FA login information.
-        /// 
-        Task SaveAsync(TwoFactorLogin twoFactorLogin);
+    /// 
+    ///     Saves the 2FA login information.
+    /// 
+    Task SaveAsync(TwoFactorLogin twoFactorLogin);
 
-        /// 
-        /// Gets all the enabled 2FA providers for the user or member with the specified key.
-        /// 
-        Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey);
+    /// 
+    /// Gets all the enabled 2FA providers for the user or member with the specified key.
+    /// 
+    Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey);
 
-        /// 
-        /// Disables 2FA with Code.
-        /// 
-        Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code);
+    /// 
+    /// Disables 2FA with Code.
+    /// 
+    Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code);
 
-        /// 
-        /// Validates and Saves.
-        /// 
-        Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code);
-    }
+    /// 
+    /// Validates and Saves.
+    /// 
+    Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code);
 }
diff --git a/src/Umbraco.Core/Services/IUpgradeService.cs b/src/Umbraco.Core/Services/IUpgradeService.cs
index 2e0f2a5f17..2f1e65f00a 100644
--- a/src/Umbraco.Core/Services/IUpgradeService.cs
+++ b/src/Umbraco.Core/Services/IUpgradeService.cs
@@ -1,10 +1,8 @@
-using System.Threading.Tasks;
 using Umbraco.Cms.Core.Semver;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IUpgradeService
 {
-    public interface IUpgradeService
-    {
-        Task CheckUpgrade(SemVersion version);
-    }
+    Task CheckUpgrade(SemVersion version);
 }
diff --git a/src/Umbraco.Core/Services/IUsageInformationService.cs b/src/Umbraco.Core/Services/IUsageInformationService.cs
index c6b2c68702..1d4caaa526 100644
--- a/src/Umbraco.Core/Services/IUsageInformationService.cs
+++ b/src/Umbraco.Core/Services/IUsageInformationService.cs
@@ -1,10 +1,8 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IUsageInformationService
 {
-    public interface IUsageInformationService
-    {
-        IEnumerable? GetDetailed();
-    }
+    IEnumerable? GetDetailed();
 }
diff --git a/src/Umbraco.Core/Services/IUserDataService.cs b/src/Umbraco.Core/Services/IUserDataService.cs
index e63ee3f697..0bb1d10cc4 100644
--- a/src/Umbraco.Core/Services/IUserDataService.cs
+++ b/src/Umbraco.Core/Services/IUserDataService.cs
@@ -1,10 +1,8 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IUserDataService
 {
-    public interface IUserDataService
-    {
-        IEnumerable GetUserData();
-    }
+    IEnumerable GetUserData();
 }
diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs
index 9a63fcf0ad..40a3fbd899 100644
--- a/src/Umbraco.Core/Services/IUserService.cs
+++ b/src/Umbraco.Core/Services/IUserService.cs
@@ -1,256 +1,289 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the UserService, which is an easy access to operations involving  and eventually
+///     Users.
+/// 
+public interface IUserService : IMembershipUserService
 {
     /// 
-    /// Defines the UserService, which is an easy access to operations involving  and eventually Users.
+    ///     Creates a database entry for starting a new login session for a user
     /// 
-    public interface IUserService : IMembershipUserService
-    {
-        /// 
-        /// Creates a database entry for starting a new login session for a user
-        /// 
-        /// 
-        /// 
-        /// 
-        Guid CreateLoginSession(int userId, string requestingIpAddress);
+    /// 
+    /// 
+    /// 
+    Guid CreateLoginSession(int userId, string requestingIpAddress);
 
-        /// 
-        /// Validates that a user login session is valid/current and hasn't been closed
-        /// 
-        /// 
-        /// 
-        /// 
-        bool ValidateLoginSession(int userId, Guid sessionId);
+    /// 
+    ///     Validates that a user login session is valid/current and hasn't been closed
+    /// 
+    /// 
+    /// 
+    /// 
+    bool ValidateLoginSession(int userId, Guid sessionId);
 
-        /// 
-        /// Removes the session's validity
-        /// 
-        /// 
-        void ClearLoginSession(Guid sessionId);
+    /// 
+    ///     Removes the session's validity
+    /// 
+    /// 
+    void ClearLoginSession(Guid sessionId);
 
-        /// 
-        /// Removes all valid sessions for the user
-        /// 
-        /// 
-        int ClearLoginSessions(int userId);
+    /// 
+    ///     Removes all valid sessions for the user
+    /// 
+    /// 
+    int ClearLoginSessions(int userId);
 
-        /// 
-        /// This is basically facets of UserStates key = state, value = count
-        /// 
-        IDictionary GetUserStates();
+    /// 
+    ///     This is basically facets of UserStates key = state, value = count
+    /// 
+    IDictionary GetUserStates();
 
-        /// 
-        /// Get paged users
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// A filter to only include user that belong to these user groups
-        /// 
-        /// 
-        /// A filter to only include users that do not belong to these user groups
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection,
-            UserState[]? userState = null,
-            string[]? includeUserGroups = null,
-            string[]? excludeUserGroups = null,
-            IQuery? filter = null);
+    /// 
+    ///     Get paged users
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     A filter to only include user that belong to these user groups
+    /// 
+    /// 
+    ///     A filter to only include users that do not belong to these user groups
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        UserState[]? userState = null,
+        string[]? includeUserGroups = null,
+        string[]? excludeUserGroups = null,
+        IQuery? filter = null);
 
-        /// 
-        /// Get paged users
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// A filter to only include user that belong to these user groups
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection,
-            UserState[]? userState = null,
-            string[]? userGroups = null,
-            string? filter = null);
+    /// 
+    ///     Get paged users
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     A filter to only include user that belong to these user groups
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        UserState[]? userState = null,
+        string[]? userGroups = null,
+        string? filter = null);
 
-        /// 
-        /// Deletes or disables a User
-        /// 
-        ///  to delete
-        /// True to permanently delete the user, False to disable the user
-        void Delete(IUser user, bool deletePermanently);
+    /// 
+    ///     Deletes or disables a User
+    /// 
+    ///  to delete
+    /// True to permanently delete the user, False to disable the user
+    void Delete(IUser user, bool deletePermanently);
 
-        /// 
-        /// Gets an IProfile by User Id.
-        /// 
-        /// Id of the User to retrieve
-        /// 
-        IProfile? GetProfileById(int id);
+    /// 
+    ///     Gets an IProfile by User Id.
+    /// 
+    /// Id of the User to retrieve
+    /// 
+    ///     
+    /// 
+    IProfile? GetProfileById(int id);
 
-        /// 
-        /// Gets a profile by username
-        /// 
-        /// Username
-        /// 
-        IProfile? GetProfileByUserName(string username);
+    /// 
+    ///     Gets a profile by username
+    /// 
+    /// Username
+    /// 
+    ///     
+    /// 
+    IProfile? GetProfileByUserName(string username);
 
-        /// 
-        /// Gets a user by Id
-        /// 
-        /// Id of the user to retrieve
-        /// 
-        IUser? GetUserById(int id);
+    /// 
+    ///     Gets a user by Id
+    /// 
+    /// Id of the user to retrieve
+    /// 
+    ///     
+    /// 
+    IUser? GetUserById(int id);
 
-        /// 
-        /// Gets a users by Id
-        /// 
-        /// Ids of the users to retrieve
-        /// 
-        IEnumerable GetUsersById(params int[]? ids);
+    /// 
+    ///     Gets a users by Id
+    /// 
+    /// Ids of the users to retrieve
+    /// 
+    ///     
+    /// 
+    IEnumerable GetUsersById(params int[]? ids);
 
-        /// 
-        /// Removes a specific section from all user groups
-        /// 
-        /// This is useful when an entire section is removed from config
-        /// Alias of the section to remove
-        void DeleteSectionFromAllUserGroups(string sectionAlias);
+    /// 
+    ///     Removes a specific section from all user groups
+    /// 
+    /// This is useful when an entire section is removed from config
+    /// Alias of the section to remove
+    void DeleteSectionFromAllUserGroups(string sectionAlias);
 
-        /// 
-        /// Get explicitly assigned permissions for a user and optional node ids
-        /// 
-        /// If no permissions are found for a particular entity then the user's default permissions will be applied
-        /// User to retrieve permissions for
-        /// Specifying nothing will return all user permissions for all nodes that have explicit permissions defined
-        /// An enumerable list of 
-        /// 
-        /// This will return the default permissions for the user's groups for node ids that don't have explicitly defined permissions
-        /// 
-        EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds);
+    /// 
+    ///     Get explicitly assigned permissions for a user and optional node ids
+    /// 
+    /// If no permissions are found for a particular entity then the user's default permissions will be applied
+    /// User to retrieve permissions for
+    /// 
+    ///     Specifying nothing will return all user permissions for all nodes that have explicit permissions
+    ///     defined
+    /// 
+    /// An enumerable list of 
+    /// 
+    ///     This will return the default permissions for the user's groups for node ids that don't have explicitly defined
+    ///     permissions
+    /// 
+    EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds);
 
-        /// 
-        /// Get explicitly assigned permissions for groups and optional node Ids
-        /// 
-        /// 
-        /// 
-        ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set
-        /// 
-        /// Specifying nothing will return all permissions for all nodes
-        /// An enumerable list of 
-        EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds);
+    /// 
+    ///     Get explicitly assigned permissions for groups and optional node Ids
+    /// 
+    /// 
+    /// 
+    ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit
+    ///     permissions set
+    /// 
+    /// Specifying nothing will return all permissions for all nodes
+    /// An enumerable list of 
+    EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds);
 
-        /// 
-        /// Gets the implicit/inherited permissions for the user for the given path
-        /// 
-        /// User to check permissions for
-        /// Path to check permissions for
-        EntityPermissionSet GetPermissionsForPath(IUser? user, string? path);
+    /// 
+    ///     Gets the implicit/inherited permissions for the user for the given path
+    /// 
+    /// User to check permissions for
+    /// Path to check permissions for
+    EntityPermissionSet GetPermissionsForPath(IUser? user, string? path);
 
-        /// 
-        /// Gets the permissions for the provided groups and path
-        /// 
-        /// 
-        /// Path to check permissions for
-        /// 
-        ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set
-        /// 
-        EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false);
+    /// 
+    ///     Gets the permissions for the provided groups and path
+    /// 
+    /// 
+    /// Path to check permissions for
+    /// 
+    ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit
+    ///     permissions set
+    /// 
+    EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false);
 
-        /// 
-        /// Replaces the same permission set for a single group to any number of entities
-        /// 
-        /// Id of the group
-        /// 
-        /// Permissions as enumerable list of ,
-        /// if no permissions are specified then all permissions for this node are removed for this group
-        /// 
-        /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed.
-        /// If no 'entityIds' are specified all permissions will be removed for the specified group.
-        void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds);
+    /// 
+    ///     Replaces the same permission set for a single group to any number of entities
+    /// 
+    /// Id of the group
+    /// 
+    ///     Permissions as enumerable list of ,
+    ///     if no permissions are specified then all permissions for this node are removed for this group
+    /// 
+    /// 
+    ///     Specify the nodes to replace permissions for. If nothing is specified all permissions are
+    ///     removed.
+    /// 
+    /// If no 'entityIds' are specified all permissions will be removed for the specified group.
+    void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds);
 
-        /// 
-        /// Assigns the same permission set for a single user group to any number of entities
-        /// 
-        /// Id of the group
-        /// 
-        /// Specify the nodes to replace permissions for
-        void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds);
+    /// 
+    ///     Assigns the same permission set for a single user group to any number of entities
+    /// 
+    /// Id of the group
+    /// 
+    /// Specify the nodes to replace permissions for
+    void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds);
 
-        /// 
-        /// Gets a list of  objects associated with a given group
-        /// 
-        /// Id of group
-        /// 
-        IEnumerable GetAllInGroup(int? groupId);
+    /// 
+    ///     Gets a list of  objects associated with a given group
+    /// 
+    /// Id of group
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAllInGroup(int? groupId);
 
-        /// 
-        /// Gets a list of  objects not associated with a given group
-        /// 
-        /// Id of group
-        /// 
-        IEnumerable GetAllNotInGroup(int groupId);
+    /// 
+    ///     Gets a list of  objects not associated with a given group
+    /// 
+    /// Id of group
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAllNotInGroup(int groupId);
 
-        IEnumerable GetNextUsers(int id, int count);
+    IEnumerable GetNextUsers(int id, int count);
 
-        #region User groups
+    #region User groups
 
-        /// 
-        /// Gets all UserGroups or those specified as parameters
-        /// 
-        /// Optional Ids of UserGroups to retrieve
-        /// An enumerable list of 
-        IEnumerable GetAllUserGroups(params int[] ids);
+    /// 
+    ///     Gets all UserGroups or those specified as parameters
+    /// 
+    /// Optional Ids of UserGroups to retrieve
+    /// An enumerable list of 
+    IEnumerable GetAllUserGroups(params int[] ids);
 
-        /// 
-        /// Gets a UserGroup by its Alias
-        /// 
-        /// Alias of the UserGroup to retrieve
-        /// 
-        IEnumerable GetUserGroupsByAlias(params string[] alias);
+    /// 
+    ///     Gets a UserGroup by its Alias
+    /// 
+    /// Alias of the UserGroup to retrieve
+    /// 
+    ///     
+    /// 
+    IEnumerable GetUserGroupsByAlias(params string[] alias);
 
-        /// 
-        /// Gets a UserGroup by its Alias
-        /// 
-        /// Name of the UserGroup to retrieve
-        /// 
-        IUserGroup? GetUserGroupByAlias(string name);
+    /// 
+    ///     Gets a UserGroup by its Alias
+    /// 
+    /// Name of the UserGroup to retrieve
+    /// 
+    ///     
+    /// 
+    IUserGroup? GetUserGroupByAlias(string name);
 
-        /// 
-        /// Gets a UserGroup by its Id
-        /// 
-        /// Id of the UserGroup to retrieve
-        /// 
-        IUserGroup? GetUserGroupById(int id);
+    /// 
+    ///     Gets a UserGroup by its Id
+    /// 
+    /// Id of the UserGroup to retrieve
+    /// 
+    ///     
+    /// 
+    IUserGroup? GetUserGroupById(int id);
 
-        /// 
-        /// Saves a UserGroup
-        /// 
-        /// UserGroup to save
-        /// 
-        /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in
-        /// than all users will be removed from this group and only these users will be added
-        /// 
-        void Save(IUserGroup userGroup, int[]? userIds = null);
+    /// 
+    ///     Saves a UserGroup
+    /// 
+    /// UserGroup to save
+    /// 
+    ///     If null than no changes are made to the users who are assigned to this group, however if a value is passed in
+    ///     than all users will be removed from this group and only these users will be added
+    /// 
+    void Save(IUserGroup userGroup, int[]? userIds = null);
 
-        /// 
-        /// Deletes a UserGroup
-        /// 
-        /// UserGroup to delete
-        void DeleteUserGroup(IUserGroup userGroup);
+    /// 
+    ///     Deletes a UserGroup
+    /// 
+    /// UserGroup to delete
+    void DeleteUserGroup(IUserGroup userGroup);
 
-        #endregion
-    }
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/IdKeyMap.cs b/src/Umbraco.Core/Services/IdKeyMap.cs
index 00acb7ad04..7aa746ae27 100644
--- a/src/Umbraco.Core/Services/IdKeyMap.cs
+++ b/src/Umbraco.Core/Services/IdKeyMap.cs
@@ -1,79 +1,56 @@
-using System;
 using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Threading;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class IdKeyMap : IIdKeyMap, IDisposable
 {
-    public class IdKeyMap : IIdKeyMap,IDisposable
+    private readonly ICoreScopeProvider _scopeProvider;
+    private readonly IIdKeyMapRepository _idKeyMapRepository;
+    private readonly ReaderWriterLockSlim _locker = new();
+
+    private readonly Dictionary> _id2Key = new();
+    private readonly Dictionary> _key2Id = new();
+
+    // note - for pure read-only we might want to *not* enforce a transaction?
+
+    // notes
+    //
+    // - this class assumes that the id/guid map is unique; that is, if an id and a guid map
+    //   to each other, then the id will never map to another guid, and the guid will never map
+    //   to another id
+    //
+    // - cache is cleared by MediaCacheRefresher, UnpublishedPageCacheRefresher, and other
+    //   refreshers - because id/guid map is unique, we only clear to avoid leaking memory, 'cos
+    //   we don't risk caching obsolete values - and only when actually deleting
+    //
+    // - we do NOT prefetch anything from database
+    //
+    // - NuCache maintains its own id/guid map for content & media items
+    //   it does *not* populate the idk map, because it directly uses its own map
+    //   still, it provides mappers so that the idk map can benefit from them
+    //   which means there will be some double-caching at some point ??
+    //
+    // - when a request comes in:
+    //   if the idkMap already knows about the map, it returns the value
+    //   else it tries the published cache via mappers
+    //   else it hits the database
+    private readonly ConcurrentDictionary id2key, Func key2id)>
+        _dictionary
+            = new();
+
+    public IdKeyMap(ICoreScopeProvider scopeProvider, IIdKeyMapRepository idKeyMapRepository)
     {
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IIdKeyMapRepository _idKeyMapRepository;
-        private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
+        _scopeProvider = scopeProvider;
+        _idKeyMapRepository = idKeyMapRepository;
+    }
 
-        private readonly Dictionary> _id2Key = new Dictionary>();
-        private readonly Dictionary> _key2Id = new Dictionary>();
+    private bool _disposedValue;
 
-        public IdKeyMap(ICoreScopeProvider scopeProvider, IIdKeyMapRepository idKeyMapRepository)
-        {
-            _scopeProvider = scopeProvider;
-            _idKeyMapRepository = idKeyMapRepository;
-        }
-
-        // note - for pure read-only we might want to *not* enforce a transaction?
-
-        // notes
-        //
-        // - this class assumes that the id/guid map is unique; that is, if an id and a guid map
-        //   to each other, then the id will never map to another guid, and the guid will never map
-        //   to another id
-        //
-        // - cache is cleared by MediaCacheRefresher, UnpublishedPageCacheRefresher, and other
-        //   refreshers - because id/guid map is unique, we only clear to avoid leaking memory, 'cos
-        //   we don't risk caching obsolete values - and only when actually deleting
-        //
-        // - we do NOT prefetch anything from database
-        //
-        // - NuCache maintains its own id/guid map for content & media items
-        //   it does *not* populate the idk map, because it directly uses its own map
-        //   still, it provides mappers so that the idk map can benefit from them
-        //   which means there will be some double-caching at some point ??
-        //
-        // - when a request comes in:
-        //   if the idkMap already knows about the map, it returns the value
-        //   else it tries the published cache via mappers
-        //   else it hits the database
-
-        private readonly ConcurrentDictionary id2key, Func key2id)> _dictionary
-            = new ConcurrentDictionary id2key, Func key2id)>();
-        private bool _disposedValue;
-
-        public void SetMapper(UmbracoObjectTypes umbracoObjectType, Func id2key, Func key2id)
-        {
-            _dictionary[umbracoObjectType] = (id2key, key2id);
-        }
-
-        internal void Populate(IEnumerable<(int id, Guid key)> pairs, UmbracoObjectTypes umbracoObjectType)
-        {
-            try
-            {
-                _locker.EnterWriteLock();
-                foreach (var pair in pairs)
-                {
-
-                    _id2Key[pair.id] = new TypedId(pair.key, umbracoObjectType);
-                    _key2Id[pair.key] = new TypedId(pair.id, umbracoObjectType);
-                }
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
-            }
-        }
+    public void SetMapper(UmbracoObjectTypes umbracoObjectType, Func id2key, Func key2id) =>
+        _dictionary[umbracoObjectType] = (id2key, key2id);
 
 #if POPULATE_FROM_DATABASE
         private void PopulateLocked()
@@ -85,7 +62,8 @@ namespace Umbraco.Cms.Core.Services
             {
                 // populate content and media items
                 var types = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media };
-                var values = scope.Database.Query("SELECT id, uniqueId, nodeObjectType FROM umbracoNode WHERE nodeObjectType IN @types", new { types });
+                var values =
+ scope.Database.Query("SELECT id, uniqueId, nodeObjectType FROM umbracoNode WHERE nodeObjectType IN @types", new { types });
                 foreach (var value in values)
                 {
                     var umbracoObjectType = ObjectTypes.GetUmbracoObjectType(value.NodeObjectType);
@@ -135,21 +113,27 @@ namespace Umbraco.Cms.Core.Services
         }
 #endif
 
-        public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType)
-        {
-            bool empty;
+    public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType)
+    {
+        bool empty;
 
-            try
+        try
+        {
+            _locker.EnterReadLock();
+            if (_key2Id.TryGetValue(key, out TypedId id) && id.UmbracoObjectType == umbracoObjectType)
             {
-                _locker.EnterReadLock();
-                if (_key2Id.TryGetValue(key, out var id) && id.UmbracoObjectType == umbracoObjectType) return Attempt.Succeed(id.Id);
-                empty = _key2Id.Count == 0;
+                return Attempt.Succeed(id.Id);
             }
-            finally
+
+            empty = _key2Id.Count == 0;
+        }
+        finally
+        {
+            if (_locker.IsReadLockHeld)
             {
-                if (_locker.IsReadLockHeld)
-                    _locker.ExitReadLock();
+                _locker.ExitReadLock();
             }
+        }
 
 #if POPULATE_FROM_DATABASE
             // if cache is empty and looking for a document or a media,
@@ -158,77 +142,115 @@ namespace Umbraco.Cms.Core.Services
                 return PopulateAndGetIdForKey(key, umbracoObjectType);
 #endif
 
-            // optimize for read speed: reading database outside a lock means that we could read
-            // multiple times, but we don't lock the cache while accessing the database = better
+        // optimize for read speed: reading database outside a lock means that we could read
+        // multiple times, but we don't lock the cache while accessing the database = better
+        int? val = null;
 
-            int? val = null;
-
-            if (_dictionary.TryGetValue(umbracoObjectType, out var mappers))
-                if ((val = mappers.key2id(key)) == default(int)) val = null;
-
-            if (val == null)
+        if (_dictionary.TryGetValue(umbracoObjectType, out (Func id2key, Func key2id) mappers))
+        {
+            if ((val = mappers.key2id(key)) == default(int))
             {
-                using (var scope = _scopeProvider.CreateCoreScope())
-                {
-                    val = _idKeyMapRepository.GetIdForKey(key, umbracoObjectType);
-                    scope.Complete();
-                }
+                val = null;
             }
-
-            if (val == null) return Attempt.Fail();
-
-            // cache reservations, when something is saved this cache is cleared anyways
-            //if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
-            //    Attempt.Succeed(val.Value);
-
-            try
-            {
-                _locker.EnterWriteLock();
-                _id2Key[val.Value] = new TypedId(key, umbracoObjectType);
-                _key2Id[key] = new TypedId(val.Value, umbracoObjectType);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
-            }
-
-            return Attempt.Succeed(val.Value);
         }
 
-        public Attempt GetIdForUdi(Udi udi)
+        if (val == null)
         {
-            var guidUdi = udi as GuidUdi;
-            if (guidUdi == null)
-                return Attempt.Fail();
-
-            var umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(guidUdi.EntityType);
-            return GetIdForKey(guidUdi.Guid, umbracoType);
+            using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+            {
+                val = _idKeyMapRepository.GetIdForKey(key, umbracoObjectType);
+                scope.Complete();
+            }
         }
 
-        public Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType)
+        if (val == null)
         {
-            var keyAttempt = GetKeyForId(id, umbracoObjectType);
-            return keyAttempt.Success
-                ? Attempt.Succeed(new GuidUdi(UdiEntityTypeHelper.FromUmbracoObjectType(umbracoObjectType), keyAttempt.Result))
-                : Attempt.Fail();
+            return Attempt.Fail();
         }
 
-        public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType)
+        // cache reservations, when something is saved this cache is cleared anyways
+        // if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
+        //    Attempt.Succeed(val.Value);
+        try
         {
-            bool empty;
+            _locker.EnterWriteLock();
+            _id2Key[val.Value] = new TypedId(key, umbracoObjectType);
+            _key2Id[key] = new TypedId(val.Value, umbracoObjectType);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
+            {
+                _locker.ExitWriteLock();
+            }
+        }
 
-            try
+        return Attempt.Succeed(val.Value);
+    }
+
+    internal void Populate(IEnumerable<(int id, Guid key)> pairs, UmbracoObjectTypes umbracoObjectType)
+    {
+        try
+        {
+            _locker.EnterWriteLock();
+            foreach ((int id, Guid key) in pairs)
             {
-                _locker.EnterReadLock();
-                if (_id2Key.TryGetValue(id, out var key) && key.UmbracoObjectType == umbracoObjectType) return Attempt.Succeed(key.Id);
-                empty = _id2Key.Count == 0;
+                _id2Key[id] = new TypedId(key, umbracoObjectType);
+                _key2Id[key] = new TypedId(id, umbracoObjectType);
             }
-            finally
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                if (_locker.IsReadLockHeld)
-                    _locker.ExitReadLock();
+                _locker.ExitWriteLock();
             }
+        }
+    }
+
+    public Attempt GetIdForUdi(Udi udi)
+    {
+        var guidUdi = udi as GuidUdi;
+        if (guidUdi == null)
+        {
+            return Attempt.Fail();
+        }
+
+        UmbracoObjectTypes umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(guidUdi.EntityType);
+        return GetIdForKey(guidUdi.Guid, umbracoType);
+    }
+
+    public Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType)
+    {
+        Attempt keyAttempt = GetKeyForId(id, umbracoObjectType);
+        return keyAttempt.Success
+            ? Attempt.Succeed(new GuidUdi(
+                UdiEntityTypeHelper.FromUmbracoObjectType(umbracoObjectType),
+                keyAttempt.Result))
+            : Attempt.Fail();
+    }
+
+    public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType)
+    {
+        bool empty;
+
+        try
+        {
+            _locker.EnterReadLock();
+            if (_id2Key.TryGetValue(id, out TypedId key) && key.UmbracoObjectType == umbracoObjectType)
+            {
+                return Attempt.Succeed(key.Id);
+            }
+
+            empty = _id2Key.Count == 0;
+        }
+        finally
+        {
+            if (_locker.IsReadLockHeld)
+            {
+                _locker.ExitReadLock();
+            }
+        }
 
 #if POPULATE_FROM_DATABASE
             // if cache is empty and looking for a document or a media,
@@ -237,133 +259,156 @@ namespace Umbraco.Cms.Core.Services
                 return PopulateAndGetKeyForId(id, umbracoObjectType);
 #endif
 
-            // optimize for read speed: reading database outside a lock means that we could read
-            // multiple times, but we don't lock the cache while accessing the database = better
+        // optimize for read speed: reading database outside a lock means that we could read
+        // multiple times, but we don't lock the cache while accessing the database = better
+        Guid? val = null;
 
-            Guid? val = null;
-
-            if (_dictionary.TryGetValue(umbracoObjectType, out var mappers))
-                if ((val = mappers.id2key(id)) == default(Guid)) val = null;
-
-            if (val == null)
-            {
-                using (var scope = _scopeProvider.CreateCoreScope())
-                {
-                    val = _idKeyMapRepository.GetIdForKey(id, umbracoObjectType);
-                    scope.Complete();
-                }
-            }
-
-            if (val == null) return Attempt.Fail();
-
-            // cache reservations, when something is saved this cache is cleared anyways
-            //if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
-            //    Attempt.Succeed(val.Value);
-
-            try
-            {
-                _locker.EnterWriteLock();
-                _id2Key[id] = new TypedId(val.Value, umbracoObjectType);
-                _key2Id[val.Value] = new TypedId(id, umbracoObjectType);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
-            }
-
-            return Attempt.Succeed(val.Value);
-        }
-
-        // invoked on UnpublishedPageCacheRefresher.RefreshAll
-        // anything else will use the id-specific overloads
-        public void ClearCache()
+        if (_dictionary.TryGetValue(umbracoObjectType, out (Func id2key, Func key2id) mappers))
         {
-            try
+            if ((val = mappers.id2key(id)) == default(Guid))
             {
-                _locker.EnterWriteLock();
-                _id2Key.Clear();
-                _key2Id.Clear();
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                val = null;
             }
         }
 
-        public void ClearCache(int id)
+        if (val == null)
         {
-            try
+            using (ICoreScope scope = _scopeProvider.CreateCoreScope())
             {
-                _locker.EnterWriteLock();
-                if (_id2Key.TryGetValue(id, out var key) == false) return;
-                _id2Key.Remove(id);
-                _key2Id.Remove(key.Id);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                val = _idKeyMapRepository.GetIdForKey(id, umbracoObjectType);
+                scope.Complete();
             }
         }
 
-        public void ClearCache(Guid key)
+        if (val == null)
         {
-            try
+            return Attempt.Fail();
+        }
+
+        // cache reservations, when something is saved this cache is cleared anyways
+        // if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
+        //    Attempt.Succeed(val.Value);
+        try
+        {
+            _locker.EnterWriteLock();
+            _id2Key[id] = new TypedId(val.Value, umbracoObjectType);
+            _key2Id[val.Value] = new TypedId(id, umbracoObjectType);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                _locker.EnterWriteLock();
-                if (_key2Id.TryGetValue(key, out var id) == false) return;
-                _id2Key.Remove(id.Id);
-                _key2Id.Remove(key);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                _locker.ExitWriteLock();
             }
         }
 
-        // ReSharper disable ClassNeverInstantiated.Local
-        // ReSharper disable UnusedAutoPropertyAccessor.Local
-        private class TypedIdDto
-        {
-            public int Id { get; set; }
-            public Guid UniqueId { get; set; }
-            public Guid NodeObjectType { get; set; }
-        }
-        // ReSharper restore ClassNeverInstantiated.Local
-        // ReSharper restore UnusedAutoPropertyAccessor.Local
+        return Attempt.Succeed(val.Value);
+    }
 
-        private struct TypedId
+    // invoked on UnpublishedPageCacheRefresher.RefreshAll
+    // anything else will use the id-specific overloads
+    public void ClearCache()
+    {
+        try
         {
-            public TypedId(T id, UmbracoObjectTypes umbracoObjectType)
+            _locker.EnterWriteLock();
+            _id2Key.Clear();
+            _key2Id.Clear();
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                UmbracoObjectType = umbracoObjectType;
-                Id = id;
+                _locker.ExitWriteLock();
             }
-
-            public UmbracoObjectTypes UmbracoObjectType { get; }
-
-            public T Id { get; }
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (!_disposedValue)
-            {
-                if (disposing)
-                {
-                    _locker.Dispose();
-                }
-                _disposedValue = true;
-            }
-        }
-
-        public void Dispose()
-        {
-            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
-            Dispose(disposing: true);
         }
     }
+
+    public void ClearCache(int id)
+    {
+        try
+        {
+            _locker.EnterWriteLock();
+            if (_id2Key.TryGetValue(id, out TypedId key) == false)
+            {
+                return;
+            }
+
+            _id2Key.Remove(id);
+            _key2Id.Remove(key.Id);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
+            {
+                _locker.ExitWriteLock();
+            }
+        }
+    }
+
+    public void ClearCache(Guid key)
+    {
+        try
+        {
+            _locker.EnterWriteLock();
+            if (_key2Id.TryGetValue(key, out TypedId id) == false)
+            {
+                return;
+            }
+
+            _id2Key.Remove(id.Id);
+            _key2Id.Remove(key);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
+            {
+                _locker.ExitWriteLock();
+            }
+        }
+    }
+
+    protected virtual void Dispose(bool disposing)
+    {
+        if (!_disposedValue)
+        {
+            if (disposing)
+            {
+                _locker.Dispose();
+            }
+
+            _disposedValue = true;
+        }
+    }
+
+    // ReSharper restore ClassNeverInstantiated.Local
+    // ReSharper restore UnusedAutoPropertyAccessor.Local
+    private struct TypedId
+    {
+        public TypedId(T id, UmbracoObjectTypes umbracoObjectType)
+        {
+            UmbracoObjectType = umbracoObjectType;
+            Id = id;
+        }
+
+        public UmbracoObjectTypes UmbracoObjectType { get; }
+
+        public T Id { get; }
+    }
+
+    // ReSharper disable ClassNeverInstantiated.Local
+    // ReSharper disable UnusedAutoPropertyAccessor.Local
+    private class TypedIdDto
+    {
+        public int Id { get; set; }
+
+        public Guid UniqueId { get; set; }
+
+        public Guid NodeObjectType { get; set; }
+    }
+
+    public void Dispose() =>
+
+        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+        Dispose(true);
 }
diff --git a/src/Umbraco.Core/Services/InstallationService.cs b/src/Umbraco.Core/Services/InstallationService.cs
index eb1632be8a..00bd00aa91 100644
--- a/src/Umbraco.Core/Services/InstallationService.cs
+++ b/src/Umbraco.Core/Services/InstallationService.cs
@@ -1,20 +1,14 @@
-using System.Threading.Tasks;
 using Umbraco.Cms.Core.Persistence.Repositories;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class InstallationService : IInstallationService
 {
-    public class InstallationService : IInstallationService
-    {
-        private readonly IInstallationRepository _installationRepository;
+    private readonly IInstallationRepository _installationRepository;
 
-        public InstallationService(IInstallationRepository installationRepository)
-        {
-            _installationRepository = installationRepository;
-        }
+    public InstallationService(IInstallationRepository installationRepository) =>
+        _installationRepository = installationRepository;
 
-        public async Task LogInstall(InstallLog installLog)
-        {
-            await _installationRepository.SaveInstallLogAsync(installLog);
-        }
-    }
+    public async Task LogInstall(InstallLog installLog) =>
+        await _installationRepository.SaveInstallLogAsync(installLog);
 }
diff --git a/src/Umbraco.Core/Services/KeyValueService.cs b/src/Umbraco.Core/Services/KeyValueService.cs
index 834c0d3116..0a38e3c284 100644
--- a/src/Umbraco.Core/Services/KeyValueService.cs
+++ b/src/Umbraco.Core/Services/KeyValueService.cs
@@ -1,97 +1,91 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+internal class KeyValueService : IKeyValueService
 {
-    internal class KeyValueService : IKeyValueService
+    private readonly IKeyValueRepository _repository;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public KeyValueService(ICoreScopeProvider scopeProvider, IKeyValueRepository repository)
     {
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IKeyValueRepository _repository;
+        _scopeProvider = scopeProvider;
+        _repository = repository;
+    }
 
-        public KeyValueService(ICoreScopeProvider scopeProvider, IKeyValueRepository repository)
+    /// 
+    public string? GetValue(string key)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _scopeProvider = scopeProvider;
-            _repository = repository;
-        }
-
-        /// 
-        public string? GetValue(string key)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _repository.Get(key)?.Value;
-            }
-        }
-
-        /// 
-        public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _repository.FindByKeyPrefix(keyPrefix);
-            }
-        }
-
-        /// 
-        public void SetValue(string key, string value)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Cms.Core.Constants.Locks.KeyValues);
-
-                var keyValue = _repository.Get(key);
-                if (keyValue == null)
-                {
-                    keyValue = new KeyValue
-                    {
-                        Identifier = key,
-                        Value = value,
-                        UpdateDate = DateTime.Now,
-                    };
-                }
-                else
-                {
-                    keyValue.Value = value;
-                    keyValue.UpdateDate = DateTime.Now;
-                }
-
-                _repository.Save(keyValue);
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void SetValue(string key, string originValue, string newValue)
-        {
-            if (!TrySetValue(key, originValue, newValue))
-                throw new InvalidOperationException("Could not set the value.");
-        }
-
-        /// 
-        public bool TrySetValue(string key, string originalValue, string newValue)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Cms.Core.Constants.Locks.KeyValues);
-
-                var keyValue = _repository.Get(key);
-                if (keyValue == null || keyValue.Value != originalValue)
-                {
-                    return false;
-                }
-
-                keyValue.Value = newValue;
-                keyValue.UpdateDate = DateTime.Now;
-                _repository.Save(keyValue);
-
-                scope.Complete();
-            }
-
-            return true;
+            return _repository.Get(key)?.Value;
         }
     }
+
+    /// 
+    public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _repository.FindByKeyPrefix(keyPrefix);
+        }
+    }
+
+    /// 
+    public void SetValue(string key, string value)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.KeyValues);
+
+            IKeyValue? keyValue = _repository.Get(key);
+            if (keyValue == null)
+            {
+                keyValue = new KeyValue { Identifier = key, Value = value, UpdateDate = DateTime.Now };
+            }
+            else
+            {
+                keyValue.Value = value;
+                keyValue.UpdateDate = DateTime.Now;
+            }
+
+            _repository.Save(keyValue);
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void SetValue(string key, string originValue, string newValue)
+    {
+        if (!TrySetValue(key, originValue, newValue))
+        {
+            throw new InvalidOperationException("Could not set the value.");
+        }
+    }
+
+    /// 
+    public bool TrySetValue(string key, string originalValue, string newValue)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.KeyValues);
+
+            IKeyValue? keyValue = _repository.Get(key);
+            if (keyValue == null || keyValue.Value != originalValue)
+            {
+                return false;
+            }
+
+            keyValue.Value = newValue;
+            keyValue.UpdateDate = DateTime.Now;
+            _repository.Save(keyValue);
+
+            scope.Complete();
+        }
+
+        return true;
+    }
 }
diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs
index 262697c935..3046ddafb5 100644
--- a/src/Umbraco.Core/Services/LocalizationService.cs
+++ b/src/Umbraco.Core/Services/LocalizationService.cs
@@ -1,178 +1,193 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
-{
-    /// 
-    /// Represents the Localization Service, which is an easy access to operations involving  and 
-    /// 
-    internal class LocalizationService : RepositoryService, ILocalizationService
-    {
-        private readonly IDictionaryRepository _dictionaryRepository;
-        private readonly ILanguageRepository _languageRepository;
-        private readonly IAuditRepository _auditRepository;
+namespace Umbraco.Cms.Core.Services;
 
-        public LocalizationService(
-            ICoreScopeProvider provider,
-            ILoggerFactory loggerFactory,
-            IEventMessagesFactory eventMessagesFactory,
-            IDictionaryRepository dictionaryRepository,
-            IAuditRepository auditRepository,
-            ILanguageRepository languageRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+/// 
+///     Represents the Localization Service, which is an easy access to operations involving  and
+///     
+/// 
+internal class LocalizationService : RepositoryService, ILocalizationService
+{
+    private readonly IAuditRepository _auditRepository;
+    private readonly IDictionaryRepository _dictionaryRepository;
+    private readonly ILanguageRepository _languageRepository;
+
+    public LocalizationService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDictionaryRepository dictionaryRepository,
+        IAuditRepository auditRepository,
+        ILanguageRepository languageRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
+    {
+        _dictionaryRepository = dictionaryRepository;
+        _auditRepository = auditRepository;
+        _languageRepository = languageRepository;
+    }
+
+    /// 
+    ///     Adds or updates a translation for a dictionary item and language
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This does not save the item, that needs to be done explicitly
+    /// 
+    public void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value)
+    {
+        if (item == null)
         {
-            _dictionaryRepository = dictionaryRepository;
-            _auditRepository = auditRepository;
-            _languageRepository = languageRepository;
+            throw new ArgumentNullException(nameof(item));
         }
 
-        /// 
-        /// Adds or updates a translation for a dictionary item and language
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This does not save the item, that needs to be done explicitly
-        /// 
-        public void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value)
+        if (language == null)
         {
-            if (item == null) throw new ArgumentNullException(nameof(item));
-            if (language == null) throw new ArgumentNullException(nameof(language));
+            throw new ArgumentNullException(nameof(language));
+        }
 
-            var existing = item.Translations?.FirstOrDefault(x => x.Language?.Id == language.Id);
-            if (existing != null)
+        IDictionaryTranslation? existing = item.Translations?.FirstOrDefault(x => x.Language?.Id == language.Id);
+        if (existing != null)
+        {
+            existing.Value = value;
+        }
+        else
+        {
+            if (item.Translations is not null)
             {
-                existing.Value = value;
+                item.Translations = new List(item.Translations)
+                {
+                    new DictionaryTranslation(language, value),
+                };
             }
             else
             {
-                if (item.Translations is not null)
-                {
-                    item.Translations = new List(item.Translations)
-                    {
-                        new DictionaryTranslation(language, value)
-                    };
-                }
-                else
-                {
-                    item.Translations = new List
-                    {
-                        new DictionaryTranslation(language, value)
-                    };
-                }
+                item.Translations = new List { new DictionaryTranslation(language, value) };
             }
         }
+    }
 
-        /// 
-        /// Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null)
+    /// 
+    ///     Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            // validate the parent
+            if (parentId.HasValue && parentId.Value != Guid.Empty)
             {
-                //validate the parent
-
-                if (parentId.HasValue && parentId.Value != Guid.Empty)
+                IDictionaryItem? parent = GetDictionaryItemById(parentId.Value);
+                if (parent == null)
                 {
-                    var parent = GetDictionaryItemById(parentId.Value);
-                    if (parent == null)
-                        throw new ArgumentException($"No parent dictionary item was found with id {parentId.Value}.");
+                    throw new ArgumentException($"No parent dictionary item was found with id {parentId.Value}.");
                 }
+            }
 
-                var item = new DictionaryItem(parentId, key);
+            var item = new DictionaryItem(parentId, key);
 
-                if (defaultValue.IsNullOrWhiteSpace() == false)
-                {
-                    var langs = GetAllLanguages();
-                    var translations = langs.Select(language => new DictionaryTranslation(language, defaultValue!))
-                        .Cast()
-                        .ToList();
+            if (defaultValue.IsNullOrWhiteSpace() == false)
+            {
+                IEnumerable langs = GetAllLanguages();
+                var translations = langs.Select(language => new DictionaryTranslation(language, defaultValue!))
+                    .Cast()
+                    .ToList();
 
-                    item.Translations = translations;
-                }
+                item.Translations = translations;
+            }
 
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new DictionaryItemSavingNotification(item, eventMessages);
-
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return item;
-                }
-                _dictionaryRepository.Save(item);
-
-                // ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-
-                scope.Notifications.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification));
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new DictionaryItemSavingNotification(item, eventMessages);
 
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
                 scope.Complete();
-
                 return item;
             }
-        }
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemById(int id)
+            _dictionaryRepository.Save(item);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+
+            scope.Notifications.Publish(
+                new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification));
+
+            scope.Complete();
+
+            return item;
+        }
+    }
+
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    public IDictionaryItem? GetDictionaryItemById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(id);
-                //ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-                return item;
-            }
-        }
+            IDictionaryItem? item = _dictionaryRepository.Get(id);
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemById(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(id);
-                //ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-                return item;
-            }
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
         }
+    }
 
-        /// 
-        /// Gets a  by its key
-        /// 
-        /// Key of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemByKey(string key)
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    public IDictionaryItem? GetDictionaryItemById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(key);
-                //ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-                return item;
-            }
+            IDictionaryItem? item = _dictionaryRepository.Get(id);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
         }
+    }
+
+    /// 
+    ///     Gets a  by its key
+    /// 
+    /// Key of the 
+    /// 
+    ///     
+    /// 
+    public IDictionaryItem? GetDictionaryItemByKey(string key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IDictionaryItem? item = _dictionaryRepository.Get(key);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
+        }
+    }
 
         /// 
         /// Gets a list of children for a 
@@ -189,26 +204,30 @@ namespace Umbraco.Cms.Core.Services
                 foreach (var item in items)
                     EnsureDictionaryItemLanguageCallback(item);
 
-                return items;
-            }
+            return items;
         }
+    }
 
-        /// 
-        /// Gets a list of descendants for a 
-        /// 
-        /// Id of the parent, null will return all dictionary items
-        /// An enumerable list of  objects
-        public IEnumerable GetDictionaryItemDescendants(Guid? parentId)
+    /// 
+    ///     Gets a list of descendants for a 
+    /// 
+    /// Id of the parent, null will return all dictionary items
+    /// An enumerable list of  objects
+    public IEnumerable GetDictionaryItemDescendants(Guid? parentId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            IDictionaryItem[] items = _dictionaryRepository.GetDictionaryItemDescendants(parentId).ToArray();
+
+            // ensure the lazy Language callback is assigned
+            foreach (IDictionaryItem item in items)
             {
-                var items = _dictionaryRepository.GetDictionaryItemDescendants(parentId).ToArray();
-                //ensure the lazy Language callback is assigned
-                foreach (var item in items)
-                    EnsureDictionaryItemLanguageCallback(item);
-                return items;
+                EnsureDictionaryItemLanguageCallback(item);
             }
+
+            return items;
         }
+    }
 
         /// 
         /// Gets the root/top  objects
@@ -227,269 +246,301 @@ namespace Umbraco.Cms.Core.Services
             }
         }
 
-        /// 
-        /// Checks if a  with given key exists
-        /// 
-        /// Key of the 
-        /// True if a  exists, otherwise false
-        public bool DictionaryItemExists(string key)
+    /// 
+    ///     Checks if a  with given key exists
+    /// 
+    /// Key of the 
+    /// True if a  exists, otherwise false
+    public bool DictionaryItemExists(string key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(key);
-                return item != null;
-            }
+            IDictionaryItem? item = _dictionaryRepository.Get(key);
+            return item != null;
         }
+    }
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the dictionary item
-        public void Save(IDictionaryItem dictionaryItem, int userId = Cms.Core.Constants.Security.SuperUserId)
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the dictionary item
+    public void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _dictionaryRepository.Save(dictionaryItem);
-
-                // ensure the lazy Language callback is assigned
-                // ensure the lazy Language callback is assigned
-
-                EnsureDictionaryItemLanguageCallback(dictionaryItem);
-                scope.Notifications.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
                 scope.Complete();
+                return;
             }
+
+            _dictionaryRepository.Save(dictionaryItem);
+
+            // ensure the lazy Language callback is assigned
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(dictionaryItem);
+            scope.Notifications.Publish(
+                new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
+            scope.Complete();
         }
+    }
 
-        /// 
-        /// Deletes a  object and its related translations
-        /// as well as its children.
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the dictionary item
-        public void Delete(IDictionaryItem dictionaryItem, int userId = Cms.Core.Constants.Security.SuperUserId)
+    /// 
+    ///     Deletes a  object and its related translations
+    ///     as well as its children.
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the dictionary item
+    public void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _dictionaryRepository.Delete(dictionaryItem);
-                scope.Notifications.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification));
-
-                Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
-
                 scope.Complete();
+                return;
             }
+
+            _dictionaryRepository.Delete(dictionaryItem);
+            scope.Notifications.Publish(
+                new DictionaryItemDeletedNotification(dictionaryItem, eventMessages)
+                    .WithStateFrom(deletingNotification));
+
+            Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Gets a  by its id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    public ILanguage? GetLanguageById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _languageRepository.Get(id);
+        }
+    }
+
+    /// 
+    ///     Gets a  by its iso code
+    /// 
+    /// Iso Code of the language (ie. en-US)
+    /// 
+    ///     
+    /// 
+    public ILanguage? GetLanguageByIsoCode(string? isoCode)
+    {
+        if (isoCode is null)
+        {
+            return null;
         }
 
-        /// 
-        /// Gets a  by its id
-        /// 
-        /// Id of the 
-        /// 
-        public ILanguage? GetLanguageById(int id)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.Get(id);
-            }
+            return _languageRepository.GetByIsoCode(isoCode);
         }
+    }
 
-        /// 
-        /// Gets a  by its iso code
-        /// 
-        /// Iso Code of the language (ie. en-US)
-        /// 
-        public ILanguage? GetLanguageByIsoCode(string? isoCode)
+    /// 
+    public int? GetLanguageIdByIsoCode(string isoCode)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (isoCode is null)
-            {
-                return null;
-            }
-            
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetByIsoCode(isoCode);
-            }
+            return _languageRepository.GetIdByIsoCode(isoCode);
         }
+    }
 
-        /// 
-        public int? GetLanguageIdByIsoCode(string isoCode)
+    /// 
+    public string? GetLanguageIsoCodeById(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetIdByIsoCode(isoCode);
-            }
+            return _languageRepository.GetIsoCodeById(id);
         }
+    }
 
-        /// 
-        public string? GetLanguageIsoCodeById(int id)
+    /// 
+    public string GetDefaultLanguageIsoCode()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetIsoCodeById(id);
-            }
+            return _languageRepository.GetDefaultIsoCode();
         }
+    }
 
-        /// 
-        public string GetDefaultLanguageIsoCode()
+    /// 
+    public int? GetDefaultLanguageId()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetDefaultIsoCode();
-            }
+            return _languageRepository.GetDefaultId();
         }
+    }
 
-        /// 
-        public int? GetDefaultLanguageId()
+    /// 
+    ///     Gets all available languages
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetAllLanguages()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetDefaultId();
-            }
+            return _languageRepository.GetMany();
         }
+    }
 
-        /// 
-        /// Gets all available languages
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetAllLanguages()
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the language
+    public void Save(ILanguage language, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetMany();
-            }
-        }
+            // write-lock languages to guard against race conds when dealing with default language
+            scope.WriteLock(Constants.Locks.Languages);
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the language
-        public void Save(ILanguage language, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            // look for cycles - within write-lock
+            if (language.FallbackLanguageId.HasValue)
             {
-                // write-lock languages to guard against race conds when dealing with default language
-                scope.WriteLock(Cms.Core.Constants.Locks.Languages);
-
-                // look for cycles - within write-lock
-                if (language.FallbackLanguageId.HasValue)
+                var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x);
+                if (!languages.ContainsKey(language.FallbackLanguageId.Value))
                 {
-                    var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x);
-                    if (!languages.ContainsKey(language.FallbackLanguageId.Value))
-                        throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id.");
-                    if (CreatesCycle(language, languages))
-                        throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle.");
+                    throw new InvalidOperationException(
+                        $"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id.");
                 }
 
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new LanguageSavingNotification(language, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
+                if (CreatesCycle(language, languages))
                 {
-                    scope.Complete();
-                    return;
+                    throw new InvalidOperationException(
+                        $"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle.");
                 }
+            }
 
-                _languageRepository.Save(language);
-                scope.Notifications.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language));
-
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new LanguageSavingNotification(language, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
                 scope.Complete();
+                return;
             }
+
+            _languageRepository.Save(language);
+            scope.Notifications.Publish(
+                new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, "Save Language", userId, language.Id, UmbracoObjectTypes.Language.GetName());
+
+            scope.Complete();
         }
+    }
 
-        private bool CreatesCycle(ILanguage language, IDictionary languages)
+    /// 
+    ///     Deletes a  by removing it (but not its usages) from the db
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the language
+    public void Delete(ILanguage language, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            // a new language is not referenced yet, so cannot be part of a cycle
-            if (!language.HasIdentity) return false;
+            // write-lock languages to guard against race conds when dealing with default language
+            scope.WriteLock(Constants.Locks.Languages);
 
-            var id = language.FallbackLanguageId;
-            while (true) // assuming languages does not already contains a cycle, this must end
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingLanguageNotification))
             {
-                if (!id.HasValue) return false; // no fallback means no cycle
-                if (id.Value == language.Id) return true; // back to language = cycle!
-                id = languages[id.Value].FallbackLanguageId; // else keep chaining
-            }
-        }
-
-        /// 
-        /// Deletes a  by removing it (but not its usages) from the db
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the language
-        public void Delete(ILanguage language, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                // write-lock languages to guard against race conds when dealing with default language
-                scope.WriteLock(Cms.Core.Constants.Locks.Languages);
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingLanguageNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted
-                _languageRepository.Delete(language);
-
-                scope.Notifications.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification));
-
-                Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language));
                 scope.Complete();
+                return;
             }
+
+            // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted
+            _languageRepository.Delete(language);
+
+            scope.Notifications.Publish(
+                new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification));
+
+            Audit(AuditType.Delete, "Delete Language", userId, language.Id, UmbracoObjectTypes.Language.GetName());
+            scope.Complete();
+        }
+    }
+
+    public Dictionary GetDictionaryItemKeyMap()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _dictionaryRepository.GetDictionaryItemKeyMap();
+        }
+    }
+
+    private bool CreatesCycle(ILanguage language, IDictionary languages)
+    {
+        // a new language is not referenced yet, so cannot be part of a cycle
+        if (!language.HasIdentity)
+        {
+            return false;
         }
 
-        private void Audit(AuditType type, string message, int userId, int objectId, string? entityType)
-        {
-            _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message));
-        }
+        var id = language.FallbackLanguageId;
 
-        /// 
-        /// This is here to take care of a hack - the DictionaryTranslation model contains an ILanguage reference which we don't want but
-        /// we cannot remove it because it would be a large breaking change, so we need to make sure it's resolved lazily. This is because
-        /// if developers have a lot of dictionary items and translations, the caching and cloning size gets much larger because of
-        /// the large object graphs. So now we don't cache or clone the attached ILanguage
-        /// 
-        private void EnsureDictionaryItemLanguageCallback(IDictionaryItem? d)
+        // assuming languages does not already contains a cycle, this must end
+        while (true)
         {
-            var item = d as DictionaryItem;
-            if (item == null) return;
-
-            item.GetLanguage = GetLanguageById;
-            var translations = item.Translations?.OfType();
-            if (translations is not null)
+            if (!id.HasValue)
             {
-                foreach (var trans in translations)
-                    trans.GetLanguage = GetLanguageById;
+                return false; // no fallback means no cycle
             }
+
+            if (id.Value == language.Id)
+            {
+                return true; // back to language = cycle!
+            }
+
+            id = languages[id.Value].FallbackLanguageId; // else keep chaining
+        }
+    }
+
+    private void Audit(AuditType type, string message, int userId, int objectId, string? entityType) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message));
+
+    /// 
+    ///     This is here to take care of a hack - the DictionaryTranslation model contains an ILanguage reference which we
+    ///     don't want but
+    ///     we cannot remove it because it would be a large breaking change, so we need to make sure it's resolved lazily. This
+    ///     is because
+    ///     if developers have a lot of dictionary items and translations, the caching and cloning size gets much larger
+    ///     because of
+    ///     the large object graphs. So now we don't cache or clone the attached ILanguage
+    /// 
+    private void EnsureDictionaryItemLanguageCallback(IDictionaryItem? d)
+    {
+        if (d is not DictionaryItem item)
+        {
+            return;
         }
 
-        public Dictionary GetDictionaryItemKeyMap()
+        item.GetLanguage = GetLanguageById;
+        IEnumerable? translations = item.Translations?.OfType();
+        if (translations is not null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            foreach (DictionaryTranslation trans in translations)
             {
-                return _dictionaryRepository.GetDictionaryItemKeyMap();
+                trans.GetLanguage = GetLanguageById;
             }
         }
     }
diff --git a/src/Umbraco.Core/Services/LocalizedTextService.cs b/src/Umbraco.Core/Services/LocalizedTextService.cs
index f02b5771a0..1634f60baa 100644
--- a/src/Umbraco.Core/Services/LocalizedTextService.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextService.cs
@@ -1,113 +1,68 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Xml.Linq;
 using System.Xml.XPath;
 using Microsoft.Extensions.Logging;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+public class LocalizedTextService : ILocalizedTextService
 {
-    /// 
-    public class LocalizedTextService : ILocalizedTextService
+    private readonly Lazy>>>>
+        _dictionarySourceLazy;
+
+    private readonly Lazy? _fileSources;
+    private readonly ILogger _logger;
+
+    private readonly Lazy>>> _noAreaDictionarySourceLazy;
+
+    /// 
+    ///     Initializes with a file sources instance
+    /// 
+    /// 
+    /// 
+    public LocalizedTextService(
+        Lazy fileSources,
+        ILogger logger)
     {
-        private readonly ILogger _logger;
-        private readonly Lazy? _fileSources;
-
-        private IDictionary>>> _dictionarySource =>
-            _dictionarySourceLazy.Value;
-
-        private IDictionary>> _noAreaDictionarySource =>
-            _noAreaDictionarySourceLazy.Value;
-
-        private readonly Lazy>>>>
-            _dictionarySourceLazy;
-
-        private readonly Lazy>>> _noAreaDictionarySourceLazy;
-
-        /// 
-        /// Initializes with a file sources instance
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(Lazy fileSources,
-            ILogger logger)
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        if (fileSources == null)
         {
-            if (logger == null) throw new ArgumentNullException(nameof(logger));
-            _logger = logger;
-            if (fileSources == null) throw new ArgumentNullException(nameof(fileSources));
-            _dictionarySourceLazy =
-                new Lazy>>>>(() =>
-                    FileSourcesToAreaDictionarySources(fileSources.Value));
-            _noAreaDictionarySourceLazy =
-                new Lazy>>>(() =>
-                    FileSourcesToNoAreaDictionarySources(fileSources.Value));
-            _fileSources = fileSources;
+            throw new ArgumentNullException(nameof(fileSources));
         }
 
-        private IDictionary>> FileSourcesToNoAreaDictionarySources(
-            LocalizedTextServiceFileSources fileSources)
-        {
-            var xmlSources = fileSources.GetXmlSources();
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                FileSourcesToAreaDictionarySources(fileSources.Value));
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() =>
+                FileSourcesToNoAreaDictionarySources(fileSources.Value));
+        _fileSources = fileSources;
+    }
 
-            return XmlSourceToNoAreaDictionary(xmlSources);
+    /// 
+    ///     Initializes with an XML source
+    /// 
+    /// 
+    /// 
+    public LocalizedTextService(
+        IDictionary> source,
+        ILogger logger)
+    {
+        if (source == null)
+        {
+            throw new ArgumentNullException(nameof(source));
         }
 
-        private IDictionary>> XmlSourceToNoAreaDictionary(
-            IDictionary> xmlSources)
-        {
-            var cultureNoAreaDictionary = new Dictionary>>();
-            foreach (var xmlSource in xmlSources)
-            {
-                var noAreaAliasValue =
-                    new Lazy>(() => GetNoAreaStoredTranslations(xmlSources, xmlSource.Key));
-                cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue);
-            }
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 
-            return cultureNoAreaDictionary;
-        }
-
-        private IDictionary>>>
-            FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
-        {
-            var xmlSources = fileSources.GetXmlSources();
-            return XmlSourcesToAreaDictionary(xmlSources);
-        }
-
-        private IDictionary>>>
-            XmlSourcesToAreaDictionary(IDictionary> xmlSources)
-        {
-            var cultureDictionary =
-                new Dictionary>>>();
-            foreach (var xmlSource in xmlSources)
-            {
-                var areaAliaValue =
-                    new Lazy>>(() =>
-                        GetAreaStoredTranslations(xmlSources, xmlSource.Key));
-                cultureDictionary.Add(xmlSource.Key, areaAliaValue);
-            }
-
-            return cultureDictionary;
-        }
-
-        /// 
-        /// Initializes with an XML source
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(IDictionary> source,
-            ILogger logger)
-        {
-            if (source == null) throw new ArgumentNullException(nameof(source));
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-
-            _dictionarySourceLazy =
-                new Lazy>>>>(() =>
-                    XmlSourcesToAreaDictionary(source));
-            _noAreaDictionarySourceLazy =
-                new Lazy>>>(() =>
-                    XmlSourceToNoAreaDictionary(source));
-        }
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                XmlSourcesToAreaDictionary(source));
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() =>
+                XmlSourceToNoAreaDictionary(source));
+    }
 
         /// 
         /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
@@ -118,346 +73,437 @@ namespace Umbraco.Cms.Core.Services
             IDictionary>>> source,
             ILogger logger)
         {
-            var dictionarySource = source ?? throw new ArgumentNullException(nameof(source));
-            _dictionarySourceLazy =
-                new Lazy>>>>(() =>
-                    dictionarySource);
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            var cultureNoAreaDictionary = new Dictionary>>();
-            foreach (var cultureDictionary in dictionarySource)
-            {
-                var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key);
+            IDictionary>>> dictionarySource =
+            source ?? throw new ArgumentNullException(nameof(source));
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                dictionarySource);
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        var cultureNoAreaDictionary = new Dictionary>>();
+        foreach (KeyValuePair>>> cultureDictionary in
+                 dictionarySource)
+        {
+            Dictionary> areaAliaValue =
+                GetAreaStoredTranslations(source, cultureDictionary.Key);
 
-                cultureNoAreaDictionary.Add(cultureDictionary.Key,
-                    new Lazy>(() => GetAliasValues(areaAliaValue)));
-            }
-
-            _noAreaDictionarySourceLazy =
-                new Lazy>>>(() => cultureNoAreaDictionary);
+            cultureNoAreaDictionary.Add(
+                cultureDictionary.Key,
+                new Lazy>(() => GetAliasValues(areaAliaValue)));
         }
 
-        private static Dictionary GetAliasValues(
-            Dictionary> areaAliaValue)
-        {
-            var aliasValue = new Dictionary();
-            foreach (var area in areaAliaValue)
-            {
-                foreach (var alias in area.Value)
-                {
-                    if (!aliasValue.ContainsKey(alias.Key))
-                    {
-                        aliasValue.Add(alias.Key, alias.Value);
-                    }
-                }
-            }
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() => cultureNoAreaDictionary);
+    }
 
-            return aliasValue;
+    private IDictionary>>> DictionarySource =>
+        _dictionarySourceLazy.Value;
+
+    private IDictionary>> NoAreaDictionarySource =>
+        _noAreaDictionarySourceLazy.Value;
+
+    public string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null)
+    {
+        if (culture == null)
+        {
+            throw new ArgumentNullException(nameof(culture));
         }
 
-        public string Localize(string key, CultureInfo culture, IDictionary? tokens = null)
+        // This is what the legacy ui service did
+        if (string.IsNullOrEmpty(alias))
         {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
-
-            //This is what the legacy ui service did
-            if (string.IsNullOrEmpty(key))
-                return string.Empty;
-
-            var keyParts = key.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
-            var area = keyParts.Length > 1 ? keyParts[0] : null;
-            var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
-            return Localize(area, alias, culture, tokens);
+            return string.Empty;
         }
 
-        public string Localize(string? area, string? alias, CultureInfo? culture,
-            IDictionary? tokens = null)
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+        return GetFromDictionarySource(culture, area, alias, tokens);
+    }
+
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    public IDictionary GetAllStoredValues(CultureInfo culture)
+    {
+        if (culture == null)
         {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
-
-            //This is what the legacy ui service did
-            if (string.IsNullOrEmpty(alias))
-                return string.Empty;
-
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
-
-            return GetFromDictionarySource(culture, area, alias, tokens);
+            throw new ArgumentNullException(nameof(culture));
         }
 
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        public IDictionary GetAllStoredValues(CultureInfo culture)
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+        if (DictionarySource.ContainsKey(culture) == false)
         {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
-
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
-
-            if (_dictionarySource.ContainsKey(culture) == false)
-            {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return new Dictionary(0);
-            }
-
-            IDictionary result = new Dictionary();
-            //convert all areas + keys to a single key with a '/'
-            foreach (var area in _dictionarySource[culture].Value)
-            {
-                foreach (var key in area.Value)
-                {
-                    var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
-                    //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
-                    if (result.ContainsKey(dictionaryKey) == false)
-                    {
-                        result.Add(dictionaryKey, key.Value);
-                    }
-                }
-            }
-
-            return result;
+            _logger.LogWarning(
+                "The culture specified {Culture} was not found in any configured sources for this service",
+                culture);
+            return new Dictionary(0);
         }
 
-        private IDictionary> GetAreaStoredTranslations(
-            IDictionary> xmlSource, CultureInfo cult)
+        IDictionary result = new Dictionary();
+
+        // convert all areas + keys to a single key with a '/'
+        foreach (KeyValuePair> area in DictionarySource[culture].Value)
         {
-            var overallResult = new Dictionary>(StringComparer.InvariantCulture);
-            var areas = xmlSource[cult].Value.XPathSelectElements("//area");
-            foreach (var area in areas)
+            foreach (KeyValuePair key in area.Value)
             {
-                var result = new Dictionary(StringComparer.InvariantCulture);
-                var keys = area.XPathSelectElements("./key");
-                foreach (var key in keys)
-                {
-                    var dictionaryKey =
-                        (string)key.Attribute("alias")!;
-                    //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                    if (result.ContainsKey(dictionaryKey) == false)
-                        result.Add(dictionaryKey, key.Value);
-                }
+                var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
 
-                overallResult.Add(area.Attribute("alias")!.Value, result);
-            }
-
-            //Merge English Dictionary
-            var englishCulture = new CultureInfo("en-US");
-            if (!cult.Equals(englishCulture))
-            {
-                var enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area");
-                foreach (var area in enUS)
-                {
-                    IDictionary
-                        result = new Dictionary(StringComparer.InvariantCulture);
-                    if (overallResult.ContainsKey(area.Attribute("alias")!.Value))
-                    {
-                        result = overallResult[area.Attribute("alias")!.Value];
-                    }
-
-                    var keys = area.XPathSelectElements("./key");
-                    foreach (var key in keys)
-                    {
-                        var dictionaryKey =
-                            (string)key.Attribute("alias")!;
-                        //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                        if (result.ContainsKey(dictionaryKey) == false)
-                            result.Add(dictionaryKey, key.Value);
-                    }
-
-                    if (!overallResult.ContainsKey(area.Attribute("alias")!.Value))
-                    {
-                        overallResult.Add(area.Attribute("alias")!.Value, result);
-                    }
-                }
-            }
-
-            return overallResult;
-        }
-
-        private Dictionary GetNoAreaStoredTranslations(
-            IDictionary> xmlSource, CultureInfo cult)
-        {
-            var result = new Dictionary(StringComparer.InvariantCulture);
-            var keys = xmlSource[cult].Value.XPathSelectElements("//key");
-
-            foreach (var key in keys)
-            {
-                var dictionaryKey =
-                    (string)key.Attribute("alias")!;
-                //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                // i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
                 if (result.ContainsKey(dictionaryKey) == false)
+                {
                     result.Add(dictionaryKey, key.Value);
-            }
-
-            //Merge English Dictionary
-            var englishCulture = new CultureInfo("en-US");
-            if (!cult.Equals(englishCulture))
-            {
-                var keysEn = xmlSource[englishCulture].Value.XPathSelectElements("//key");
-
-                foreach (var key in keys)
-                {
-                    var dictionaryKey =
-                        (string)key.Attribute("alias")!;
-                    //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                    if (result.ContainsKey(dictionaryKey) == false)
-                        result.Add(dictionaryKey, key.Value);
                 }
             }
-
-            return result;
         }
 
-        private Dictionary> GetAreaStoredTranslations(
-            IDictionary>>> dictionarySource,
-            CultureInfo cult)
+        return result;
+    }
+
+    /// 
+    ///     Returns a list of all currently supported cultures
+    /// 
+    /// 
+    public IEnumerable GetSupportedCultures() => DictionarySource.Keys;
+
+    /// 
+    ///     Tries to resolve a full 4 letter culture from a 2 letter culture name
+    /// 
+    /// 
+    ///     The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be
+    ///     returned
+    /// 
+    /// 
+    /// 
+    ///     TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since
+    ///     that
+    ///     is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this
+    ///     attempts
+    ///     to resolve the full culture if possible.
+    ///     This only works when this service is constructed with the LocalizedTextServiceFileSources
+    /// 
+    public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture)
+    {
+        if (currentCulture == null)
         {
-            var overallResult = new Dictionary>(StringComparer.InvariantCulture);
-            var areaDict = dictionarySource[cult];
-
-            foreach (var area in areaDict.Value)
-            {
-                var result = new Dictionary(StringComparer.InvariantCulture);
-                var keys = area.Value.Keys;
-                foreach (var key in keys)
-                {
-                    //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                    if (result.ContainsKey(key) == false)
-                        result.Add(key, area.Value[key]);
-                }
-
-                overallResult.Add(area.Key, result);
-            }
-
-            return overallResult;
+            throw new ArgumentNullException("currentCulture");
         }
 
-        /// 
-        /// Returns a list of all currently supported cultures
-        /// 
-        /// 
-        public IEnumerable GetSupportedCultures()
+        if (_fileSources == null)
         {
-            return _dictionarySource.Keys;
+            return currentCulture;
         }
 
-        /// 
-        /// Tries to resolve a full 4 letter culture from a 2 letter culture name
-        /// 
-        /// 
-        /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned
-        /// 
-        /// 
-        /// 
-        /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that
-        /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts
-        /// to resolve the full culture if possible.
-        ///
-        /// This only works when this service is constructed with the LocalizedTextServiceFileSources
-        /// 
-        public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture)
+        if (currentCulture.Name.Length > 2)
         {
-            if (currentCulture == null) throw new ArgumentNullException("currentCulture");
-
-            if (_fileSources == null) return currentCulture;
-            if (currentCulture.Name.Length > 2) return currentCulture;
-
-            var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
-            return attempt.Success ? attempt.Result! : currentCulture;
+            return currentCulture;
         }
 
-        private string GetFromDictionarySource(CultureInfo culture, string? area, string key,
-            IDictionary? tokens)
+        Attempt attempt =
+            _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
+        return attempt.Success ? attempt.Result! : currentCulture;
+    }
+
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    /// 
+    public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
+    {
+        if (culture == null)
         {
-            if (_dictionarySource.ContainsKey(culture) == false)
-            {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return "[" + key + "]";
-            }
-
-
-            string? found = null;
-            if (string.IsNullOrWhiteSpace(area))
-            {
-                _noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
-            }
-            else
-            {
-                if (_dictionarySource[culture].Value.TryGetValue(area, out var areaDictionary))
-                {
-                    areaDictionary.TryGetValue(key, out found);
-                }
-
-                if (found == null)
-                {
-                    _noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
-                }
-            }
-
-
-            if (found != null)
-            {
-                return ParseTokens(found, tokens);
-            }
-
-            //NOTE: Based on how legacy works, the default text does not contain the area, just the key
-            return "[" + key + "]";
+            throw new ArgumentNullException("culture");
         }
 
-        /// 
-        /// Parses the tokens in the value
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a % symbol.
-        /// For example: hello %0%, you are %1% !
-        ///
-        /// Since we're going to continue using the same language files for now, the token system needs to remain the same. With our new service
-        /// we support a dictionary which means in the future we can really have any sort of token system.
-        /// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case.
-        /// 
-        internal static string ParseTokens(string value, IDictionary? tokens)
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+        if (DictionarySource.ContainsKey(culture) == false)
         {
-            if (tokens == null || tokens.Any() == false)
-            {
-                return value;
-            }
+            _logger.LogWarning(
+                "The culture specified {Culture} was not found in any configured sources for this service",
+                culture);
+            return new Dictionary>(0);
+        }
 
-            foreach (var token in tokens)
-            {
-                value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
-            }
+        return DictionarySource[culture].Value;
+    }
 
+    public string Localize(string key, CultureInfo culture, IDictionary? tokens = null)
+    {
+        if (culture == null)
+        {
+            throw new ArgumentNullException(nameof(culture));
+        }
+
+        // This is what the legacy ui service did
+        if (string.IsNullOrEmpty(key))
+        {
+            return string.Empty;
+        }
+
+        var keyParts = key.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
+        var area = keyParts.Length > 1 ? keyParts[0] : null;
+        var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
+        return Localize(area, alias, culture, tokens);
+    }
+
+    /// 
+    ///     Parses the tokens in the value
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a %
+    ///     symbol.
+    ///     For example: hello %0%, you are %1% !
+    ///     Since we're going to continue using the same language files for now, the token system needs to remain the same.
+    ///     With our new service
+    ///     we support a dictionary which means in the future we can really have any sort of token system.
+    ///     Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw
+    ///     exceptions if that is not the case.
+    /// 
+    internal static string ParseTokens(string value, IDictionary? tokens)
+    {
+        if (tokens == null || tokens.Any() == false)
+        {
             return value;
         }
 
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
+        foreach (KeyValuePair token in tokens)
         {
-            if (culture == null)
-            {
-                throw new ArgumentNullException("culture");
-            }
-
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
-
-            if (_dictionarySource.ContainsKey(culture) == false)
-            {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return new Dictionary>(0);
-            }
-
-            return _dictionarySource[culture].Value;
+            value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
         }
+
+        return value;
+    }
+
+    private static Dictionary GetAliasValues(
+        Dictionary> areaAliaValue)
+    {
+        var aliasValue = new Dictionary();
+        foreach (KeyValuePair> area in areaAliaValue)
+        {
+            foreach (KeyValuePair alias in area.Value)
+            {
+                if (!aliasValue.ContainsKey(alias.Key))
+                {
+                    aliasValue.Add(alias.Key, alias.Value);
+                }
+            }
+        }
+
+        return aliasValue;
+    }
+
+    private IDictionary>> FileSourcesToNoAreaDictionarySources(
+        LocalizedTextServiceFileSources fileSources)
+    {
+        IDictionary> xmlSources = fileSources.GetXmlSources();
+
+        return XmlSourceToNoAreaDictionary(xmlSources);
+    }
+
+    private IDictionary>> XmlSourceToNoAreaDictionary(
+        IDictionary> xmlSources)
+    {
+        var cultureNoAreaDictionary = new Dictionary>>();
+        foreach (KeyValuePair> xmlSource in xmlSources)
+        {
+            var noAreaAliasValue =
+                new Lazy>(() => GetNoAreaStoredTranslations(xmlSources, xmlSource.Key));
+            cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue);
+        }
+
+        return cultureNoAreaDictionary;
+    }
+
+    private IDictionary>>>
+        FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
+    {
+        IDictionary> xmlSources = fileSources.GetXmlSources();
+        return XmlSourcesToAreaDictionary(xmlSources);
+    }
+
+    private IDictionary>>>
+        XmlSourcesToAreaDictionary(IDictionary> xmlSources)
+    {
+        var cultureDictionary =
+            new Dictionary>>>();
+        foreach (KeyValuePair> xmlSource in xmlSources)
+        {
+            var areaAliaValue =
+                new Lazy>>(() =>
+                    GetAreaStoredTranslations(xmlSources, xmlSource.Key));
+            cultureDictionary.Add(xmlSource.Key, areaAliaValue);
+        }
+
+        return cultureDictionary;
+    }
+
+    private IDictionary> GetAreaStoredTranslations(
+        IDictionary> xmlSource, CultureInfo cult)
+    {
+        var overallResult = new Dictionary>(StringComparer.InvariantCulture);
+        IEnumerable areas = xmlSource[cult].Value.XPathSelectElements("//area");
+        foreach (XElement area in areas)
+        {
+            var result = new Dictionary(StringComparer.InvariantCulture);
+            IEnumerable keys = area.XPathSelectElements("./key");
+            foreach (XElement key in keys)
+            {
+                var dictionaryKey =
+                    (string)key.Attribute("alias")!;
+
+                // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                if (result.ContainsKey(dictionaryKey) == false)
+                {
+                    result.Add(dictionaryKey, key.Value);
+                }
+            }
+
+            overallResult.Add(area.Attribute("alias")!.Value, result);
+        }
+
+        // Merge English Dictionary
+        var englishCulture = new CultureInfo("en-US");
+        if (!cult.Equals(englishCulture))
+        {
+            IEnumerable enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area");
+            foreach (XElement area in enUS)
+            {
+                IDictionary
+                    result = new Dictionary(StringComparer.InvariantCulture);
+                if (overallResult.ContainsKey(area.Attribute("alias")!.Value))
+                {
+                    result = overallResult[area.Attribute("alias")!.Value];
+                }
+
+                IEnumerable keys = area.XPathSelectElements("./key");
+                foreach (XElement key in keys)
+                {
+                    var dictionaryKey =
+                        (string)key.Attribute("alias")!;
+
+                    // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                    if (result.ContainsKey(dictionaryKey) == false)
+                    {
+                        result.Add(dictionaryKey, key.Value);
+                    }
+                }
+
+                if (!overallResult.ContainsKey(area.Attribute("alias")!.Value))
+                {
+                    overallResult.Add(area.Attribute("alias")!.Value, result);
+                }
+            }
+        }
+
+        return overallResult;
+    }
+
+    private Dictionary GetNoAreaStoredTranslations(
+        IDictionary> xmlSource, CultureInfo cult)
+    {
+        var result = new Dictionary(StringComparer.InvariantCulture);
+        IEnumerable keys = xmlSource[cult].Value.XPathSelectElements("//key");
+
+        foreach (XElement key in keys)
+        {
+            var dictionaryKey =
+                (string)key.Attribute("alias")!;
+
+            // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+            if (result.ContainsKey(dictionaryKey) == false)
+            {
+                result.Add(dictionaryKey, key.Value);
+            }
+        }
+
+        // Merge English Dictionary
+        var englishCulture = new CultureInfo("en-US");
+        if (!cult.Equals(englishCulture))
+        {
+            IEnumerable keysEn = xmlSource[englishCulture].Value.XPathSelectElements("//key");
+
+            foreach (XElement key in keys)
+            {
+                var dictionaryKey =
+                    (string)key.Attribute("alias")!;
+
+                // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                if (result.ContainsKey(dictionaryKey) == false)
+                {
+                    result.Add(dictionaryKey, key.Value);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private Dictionary> GetAreaStoredTranslations(
+        IDictionary>>> dictionarySource,
+        CultureInfo cult)
+    {
+        var overallResult = new Dictionary>(StringComparer.InvariantCulture);
+        Lazy>> areaDict = dictionarySource[cult];
+
+        foreach (KeyValuePair> area in areaDict.Value)
+        {
+            var result = new Dictionary(StringComparer.InvariantCulture);
+            ICollection keys = area.Value.Keys;
+            foreach (var key in keys)
+            {
+                // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                if (result.ContainsKey(key) == false)
+                {
+                    result.Add(key, area.Value[key]);
+                }
+            }
+
+            overallResult.Add(area.Key, result);
+        }
+
+        return overallResult;
+    }
+
+    private string GetFromDictionarySource(CultureInfo culture, string? area, string key, IDictionary? tokens)
+    {
+        if (DictionarySource.ContainsKey(culture) == false)
+        {
+            _logger.LogWarning(
+                "The culture specified {Culture} was not found in any configured sources for this service",
+                culture);
+            return "[" + key + "]";
+        }
+
+        string? found = null;
+        if (string.IsNullOrWhiteSpace(area))
+        {
+            NoAreaDictionarySource[culture].Value.TryGetValue(key, out found);
+        }
+        else
+        {
+            if (DictionarySource[culture].Value.TryGetValue(area, out IDictionary? areaDictionary))
+            {
+                areaDictionary.TryGetValue(key, out found);
+            }
+
+            if (found == null)
+            {
+                NoAreaDictionarySource[culture].Value.TryGetValue(key, out found);
+            }
+        }
+
+        if (found != null)
+        {
+            return ParseTokens(found, tokens);
+        }
+
+        // NOTE: Based on how legacy works, the default text does not contain the area, just the key
+        return "[" + key + "]";
     }
 }
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
index 8a9559f7bc..f8b44759a0 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
@@ -1,91 +1,103 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
-using System.Threading;
 using Umbraco.Cms.Core.Dictionary;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+/// 
+///     Extension methods for ILocalizedTextService
+/// 
+public static class LocalizedTextServiceExtensions
 {
+    public static string Localize(this ILocalizedTextService manager, string area, T key)
+        where T : Enum =>
+        manager.Localize(area, key.ToString(), Thread.CurrentThread.CurrentUICulture);
+
+    public static string Localize(this ILocalizedTextService manager, string? area, string? alias)
+        => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture);
+
     /// 
-    /// Extension methods for ILocalizedTextService
+    ///     Localize using the current thread culture
     /// 
-    public static class LocalizedTextServiceExtensions
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public static string Localize(this ILocalizedTextService manager, string? area, string alias, string?[]? tokens)
+        => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens));
+
+    /// 
+    ///     Localize a key without any variables
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string?[] tokens)
+        => manager.Localize(area, alias, culture, ConvertToDictionaryVars(tokens));
+
+    public static string? UmbracoDictionaryTranslate(
+        this ILocalizedTextService manager,
+        ICultureDictionary cultureDictionary,
+        string? text)
     {
-         public static string Localize(this ILocalizedTextService manager, string area, T key)
-         where T: System.Enum =>
-             manager.Localize(area, key.ToString(), Thread.CurrentThread.CurrentUICulture);
+        if (text == null)
+        {
+            return null;
+        }
 
-        public static string Localize(this ILocalizedTextService manager, string? area, string? alias)
-            => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture);
+        if (text.StartsWith("#") == false)
+        {
+            return text;
+        }
 
-        /// 
-        /// Localize using the current thread culture
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public static string Localize(this ILocalizedTextService manager, string? area, string alias, string?[]? tokens)
-                    => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens));
+        text = text.Substring(1);
+        var value = cultureDictionary[text];
+        if (value.IsNullOrWhiteSpace() == false)
+        {
+            return value;
+        }
 
-        /// 
-        /// Localize a key without any variables
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string?[] tokens)
-            => manager.Localize(area, alias, culture, ConvertToDictionaryVars(tokens));
+        if (text.IndexOf('_') == -1)
+        {
+            return text;
+        }
 
-         /// 
-         /// Convert an array of strings to a dictionary of indices -> values
-         /// 
-         /// 
-         /// 
-         internal static IDictionary? ConvertToDictionaryVars(string?[]? variables)
-         {
-             if (variables == null) return null;
-             if (variables.Any() == false) return null;
+        var areaAndKey = text.Split('_');
 
-             return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s })
-                 .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value);
-         }
+        if (areaAndKey.Length < 2)
+        {
+            return text;
+        }
 
-         public static string? UmbracoDictionaryTranslate(this ILocalizedTextService manager, ICultureDictionary cultureDictionary, string? text)
-         {
-             if (text == null)
-                 return null;
+        value = manager.Localize(areaAndKey[0], areaAndKey[1]);
+        return value.StartsWith("[") ? text : value;
+    }
 
-             if (text.StartsWith("#") == false)
-                 return text;
+    /// 
+    ///     Convert an array of strings to a dictionary of indices -> values
+    /// 
+    /// 
+    /// 
+    internal static IDictionary? ConvertToDictionaryVars(string?[]? variables)
+    {
+        if (variables == null)
+        {
+            return null;
+        }
 
-             text = text.Substring(1);
-             var value = cultureDictionary[text];
-             if (value.IsNullOrWhiteSpace() == false)
-             {
-                 return value;
-             }
-
-             if (text.IndexOf('_') == -1)
-                 return text;
-
-             var areaAndKey = text.Split('_');
-
-             if (areaAndKey.Length < 2)
-                return text;
-
-             value = manager.Localize(areaAndKey[0], areaAndKey[1]);
-             return value.StartsWith("[") ? text : value;
-         }
+        if (variables.Any() == false)
+        {
+            return null;
+        }
 
+        return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s })
+            .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value);
     }
 }
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
index 41d12f9a45..26a2e9fb60 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.IO;
-using System.Linq;
 using System.Xml;
 using System.Xml.Linq;
 using Microsoft.Extensions.FileProviders;
@@ -11,291 +7,313 @@ using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Cache;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care
+///     of
+/// 
+public class LocalizedTextServiceFileSources
 {
-    /// 
-    /// Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care of
-    /// 
-    public class LocalizedTextServiceFileSources
+    private readonly IAppPolicyCache _cache;
+    private readonly IDirectoryContents _directoryContents;
+    private readonly DirectoryInfo? _fileSourceFolder;
+    private readonly ILogger _logger;
+    private readonly IEnumerable? _supplementFileSources;
+
+    // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
+    private readonly Dictionary _twoLetterCultureConverter = new();
+
+    private readonly Lazy>> _xmlSources;
+
+    [Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
+    public LocalizedTextServiceFileSources(
+        ILogger logger,
+        AppCaches appCaches,
+        DirectoryInfo fileSourceFolder,
+        IEnumerable supplementFileSources)
+        : this(
+            logger,
+            appCaches,
+            fileSourceFolder,
+            supplementFileSources,
+            new NotFoundDirectoryContents())
     {
-        private readonly ILogger _logger;
-        private readonly IDirectoryContents _directoryContents;
-        private readonly IAppPolicyCache _cache;
-        private readonly IEnumerable? _supplementFileSources;
-        private readonly DirectoryInfo? _fileSourceFolder;
+    }
 
-        // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
-        private readonly Dictionary _twoLetterCultureConverter = new Dictionary();
-
-        private readonly Lazy>> _xmlSources;
-
-        [Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
-        public LocalizedTextServiceFileSources(
-            ILogger logger,
-            AppCaches appCaches,
-            DirectoryInfo fileSourceFolder,
-            IEnumerable supplementFileSources)
-            :this(
-                logger,
-                appCaches,
-                fileSourceFolder,
-                supplementFileSources, new NotFoundDirectoryContents())
+    /// 
+    ///     This is used to configure the file sources with the main file sources shipped with Umbraco and also including
+    ///     supplemental/plugin based
+    ///     localization files. The supplemental files will be loaded in and merged in after the primary files.
+    ///     The supplemental files must be named with the 4 letter culture name with a hyphen such as : en-AU.xml
+    /// 
+    public LocalizedTextServiceFileSources(
+        ILogger logger,
+        AppCaches appCaches,
+        DirectoryInfo fileSourceFolder,
+        IEnumerable supplementFileSources,
+        IDirectoryContents directoryContents)
+    {
+        if (appCaches == null)
         {
-
+            throw new ArgumentNullException("appCaches");
         }
 
-        /// 
-        /// This is used to configure the file sources with the main file sources shipped with Umbraco and also including supplemental/plugin based
-        /// localization files. The supplemental files will be loaded in and merged in after the primary files.
-        /// The supplemental files must be named with the 4 letter culture name with a hyphen such as : en-AU.xml
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public LocalizedTextServiceFileSources(
-            ILogger logger,
-            AppCaches appCaches,
-            DirectoryInfo fileSourceFolder,
-            IEnumerable supplementFileSources,
-            IDirectoryContents directoryContents
-            )
+        _logger = logger ?? throw new ArgumentNullException("logger");
+        _directoryContents = directoryContents;
+        _cache = appCaches.RuntimeCache;
+        _fileSourceFolder = fileSourceFolder ?? throw new ArgumentNullException("fileSourceFolder");
+        _supplementFileSources = supplementFileSources;
+
+        // Create the lazy source for the _xmlSources
+        _xmlSources = new Lazy>>(() =>
         {
-            if (logger == null) throw new ArgumentNullException("logger");
-            if (appCaches == null) throw new ArgumentNullException("cache");
-            if (fileSourceFolder == null) throw new ArgumentNullException("fileSourceFolder");
+            var result = new Dictionary>();
 
-            _logger = logger;
-            _directoryContents = directoryContents;
-            _cache = appCaches.RuntimeCache;
-            _fileSourceFolder = fileSourceFolder;
-            _supplementFileSources = supplementFileSources;
+            IEnumerable files = GetLanguageFiles();
 
-            //Create the lazy source for the _xmlSources
-            _xmlSources = new Lazy>>(() =>
+            if (!files.Any())
             {
-                var result = new Dictionary>();
+                return result;
+            }
 
+            foreach (IFileInfo fileInfo in files)
+            {
+                IFileInfo localCopy = fileInfo;
+                var filename = Path.GetFileNameWithoutExtension(localCopy.Name).Replace("_", "-");
 
-                var files = GetLanguageFiles();
-
-                if (!files.Any())
+                // TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct
+                // names instead of storing them as 2 letters but actually having a 4 letter culture. So now, we
+                // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that
+                // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea)
+                // that any 4 letter file is named with the actual culture that it is!
+                CultureInfo? culture = null;
+                if (filename.Length == 2)
                 {
-                    return result;
-                }
-
-                foreach (var fileInfo in files)
-                {
-                    var localCopy = fileInfo;
-                    var filename = Path.GetFileNameWithoutExtension(localCopy.Name).Replace("_", "-");
-
-                    // TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct
-                    // names instead of storing them as 2 letters but actually having a 4 letter culture. So now, we
-                    // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that
-                    // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea)
-                    // that any 4 letter file is named with the actual culture that it is!
-                    CultureInfo? culture = null;
-                    if (filename.Length == 2)
+                    // we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't
+                    // want to load in the entire doc into mem just to read a single value
+                    using (Stream fs = fileInfo.CreateReadStream())
+                    using (var reader = XmlReader.Create(fs))
                     {
-                        //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't
-                        //want to load in the entire doc into mem just to read a single value
-                        using (var fs = fileInfo.CreateReadStream())
-                        using (var reader = XmlReader.Create(fs))
+                        if (reader.IsStartElement())
                         {
-                            if (reader.IsStartElement())
+                            if (reader.Name == "language")
                             {
-                                if (reader.Name == "language")
+                                if (reader.MoveToAttribute("culture"))
                                 {
-                                    if (reader.MoveToAttribute("culture"))
+                                    var cultureVal = reader.Value;
+                                    try
                                     {
-                                        var cultureVal = reader.Value;
-                                        try
-                                        {
-                                            culture = CultureInfo.GetCultureInfo(cultureVal);
-                                            //add to the tracked dictionary
-                                            _twoLetterCultureConverter[filename] = culture;
-                                        }
-                                        catch (CultureNotFoundException)
-                                        {
-                                            _logger.LogWarning("The culture {CultureValue} found in the file {CultureFile} is not a valid culture", cultureVal, fileInfo.Name);
-                                            //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise
-                                            // an exception will be thrown.
-                                        }
+                                        culture = CultureInfo.GetCultureInfo(cultureVal);
+
+                                        // add to the tracked dictionary
+                                        _twoLetterCultureConverter[filename] = culture;
+                                    }
+                                    catch (CultureNotFoundException)
+                                    {
+                                        _logger.LogWarning(
+                                            "The culture {CultureValue} found in the file {CultureFile} is not a valid culture",
+                                            cultureVal,
+                                            fileInfo.Name);
+
+                                        // If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise
+                                        // an exception will be thrown.
                                     }
                                 }
                             }
                         }
                     }
-                    if (culture == null)
-                    {
-                        culture = CultureInfo.GetCultureInfo(filename);
-                    }
-
-                    //get the lazy value from cache
-                    result[culture] = new Lazy(() => _cache.GetCacheItem(
-                        string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name), () =>
-                        {
-                            XDocument xdoc;
-
-                            //load in primary
-                            using (var fs = localCopy.CreateReadStream())
-                            {
-                                xdoc = XDocument.Load(fs);
-                            }
-
-                            //load in supplementary
-                            MergeSupplementaryFiles(culture, xdoc);
-
-                            return xdoc;
-                        }, isSliding: true, timeout: TimeSpan.FromMinutes(10))!);
                 }
-                return result;
-            });
 
+                if (culture == null)
+                {
+                    culture = CultureInfo.GetCultureInfo(filename);
+                }
 
-        }
+                // get the lazy value from cache
+                result[culture] = new Lazy(
+                    () => _cache.GetCacheItem(
+                        string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name),
+                        () =>
+                    {
+                        XDocument xdoc;
 
-        private IEnumerable GetLanguageFiles()
-        {
-            var result = new List();
+                        // load in primary
+                        using (Stream fs = localCopy.CreateReadStream())
+                        {
+                            xdoc = XDocument.Load(fs);
+                        }
 
-            if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
-            {
+                        // load in supplementary
+                        MergeSupplementaryFiles(culture, xdoc);
 
-                result.AddRange(
-                    new PhysicalDirectoryContents(_fileSourceFolder.FullName)
-                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
-                );
-            }
-
-            if (_directoryContents.Exists)
-            {
-                result.AddRange(
-                _directoryContents
-                        .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
-                );
+                        return xdoc;
+                    },
+                        isSliding: true,
+                        timeout: TimeSpan.FromMinutes(10))!);
             }
 
             return result;
+        });
+    }
+
+    /// 
+    ///     Constructor
+    /// 
+    public LocalizedTextServiceFileSources(ILogger logger, AppCaches appCaches, DirectoryInfo fileSourceFolder)
+        : this(logger, appCaches, fileSourceFolder, Enumerable.Empty())
+    {
+    }
+
+    /// 
+    ///     returns all xml sources for all culture files found in the folder
+    /// 
+    /// 
+    public IDictionary> GetXmlSources() => _xmlSources.Value;
+
+    private IEnumerable GetLanguageFiles()
+    {
+        var result = new List();
+
+        if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
+        {
+            result.AddRange(
+                new PhysicalDirectoryContents(_fileSourceFolder.FullName)
+                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml")));
         }
 
-        /// 
-        /// Constructor
-        /// 
-        public LocalizedTextServiceFileSources(ILogger logger, AppCaches appCaches, DirectoryInfo fileSourceFolder)
-            : this(logger, appCaches, fileSourceFolder, Enumerable.Empty())
-        { }
-
-        /// 
-        /// returns all xml sources for all culture files found in the folder
-        /// 
-        /// 
-        public IDictionary> GetXmlSources()
+        if (_directoryContents.Exists)
         {
-            return _xmlSources.Value;
+            result.AddRange(
+                _directoryContents
+                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml")));
         }
 
-        // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
-        public Attempt TryConvert2LetterCultureTo4Letter(string twoLetterCulture)
+        return result;
+    }
+
+    // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
+    public Attempt TryConvert2LetterCultureTo4Letter(string twoLetterCulture)
+    {
+        if (twoLetterCulture.Length != 2)
         {
-            if (twoLetterCulture.Length != 2) return Attempt.Fail();
-
-            //This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
-            var resolved = _xmlSources.Value;
-
-            return _twoLetterCultureConverter.ContainsKey(twoLetterCulture)
-                ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture])
-                : Attempt.Fail();
+            return Attempt.Fail();
         }
 
-        // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
-        public Attempt TryConvert4LetterCultureTo2Letter(CultureInfo culture)
+        // This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
+        Dictionary> resolved = _xmlSources.Value;
+
+        return _twoLetterCultureConverter.ContainsKey(twoLetterCulture)
+            ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture])
+            : Attempt.Fail();
+    }
+
+    // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
+    public Attempt TryConvert4LetterCultureTo2Letter(CultureInfo culture)
+    {
+        if (culture == null)
         {
-            if (culture == null) throw new ArgumentNullException("culture");
-
-            //This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
-            var resolved = _xmlSources.Value;
-
-            return _twoLetterCultureConverter.Values.Contains(culture)
-                ? Attempt.Succeed(culture.Name.Substring(0, 2))
-                : Attempt.Fail();
+            throw new ArgumentNullException("culture");
         }
 
-        private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc)
+        // This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
+        Dictionary> resolved = _xmlSources.Value;
+
+        return _twoLetterCultureConverter.Values.Contains(culture)
+            ? Attempt.Succeed(culture.Name.Substring(0, 2))
+            : Attempt.Fail();
+    }
+
+    private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc)
+    {
+        if (xMasterDoc.Root == null)
         {
-            if (xMasterDoc.Root == null) return;
-            if (_supplementFileSources != null)
+            return;
+        }
+
+        if (_supplementFileSources != null)
+        {
+            // now load in supplementary
+            IEnumerable found = _supplementFileSources.Where(x =>
             {
-                //now load in supplementary
-                var found = _supplementFileSources.Where(x =>
-                {
-                    var extension = Path.GetExtension(x.File.FullName);
-                    var fileCultureName = Path.GetFileNameWithoutExtension(x.File.FullName).Replace("_", "-").Replace(".user", "");
-                    return extension.InvariantEquals(".xml") && (
-                        fileCultureName.InvariantEquals(culture.Name)
-                        || fileCultureName.InvariantEquals(culture.TwoLetterISOLanguageName)
-                    );
-                });
+                var extension = Path.GetExtension(x.File.FullName);
+                var fileCultureName = Path.GetFileNameWithoutExtension(x.File.FullName).Replace("_", "-")
+                    .Replace(".user", string.Empty);
+                return extension.InvariantEquals(".xml") && (
+                    fileCultureName.InvariantEquals(culture.Name)
+                    || fileCultureName.InvariantEquals(culture.TwoLetterISOLanguageName));
+            });
 
-                foreach (var supplementaryFile in found)
+            foreach (LocalizedTextServiceSupplementaryFileSource supplementaryFile in found)
+            {
+                using (FileStream fs = supplementaryFile.File.OpenRead())
                 {
-                    using (var fs = supplementaryFile.File.OpenRead())
+                    XDocument xChildDoc;
+                    try
                     {
-                        XDocument xChildDoc;
-                        try
-                        {
-                            xChildDoc = XDocument.Load(fs);
-                        }
-                        catch (Exception ex)
-                        {
-                            _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName);
-                            continue;
-                        }
+                        xChildDoc = XDocument.Load(fs);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName);
+                        continue;
+                    }
 
-                        if (xChildDoc.Root == null || xChildDoc.Root.Name != "language") continue;
-                        foreach (var xArea in xChildDoc.Root.Elements("area")
-                            .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
-                        {
-                            var areaAlias = (string)xArea.Attribute("alias")!;
+                    if (xChildDoc.Root == null || xChildDoc.Root.Name != "language")
+                    {
+                        continue;
+                    }
 
-                            var areaFound = xMasterDoc.Root.Elements("area").FirstOrDefault(x => ((string)x.Attribute("alias")!) == areaAlias);
-                            if (areaFound == null)
-                            {
-                                //add the whole thing
-                                xMasterDoc.Root.Add(xArea);
-                            }
-                            else
-                            {
-                                MergeChildKeys(xArea, areaFound, supplementaryFile.OverwriteCoreKeys);
-                            }
+                    foreach (XElement xArea in xChildDoc.Root.Elements("area")
+                                 .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
+                    {
+                        var areaAlias = (string)xArea.Attribute("alias")!;
+
+                        XElement? areaFound = xMasterDoc.Root.Elements("area").FirstOrDefault(x => (string)x.Attribute("alias")! == areaAlias);
+                        if (areaFound == null)
+                        {
+                            // add the whole thing
+                            xMasterDoc.Root.Add(xArea);
+                        }
+                        else
+                        {
+                            MergeChildKeys(xArea, areaFound, supplementaryFile.OverwriteCoreKeys);
                         }
                     }
                 }
             }
         }
-
-        private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
-        {
-            if (destination == null) throw new ArgumentNullException("destination");
-            if (source == null) throw new ArgumentNullException("source");
-
-            //merge in the child elements
-            foreach (var key in source.Elements("key")
-                .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
-            {
-                var keyAlias = (string)key.Attribute("alias")!;
-                var keyFound = destination.Elements("key").FirstOrDefault(x => ((string)x.Attribute("alias")!) == keyAlias);
-                if (keyFound == null)
-                {
-                    //append, it doesn't exist
-                    destination.Add(key);
-                }
-                else if (overwrite)
-                {
-                    //overwrite
-                    keyFound.Value = key.Value;
-                }
-            }
-        }
+    }
+
+    private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
+    {
+        if (destination == null)
+        {
+            throw new ArgumentNullException("destination");
+        }
+
+        if (source == null)
+        {
+            throw new ArgumentNullException("source");
+        }
+
+        // merge in the child elements
+        foreach (XElement key in source.Elements("key")
+                     .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
+        {
+            var keyAlias = (string)key.Attribute("alias")!;
+            XElement? keyFound = destination.Elements("key")
+                .FirstOrDefault(x => (string)x.Attribute("alias")! == keyAlias);
+            if (keyFound == null)
+            {
+                // append, it doesn't exist
+                destination.Add(key);
+            }
+            else if (overwrite)
+            {
+                // overwrite
+                keyFound.Value = key.Value;
+            }
+        }
     }
 }
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs
index 7fe5e0e48a..cff9a55234 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs
@@ -1,20 +1,14 @@
-using System;
-using System.IO;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+public class LocalizedTextServiceSupplementaryFileSource
 {
-    public class LocalizedTextServiceSupplementaryFileSource
+    public LocalizedTextServiceSupplementaryFileSource(FileInfo file, bool overwriteCoreKeys)
     {
-
-        public LocalizedTextServiceSupplementaryFileSource(FileInfo file, bool overwriteCoreKeys)
-        {
-            if (file == null) throw new ArgumentNullException("file");
-
-            File = file;
-            OverwriteCoreKeys = overwriteCoreKeys;
-        }
-
-        public FileInfo File { get; private set; }
-        public bool OverwriteCoreKeys { get; private set; }
+        File = file ?? throw new ArgumentNullException("file");
+        OverwriteCoreKeys = overwriteCoreKeys;
     }
+
+    public FileInfo File { get; }
+
+    public bool OverwriteCoreKeys { get; }
 }
diff --git a/src/Umbraco.Core/Services/MacroService.cs b/src/Umbraco.Core/Services/MacroService.cs
index 8226ca2f69..be07d1ef02 100644
--- a/src/Umbraco.Core/Services/MacroService.cs
+++ b/src/Umbraco.Core/Services/MacroService.cs
@@ -1,179 +1,176 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the Macro Service, which is an easy access to operations involving 
+/// 
+internal class MacroService : RepositoryService, IMacroService
 {
-    /// 
-    /// Represents the Macro Service, which is an easy access to operations involving 
-    /// 
-    internal class MacroService : RepositoryService, IMacroService
+    private readonly IAuditRepository _auditRepository;
+    private readonly IMacroRepository _macroRepository;
+
+    public MacroService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMacroRepository macroRepository,
+        IAuditRepository auditRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IMacroRepository _macroRepository;
-        private readonly IAuditRepository _auditRepository;
+        _macroRepository = macroRepository;
+        _auditRepository = auditRepository;
+    }
 
-        public MacroService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMacroRepository macroRepository, IAuditRepository auditRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    /// 
+    ///     Gets an  object by its alias
+    /// 
+    /// Alias to retrieve an  for
+    /// An  object
+    public IMacro? GetByAlias(string alias)
+    {
+        if (_macroRepository is not IMacroRepository macroWithAliasRepository)
         {
-            _macroRepository = macroRepository;
-            _auditRepository = auditRepository;
+            return GetAll().FirstOrDefault(x => x.Alias == alias);
         }
 
-        /// 
-        /// Gets an  object by its alias
-        /// 
-        /// Alias to retrieve an  for
-        /// An  object
-        public IMacro? GetByAlias(string alias)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (_macroRepository is not IMacroRepository macroWithAliasRepository)
-            {
-                return GetAll().FirstOrDefault(x => x.Alias == alias);
-            }
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return macroWithAliasRepository.GetByAlias(alias);
-            }
-        }
-
-        public IEnumerable GetAll()
-        {
-            return GetAll(new int[0]);
-        }
-
-        public IEnumerable GetAll(params int[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.GetMany(ids);
-            }
-        }
-
-        public IEnumerable GetAll(params Guid[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.GetMany(ids);
-            }
-        }
-
-        public IEnumerable GetAll(params string[] aliases)
-        {
-            if (_macroRepository is not IMacroRepository macroWithAliasRepository)
-            {
-                var hashset = new HashSet(aliases);
-                return GetAll().Where(x => hashset.Contains(x.Alias));
-            }
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return macroWithAliasRepository.GetAllByAlias(aliases) ?? Enumerable.Empty();
-            }
-        }
-
-        public IMacro? GetById(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.Get(id);
-            }
-        }
-
-        public IMacro? GetById(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.Get(id);
-            }
-        }
-
-        /// 
-        /// Deletes an 
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the macro
-        public void Delete(IMacro macro, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new MacroDeletingNotification(macro, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _macroRepository.Delete(macro);
-
-                scope.Notifications.Publish(new MacroDeletedNotification(macro, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId, -1);
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Saves an 
-        /// 
-        ///  to save
-        /// Optional Id of the user deleting the macro
-        public void Save(IMacro macro, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new MacroSavingNotification(macro, eventMessages);
-
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(macro.Name))
-                {
-                    throw new ArgumentException("Cannot save macro with empty name.");
-                }
-
-                _macroRepository.Save(macro);
-
-                scope.Notifications.Publish(new MacroSavedNotification(macro, eventMessages).WithStateFrom(savingNotification));
-                Audit(AuditType.Save, userId, -1);
-
-                scope.Complete();
-            }
-        }
-
-        ///// 
-        ///// Gets a list all available  plugins
-        ///// 
-        ///// An enumerable list of  objects
-        //public IEnumerable GetMacroPropertyTypes()
-        //{
-        //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes;
-        //}
-
-        ///// 
-        ///// Gets an  by its alias
-        ///// 
-        ///// Alias to retrieve an  for
-        ///// An  object
-        //public IMacroPropertyType GetMacroPropertyTypeByAlias(string alias)
-        //{
-        //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes.FirstOrDefault(x => x.Alias == alias);
-        //}
-
-        private void Audit(AuditType type, int userId, int objectId)
-        {
-            _auditRepository.Save(new AuditItem(objectId, type, userId, "Macro"));
+            return macroWithAliasRepository.GetByAlias(alias);
         }
     }
+
+    public IEnumerable GetAll() => GetAll(new int[0]);
+
+    public IEnumerable GetAll(params int[] ids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.GetMany(ids);
+        }
+    }
+
+    public IEnumerable GetAll(params Guid[] ids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.GetMany(ids);
+        }
+    }
+
+    public IEnumerable GetAll(params string[] aliases)
+    {
+        if (_macroRepository is not IMacroRepository macroWithAliasRepository)
+        {
+            var hashset = new HashSet(aliases);
+            return GetAll().Where(x => hashset.Contains(x.Alias));
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return macroWithAliasRepository.GetAllByAlias(aliases);
+        }
+    }
+
+    public IMacro? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.Get(id);
+        }
+    }
+
+    public IMacro? GetById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.Get(id);
+        }
+    }
+
+    /// 
+    ///     Deletes an 
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the macro
+    public void Delete(IMacro macro, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new MacroDeletingNotification(macro, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            _macroRepository.Delete(macro);
+
+            scope.Notifications.Publish(
+                new MacroDeletedNotification(macro, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId, -1);
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Saves an 
+    /// 
+    ///  to save
+    /// Optional Id of the user deleting the macro
+    public void Save(IMacro macro, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new MacroSavingNotification(macro, eventMessages);
+
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            if (string.IsNullOrWhiteSpace(macro.Name))
+            {
+                throw new ArgumentException("Cannot save macro with empty name.");
+            }
+
+            _macroRepository.Save(macro);
+
+            scope.Notifications.Publish(
+                new MacroSavedNotification(macro, eventMessages).WithStateFrom(savingNotification));
+            Audit(AuditType.Save, userId, -1);
+
+            scope.Complete();
+        }
+    }
+
+    ///// 
+    ///// Gets a list all available  plugins
+    ///// 
+    ///// An enumerable list of  objects
+    // public IEnumerable GetMacroPropertyTypes()
+    // {
+    //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes;
+    // }
+
+    ///// 
+    ///// Gets an  by its alias
+    ///// 
+    ///// Alias to retrieve an  for
+    ///// An  object
+    // public IMacroPropertyType GetMacroPropertyTypeByAlias(string alias)
+    // {
+    //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes.FirstOrDefault(x => x.Alias == alias);
+    // }
+    private void Audit(AuditType type, int userId, int objectId) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, "Macro"));
 }
diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs
index 13ba415fee..325677407e 100644
--- a/src/Umbraco.Core/Services/MediaService.cs
+++ b/src/Umbraco.Core/Services/MediaService.cs
@@ -1,12 +1,9 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.IO;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.IO;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence;
 using Umbraco.Cms.Core.Persistence.Querying;
@@ -33,9 +30,16 @@ namespace Umbraco.Cms.Core.Services
 
         #region Constructors
 
-        public MediaService(ICoreScopeProvider provider, MediaFileManager mediaFileManager, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IMediaRepository mediaRepository, IAuditRepository auditRepository, IMediaTypeRepository mediaTypeRepository,
-            IEntityRepository entityRepository, IShortStringHelper shortStringHelper)
+        public MediaService(
+            ICoreScopeProvider provider,
+            MediaFileManager mediaFileManager,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IMediaRepository mediaRepository,
+            IAuditRepository auditRepository,
+            IMediaTypeRepository mediaTypeRepository,
+            IEntityRepository entityRepository,
+            IShortStringHelper shortStringHelper)
             : base(provider, loggerFactory, eventMessagesFactory)
         {
             _mediaFileManager = mediaFileManager;
@@ -52,50 +56,51 @@ namespace Umbraco.Cms.Core.Services
 
         public int Count(string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.Count(mediaTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.Count(mediaTypeAlias);
         }
 
         public int CountNotTrashed(string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
 
-                var mediaTypeId = 0;
-                if (string.IsNullOrWhiteSpace(mediaTypeAlias) == false)
+            var mediaTypeId = 0;
+            if (string.IsNullOrWhiteSpace(mediaTypeAlias) == false)
+            {
+                IMediaType? mediaType = _mediaTypeRepository.Get(mediaTypeAlias);
+                if (mediaType == null)
                 {
-                    var mediaType = _mediaTypeRepository.Get(mediaTypeAlias);
-                    if (mediaType == null) return 0;
-                    mediaTypeId = mediaType.Id;
+                    return 0;
                 }
 
-                var query = Query().Where(x => x.Trashed == false);
-                if (mediaTypeId > 0)
-                    query = query.Where(x => x.ContentTypeId == mediaTypeId);
-                return _mediaRepository.Count(query);
+                mediaTypeId = mediaType.Id;
             }
+
+            IQuery query = Query().Where(x => x.Trashed == false);
+            if (mediaTypeId > 0)
+            {
+                query = query.Where(x => x.ContentTypeId == mediaTypeId);
+            }
+
+            return _mediaRepository.Count(query);
         }
 
         public int CountChildren(int parentId, string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.ReadLock(Constants.Locks.MediaTree);
                 return _mediaRepository.CountChildren(parentId, mediaTypeAlias);
             }
         }
 
         public int CountDescendants(int parentId, string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.CountDescendants(parentId, mediaTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.CountDescendants(parentId, mediaTypeAlias);
         }
 
         #endregion
@@ -116,9 +121,9 @@ namespace Umbraco.Cms.Core.Services
         /// Alias of the 
         /// Optional id of the user creating the media item
         /// 
-        public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            var parent = GetById(parentId);
+            IMedia? parent = GetById(parentId);
             return CreateMedia(name, parent, mediaTypeAlias, userId);
         }
 
@@ -134,25 +139,29 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            var mediaType = GetMediaType(mediaTypeAlias);
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias);
             if (mediaType == null)
+            {
                 throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias));
-            var parent = parentId > 0 ? GetById(parentId) : null;
+            }
+
+            IMedia? parent = parentId > 0 ? GetById(parentId) : null;
             if (parentId > 0 && parent == null)
+            {
                 throw new ArgumentException("No media with that id.", nameof(parentId));
+            }
+
             if (name != null && name.Length > 255)
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
             }
 
             var media = new Core.Models.Media(name, parentId, mediaType);
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                CreateMedia(scope, media, parent!, userId, false);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            CreateMedia(scope, media, parent!, userId, false);
+            scope.Complete();
 
             return media;
         }
@@ -168,24 +177,25 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMedia(string name, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string name, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
             // not locking since not saving anything
 
-            var mediaType = GetMediaType(mediaTypeAlias);
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias);
             if (mediaType == null)
+            {
                 throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias));
+            }
+
             if (name != null && name.Length > 255)
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
             }
 
             var media = new Core.Models.Media(name, -1, mediaType);
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                CreateMedia(scope, media, null, userId, false);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            CreateMedia(scope, media, null, userId, false);
+            scope.Complete();
 
             return media;
         }
@@ -202,28 +212,32 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            if (parent == null) throw new ArgumentNullException(nameof(parent));
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            if (parent == null)
             {
-                // not locking since not saving anything
-
-                var mediaType = GetMediaType(mediaTypeAlias);
-                if (mediaType == null)
-                    throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
-                if (name != null && name.Length > 255)
-                {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-                }
-
-                var media = new Core.Models.Media(name, parent, mediaType);
-                CreateMedia(scope, media, parent, userId, false);
-
-                scope.Complete();
-                return media;
+                throw new ArgumentNullException(nameof(parent));
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // not locking since not saving anything
+
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias);
+            if (mediaType == null)
+            {
+                throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
+            }
+
+            if (name != null && name.Length > 255)
+            {
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+            }
+
+            var media = new Core.Models.Media(name, parent, mediaType);
+            CreateMedia(scope, media, parent, userId, false);
+
+            scope.Complete();
+            return media;
         }
 
         /// 
@@ -235,27 +249,29 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // locking the media tree secures media types too
+            scope.WriteLock(Constants.Locks.MediaTree);
+
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias); // + locks
+            if (mediaType == null)
             {
-                // locking the media tree secures media types too
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
-
-                var mediaType = GetMediaType(mediaTypeAlias); // + locks
-                if (mediaType == null)
-                    throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
-
-                var parent = parentId > 0 ? GetById(parentId) : null; // + locks
-                if (parentId > 0 && parent == null)
-                    throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback
-
-                var media = parentId > 0 ? new Core.Models.Media(name, parent, mediaType) : new Core.Models.Media(name, parentId, mediaType);
-                CreateMedia(scope, media, parent, userId, true);
-
-                scope.Complete();
-                return media;
+                throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
             }
+
+            IMedia? parent = parentId > 0 ? GetById(parentId) : null; // + locks
+            if (parentId > 0 && parent == null)
+            {
+                throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback
+            }
+
+            Models.Media media = parentId > 0 ? new Core.Models.Media(name, parent, mediaType) : new Core.Models.Media(name, parentId, mediaType);
+            CreateMedia(scope, media, parent, userId, true);
+
+            scope.Complete();
+            return media;
         }
 
         /// 
@@ -267,25 +283,28 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            if (parent == null) throw new ArgumentNullException(nameof(parent));
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            if (parent == null)
             {
-                // locking the media tree secures media types too
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
-
-                var mediaType = GetMediaType(mediaTypeAlias); // + locks
-                if (mediaType == null)
-                    throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
-
-                var media = new Core.Models.Media(name, parent, mediaType);
-                CreateMedia(scope, media, parent, userId, true);
-
-                scope.Complete();
-                return media;
+                throw new ArgumentNullException(nameof(parent));
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // locking the media tree secures media types too
+            scope.WriteLock(Constants.Locks.MediaTree);
+
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias); // + locks
+            if (mediaType == null)
+            {
+                throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
+            }
+
+            var media = new Core.Models.Media(name, parent, mediaType);
+            CreateMedia(scope, media, parent, userId, true);
+
+            scope.Complete();
+            return media;
         }
 
         private void CreateMedia(ICoreScope scope, Core.Models.Media media, IMedia? parent, int userId, bool withIdentity)
@@ -309,7 +328,9 @@ namespace Umbraco.Cms.Core.Services
             }
 
             if (withIdentity == false)
+            {
                 return;
+            }
 
             Audit(AuditType.New, media.CreatorId, media.Id, $"Media '{media.Name}' was created with Id {media.Id}");
         }
@@ -325,11 +346,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMedia? GetById(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.Get(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.Get(id);
         }
 
         /// 
@@ -340,13 +359,14 @@ namespace Umbraco.Cms.Core.Services
         public IEnumerable GetByIds(IEnumerable ids)
         {
             var idsA = ids.ToArray();
-            if (idsA.Length == 0) return Enumerable.Empty();
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            if (idsA.Length == 0)
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetMany(idsA);
+                return Enumerable.Empty();
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetMany(idsA);
         }
 
         /// 
@@ -356,11 +376,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMedia? GetById(Guid key)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.Get(key);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.Get(key);
         }
 
         /// 
@@ -370,50 +388,62 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetByIds(IEnumerable ids)
         {
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0) return Enumerable.Empty();
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            Guid[] idsA = ids.ToArray();
+            if (idsA.Length == 0)
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetMany(idsA);
+                return Enumerable.Empty();
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetMany(idsA);
         }
 
         /// 
         public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
             if (ordering == null)
-                ordering = Ordering.By("sortOrder");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.ContentTree);
-                return _mediaRepository.GetPage(
-                    Query()?.Where(x => x.ContentTypeId == contentTypeId),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
+                ordering = Ordering.By("sortOrder");
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _mediaRepository.GetPage(Query()?.Where(x => x.ContentTypeId == contentTypeId), pageIndex, pageSize, out totalRecords, filter, ordering);
         }
 
         /// 
         public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
             if (ordering == null)
-                ordering = Ordering.By("sortOrder");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.ContentTree);
-                return _mediaRepository.GetPage(
-                    Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
+                ordering = Ordering.By("sortOrder");
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _mediaRepository.GetPage(
+                Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)), pageIndex, pageSize, out totalRecords, filter, ordering);
         }
 
         /// 
@@ -424,12 +454,10 @@ namespace Umbraco.Cms.Core.Services
         /// Contrary to most methods, this method filters out trashed media items.
         public IEnumerable? GetByLevel(int level)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                var query = Query().Where(x => x.Level == level && x.Trashed == false);
-                return _mediaRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            IQuery query = Query().Where(x => x.Level == level && x.Trashed == false);
+            return _mediaRepository.Get(query);
         }
 
         /// 
@@ -439,11 +467,9 @@ namespace Umbraco.Cms.Core.Services
         /// An  item
         public IMedia? GetVersion(int versionId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetVersion(versionId);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetVersion(versionId);
         }
 
         /// 
@@ -453,11 +479,9 @@ namespace Umbraco.Cms.Core.Services
         /// An Enumerable list of  objects
         public IEnumerable GetVersions(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetAllVersions(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetAllVersions(id);
         }
 
         /// 
@@ -468,7 +492,7 @@ namespace Umbraco.Cms.Core.Services
         public IEnumerable GetAncestors(int id)
         {
             // intentionally not locking
-            var media = GetById(id);
+            IMedia? media = GetById(id);
             return GetAncestors(media);
         }
 
@@ -480,82 +504,105 @@ namespace Umbraco.Cms.Core.Services
         public IEnumerable GetAncestors(IMedia? media)
         {
             //null check otherwise we get exceptions
-            if (media is null || media.Path.IsNullOrWhiteSpace()) return Enumerable.Empty();
+            if (media is null || media.Path.IsNullOrWhiteSpace())
+            {
+                return Enumerable.Empty();
+            }
 
-            var rootId = Cms.Core.Constants.System.RootString;
+            var rootId = Constants.System.RootString;
             var ids = media.Path.Split(Constants.CharArrays.Comma)
                 .Where(x => x != rootId && x != media.Id.ToString(CultureInfo.InvariantCulture))
                 .Select(s => int.Parse(s, CultureInfo.InvariantCulture))
                 .ToArray();
             if (ids.Any() == false)
+            {
                 return new List();
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetMany(ids);
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetMany(ids);
         }
 
         /// 
-        public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+        public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
             if (ordering == null)
+            {
                 ordering = Ordering.By("sortOrder");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-
-                var query = Query()?.Where(x => x.ParentId == id);
-                return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+
+            IQuery? query = Query()?.Where(x => x.ParentId == id);
+            return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
         }
 
         /// 
-        public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+        public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
         {
             if (ordering == null)
-                ordering = Ordering.By("Path");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-
-                //if the id is System Root, then just get all
-                if (id != Cms.Core.Constants.System.Root)
-                {
-                    var mediaPath = _entityRepository.GetAllPaths(Cms.Core.Constants.ObjectTypes.Media, id).ToArray();
-                    if (mediaPath.Length == 0)
-                    {
-                        totalChildren = 0;
-                        return Enumerable.Empty();
-                    }
-                    return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
-                }
-                return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
+                ordering = Ordering.By("Path");
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+
+            //if the id is System Root, then just get all
+            if (id != Constants.System.Root)
+            {
+                TreeEntityPath[] mediaPath = _entityRepository.GetAllPaths(Constants.ObjectTypes.Media, id).ToArray();
+                if (mediaPath.Length == 0)
+                {
+                    totalChildren = 0;
+                    return Enumerable.Empty();
+                }
+
+                return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
+            }
+
+            return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
         }
 
         private IQuery? GetPagedDescendantQuery(string? mediaPath)
         {
-            var query = Query();
+            IQuery? query = Query();
             if (!mediaPath.IsNullOrWhiteSpace())
+            {
                 query?.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
+            }
+
             return query;
         }
 
-        private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter, Ordering ordering)
+        private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize, out long totalChildren, IQuery? filter, Ordering ordering)
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
-            if (ordering == null) throw new ArgumentNullException(nameof(ordering));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
+
+            if (ordering == null)
+            {
+                throw new ArgumentNullException(nameof(ordering));
+            }
 
             return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
         }
@@ -568,7 +615,7 @@ namespace Umbraco.Cms.Core.Services
         public IMedia? GetParent(int id)
         {
             // intentionally not locking
-            var media = GetById(id);
+            IMedia? media = GetById(id);
             return GetParent(media);
         }
 
@@ -580,8 +627,10 @@ namespace Umbraco.Cms.Core.Services
         public IMedia? GetParent(IMedia? media)
         {
             var parentId = media?.ParentId;
-            if (parentId is null || media?.ParentId == Cms.Core.Constants.System.Root || media?.ParentId == Cms.Core.Constants.System.RecycleBinMedia)
+            if (parentId is null || media?.ParentId == Constants.System.Root || media?.ParentId == Constants.System.RecycleBinMedia)
+            {
                 return null;
+            }
 
             return GetById(parentId.Value);
         }
@@ -592,27 +641,24 @@ namespace Umbraco.Cms.Core.Services
         /// An Enumerable list of  objects
         public IEnumerable GetRootMedia()
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                var query = Query().Where(x => x.ParentId == Cms.Core.Constants.System.Root);
-                return _mediaRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            IQuery query = Query().Where(x => x.ParentId == Constants.System.Root);
+            return _mediaRepository.Get(query);
         }
 
         /// 
-        public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
+        public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            if (ordering == null)
             {
-                if (ordering == null)
-                    ordering = Ordering.By("Path");
-
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                var query = Query()?.Where(x => x.Path.StartsWith(Cms.Core.Constants.System.RecycleBinMediaPathPrefix));
-                return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
+                ordering = Ordering.By("Path");
             }
+
+            scope.ReadLock(Constants.Locks.MediaTree);
+            IQuery? query = Query()?.Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix));
+            return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
         }
 
         /// 
@@ -622,12 +668,10 @@ namespace Umbraco.Cms.Core.Services
         /// True if the media has any children otherwise False
         public bool HasChildren(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == id);
-                var count = _mediaRepository.Count(query);
-                return count > 0;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery query = Query().Where(x => x.ParentId == id);
+            var count = _mediaRepository.Count(query);
+            return count > 0;
         }
 
         /// 
@@ -654,7 +698,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// The  to save
         /// Id of the User saving the Media
-        public Attempt Save(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId)
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
 
@@ -675,10 +719,10 @@ namespace Umbraco.Cms.Core.Services
 
                 if (media.Name != null && media.Name.Length > 255)
                 {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                    throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
                 }
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
                 if (media.HasIdentity == false)
                 {
                     media.CreatorId = userId;
@@ -701,7 +745,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// Collection of  to save
         /// Id of the User saving the Media
-        public Attempt Save(IEnumerable medias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
             IMedia[] mediasA = medias.ToArray();
@@ -717,7 +761,7 @@ namespace Umbraco.Cms.Core.Services
 
                 IEnumerable> treeChanges = mediasA.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode));
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
                 foreach (IMedia media in mediasA)
                 {
                     if (media.HasIdentity == false)
@@ -731,7 +775,7 @@ namespace Umbraco.Cms.Core.Services
                 scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification));
                 // TODO: See note about suppressing events in content service
                 scope.Notifications.Publish(new MediaTreeChangeNotification(treeChanges, messages));
-                Audit(AuditType.Save, userId == -1 ? 0 : userId, Cms.Core.Constants.System.Root, "Bulk save media");
+                Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Bulk save media");
 
                 scope.Complete();
             }
@@ -748,7 +792,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// The  to delete
         /// Id of the User deleting the Media
-        public Attempt Delete(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Delete(IMedia media, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
 
@@ -760,7 +804,7 @@ namespace Umbraco.Cms.Core.Services
                     return OperationResult.Attempt.Cancel(messages);
                 }
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 DeleteLocked(scope, media, messages);
 
@@ -789,10 +833,13 @@ namespace Umbraco.Cms.Core.Services
             while (page * pageSize < total)
             {
                 //get descendants - ordered from deepest to shallowest
-                var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
-                foreach (var c in descendants)
+                IEnumerable descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
+                foreach (IMedia c in descendants)
+                {
                     DoDelete(c);
+                }
             }
+
             DoDelete(media);
         }
 
@@ -808,18 +855,16 @@ namespace Umbraco.Cms.Core.Services
         /// Id of the  object to delete versions from
         /// Latest version date
         /// Optional Id of the User deleting versions of a Media object
-        public void DeleteVersions(int id, DateTime versionDate, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                DeleteVersions(scope, true, id, versionDate, userId);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            DeleteVersions(scope, true, id, versionDate, userId);
+            scope.Complete();
         }
 
-        private void DeleteVersions(ICoreScope scope, bool wlock, int id, DateTime versionDate, int userId = Cms.Core.Constants.Security.SuperUserId)
+        private void DeleteVersions(ICoreScope scope, bool wlock, int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
             var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
             if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
@@ -828,11 +873,14 @@ namespace Umbraco.Cms.Core.Services
             }
 
             if (wlock)
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+            {
+                scope.WriteLock(Constants.Locks.MediaTree);
+            }
+
             _mediaRepository.DeleteVersions(id, versionDate);
 
             scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(deletingVersionsNotification));
-            Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete Media by version date");
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version date");
         }
 
         /// 
@@ -843,39 +891,37 @@ namespace Umbraco.Cms.Core.Services
         /// Id of the version to delete
         /// Boolean indicating whether to delete versions prior to the versionId
         /// Optional Id of the User deleting versions of a Media object
-        public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, specificVersion: versionId);
+            if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
             {
-                var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, specificVersion: versionId);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (deletePriorVersions)
-                {
-                    var media = GetVersion(versionId);
-                    if (media is not null)
-                    {
-                        DeleteVersions(scope, true, id, media.UpdateDate, userId);
-                    }
-                }
-                else
-                {
-                    scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
-                }
-
-                _mediaRepository.DeleteVersion(versionId);
-
-                scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete Media by version");
-
                 scope.Complete();
+                return;
             }
+
+            if (deletePriorVersions)
+            {
+                IMedia? media = GetVersion(versionId);
+                if (media is not null)
+                {
+                    DeleteVersions(scope, true, id, media.UpdateDate, userId);
+                }
+            }
+            else
+            {
+                scope.WriteLock(Constants.Locks.MediaTree);
+            }
+
+            _mediaRepository.DeleteVersion(versionId);
+
+            scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version");
+
+            scope.Complete();
         }
 
         #endregion
@@ -887,21 +933,21 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// The  to delete
         /// Id of the User deleting the Media
-        public Attempt MoveToRecycleBin(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
             var moves = new List<(IMedia, string)>();
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 // TODO: missing 7.6 "ensure valid path" thing here?
                 // but then should be in PerformMoveLocked on every moved item?
 
                 var originalPath = media.Path;
 
-                var moveEventInfo = new MoveEventInfo(media, originalPath, Cms.Core.Constants.System.RecycleBinMedia);
+                var moveEventInfo = new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia);
 
                 var movingToRecycleBinNotification = new MediaMovingToRecycleBinNotification(moveEventInfo, messages);
                 if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
@@ -910,7 +956,7 @@ namespace Umbraco.Cms.Core.Services
                     return OperationResult.Attempt.Cancel(messages);
                 }
 
-                PerformMoveLocked(media, Cms.Core.Constants.System.RecycleBinMedia, null, userId, moves, true);
+                PerformMoveLocked(media, Constants.System.RecycleBinMedia, null, userId, moves, true);
 
                 scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshBranch, messages));
                 MoveEventInfo[] moveInfo = moves.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)).ToArray();
@@ -929,12 +975,12 @@ namespace Umbraco.Cms.Core.Services
         /// The  to move
         /// Id of the Media's new Parent
         /// Id of the User moving the Media
-        public Attempt Move(IMedia media, int parentId, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Move(IMedia media, int parentId, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
 
             // if moving to the recycle bin then use the proper method
-            if (parentId == Cms.Core.Constants.System.RecycleBinMedia)
+            if (parentId == Constants.System.RecycleBinMedia)
             {
                 MoveToRecycleBin(media, userId);
                 return OperationResult.Attempt.Succeed(messages);
@@ -944,10 +990,10 @@ namespace Umbraco.Cms.Core.Services
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
-                IMedia? parent = parentId == Cms.Core.Constants.System.Root ? null : GetById(parentId);
-                if (parentId != Cms.Core.Constants.System.Root && (parent == null || parent.Trashed))
+                IMedia? parent = parentId == Constants.System.Root ? null : GetById(parentId);
+                if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
                 {
                     throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
                 }
@@ -999,38 +1045,43 @@ namespace Umbraco.Cms.Core.Services
             //media.Path = (parent == null ? "-1" : parent.Path) + "," + media.Id;
             //media.SortOrder = ((MediaRepository) repository).NextChildSortOrder(parentId);
             //media.Level += levelDelta;
-            PerformMoveMediaLocked(media, userId, trash);
+            PerformMoveMediaLocked(media, trash);
 
             // if uow is not immediate, content.Path will be updated only when the UOW commits,
             // and because we want it now, we have to calculate it by ourselves
             //paths[media.Id] = media.Path;
-            paths[media.Id] = (parent == null ? (parentId == Cms.Core.Constants.System.RecycleBinMedia ? "-1,-21" : Cms.Core.Constants.System.RootString) : parent.Path) + "," + media.Id;
+            paths[media.Id] = (parent == null ? parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString : parent.Path) + "," + media.Id;
 
             const int pageSize = 500;
-            var query = GetPagedDescendantQuery(originalPath);
+            IQuery? query = GetPagedDescendantQuery(originalPath);
             long total;
             do
             {
                 // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
-                var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
+                IEnumerable descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path"));
 
-                foreach (var descendant in descendants)
+                foreach (IMedia descendant in descendants)
                 {
                     moves.Add((descendant, descendant.Path)); // capture original path
 
                     // update path and level since we do not update parentId
                     descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
                     descendant.Level += levelDelta;
-                    PerformMoveMediaLocked(descendant, userId, trash);
+                    PerformMoveMediaLocked(descendant, trash);
                 }
 
-            } while (total > pageSize);
+            }
+            while (total > pageSize);
 
         }
 
-        private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash)
+        private void PerformMoveMediaLocked(IMedia media, bool? trash)
         {
-            if (trash.HasValue) ((ContentBase)media).Trashed = trash.Value;
+            if (trash.HasValue)
+            {
+                ((ContentBase)media).Trashed = trash.Value;
+            }
+
             _mediaRepository.Save(media);
         }
 
@@ -1038,17 +1089,17 @@ namespace Umbraco.Cms.Core.Services
         /// Empties the Recycle Bin by deleting all  that resides in the bin
         /// 
         /// Optional Id of the User emptying the Recycle Bin
-        public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security.SuperUserId)
+        public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId)
         {
             var deleted = new List();
             EventMessages messages = EventMessagesFactory.Get(); // TODO: and then?
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 // emptying the recycle bin means deleting whatever is in there - do it properly!
-                IQuery? query = Query().Where(x => x.ParentId == Cms.Core.Constants.System.RecycleBinMedia);
+                IQuery? query = Query().Where(x => x.ParentId == Constants.System.RecycleBinMedia);
                 IMedia[] medias = _mediaRepository.Get(query)?.ToArray() ?? Array.Empty();
 
                 var emptyingRecycleBinNotification = new MediaEmptyingRecycleBinNotification(medias, messages);
@@ -1065,7 +1116,7 @@ namespace Umbraco.Cms.Core.Services
                 }
                 scope.Notifications.Publish(new MediaEmptiedRecycleBinNotification(deleted, new EventMessages()).WithStateFrom(emptyingRecycleBinNotification));
                 scope.Notifications.Publish(new MediaTreeChangeNotification(deleted, TreeChangeTypes.Remove, messages));
-                Audit(AuditType.Delete, userId, Cms.Core.Constants.System.RecycleBinMedia, "Empty Media recycle bin");
+                Audit(AuditType.Delete, userId, Constants.System.RecycleBinMedia, "Empty Media recycle bin");
                 scope.Complete();
             }
 
@@ -1074,11 +1125,9 @@ namespace Umbraco.Cms.Core.Services
 
         public bool RecycleBinSmells()
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MediaTree);
-                return _mediaRepository.RecycleBinSmells();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.RecycleBinSmells();
         }
 
         #endregion
@@ -1092,7 +1141,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// 
         /// True if sorting succeeded, otherwise False
-        public bool Sort(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId)
         {
             IMedia[] itemsA = items.ToArray();
             if (itemsA.Length == 0)
@@ -1113,7 +1162,7 @@ namespace Umbraco.Cms.Core.Services
 
                 var saved = new List();
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
                 var sortOrder = 0;
 
                 foreach (IMedia media in itemsA)
@@ -1148,7 +1197,7 @@ namespace Umbraco.Cms.Core.Services
         {
             using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 ContentDataIntegrityReport report = _mediaRepository.CheckDataIntegrity(options);
 
@@ -1222,7 +1271,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// Id of the 
         /// Optional id of the user deleting the media
-        public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Constants.Security.SuperUserId)
         {
             // TODO: This currently this is called from the ContentTypeService but that needs to change,
             // if we are deleting a content type, we should just delete the data and do this operation slightly differently.
@@ -1238,7 +1287,7 @@ namespace Umbraco.Cms.Core.Services
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 IQuery? query = Query().WhereIn(x => x.ContentTypeId, mediaTypeIdsA);
                 IMedia[] medias = _mediaRepository.Get(query)?.ToArray() ?? Array.Empty();
@@ -1262,7 +1311,7 @@ namespace Umbraco.Cms.Core.Services
                         foreach (IMedia child in children.Where(x => mediaTypeIdsA.Contains(x.ContentTypeId) == false))
                         {
                             // see MoveToRecycleBin
-                            PerformMoveLocked(child, Cms.Core.Constants.System.RecycleBinMedia, null, userId, moves, true);
+                            PerformMoveLocked(child, Constants.System.RecycleBinMedia, null, userId, moves, true);
                             changes.Add(new TreeChange(media, TreeChangeTypes.RefreshBranch));
                         }
                     }
@@ -1281,7 +1330,7 @@ namespace Umbraco.Cms.Core.Services
                 }
                 scope.Notifications.Publish(new MediaTreeChangeNotification(changes, messages));
 
-                Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, $"Delete Media of types {string.Join(",", mediaTypeIdsA)}");
+                Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete Media of types {string.Join(",", mediaTypeIdsA)}");
 
                 scope.Complete();
             }
@@ -1293,29 +1342,36 @@ namespace Umbraco.Cms.Core.Services
         /// This needs extra care and attention as its potentially a dangerous and extensive operation
         /// Id of the 
         /// Optional id of the user deleting the media
-        public void DeleteMediaOfType(int mediaTypeId, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteMediaOfType(int mediaTypeId, int userId = Constants.Security.SuperUserId)
         {
             DeleteMediaOfTypes(new[] { mediaTypeId }, userId);
         }
 
         private IMediaType GetMediaType(string mediaTypeAlias)
         {
-            if (mediaTypeAlias == null) throw new ArgumentNullException(nameof(mediaTypeAlias));
-            if (string.IsNullOrWhiteSpace(mediaTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(mediaTypeAlias));
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            if (mediaTypeAlias == null)
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTypes);
-
-                var query = Query().Where(x => x.Alias == mediaTypeAlias);
-                var mediaType = _mediaTypeRepository.Get(query)?.FirstOrDefault();
-
-                if (mediaType == null)
-                    throw new InvalidOperationException($"No media type matched the specified alias '{mediaTypeAlias}'.");
-
-                scope.Complete();
-                return mediaType;
+                throw new ArgumentNullException(nameof(mediaTypeAlias));
             }
+
+            if (string.IsNullOrWhiteSpace(mediaTypeAlias))
+            {
+                throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(mediaTypeAlias));
+            }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.ReadLock(Constants.Locks.MediaTypes);
+
+            IQuery query = Query().Where(x => x.Alias == mediaTypeAlias);
+            IMediaType? mediaType = _mediaTypeRepository.Get(query)?.FirstOrDefault();
+
+            if (mediaType == null)
+            {
+                throw new InvalidOperationException($"No media type matched the specified alias '{mediaTypeAlias}'.");
+            }
+
+            scope.Complete();
+            return mediaType;
         }
 
         #endregion
diff --git a/src/Umbraco.Core/Services/MediaServiceExtensions.cs b/src/Umbraco.Core/Services/MediaServiceExtensions.cs
index 1cf648c35d..8d45367e61 100644
--- a/src/Umbraco.Core/Services/MediaServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/MediaServiceExtensions.cs
@@ -1,45 +1,48 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+/// 
+///     Media service extension methods
+/// 
+/// 
+///     Many of these have to do with UDI lookups but we don't need to add these methods to the service interface since a
+///     UDI is just a GUID
+///     and the services already support GUIDs
+/// 
+public static class MediaServiceExtensions
 {
-    /// 
-    /// Media service extension methods
-    /// 
-    /// 
-    /// Many of these have to do with UDI lookups but we don't need to add these methods to the service interface since a UDI is just a GUID
-    /// and the services already support GUIDs
-    /// 
-    public static class MediaServiceExtensions
+    public static IEnumerable GetByIds(this IMediaService mediaService, IEnumerable ids)
     {
-        public static IEnumerable GetByIds(this IMediaService mediaService, IEnumerable ids)
+        var guids = new List();
+        foreach (Udi udi in ids)
         {
-            var guids = new List();
-            foreach (var udi in ids)
+            if (udi is not GuidUdi guidUdi)
             {
-                var guidUdi = udi as GuidUdi;
-                if (guidUdi is null)
-                    throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by media");
-                guids.Add(guidUdi);
+                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                    " which is required by media");
             }
 
-            return mediaService.GetByIds(guids.Select(x => x.Guid));
+            guids.Add(guidUdi);
         }
 
-        public static IMedia CreateMedia(this IMediaService mediaService, string name, Udi parentId, string mediaTypeAlias, int userId = 0)
+        return mediaService.GetByIds(guids.Select(x => x.Guid));
+    }
+
+    public static IMedia CreateMedia(this IMediaService mediaService, string name, Udi parentId, string mediaTypeAlias, int userId = 0)
+    {
+        if (parentId is not GuidUdi guidUdi)
         {
-            var guidUdi = parentId as GuidUdi;
-            if (guidUdi is null)
-                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by media");
-            var parent = mediaService.GetById(guidUdi.Guid);
-            return mediaService.CreateMedia(name, parent, mediaTypeAlias, userId);
+            throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                " which is required by media");
         }
+
+        IMedia? parent = mediaService.GetById(guidUdi.Guid);
+        return mediaService.CreateMedia(name, parent, mediaTypeAlias, userId);
     }
 }
diff --git a/src/Umbraco.Core/Services/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs
index 6873fb4a39..eff6ba0fba 100644
--- a/src/Umbraco.Core/Services/MediaTypeService.cs
+++ b/src/Umbraco.Core/Services/MediaTypeService.cs
@@ -1,5 +1,3 @@
-using System;
-using System.Collections.Generic;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -8,70 +6,84 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class MediaTypeService : ContentTypeServiceBase, IMediaTypeService
 {
-    public class MediaTypeService : ContentTypeServiceBase, IMediaTypeService
+    public MediaTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMediaService mediaService,
+        IMediaTypeRepository mediaTypeRepository,
+        IAuditRepository auditRepository,
+        IMediaTypeContainerRepository entityContainerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(provider, loggerFactory, eventMessagesFactory, mediaTypeRepository, auditRepository, entityContainerRepository, entityRepository, eventAggregator) => MediaService = mediaService;
+
+    // beware! order is important to avoid deadlocks
+    protected override int[] ReadLockIds { get; } = { Constants.Locks.MediaTypes };
+
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.MediaTree, Constants.Locks.MediaTypes };
+
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.MediaType;
+
+    private IMediaService MediaService { get; }
+
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
     {
-        public MediaTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMediaService mediaService,
-            IMediaTypeRepository mediaTypeRepository, IAuditRepository auditRepository, IMediaTypeContainerRepository entityContainerRepository,
-            IEntityRepository entityRepository, IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory, mediaTypeRepository, auditRepository, entityContainerRepository, entityRepository, eventAggregator)
+        foreach (var typeId in typeIds)
         {
-            MediaService = mediaService;
-        }
-
-        // beware! order is important to avoid deadlocks
-        protected override int[] ReadLockIds { get; } = { Cms.Core.Constants.Locks.MediaTypes };
-        protected override int[] WriteLockIds { get; } = { Cms.Core.Constants.Locks.MediaTree, Cms.Core.Constants.Locks.MediaTypes };
-
-        private IMediaService MediaService { get; }
-
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.MediaType;
-
-        #region Notifications
-
-        protected override SavingNotification GetSavingNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeSavingNotification(item, eventMessages);
-
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeSavingNotification(items, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeSavedNotification(item, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeSavedNotification(items, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeDeletingNotification(item, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeDeletingNotification(items, eventMessages);
-
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeDeletedNotification(items, eventMessages);
-
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new MediaTypeMovingNotification(moveInfo, eventMessages);
-
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new MediaTypeMovedNotification(moveInfo, eventMessages);
-
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MediaTypeChangedNotification(changes, eventMessages);
-
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MediaTypeRefreshedNotification(changes, eventMessages);
-
-        #endregion
-
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            foreach (var typeId in typeIds)
-                MediaService.DeleteMediaOfType(typeId);
+            MediaService.DeleteMediaOfType(typeId);
         }
     }
+
+    #region Notifications
+
+    protected override SavingNotification GetSavingNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeSavingNotification(item, eventMessages);
+
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeSavingNotification(items, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeSavedNotification(item, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeSavedNotification(items, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeDeletingNotification(item, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeDeletingNotification(items, eventMessages);
+
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeDeletedNotification(items, eventMessages);
+
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new MediaTypeMovingNotification(moveInfo, eventMessages);
+
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new MediaTypeMovedNotification(moveInfo, eventMessages);
+
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MediaTypeChangedNotification(changes, eventMessages);
+
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MediaTypeRefreshedNotification(changes, eventMessages);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/MemberGroupService.cs b/src/Umbraco.Core/Services/MemberGroupService.cs
index 2290f9d84a..5a68236455 100644
--- a/src/Umbraco.Core/Services/MemberGroupService.cs
+++ b/src/Umbraco.Core/Services/MemberGroupService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -8,105 +5,105 @@ using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+internal class MemberGroupService : RepositoryService, IMemberGroupService
 {
-    internal class MemberGroupService : RepositoryService, IMemberGroupService
+    private readonly IMemberGroupRepository _memberGroupRepository;
+
+    public MemberGroupService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberGroupRepository memberGroupRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _memberGroupRepository = memberGroupRepository;
+
+    public IEnumerable GetAll()
     {
-        private readonly IMemberGroupRepository _memberGroupRepository;
-
-        public MemberGroupService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IMemberGroupRepository memberGroupRepository)
-            : base(provider, loggerFactory, eventMessagesFactory) =>
-            _memberGroupRepository = memberGroupRepository;
-
-        public IEnumerable GetAll()
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetMany();
-            }
+            return _memberGroupRepository.GetMany();
+        }
+    }
+
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        if (ids == null || ids.Any() == false)
+        {
+            return new IMemberGroup[0];
         }
 
-        public IEnumerable GetByIds(IEnumerable ids)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (ids == null || ids.Any() == false)
-            {
-                return new IMemberGroup[0];
-            }
+            return _memberGroupRepository.GetMany(ids.ToArray());
+        }
+    }
 
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetMany(ids.ToArray());
-            }
+    public IMemberGroup? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _memberGroupRepository.Get(id);
+        }
+    }
+
+    public IMemberGroup? GetById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _memberGroupRepository.Get(id);
+        }
+    }
+
+    public IMemberGroup? GetByName(string? name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _memberGroupRepository.GetByName(name);
+        }
+    }
+
+    public void Save(IMemberGroup memberGroup)
+    {
+        if (string.IsNullOrWhiteSpace(memberGroup.Name))
+        {
+            throw new InvalidOperationException("The name of a MemberGroup can not be empty");
         }
 
-        public IMemberGroup? GetById(int id)
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            var savingNotification = new MemberGroupSavingNotification(memberGroup, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                return _memberGroupRepository.Get(id);
-            }
-        }
-
-        public IMemberGroup? GetById(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.Get(id);
-            }
-        }
-
-        public IMemberGroup? GetByName(string? name)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetByName(name);
-            }
-        }
-
-        public void Save(IMemberGroup memberGroup)
-        {
-            if (string.IsNullOrWhiteSpace(memberGroup.Name))
-            {
-                throw new InvalidOperationException("The name of a MemberGroup can not be empty");
-            }
-
-            var evtMsgs = EventMessagesFactory.Get();
-
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingNotification = new MemberGroupSavingNotification(memberGroup, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _memberGroupRepository.Save(memberGroup);
                 scope.Complete();
-
-                scope.Notifications.Publish(new MemberGroupSavedNotification(memberGroup, evtMsgs).WithStateFrom(savingNotification));
+                return;
             }
+
+            _memberGroupRepository.Save(memberGroup);
+            scope.Complete();
+
+            scope.Notifications.Publish(
+                new MemberGroupSavedNotification(memberGroup, evtMsgs).WithStateFrom(savingNotification));
         }
+    }
 
-        public void Delete(IMemberGroup memberGroup)
+    public void Delete(IMemberGroup memberGroup)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            var evtMsgs = EventMessagesFactory.Get();
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            var deletingNotification = new MemberGroupDeletingNotification(memberGroup, evtMsgs);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                var deletingNotification = new MemberGroupDeletingNotification(memberGroup, evtMsgs);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _memberGroupRepository.Delete(memberGroup);
                 scope.Complete();
-
-                scope.Notifications.Publish(new MemberGroupDeletedNotification(memberGroup, evtMsgs).WithStateFrom(deletingNotification));
+                return;
             }
+
+            _memberGroupRepository.Delete(memberGroup);
+            scope.Complete();
+
+            scope.Notifications.Publish(
+                new MemberGroupDeletedNotification(memberGroup, evtMsgs).WithStateFrom(deletingNotification));
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs
index 32b8961143..d8f9f787aa 100644
--- a/src/Umbraco.Core/Services/MemberService.cs
+++ b/src/Umbraco.Core/Services/MemberService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -28,8 +25,15 @@ namespace Umbraco.Cms.Core.Services
 
         #region Constructor
 
-        public MemberService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService,
-            IMemberRepository memberRepository, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, IAuditRepository auditRepository)
+        public MemberService(
+            ICoreScopeProvider provider,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IMemberGroupService memberGroupService,
+            IMemberRepository memberRepository,
+            IMemberTypeRepository memberTypeRepository,
+            IMemberGroupRepository memberGroupRepository,
+            IAuditRepository auditRepository)
             : base(provider, loggerFactory, eventMessagesFactory)
         {
             _memberRepository = memberRepository;
@@ -55,29 +59,27 @@ namespace Umbraco.Cms.Core.Services
         ///  with number of Members for passed in type
         public int GetCount(MemberCountType countType)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+
+            IQuery? query;
+
+            switch (countType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-
-                IQuery? query;
-
-                switch (countType)
-                {
-                    case MemberCountType.All:
-                        query = Query();
-                        break;
-                    case MemberCountType.LockedOut:
-                        query = Query()?.Where(x => x.IsLockedOut == true);
-                        break;
-                    case MemberCountType.Approved:
-                        query = Query()?.Where(x => x.IsApproved == true);
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(countType));
-                }
-
-                return _memberRepository.GetCountByQuery(query);
+                case MemberCountType.All:
+                    query = Query();
+                    break;
+                case MemberCountType.LockedOut:
+                    query = Query()?.Where(x => x.IsLockedOut == true);
+                    break;
+                case MemberCountType.Approved:
+                    query = Query()?.Where(x => x.IsApproved == true);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(countType));
             }
+
+            return _memberRepository.GetCountByQuery(query);
         }
 
         /// 
@@ -88,11 +90,9 @@ namespace Umbraco.Cms.Core.Services
         ///  with number of Members
         public int Count(string? memberTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Count(memberTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Count(memberTypeAlias);
         }
 
         #endregion
@@ -137,7 +137,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember CreateMember(string username, string email, string name, IMemberType memberType)
         {
-            if (memberType == null) throw new ArgumentNullException(nameof(memberType));
+            if (memberType == null)
+            {
+                throw new ArgumentNullException(nameof(memberType));
+            }
 
             var member = new Member(name, email.ToLower().Trim(), username, memberType, 0);
 
@@ -171,16 +174,16 @@ namespace Umbraco.Cms.Core.Services
             => CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias, isApproved);
 
         public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias)
-            => CreateMemberWithIdentity(username, email, username, "", memberTypeAlias);
+            => CreateMemberWithIdentity(username, email, username, string.Empty, memberTypeAlias);
 
         public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias, bool isApproved)
-            => CreateMemberWithIdentity(username, email, username, "", memberTypeAlias, isApproved);
+            => CreateMemberWithIdentity(username, email, string.Empty, string.Empty, memberTypeAlias, isApproved);
 
         public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias)
-            => CreateMemberWithIdentity(username, email, name, "", memberTypeAlias);
+            => CreateMemberWithIdentity(username, email, string.Empty, string.Empty, memberTypeAlias);
 
         public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias, bool isApproved)
-            => CreateMemberWithIdentity(username, email, name, "", memberTypeAlias, isApproved);
+            => CreateMemberWithIdentity(username, string.Empty, name, string.Empty, memberTypeAlias, isApproved);
 
         /// 
         /// Creates and persists a Member
@@ -217,7 +220,7 @@ namespace Umbraco.Cms.Core.Services
         }
 
         public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType)
-            => CreateMemberWithIdentity(username, email, username, "", memberType);
+            => CreateMemberWithIdentity(username, email, username, string.Empty, memberType);
 
         /// 
         /// Creates and persists a Member
@@ -229,10 +232,10 @@ namespace Umbraco.Cms.Core.Services
         /// MemberType the Member should be based on
         /// 
         public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType, bool isApproved)
-            => CreateMemberWithIdentity(username, email, username, "", memberType, isApproved);
+            => CreateMemberWithIdentity(username, email, username, string.Empty, memberType, isApproved);
 
         public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType)
-            => CreateMemberWithIdentity(username, email, name, "", memberType);
+            => CreateMemberWithIdentity(username, email, name, string.Empty, memberType);
 
         /// 
         /// Creates and persists a Member
@@ -245,7 +248,7 @@ namespace Umbraco.Cms.Core.Services
         /// MemberType the Member should be based on
         /// 
         public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType, bool isApproved)
-            => CreateMemberWithIdentity(username, email, name, "", memberType, isApproved);
+            => CreateMemberWithIdentity(username, email, name, string.Empty, memberType, isApproved);
 
         /// 
         /// Creates and persists a Member
@@ -260,29 +263,30 @@ namespace Umbraco.Cms.Core.Services
         /// 
         private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType, bool isApproved = true)
         {
-            if (memberType == null) throw new ArgumentNullException(nameof(memberType));
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            if (memberType == null)
             {
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                // ensure it all still make sense
-                // ensure it all still make sense
-                var vrfy = GetMemberType(scope, memberType.Alias); // + locks
-
-                if (vrfy == null || vrfy.Id != memberType.Id)
-                {
-                    throw new ArgumentException($"Member type with alias {memberType.Alias} does not exist or is a different member type."); // causes rollback
-                }
-
-                var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1);
-
-                Save(member);
-
-                scope.Complete();
-
-                return member;
+                throw new ArgumentNullException(nameof(memberType));
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            // ensure it all still make sense
+            // ensure it all still make sense
+            IMemberType? vrfy = GetMemberType(scope, memberType.Alias); // + locks
+
+            if (vrfy == null || vrfy.Id != memberType.Id)
+            {
+                throw new ArgumentException($"Member type with alias {memberType.Alias} does not exist or is a different member type."); // causes rollback
+            }
+
+            var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1);
+
+            Save(member);
+
+            scope.Complete();
+
+            return member;
         }
 
         #endregion
@@ -296,11 +300,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetById(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Get(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Get(id);
         }
 
         /// 
@@ -312,12 +314,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByKey(Guid id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.Key == id);
-                return _memberRepository.Get(query)?.FirstOrDefault();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.Key == id);
+            return _memberRepository.Get(query)?.FirstOrDefault();
         }
 
         /// 
@@ -329,29 +329,36 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetPage(null, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetPage(null, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
         }
 
-        public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, string? memberTypeAlias = null, string filter = "")
-        {
-            return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter);
-        }
+        public IEnumerable GetAll(
+            long pageIndex,
+            int pageSize,
+            out long totalRecords,
+            string orderBy,
+            Direction orderDirection,
+            string? memberTypeAlias = null,
+            string filter = "") =>
+            GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter);
 
-        public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, bool orderBySystemField, string? memberTypeAlias, string filter)
+        public IEnumerable GetAll(
+            long pageIndex,
+            int pageSize,
+            out long totalRecords,
+            string orderBy,
+            Direction orderDirection,
+            bool orderBySystemField,
+            string? memberTypeAlias,
+            string filter)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query1 = memberTypeAlias == null ? null : Query()?.Where(x => x.ContentTypeAlias == memberTypeAlias);
-                var query2 = filter == null ? null : Query()?.Where(x => (x.Name != null && x.Name.Contains(filter)) || x.Username.Contains(filter) || x.Email.Contains(filter));
-                return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, query2, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query1 = memberTypeAlias == null ? null : Query()?.Where(x => x.ContentTypeAlias == memberTypeAlias);
+            IQuery? query2 = filter == null ? null : Query()?.Where(x => (x.Name != null && x.Name.Contains(filter)) || x.Username.Contains(filter) || x.Email.Contains(filter));
+            return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, query2, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
         }
 
         /// 
@@ -361,13 +368,17 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByProviderKey(object id)
         {
-            var asGuid = id.TryConvertTo();
+            Attempt asGuid = id.TryConvertTo();
             if (asGuid.Success)
+            {
                 return GetByKey(asGuid.Result);
+            }
 
-            var asInt = id.TryConvertTo();
+            Attempt asInt = id.TryConvertTo();
             if (asInt.Success)
+            {
                 return GetById(asInt.Result);
+            }
 
             return null;
         }
@@ -379,12 +390,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByEmail(string email)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.Email.Equals(email));
-                return _memberRepository.Get(query)?.FirstOrDefault();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.Email.Equals(email));
+            return _memberRepository.Get(query)?.FirstOrDefault();
         }
 
         /// 
@@ -394,11 +403,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByUsername(string? username)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetByUsername(username);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetByUsername(username);
         }
 
         /// 
@@ -408,12 +415,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetMembersByMemberType(string memberTypeAlias)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.ContentTypeAlias == memberTypeAlias);
-                return _memberRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.ContentTypeAlias == memberTypeAlias);
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -423,12 +428,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetMembersByMemberType(int memberTypeId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.ContentTypeId == memberTypeId);
-                return _memberRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.ContentTypeId == memberTypeId);
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -438,11 +441,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetMembersByGroup(string memberGroupName)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetByMemberGroup(memberGroupName);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetByMemberGroup(memberGroupName);
         }
 
         /// 
@@ -453,11 +454,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetAllMembers(params int[] ids)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetMany(ids);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetMany(ids);
         }
 
         /// 
@@ -471,34 +470,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query();
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query?.Where(member => string.Equals(member.Name, displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query?.Where(member => member.Name != null && member.Name.Contains(displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query?.Where(member => member.Name != null && member.Name.StartsWith(displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query?.Where(member => member.Name != null && member.Name.EndsWith(displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.Wildcard:
-                        query?.Where(member => member.Name != null && member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
-                }
-
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Name"));
+                case StringPropertyMatchType.Exact:
+                    query?.Where(member => string.Equals(member.Name, displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query?.Where(member => member.Name != null && member.Name.Contains(displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query?.Where(member => member.Name != null && member.Name.StartsWith(displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query?.Where(member => member.Name != null && member.Name.EndsWith(displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.Wildcard:
+                    query?.Where(member => member.Name != null && member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
             }
+
+            return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Name"));
         }
 
         /// 
@@ -512,34 +509,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query();
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query?.Where(member => member.Email.Equals(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query?.Where(member => member.Email.Contains(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query?.Where(member => member.Email.StartsWith(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query?.Where(member => member.Email.EndsWith(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.Wildcard:
-                        query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Email"));
+                case StringPropertyMatchType.Exact:
+                    query?.Where(member => member.Email.Equals(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query?.Where(member => member.Email.Contains(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query?.Where(member => member.Email.StartsWith(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query?.Where(member => member.Email.EndsWith(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.Wildcard:
+                    query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Email"));
         }
 
         /// 
@@ -553,34 +548,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query();
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query?.Where(member => member.Username.Equals(login));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query?.Where(member => member.Username.Contains(login));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query?.Where(member => member.Username.StartsWith(login));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query?.Where(member => member.Username.EndsWith(login));
-                        break;
-                    case StringPropertyMatchType.Wildcard:
-                        query?.Where(member => member.Username.SqlWildcard(login, TextColumnType.NVarchar));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
+                case StringPropertyMatchType.Exact:
+                    query?.Where(member => member.Username.Equals(login));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query?.Where(member => member.Username.Contains(login));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query?.Where(member => member.Username.StartsWith(login));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query?.Where(member => member.Username.EndsWith(login));
+                    break;
+                case StringPropertyMatchType.Wildcard:
+                    query?.Where(member => member.Username.SqlWildcard(login, TextColumnType.NVarchar));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
         }
 
         /// 
@@ -592,31 +585,29 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IQuery query;
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEquals(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEquals(value, TextColumnType.NVarchar)));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlContains(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlContains(value, TextColumnType.NVarchar)));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar)));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEndsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEndsWith(value, TextColumnType.NVarchar)));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.Get(query);
+                case StringPropertyMatchType.Exact:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEquals(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEquals(value, TextColumnType.NVarchar)));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlContains(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlContains(value, TextColumnType.NVarchar)));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar)));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEndsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEndsWith(value, TextColumnType.NVarchar)));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -628,34 +619,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IQuery query;
-
-                switch (matchType)
-                {
-                    case ValuePropertyMatchType.Exact:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue == value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue > value);
-                        break;
-                    case ValuePropertyMatchType.LessThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue < value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue >= value);
-                        break;
-                    case ValuePropertyMatchType.LessThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue <= value);
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.Get(query);
+                case ValuePropertyMatchType.Exact:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue == value);
+                    break;
+                case ValuePropertyMatchType.GreaterThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue > value);
+                    break;
+                case ValuePropertyMatchType.LessThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue < value);
+                    break;
+                case ValuePropertyMatchType.GreaterThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue >= value);
+                    break;
+                case ValuePropertyMatchType.LessThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue <= value);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -666,13 +655,11 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, bool value)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).BoolPropertyValue == value);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).BoolPropertyValue == value);
 
-                return _memberRepository.Get(query);
-            }
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -684,35 +671,33 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IQuery query;
-
-                switch (matchType)
-                {
-                    case ValuePropertyMatchType.Exact:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue == value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue > value);
-                        break;
-                    case ValuePropertyMatchType.LessThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue < value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue >= value);
-                        break;
-                    case ValuePropertyMatchType.LessThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue <= value);
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
-                }
-
-                // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo!
-                return _memberRepository.Get(query);
+                case ValuePropertyMatchType.Exact:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue == value);
+                    break;
+                case ValuePropertyMatchType.GreaterThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue > value);
+                    break;
+                case ValuePropertyMatchType.LessThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue < value);
+                    break;
+                case ValuePropertyMatchType.GreaterThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue >= value);
+                    break;
+                case ValuePropertyMatchType.LessThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue <= value);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
             }
+
+            // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo!
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -722,11 +707,9 @@ namespace Umbraco.Cms.Core.Services
         /// True if the Member exists otherwise False
         public bool Exists(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Exists(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Exists(id);
         }
 
         /// 
@@ -736,17 +719,17 @@ namespace Umbraco.Cms.Core.Services
         /// True if the Member exists otherwise False
         public bool Exists(string username)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Exists(username);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Exists(username);
         }
 
         #endregion
 
         #region Save
 
+        public void SetLastLogin(string username, DateTime date) => throw new NotImplementedException();
+
         /// 
         public void Save(IMember member)
         {
@@ -754,67 +737,63 @@ namespace Umbraco.Cms.Core.Services
             member.Username = member.Username.Trim();
             member.Email = member.Email.Trim();
 
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var savingNotification = new MemberSavingNotification(member, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var savingNotification = new MemberSavingNotification(member, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(member.Name))
-                {
-                    throw new ArgumentException("Cannot save member with empty name.");
-                }
-
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                _memberRepository.Save(member);
-
-                scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, 0, member.Id);
-
                 scope.Complete();
+                return;
             }
+
+            if (string.IsNullOrWhiteSpace(member.Name))
+            {
+                throw new ArgumentException("Cannot save member with empty name.");
+            }
+
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            _memberRepository.Save(member);
+
+            scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, 0, member.Id);
+
+            scope.Complete();
         }
 
         /// 
         public void Save(IEnumerable members)
         {
-            var membersA = members.ToArray();
+            IMember[] membersA = members.ToArray();
 
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var savingNotification = new MemberSavingNotification(membersA, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var savingNotification = new MemberSavingNotification(membersA, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                foreach (var member in membersA)
-                {
-                    //trimming username and email to make sure we have no trailing space
-                    member.Username = member.Username.Trim();
-                    member.Email = member.Email.Trim();
-
-                    _memberRepository.Save(member);
-                }
-
-                scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, 0, -1, "Save multiple Members");
-
                 scope.Complete();
+                return;
             }
+
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            foreach (IMember member in membersA)
+            {
+                //trimming username and email to make sure we have no trailing space
+                member.Username = member.Username.Trim();
+                member.Email = member.Email.Trim();
+
+                _memberRepository.Save(member);
+            }
+
+            scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, 0, -1, "Save multiple Members");
+
+            scope.Complete();
         }
 
         #endregion
@@ -827,23 +806,21 @@ namespace Umbraco.Cms.Core.Services
         ///  to Delete
         public void Delete(IMember member)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var deletingNotification = new MemberDeletingNotification(member, evtMsgs);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                var deletingNotification = new MemberDeletingNotification(member, evtMsgs);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(Constants.Locks.MemberTree);
-                DeleteLocked(scope, member, evtMsgs, deletingNotification.State);
-
-                Audit(AuditType.Delete, 0, member.Id);
                 scope.Complete();
+                return;
             }
+
+            scope.WriteLock(Constants.Locks.MemberTree);
+            DeleteLocked(scope, member, evtMsgs, deletingNotification.State);
+
+            Audit(AuditType.Delete, 0, member.Id);
+            scope.Complete();
         }
 
         private void DeleteLocked(ICoreScope scope, IMember member, EventMessages evtMsgs, IDictionary? notificationState = null)
@@ -861,12 +838,10 @@ namespace Umbraco.Cms.Core.Services
 
         public void AddRole(string roleName)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.CreateIfNotExists(roleName);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.CreateIfNotExists(roleName);
+            scope.Complete();
         }
 
         /// 
@@ -876,11 +851,9 @@ namespace Umbraco.Cms.Core.Services
 
         public IEnumerable GetAllRoles()
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberGroupRepository.GetMany().Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberGroupRepository.GetMany().Distinct();
         }
 
         /// 
@@ -890,178 +863,150 @@ namespace Umbraco.Cms.Core.Services
         /// A list of member roles
         public IEnumerable GetAllRoles(int memberId)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
-                return result.Select(x => x.Name).WhereNotNull().Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
+            return result.Select(x => x.Name).WhereNotNull().Distinct();
         }
 
         public IEnumerable GetAllRoles(string username)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
-                return result.Where(x => x.Name != null).Select(x => x.Name).Distinct()!;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
+            return result.Where(x => x.Name != null).Select(x => x.Name).Distinct()!;
         }
 
         public IEnumerable GetAllRolesIds()
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct();
         }
 
         public IEnumerable GetAllRolesIds(int memberId)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
-                return result.Select(x => x.Id).Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
+            return result.Select(x => x.Id).Distinct();
         }
 
         public IEnumerable GetAllRolesIds(string username)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
-                return result.Select(x => x.Id).Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
+            return result.Select(x => x.Id).Distinct();
         }
 
         public IEnumerable GetMembersInRole(string roleName)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetByMemberGroup(roleName);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetByMemberGroup(roleName);
         }
 
         public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType);
         }
 
         public bool DeleteRole(string roleName, bool throwIfBeingUsed)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            if (throwIfBeingUsed)
             {
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                if (throwIfBeingUsed)
+                // get members in role
+                IEnumerable membersInRole = _memberRepository.GetByMemberGroup(roleName);
+                if (membersInRole.Any())
                 {
-                    // get members in role
-                    IEnumerable membersInRole = _memberRepository.GetByMemberGroup(roleName);
-                    if (membersInRole.Any())
-                    {
-                        throw new InvalidOperationException("The role " + roleName + " is currently assigned to members");
-                    }
+                    throw new InvalidOperationException("The role " + roleName + " is currently assigned to members");
                 }
-
-                IQuery query = Query().Where(g => g.Name == roleName);
-                IMemberGroup[]? found = _memberGroupRepository.Get(query)?.ToArray();
-
-                if (found is not null)
-                {
-                    foreach (IMemberGroup memberGroup in found)
-                    {
-                        _memberGroupService.Delete(memberGroup);
-                    }
-                }
-
-                scope.Complete();
-                return found?.Length > 0;
             }
+
+            IQuery query = Query().Where(g => g.Name == roleName);
+            IMemberGroup[]? found = _memberGroupRepository.Get(query)?.ToArray();
+
+            if (found is not null)
+            {
+                foreach (IMemberGroup memberGroup in found)
+                {
+                    _memberGroupService.Delete(memberGroup);
+                }
+            }
+
+            scope.Complete();
+            return found?.Length > 0;
         }
 
         public void AssignRole(string username, string roleName) => AssignRoles(new[] { username }, new[] { roleName });
 
         public void AssignRoles(string[] usernames, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                int[] ids = _memberRepository.GetMemberIds(usernames);
-                _memberGroupRepository.AssignRoles(ids, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            var ids = _memberRepository.GetMemberIds(usernames);
+            _memberGroupRepository.AssignRoles(ids, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
+            scope.Complete();
         }
 
         public void DissociateRole(string username, string roleName) => DissociateRoles(new[] { username }, new[] { roleName });
 
         public void DissociateRoles(string[] usernames, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                int[] ids = _memberRepository.GetMemberIds(usernames);
-                _memberGroupRepository.DissociateRoles(ids, roleNames);
-                scope.Notifications.Publish(new RemovedMemberRolesNotification(ids, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            var ids = _memberRepository.GetMemberIds(usernames);
+            _memberGroupRepository.DissociateRoles(ids, roleNames);
+            scope.Notifications.Publish(new RemovedMemberRolesNotification(ids, roleNames));
+            scope.Complete();
         }
 
         public void AssignRole(int memberId, string roleName) => AssignRoles(new[] { memberId }, new[] { roleName });
 
         public void AssignRoles(int[] memberIds, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.AssignRoles(memberIds, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.AssignRoles(memberIds, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
+            scope.Complete();
         }
 
         public void DissociateRole(int memberId, string roleName) => DissociateRoles(new[] { memberId }, new[] { roleName });
 
         public void DissociateRoles(int[] memberIds, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.DissociateRoles(memberIds, roleNames);
-                scope.Notifications.Publish(new RemovedMemberRolesNotification(memberIds, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.DissociateRoles(memberIds, roleNames);
+            scope.Notifications.Publish(new RemovedMemberRolesNotification(memberIds, roleNames));
+            scope.Complete();
         }
 
         public void ReplaceRoles(string[] usernames, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                int[] ids = _memberRepository.GetMemberIds(usernames);
-                _memberGroupRepository.ReplaceRoles(ids, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            int[] ids = _memberRepository.GetMemberIds(usernames);
+            _memberGroupRepository.ReplaceRoles(ids, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
+            scope.Complete();
         }
 
         public void ReplaceRoles(int[] memberIds, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.ReplaceRoles(memberIds, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.ReplaceRoles(memberIds, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
+            scope.Complete();
         }
 
         #endregion
@@ -1084,34 +1029,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public MemberExportModel? ExportMember(Guid key)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery? query = Query().Where(x => x.Key == key);
+            IMember? member = _memberRepository.Get(query)?.FirstOrDefault();
+
+            if (member == null)
             {
-                IQuery? query = Query().Where(x => x.Key == key);
-                IMember? member = _memberRepository.Get(query)?.FirstOrDefault();
-
-                if (member == null)
-                {
-                    return null;
-                }
-
-                var model = new MemberExportModel
-                {
-                    Id = member.Id,
-                    Key = member.Key,
-                    Name = member.Name,
-                    Username = member.Username,
-                    Email = member.Email,
-                    Groups = GetAllRoles(member.Id).ToList(),
-                    ContentTypeAlias = member.ContentTypeAlias,
-                    CreateDate = member.CreateDate,
-                    UpdateDate = member.UpdateDate,
-                    Properties = new List(GetPropertyExportItems(member))
-                };
-
-                scope.Notifications.Publish(new ExportedMemberNotification(member, model));
-
-                return model;
+                return null;
             }
+
+            var model = new MemberExportModel
+            {
+                Id = member.Id,
+                Key = member.Key,
+                Name = member.Name,
+                Username = member.Username,
+                Email = member.Email,
+                Groups = GetAllRoles(member.Id).ToList(),
+                ContentTypeAlias = member.ContentTypeAlias,
+                CreateDate = member.CreateDate,
+                UpdateDate = member.UpdateDate,
+                Properties = new List(GetPropertyExportItems(member))
+            };
+
+            scope.Notifications.Publish(new ExportedMemberNotification(member, model));
+
+            return model;
         }
 
         private static IEnumerable GetPropertyExportItems(IMember member)
@@ -1150,38 +1093,36 @@ namespace Umbraco.Cms.Core.Services
         /// Id of the MemberType
         public void DeleteMembersOfType(int memberTypeId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
             // note: no tree to manage here
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            // TODO: What about content that has the contenttype as part of its composition?
+            IQuery? query = Query().Where(x => x.ContentTypeId == memberTypeId);
+
+            IMember[]? members = _memberRepository.Get(query)?.ToArray();
+
+            if (members is null)
             {
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                // TODO: What about content that has the contenttype as part of its composition?
-                IQuery? query = Query().Where(x => x.ContentTypeId == memberTypeId);
-
-                IMember[]? members = _memberRepository.Get(query)?.ToArray();
-
-                if (members is null)
-                {
-                    return;
-                }
-
-                if (scope.Notifications.PublishCancelable(new MemberDeletingNotification(members, evtMsgs)))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                foreach (IMember member in members)
-                {
-                    // delete media
-                    // triggers the deleted event (and handles the files)
-                    DeleteLocked(scope, member, evtMsgs);
-                }
-
-                scope.Complete();
+                return;
             }
+
+            if (scope.Notifications.PublishCancelable(new MemberDeletingNotification(members, evtMsgs)))
+            {
+                scope.Complete();
+                return;
+            }
+
+            foreach (IMember member in members)
+            {
+                // delete media
+                // triggers the deleted event (and handles the files)
+                DeleteLocked(scope, member, evtMsgs);
+            }
+
+            scope.Complete();
         }
 
         private IMemberType GetMemberType(ICoreScope scope, string memberTypeAlias)
@@ -1220,10 +1161,8 @@ namespace Umbraco.Cms.Core.Services
                 throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias));
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return GetMemberType(scope, memberTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return GetMemberType(scope, memberTypeAlias);
         }
         #endregion
     }
diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs
index 1d42989841..67b7f08111 100644
--- a/src/Umbraco.Core/Services/MemberTypeService.cs
+++ b/src/Umbraco.Core/Services/MemberTypeService.cs
@@ -9,98 +9,145 @@ using Umbraco.Cms.Core.Services.Changes;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class MemberTypeService : ContentTypeServiceBase, IMemberTypeService
 {
-    public class MemberTypeService : ContentTypeServiceBase, IMemberTypeService
+    private readonly IMemberTypeRepository _memberTypeRepository;
+
+    [Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V12.")]
+    public MemberTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMemberService memberService,
+        IMemberTypeRepository memberTypeRepository,
+        IAuditRepository auditRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : this(
+            provider,
+            loggerFactory,
+            eventMessagesFactory,
+            memberService,
+            memberTypeRepository,
+            auditRepository,
+            StaticServiceProvider.Instance.GetRequiredService(),
+            entityRepository,
+            eventAggregator)
     {
-        private readonly IMemberTypeRepository _memberTypeRepository;
+    }
 
-        [Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V12.")]
-        public MemberTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberService memberService,
-            IMemberTypeRepository memberTypeRepository, IAuditRepository auditRepository, IEntityRepository entityRepository, IEventAggregator eventAggregator)
-            : this(provider, loggerFactory, eventMessagesFactory, memberService, memberTypeRepository, auditRepository, StaticServiceProvider.Instance.GetRequiredService(), entityRepository, eventAggregator)
+    public MemberTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMemberService memberService,
+        IMemberTypeRepository memberTypeRepository,
+        IAuditRepository auditRepository,
+        IMemberTypeContainerRepository entityContainerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(
+            provider,
+            loggerFactory,
+            eventMessagesFactory,
+            memberTypeRepository,
+            auditRepository,
+            entityContainerRepository,
+            entityRepository,
+            eventAggregator)
+    {
+        MemberService = memberService;
+        _memberTypeRepository = memberTypeRepository;
+    }
+
+    // beware! order is important to avoid deadlocks
+    protected override int[] ReadLockIds { get; } = { Constants.Locks.MemberTypes };
+
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.MemberTree, Constants.Locks.MemberTypes };
+
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.MemberType;
+
+    private IMemberService MemberService { get; }
+
+    public string GetDefault()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-        }
+            scope.ReadLock(ReadLockIds);
 
-        public MemberTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberService memberService,
-            IMemberTypeRepository memberTypeRepository, IAuditRepository auditRepository, IMemberTypeContainerRepository entityContainerRepository, IEntityRepository entityRepository, IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory, memberTypeRepository, auditRepository, entityContainerRepository, entityRepository, eventAggregator)
-        {
-            MemberService = memberService;
-            _memberTypeRepository = memberTypeRepository;
-        }
-
-        // beware! order is important to avoid deadlocks
-        protected override int[] ReadLockIds { get; } = { Cms.Core.Constants.Locks.MemberTypes };
-        protected override int[] WriteLockIds { get; } = { Cms.Core.Constants.Locks.MemberTree, Cms.Core.Constants.Locks.MemberTypes };
-
-        private IMemberService MemberService { get; }
-
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.MemberType;
-
-        #region Notifications
-
-        protected override SavingNotification GetSavingNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeSavingNotification(item, eventMessages);
-
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeSavingNotification(items, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeSavedNotification(item, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeSavedNotification(items, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeDeletingNotification(item, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeDeletingNotification(items, eventMessages);
-
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeDeletedNotification(items, eventMessages);
-
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new MemberTypeMovingNotification(moveInfo, eventMessages);
-
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new MemberTypeMovedNotification(moveInfo, eventMessages);
-
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MemberTypeChangedNotification(changes, eventMessages);
-
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MemberTypeRefreshedNotification(changes, eventMessages);
-
-        #endregion
-
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            foreach (var typeId in typeIds)
-                MemberService.DeleteMembersOfType(typeId);
-        }
-
-        public string GetDefault()
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using (IEnumerator e = _memberTypeRepository.GetMany(new int[0]).GetEnumerator())
             {
-                scope.ReadLock(ReadLockIds);
-
-                using (var e = _memberTypeRepository.GetMany(new int[0]).GetEnumerator())
+                if (e.MoveNext() == false)
                 {
-                    if (e.MoveNext() == false)
-                        throw new InvalidOperationException("No member types could be resolved");
-                    var first = e.Current.Alias;
-                    var current = true;
-                    while (e.Current.Alias.InvariantEquals("Member") == false && (current = e.MoveNext()))
-                    { }
-                    return current ? e.Current.Alias : first;
+                    throw new InvalidOperationException("No member types could be resolved");
                 }
+
+                var first = e.Current.Alias;
+                var current = true;
+                while (e.Current.Alias.InvariantEquals("Member") == false && (current = e.MoveNext()))
+                {
+                }
+
+                return current ? e.Current.Alias : first;
             }
         }
     }
+
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
+    {
+        foreach (var typeId in typeIds)
+        {
+            MemberService.DeleteMembersOfType(typeId);
+        }
+    }
+
+    #region Notifications
+
+    protected override SavingNotification GetSavingNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeSavingNotification(item, eventMessages);
+
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeSavingNotification(items, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeSavedNotification(item, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeSavedNotification(items, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeDeletingNotification(item, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeDeletingNotification(items, eventMessages);
+
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeDeletedNotification(items, eventMessages);
+
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new MemberTypeMovingNotification(moveInfo, eventMessages);
+
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new MemberTypeMovedNotification(moveInfo, eventMessages);
+
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MemberTypeChangedNotification(changes, eventMessages);
+
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MemberTypeRefreshedNotification(changes, eventMessages);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/MetricsConsentService.cs b/src/Umbraco.Core/Services/MetricsConsentService.cs
index d494dbcf4b..a5309d35f1 100644
--- a/src/Umbraco.Core/Services/MetricsConsentService.cs
+++ b/src/Umbraco.Core/Services/MetricsConsentService.cs
@@ -1,57 +1,80 @@
-using System;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Security;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class MetricsConsentService : IMetricsConsentService
 {
-    public class MetricsConsentService : IMetricsConsentService
-    {
-        internal const string Key = "UmbracoAnalyticsLevel";
+    internal const string Key = "UmbracoAnalyticsLevel";
 
-        private readonly IKeyValueService _keyValueService;
-        private readonly ILogger _logger;
-        private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
+    private readonly IKeyValueService _keyValueService;
+    private readonly ILogger _logger;
+    private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
+    private readonly IUserService _userService;
 
-        // Scheduled for removal in V12
-        [Obsolete("Please use the constructor that takes and ILogger and IBackOfficeSecurity instead")]
-        public MetricsConsentService(IKeyValueService keyValueService)
+    // Scheduled for removal in V12
+    [Obsolete("Please use the constructor that takes an ILogger and IBackOfficeSecurity instead")]
+    public MetricsConsentService(IKeyValueService keyValueService)
         : this(
             keyValueService,
             StaticServiceProvider.Instance.GetRequiredService>(),
-            StaticServiceProvider.Instance.GetRequiredService())
+            StaticServiceProvider.Instance.GetRequiredService(),
+            StaticServiceProvider.Instance.GetRequiredService())
+    {
+    }
+
+    // Scheduled for removal in V12
+    [Obsolete("Please use the constructor that takes an IUserService instead")]
+    public MetricsConsentService(
+        IKeyValueService keyValueService,
+        ILogger logger,
+        IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
+        : this(
+            keyValueService,
+            logger,
+            backOfficeSecurityAccessor,
+            StaticServiceProvider.Instance.GetRequiredService())
+    {
+    }
+
+    public MetricsConsentService(
+        IKeyValueService keyValueService,
+        ILogger logger,
+        IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+        IUserService userService)
+    {
+        _keyValueService = keyValueService;
+        _logger = logger;
+        _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
+        _userService = userService;
+    }
+
+    public TelemetryLevel GetConsentLevel()
+    {
+        var analyticsLevelString = _keyValueService.GetValue(Key);
+
+        if (analyticsLevelString is null ||
+            Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false)
         {
+            return TelemetryLevel.Basic;
         }
 
-        public MetricsConsentService(
-            IKeyValueService keyValueService,
-            ILogger logger,
-            IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
+        return analyticsLevel;
+    }
+
+    public void SetConsentLevel(TelemetryLevel telemetryLevel)
+    {
+        IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
+        if (currentUser is null)
         {
-            _keyValueService = keyValueService;
-            _logger = logger;
-            _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
+            currentUser = _userService.GetUserById(Constants.Security.SuperUserId);
         }
 
-        public TelemetryLevel GetConsentLevel()
-        {
-            var analyticsLevelString = _keyValueService.GetValue(Key);
-
-            if (analyticsLevelString is null || Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false)
-            {
-                return TelemetryLevel.Basic;
-            }
-
-            return analyticsLevel;
-        }
-
-        public void SetConsentLevel(TelemetryLevel telemetryLevel)
-        {
-            var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
-            _logger.LogInformation("Telemetry level set to {telemetryLevel} by {username}", telemetryLevel, currentUser?.Username);
-            _keyValueService.SetValue(Key, telemetryLevel.ToString());
-        }
+        _logger.LogInformation("Telemetry level set to {telemetryLevel} by {username}", telemetryLevel, currentUser?.Username);
+        _keyValueService.SetValue(Key, telemetryLevel.ToString());
     }
 }
diff --git a/src/Umbraco.Core/Services/MoveOperationStatusType.cs b/src/Umbraco.Core/Services/MoveOperationStatusType.cs
index 4de17b2fa5..26e70eb9e0 100644
--- a/src/Umbraco.Core/Services/MoveOperationStatusType.cs
+++ b/src/Umbraco.Core/Services/MoveOperationStatusType.cs
@@ -1,32 +1,30 @@
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     A status type of the result of moving an item
+/// 
+/// 
+///     Anything less than 10 = Success!
+/// 
+public enum MoveOperationStatusType : byte
 {
+    /// 
+    ///     The move was successful.
+    /// 
+    Success = 0,
 
     /// 
-    /// A status type of the result of moving an item
+    ///     The parent being moved to doesn't exist
     /// 
-    /// 
-    /// Anything less than 10 = Success!
-    /// 
-    public enum MoveOperationStatusType : byte
-    {
-        /// 
-        /// The move was successful.
-        /// 
-        Success = 0,
+    FailedParentNotFound = 13,
 
-        /// 
-        /// The parent being moved to doesn't exist
-        /// 
-        FailedParentNotFound = 13,
+    /// 
+    ///     The move action has been cancelled by an event handler
+    /// 
+    FailedCancelledByEvent = 14,
 
-        /// 
-        /// The move action has been cancelled by an event handler
-        /// 
-        FailedCancelledByEvent = 14,
-
-        /// 
-        /// Trying to move an item to an invalid path (i.e. a child of itself)
-        /// 
-        FailedNotAllowedByPath = 15,
-    }
+    /// 
+    ///     Trying to move an item to an invalid path (i.e. a child of itself)
+    /// 
+    FailedNotAllowedByPath = 15,
 }
diff --git a/src/Umbraco.Core/Services/NodeCountService.cs b/src/Umbraco.Core/Services/NodeCountService.cs
index 7298d7f23a..cf7417058e 100644
--- a/src/Umbraco.Core/Services/NodeCountService.cs
+++ b/src/Umbraco.Core/Services/NodeCountService.cs
@@ -1,31 +1,29 @@
-using System;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Infrastructure.Services.Implement
+namespace Umbraco.Cms.Infrastructure.Services.Implement;
+
+public class NodeCountService : INodeCountService
 {
-    public class NodeCountService : INodeCountService
+    private readonly INodeCountRepository _nodeCountRepository;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public NodeCountService(INodeCountRepository nodeCountRepository, ICoreScopeProvider scopeProvider)
     {
-        private readonly INodeCountRepository _nodeCountRepository;
-        private readonly ICoreScopeProvider _scopeProvider;
+        _nodeCountRepository = nodeCountRepository;
+        _scopeProvider = scopeProvider;
+    }
 
-        public NodeCountService(INodeCountRepository nodeCountRepository, ICoreScopeProvider scopeProvider)
-        {
-            _nodeCountRepository = nodeCountRepository;
-            _scopeProvider = scopeProvider;
-        }
+    public int GetNodeCount(Guid nodeType)
+    {
+        using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+        return _nodeCountRepository.GetNodeCount(nodeType);
+    }
 
-        public int GetNodeCount(Guid nodeType)
-        {
-            using var scope = _scopeProvider.CreateCoreScope(autoComplete: true);
-            return _nodeCountRepository.GetNodeCount(nodeType);
-        }
-
-        public int GetMediaCount()
-        {
-            using var scope = _scopeProvider.CreateCoreScope(autoComplete: true);
-            return _nodeCountRepository.GetMediaCount();
-        }
+    public int GetMediaCount()
+    {
+        using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+        return _nodeCountRepository.GetMediaCount();
     }
 }
diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs
index 39aa6a863d..822ba89079 100644
--- a/src/Umbraco.Core/Services/NotificationService.cs
+++ b/src/Umbraco.Core/Services/NotificationService.cs
@@ -1,10 +1,6 @@
-using System;
 using System.Collections.Concurrent;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Text;
-using System.Threading;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 using Umbraco.Cms.Core.Configuration.Models;
@@ -18,541 +14,625 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class NotificationService : INotificationService
 {
-    public class NotificationService : INotificationService
+    // manage notifications
+    // ideally, would need to use IBackgroundTasks - but they are not part of Core!
+    private static readonly object Locker = new();
+
+    private readonly IContentService _contentService;
+    private readonly ContentSettings _contentSettings;
+    private readonly IEmailSender _emailSender;
+    private readonly GlobalSettings _globalSettings;
+    private readonly IIOHelper _ioHelper;
+    private readonly ILocalizationService _localizationService;
+    private readonly ILogger _logger;
+    private readonly INotificationsRepository _notificationsRepository;
+    private readonly ICoreScopeProvider _uowProvider;
+    private readonly IUserService _userService;
+
+    public NotificationService(
+        ICoreScopeProvider provider,
+        IUserService userService,
+        IContentService contentService,
+        ILocalizationService localizationService,
+        ILogger logger,
+        IIOHelper ioHelper,
+        INotificationsRepository notificationsRepository,
+        IOptions globalSettings,
+        IOptions contentSettings,
+        IEmailSender emailSender)
     {
-        private readonly ICoreScopeProvider _uowProvider;
-        private readonly IUserService _userService;
-        private readonly IContentService _contentService;
-        private readonly ILocalizationService _localizationService;
-        private readonly INotificationsRepository _notificationsRepository;
-        private readonly GlobalSettings _globalSettings;
-        private readonly ContentSettings _contentSettings;
-        private readonly IEmailSender _emailSender;
-        private readonly ILogger _logger;
-        private readonly IIOHelper _ioHelper;
+        _notificationsRepository = notificationsRepository;
+        _globalSettings = globalSettings.Value;
+        _contentSettings = contentSettings.Value;
+        _emailSender = emailSender;
+        _uowProvider = provider ?? throw new ArgumentNullException(nameof(provider));
+        _userService = userService ?? throw new ArgumentNullException(nameof(userService));
+        _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
+        _localizationService = localizationService;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        _ioHelper = ioHelper;
+    }
 
-        public NotificationService(ICoreScopeProvider provider, IUserService userService, IContentService contentService, ILocalizationService localizationService,
-            ILogger logger, IIOHelper ioHelper, INotificationsRepository notificationsRepository, IOptions globalSettings, IOptions contentSettings, IEmailSender emailSender)
+    /// 
+    ///     Sends the notifications for the specified user regarding the specified node and action.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public void SendNotifications(
+        IUser operatingUser,
+        IEnumerable entities,
+        string? action,
+        string? actionName,
+        Uri siteUri,
+        Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
+        Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
+    {
+        var entitiesL = entities.ToList();
+
+        // exit if there are no entities
+        if (entitiesL.Count == 0)
         {
-            _notificationsRepository = notificationsRepository;
-            _globalSettings = globalSettings.Value;
-            _contentSettings = contentSettings.Value;
-            _emailSender = emailSender;
-            _uowProvider = provider ?? throw new ArgumentNullException(nameof(provider));
-            _userService = userService ?? throw new ArgumentNullException(nameof(userService));
-            _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
-            _localizationService = localizationService;
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            _ioHelper = ioHelper;
+            return;
         }
 
-        /// 
-        /// Gets the previous version to the latest version of the content item if there is one
-        /// 
-        /// 
-        /// 
-        private IContentBase? GetPreviousVersion(int contentId)
+        // put all entity's paths into a list with the same indices
+        var paths = entitiesL.Select(x =>
+                x.Path.Split(Constants.CharArrays.Comma).Select(s => int.Parse(s, CultureInfo.InvariantCulture))
+                    .ToArray())
+            .ToArray();
+
+        // lazily get versions
+        var prevVersionDictionary = new Dictionary();
+
+        // see notes above
+        var id = Constants.Security.SuperUserId;
+        const int pagesz = 400; // load batches of 400 users
+        do
         {
-            // Regarding this: http://issues.umbraco.org/issue/U4-5180
-            // we know they are descending from the service so we know that newest is first
-            // we are only selecting the top 2 rows since that is all we need
-            var allVersions = _contentService.GetVersionIds(contentId, 2).ToList();
-            var prevVersionIndex = allVersions.Count > 1 ? 1 : 0;
-            return _contentService.GetVersion(allVersions[prevVersionIndex]);
-        }
-
-        /// 
-        /// Sends the notifications for the specified user regarding the specified node and action.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public void SendNotifications(IUser operatingUser, IEnumerable entities, string? action, string? actionName, Uri siteUri,
-            Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
-            Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
-        {
-            var entitiesL = entities.ToList();
-
-            //exit if there are no entities
-            if (entitiesL.Count == 0) return;
-
-            //put all entity's paths into a list with the same indices
-            var paths = entitiesL.Select(x => x.Path.Split(Constants.CharArrays.Comma).Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray()).ToArray();
-
-            // lazily get versions
-            var prevVersionDictionary = new Dictionary();
-
-            // see notes above
-            var id = Cms.Core.Constants.Security.SuperUserId;
-            const int pagesz = 400; // load batches of 400 users
-            do
+            // users are returned ordered by id, notifications are returned ordered by user id
+            var users = _userService.GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
+            var notifications = GetUsersNotifications(users.Select(x => x.Id), action, Enumerable.Empty(), Constants.ObjectTypes.Document)?.ToList();
+            if (notifications is null || notifications.Count == 0)
             {
-                // users are returned ordered by id, notifications are returned ordered by user id
-                var users = _userService.GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
-                var notifications = GetUsersNotifications(users.Select(x => x.Id), action, Enumerable.Empty(), Cms.Core.Constants.ObjectTypes.Document)?.ToList();
-                if (notifications is null || notifications.Count == 0) break;
+                break;
+            }
 
-                var i = 0;
-                foreach (var user in users)
+            var i = 0;
+            foreach (IUser user in users)
+            {
+                // continue if there's no notification for this user
+                if (notifications[i].UserId != user.Id)
                 {
-                    // continue if there's no notification for this user
-                    if (notifications[i].UserId != user.Id) continue; // next user
+                    continue; // next user
+                }
 
-                    for (var j = 0; j < entitiesL.Count; j++)
+                for (var j = 0; j < entitiesL.Count; j++)
+                {
+                    IContent content = entitiesL[j];
+                    var path = paths[j];
+
+                    // test if the notification applies to the path ie to this entity
+                    if (path.Contains(notifications[i].EntityId) == false)
                     {
-                        var content = entitiesL[j];
-                        var path = paths[j];
-
-                        // test if the notification applies to the path ie to this entity
-                        if (path.Contains(notifications[i].EntityId) == false) continue; // next entity
-
-                        if (prevVersionDictionary.ContainsKey(content.Id) == false)
-                        {
-                            prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id);
-                        }
-
-                        // queue notification
-                        var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody);
-                        Enqueue(req);
+                        continue; // next entity
                     }
 
-                    // skip other notifications for this user, essentially this means moving i to the next index of notifications
-                    // for the next user.
-                    do
+                    if (prevVersionDictionary.ContainsKey(content.Id) == false)
                     {
-                        i++;
-                    } while (i < notifications.Count && notifications[i].UserId == user.Id);
-
-                    if (i >= notifications.Count) break; // break if no more notifications
-                }
-
-                // load more users if any
-                id = users.Count == pagesz ? users.Last().Id + 1 : -1;
-
-            } while (id > 0);
-        }
-
-        private IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType)
-        {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetUsersNotifications(userIds, action, nodeIds, objectType);
-            }
-        }
-        /// 
-        /// Gets the notifications for the user
-        /// 
-        /// 
-        /// 
-        public IEnumerable? GetUserNotifications(IUser user)
-        {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetUserNotifications(user);
-            }
-        }
-
-        /// 
-        /// Gets the notifications for the user based on the specified node path
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors)
-        /// 
-        public IEnumerable? GetUserNotifications(IUser? user, string path)
-        {
-            if (user is null)
-            {
-                return null;
-            }
-
-            var userNotifications = GetUserNotifications(user);
-            return FilterUserNotificationsByPath(userNotifications, path);
-        }
-
-        /// 
-        /// Filters a userNotifications collection by a path
-        /// 
-        /// 
-        /// 
-        /// 
-        public IEnumerable? FilterUserNotificationsByPath(IEnumerable? userNotifications, string path)
-        {
-            var pathParts = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
-            return userNotifications?.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList();
-        }
-
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        public IEnumerable? GetEntityNotifications(IEntity entity)
-        {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetEntityNotifications(entity);
-            }
-        }
-
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        public void DeleteNotifications(IEntity entity)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(entity);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Deletes notifications by user
-        /// 
-        /// 
-        public void DeleteNotifications(IUser user)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(user);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Delete notifications by user and entity
-        /// 
-        /// 
-        /// 
-        public void DeleteNotifications(IUser user, IEntity entity)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(user, entity);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Sets the specific notifications for the user and entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This performs a full replace
-        /// 
-        public IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions)
-        {
-            if (user is null)
-            {
-                return null;
-            }
-
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                var notifications = _notificationsRepository.SetNotifications(user, entity, actions);
-                scope.Complete();
-                return notifications;
-            }
-        }
-
-        /// 
-        /// Creates a new notification
-        /// 
-        /// 
-        /// 
-        /// The action letter - note: this is a string for future compatibility
-        /// 
-        public Notification CreateNotification(IUser user, IEntity entity, string action)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                var notification = _notificationsRepository.CreateNotification(user, entity, action);
-                scope.Complete();
-                return notification;
-            }
-        }
-
-        #region private methods
-
-        /// 
-        /// Sends the notification
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The action readable name - currently an action is just a single letter, this is the name associated with the letter 
-        /// 
-        /// Callback to create the mail subject
-        /// Callback to create the mail body
-        private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContent content, IContentBase? oldDoc,
-            string? actionName,
-            Uri siteUri,
-            Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
-            Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
-        {
-            if (performingUser == null) throw new ArgumentNullException("performingUser");
-            if (mailingUser == null) throw new ArgumentNullException("mailingUser");
-            if (content == null) throw new ArgumentNullException("content");
-            if (siteUri == null) throw new ArgumentNullException("siteUri");
-            if (createSubject == null) throw new ArgumentNullException("createSubject");
-            if (createBody == null) throw new ArgumentNullException("createBody");
-
-            // build summary
-            var summary = new StringBuilder();
-
-            if (content.ContentType.VariesByNothing())
-            {
-                if (!_contentSettings.Notifications.DisableHtmlEmail)
-                {
-                    //create the HTML summary for invariant content
-
-                    //list all of the property values like we used to
-                    summary.Append("");
-                    foreach (var p in content.Properties)
-                    {
-                        // TODO: doesn't take into account variants
-
-                        var newText = p.GetValue() != null ? p.GetValue()?.ToString() : "";
-                        var oldText = newText;
-
-                        // check if something was changed and display the changes otherwise display the fields
-                        if (oldDoc?.Properties.Contains(p.PropertyType.Alias) ?? false)
-                        {
-                            var oldProperty = oldDoc.Properties[p.PropertyType.Alias];
-                            oldText = oldProperty?.GetValue() != null ? oldProperty.GetValue()?.ToString() : "";
-
-                            // replace HTML with char equivalent
-                            ReplaceHtmlSymbols(ref oldText);
-                            ReplaceHtmlSymbols(ref newText);
-                        }
-
-                        //show the values
-                        summary.Append("");
-                        summary.Append("");
-                        summary.Append("");
-                        summary.Append("");
-                    }
-                    summary.Append("
"); - summary.Append(p.PropertyType.Name); - summary.Append(""); - summary.Append(newText); - summary.Append("
"); - } - - } - else if (content.ContentType.VariesByCulture()) - { - //it's variant, so detect what cultures have changed - - if (!_contentSettings.Notifications.DisableHtmlEmail) - { - //Create the HTML based summary (ul of culture names) - - var culturesChanged = content.CultureInfos?.Values.Where(x => x.WasDirty()) - .Select(x => x.Culture) - .Select(_localizationService.GetLanguageByIsoCode) - .WhereNotNull() - .Select(x => x.CultureName); - summary.Append("
    "); - if (culturesChanged is not null) - { - foreach (var culture in culturesChanged) - { - summary.Append("
  • "); - summary.Append(culture); - summary.Append("
  • "); - } + prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id); } - summary.Append("
"); + // queue notification + NotificationRequest req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody); + Enqueue(req); } - else + + // skip other notifications for this user, essentially this means moving i to the next index of notifications + // for the next user. + do { - //Create the text based summary (csv of culture names) - - var culturesChanged = string.Join(", ", content.CultureInfos!.Values.Where(x => x.WasDirty()) - .Select(x => x.Culture) - .Select(_localizationService.GetLanguageByIsoCode) - .WhereNotNull() - .Select(x => x.CultureName)); - - summary.Append("'"); - summary.Append(culturesChanged); - summary.Append("'"); + i++; } + while (i < notifications.Count && notifications[i].UserId == user.Id); + + if (i >= notifications.Count) + { + break; // break if no more notifications + } + } + + // load more users if any + id = users.Count == pagesz ? users.Last().Id + 1 : -1; + } + while (id > 0); + } + + /// + /// Gets the notifications for the user + /// + /// + /// + public IEnumerable? GetUserNotifications(IUser user) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true)) + { + return _notificationsRepository.GetUserNotifications(user); + } + } + + /// + /// Gets the notifications for the user based on the specified node path + /// + /// + /// + /// + /// + /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's + /// parent (ancestors) + /// + public IEnumerable? GetUserNotifications(IUser? user, string path) + { + if (user is null) + { + return null; + } + + IEnumerable? userNotifications = GetUserNotifications(user); + return FilterUserNotificationsByPath(userNotifications, path); + } + + /// + /// Deletes notifications by entity + /// + /// + public IEnumerable? GetEntityNotifications(IEntity entity) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true)) + { + return _notificationsRepository.GetEntityNotifications(entity); + } + } + + /// + /// Deletes notifications by entity + /// + /// + public void DeleteNotifications(IEntity entity) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + _notificationsRepository.DeleteNotifications(entity); + scope.Complete(); + } + } + + /// + /// Deletes notifications by user + /// + /// + public void DeleteNotifications(IUser user) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + _notificationsRepository.DeleteNotifications(user); + scope.Complete(); + } + } + + /// + /// Delete notifications by user and entity + /// + /// + /// + public void DeleteNotifications(IUser user, IEntity entity) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + _notificationsRepository.DeleteNotifications(user, entity); + scope.Complete(); + } + } + + /// + /// Sets the specific notifications for the user and entity + /// + /// + /// + /// + /// + /// This performs a full replace + /// + public IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions) + { + if (user is null) + { + return null; + } + + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + IEnumerable notifications = _notificationsRepository.SetNotifications(user, entity, actions); + scope.Complete(); + return notifications; + } + } + + /// + /// Creates a new notification + /// + /// + /// + /// The action letter - note: this is a string for future compatibility + /// + public Notification CreateNotification(IUser user, IEntity entity, string action) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + Notification notification = _notificationsRepository.CreateNotification(user, entity, action); + scope.Complete(); + return notification; + } + } + + /// + /// Filters a userNotifications collection by a path + /// + /// + /// + /// + public IEnumerable? FilterUserNotificationsByPath( + IEnumerable? userNotifications, + string path) + { + var pathParts = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return userNotifications + ?.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); + } + + /// + /// Gets the previous version to the latest version of the content item if there is one + /// + /// + /// + private IContentBase? GetPreviousVersion(int contentId) + { + // Regarding this: http://issues.umbraco.org/issue/U4-5180 + // we know they are descending from the service so we know that newest is first + // we are only selecting the top 2 rows since that is all we need + var allVersions = _contentService.GetVersionIds(contentId, 2).ToList(); + var prevVersionIndex = allVersions.Count > 1 ? 1 : 0; + return _contentService.GetVersion(allVersions[prevVersionIndex]); + } + + private IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true)) + { + return _notificationsRepository.GetUsersNotifications(userIds, action, nodeIds, objectType); + } + } + + /// + /// Replaces the HTML symbols with the character equivalent. + /// + /// The old string. + private static void ReplaceHtmlSymbols(ref string? oldString) + { + if (oldString.IsNullOrWhiteSpace()) + { + return; + } + + oldString = oldString!.Replace(" ", " "); + oldString = oldString.Replace("’", "'"); + oldString = oldString.Replace("&", "&"); + oldString = oldString.Replace("“", "“"); + oldString = oldString.Replace("”", "”"); + oldString = oldString.Replace(""", "\""); + } + + #region private methods + + /// + /// Sends the notification + /// + /// + /// + /// + /// + /// + /// The action readable name - currently an action is just a single letter, this is the name + /// associated with the letter + /// + /// + /// Callback to create the mail subject + /// Callback to create the mail body + private NotificationRequest CreateNotificationRequest( + IUser performingUser, + IUser mailingUser, + IContent content, + IContentBase? oldDoc, + string? actionName, + Uri siteUri, + Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject, + Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody) + { + if (performingUser == null) + { + throw new ArgumentNullException("performingUser"); + } + + if (mailingUser == null) + { + throw new ArgumentNullException("mailingUser"); + } + + if (content == null) + { + throw new ArgumentNullException("content"); + } + + if (siteUri == null) + { + throw new ArgumentNullException("siteUri"); + } + + if (createSubject == null) + { + throw new ArgumentNullException("createSubject"); + } + + if (createBody == null) + { + throw new ArgumentNullException("createBody"); + } + + // build summary + var summary = new StringBuilder(); + + if (content.ContentType.VariesByNothing()) + { + if (!_contentSettings.Notifications.DisableHtmlEmail) + { + // create the HTML summary for invariant content + + // list all of the property values like we used to + summary.Append(""); + foreach (IProperty p in content.Properties) + { + // TODO: doesn't take into account variants + var newText = p.GetValue() != null ? p.GetValue()?.ToString() : string.Empty; + var oldText = newText; + + // check if something was changed and display the changes otherwise display the fields + if (oldDoc?.Properties.Contains(p.PropertyType.Alias) ?? false) + { + IProperty? oldProperty = oldDoc.Properties[p.PropertyType.Alias]; + oldText = oldProperty?.GetValue() != null ? oldProperty.GetValue()?.ToString() : string.Empty; + + // replace HTML with char equivalent + ReplaceHtmlSymbols(ref oldText); + ReplaceHtmlSymbols(ref newText); + } + + // show the values + summary.Append(""); + summary.Append( + ""); + summary.Append(""); + summary.Append(""); + } + + summary.Append("
"); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(newText); + summary.Append("
"); + } + } + else if (content.ContentType.VariesByCulture()) + { + // it's variant, so detect what cultures have changed + if (!_contentSettings.Notifications.DisableHtmlEmail) + { + // Create the HTML based summary (ul of culture names) + IEnumerable? culturesChanged = content.CultureInfos?.Values.Where(x => x.WasDirty()) + .Select(x => x.Culture) + .Select(_localizationService.GetLanguageByIsoCode) + .WhereNotNull() + .Select(x => x.CultureName); + summary.Append("
    "); + if (culturesChanged is not null) + { + foreach (var culture in culturesChanged) + { + summary.Append("
  • "); + summary.Append(culture); + summary.Append("
  • "); + } + } + + summary.Append("
"); } else { - //not supported yet... - throw new NotSupportedException(); + // Create the text based summary (csv of culture names) + var culturesChanged = string.Join(", ", content.CultureInfos!.Values.Where(x => x.WasDirty()) + .Select(x => x.Culture) + .Select(_localizationService.GetLanguageByIsoCode) + .WhereNotNull() + .Select(x => x.CultureName)); + + summary.Append("'"); + summary.Append(culturesChanged); + summary.Append("'"); } + } + else + { + // not supported yet... + throw new NotSupportedException(); + } - var protocol = _globalSettings.UseHttps ? "https" : "http"; + var protocol = _globalSettings.UseHttps ? "https" : "http"; - var subjectVars = new NotificationEmailSubjectParams( - string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), - actionName, - content.Name); + var subjectVars = new NotificationEmailSubjectParams( + string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), + actionName, + content.Name); - var bodyVars = new NotificationEmailBodyParams( - mailingUser.Name, - actionName, - content.Name, - content.Id.ToString(CultureInfo.InvariantCulture), - string.Format("{2}://{0}/{1}", - string.Concat(siteUri.Authority), - // TODO: RE-enable this so we can have a nice URL - /*umbraco.library.NiceUrl(documentObject.Id))*/ - string.Concat(content.Id, ".aspx"), - protocol), - performingUser.Name, - string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), - summary.ToString()); + var bodyVars = new NotificationEmailBodyParams( + mailingUser.Name, + actionName, + content.Name, + content.Id.ToString(CultureInfo.InvariantCulture), + string.Format( + "{2}://{0}/{1}", + string.Concat(siteUri.Authority), - var fromMail = _contentSettings.Notifications.Email ?? _globalSettings.Smtp?.From; + // TODO: RE-enable this so we can have a nice URL + /*umbraco.library.NiceUrl(documentObject.Id))*/ + string.Concat(content.Id, ".aspx"), + protocol), + performingUser.Name, + string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), + summary.ToString()); - var subject = createSubject((mailingUser, subjectVars)); - var body = ""; - var isBodyHtml = false; + var fromMail = _contentSettings.Notifications.Email ?? _globalSettings.Smtp?.From; - if (_contentSettings.Notifications.DisableHtmlEmail) - { - body = createBody((user: mailingUser, body: bodyVars, false)); - } - else - { - isBodyHtml = true; - body = - string.Concat(@" + var subject = createSubject((mailingUser, subjectVars)); + var body = string.Empty; + var isBodyHtml = false; + + if (_contentSettings.Notifications.DisableHtmlEmail) + { + body = createBody((user: mailingUser, body: bodyVars, false)); + } + else + { + isBodyHtml = true; + body = + string.Concat( + @" -", createBody((user: mailingUser, body: bodyVars, true))); +", + createBody((user: mailingUser, body: bodyVars, true))); + } + + // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here + // adding the server name to make sure we don't replace external links + if (_globalSettings.UseHttps && string.IsNullOrEmpty(body) == false) + { + var serverName = siteUri.Host; + body = body.Replace( + $"http://{serverName}", + $"https://{serverName}"); + } + + // create the mail message + var mail = new EmailMessage(fromMail, mailingUser.Email, subject, body, isBodyHtml); + + return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); + } + + private string ReplaceLinks(string text, Uri siteUri) + { + var sb = new StringBuilder(_globalSettings.UseHttps ? "https://" : "http://"); + sb.Append(siteUri.Authority); + sb.Append("/"); + var domain = sb.ToString(); + text = text.Replace("href=\"/", "href=\"" + domain); + text = text.Replace("src=\"/", "src=\"" + domain); + return text; + } + + private static readonly BlockingCollection Queue = new(); + private static volatile bool _running; + + private void Enqueue(NotificationRequest notification) + { + Queue.Add(notification); + if (_running) + { + return; + } + + lock (Locker) + { + if (_running) + { + return; } - // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here - // adding the server name to make sure we don't replace external links - if (_globalSettings.UseHttps && string.IsNullOrEmpty(body) == false) + Process(Queue); + _running = true; + } + } + + private void Process(BlockingCollection notificationRequests) => + ThreadPool.QueueUserWorkItem(state => + { + _logger.LogDebug("Begin processing notifications."); + while (true) { - var serverName = siteUri.Host; - body = body.Replace( - $"http://{serverName}", - $"https://{serverName}"); - } - - // create the mail message - var mail = new EmailMessage(fromMail, mailingUser.Email, subject, body, isBodyHtml); - - return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); - } - - private string ReplaceLinks(string text, Uri siteUri) - { - var sb = new StringBuilder(_globalSettings.UseHttps ? "https://" : "http://"); - sb.Append(siteUri.Authority); - sb.Append("/"); - var domain = sb.ToString(); - text = text.Replace("href=\"/", "href=\"" + domain); - text = text.Replace("src=\"/", "src=\"" + domain); - return text; - } - - /// - /// Replaces the HTML symbols with the character equivalent. - /// - /// The old string. - private static void ReplaceHtmlSymbols(ref string? oldString) - { - if (oldString.IsNullOrWhiteSpace()) return; - oldString = oldString!.Replace(" ", " "); - oldString = oldString.Replace("’", "'"); - oldString = oldString.Replace("&", "&"); - oldString = oldString.Replace("“", "“"); - oldString = oldString.Replace("”", "”"); - oldString = oldString.Replace(""", "\""); - } - - // manage notifications - // ideally, would need to use IBackgroundTasks - but they are not part of Core! - - private static readonly object Locker = new object(); - private static readonly BlockingCollection Queue = new BlockingCollection(); - private static volatile bool _running; - - private void Enqueue(NotificationRequest notification) - { - Queue.Add(notification); - if (_running) return; - lock (Locker) - { - if (_running) return; - Process(Queue); - _running = true; - } - } - - private class NotificationRequest - { - public NotificationRequest(EmailMessage mail, string? action, string? userName, string? email) - { - Mail = mail; - Action = action; - UserName = userName; - Email = email; - } - - public EmailMessage Mail { get; } - - public string? Action { get; } - - public string? UserName { get; } - - public string? Email { get; } - } - - private void Process(BlockingCollection notificationRequests) - { - ThreadPool.QueueUserWorkItem(state => - { - _logger.LogDebug("Begin processing notifications."); - while (true) + // stay on for 8s + while (notificationRequests.TryTake(out NotificationRequest? request, 8 * 1000)) { - NotificationRequest? request; - while (notificationRequests.TryTake(out request, 8 * 1000)) // stay on for 8s + try { - try - { - _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter().GetResult(); - _logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred sending notification"); - } + _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter() + .GetResult(); + _logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email); } - lock (Locker) + catch (Exception ex) { - if (notificationRequests.Count > 0) continue; // last chance - _running = false; // going down - break; + _logger.LogError(ex, "An error occurred sending notification"); } } - _logger.LogDebug("Done processing notifications."); - }); + lock (Locker) + { + if (notificationRequests.Count > 0) + { + continue; // last chance + } + + _running = false; // going down + break; + } + } + + _logger.LogDebug("Done processing notifications."); + }); + + private class NotificationRequest + { + public NotificationRequest(EmailMessage mail, string? action, string? userName, string? email) + { + Mail = mail; + Action = action; + UserName = userName; + Email = email; } - #endregion + public EmailMessage Mail { get; } + + public string? Action { get; } + + public string? UserName { get; } + + public string? Email { get; } } + + #endregion } diff --git a/src/Umbraco.Core/Services/OperationResult.cs b/src/Umbraco.Core/Services/OperationResult.cs index a69dc6ee12..919077919c 100644 --- a/src/Umbraco.Core/Services/OperationResult.cs +++ b/src/Umbraco.Core/Services/OperationResult.cs @@ -1,246 +1,246 @@ -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +// TODO: no need for Attempt - the operation result SHOULD KNOW if it's a success or a failure! +// but then each WhateverResultType must + +/// +/// Represents the result of a service operation. +/// +/// The type of the result type. +/// +/// Type must be an enumeration, and its +/// underlying type must be byte. Values indicating success should be in the 0-127 +/// range, while values indicating failure should be in the 128-255 range. See +/// for a base implementation. +/// +public class OperationResult + where TResultType : struct { - // TODO: no need for Attempt - the operation result SHOULD KNOW if it's a success or a failure! - // but then each WhateverResultType must + static OperationResult() + { + // ensure that TResultType is an enum and the underlying type is byte + // so we can safely cast in Success and test against 128 for failures + Type type = typeof(TResultType); + if (type.IsEnum == false) + { + throw new InvalidOperationException($"Type {type} is not an enum."); + } + + if (Enum.GetUnderlyingType(type) != typeof(byte)) + { + throw new InvalidOperationException($"Enum {type} underlying type is not ."); + } + } /// - /// Represents the result of a service operation. + /// Initializes a new instance of the class. /// - /// The type of the result type. - /// Type must be an enumeration, and its - /// underlying type must be byte. Values indicating success should be in the 0-127 - /// range, while values indicating failure should be in the 128-255 range. See - /// for a base implementation. - public class OperationResult - where TResultType : struct + public OperationResult(TResultType result, EventMessages? eventMessages) { - /// - /// Initializes a new instance of the class. - /// - public OperationResult(TResultType result, EventMessages? eventMessages) - { - Result = result; - EventMessages = eventMessages; - } + Result = result; + EventMessages = eventMessages; + } - static OperationResult() - { - // ensure that TResultType is an enum and the underlying type is byte - // so we can safely cast in Success and test against 128 for failures - var type = typeof(TResultType); - if (type.IsEnum == false) - throw new InvalidOperationException($"Type {type} is not an enum."); - if (Enum.GetUnderlyingType(type) != typeof (byte)) - throw new InvalidOperationException($"Enum {type} underlying type is not ."); - } + /// + /// Gets a value indicating whether the operation was successful. + /// + public bool Success => ((byte)(object)Result & 128) == 0; // we *know* it's a byte - /// - /// Gets a value indicating whether the operation was successful. - /// - public bool Success => ((byte) (object) Result & 128) == 0; // we *know* it's a byte + /// + /// Gets the result of the operation. + /// + public TResultType Result { get; } - /// - /// Gets the result of the operation. - /// - public TResultType Result { get; } + /// + /// Gets the event messages produced by the operation. + /// + public EventMessages? EventMessages { get; } +} - /// - /// Gets the event messages produced by the operation. - /// - public EventMessages? EventMessages { get; } +/// +/// +/// Represents the result of a service operation for a given entity. +/// +/// The type of the result type. +/// The type of the entity. +/// +/// Type must be an enumeration, and its +/// underlying type must be byte. Values indicating success should be in the 0-127 +/// range, while values indicating failure should be in the 128-255 range. See +/// for a base implementation. +/// +public class OperationResult : OperationResult + where TResultType : struct +{ + /// + /// + /// Initializes a new instance of the class. + /// + /// The status of the operation. + /// Event messages produced by the operation. + public OperationResult(TResultType result, EventMessages eventMessages) + : base(result, eventMessages) + { } /// /// - /// Represents the result of a service operation for a given entity. + /// Initializes a new instance of the class. /// - /// The type of the result type. - /// The type of the entity. - /// Type must be an enumeration, and its - /// underlying type must be byte. Values indicating success should be in the 0-127 - /// range, while values indicating failure should be in the 128-255 range. See - /// for a base implementation. - public class OperationResult : OperationResult - where TResultType : struct - { - /// - /// - /// Initializes a new instance of the class. - /// - /// The status of the operation. - /// Event messages produced by the operation. - public OperationResult(TResultType result, EventMessages eventMessages) - : base(result, eventMessages) - { } + public OperationResult(TResultType result, EventMessages? eventMessages, TEntity? entity) + : base(result, eventMessages) => + Entity = entity; - /// - /// - /// Initializes a new instance of the class. - /// - public OperationResult(TResultType result, EventMessages? eventMessages, TEntity? entity) - : base(result, eventMessages) - { - Entity = entity; - } - - /// - /// Gets the entity. - /// - public TEntity? Entity { get; } - } + /// + /// Gets the entity. + /// + public TEntity? Entity { get; } +} +/// +/// +/// Represents the default operation result. +/// +public class OperationResult : OperationResult +{ /// /// - /// Represents the default operation result. + /// Initializes a new instance of the class with a status and event messages. /// - public class OperationResult : OperationResult + /// The status of the operation. + /// Event messages produced by the operation. + public OperationResult(OperationResultType result, EventMessages eventMessages) + : base(result, eventMessages) + { + } + + public static OperationResult Succeed(EventMessages eventMessages) => + new OperationResult(OperationResultType.Success, eventMessages); + + public static OperationResult Cancel(EventMessages eventMessages) => + new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages); + + // TODO: this exists to support services that still return Attempt + // these services should directly return an OperationResult, and then this static class should be deleted + public static class Attempt { - /// /// - /// Initializes a new instance of the class with a status and event messages. + /// Creates a successful operation attempt. /// - /// The status of the operation. - /// Event messages produced by the operation. - public OperationResult(OperationResultType result, EventMessages eventMessages) - : base(result, eventMessages) - { } + /// The event messages produced by the operation. + /// A new attempt instance. + public static Attempt Succeed(EventMessages eventMessages) => + Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages)); - public static OperationResult Succeed(EventMessages eventMessages) + public static Attempt?> + Succeed(EventMessages eventMessages) => Core.Attempt.Succeed( + new OperationResult(OperationResultType.Success, eventMessages)); + + public static Attempt?> + Succeed(EventMessages eventMessages, TValue value) => Core.Attempt.Succeed( + new OperationResult(OperationResultType.Success, eventMessages, value)); + + public static Attempt?> Succeed( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Succeed(new OperationResult(statusType, eventMessages)); + + public static Attempt?> Succeed( + TStatusType statusType, EventMessages eventMessages, TValue value) + where TStatusType : struct => + Core.Attempt.Succeed(new OperationResult(statusType, eventMessages, value)); + + /// + /// Creates a successful operation attempt indicating that nothing was done. + /// + /// The event messages produced by the operation. + /// A new attempt instance. + public static Attempt NoOperation(EventMessages eventMessages) => + Core.Attempt.Succeed(new OperationResult(OperationResultType.NoOperation, eventMessages)); + + /// + /// Creates a failed operation attempt indicating that the operation has been cancelled. + /// + /// The event messages produced by the operation. + /// A new attempt instance. + public static Attempt Cancel(EventMessages eventMessages) => + Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages)); + + public static Attempt?> + Cancel(EventMessages eventMessages) => Core.Attempt.Fail( + new OperationResult( + OperationResultType.FailedCancelledByEvent, + eventMessages)); + + public static Attempt?> + Cancel(EventMessages eventMessages, TValue value) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages, value)); + + /// + /// Creates a failed operation attempt indicating that an exception was thrown during the operation. + /// + /// The event messages produced by the operation. + /// The exception that caused the operation to fail. + /// A new attempt instance. + public static Attempt Fail(EventMessages eventMessages, Exception exception) { - return new OperationResult(OperationResultType.Success, eventMessages); + eventMessages.Add(new EventMessage(string.Empty, exception.Message, EventMessageType.Error)); + return Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), + exception); } - public static OperationResult Cancel(EventMessages eventMessages) - { - return new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages); - } + public static Attempt?> + Fail(EventMessages eventMessages, Exception exception) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), + exception); - // TODO: this exists to support services that still return Attempt - // these services should directly return an OperationResult, and then this static class should be deleted - public static class Attempt - { - /// - /// Creates a successful operation attempt. - /// - /// The event messages produced by the operation. - /// A new attempt instance. - public static Attempt Succeed(EventMessages eventMessages) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - public static Attempt?> Succeed(EventMessages eventMessages) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - public static Attempt?> Succeed(EventMessages eventMessages, TValue value) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages, value)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - public static Attempt?> Succeed(TStatusType statusType, EventMessages eventMessages) - where TStatusType : struct - { - return Core.Attempt.Succeed(new OperationResult(statusType, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + TValue value) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value)); - public static Attempt?> Succeed(TStatusType statusType, EventMessages eventMessages, TValue value) - where TStatusType : struct - { - return Core.Attempt.Succeed(new OperationResult(statusType, eventMessages, value)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - /// - /// Creates a successful operation attempt indicating that nothing was done. - /// - /// The event messages produced by the operation. - /// A new attempt instance. - public static Attempt NoOperation(EventMessages eventMessages) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.NoOperation, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + TValue value, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value), exception); - /// - /// Creates a failed operation attempt indicating that the operation has been cancelled. - /// - /// The event messages produced by the operation. - /// A new attempt instance. - public static Attempt Cancel(EventMessages eventMessages) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages)); - } - - public static Attempt?> Cancel(EventMessages eventMessages) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages)); - } - - public static Attempt?> Cancel(EventMessages eventMessages, TValue value) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages, value)); - } - - /// - /// Creates a failed operation attempt indicating that an exception was thrown during the operation. - /// - /// The event messages produced by the operation. - /// The exception that caused the operation to fail. - /// A new attempt instance. - public static Attempt Fail(EventMessages eventMessages, Exception exception) - { - eventMessages.Add(new EventMessage("", exception.Message, EventMessageType.Error)); - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), exception); - } - - public static Attempt?> Fail(EventMessages eventMessages, Exception exception) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), exception); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, Exception exception) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, TValue value) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value)); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, Exception exception) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, TValue value, Exception exception) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value), exception); - } - - public static Attempt?> Cannot(EventMessages eventMessages) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, eventMessages)); - } - } + public static Attempt?> + Cannot(EventMessages eventMessages) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedCannot, eventMessages)); } } diff --git a/src/Umbraco.Core/Services/OperationResultType.cs b/src/Umbraco.Core/Services/OperationResultType.cs index 15b332e43c..c87b70c2a2 100644 --- a/src/Umbraco.Core/Services/OperationResultType.cs +++ b/src/Umbraco.Core/Services/OperationResultType.cs @@ -1,45 +1,44 @@ -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// A value indicating the result of an operation. +/// +public enum OperationResultType : byte { + // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare + // every failure codes as >128 - see OperationResult and OperationResultType for details. + /// - /// A value indicating the result of an operation. + /// The operation was successful. /// - public enum OperationResultType : byte - { - // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare - // every failure codes as >128 - see OperationResult and OperationResultType for details. + Success = 0, - /// - /// The operation was successful. - /// - Success = 0, + /// + /// The operation failed. + /// + /// All values above this value indicate a failure. + Failed = 128, - /// - /// The operation failed. - /// - /// All values above this value indicate a failure. - Failed = 128, + /// + /// The operation could not complete because of invalid preconditions (eg creating a reference + /// to an item that does not exist). + /// + FailedCannot = Failed | 2, - /// - /// The operation could not complete because of invalid preconditions (eg creating a reference - /// to an item that does not exist). - /// - FailedCannot = Failed | 2, + /// + /// The operation has been cancelled by an event handler. + /// + FailedCancelledByEvent = Failed | 4, - /// - /// The operation has been cancelled by an event handler. - /// - FailedCancelledByEvent = Failed | 4, + /// + /// The operation could not complete due to an exception. + /// + FailedExceptionThrown = Failed | 5, - /// - /// The operation could not complete due to an exception. - /// - FailedExceptionThrown = Failed | 5, + /// + /// No operation has been executed because it was not needed (eg deleting an item that doesn't exist). + /// + NoOperation = Failed | 6, // TODO: shouldn't it be a success? - /// - /// No operation has been executed because it was not needed (eg deleting an item that doesn't exist). - /// - NoOperation = Failed | 6, // TODO: shouldn't it be a success? - - // TODO: In the future, we might need to add more operations statuses, potentially like 'FailedByPermissions', etc... - } + // TODO: In the future, we might need to add more operations statuses, potentially like 'FailedByPermissions', etc... } diff --git a/src/Umbraco.Core/Services/Ordering.cs b/src/Umbraco.Core/Services/Ordering.cs index 513654428b..39c89e5c4a 100644 --- a/src/Umbraco.Core/Services/Ordering.cs +++ b/src/Umbraco.Core/Services/Ordering.cs @@ -1,81 +1,92 @@ using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents ordering information. +/// +public class Ordering { + private static readonly Ordering DefaultOrdering = new(null); + /// - /// Represents ordering information. + /// Initializes a new instance of the class. /// - public class Ordering + /// The name of the ordering field. + /// The ordering direction. + /// The (ISO) culture to consider when sorting multi-lingual fields. + /// A value indicating whether the ordering field is a custom user property. + /// + /// + /// The can be null, meaning: not sorting. If it is the empty string, it becomes + /// null. + /// + /// + /// The can be the empty string, meaning: invariant. If it is null, it becomes the + /// empty string. + /// + /// + public Ordering(string? orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) { - private static readonly Ordering DefaultOrdering = new Ordering(null); - - /// - /// Initializes a new instance of the class. - /// - /// The name of the ordering field. - /// The ordering direction. - /// The (ISO) culture to consider when sorting multi-lingual fields. - /// A value indicating whether the ordering field is a custom user property. - /// - /// The can be null, meaning: not sorting. If it is the empty string, it becomes null. - /// The can be the empty string, meaning: invariant. If it is null, it becomes the empty string. - /// - public Ordering(string? orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) - { - OrderBy = orderBy.IfNullOrWhiteSpace(null); // empty is null and means, not sorting - Direction = direction; - Culture = culture.IfNullOrWhiteSpace(string.Empty); // empty is "" and means, invariant - IsCustomField = isCustomField; - } - - /// - /// Creates a new instance of the class. - /// - /// The name of the ordering field. - /// The ordering direction. - /// The (ISO) culture to consider when sorting multi-lingual fields. - /// A value indicating whether the ordering field is a custom user property. - /// - /// The can be null, meaning: not sorting. If it is the empty string, it becomes null. - /// The can be the empty string, meaning: invariant. If it is null, it becomes the empty string. - /// - public static Ordering By(string orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) - => new Ordering(orderBy, direction, culture, isCustomField); - - /// - /// Gets the default instance. - /// - public static Ordering ByDefault() - => DefaultOrdering; - - /// - /// Gets the name of the ordering field. - /// - public string? OrderBy { get; } - - /// - /// Gets the ordering direction. - /// - public Direction Direction { get; } - - /// - /// Gets (ISO) culture to consider when sorting multi-lingual fields. - /// - public string? Culture { get; } - - /// - /// Gets a value indicating whether the ordering field is a custom user property. - /// - public bool IsCustomField { get; } - - /// - /// Gets a value indicating whether this ordering is the default ordering. - /// - public bool IsEmpty => this == DefaultOrdering || OrderBy == null; - - /// - /// Gets a value indicating whether the culture of this ordering is invariant. - /// - public bool IsInvariant => this == DefaultOrdering || Culture == string.Empty; + OrderBy = orderBy.IfNullOrWhiteSpace(null); // empty is null and means, not sorting + Direction = direction; + Culture = culture.IfNullOrWhiteSpace(string.Empty); // empty is "" and means, invariant + IsCustomField = isCustomField; } + + /// + /// Gets the name of the ordering field. + /// + public string? OrderBy { get; } + + /// + /// Gets the ordering direction. + /// + public Direction Direction { get; } + + /// + /// Gets (ISO) culture to consider when sorting multi-lingual fields. + /// + public string? Culture { get; } + + /// + /// Gets a value indicating whether the ordering field is a custom user property. + /// + public bool IsCustomField { get; } + + /// + /// Gets a value indicating whether this ordering is the default ordering. + /// + public bool IsEmpty => this == DefaultOrdering || OrderBy == null; + + /// + /// Gets a value indicating whether the culture of this ordering is invariant. + /// + public bool IsInvariant => this == DefaultOrdering || Culture == string.Empty; + + /// + /// Creates a new instance of the class. + /// + /// The name of the ordering field. + /// The ordering direction. + /// The (ISO) culture to consider when sorting multi-lingual fields. + /// A value indicating whether the ordering field is a custom user property. + /// + /// + /// The can be null, meaning: not sorting. If it is the empty string, it becomes + /// null. + /// + /// + /// The can be the empty string, meaning: invariant. If it is null, it becomes the + /// empty string. + /// + /// + public static Ordering By(string orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) + => new(orderBy, direction, culture, isCustomField); + + /// + /// Gets the default instance. + /// + public static Ordering ByDefault() + => DefaultOrdering; } diff --git a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs index 9a368dab7e..39751dad61 100644 --- a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs +++ b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs @@ -1,26 +1,29 @@ -using System; +namespace Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Services +/// +/// Defines a result object for the +/// operation. +/// +public class ProcessInstructionsResult { - /// - /// Defines a result object for the operation. - /// - public class ProcessInstructionsResult + private ProcessInstructionsResult() { - private ProcessInstructionsResult() + } + + public int NumberOfInstructionsProcessed { get; private set; } + + public int LastId { get; private set; } + + public bool InstructionsWerePruned { get; private set; } + + public static ProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) => + new() { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; + + public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => + new() { - } - - public int NumberOfInstructionsProcessed { get; private set; } - - public int LastId { get; private set; } - - public bool InstructionsWerePruned { get; private set; } - - public static ProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) => - new ProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; - - public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => - new ProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId, InstructionsWerePruned = true }; - }; + NumberOfInstructionsProcessed = numberOfInstructionsProcessed, + LastId = lastId, + InstructionsWerePruned = true, + }; } diff --git a/src/Umbraco.Core/Services/PropertyTypeUsageService.cs b/src/Umbraco.Core/Services/PropertyTypeUsageService.cs new file mode 100644 index 0000000000..4e6c273a91 --- /dev/null +++ b/src/Umbraco.Core/Services/PropertyTypeUsageService.cs @@ -0,0 +1,25 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services; + +public class PropertyTypeUsageService : IPropertyTypeUsageService +{ + private readonly IPropertyTypeUsageRepository _propertyTypeUsageRepository; + private readonly ICoreScopeProvider _scopeProvider; + + public PropertyTypeUsageService( + IPropertyTypeUsageRepository propertyTypeUsageRepository, + ICoreScopeProvider scopeProvider) + { + _propertyTypeUsageRepository = propertyTypeUsageRepository; + _scopeProvider = scopeProvider; + } + + /// + public bool HasSavedPropertyValues(string propertyTypeAlias) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + return _propertyTypeUsageRepository.HasSavedPropertyValues(propertyTypeAlias); + } +} diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index c5a4312776..fc42b13232 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -1,198 +1,218 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; +using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class PropertyValidationService : IPropertyValidationService { - public class PropertyValidationService : IPropertyValidationService + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ILocalizedTextService _textService; + private readonly IValueEditorCache _valueEditorCache; + + public PropertyValidationService( + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + ILocalizedTextService textService, + IValueEditorCache valueEditorCache) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly ILocalizedTextService _textService; - private readonly IValueEditorCache _valueEditorCache; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _textService = textService; + _valueEditorCache = valueEditorCache; + } - public PropertyValidationService( - PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, - ILocalizedTextService textService, - IValueEditorCache valueEditorCache) + /// + public IEnumerable ValidatePropertyValue( + IPropertyType propertyType, + object? postedValue) + { + if (propertyType is null) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _textService = textService; - _valueEditorCache = valueEditorCache; + throw new ArgumentNullException(nameof(propertyType)); } - /// - public IEnumerable ValidatePropertyValue( - IPropertyType propertyType, - object? postedValue) + IDataType? dataType = _dataTypeService.GetDataType(propertyType.DataTypeId); + if (dataType == null) { - if (propertyType is null) throw new ArgumentNullException(nameof(propertyType)); - var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId); - if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId); - - var editor = _propertyEditors[propertyType.PropertyEditorAlias]; - if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias); - - return ValidatePropertyValue(editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage); + throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId); } - /// - public IEnumerable ValidatePropertyValue( - IDataEditor editor, - IDataType dataType, - object? postedValue, - bool isRequired, - string? validationRegExp, - string? isRequiredMessage, - string? validationRegExpMessage) + IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) { - // Retrieve default messages used for required and regex validatation. We'll replace these - // if set with custom ones if they've been provided for a given property. - var requiredDefaultMessages = new[] - { - _textService.Localize("validation", "invalidNull"), - _textService.Localize("validation", "invalidEmpty") - }; - var formatDefaultMessages = new[] - { - _textService.Localize("validation", "invalidPattern"), - }; + throw new InvalidOperationException("No property editor found by alias " + + propertyType.PropertyEditorAlias); + } - IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType); - foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp)) + return ValidatePropertyValue(editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage); + } + + /// + public IEnumerable ValidatePropertyValue( + IDataEditor editor, + IDataType dataType, + object? postedValue, + bool isRequired, + string? validationRegExp, + string? isRequiredMessage, + string? validationRegExpMessage) + { + // Retrieve default messages used for required and regex validatation. We'll replace these + // if set with custom ones if they've been provided for a given property. + var requiredDefaultMessages = new[] + { + _textService.Localize("validation", "invalidNull"), _textService.Localize("validation", "invalidEmpty"), + }; + var formatDefaultMessages = new[] { _textService.Localize("validation", "invalidPattern") }; + + IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType); + foreach (ValidationResult validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp)) + { + // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). + if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && + requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) { - // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). - if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) - { - validationResult.ErrorMessage = isRequiredMessage; - } - if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) - { - validationResult.ErrorMessage = validationRegExpMessage; - } - yield return validationResult; - } - } - - /// - public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact) - { - // select invalid properties - invalidProperties = content.Properties.Where(x => - { - var propertyTypeVaries = x.PropertyType.VariesByCulture(); - - if (impact is null) - { - return false; - } - // impacts invariant = validate invariant property, invariant culture - if (impact.ImpactsOnlyInvariantCulture) - return !(propertyTypeVaries || IsPropertyValid(x, null)); - - // impacts all = validate property, all cultures (incl. invariant) - if (impact.ImpactsAllCultures) - return !IsPropertyValid(x); - - // impacts explicit culture = validate variant property, explicit culture - if (propertyTypeVaries) - return !IsPropertyValid(x, impact.Culture); - - // and, for explicit culture, we may also have to validate invariant property, invariant culture - // if either - // - it is impacted (default culture), or - // - there is no published version of the content - maybe non-default culture, but no published version - - var alsoInvariant = impact.ImpactsAlsoInvariantProperties || !content.Published; - return alsoInvariant && !IsPropertyValid(x, null); - - }).ToArray(); - - return invalidProperties.Length == 0; - } - - /// - public bool IsPropertyValid(IProperty property, string? culture = "*", string? segment = "*") - { - //NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there. - // The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing. - - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - IPropertyValue? pvalue = null; - - // if validating invariant/neutral, and it is supported, validate - // (including ensuring that the value exists, if mandatory) - if ((culture == null || culture == "*") && (segment == null || segment == "*") && property.PropertyType.SupportsVariation(null, null)) - { - // validate pvalue (which is the invariant value) - pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null); - if (!IsValidPropertyValue(property, pvalue?.EditedValue)) - return false; + validationResult.ErrorMessage = isRequiredMessage; } - // if validating only invariant/neutral, we are good - if (culture == null && segment == null) - return true; - - // if nothing else to validate, we are good - if ((culture == null || culture == "*") && (segment == null || segment == "*") && !property.PropertyType.VariesByCulture()) - return true; - - // for anything else, validate the existing values (including mandatory), - // but we cannot validate mandatory globally (we don't know the possible cultures and segments) - - // validate vvalues (which are the variant values) - - // if we don't have vvalues (property.Values is empty or only contains pvalue), validate null - if (property.Values.Count == (pvalue == null ? 0 : 1)) - return culture == "*" || IsValidPropertyValue(property, null); - - // else validate vvalues (but don't revalidate pvalue) - var pvalues = property.Values.Where(x => - x != pvalue && // don't revalidate pvalue - property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok - (culture == "*" || (x.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Segment?.InvariantEquals(segment) ?? false))) // the segment matches - .ToList(); - - return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); - } - - /// - /// Boolean indicating whether the passed in value is valid - /// - /// - /// - /// True is property value is valid, otherwise false - private bool IsValidPropertyValue(IProperty property, object? value) - { - return IsPropertyValueValid(property.PropertyType, value); - } - - /// - /// Determines whether a value is valid for this property type. - /// - private bool IsPropertyValueValid(IPropertyType propertyType, object? value) - { - var editor = _propertyEditors[propertyType.PropertyEditorAlias]; - if (editor == null) + if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && + formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) { - // nothing much we can do validation wise if the property editor has been removed. - // the property will be displayed as a label, so flagging it as invalid would be pointless. - return true; + validationResult.ErrorMessage = validationRegExpMessage; } - var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.Configuration; - var valueEditor = editor.GetValueEditor(configuration); - return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); + + yield return validationResult; } } + + /// + public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact) + { + // select invalid properties + invalidProperties = content.Properties.Where(x => + { + var propertyTypeVaries = x.PropertyType.VariesByCulture(); + + if (impact is null) + { + return false; + } + + // impacts invariant = validate invariant property, invariant culture + if (impact.ImpactsOnlyInvariantCulture) + { + return !(propertyTypeVaries || IsPropertyValid(x, null)); + } + + // impacts all = validate property, all cultures (incl. invariant) + if (impact.ImpactsAllCultures) + { + return !IsPropertyValid(x); + } + + // impacts explicit culture = validate variant property, explicit culture + if (propertyTypeVaries) + { + return !IsPropertyValid(x, impact.Culture); + } + + // and, for explicit culture, we may also have to validate invariant property, invariant culture + // if either + // - it is impacted (default culture), or + // - there is no published version of the content - maybe non-default culture, but no published version + var alsoInvariant = impact.ImpactsAlsoInvariantProperties || !content.Published; + return alsoInvariant && !IsPropertyValid(x, null); + }).ToArray(); + + return invalidProperties.Length == 0; + } + + /// + public bool IsPropertyValid(IProperty property, string? culture = "*", string? segment = "*") + { + // NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there. + // The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing. + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + IPropertyValue? pvalue = null; + + // if validating invariant/neutral, and it is supported, validate + // (including ensuring that the value exists, if mandatory) + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + property.PropertyType.SupportsVariation(null, null)) + { + // validate pvalue (which is the invariant value) + pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null); + if (!IsValidPropertyValue(property, pvalue?.EditedValue)) + { + return false; + } + } + + // if validating only invariant/neutral, we are good + if (culture == null && segment == null) + { + return true; + } + + // if nothing else to validate, we are good + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + !property.PropertyType.VariesByCulture()) + { + return true; + } + + // for anything else, validate the existing values (including mandatory), + // but we cannot validate mandatory globally (we don't know the possible cultures and segments) + + // validate vvalues (which are the variant values) + + // if we don't have vvalues (property.Values is empty or only contains pvalue), validate null + if (property.Values.Count == (pvalue == null ? 0 : 1)) + { + return culture == "*" || IsValidPropertyValue(property, null); + } + + // else validate vvalues (but don't revalidate pvalue) + var pvalues = property.Values.Where(x => + x != pvalue && // don't revalidate pvalue + property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok + (culture == "*" || x.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Segment.InvariantEquals(segment))) // the segment matches + .ToList(); + + return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); + } + + /// + /// Boolean indicating whether the passed in value is valid + /// + /// + /// + /// True is property value is valid, otherwise false + private bool IsValidPropertyValue(IProperty property, object? value) => + IsPropertyValueValid(property.PropertyType, value); + + /// + /// Determines whether a value is valid for this property type. + /// + private bool IsPropertyValueValid(IPropertyType propertyType, object? value) + { + IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) + { + // nothing much we can do validation wise if the property editor has been removed. + // the property will be displayed as a label, so flagging it as invalid would be pointless. + return true; + } + + var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.Configuration; + IDataValueEditor valueEditor = editor.GetValueEditor(configuration); + return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); + } } diff --git a/src/Umbraco.Core/Services/PublicAccessService.cs b/src/Umbraco.Core/Services/PublicAccessService.cs index b6216e4b58..6f3de02c55 100644 --- a/src/Umbraco.Core/Services/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/PublicAccessService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -10,228 +7,243 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +internal class PublicAccessService : RepositoryService, IPublicAccessService { - internal class PublicAccessService : RepositoryService, IPublicAccessService + private readonly IPublicAccessRepository _publicAccessRepository; + + public PublicAccessService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IPublicAccessRepository publicAccessRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _publicAccessRepository = publicAccessRepository; + + /// + /// Gets all defined entries and associated rules + /// + /// + public IEnumerable GetAll() { - private readonly IPublicAccessRepository _publicAccessRepository; - - public PublicAccessService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IPublicAccessRepository publicAccessRepository) - : base(provider, loggerFactory, eventMessagesFactory) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - _publicAccessRepository = publicAccessRepository; - } - - /// - /// Gets all defined entries and associated rules - /// - /// - public IEnumerable GetAll() - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _publicAccessRepository.GetMany(); - } - } - - /// - /// Gets the entry defined for the content item's path - /// - /// - /// Returns null if no entry is found - public PublicAccessEntry? GetEntryForContent(IContent content) - { - return GetEntryForContent(content.Path.EnsureEndsWith("," + content.Id)); - } - - /// - /// Gets the entry defined for the content item based on a content path - /// - /// - /// Returns null if no entry is found - /// - /// NOTE: This method get's called *very* often! This will return the results from cache - /// - public PublicAccessEntry? GetEntryForContent(string contentPath) - { - //Get all ids in the path for the content item and ensure they all - // parse to ints that are not -1. - var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out int val) ? val : -1) - .Where(x => x != -1) - .ToList(); - - //start with the deepest id - ids.Reverse(); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - //This will retrieve from cache! - var entries = _publicAccessRepository.GetMany().ToList(); - foreach (var id in ids) - { - var found = entries.FirstOrDefault(x => x.ProtectedNodeId == id); - if (found != null) return found; - } - } - - return null; - } - - /// - /// Returns true if the content has an entry for it's path - /// - /// - /// - public Attempt IsProtected(IContent content) - { - var result = GetEntryForContent(content); - return Attempt.If(result != null, result); - } - - /// - /// Returns true if the content has an entry based on a content path - /// - /// - /// - public Attempt IsProtected(string contentPath) - { - var result = GetEntryForContent(contentPath); - return Attempt.If(result != null, result); - } - - /// - /// Adds a rule - /// - /// - /// - /// - /// - public Attempt?> AddRule(IContent content, string ruleType, string ruleValue) - { - var evtMsgs = EventMessagesFactory.Get(); - PublicAccessEntry? entry; - using (var scope = ScopeProvider.CreateCoreScope()) - { - entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); - if (entry == null) - return OperationResult.Attempt.Cannot(evtMsgs); // causes rollback - - var existingRule = entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); - if (existingRule == null) - { - entry.AddRule(ruleValue, ruleType); - } - else - { - //If they are both the same already then there's nothing to update, exit - return OperationResult.Attempt.Succeed(evtMsgs, entry); - } - - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs, entry); - } - - _publicAccessRepository.Save(entry); - - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - } - - return OperationResult.Attempt.Succeed(evtMsgs, entry); - } - - /// - /// Removes a rule - /// - /// - /// - /// - public Attempt RemoveRule(IContent content, string ruleType, string ruleValue) - { - var evtMsgs = EventMessagesFactory.Get(); - PublicAccessEntry? entry; - using (var scope = ScopeProvider.CreateCoreScope()) - { - entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); - if (entry == null) return Attempt.Fail(); // causes rollback // causes rollback - - var existingRule = entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); - if (existingRule == null) return Attempt.Fail(); // causes rollback // causes rollback - - entry.RemoveRule(existingRule); - - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } - - _publicAccessRepository.Save(entry); - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - } - - return OperationResult.Attempt.Succeed(evtMsgs); - } - - /// - /// Saves the entry - /// - /// - public Attempt Save(PublicAccessEntry entry) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } - - _publicAccessRepository.Save(entry); - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - } - - return OperationResult.Attempt.Succeed(evtMsgs); - } - - /// - /// Deletes the entry and all associated rules - /// - /// - public Attempt Delete(PublicAccessEntry entry) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var deletingNotification = new PublicAccessEntryDeletingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } - - _publicAccessRepository.Delete(entry); - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); - } - - return OperationResult.Attempt.Succeed(evtMsgs); + return _publicAccessRepository.GetMany(); } } + + /// + /// Gets the entry defined for the content item's path + /// + /// + /// Returns null if no entry is found + public PublicAccessEntry? GetEntryForContent(IContent content) => + GetEntryForContent(content.Path.EnsureEndsWith("," + content.Id)); + + /// + /// Gets the entry defined for the content item based on a content path + /// + /// + /// Returns null if no entry is found + /// + /// NOTE: This method get's called *very* often! This will return the results from cache + /// + public PublicAccessEntry? GetEntryForContent(string contentPath) + { + // Get all ids in the path for the content item and ensure they all + // parse to ints that are not -1. + var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val) ? val : -1) + .Where(x => x != -1) + .ToList(); + + // start with the deepest id + ids.Reverse(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + // This will retrieve from cache! + var entries = _publicAccessRepository.GetMany().ToList(); + foreach (var id in ids) + { + PublicAccessEntry? found = entries.FirstOrDefault(x => x.ProtectedNodeId == id); + if (found != null) + { + return found; + } + } + } + + return null; + } + + /// + /// Returns true if the content has an entry for it's path + /// + /// + /// + public Attempt IsProtected(IContent content) + { + PublicAccessEntry? result = GetEntryForContent(content); + return Attempt.If(result != null, result); + } + + /// + /// Returns true if the content has an entry based on a content path + /// + /// + /// + public Attempt IsProtected(string contentPath) + { + PublicAccessEntry? result = GetEntryForContent(contentPath); + return Attempt.If(result != null, result); + } + + /// + /// Adds a rule + /// + /// + /// + /// + /// + public Attempt?> AddRule(IContent content, string ruleType, string ruleValue) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + PublicAccessEntry? entry; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); + if (entry == null) + { + return OperationResult.Attempt.Cannot(evtMsgs); // causes rollback + } + + PublicAccessRule? existingRule = + entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); + if (existingRule == null) + { + entry.AddRule(ruleValue, ruleType); + } + else + { + // If they are both the same already then there's nothing to update, exit + return OperationResult.Attempt.Succeed(evtMsgs, entry); + } + + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs, entry); + } + + _publicAccessRepository.Save(entry); + + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + } + + return OperationResult.Attempt.Succeed(evtMsgs, entry); + } + + /// + /// Removes a rule + /// + /// + /// + /// + public Attempt RemoveRule(IContent content, string ruleType, string ruleValue) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + PublicAccessEntry? entry; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); + if (entry == null) + { + return Attempt.Fail(); // causes rollback // causes rollback + } + + PublicAccessRule? existingRule = + entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); + if (existingRule == null) + { + return Attempt.Fail(); // causes rollback // causes rollback + } + + entry.RemoveRule(existingRule); + + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs); + } + + _publicAccessRepository.Save(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + } + + return OperationResult.Attempt.Succeed(evtMsgs); + } + + /// + /// Saves the entry + /// + /// + public Attempt Save(PublicAccessEntry entry) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs); + } + + _publicAccessRepository.Save(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + } + + return OperationResult.Attempt.Succeed(evtMsgs); + } + + /// + /// Deletes the entry and all associated rules + /// + /// + public Attempt Delete(PublicAccessEntry entry) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var deletingNotification = new PublicAccessEntryDeletingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs); + } + + _publicAccessRepository.Delete(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); + } + + return OperationResult.Attempt.Succeed(evtMsgs); + } } diff --git a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index d8a7f201de..eb42dcda73 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -1,105 +1,125 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for the IPublicAccessService +/// +public static class PublicAccessServiceExtensions { - /// - /// Extension methods for the IPublicAccessService - /// - public static class PublicAccessServiceExtensions + public static bool RenameMemberGroupRoleRules(this IPublicAccessService publicAccessService, string? oldRolename, string? newRolename) { - public static bool RenameMemberGroupRoleRules(this IPublicAccessService publicAccessService, string? oldRolename, string? newRolename) + var hasChange = false; + if (oldRolename == newRolename) { - var hasChange = false; - if (oldRolename == newRolename) return false; - - var allEntries = publicAccessService.GetAll(); - - foreach (var entry in allEntries) - { - //get rules that match - var roleRules = entry.Rules - .Where(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) - .Where(x => x.RuleValue == oldRolename); - var save = false; - foreach (var roleRule in roleRules) - { - //a rule is being updated so flag this entry to be saved - roleRule.RuleValue = newRolename ?? String.Empty; - save = true; - } - if (save) - { - hasChange = true; - publicAccessService.Save(entry); - } - } - - return hasChange; + return false; } - public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) + IEnumerable allEntries = publicAccessService.GetAll(); + + foreach (PublicAccessEntry entry in allEntries) { - var content = contentService.GetById(documentId); - if (content == null) return true; - - var entry = publicAccessService.GetEntryForContent(content); - if (entry == null) return true; - - return HasAccess(entry, username, currentMemberRoles); - } - - /// - /// Checks if the member with the specified username has access to the path which is also based on the passed in roles for the member - /// - /// - /// - /// - /// A callback to retrieve the roles for this member - /// - public static async Task HasAccessAsync(this IPublicAccessService publicAccessService, string path, string username, Func>> rolesCallback) - { - if (rolesCallback == null) throw new ArgumentNullException("roles"); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); - - var entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); - if (entry == null) return true; - - var roles = await rolesCallback(); - - return HasAccess(entry, username, roles); - } - - private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) - { - if (entry is null) + // get rules that match + IEnumerable roleRules = entry.Rules + .Where(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) + .Where(x => x.RuleValue == oldRolename); + var save = false; + foreach (PublicAccessRule roleRule in roleRules) { - throw new ArgumentNullException(nameof(entry)); + // a rule is being updated so flag this entry to be saved + roleRule.RuleValue = newRolename ?? string.Empty; + save = true; } - if (string.IsNullOrEmpty(username)) + if (save) { - throw new ArgumentException($"'{nameof(username)}' cannot be null or empty.", nameof(username)); + hasChange = true; + publicAccessService.Save(entry); } - - if (roles is null) - { - throw new ArgumentNullException(nameof(roles)); - } - - return entry.Rules.Any(x => - (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) - || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue)) - ); } + + return hasChange; + } + + public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) + { + IContent? content = contentService.GetById(documentId); + if (content == null) + { + return true; + } + + PublicAccessEntry? entry = publicAccessService.GetEntryForContent(content); + if (entry == null) + { + return true; + } + + return HasAccess(entry, username, currentMemberRoles); + } + + /// + /// Checks if the member with the specified username has access to the path which is also based on the passed in roles + /// for the member + /// + /// + /// + /// + /// A callback to retrieve the roles for this member + /// + public static async Task HasAccessAsync(this IPublicAccessService publicAccessService, string path, string username, Func>> rolesCallback) + { + if (rolesCallback == null) + { + throw new ArgumentNullException("roles"); + } + + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "username"); + } + + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "path"); + } + + PublicAccessEntry? entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); + if (entry == null) + { + return true; + } + + IEnumerable roles = await rolesCallback(); + + return HasAccess(entry, username, roles); + } + + private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) + { + if (entry is null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentException($"'{nameof(username)}' cannot be null or empty.", nameof(username)); + } + + if (roles is null) + { + throw new ArgumentNullException(nameof(roles)); + } + + return entry.Rules.Any(x => + (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && + username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) + || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue))); } } diff --git a/src/Umbraco.Core/Services/PublishResult.cs b/src/Umbraco.Core/Services/PublishResult.cs index 0ab820e7a6..f689249afc 100644 --- a/src/Umbraco.Core/Services/PublishResult.cs +++ b/src/Umbraco.Core/Services/PublishResult.cs @@ -1,37 +1,36 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents the result of publishing a document. +/// +public class PublishResult : OperationResult { + /// + /// Initializes a new instance of the class. + /// + public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent? content) + : base(resultType, eventMessages, content) + { + } /// - /// Represents the result of publishing a document. + /// Initializes a new instance of the class. /// - public class PublishResult : OperationResult + public PublishResult(EventMessages eventMessages, IContent content) + : base(PublishResultType.SuccessPublish, eventMessages, content) { - /// - /// Initializes a new instance of the class. - /// - public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent? content) - : base(resultType, eventMessages, content) - { } - - /// - /// Initializes a new instance of the class. - /// - public PublishResult(EventMessages eventMessages, IContent content) - : base(PublishResultType.SuccessPublish, eventMessages, content) - { } - - /// - /// Gets the document. - /// - public IContent? Content => Entity; - - /// - /// Gets or sets the invalid properties, if the status failed due to validation. - /// - public IEnumerable? InvalidProperties { get; set; } } + + /// + /// Gets the document. + /// + public IContent? Content => Entity; + + /// + /// Gets or sets the invalid properties, if the status failed due to validation. + /// + public IEnumerable? InvalidProperties { get; set; } } diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 43fab58218..b8ebd5edd4 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -1,151 +1,152 @@ -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// A value indicating the result of publishing or unpublishing a document. +/// +public enum PublishResultType : byte { + // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare + // every failure codes as >128 - see OperationResult and OperationResultType for details. + #region Success - Publish + /// - /// A value indicating the result of publishing or unpublishing a document. + /// The document was successfully published. /// - public enum PublishResultType : byte - { - // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare - // every failure codes as >128 - see OperationResult and OperationResultType for details. + SuccessPublish = 0, - #region Success - Publish + /// + /// The specified document culture was successfully published. + /// + SuccessPublishCulture = 1, - /// - /// The document was successfully published. - /// - SuccessPublish = 0, + /// + /// The document was already published. + /// + SuccessPublishAlready = 2, - /// - /// The specified document culture was successfully published. - /// - SuccessPublishCulture = 1, + #endregion - /// - /// The document was already published. - /// - SuccessPublishAlready = 2, + #region Success - Unpublish - #endregion + /// + /// The document was successfully unpublished. + /// + SuccessUnpublish = 3, - #region Success - Unpublish + /// + /// The document was already unpublished. + /// + SuccessUnpublishAlready = 4, - /// - /// The document was successfully unpublished. - /// - SuccessUnpublish = 3, + /// + /// The specified document culture was unpublished, the document item itself remains published. + /// + SuccessUnpublishCulture = 5, - /// - /// The document was already unpublished. - /// - SuccessUnpublishAlready = 4, + /// + /// The specified document culture was unpublished, and was a mandatory culture, therefore the document itself was + /// unpublished. + /// + SuccessUnpublishMandatoryCulture = 6, - /// - /// The specified document culture was unpublished, the document item itself remains published. - /// - SuccessUnpublishCulture = 5, + /// + /// The specified document culture was unpublished, and was the last published culture in the document, therefore the + /// document itself was unpublished. + /// + SuccessUnpublishLastCulture = 8, - /// - /// The specified document culture was unpublished, and was a mandatory culture, therefore the document itself was unpublished. - /// - SuccessUnpublishMandatoryCulture = 6, + #endregion - /// - /// The specified document culture was unpublished, and was the last published culture in the document, therefore the document itself was unpublished. - /// - SuccessUnpublishLastCulture = 8, + #region Success - Mixed - #endregion + /// + /// Specified document cultures were successfully published and unpublished (in the same operation). + /// + SuccessMixedCulture = 7, - #region Success - Mixed + #endregion - /// - /// Specified document cultures were successfully published and unpublished (in the same operation). - /// - SuccessMixedCulture = 7, + #region Failed - Publish - #endregion + /// + /// The operation failed. + /// + /// All values above this value indicate a failure. + FailedPublish = 128, - #region Failed - Publish + /// + /// The document could not be published because its ancestor path is not published. + /// + FailedPublishPathNotPublished = FailedPublish | 1, - /// - /// The operation failed. - /// - /// All values above this value indicate a failure. - FailedPublish = 128, + /// + /// The document has expired so we cannot force it to be + /// published again as part of a bulk publish operation. + /// + FailedPublishHasExpired = FailedPublish | 2, - /// - /// The document could not be published because its ancestor path is not published. - /// - FailedPublishPathNotPublished = FailedPublish | 1, + /// + /// The document is scheduled to be released in the future and therefore we cannot force it to + /// be published during a bulk publish operation. + /// + FailedPublishAwaitingRelease = FailedPublish | 3, - /// - /// The document has expired so we cannot force it to be - /// published again as part of a bulk publish operation. - /// - FailedPublishHasExpired = FailedPublish | 2, + /// + /// A document culture has expired so we cannot force it to be + /// published again as part of a bulk publish operation. + /// + FailedPublishCultureHasExpired = FailedPublish | 4, - /// - /// The document is scheduled to be released in the future and therefore we cannot force it to - /// be published during a bulk publish operation. - /// - FailedPublishAwaitingRelease = FailedPublish | 3, + /// + /// A document culture is scheduled to be released in the future and therefore we cannot force it to + /// be published during a bulk publish operation. + /// + FailedPublishCultureAwaitingRelease = FailedPublish | 5, - /// - /// A document culture has expired so we cannot force it to be - /// published again as part of a bulk publish operation. - /// - FailedPublishCultureHasExpired = FailedPublish | 4, + /// + /// The document could not be published because it is in the trash. + /// + FailedPublishIsTrashed = FailedPublish | 6, - /// - /// A document culture is scheduled to be released in the future and therefore we cannot force it to - /// be published during a bulk publish operation. - /// - FailedPublishCultureAwaitingRelease = FailedPublish | 5, + /// + /// The publish action has been cancelled by an event handler. + /// + FailedPublishCancelledByEvent = FailedPublish | 7, - /// - /// The document could not be published because it is in the trash. - /// - FailedPublishIsTrashed = FailedPublish | 6, + /// + /// The document could not be published because it contains invalid data (has not passed validation requirements). + /// + FailedPublishContentInvalid = FailedPublish | 8, - /// - /// The publish action has been cancelled by an event handler. - /// - FailedPublishCancelledByEvent = FailedPublish | 7, + /// + /// The document could not be published because it has no publishing flags or values or if its a variant document, no + /// cultures were specified to be published. + /// + FailedPublishNothingToPublish = FailedPublish | 9, - /// - /// The document could not be published because it contains invalid data (has not passed validation requirements). - /// - FailedPublishContentInvalid = FailedPublish | 8, + /// + /// The document could not be published because some mandatory cultures are missing. + /// + FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing - /// - /// The document could not be published because it has no publishing flags or values or if its a variant document, no cultures were specified to be published. - /// - FailedPublishNothingToPublish = FailedPublish | 9, + /// + /// The document could not be published because it has been modified by another user. + /// + FailedPublishConcurrencyViolation = FailedPublish | 11, - /// - /// The document could not be published because some mandatory cultures are missing. - /// - FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing + #endregion - /// - /// The document could not be published because it has been modified by another user. - /// - FailedPublishConcurrencyViolation = FailedPublish | 11, + #region Failed - Unpublish - #endregion + /// + /// The document could not be unpublished. + /// + FailedUnpublish = FailedPublish | 11, // in ContentService.SavePublishing - #region Failed - Unpublish + /// + /// The unpublish action has been cancelled by an event handler. + /// + FailedUnpublishCancelledByEvent = FailedPublish | 12, - /// - /// The document could not be unpublished. - /// - FailedUnpublish = FailedPublish | 11, // in ContentService.SavePublishing - - /// - /// The unpublish action has been cancelled by an event handler. - /// - FailedUnpublishCancelledByEvent = FailedPublish | 12, - - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Services/RedirectUrlService.cs b/src/Umbraco.Core/Services/RedirectUrlService.cs index 14c3e834bf..07646de3de 100644 --- a/src/Umbraco.Core/Services/RedirectUrlService.cs +++ b/src/Umbraco.Core/Services/RedirectUrlService.cs @@ -1,122 +1,150 @@ -using System; -using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +internal class RedirectUrlService : RepositoryService, IRedirectUrlService { - internal class RedirectUrlService : RepositoryService, IRedirectUrlService + private readonly IRedirectUrlRepository _redirectUrlRepository; + + public RedirectUrlService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IRedirectUrlRepository redirectUrlRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _redirectUrlRepository = redirectUrlRepository; + + public void Register(string url, Guid contentKey, string? culture = null) { - private readonly IRedirectUrlRepository _redirectUrlRepository; - - public RedirectUrlService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IRedirectUrlRepository redirectUrlRepository) - : base(provider, loggerFactory, eventMessagesFactory) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - _redirectUrlRepository = redirectUrlRepository; - } - - public void Register(string url, Guid contentKey, string? culture = null) - { - using (var scope = ScopeProvider.CreateCoreScope()) + IRedirectUrl? redir = _redirectUrlRepository.Get(url, contentKey, culture); + if (redir != null) { - var redir = _redirectUrlRepository.Get(url, contentKey, culture); - if (redir != null) - redir.CreateDateUtc = DateTime.UtcNow; - else - redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture}; - _redirectUrlRepository.Save(redir); - scope.Complete(); + redir.CreateDateUtc = DateTime.UtcNow; } - } - - public void Delete(IRedirectUrl redirectUrl) - { - using (var scope = ScopeProvider.CreateCoreScope()) + else { - _redirectUrlRepository.Delete(redirectUrl); - scope.Complete(); - } - } - - public void Delete(Guid id) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.Delete(id); - scope.Complete(); - } - } - - public void DeleteContentRedirectUrls(Guid contentKey) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.DeleteContentUrls(contentKey); - scope.Complete(); - } - } - - public void DeleteAll() - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.DeleteAll(); - scope.Complete(); - } - } - - public IRedirectUrl? GetMostRecentRedirectUrl(string url) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetMostRecentUrl(url); - } - } - - public IEnumerable GetContentRedirectUrls(Guid contentKey) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetContentUrls(contentKey); - } - } - - public IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total); - } - } - - public IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total); - } - } - - public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total); - } - } - - public IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture) - { - if (string.IsNullOrWhiteSpace(culture)) return GetMostRecentRedirectUrl(url); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetMostRecentUrl(url, culture); + redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture }; } + _redirectUrlRepository.Save(redir); + scope.Complete(); } } + + public void Delete(IRedirectUrl redirectUrl) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.Delete(redirectUrl); + scope.Complete(); + } + } + + public void Delete(Guid id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.Delete(id); + scope.Complete(); + } + } + + public void DeleteContentRedirectUrls(Guid contentKey) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.DeleteContentUrls(contentKey); + scope.Complete(); + } + } + + public void DeleteAll() + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.DeleteAll(); + scope.Complete(); + } + } + + public IRedirectUrl? GetMostRecentRedirectUrl(string url) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetMostRecentUrl(url); + } + } + + public async Task GetMostRecentRedirectUrlAsync(string url) + { + using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return await _redirectUrlRepository.GetMostRecentUrlAsync(url); + } + } + + public IEnumerable GetContentRedirectUrls(Guid contentKey) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetContentUrls(contentKey); + } + } + + public IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total); + } + } + + public IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total); + } + } + + public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total); + } + } + + public IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture) + { + if (string.IsNullOrWhiteSpace(culture)) + { + return GetMostRecentRedirectUrl(url); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetMostRecentUrl(url, culture); + } + } + + public async Task GetMostRecentRedirectUrlAsync(string url, string? culture) + { + if (string.IsNullOrWhiteSpace(culture)) + { + return await GetMostRecentRedirectUrlAsync(url); + } + + using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return await _redirectUrlRepository.GetMostRecentUrlAsync(url, culture); + } + } } diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs index 966e4ec7df..20cd72e7cc 100644 --- a/src/Umbraco.Core/Services/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -11,605 +8,612 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class RelationService : RepositoryService, IRelationService { - public class RelationService : RepositoryService, IRelationService + private readonly IAuditRepository _auditRepository; + private readonly IEntityService _entityService; + private readonly IRelationRepository _relationRepository; + private readonly IRelationTypeRepository _relationTypeRepository; + + public RelationService(ICoreScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IEntityService entityService, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, IAuditRepository auditRepository) + : base(uowProvider, loggerFactory, eventMessagesFactory) { - private readonly IEntityService _entityService; - private readonly IRelationRepository _relationRepository; - private readonly IRelationTypeRepository _relationTypeRepository; - private readonly IAuditRepository _auditRepository; - - public RelationService(ICoreScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IEntityService entityService, - IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, IAuditRepository auditRepository) - : base(uowProvider, loggerFactory, eventMessagesFactory) - { - _relationRepository = relationRepository; - _relationTypeRepository = relationTypeRepository; - _auditRepository = auditRepository; - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - } - - /// - public IRelation? GetById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.Get(id); - } - } - - /// - public IRelationType? GetRelationTypeById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.Get(id); - } - } - - /// - public IRelationType? GetRelationTypeById(Guid id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.Get(id); - } - } - - /// - public IRelationType? GetRelationTypeByAlias(string alias) => GetRelationType(alias); - - /// - public IEnumerable GetAllRelations(params int[] ids) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetMany(ids); - } - } - - /// - public IEnumerable? GetAllRelationsByRelationType(IRelationType relationType) - { - return GetAllRelationsByRelationType(relationType.Id); - } - - /// - public IEnumerable? GetAllRelationsByRelationType(int relationTypeId) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.RelationTypeId == relationTypeId); - return _relationRepository.Get(query); - } - } - - /// - public IEnumerable GetAllRelationTypes(params int[] ids) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.GetMany(ids); - } - } - - /// - public IEnumerable? GetByParentId(int id) => GetByParentId(id, null); - - /// - public IEnumerable GetByParentId(int id, string? relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - if (relationTypeAlias.IsNullOrWhiteSpace()) - { - var qry1 = Query().Where(x => x.ParentId == id); - return _relationRepository.Get(qry1) ?? Enumerable.Empty(); - } - - var relationType = GetRelationType(relationTypeAlias!); - if (relationType == null) - return Enumerable.Empty(); - - var qry2 = Query().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(qry2) ?? Enumerable.Empty(); - } - } - - /// - public IEnumerable? GetByParent(IUmbracoEntity parent) => GetByParentId(parent.Id); - - /// - public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => GetByParentId(parent.Id, relationTypeAlias); - - /// - public IEnumerable GetByChildId(int id) => GetByChildId(id, null); - - /// - public IEnumerable GetByChildId(int id, string? relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - if (relationTypeAlias.IsNullOrWhiteSpace()) - { - var qry1 = Query().Where(x => x.ChildId == id); - return _relationRepository.Get(qry1) ?? Enumerable.Empty(); - } - - var relationType = GetRelationType(relationTypeAlias!); - if (relationType == null) - return Enumerable.Empty(); - - var qry2 = Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(qry2) ?? Enumerable.Empty(); - } - } - - /// - public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); - - /// - public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => GetByChildId(child.Id, relationTypeAlias); - - /// - public IEnumerable GetByParentOrChildId(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ChildId == id || x.ParentId == id); - return _relationRepository.Get(query); - } - } - - public IEnumerable GetByParentOrChildId(int id, string relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var relationType = GetRelationType(relationTypeAlias); - if (relationType == null) - return Enumerable.Empty(); - - var query = Query().Where(x => (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query); - } - } - - /// - public IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && - x.ChildId == childId && - x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query)?.FirstOrDefault(); - } - } - - /// - public IEnumerable GetByRelationTypeName(string relationTypeName) - { - List? relationTypeIds; - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - //This is a silly query - but i guess it's needed in case someone has more than one relation with the same Name (not alias), odd. - var query = Query().Where(x => x.Name == relationTypeName); - var relationTypes = _relationTypeRepository.Get(query); - relationTypeIds = relationTypes?.Select(x => x.Id).ToList(); - } - - return relationTypeIds is null || relationTypeIds.Count == 0 - ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(relationTypeIds); - } - - /// - public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) - { - var relationType = GetRelationType(relationTypeAlias); - - return relationType == null - ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(new[] { relationType.Id }); - } - - /// - public IEnumerable? GetByRelationTypeId(int relationTypeId) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.RelationTypeId == relationTypeId); - return _relationRepository.Get(query); - } - } - - /// - public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query()?.Where(x => x.RelationTypeId == relationTypeId); - return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); - } - } - - /// - public IUmbracoEntity? GetChildEntityFromRelation(IRelation relation) - { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - return _entityService.Get(relation.ChildId, objectType); - } - - /// - public IUmbracoEntity? GetParentEntityFromRelation(IRelation relation) - { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - return _entityService.Get(relation.ParentId, objectType); - } - - /// - public Tuple? GetEntitiesFromRelation(IRelation relation) - { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - - var child = _entityService.Get(relation.ChildId, childObjectType); - var parent = _entityService.Get(relation.ParentId, parentObjectType); - - if (parent is null || child is null) - { - return null; - } - - return new Tuple(parent, child); - } - - /// - public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) - { - // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll - // method to lookup batches of entities for each parent object type - - foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ChildObjectType))) - { - var objectType = groupedRelations.Key; - var ids = groupedRelations.Select(x => x.ChildId).ToArray(); - foreach (var e in _entityService.GetAll(objectType, ids)) - yield return e; - } - } - - /// - public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) - { - // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll - // method to lookup batches of entities for each parent object type - - foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) - { - var objectType = groupedRelations.Key; - var ids = groupedRelations.Select(x => x.ParentId).ToArray(); - foreach (var e in _entityService.GetAll(objectType, ids)) - yield return e; - } - } - - /// - public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); - } - } - - /// - public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); - } - } - - /// - public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) - { - //TODO: Argh! N+1 - - foreach (var relation in relations) - { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - - var child = _entityService.Get(relation.ChildId, childObjectType); - var parent = _entityService.Get(relation.ParentId, parentObjectType); - - if (parent is not null && child is not null) - { - yield return new Tuple(parent, child); - } - } - } - - /// - public IRelation Relate(int parentId, int childId, IRelationType relationType) - { - // Ensure that the RelationType has an identity before using it to relate two entities - if (relationType.HasIdentity == false) - { - Save(relationType); - } - - //TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok? - - var relation = new Relation(parentId, childId, relationType); - - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var savingNotification = new RelationSavingNotification(relation, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return relation; // TODO: returning sth that does not exist here?! - } - - _relationRepository.Save(relation); - scope.Notifications.Publish(new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); - scope.Complete(); - return relation; - } - } - - /// - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) - { - return Relate(parent.Id, child.Id, relationType); - } - - /// - public IRelation Relate(int parentId, int childId, string relationTypeAlias) - { - var relationType = GetRelationTypeByAlias(relationTypeAlias); - if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) - throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); - - return Relate(parentId, childId, relationType); - } - - /// - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) - { - var relationType = GetRelationTypeByAlias(relationTypeAlias); - if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) - throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); - - return Relate(parent.Id, child.Id, relationType); - } - - /// - public bool HasRelations(IRelationType relationType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool IsRelated(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == id || x.ChildId == id); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool AreRelated(int parentId, int childId) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool AreRelated(int parentId, int childId, string relationTypeAlias) - { - var relType = GetRelationTypeByAlias(relationTypeAlias); - if (relType == null) - return false; - - return AreRelated(parentId, childId, relType); - } - - - /// - public bool AreRelated(int parentId, int childId, IRelationType relationType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) - { - return AreRelated(parent.Id, child.Id); - } - - /// - public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) - { - return AreRelated(parent.Id, child.Id, relationTypeAlias); - } - - - /// - public void Save(IRelation relation) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var savingNotification = new RelationSavingNotification(relation, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - _relationRepository.Save(relation); - scope.Complete(); - scope.Notifications.Publish(new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); - } - } - - public void Save(IEnumerable relations) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - IRelation[] relationsA = relations.ToArray(); - - EventMessages messages = EventMessagesFactory.Get(); - var savingNotification = new RelationSavingNotification(relationsA, messages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - _relationRepository.Save(relationsA); - scope.Complete(); - scope.Notifications.Publish(new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification)); - } - } - - /// - public void Save(IRelationType relationType) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var savingNotification = new RelationTypeSavingNotification(relationType, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - _relationTypeRepository.Save(relationType); - Audit(AuditType.Save, Cms.Core.Constants.Security.SuperUserId, relationType.Id, $"Saved relation type: {relationType.Name}"); - scope.Complete(); - scope.Notifications.Publish(new RelationTypeSavedNotification(relationType, eventMessages).WithStateFrom(savingNotification)); - } - } - - /// - public void Delete(IRelation relation) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var deletingNotification = new RelationDeletingNotification(relation, eventMessages); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } - - _relationRepository.Delete(relation); - scope.Complete(); - scope.Notifications.Publish(new RelationDeletedNotification(relation, eventMessages).WithStateFrom(deletingNotification)); - } - } - - /// - public void Delete(IRelationType relationType) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } - - _relationTypeRepository.Delete(relationType); - scope.Complete(); - scope.Notifications.Publish(new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification)); - } - } - - /// - public void DeleteRelationsOfType(IRelationType relationType) - { - var relations = new List(); - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - IQuery? query = Query().Where(x => x.RelationTypeId == relationType.Id); - var allRelations = _relationRepository.Get(query)?.ToList(); - if (allRelations is not null) - { - relations.AddRange(allRelations); - } - - //TODO: N+1, we should be able to do this in a single call - - foreach (IRelation relation in relations) - { - _relationRepository.Delete(relation); - } - - scope.Complete(); - - scope.Notifications.Publish(new RelationDeletedNotification(relations, EventMessagesFactory.Get())); - } - } - - #region Private Methods - - private IRelationType? GetRelationType(string relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == relationTypeAlias); - return _relationTypeRepository.Get(query)?.FirstOrDefault(); - } - } - - private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) - { - var relations = new List(); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - foreach (var relationTypeId in relationTypeIds) - { - var id = relationTypeId; - var query = Query().Where(x => x.RelationTypeId == id); - var relation = _relationRepository.Get(query); - if (relation is not null) - { - relations.AddRange(relation); - } - } - } - return relations; - } - - private void Audit(AuditType type, int userId, int objectId, string? message = null) - { - _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.RelationType), message)); - } - #endregion + _relationRepository = relationRepository; + _relationTypeRepository = relationTypeRepository; + _auditRepository = auditRepository; + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); } + + /// + public IRelation? GetById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.Get(id); + } + } + + /// + public IRelationType? GetRelationTypeById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationTypeRepository.Get(id); + } + } + + /// + public IRelationType? GetRelationTypeById(Guid id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationTypeRepository.Get(id); + } + } + + /// + public IRelationType? GetRelationTypeByAlias(string alias) => GetRelationType(alias); + + /// + public IEnumerable GetAllRelations(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.GetMany(ids); + } + } + + /// + public IEnumerable GetAllRelationsByRelationType(IRelationType relationType) => + GetAllRelationsByRelationType(relationType.Id); + + /// + public IEnumerable GetAllRelationsByRelationType(int relationTypeId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.Get(query); + } + } + + /// + public IEnumerable GetAllRelationTypes(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationTypeRepository.GetMany(ids); + } + } + + /// + public IEnumerable GetByParentId(int id) => GetByParentId(id, null); + + /// + public IEnumerable GetByParentId(int id, string? relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + IQuery qry1 = Query().Where(x => x.ParentId == id); + return _relationRepository.Get(qry1); + } + + IRelationType? relationType = GetRelationType(relationTypeAlias!); + if (relationType == null) + { + return Enumerable.Empty(); + } + + IQuery qry2 = + Query().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); + } + } + + /// + public IEnumerable GetByParent(IUmbracoEntity parent) => GetByParentId(parent.Id); + + /// + public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => + GetByParentId(parent.Id, relationTypeAlias); + + /// + public IEnumerable GetByChildId(int id) => GetByChildId(id, null); + + /// + public IEnumerable GetByChildId(int id, string? relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + IQuery qry1 = Query().Where(x => x.ChildId == id); + return _relationRepository.Get(qry1); + } + + IRelationType? relationType = GetRelationType(relationTypeAlias!); + if (relationType == null) + { + return Enumerable.Empty(); + } + + IQuery qry2 = + Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); + } + } + + /// + public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); + + /// + public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => + GetByChildId(child.Id, relationTypeAlias); + + /// + public IEnumerable GetByParentOrChildId(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ChildId == id || x.ParentId == id); + return _relationRepository.Get(query); + } + } + + public IEnumerable GetByParentOrChildId(int id, string relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IRelationType? relationType = GetRelationType(relationTypeAlias); + if (relationType == null) + { + return Enumerable.Empty(); + } + + IQuery query = Query().Where(x => + (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query); + } + } + + /// + public IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == parentId && + x.ChildId == childId && + x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query).FirstOrDefault(); + } + } + + /// + public IEnumerable GetByRelationTypeName(string relationTypeName) + { + List? relationTypeIds; + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + // This is a silly query - but i guess it's needed in case someone has more than one relation with the same Name (not alias), odd. + IQuery query = Query().Where(x => x.Name == relationTypeName); + IEnumerable relationTypes = _relationTypeRepository.Get(query); + relationTypeIds = relationTypes.Select(x => x.Id).ToList(); + } + + return relationTypeIds.Count == 0 + ? Enumerable.Empty() + : GetRelationsByListOfTypeIds(relationTypeIds); + } + + /// + public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) + { + IRelationType? relationType = GetRelationType(relationTypeAlias); + + return relationType == null + ? Enumerable.Empty() + : GetRelationsByListOfTypeIds(new[] { relationType.Id }); + } + + /// + public IEnumerable GetByRelationTypeId(int relationTypeId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.Get(query); + } + } + + /// + public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery? query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); + } + } + + /// + public IUmbracoEntity? GetChildEntityFromRelation(IRelation relation) + { + UmbracoObjectTypes objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + return _entityService.Get(relation.ChildId, objectType); + } + + /// + public IUmbracoEntity? GetParentEntityFromRelation(IRelation relation) + { + UmbracoObjectTypes objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + return _entityService.Get(relation.ParentId, objectType); + } + + /// + public Tuple? GetEntitiesFromRelation(IRelation relation) + { + UmbracoObjectTypes childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + UmbracoObjectTypes parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + + IEntitySlim? child = _entityService.Get(relation.ChildId, childObjectType); + IEntitySlim? parent = _entityService.Get(relation.ParentId, parentObjectType); + + if (parent is null || child is null) + { + return null; + } + + return new Tuple(parent, child); + } + + /// + public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) + { + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type + foreach (IGrouping groupedRelations in relations.GroupBy(x => + ObjectTypes.GetUmbracoObjectType(x.ChildObjectType))) + { + UmbracoObjectTypes objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ChildId).ToArray(); + foreach (IEntitySlim e in _entityService.GetAll(objectType, ids)) + { + yield return e; + } + } + } + + /// + public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) + { + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type + foreach (IGrouping groupedRelations in relations.GroupBy(x => + ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) + { + UmbracoObjectTypes objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ParentId).ToArray(); + foreach (IEntitySlim e in _entityService.GetAll(objectType, ids)) + { + yield return e; + } + } + } + + /// + public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); + } + } + + /// + public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); + } + } + + /// + public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) + { + // TODO: Argh! N+1 + foreach (IRelation relation in relations) + { + UmbracoObjectTypes childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + UmbracoObjectTypes parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + + IEntitySlim? child = _entityService.Get(relation.ChildId, childObjectType); + IEntitySlim? parent = _entityService.Get(relation.ParentId, parentObjectType); + + if (parent is not null && child is not null) + { + yield return new Tuple(parent, child); + } + } + } + + /// + public IRelation Relate(int parentId, int childId, IRelationType relationType) + { + // Ensure that the RelationType has an identity before using it to relate two entities + if (relationType.HasIdentity == false) + { + Save(relationType); + } + + // TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok? + var relation = new Relation(parentId, childId, relationType); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return relation; // TODO: returning sth that does not exist here?! + } + + _relationRepository.Save(relation); + scope.Notifications.Publish( + new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); + scope.Complete(); + return relation; + } + } + + /// + public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) => + Relate(parent.Id, child.Id, relationType); + + /// + public IRelation Relate(int parentId, int childId, string relationTypeAlias) + { + IRelationType? relationType = GetRelationTypeByAlias(relationTypeAlias); + if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) + { + throw new ArgumentNullException( + string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); + } + + return Relate(parentId, childId, relationType); + } + + /// + public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) + { + IRelationType? relationType = GetRelationTypeByAlias(relationTypeAlias); + if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) + { + throw new ArgumentNullException( + string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); + } + + return Relate(parent.Id, child.Id, relationType); + } + + /// + public bool HasRelations(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query).Any(); + } + } + + /// + public bool IsRelated(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == id || x.ChildId == id); + return _relationRepository.Get(query).Any(); + } + } + + /// + public bool AreRelated(int parentId, int childId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId); + return _relationRepository.Get(query).Any(); + } + } + + /// + public bool AreRelated(int parentId, int childId, string relationTypeAlias) + { + IRelationType? relType = GetRelationTypeByAlias(relationTypeAlias); + if (relType == null) + { + return false; + } + + return AreRelated(parentId, childId, relType); + } + + /// + public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) => AreRelated(parent.Id, child.Id); + + /// + public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) => + AreRelated(parent.Id, child.Id, relationTypeAlias); + + /// + public void Save(IRelation relation) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + _relationRepository.Save(relation); + scope.Complete(); + scope.Notifications.Publish( + new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); + } + } + + public void Save(IEnumerable relations) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + IRelation[] relationsA = relations.ToArray(); + + EventMessages messages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relationsA, messages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + _relationRepository.Save(relationsA); + scope.Complete(); + scope.Notifications.Publish( + new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification)); + } + } + + /// + public void Save(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationTypeSavingNotification(relationType, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + _relationTypeRepository.Save(relationType); + Audit(AuditType.Save, Constants.Security.SuperUserId, relationType.Id, $"Saved relation type: {relationType.Name}"); + scope.Complete(); + scope.Notifications.Publish( + new RelationTypeSavedNotification(relationType, eventMessages).WithStateFrom(savingNotification)); + } + } + + /// + public void Delete(IRelation relation) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new RelationDeletingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return; + } + + _relationRepository.Delete(relation); + scope.Complete(); + scope.Notifications.Publish( + new RelationDeletedNotification(relation, eventMessages).WithStateFrom(deletingNotification)); + } + } + + /// + public void Delete(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return; + } + + _relationTypeRepository.Delete(relationType); + scope.Complete(); + scope.Notifications.Publish( + new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification)); + } + } + + /// + public void DeleteRelationsOfType(IRelationType relationType) + { + var relations = new List(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationType.Id); + var allRelations = _relationRepository.Get(query).ToList(); + relations.AddRange(allRelations); + + // TODO: N+1, we should be able to do this in a single call + foreach (IRelation relation in relations) + { + _relationRepository.Delete(relation); + } + + scope.Complete(); + + scope.Notifications.Publish(new RelationDeletedNotification(relations, EventMessagesFactory.Get())); + } + } + + public bool AreRelated(int parentId, int childId, IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => + x.ParentId == parentId && x.ChildId == childId && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query).Any(); + } + } + + #region Private Methods + + private IRelationType? GetRelationType(string relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.Alias == relationTypeAlias); + return _relationTypeRepository.Get(query).FirstOrDefault(); + } + } + + private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) + { + var relations = new List(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + foreach (var relationTypeId in relationTypeIds) + { + var id = relationTypeId; + IQuery query = Query().Where(x => x.RelationTypeId == id); + IEnumerable relation = _relationRepository.Get(query); + relations.AddRange(relation); + } + } + + return relations; + } + + private void Audit(AuditType type, int userId, int objectId, string? message = null) => + _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.RelationType.GetName(), message)); + + #endregion } diff --git a/src/Umbraco.Core/Services/RepositoryService.cs b/src/Umbraco.Core/Services/RepositoryService.cs index 85e78672ee..2c7bb39085 100644 --- a/src/Umbraco.Core/Services/RepositoryService.cs +++ b/src/Umbraco.Core/Services/RepositoryService.cs @@ -1,29 +1,27 @@ -using System; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents a service that works on top of repositories. +/// +public abstract class RepositoryService : IService { - /// - /// Represents a service that works on top of repositories. - /// - public abstract class RepositoryService : IService + protected RepositoryService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) { - protected IEventMessagesFactory EventMessagesFactory { get; } - - protected ICoreScopeProvider ScopeProvider { get; } - - protected ILoggerFactory LoggerFactory { get; } - - protected RepositoryService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) - { - EventMessagesFactory = eventMessagesFactory ?? throw new ArgumentNullException(nameof(eventMessagesFactory)); - ScopeProvider = provider ?? throw new ArgumentNullException(nameof(provider)); - LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - } - - protected IQuery Query() => ScopeProvider.CreateQuery(); + EventMessagesFactory = eventMessagesFactory ?? throw new ArgumentNullException(nameof(eventMessagesFactory)); + ScopeProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } + + protected IEventMessagesFactory EventMessagesFactory { get; } + + protected ICoreScopeProvider ScopeProvider { get; } + + protected ILoggerFactory LoggerFactory { get; } + + protected IQuery Query() => ScopeProvider.CreateQuery(); } diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index b698579b65..61ff978894 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -1,41 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Sections; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class SectionService : ISectionService { - public class SectionService : ISectionService + private readonly SectionCollection _sectionCollection; + private readonly IUserService _userService; + + public SectionService( + IUserService userService, + SectionCollection sectionCollection) { - private readonly IUserService _userService; - private readonly SectionCollection _sectionCollection; - - public SectionService( - IUserService userService, - SectionCollection sectionCollection) - { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _sectionCollection = sectionCollection ?? throw new ArgumentNullException(nameof(sectionCollection)); - } - - /// - /// The cache storage for all applications - /// - public IEnumerable GetSections() - => _sectionCollection; - - /// - public IEnumerable GetAllowedSections(int userId) - { - var user = _userService.GetUserById(userId); - if (user == null) - throw new InvalidOperationException("No user found with id " + userId); - - return GetSections().Where(x => user.AllowedSections.Contains(x.Alias)); - } - - /// - public ISection? GetByAlias(string appAlias) - => GetSections().FirstOrDefault(t => t.Alias.Equals(appAlias, StringComparison.OrdinalIgnoreCase)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _sectionCollection = sectionCollection ?? throw new ArgumentNullException(nameof(sectionCollection)); } + + /// + /// The cache storage for all applications + /// + public IEnumerable GetSections() + => _sectionCollection; + + /// + public IEnumerable GetAllowedSections(int userId) + { + IUser? user = _userService.GetUserById(userId); + if (user == null) + { + throw new InvalidOperationException("No user found with id " + userId); + } + + return GetSections().Where(x => user.AllowedSections.Contains(x.Alias)); + } + + /// + public ISection? GetByAlias(string appAlias) + => GetSections().FirstOrDefault(t => t.Alias.Equals(appAlias, StringComparison.OrdinalIgnoreCase)); } diff --git a/src/Umbraco.Core/Services/ServerRegistrationService.cs b/src/Umbraco.Core/Services/ServerRegistrationService.cs index c92977aab0..070e9e8e1f 100644 --- a/src/Umbraco.Core/Services/ServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/ServerRegistrationService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; @@ -10,163 +7,174 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services.Implement; + +/// +/// Manages server registrations in the database. +/// +public sealed class ServerRegistrationService : RepositoryService, IServerRegistrationService { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IServerRegistrationRepository _serverRegistrationRepository; + + private ServerRole _currentServerRole = ServerRole.Unknown; + /// - /// Manages server registrations in the database. + /// Initializes a new instance of the class. /// - public sealed class ServerRegistrationService : RepositoryService, IServerRegistrationService + public ServerRegistrationService( + ICoreScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IServerRegistrationRepository serverRegistrationRepository, + IHostingEnvironment hostingEnvironment) + : base(scopeProvider, loggerFactory, eventMessagesFactory) { - private readonly IServerRegistrationRepository _serverRegistrationRepository; - private readonly IHostingEnvironment _hostingEnvironment; - - private ServerRole _currentServerRole = ServerRole.Unknown; - - /// - /// Initializes a new instance of the class. - /// - public ServerRegistrationService( - ICoreScopeProvider scopeProvider, - ILoggerFactory loggerFactory, - IEventMessagesFactory eventMessagesFactory, - IServerRegistrationRepository serverRegistrationRepository, - IHostingEnvironment hostingEnvironment) - : base(scopeProvider, loggerFactory, eventMessagesFactory) - { - _serverRegistrationRepository = serverRegistrationRepository; - _hostingEnvironment = hostingEnvironment; - } - - /// - /// Touches a server to mark it as active; deactivate stale servers. - /// - /// The server URL. - /// The time after which a server is considered stale. - public void TouchServer(string serverAddress, TimeSpan staleTimeout) - { - var serverIdentity = GetCurrentServerIdentity(); - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - - _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache - - var regs = _serverRegistrationRepository.GetMany()?.ToArray(); - var hasSchedulingPublisher = regs?.Any(x => ((ServerRegistration) x).IsSchedulingPublisher); - var server = regs?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); - - if (server == null) - { - server = new ServerRegistration(serverAddress, serverIdentity, DateTime.Now); - } - else - { - server.ServerAddress = serverAddress; // should not really change but it might! - server.UpdateDate = DateTime.Now; - } - - server.IsActive = true; - if (hasSchedulingPublisher == false) - server.IsSchedulingPublisher = true; - - _serverRegistrationRepository.Save(server); - _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); // triggers a cache reload - - // reload - cheap, cached - - regs = _serverRegistrationRepository.GetMany()?.ToArray(); - - // default role is single server, but if registrations contain more - // than one active server, then role is scheduling publisher or subscriber - _currentServerRole = regs?.Count(x => x.IsActive) > 1 - ? (server.IsSchedulingPublisher ? ServerRole.SchedulingPublisher : ServerRole.Subscriber) - : ServerRole.Single; - - scope.Complete(); - } - } - - /// - /// Deactivates a server. - /// - /// The server unique identity. - public void DeactiveServer(string serverIdentity) - { - // because the repository caches "all" and has queries disabled... - - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - - _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache // ensure we have up-to-date cache - - var server = _serverRegistrationRepository.GetMany()?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); - if (server == null) return; - server.IsActive = server.IsSchedulingPublisher = false; - _serverRegistrationRepository.Save(server); // will trigger a cache reload // will trigger a cache reload - - scope.Complete(); - } - } - - /// - /// Deactivates stale servers. - /// - /// The time after which a server is considered stale. - public void DeactiveStaleServers(TimeSpan staleTimeout) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); - scope.Complete(); - } - } - - /// - /// Return all active servers. - /// - /// A value indicating whether to force-refresh the cache. - /// All active servers. - /// By default this method will rely on the repository's cache, which is updated each - /// time the current server is touched, and the period depends on the configuration. Use the - /// parameter to force a cache refresh and reload active servers - /// from the database. - public IEnumerable? GetActiveServers(bool refresh = false) => GetServers(refresh).Where(x => x.IsActive); - - /// - /// Return all servers (active and inactive). - /// - /// A value indicating whether to force-refresh the cache. - /// All servers. - /// By default this method will rely on the repository's cache, which is updated each - /// time the current server is touched, and the period depends on the configuration. Use the - /// parameter to force a cache refresh and reload all servers - /// from the database. - public IEnumerable GetServers(bool refresh = false) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - scope.ReadLock(Cms.Core.Constants.Locks.Servers); - if (refresh) - { - _serverRegistrationRepository.ClearCache(); - } - - return _serverRegistrationRepository.GetMany().ToArray(); // fast, cached // fast, cached - } - } - - /// - /// Gets the role of the current server. - /// - /// The role of the current server. - public ServerRole GetCurrentServerRole() => _currentServerRole; - - /// - /// Gets the local server identity. - /// - private string GetCurrentServerIdentity() => Environment.MachineName // eg DOMAIN\SERVER - + "/" + _hostingEnvironment.ApplicationId; // eg /LM/S3SVC/11/ROOT; + _serverRegistrationRepository = serverRegistrationRepository; + _hostingEnvironment = hostingEnvironment; } + + /// + /// Touches a server to mark it as active; deactivate stale servers. + /// + /// The server URL. + /// The time after which a server is considered stale. + public void TouchServer(string serverAddress, TimeSpan staleTimeout) + { + var serverIdentity = GetCurrentServerIdentity(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.Servers); + + _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache + + IServerRegistration[]? regs = _serverRegistrationRepository.GetMany()?.ToArray(); + var hasSchedulingPublisher = regs?.Any(x => ((ServerRegistration)x).IsSchedulingPublisher); + IServerRegistration? server = + regs?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); + + if (server == null) + { + server = new ServerRegistration(serverAddress, serverIdentity, DateTime.Now); + } + else + { + server.ServerAddress = serverAddress; // should not really change but it might! + server.UpdateDate = DateTime.Now; + } + + server.IsActive = true; + if (hasSchedulingPublisher == false) + { + server.IsSchedulingPublisher = true; + } + + _serverRegistrationRepository.Save(server); + _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); // triggers a cache reload + + // reload - cheap, cached + regs = _serverRegistrationRepository.GetMany().ToArray(); + + // default role is single server, but if registrations contain more + // than one active server, then role is scheduling publisher or subscriber + _currentServerRole = regs.Count(x => x.IsActive) > 1 + ? server.IsSchedulingPublisher ? ServerRole.SchedulingPublisher : ServerRole.Subscriber + : ServerRole.Single; + + scope.Complete(); + } + } + + /// + /// Deactivates a server. + /// + /// The server unique identity. + public void DeactiveServer(string serverIdentity) + { + // because the repository caches "all" and has queries disabled... + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.Servers); + + _serverRegistrationRepository + .ClearCache(); // ensure we have up-to-date cache // ensure we have up-to-date cache + + IServerRegistration? server = _serverRegistrationRepository.GetMany() + ?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); + if (server == null) + { + return; + } + + server.IsActive = server.IsSchedulingPublisher = false; + _serverRegistrationRepository.Save(server); // will trigger a cache reload // will trigger a cache reload + + scope.Complete(); + } + } + + /// + /// Deactivates stale servers. + /// + /// The time after which a server is considered stale. + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.Servers); + _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); + scope.Complete(); + } + } + + /// + /// Return all active servers. + /// + /// A value indicating whether to force-refresh the cache. + /// All active servers. + /// + /// By default this method will rely on the repository's cache, which is updated each + /// time the current server is touched, and the period depends on the configuration. Use the + /// parameter to force a cache refresh and reload active servers + /// from the database. + /// + public IEnumerable? GetActiveServers(bool refresh = false) => + GetServers(refresh).Where(x => x.IsActive); + + /// + /// Return all servers (active and inactive). + /// + /// A value indicating whether to force-refresh the cache. + /// All servers. + /// + /// By default this method will rely on the repository's cache, which is updated each + /// time the current server is touched, and the period depends on the configuration. Use the + /// parameter to force a cache refresh and reload all servers + /// from the database. + /// + public IEnumerable GetServers(bool refresh = false) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.Servers); + if (refresh) + { + _serverRegistrationRepository.ClearCache(); + } + + return _serverRegistrationRepository.GetMany().ToArray(); // fast, cached // fast, cached + } + } + + /// + /// Gets the role of the current server. + /// + /// The role of the current server. + public ServerRole GetCurrentServerRole() => _currentServerRole; + + /// + /// Gets the local server identity. + /// + private string GetCurrentServerIdentity() => Environment.MachineName // eg DOMAIN\SERVER + + "/" + _hostingEnvironment.ApplicationId; // eg /LM/S3SVC/11/ROOT; } diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index ea419f99f8..9def2bd8fa 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -1,275 +1,301 @@ -using System; +namespace Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Services +/// +/// Represents the Umbraco Service context, which provides access to all services. +/// +public class ServiceContext { + private readonly Lazy? _auditService; + private readonly Lazy? _consentService; + private readonly Lazy? _contentService; + private readonly Lazy? _contentTypeBaseServiceProvider; + private readonly Lazy? _contentTypeService; + private readonly Lazy? _dataTypeService; + private readonly Lazy? _domainService; + private readonly Lazy? _entityService; + private readonly Lazy? _externalLoginService; + private readonly Lazy? _fileService; + private readonly Lazy? _keyValueService; + private readonly Lazy? _localizationService; + private readonly Lazy? _localizedTextService; + private readonly Lazy? _macroService; + private readonly Lazy? _mediaService; + private readonly Lazy? _mediaTypeService; + private readonly Lazy? _memberGroupService; + private readonly Lazy? _memberService; + private readonly Lazy? _memberTypeService; + private readonly Lazy? _notificationService; + private readonly Lazy? _packagingService; + private readonly Lazy? _publicAccessService; + private readonly Lazy? _redirectUrlService; + private readonly Lazy? _relationService; + private readonly Lazy? _serverRegistrationService; + private readonly Lazy? _tagService; + private readonly Lazy? _userService; + /// - /// Represents the Umbraco Service context, which provides access to all services. + /// Initializes a new instance of the class with lazy services. /// - public class ServiceContext + public ServiceContext( + Lazy? publicAccessService, + Lazy? domainService, + Lazy? auditService, + Lazy? localizedTextService, + Lazy? tagService, + Lazy? contentService, + Lazy? userService, + Lazy? memberService, + Lazy? mediaService, + Lazy? contentTypeService, + Lazy? mediaTypeService, + Lazy? dataTypeService, + Lazy? fileService, + Lazy? localizationService, + Lazy? packagingService, + Lazy? serverRegistrationService, + Lazy? entityService, + Lazy? relationService, + Lazy? macroService, + Lazy? memberTypeService, + Lazy? memberGroupService, + Lazy? notificationService, + Lazy? externalLoginService, + Lazy? redirectUrlService, + Lazy? consentService, + Lazy? keyValueService, + Lazy? contentTypeBaseServiceProvider) { - private readonly Lazy? _publicAccessService; - private readonly Lazy? _domainService; - private readonly Lazy? _auditService; - private readonly Lazy? _localizedTextService; - private readonly Lazy? _tagService; - private readonly Lazy? _contentService; - private readonly Lazy? _userService; - private readonly Lazy? _memberService; - private readonly Lazy? _mediaService; - private readonly Lazy? _contentTypeService; - private readonly Lazy? _mediaTypeService; - private readonly Lazy? _dataTypeService; - private readonly Lazy? _fileService; - private readonly Lazy? _localizationService; - private readonly Lazy? _packagingService; - private readonly Lazy? _serverRegistrationService; - private readonly Lazy? _entityService; - private readonly Lazy? _relationService; - private readonly Lazy? _macroService; - private readonly Lazy? _memberTypeService; - private readonly Lazy? _memberGroupService; - private readonly Lazy? _notificationService; - private readonly Lazy? _externalLoginService; - private readonly Lazy? _redirectUrlService; - private readonly Lazy? _consentService; - private readonly Lazy? _keyValueService; - private readonly Lazy? _contentTypeBaseServiceProvider; + _publicAccessService = publicAccessService; + _domainService = domainService; + _auditService = auditService; + _localizedTextService = localizedTextService; + _tagService = tagService; + _contentService = contentService; + _userService = userService; + _memberService = memberService; + _mediaService = mediaService; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _dataTypeService = dataTypeService; + _fileService = fileService; + _localizationService = localizationService; + _packagingService = packagingService; + _serverRegistrationService = serverRegistrationService; + _entityService = entityService; + _relationService = relationService; + _macroService = macroService; + _memberTypeService = memberTypeService; + _memberGroupService = memberGroupService; + _notificationService = notificationService; + _externalLoginService = externalLoginService; + _redirectUrlService = redirectUrlService; + _consentService = consentService; + _keyValueService = keyValueService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + } - /// - /// Initializes a new instance of the class with lazy services. - /// - public ServiceContext(Lazy? publicAccessService, Lazy? domainService, Lazy? auditService, Lazy? localizedTextService, Lazy? tagService, Lazy? contentService, Lazy? userService, Lazy? memberService, Lazy? mediaService, Lazy? contentTypeService, Lazy? mediaTypeService, Lazy? dataTypeService, Lazy? fileService, Lazy? localizationService, Lazy? packagingService, Lazy? serverRegistrationService, Lazy? entityService, Lazy? relationService, Lazy? macroService, Lazy? memberTypeService, Lazy? memberGroupService, Lazy? notificationService, Lazy? externalLoginService, Lazy? redirectUrlService, Lazy? consentService, Lazy? keyValueService, Lazy? contentTypeBaseServiceProvider) + /// + /// Gets the + /// + public IPublicAccessService? PublicAccessService => _publicAccessService?.Value; + + /// + /// Gets the + /// + public IDomainService? DomainService => _domainService?.Value; + + /// + /// Gets the + /// + public IAuditService? AuditService => _auditService?.Value; + + /// + /// Gets the + /// + public ILocalizedTextService? TextService => _localizedTextService?.Value; + + /// + /// Gets the + /// + public INotificationService? NotificationService => _notificationService?.Value; + + /// + /// Gets the + /// + public IServerRegistrationService? ServerRegistrationService => _serverRegistrationService?.Value; + + /// + /// Gets the + /// + public ITagService? TagService => _tagService?.Value; + + /// + /// Gets the + /// + public IMacroService? MacroService => _macroService?.Value; + + /// + /// Gets the + /// + public IEntityService? EntityService => _entityService?.Value; + + /// + /// Gets the + /// + public IRelationService? RelationService => _relationService?.Value; + + /// + /// Gets the + /// + public IContentService? ContentService => _contentService?.Value; + + /// + /// Gets the + /// + public IContentTypeService? ContentTypeService => _contentTypeService?.Value; + + /// + /// Gets the + /// + public IMediaTypeService? MediaTypeService => _mediaTypeService?.Value; + + /// + /// Gets the + /// + public IDataTypeService? DataTypeService => _dataTypeService?.Value; + + /// + /// Gets the + /// + public IFileService? FileService => _fileService?.Value; + + /// + /// Gets the + /// + public ILocalizationService? LocalizationService => _localizationService?.Value; + + /// + /// Gets the + /// + public IMediaService? MediaService => _mediaService?.Value; + + /// + /// Gets the + /// + public IPackagingService? PackagingService => _packagingService?.Value; + + /// + /// Gets the + /// + public IUserService? UserService => _userService?.Value; + + /// + /// Gets the + /// + public IMemberService? MemberService => _memberService?.Value; + + /// + /// Gets the MemberTypeService + /// + public IMemberTypeService? MemberTypeService => _memberTypeService?.Value; + + /// + /// Gets the MemberGroupService + /// + public IMemberGroupService? MemberGroupService => _memberGroupService?.Value; + + /// + /// Gets the ExternalLoginService. + /// + public IExternalLoginWithKeyService? ExternalLoginService => _externalLoginService?.Value; + + /// + /// Gets the RedirectUrlService. + /// + public IRedirectUrlService? RedirectUrlService => _redirectUrlService?.Value; + + /// + /// Gets the ConsentService. + /// + public IConsentService? ConsentService => _consentService?.Value; + + /// + /// Gets the KeyValueService. + /// + public IKeyValueService? KeyValueService => _keyValueService?.Value; + + /// + /// Gets the ContentTypeServiceBaseFactory. + /// + public IContentTypeBaseServiceProvider? ContentTypeBaseServices => _contentTypeBaseServiceProvider?.Value; + + /// + /// Creates a partial service context with only some services (for tests). + /// + /// + /// Using a true constructor for this confuses DI containers. + /// + public static ServiceContext CreatePartial( + IContentService? contentService = null, + IMediaService? mediaService = null, + IContentTypeService? contentTypeService = null, + IMediaTypeService? mediaTypeService = null, + IDataTypeService? dataTypeService = null, + IFileService? fileService = null, + ILocalizationService? localizationService = null, + IPackagingService? packagingService = null, + IEntityService? entityService = null, + IRelationService? relationService = null, + IMemberGroupService? memberGroupService = null, + IMemberTypeService? memberTypeService = null, + IMemberService? memberService = null, + IUserService? userService = null, + ITagService? tagService = null, + INotificationService? notificationService = null, + ILocalizedTextService? localizedTextService = null, + IAuditService? auditService = null, + IDomainService? domainService = null, + IMacroService? macroService = null, + IPublicAccessService? publicAccessService = null, + IExternalLoginWithKeyService? externalLoginService = null, + IServerRegistrationService? serverRegistrationService = null, + IRedirectUrlService? redirectUrlService = null, + IConsentService? consentService = null, + IKeyValueService? keyValueService = null, + IContentTypeBaseServiceProvider? contentTypeBaseServiceProvider = null) + { + Lazy? Lazy(T? service) { - _publicAccessService = publicAccessService; - _domainService = domainService; - _auditService = auditService; - _localizedTextService = localizedTextService; - _tagService = tagService; - _contentService = contentService; - _userService = userService; - _memberService = memberService; - _mediaService = mediaService; - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _dataTypeService = dataTypeService; - _fileService = fileService; - _localizationService = localizationService; - _packagingService = packagingService; - _serverRegistrationService = serverRegistrationService; - _entityService = entityService; - _relationService = relationService; - _macroService = macroService; - _memberTypeService = memberTypeService; - _memberGroupService = memberGroupService; - _notificationService = notificationService; - _externalLoginService = externalLoginService; - _redirectUrlService = redirectUrlService; - _consentService = consentService; - _keyValueService = keyValueService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + return service == null ? null : new Lazy(() => service); } - /// - /// Creates a partial service context with only some services (for tests). - /// - /// - /// Using a true constructor for this confuses DI containers. - /// - public static ServiceContext CreatePartial( - IContentService? contentService = null, - IMediaService? mediaService = null, - IContentTypeService? contentTypeService = null, - IMediaTypeService? mediaTypeService = null, - IDataTypeService? dataTypeService = null, - IFileService? fileService = null, - ILocalizationService? localizationService = null, - IPackagingService? packagingService = null, - IEntityService? entityService = null, - IRelationService? relationService = null, - IMemberGroupService? memberGroupService = null, - IMemberTypeService? memberTypeService = null, - IMemberService? memberService = null, - IUserService? userService = null, - ITagService? tagService = null, - INotificationService? notificationService = null, - ILocalizedTextService? localizedTextService = null, - IAuditService? auditService = null, - IDomainService? domainService = null, - IMacroService? macroService = null, - IPublicAccessService? publicAccessService = null, - IExternalLoginWithKeyService? externalLoginService = null, - IServerRegistrationService? serverRegistrationService = null, - IRedirectUrlService? redirectUrlService = null, - IConsentService? consentService = null, - IKeyValueService? keyValueService = null, - IContentTypeBaseServiceProvider? contentTypeBaseServiceProvider = null) - { - Lazy? Lazy(T? service) => service == null ? null : new Lazy(() => service); - - return new ServiceContext( - Lazy(publicAccessService), - Lazy(domainService), - Lazy(auditService), - Lazy(localizedTextService), - Lazy(tagService), - Lazy(contentService), - Lazy(userService), - Lazy(memberService), - Lazy(mediaService), - Lazy(contentTypeService), - Lazy(mediaTypeService), - Lazy(dataTypeService), - Lazy(fileService), - Lazy(localizationService), - Lazy(packagingService), - Lazy(serverRegistrationService), - Lazy(entityService), - Lazy(relationService), - Lazy(macroService), - Lazy(memberTypeService), - Lazy(memberGroupService), - Lazy(notificationService), - Lazy(externalLoginService), - Lazy(redirectUrlService), - Lazy(consentService), - Lazy(keyValueService), - Lazy(contentTypeBaseServiceProvider) - ); - } - - /// - /// Gets the - /// - public IPublicAccessService? PublicAccessService => _publicAccessService?.Value; - - /// - /// Gets the - /// - public IDomainService? DomainService => _domainService?.Value; - - /// - /// Gets the - /// - public IAuditService? AuditService => _auditService?.Value; - - /// - /// Gets the - /// - public ILocalizedTextService? TextService => _localizedTextService?.Value; - - /// - /// Gets the - /// - public INotificationService? NotificationService => _notificationService?.Value; - - /// - /// Gets the - /// - public IServerRegistrationService? ServerRegistrationService => _serverRegistrationService?.Value; - - /// - /// Gets the - /// - public ITagService? TagService => _tagService?.Value; - - /// - /// Gets the - /// - public IMacroService? MacroService => _macroService?.Value; - - /// - /// Gets the - /// - public IEntityService? EntityService => _entityService?.Value; - - /// - /// Gets the - /// - public IRelationService? RelationService => _relationService?.Value; - - /// - /// Gets the - /// - public IContentService? ContentService => _contentService?.Value; - - /// - /// Gets the - /// - public IContentTypeService? ContentTypeService => _contentTypeService?.Value; - - /// - /// Gets the - /// - public IMediaTypeService? MediaTypeService => _mediaTypeService?.Value; - - /// - /// Gets the - /// - public IDataTypeService? DataTypeService => _dataTypeService?.Value; - - /// - /// Gets the - /// - public IFileService? FileService => _fileService?.Value; - - /// - /// Gets the - /// - public ILocalizationService? LocalizationService => _localizationService?.Value; - - /// - /// Gets the - /// - public IMediaService? MediaService => _mediaService?.Value; - - /// - /// Gets the - /// - public IPackagingService? PackagingService => _packagingService?.Value; - - /// - /// Gets the - /// - public IUserService? UserService => _userService?.Value; - - /// - /// Gets the - /// - public IMemberService? MemberService => _memberService?.Value; - - /// - /// Gets the MemberTypeService - /// - public IMemberTypeService? MemberTypeService => _memberTypeService?.Value; - - /// - /// Gets the MemberGroupService - /// - public IMemberGroupService? MemberGroupService => _memberGroupService?.Value; - - /// - /// Gets the ExternalLoginService. - /// - public IExternalLoginWithKeyService? ExternalLoginService => _externalLoginService?.Value; - - /// - /// Gets the RedirectUrlService. - /// - public IRedirectUrlService? RedirectUrlService => _redirectUrlService?.Value; - - /// - /// Gets the ConsentService. - /// - public IConsentService? ConsentService => _consentService?.Value; - - /// - /// Gets the KeyValueService. - /// - public IKeyValueService? KeyValueService => _keyValueService?.Value; - - /// - /// Gets the ContentTypeServiceBaseFactory. - /// - public IContentTypeBaseServiceProvider? ContentTypeBaseServices => _contentTypeBaseServiceProvider?.Value; + return new ServiceContext( + Lazy(publicAccessService), + Lazy(domainService), + Lazy(auditService), + Lazy(localizedTextService), + Lazy(tagService), + Lazy(contentService), + Lazy(userService), + Lazy(memberService), + Lazy(mediaService), + Lazy(contentTypeService), + Lazy(mediaTypeService), + Lazy(dataTypeService), + Lazy(fileService), + Lazy(localizationService), + Lazy(packagingService), + Lazy(serverRegistrationService), + Lazy(entityService), + Lazy(relationService), + Lazy(macroService), + Lazy(memberTypeService), + Lazy(memberGroupService), + Lazy(notificationService), + Lazy(externalLoginService), + Lazy(redirectUrlService), + Lazy(consentService), + Lazy(keyValueService), + Lazy(contentTypeBaseServiceProvider)); } } diff --git a/src/Umbraco.Core/Services/TagService.cs b/src/Umbraco.Core/Services/TagService.cs index 65e4a32f9e..c75863f6de 100644 --- a/src/Umbraco.Core/Services/TagService.cs +++ b/src/Umbraco.Core/Services/TagService.cs @@ -1,172 +1,167 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & +/// saved media or members +/// +/// +/// If there is unpublished content with tags, those tags will not be contained +/// +public class TagService : RepositoryService, ITagService { - /// - /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members - /// - /// - /// If there is unpublished content with tags, those tags will not be contained - /// - public class TagService : RepositoryService, ITagService + private readonly ITagRepository _tagRepository; + + public TagService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, ITagRepository tagRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _tagRepository = tagRepository; + + /// + public TaggedEntity? GetTaggedEntityById(int id) { - private readonly ITagRepository _tagRepository; - - public TagService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - ITagRepository tagRepository) - : base(provider, loggerFactory, eventMessagesFactory) + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - _tagRepository = tagRepository; + return _tagRepository.GetTaggedEntityById(id); } + } - /// - public TaggedEntity? GetTaggedEntityById(int id) + /// + public TaggedEntity? GetTaggedEntityByKey(Guid key) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntityById(id); - } + return _tagRepository.GetTaggedEntityByKey(key); } + } - /// - public TaggedEntity? GetTaggedEntityByKey(Guid key) + /// + public IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntityByKey(key); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); } + } - /// - public IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); } + } - /// - public IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null) + /// + public IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); } + } - /// - public IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); } + } - /// - public IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null) + /// + public IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); } + } - /// - public IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); } + } - /// - public IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null) + /// + public IEnumerable GetAllTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); } + } - /// - public IEnumerable GetAllTags(string? group = null, string? culture = null) + /// + public IEnumerable GetAllContentTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); } + } - /// - public IEnumerable GetAllContentTags(string? group = null, string? culture = null) + /// + public IEnumerable GetAllMediaTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); } + } - /// - public IEnumerable GetAllMediaTags(string? group = null, string? culture = null) + /// + public IEnumerable GetAllMemberTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); } + } - /// - public IEnumerable GetAllMemberTags(string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); - } + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } + } - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); - } + return _tagRepository.GetTagsForEntity(contentId, group, culture); } + } - /// - public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntity(contentId, group, culture); - } + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } + } - /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); - } - } - - /// - public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) - { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntity(contentId, group, culture); - } + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } } diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs index ab5a09ce8b..32dc9c18cc 100644 --- a/src/Umbraco.Core/Services/TrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs @@ -2,58 +2,60 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class TrackedReferencesService : ITrackedReferencesService { - public class TrackedReferencesService : ITrackedReferencesService + private readonly IEntityService _entityService; + private readonly ICoreScopeProvider _scopeProvider; + private readonly ITrackedReferencesRepository _trackedReferencesRepository; + + public TrackedReferencesService( + ITrackedReferencesRepository trackedReferencesRepository, + ICoreScopeProvider scopeProvider, + IEntityService entityService) { - private readonly ITrackedReferencesRepository _trackedReferencesRepository; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IEntityService _entityService; + _trackedReferencesRepository = trackedReferencesRepository; + _scopeProvider = scopeProvider; + _entityService = entityService; + } - public TrackedReferencesService(ITrackedReferencesRepository trackedReferencesRepository, ICoreScopeProvider scopeProvider, IEntityService entityService) - { - _trackedReferencesRepository = trackedReferencesRepository; - _scopeProvider = scopeProvider; - _entityService = entityService; - } + /// + /// Gets a paged result of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + public PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedRelationsForItem(id, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - /// - /// Gets a paged result of items which are in relation with the current item. - /// Basically, shows the items which depend on the current item. - /// - public PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - var items = _trackedReferencesRepository.GetPagedRelationsForItem(id, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; + } - return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; - } + /// + /// Gets a paged result of items used in any kind of relation from selected integer ids. + /// + public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - /// - /// Gets a paged result of items used in any kind of relation from selected integer ids. - /// - public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; + } - return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; - } + /// + /// Gets a paged result of the descending items that have any references, given a parent id. + /// + public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - /// - /// Gets a paged result of the descending items that have any references, given a parent id. - /// - public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - - var items = _trackedReferencesRepository.GetPagedDescendantsInReferences( - parentId, - pageIndex, - pageSize, - filterMustBeIsDependency, - out var totalItems); - return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; - } + IEnumerable items = _trackedReferencesRepository.GetPagedDescendantsInReferences( + parentId, + pageIndex, + pageSize, + filterMustBeIsDependency, + out var totalItems); + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; } } diff --git a/src/Umbraco.Core/Services/TreeService.cs b/src/Umbraco.Core/Services/TreeService.cs index f325712d77..3b2b5f3618 100644 --- a/src/Umbraco.Core/Services/TreeService.cs +++ b/src/Umbraco.Core/Services/TreeService.cs @@ -1,45 +1,41 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Trees; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Implements . +/// +public class TreeService : ITreeService { + private readonly TreeCollection _treeCollection; + /// - /// Implements . + /// Initializes a new instance of the class. /// - public class TreeService : ITreeService - { - private readonly TreeCollection _treeCollection; + /// + public TreeService(TreeCollection treeCollection) => _treeCollection = treeCollection; - /// - /// Initializes a new instance of the class. - /// - /// - public TreeService(TreeCollection treeCollection) - { - _treeCollection = treeCollection; - } + /// + public Tree? GetByAlias(string treeAlias) => _treeCollection.FirstOrDefault(x => x.TreeAlias == treeAlias); - /// - public Tree? GetByAlias(string treeAlias) => _treeCollection.FirstOrDefault(x => x.TreeAlias == treeAlias); + /// + public IEnumerable GetAll(TreeUse use = TreeUse.Main) - /// - public IEnumerable GetAll(TreeUse use = TreeUse.Main) - // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees - => _treeCollection.Where(x => x.TreeUse.HasFlagAny(use)); + // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees + => _treeCollection.Where(x => x.TreeUse.HasFlagAny(use)); - /// - public IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main) - // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees - => _treeCollection.Where(x => x.SectionAlias.InvariantEquals(sectionAlias) && x.TreeUse.HasFlagAny(use)).OrderBy(x => x.SortOrder).ToList(); + /// + public IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main) - /// - public IDictionary> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main) - { - return GetBySection(sectionAlias, use).GroupBy(x => x.TreeGroup).ToDictionary( - x => x.Key ?? "", - x => (IEnumerable) x.ToArray()); - } - } + // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees + => _treeCollection.Where(x => x.SectionAlias.InvariantEquals(sectionAlias) && x.TreeUse.HasFlagAny(use)) + .OrderBy(x => x.SortOrder).ToList(); + + /// + public IDictionary> + GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main) => + GetBySection(sectionAlias, use).GroupBy(x => x.TreeGroup).ToDictionary( + x => x.Key ?? string.Empty, + x => (IEnumerable)x.ToArray()); } diff --git a/src/Umbraco.Core/Services/TwoFactorLoginService.cs b/src/Umbraco.Core/Services/TwoFactorLoginService.cs index 426d100e93..acbdded1c9 100644 --- a/src/Umbraco.Core/Services/TwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/TwoFactorLoginService.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -12,199 +8,195 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +public class TwoFactorLoginService : ITwoFactorLoginService { - /// - public class TwoFactorLoginService : ITwoFactorLoginService + private readonly IOptions _backOfficeIdentityOptions; + private readonly IOptions _identityOptions; + private readonly ILogger _logger; + private readonly ICoreScopeProvider _scopeProvider; + private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; + private readonly IDictionary _twoFactorSetupGenerators; + + /// + /// Initializes a new instance of the class. + /// + public TwoFactorLoginService( + ITwoFactorLoginRepository twoFactorLoginRepository, + ICoreScopeProvider scopeProvider, + IEnumerable twoFactorSetupGenerators, + IOptions identityOptions, + IOptions backOfficeIdentityOptions, + ILogger logger) { - private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IOptions _identityOptions; - private readonly IOptions _backOfficeIdentityOptions; - private readonly IDictionary _twoFactorSetupGenerators; - private readonly ILogger _logger; + _twoFactorLoginRepository = twoFactorLoginRepository; + _scopeProvider = scopeProvider; + _identityOptions = identityOptions; + _backOfficeIdentityOptions = backOfficeIdentityOptions; + _logger = logger; + _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x => x.ProviderName); + } - /// - /// Initializes a new instance of the class. - /// - public TwoFactorLoginService( - ITwoFactorLoginRepository twoFactorLoginRepository, - ICoreScopeProvider scopeProvider, - IEnumerable twoFactorSetupGenerators, - IOptions identityOptions, - IOptions backOfficeIdentityOptions, - ILogger logger) + /// + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); + } + + /// + public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) => + await GetEnabledProviderNamesAsync(userOrMemberKey); + + public async Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code) + { + var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) { - _twoFactorLoginRepository = twoFactorLoginRepository; - _scopeProvider = scopeProvider; - _identityOptions = identityOptions; - _backOfficeIdentityOptions = backOfficeIdentityOptions; - _logger = logger; - _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName); + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); } - /// - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + var isValid = secret is not null && generator.ValidateTwoFactorPIN(secret, code); + + if (!isValid) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); + return false; } - /// - public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) + return await DisableAsync(userOrMemberKey, providerName); + } + + public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + { + try { - return await GetEnabledProviderNamesAsync(userOrMemberKey); - } - - public async Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code) - { - var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); - - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } - - var isValid = secret is not null && generator.ValidateTwoFactorPIN(secret, code); - - if (!isValid) + var isValid = ValidateTwoFactorSetup(providerName, secret, code); + if (isValid == false) { return false; } - return await DisableAsync(userOrMemberKey, providerName); + var twoFactorLogin = new TwoFactorLogin + { + Confirmed = true, + Secret = secret, + UserOrMemberKey = userOrMemberKey, + ProviderName = providerName, + }; + + await SaveAsync(twoFactorLogin); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not log in with the provided one-time-password"); } - public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + return false; + } + + /// + public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) => + (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); + + /// + public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) + .FirstOrDefault(x => x.ProviderName == providerName)?.Secret; + } + + /// + public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) + { + var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + + // Dont allow to generate a new secrets if user already has one + if (!string.IsNullOrEmpty(secret)) { + return default; + } - try - { - var isValid = ValidateTwoFactorSetup(providerName, secret, code); - if (isValid == false) - { - return false; - } + secret = GenerateSecret(); - var twoFactorLogin = new TwoFactorLogin() - { - Confirmed = true, - Secret = secret, - UserOrMemberKey = userOrMemberKey, - ProviderName = providerName - }; + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } - await SaveAsync(twoFactorLogin); + return await generator.GetSetupDataAsync(userOrMemberKey, secret); + } - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not log in with the provided one-time-password"); - } + /// + public IEnumerable GetAllProviderNames() => _twoFactorSetupGenerators.Keys; + /// + public async Task DisableAsync(Guid userOrMemberKey, string providerName) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); + } + + /// + public bool ValidateTwoFactorSetup(string providerName, string secret, string code) + { + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } + + return generator.ValidateTwoFactorSetup(secret, code); + } + + /// + public Task SaveAsync(TwoFactorLogin twoFactorLogin) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + _twoFactorLoginRepository.Save(twoFactorLogin); + + return Task.CompletedTask; + } + + /// + /// Generates a new random unique secret. + /// + /// The random secret + protected virtual string GenerateSecret() => Guid.NewGuid().ToString(); + + private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) + .Select(x => x.ProviderName).ToArray(); + + return providersOnUser.Where(IsKnownProviderName); + } + + /// + /// The provider needs to be registered as either a member provider or backoffice provider to show up. + /// + private bool IsKnownProviderName(string? providerName) + { + if (providerName is null) + { return false; } - private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) + if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) - .Select(x => x.ProviderName).ToArray(); - - return providersOnUser.Where(IsKnownProviderName)!; + return true; } - /// - /// The provider needs to be registered as either a member provider or backoffice provider to show up. - /// - private bool IsKnownProviderName(string? providerName) + if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) { - if (providerName is null) - { - return false; - } - if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) - { - return true; - } - - if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) - { - return true; - } - - return false; + return true; } - /// - public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) - { - return (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); - } - - /// - public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x => x.ProviderName == providerName)?.Secret; - } - - /// - public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) - { - var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); - - // Dont allow to generate a new secrets if user already has one - if (!string.IsNullOrEmpty(secret)) - { - return default; - } - - secret = GenerateSecret(); - - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } - - return await generator.GetSetupDataAsync(userOrMemberKey, secret); - } - - /// - public IEnumerable GetAllProviderNames() => _twoFactorSetupGenerators.Keys; - - /// - public async Task DisableAsync(Guid userOrMemberKey, string providerName) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); - } - - /// - public bool ValidateTwoFactorSetup(string providerName, string secret, string code) - { - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } - - return generator.ValidateTwoFactorSetup(secret, code); - } - - /// - public Task SaveAsync(TwoFactorLogin twoFactorLogin) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - _twoFactorLoginRepository.Save(twoFactorLogin); - - return Task.CompletedTask; - } - - /// - /// Generates a new random unique secret. - /// - /// The random secret - protected virtual string GenerateSecret() => Guid.NewGuid().ToString(); + return false; } } diff --git a/src/Umbraco.Core/Services/UpgradeService.cs b/src/Umbraco.Core/Services/UpgradeService.cs index e2003f8370..7a5269d2bf 100644 --- a/src/Umbraco.Core/Services/UpgradeService.cs +++ b/src/Umbraco.Core/Services/UpgradeService.cs @@ -1,21 +1,15 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class UpgradeService : IUpgradeService { - public class UpgradeService : IUpgradeService - { - private readonly IUpgradeCheckRepository _upgradeCheckRepository; + private readonly IUpgradeCheckRepository _upgradeCheckRepository; - public UpgradeService(IUpgradeCheckRepository upgradeCheckRepository) - { - _upgradeCheckRepository = upgradeCheckRepository; - } + public UpgradeService(IUpgradeCheckRepository upgradeCheckRepository) => + _upgradeCheckRepository = upgradeCheckRepository; - public async Task CheckUpgrade(SemVersion version) - { - return await _upgradeCheckRepository.CheckUpgradeAsync(version); - } - } + public async Task CheckUpgrade(SemVersion version) => + await _upgradeCheckRepository.CheckUpgradeAsync(version); } diff --git a/src/Umbraco.Core/Services/UserDataService.cs b/src/Umbraco.Core/Services/UserDataService.cs index a3c6bd11b4..14b2e581f9 100644 --- a/src/Umbraco.Core/Services/UserDataService.cs +++ b/src/Umbraco.Core/Services/UserDataService.cs @@ -1,51 +1,45 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; -using System.Threading; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +[Obsolete("Use the IUserDataService interface instead")] +public class UserDataService : IUserDataService { - [Obsolete("Use the IUserDataService interface instead")] - public class UserDataService : IUserDataService + private readonly ILocalizationService _localizationService; + private readonly IUmbracoVersion _version; + + public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) { - private readonly IUmbracoVersion _version; - private readonly ILocalizationService _localizationService; - - - public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) - { - _version = version; - _localizationService = localizationService; - } - - public IEnumerable GetUserData() => - new List - { - new("Server OS", RuntimeInformation.OSDescription), - new("Server Framework", RuntimeInformation.FrameworkDescription), - new("Default Language", _localizationService.GetDefaultLanguageIsoCode()), - new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()), - new("Current Culture", Thread.CurrentThread.CurrentCulture.ToString()), - new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), - new("Current Webserver", GetCurrentWebServer()) - }; - - private string GetCurrentWebServer() => IsRunningInProcessIIS() ? "IIS" : "Kestrel"; - - public bool IsRunningInProcessIIS() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return false; - } - - string processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); - return (processName.Contains("w3wp") || processName.Contains("iisexpress")); - } + _version = version; + _localizationService = localizationService; } + + public IEnumerable GetUserData() => + new List + { + new("Server OS", RuntimeInformation.OSDescription), + new("Server Framework", RuntimeInformation.FrameworkDescription), + new("Default Language", _localizationService.GetDefaultLanguageIsoCode()), + new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()), + new("Current Culture", Thread.CurrentThread.CurrentCulture.ToString()), + new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), + new("Current Webserver", GetCurrentWebServer()), + }; + + public bool IsRunningInProcessIIS() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + + var processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); + return processName.Contains("w3wp") || processName.Contains("iisexpress"); + } + + private string GetCurrentWebServer() => IsRunningInProcessIIS() ? "IIS" : "Kestrel"; } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 9dfc09d6d7..69e6351fbd 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; using System.Data.Common; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,1152 +13,1306 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents the UserService, which is an easy access to operations involving , +/// and eventually Backoffice Users. +/// +internal class UserService : RepositoryService, IUserService { - /// - /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. - /// - internal class UserService : RepositoryService, IUserService + private readonly GlobalSettings _globalSettings; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IUserGroupRepository _userGroupRepository; + private readonly IUserRepository _userRepository; + + public UserService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IRuntimeState runtimeState, + IUserRepository userRepository, + IUserGroupRepository userGroupRepository, + IOptions globalSettings) + : base(provider, loggerFactory, eventMessagesFactory) { - private readonly IRuntimeState _runtimeState; - private readonly IUserRepository _userRepository; - private readonly IUserGroupRepository _userGroupRepository; - private readonly GlobalSettings _globalSettings; - private readonly ILogger _logger; + _runtimeState = runtimeState; + _userRepository = userRepository; + _userGroupRepository = userGroupRepository; + _globalSettings = globalSettings.Value; + _logger = loggerFactory.CreateLogger(); + } - public UserService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IRuntimeState runtimeState, - IUserRepository userRepository, IUserGroupRepository userGroupRepository, IOptions globalSettings) - : base(provider, loggerFactory, eventMessagesFactory) + private bool IsUpgrading => + _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade; + + /// + /// Checks in a set of permissions associated with a user for those related to a given nodeId + /// + /// The set of permissions + /// The node Id + /// The permissions to return + /// True if permissions for the given path are found + public static bool TryGetAssignedPermissionsForNode( + IList permissions, + int nodeId, + out string assignedPermissions) + { + if (permissions.Any(x => x.EntityId == nodeId)) { - _runtimeState = runtimeState; - _userRepository = userRepository; - _userGroupRepository = userGroupRepository; - _globalSettings = globalSettings.Value; - _logger = loggerFactory.CreateLogger(); - } + EntityPermission found = permissions.First(x => x.EntityId == nodeId); + var assignedPermissionsArray = found.AssignedPermissions.ToList(); - private bool IsUpgrading => _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade; - - #region Implementation of IMembershipUserService - - /// - /// Checks if a User with the username exists - /// - /// Username to check - /// True if the User exists otherwise False - public bool Exists(string username) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + // Working with permissions assigned directly to a user AND to their groups, so maybe several per node + // and we need to get the most permissive set + foreach (EntityPermission permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) { - return _userRepository.ExistsByUserName(username); + AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); } + + assignedPermissions = string.Join(string.Empty, assignedPermissionsArray); + return true; } - /// - /// Creates a new User - /// - /// The user will be saved in the database and returned with an Id - /// Username of the user to create - /// Email of the user to create - /// - public IUser CreateUserWithIdentity(string username, string email) + assignedPermissions = string.Empty; + return false; + } + + #region Implementation of IMembershipUserService + + /// + /// Checks if a User with the username exists + /// + /// Username to check + /// True if the User exists otherwise False + public bool Exists(string username) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - return CreateUserWithIdentity(username, email, string.Empty); + return _userRepository.ExistsByUserName(username); + } + } + + /// + /// Creates a new User + /// + /// The user will be saved in the database and returned with an Id + /// Username of the user to create + /// Email of the user to create + /// + /// + /// + public IUser CreateUserWithIdentity(string username, string email) => + CreateUserWithIdentity(username, email, string.Empty); + + /// + /// Creates and persists a new + /// + /// Username of the to create + /// Email of the to create + /// + /// This value should be the encoded/encrypted/hashed value for the password that will be + /// stored in the database + /// + /// Not used for users + /// + /// + /// + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) => CreateUserWithIdentity(username, email, passwordValue); + + /// + /// Creates and persists a new + /// + /// Username of the to create + /// Email of the to create + /// + /// This value should be the encoded/encrypted/hashed value for the password that will be + /// stored in the database + /// + /// Alias of the Type + /// Is the member approved + /// + /// + /// + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) => CreateUserWithIdentity(username, email, passwordValue, isApproved); + + /// + /// Gets a User by its integer id + /// + /// Id + /// + /// + /// + public IUser? GetById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.Get(id); + } + } + + /// + /// Creates and persists a Member + /// + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// + /// Username of the Member to create + /// Email of the Member to create + /// + /// This value should be the encoded/encrypted/hashed value for the password that will be + /// stored in the database + /// + /// Is the user approved + /// + /// + /// + private IUser CreateUserWithIdentity(string username, string email, string passwordValue, bool isApproved = true) + { + if (username == null) + { + throw new ArgumentNullException(nameof(username)); } - /// - /// Creates and persists a new - /// - /// Username of the to create - /// Email of the to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Not used for users - /// - IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) + if (string.IsNullOrWhiteSpace(username)) { - return CreateUserWithIdentity(username, email, passwordValue); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); } - /// - /// Creates and persists a new - /// - /// Username of the to create - /// Email of the to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Alias of the Type - /// Is the member approved - /// - IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) + EventMessages evtMsgs = EventMessagesFactory.Get(); + + // TODO: PUT lock here!! + User user; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - return CreateUserWithIdentity(username, email, passwordValue, isApproved); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Is the user approved - /// - private IUser CreateUserWithIdentity(string username, string email, string passwordValue, bool isApproved = true) - { - if (username == null) throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); - - var evtMsgs = EventMessagesFactory.Get(); - - // TODO: PUT lock here!! - - User user; - using (var scope = ScopeProvider.CreateCoreScope()) + var loginExists = _userRepository.ExistsByLogin(username); + if (loginExists) { - var loginExists = _userRepository.ExistsByLogin(username); - if (loginExists) - throw new ArgumentException("Login already exists"); // causes rollback + throw new ArgumentException("Login already exists"); // causes rollback + } - user = new User(_globalSettings) - { - Email = email, - Language = _globalSettings.DefaultUILanguage, - Name = username, - RawPasswordValue = passwordValue, - Username = username, - IsLockedOut = false, - IsApproved = isApproved - }; + user = new User(_globalSettings) + { + Email = email, + Language = _globalSettings.DefaultUILanguage, + Name = username, + RawPasswordValue = passwordValue, + Username = username, + IsLockedOut = false, + IsApproved = isApproved, + }; - var savingNotification = new UserSavingNotification(user, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return user; - } - - _userRepository.Save(user); - - scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); + var savingNotification = new UserSavingNotification(user, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { scope.Complete(); + return user; } - return user; + _userRepository.Save(user); + + scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); + scope.Complete(); } - /// - /// Gets a User by its integer id - /// - /// Id - /// - public IUser? GetById(int id) + return user; + } + + /// + /// Gets an by its provider key + /// + /// Id to use for retrieval + /// + /// + /// + public IUser? GetByProviderKey(object id) + { + Attempt asInt = id.TryConvertTo(); + return asInt.Success ? GetById(asInt.Result) : null; + } + + /// + /// Get an by email + /// + /// Email to use for retrieval + /// + /// + /// + public IUser? GetByEmail(string email) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + IQuery query = Query().Where(x => x.Email.Equals(email)); + return _userRepository.Get(query)?.FirstOrDefault(); + } + } + + /// + /// Get an by username + /// + /// Username to use for retrieval + /// + /// + /// + public IUser? GetByUsername(string? username) + { + if (username is null) + { + return null; + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + try { - return _userRepository.Get(id); + return _userRepository.GetByUsername(username, true); } - } - - /// - /// Gets an by its provider key - /// - /// Id to use for retrieval - /// - public IUser? GetByProviderKey(object id) - { - var asInt = id.TryConvertTo(); - return asInt.Success ? GetById(asInt.Result) : null; - } - - /// - /// Get an by email - /// - /// Email to use for retrieval - /// - public IUser? GetByEmail(string email) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + catch (DbException) { - var query = Query().Where(x => x.Email.Equals(email)); - return _userRepository.Get(query)?.FirstOrDefault(); - } - } - - /// - /// Get an by username - /// - /// Username to use for retrieval - /// - public IUser? GetByUsername(string? username) - { - if (username is null) - { - return null; - } - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - try + // TODO: refactor users/upgrade + // currently kinda accepting anything on upgrade, but that won't deal with all cases + // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should + // be better BUT requires that the app restarts after the upgrade! + if (IsUpgrading) { - return _userRepository.GetByUsername(username, includeSecurityData: true); - } - catch (DbException) - { - // TODO: refactor users/upgrade - // currently kinda accepting anything on upgrade, but that won't deal with all cases - // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should - // be better BUT requires that the app restarts after the upgrade! - if (IsUpgrading) - { - //NOTE: this will not be cached - return _userRepository.GetByUsername(username, includeSecurityData: false); - } - - throw; - } - } - } - - /// - /// Disables an - /// - /// to disable - public void Delete(IUser membershipUser) - { - //disable - membershipUser.IsApproved = false; - - Save(membershipUser); - } - - /// - /// Deletes or disables a User - /// - /// to delete - /// True to permanently delete the user, False to disable the user - public void Delete(IUser user, bool deletePermanently) - { - if (deletePermanently == false) - { - Delete(user); - } - else - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var deletingNotification = new UserDeletingNotification(user, evtMsgs); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } - - _userRepository.Delete(user); - - scope.Notifications.Publish(new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); - scope.Complete(); - } - } - } - - /// - /// Saves an - /// - /// to Save - public void Save(IUser entity) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var savingNotification = new UserSavingNotification(entity, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; + // NOTE: this will not be cached + return _userRepository.GetByUsername(username, false); } - if (string.IsNullOrWhiteSpace(entity.Username)) - throw new ArgumentException("Empty username.", nameof(entity)); - - if (string.IsNullOrWhiteSpace(entity.Name)) - throw new ArgumentException("Empty name.", nameof(entity)); - - try - { - _userRepository.Save(entity); - scope.Notifications.Publish(new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); - - scope.Complete(); - } - catch (DbException ex) - { - // if we are upgrading and an exception occurs, log and swallow it - if (IsUpgrading == false) throw; - - _logger.LogWarning(ex, "An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored"); - - // we don't want the uow to rollback its scope! - scope.Complete(); - } + throw; } } + } - /// - /// Saves a list of objects - /// - /// to save - public void Save(IEnumerable entities) + /// + /// Disables an + /// + /// to disable + public void Delete(IUser membershipUser) + { + // disable + membershipUser.IsApproved = false; + + Save(membershipUser); + } + + /// + /// Deletes or disables a User + /// + /// to delete + /// True to permanently delete the user, False to disable the user + public void Delete(IUser user, bool deletePermanently) + { + if (deletePermanently == false) { - var evtMsgs = EventMessagesFactory.Get(); - - var entitiesA = entities.ToArray(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - foreach (var user in entitiesA) - { - if (string.IsNullOrWhiteSpace(user.Username)) - throw new ArgumentException("Empty username.", nameof(entities)); - - if (string.IsNullOrWhiteSpace(user.Name)) - throw new ArgumentException("Empty name.", nameof(entities)); - - _userRepository.Save(user); - - } - - scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); - - //commit the whole lot in one go - scope.Complete(); - } + Delete(user); } - - /// - /// This is just the default user group that the membership provider will use - /// - /// - public string GetDefaultMemberType() + else { - return Cms.Core.Constants.Security.WriterGroupAlias; - } + EventMessages evtMsgs = EventMessagesFactory.Get(); - /// - /// Finds a list of objects by a partial email string - /// - /// Partial email string to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query?.Where(member => member.Email.Equals(emailStringToMatch)); - break; - case StringPropertyMatchType.Contains: - query?.Where(member => member.Email.Contains(emailStringToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query?.Where(member => member.Email.StartsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query?.Where(member => member.Email.EndsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - - return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Email); - } - } - - /// - /// Finds a list of objects by a partial username - /// - /// Partial username to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query?.Where(member => member.Username.Equals(login)); - break; - case StringPropertyMatchType.Contains: - query?.Where(member => member.Username.Contains(login)); - break; - case StringPropertyMatchType.StartsWith: - query?.Where(member => member.Username.StartsWith(login)); - break; - case StringPropertyMatchType.EndsWith: - query?.Where(member => member.Username.EndsWith(login)); - break; - case StringPropertyMatchType.Wildcard: - query?.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - - return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Username); - } - } - - /// - /// Gets the total number of Users based on the count type - /// - /// - /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members - /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science - /// but that is how MS have made theirs so we'll follow that principal. - /// - /// to count by - /// with number of Users for passed in type - public int GetCount(MemberCountType countType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - IQuery? query; - - switch (countType) - { - case MemberCountType.All: - query = Query(); - break; - case MemberCountType.LockedOut: - query = Query()?.Where(x => x.IsLockedOut); - break; - case MemberCountType.Approved: - query = Query()?.Where(x => x.IsApproved); - break; - default: - throw new ArgumentOutOfRangeException(nameof(countType)); - } - - return _userRepository.GetCountByQuery(query); - } - } - - public Guid CreateLoginSession(int userId, string requestingIpAddress) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - var session = _userRepository.CreateLoginSession(userId, requestingIpAddress); - scope.Complete(); - return session; - } - } - - public int ClearLoginSessions(int userId) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - var count = _userRepository.ClearLoginSessions(userId); - scope.Complete(); - return count; - } - } - - public void ClearLoginSession(Guid sessionId) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _userRepository.ClearLoginSession(sessionId); - scope.Complete(); - } - } - - public bool ValidateLoginSession(int userId, Guid sessionId) - { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - var result = _userRepository.ValidateLoginSession(userId, sessionId); - scope.Complete(); - return result; - } - } - - public IDictionary GetUserStates() - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetUserStates(); - } - } - - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? userGroups = null, string? filter = null) - { - IQuery? filterQuery = null; - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query()?.Where(x => (x.Name != null && x.Name.Contains(filter!)) || x.Username.Contains(filter!)); - } - - return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, userState, userGroups, null, filterQuery); - } - - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? includeUserGroups = null, string[]? excludeUserGroups = null, IQuery? filter = null) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - Expression> sort; - switch (orderBy.ToUpperInvariant()) - { - case "USERNAME": - sort = member => member.Username; - break; - case "LANGUAGE": - sort = member => member.Language; - break; - case "NAME": - sort = member => member.Name; - break; - case "EMAIL": - sort = member => member.Email; - break; - case "ID": - sort = member => member.Id; - break; - case "CREATEDATE": - sort = member => member.CreateDate; - break; - case "UPDATEDATE": - sort = member => member.UpdateDate; - break; - case "ISAPPROVED": - sort = member => member.IsApproved; - break; - case "ISLOCKEDOUT": - sort = member => member.IsLockedOut; - break; - case "LASTLOGINDATE": - sort = member => member.LastLoginDate; - break; - default: - throw new IndexOutOfRangeException("The orderBy parameter " + orderBy + " is not valid"); - } - - return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, includeUserGroups, excludeUserGroups, userState, filter); - } - } - - /// - /// Gets a list of paged objects - /// - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Name); - } - } - - public IEnumerable GetNextUsers(int id, int count) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetNextUsers(id, count); - } - } - - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - /// - public IEnumerable GetAllInGroup(int? groupId) - { - if (groupId is null) - { - return Array.Empty(); - } - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetAllInGroup(groupId.Value); - } - } - - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - /// - public IEnumerable GetAllNotInGroup(int groupId) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - return _userRepository.GetAllNotInGroup(groupId); - } - } - - #endregion - - #region Implementation of IUserService - - /// - /// Gets an IProfile by User Id. - /// - /// Id of the User to retrieve - /// - public IProfile? GetProfileById(int id) - { - //This is called a TON. Go get the full user from cache which should already be IProfile - var fullUser = GetUserById(id); - if (fullUser == null) return null; - var asProfile = fullUser as IProfile; - return asProfile ?? new UserProfile(fullUser.Id, fullUser.Name); - } - - /// - /// Gets a profile by username - /// - /// Username - /// - public IProfile? GetProfileByUserName(string username) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetProfile(username); - } - } - - /// - /// Gets a user by Id - /// - /// Id of the user to retrieve - /// - public IUser? GetUserById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - try - { - return _userRepository.Get(id); - } - catch (DbException) - { - // TODO: refactor users/upgrade - // currently kinda accepting anything on upgrade, but that won't deal with all cases - // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should - // be better BUT requires that the app restarts after the upgrade! - if (IsUpgrading) - { - //NOTE: this will not be cached - return _userRepository.Get(id, includeSecurityData: false); - } - - throw; - } - } - } - - public IEnumerable GetUsersById(params int[]? ids) - { - if (ids?.Length <= 0) return Enumerable.Empty(); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetMany(ids); - } - } - - /// - /// Replaces the same permission set for a single group to any number of entities - /// - /// If no 'entityIds' are specified all permissions will be removed for the specified group. - /// Id of the group - /// Permissions as enumerable list of If nothing is specified all permissions are removed. - /// Specify the nodes to replace permissions for. - public void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) - { - if (entityIds.Length == 0) - return; - - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); - scope.Complete(); - - var assigned = permissions?.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); - if (assigned is not null) - { - var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); - scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); - } - } - } - - /// - /// Assigns the same permission set for a single user group to any number of entities - /// - /// Id of the user group - /// - /// Specify the nodes to replace permissions for - public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) - { - if (entityIds.Length == 0) - return; - - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); - scope.Complete(); - - var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; - var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); - scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); - } - } - - /// - /// Gets all UserGroups or those specified as parameters - /// - /// Optional Ids of UserGroups to retrieve - /// An enumerable list of - public IEnumerable GetAllUserGroups(params int[] ids) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); - } - } - - public IEnumerable GetUserGroupsByAlias(params string[] aliases) - { - if (aliases.Length == 0) return Enumerable.Empty(); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => aliases.SqlIn(x.Alias)); - var contents = _userGroupRepository.Get(query); - return contents?.WhereNotNull().ToArray() ?? Enumerable.Empty(); - } - } - - /// - /// Gets a UserGroup by its Alias - /// - /// Alias of the UserGroup to retrieve - /// - public IUserGroup? GetUserGroupByAlias(string alias) - { - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value cannot be null or whitespace.", "alias"); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == alias); - var contents = _userGroupRepository.Get(query); - return contents?.FirstOrDefault(); - } - } - - /// - /// Gets a UserGroup by its Id - /// - /// Id of the UserGroup to retrieve - /// - public IUserGroup? GetUserGroupById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.Get(id); - } - } - - /// - /// Saves a UserGroup - /// - /// UserGroup to save - /// - /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in - /// than all users will be removed from this group and only these users will be added - /// - /// Default is True otherwise set to False to not raise events - public void Save(IUserGroup userGroup, int[]? userIds = null) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - // we need to figure out which users have been added / removed, for audit purposes - var empty = new IUser[0]; - var addedUsers = empty; - var removedUsers = empty; - - if (userIds != null) - { - var groupUsers = userGroup.HasIdentity ? _userRepository.GetAllInGroup(userGroup.Id).ToArray() : empty; - var xGroupUsers = groupUsers.ToDictionary(x => x.Id, x => x); - var groupIds = groupUsers.Select(x => x.Id).ToArray(); - var addedUserIds = userIds.Except(groupIds); - - addedUsers = addedUserIds.Count() > 0 ? _userRepository.GetMany(addedUserIds.ToArray()).Where(x => x.Id != 0).ToArray() : new IUser[] { }; - removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); - } - - var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); - - // this is the default/expected notification for the IUserGroup entity being saved - var savingNotification = new UserGroupSavingNotification(userGroup, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - // this is an additional notification for special auditing - var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); - if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) - { - scope.Complete(); - return; - } - - _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); - - scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); - scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); - - scope.Complete(); - } - } - - /// - /// Deletes a UserGroup - /// - /// UserGroup to delete - public void DeleteUserGroup(IUserGroup userGroup) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); + var deletingNotification = new UserDeletingNotification(user, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } - _userGroupRepository.Delete(userGroup); - - scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + _userRepository.Delete(user); + scope.Notifications.Publish( + new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); scope.Complete(); } } + } - /// - /// Removes a specific section from all users - /// - /// This is useful when an entire section is removed from config - /// Alias of the section to remove - public void DeleteSectionFromAllUserGroups(string sectionAlias) + /// + /// Saves an + /// + /// to Save + public void Save(IUser entity) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) + var savingNotification = new UserSavingNotification(entity, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) { - var assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); - foreach (var group in assignedGroups) + scope.Complete(); + return; + } + + if (string.IsNullOrWhiteSpace(entity.Username)) + { + throw new ArgumentException("Empty username.", nameof(entity)); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new ArgumentException("Empty name.", nameof(entity)); + } + + try + { + _userRepository.Save(entity); + scope.Notifications.Publish( + new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); + + scope.Complete(); + } + catch (DbException ex) + { + // if we are upgrading and an exception occurs, log and swallow it + if (IsUpgrading == false) { - //now remove the section for each user and commit - //now remove the section for each user and commit - group.RemoveAllowedSection(sectionAlias); - _userGroupRepository.Save(group); + throw; } + _logger.LogWarning( + ex, + "An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored"); + + // we don't want the uow to rollback its scope! scope.Complete(); } } + } - /// - /// Get explicitly assigned permissions for a user and optional node ids - /// - /// User to retrieve permissions for - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - public EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds) + /// + /// Saves a list of objects + /// + /// to save + public void Save(IEnumerable entities) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + IUser[] entitiesA = entities.ToArray(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) { - return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, nodeIds); + scope.Complete(); + return; + } + + foreach (IUser user in entitiesA) + { + if (string.IsNullOrWhiteSpace(user.Username)) + { + throw new ArgumentException("Empty username.", nameof(entities)); + } + + if (string.IsNullOrWhiteSpace(user.Name)) + { + throw new ArgumentException("Empty name.", nameof(entities)); + } + + _userRepository.Save(user); + } + + scope.Notifications.Publish( + new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); + + // commit the whole lot in one go + scope.Complete(); + } + } + + /// + /// This is just the default user group that the membership provider will use + /// + /// + public string GetDefaultMemberType() => Constants.Security.WriterGroupAlias; + + /// + /// Finds a list of objects by a partial email string + /// + /// Partial email string to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + /// The type of match to make as . Default is + /// + /// + /// + /// + /// + public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query?.Where(member => member.Email.Equals(emailStringToMatch)); + break; + case StringPropertyMatchType.Contains: + query?.Where(member => member.Email.Contains(emailStringToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query?.Where(member => member.Email.StartsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query?.Where(member => member.Email.EndsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Email); + } + } + + /// + /// Finds a list of objects by a partial username + /// + /// Partial username to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + /// The type of match to make as . Default is + /// + /// + /// + /// + /// + public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query?.Where(member => member.Username.Equals(login)); + break; + case StringPropertyMatchType.Contains: + query?.Where(member => member.Username.Contains(login)); + break; + case StringPropertyMatchType.StartsWith: + query?.Where(member => member.Username.StartsWith(login)); + break; + case StringPropertyMatchType.EndsWith: + query?.Where(member => member.Username.EndsWith(login)); + break; + case StringPropertyMatchType.Wildcard: + query?.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Username); + } + } + + /// + /// Gets the total number of Users based on the count type + /// + /// + /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any + /// members + /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact + /// science + /// but that is how MS have made theirs so we'll follow that principal. + /// + /// to count by + /// with number of Users for passed in type + public int GetCount(MemberCountType countType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery? query; + + switch (countType) + { + case MemberCountType.All: + query = Query(); + break; + case MemberCountType.LockedOut: + query = Query()?.Where(x => x.IsLockedOut); + break; + case MemberCountType.Approved: + query = Query()?.Where(x => x.IsApproved); + break; + default: + throw new ArgumentOutOfRangeException(nameof(countType)); + } + + return _userRepository.GetCountByQuery(query); + } + } + + public Guid CreateLoginSession(int userId, string requestingIpAddress) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + Guid session = _userRepository.CreateLoginSession(userId, requestingIpAddress); + scope.Complete(); + return session; + } + } + + public int ClearLoginSessions(int userId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var count = _userRepository.ClearLoginSessions(userId); + scope.Complete(); + return count; + } + } + + public void ClearLoginSession(Guid sessionId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userRepository.ClearLoginSession(sessionId); + scope.Complete(); + } + } + + public bool ValidateLoginSession(int userId, Guid sessionId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var result = _userRepository.ValidateLoginSession(userId, sessionId); + scope.Complete(); + return result; + } + } + + public IDictionary GetUserStates() + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetUserStates(); + } + } + + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? userGroups = null, string? filter = null) + { + IQuery? filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query()?.Where(x => + (x.Name != null && x.Name.Contains(filter!)) || x.Username.Contains(filter!)); + } + + return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, userState, userGroups, null, filterQuery); + } + + public IEnumerable GetAll( + long pageIndex, + int pageSize, + out long totalRecords, + string orderBy, + Direction orderDirection, + UserState[]? userState = null, + string[]? includeUserGroups = null, + string[]? excludeUserGroups = null, + IQuery? filter = null) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + Expression> sort; + switch (orderBy.ToUpperInvariant()) + { + case "USERNAME": + sort = member => member.Username; + break; + case "LANGUAGE": + sort = member => member.Language; + break; + case "NAME": + sort = member => member.Name; + break; + case "EMAIL": + sort = member => member.Email; + break; + case "ID": + sort = member => member.Id; + break; + case "CREATEDATE": + sort = member => member.CreateDate; + break; + case "UPDATEDATE": + sort = member => member.UpdateDate; + break; + case "ISAPPROVED": + sort = member => member.IsApproved; + break; + case "ISLOCKEDOUT": + sort = member => member.IsLockedOut; + break; + case "LASTLOGINDATE": + sort = member => member.LastLoginDate; + break; + default: + throw new IndexOutOfRangeException("The orderBy parameter " + orderBy + " is not valid"); + } + + return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, includeUserGroups, excludeUserGroups, userState, filter); + } + } + + /// + /// Gets a list of paged objects + /// + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + /// + /// + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Name); + } + } + + public IEnumerable GetNextUsers(int id, int count) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetNextUsers(id, count); + } + } + + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + /// + /// + /// + public IEnumerable GetAllInGroup(int? groupId) + { + if (groupId is null) + { + return Array.Empty(); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetAllInGroup(groupId.Value); + } + } + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + /// + /// + /// + public IEnumerable GetAllNotInGroup(int groupId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + return _userRepository.GetAllNotInGroup(groupId); + } + } + + #endregion + + #region Implementation of IUserService + + /// + /// Gets an IProfile by User Id. + /// + /// Id of the User to retrieve + /// + /// + /// + public IProfile? GetProfileById(int id) + { + // This is called a TON. Go get the full user from cache which should already be IProfile + IUser? fullUser = GetUserById(id); + if (fullUser == null) + { + return null; + } + + var asProfile = fullUser as IProfile; + return asProfile ?? new UserProfile(fullUser.Id, fullUser.Name); + } + + /// + /// Gets a profile by username + /// + /// Username + /// + /// + /// + public IProfile? GetProfileByUserName(string username) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetProfile(username); + } + } + + /// + /// Gets a user by Id + /// + /// Id of the user to retrieve + /// + /// + /// + public IUser? GetUserById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + try + { + return _userRepository.Get(id); + } + catch (DbException) + { + // TODO: refactor users/upgrade + // currently kinda accepting anything on upgrade, but that won't deal with all cases + // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should + // be better BUT requires that the app restarts after the upgrade! + if (IsUpgrading) + { + // NOTE: this will not be cached + return _userRepository.Get(id, false); + } + + throw; + } + } + } + + public IEnumerable GetUsersById(params int[]? ids) + { + if (ids?.Length <= 0) + { + return Enumerable.Empty(); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetMany(ids); + } + } + + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// If no 'entityIds' are specified all permissions will be removed for the specified group. + /// Id of the group + /// + /// Permissions as enumerable list of If nothing is specified all permissions + /// are removed. + /// + /// Specify the nodes to replace permissions for. + public void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + { + if (entityIds.Length == 0) + { + return; + } + + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); + scope.Complete(); + + var assigned = permissions?.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); + if (assigned is not null) + { + EntityPermission[] entityPermissions = + entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); + } + } + } + + /// + /// Assigns the same permission set for a single user group to any number of entities + /// + /// Id of the user group + /// + /// Specify the nodes to replace permissions for + public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) + { + if (entityIds.Length == 0) + { + return; + } + + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); + scope.Complete(); + + var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; + EntityPermission[] entityPermissions = + entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); + } + } + + /// + /// Gets all UserGroups or those specified as parameters + /// + /// Optional Ids of UserGroups to retrieve + /// An enumerable list of + public IEnumerable GetAllUserGroups(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); + } + } + + public IEnumerable GetUserGroupsByAlias(params string[] aliases) + { + if (aliases.Length == 0) + { + return Enumerable.Empty(); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => aliases.SqlIn(x.Alias)); + IEnumerable contents = _userGroupRepository.Get(query); + return contents?.WhereNotNull().ToArray() ?? Enumerable.Empty(); + } + } + + /// + /// Gets a UserGroup by its Alias + /// + /// Alias of the UserGroup to retrieve + /// + /// + /// + public IUserGroup? GetUserGroupByAlias(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "alias"); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.Alias == alias); + IEnumerable contents = _userGroupRepository.Get(query); + return contents?.FirstOrDefault(); + } + } + + /// + /// Gets a UserGroup by its Id + /// + /// Id of the UserGroup to retrieve + /// + /// + /// + public IUserGroup? GetUserGroupById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.Get(id); + } + } + + /// + /// Saves a UserGroup + /// + /// UserGroup to save + /// + /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in + /// than all users will be removed from this group and only these users will be added + /// + /// Default is + /// True + /// otherwise set to + /// False + /// to not raise events + /// + public void Save(IUserGroup userGroup, int[]? userIds = null) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + // we need to figure out which users have been added / removed, for audit purposes + var empty = new IUser[0]; + IUser[] addedUsers = empty; + IUser[] removedUsers = empty; + + if (userIds != null) + { + IUser[] groupUsers = + userGroup.HasIdentity ? _userRepository.GetAllInGroup(userGroup.Id).ToArray() : empty; + var xGroupUsers = groupUsers.ToDictionary(x => x.Id, x => x); + var groupIds = groupUsers.Select(x => x.Id).ToArray(); + IEnumerable addedUserIds = userIds.Except(groupIds); + + addedUsers = addedUserIds.Count() > 0 + ? _userRepository.GetMany(addedUserIds.ToArray()).Where(x => x.Id != 0).ToArray() + : new IUser[] { }; + removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); + } + + var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); + + // this is the default/expected notification for the IUserGroup entity being saved + var savingNotification = new UserGroupSavingNotification(userGroup, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + // this is an additional notification for special auditing + var savingUserGroupWithUsersNotification = + new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); + if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) + { + scope.Complete(); + return; + } + + _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); + + scope.Notifications.Publish( + new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom( + savingUserGroupWithUsersNotification)); + + scope.Complete(); + } + } + + /// + /// Deletes a UserGroup + /// + /// UserGroup to delete + public void DeleteUserGroup(IUserGroup userGroup) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return; + } + + _userGroupRepository.Delete(userGroup); + + scope.Notifications.Publish( + new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + + scope.Complete(); + } + } + + /// + /// Removes a specific section from all users + /// + /// This is useful when an entire section is removed from config + /// Alias of the section to remove + public void DeleteSectionFromAllUserGroups(string sectionAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + IEnumerable assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); + foreach (IUserGroup group in assignedGroups) + { + // now remove the section for each user and commit + // now remove the section for each user and commit + group.RemoveAllowedSection(sectionAlias); + _userGroupRepository.Save(group); + } + + scope.Complete(); + } + } + + /// + /// Get explicitly assigned permissions for a user and optional node ids + /// + /// User to retrieve permissions for + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + public EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, nodeIds); + } + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + public EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetPermissions( + groups.WhereNotNull().Select(x => x.ToReadOnlyGroup()).ToArray(), + fallbackToDefaultPermissions, + nodeIds); + } + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// Groups to retrieve permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + private IEnumerable GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); + } + } + + /// + /// Gets the implicit/inherited permissions for the user for the given path + /// + /// User to check permissions for + /// Path to check permissions for + public EntityPermissionSet GetPermissionsForPath(IUser? user, string? path) + { + var nodeIds = path?.GetIdsFromPathReversed(); + + if (nodeIds is null || nodeIds.Length == 0 || user is null) + { + return EntityPermissionSet.Empty(); + } + + // collect all permissions structures for all nodes for all groups belonging to the user + EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } + + /// + /// Gets the permissions for the provided group and path + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// String indicating permissions for provided user and path + public EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false) + { + var nodeIds = path.GetIdsFromPathReversed(); + + if (nodeIds.Length == 0) + { + return EntityPermissionSet.Empty(); + } + + // collect all permissions structures for all nodes for all groups + EntityPermission[] groupPermissions = + GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } + + /// + /// This performs the calculations for inherited nodes based on this + /// http://issues.umbraco.org/issue/U4-10075#comment=67-40085 + /// + /// + /// + /// + internal static EntityPermissionSet CalculatePermissionsForPathForUser( + EntityPermission[] groupPermissions, + int[] pathIds) + { + // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? + if (groupPermissions.Length == 0 || pathIds.Length == 0) + { + return EntityPermissionSet.Empty(); + } + + // The actual entity id being looked at (deepest part of the path) + var entityId = pathIds[0]; + + var resultPermissions = new EntityPermissionCollection(); + + // create a grouped by dictionary of another grouped by dictionary + var permissionsByGroup = groupPermissions + .GroupBy(x => x.UserGroupId) + .ToDictionary( + x => x.Key, + x => x.GroupBy(a => a.EntityId).ToDictionary(a => a.Key, a => a.ToArray())); + + // iterate through each group + foreach (KeyValuePair> byGroup in permissionsByGroup) + { + var added = false; + + // iterate deepest to shallowest + foreach (var pathId in pathIds) + { + if (byGroup.Value.TryGetValue(pathId, out EntityPermission[]? permissionsForNodeAndGroup) == false) + { + continue; + } + + // In theory there will only be one EntityPermission in this group + // but there's nothing stopping the logic of this method + // from having more so we deal with it here + foreach (EntityPermission entityPermission in permissionsForNodeAndGroup) + { + if (entityPermission.IsDefaultPermissions == false) + { + // explicit permission found so we'll append it and move on, the collection is a hashset anyways + // so only supports adding one element per groupid/contentid + resultPermissions.Add(entityPermission); + added = true; + break; + } + } + + // if the permission has been added for this group and this branch then we can exit this loop + if (added) + { + break; + } + } + + if (added == false && byGroup.Value.Count > 0) + { + // if there was no explicit permissions assigned in this branch for this group, then we will + // add the group's default permissions + resultPermissions.Add(byGroup.Value[entityId][0]); } } - /// - /// Get explicitly assigned permissions for a group and optional node Ids - /// - /// Groups to retrieve permissions for - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - private IEnumerable GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (groups == null) throw new ArgumentNullException(nameof(groups)); + var permissionSet = new EntityPermissionSet(entityId, resultPermissions); + return permissionSet; + } - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); - } + private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) + { + if (pathIds.Length == 0) + { + return new EntityPermissionCollection(Enumerable.Empty()); } - /// - /// Get explicitly assigned permissions for a group and optional node Ids - /// - /// - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - public EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (groups == null) throw new ArgumentNullException(nameof(groups)); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetPermissions(groups.WhereNotNull().Select(x => x.ToReadOnlyGroup()).ToArray(), fallbackToDefaultPermissions, nodeIds); - } - } - /// - /// Gets the implicit/inherited permissions for the user for the given path - /// - /// User to check permissions for - /// Path to check permissions for - public EntityPermissionSet GetPermissionsForPath(IUser? user, string? path) - { - var nodeIds = path?.GetIdsFromPathReversed(); - - if (nodeIds is null || nodeIds.Length == 0 || user is null) - return EntityPermissionSet.Empty(); - - //collect all permissions structures for all nodes for all groups belonging to the user - var groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); - - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); - } - - /// - /// Gets the permissions for the provided group and path - /// - /// - /// Path to check permissions for - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// String indicating permissions for provided user and path - public EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false) - { - var nodeIds = path.GetIdsFromPathReversed(); - - if (nodeIds.Length == 0) - return EntityPermissionSet.Empty(); - - //collect all permissions structures for all nodes for all groups - var groupPermissions = GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); - - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); - } - - private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) - { - if (pathIds.Length == 0) - return new EntityPermissionCollection(Enumerable.Empty()); - - //get permissions for all nodes in the path by group - var permissions = GetPermissions(groups, fallbackToDefaultPermissions, pathIds) + // get permissions for all nodes in the path by group + IEnumerable> permissions = + GetPermissions(groups, fallbackToDefaultPermissions, pathIds) .GroupBy(x => x.UserGroupId); - return new EntityPermissionCollection( - permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions)).Where(x => x is not null)!); - } - - /// - /// This performs the calculations for inherited nodes based on this http://issues.umbraco.org/issue/U4-10075#comment=67-40085 - /// - /// - /// - /// - internal static EntityPermissionSet CalculatePermissionsForPathForUser( - EntityPermission[] groupPermissions, - int[] pathIds) - { - // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? - if (groupPermissions.Length == 0 || pathIds.Length == 0) - return EntityPermissionSet.Empty(); - - //The actual entity id being looked at (deepest part of the path) - var entityId = pathIds[0]; - - var resultPermissions = new EntityPermissionCollection(); - - //create a grouped by dictionary of another grouped by dictionary - var permissionsByGroup = groupPermissions - .GroupBy(x => x.UserGroupId) - .ToDictionary( - x => x.Key, - x => x.GroupBy(a => a.EntityId).ToDictionary(a => a.Key, a => a.ToArray())); - - //iterate through each group - foreach (var byGroup in permissionsByGroup) - { - var added = false; - - //iterate deepest to shallowest - foreach (var pathId in pathIds) - { - EntityPermission[]? permissionsForNodeAndGroup; - if (byGroup.Value.TryGetValue(pathId, out permissionsForNodeAndGroup) == false) - continue; - - //In theory there will only be one EntityPermission in this group - // but there's nothing stopping the logic of this method - // from having more so we deal with it here - foreach (var entityPermission in permissionsForNodeAndGroup) - { - if (entityPermission.IsDefaultPermissions == false) - { - //explicit permission found so we'll append it and move on, the collection is a hashset anyways - //so only supports adding one element per groupid/contentid - resultPermissions.Add(entityPermission); - added = true; - break; - } - } - - //if the permission has been added for this group and this branch then we can exit this loop - if (added) - break; - } - - if (added == false && byGroup.Value.Count > 0) - { - //if there was no explicit permissions assigned in this branch for this group, then we will - //add the group's default permissions - resultPermissions.Add(byGroup.Value[entityId][0]); - } - - } - - var permissionSet = new EntityPermissionSet(entityId, resultPermissions); - return permissionSet; - } - - /// - /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch - /// - /// - /// The collective set of permissions provided to calculate the resulting permissions set for the path - /// based on a single group - /// - /// Must be ordered deepest to shallowest (right to left) - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// - internal static EntityPermission? GetPermissionsForPathForGroup( - IEnumerable pathPermissions, - int[] pathIds, - bool fallbackToDefaultPermissions = false) - { - //get permissions for all nodes in the path - var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); - - //then the permissions assigned to the path will be the 'deepest' node found that has permissions - foreach (var id in pathIds) - { - EntityPermission? permission; - if (permissionsByEntityId.TryGetValue(id, out permission)) - { - //don't return the default permissions if that is the one assigned here (we'll do that below if nothing was found) - if (permission.IsDefaultPermissions == false) - return permission; - } - } - - //if we've made it here it means that no implicit/inherited permissions were found so we return the defaults if that is specified - if (fallbackToDefaultPermissions == false) - return null; - - return permissionsByEntityId[pathIds[0]]; - } - - /// - /// Checks in a set of permissions associated with a user for those related to a given nodeId - /// - /// The set of permissions - /// The node Id - /// The permissions to return - /// True if permissions for the given path are found - public static bool TryGetAssignedPermissionsForNode(IList permissions, - int nodeId, - out string assignedPermissions) - { - if (permissions.Any(x => x.EntityId == nodeId)) - { - var found = permissions.First(x => x.EntityId == nodeId); - var assignedPermissionsArray = found.AssignedPermissions.ToList(); - - // Working with permissions assigned directly to a user AND to their groups, so maybe several per node - // and we need to get the most permissive set - foreach (var permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) - { - AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); - } - - assignedPermissions = string.Join("", assignedPermissionsArray); - return true; - } - - assignedPermissions = string.Empty; - return false; - } - - private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) - { - var permissionsToAdd = additionalPermissions - .Where(x => assignedPermissions.Contains(x) == false); - assignedPermissions.AddRange(permissionsToAdd); - } - - #endregion + return new EntityPermissionCollection( + permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions)) + .Where(x => x is not null)!); } + + /// + /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch + /// + /// + /// The collective set of permissions provided to calculate the resulting permissions set for the path + /// based on a single group + /// + /// Must be ordered deepest to shallowest (right to left) + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// + internal static EntityPermission? GetPermissionsForPathForGroup( + IEnumerable pathPermissions, + int[] pathIds, + bool fallbackToDefaultPermissions = false) + { + // get permissions for all nodes in the path + var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); + + // then the permissions assigned to the path will be the 'deepest' node found that has permissions + foreach (var id in pathIds) + { + if (permissionsByEntityId.TryGetValue(id, out EntityPermission? permission)) + { + // don't return the default permissions if that is the one assigned here (we'll do that below if nothing was found) + if (permission.IsDefaultPermissions == false) + { + return permission; + } + } + } + + // if we've made it here it means that no implicit/inherited permissions were found so we return the defaults if that is specified + if (fallbackToDefaultPermissions == false) + { + return null; + } + + return permissionsByEntityId[pathIds[0]]; + } + + private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) + { + IEnumerable permissionsToAdd = additionalPermissions + .Where(x => assignedPermissions.Contains(x) == false); + assignedPermissions.AddRange(permissionsToAdd); + } + + #endregion } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 86e823f8bc..f17a266616 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,94 +1,89 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UserServiceExtensions { - public static class UserServiceExtensions + public static EntityPermission? GetPermissions(this IUserService userService, IUser? user, string path) { - public static EntityPermission? GetPermissions(this IUserService userService, IUser? user, string path) + var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) + ? Attempt.Succeed(value) + : Attempt.Fail()) + .Where(x => x.Success) + .Select(x => x.Result) + .ToArray(); + if (ids.Length == 0) { - var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? Attempt.Succeed(value) : Attempt.Fail()) - .Where(x => x.Success) - .Select(x=>x.Result) - .ToArray(); - if (ids.Length == 0) throw new InvalidOperationException("The path: " + path + " could not be parsed into an array of integers or the path was empty"); - - return userService.GetPermissions(user, ids[ids.Length - 1]).FirstOrDefault(); + throw new InvalidOperationException("The path: " + path + + " could not be parsed into an array of integers or the path was empty"); } - /// - /// Get explicitly assigned permissions for a group and optional node Ids - /// - /// - /// - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - public static EntityPermissionCollection GetPermissions(this IUserService service, IUserGroup? group, bool fallbackToDefaultPermissions, params int[] nodeIds) + return userService.GetPermissions(user, ids[^1]).FirstOrDefault(); + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + public static EntityPermissionCollection GetPermissions(this IUserService service, IUserGroup? group, bool fallbackToDefaultPermissions, params int[] nodeIds) => + service.GetPermissions(new[] { group }, fallbackToDefaultPermissions, nodeIds); + + /// + /// Gets the permissions for the provided group and path + /// + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + public static EntityPermissionSet GetPermissionsForPath(this IUserService service, IUserGroup group, string path, bool fallbackToDefaultPermissions = false) => + service.GetPermissionsForPath(new[] { group }, path, fallbackToDefaultPermissions); + + /// + /// Remove all permissions for this user group for all nodes specified + /// + /// + /// + /// + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds) => + userService.ReplaceUserGroupPermissions(groupId, null, entityIds); + + /// + /// Remove all permissions for this user group for all nodes + /// + /// + /// + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) => + userService.ReplaceUserGroupPermissions(groupId, null); + + public static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) + { + IEnumerable fullUsers = userService.GetUsersById(ids); + + return fullUsers.Select(user => { - return service.GetPermissions(new[] {group}, fallbackToDefaultPermissions, nodeIds); - } + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + } - /// - /// Gets the permissions for the provided group and path - /// - /// - /// - /// Path to check permissions for - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - public static EntityPermissionSet GetPermissionsForPath(this IUserService service, IUserGroup group, string path, bool fallbackToDefaultPermissions = false) - { - return service.GetPermissionsForPath(new[] { group }, path, fallbackToDefaultPermissions); - } - - /// - /// Remove all permissions for this user group for all nodes specified - /// - /// - /// - /// - public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds) - { - userService.ReplaceUserGroupPermissions(groupId, null, entityIds); - } - - /// - /// Remove all permissions for this user group for all nodes - /// - /// - /// - public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) - { - userService.ReplaceUserGroupPermissions(groupId, null); - } - - - public static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) - { - var fullUsers = userService.GetUsersById(ids); - - return fullUsers.Select(user => - { - var asProfile = user as IProfile; - return asProfile ?? new UserProfile(user.Id, user.Name); - }); - - } - - public static IUser? GetByKey(this IUserService userService, Guid key) - { - int id = BitConverter.ToInt32(key.ToByteArray(), 0); - return userService.GetUserById(id); - } + public static IUser? GetByKey(this IUserService userService, Guid key) + { + var id = BitConverter.ToInt32(key.ToByteArray(), 0); + return userService.GetUserById(id); } } diff --git a/src/Umbraco.Core/Settable.cs b/src/Umbraco.Core/Settable.cs index 07f53c2080..9f91ee15ff 100644 --- a/src/Umbraco.Core/Settable.cs +++ b/src/Umbraco.Core/Settable.cs @@ -1,95 +1,94 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents a value that can be assigned a value. +/// +/// The type of the value +public class Settable { + private T? _value; + /// - /// Represents a value that can be assigned a value. + /// Gets a value indicating whether a value has been assigned to this instance. /// - /// The type of the value - public class Settable + public bool HasValue { get; private set; } + + /// + /// Gets the value assigned to this instance. + /// + /// An exception is thrown if the HasValue property is false. + /// No value has been assigned to this instance. + public T? Value { - private T? _value; - - /// - /// Assigns a value to this instance. - /// - /// The value. - public void Set(T? value) + get { - if (value is not null) + if (HasValue == false) { - HasValue = true; + throw new InvalidOperationException("The HasValue property is false."); } - _value = value; - } - /// - /// Assigns a value to this instance by copying the value - /// of another instance, if the other instance has a value. - /// - /// The other instance. - public void Set(Settable other) - { - // set only if has value else don't change anything - if (other.HasValue) Set(other.Value); - } - - /// - /// Clears the value. - /// - public void Clear() - { - HasValue = false; - _value = default (T); - } - - /// - /// Gets a value indicating whether a value has been assigned to this instance. - /// - public bool HasValue { get; private set; } - - /// - /// Gets the value assigned to this instance. - /// - /// An exception is thrown if the HasValue property is false. - /// No value has been assigned to this instance. - public T? Value - { - get - { - if (HasValue == false) - throw new InvalidOperationException("The HasValue property is false."); - return _value; - } - } - - /// - /// Gets the value assigned to this instance, if a value has been assigned, - /// otherwise the default value of . - /// - /// The value assigned to this instance, if a value has been assigned, - /// else the default value of . - public T? ValueOrDefault() - { - return HasValue ? _value : default(T); - } - - /// - /// Gets the value assigned to this instance, if a value has been assigned, - /// otherwise a specified default value. - /// - /// The default value. - /// The value assigned to this instance, if a value has been assigned, - /// else . - public T? ValueOrDefault(T defaultValue) - { - return HasValue ? _value : defaultValue; - } - - /// - public override string? ToString() - { - return HasValue ? _value?.ToString() : "void"; + return _value; } } + + /// + /// Assigns a value to this instance. + /// + /// The value. + public void Set(T? value) + { + if (value is not null) + { + HasValue = true; + } + + _value = value; + } + + /// + /// Assigns a value to this instance by copying the value + /// of another instance, if the other instance has a value. + /// + /// The other instance. + public void Set(Settable other) + { + // set only if has value else don't change anything + if (other.HasValue) + { + Set(other.Value); + } + } + + /// + /// Clears the value. + /// + public void Clear() + { + HasValue = false; + _value = default; + } + + /// + /// Gets the value assigned to this instance, if a value has been assigned, + /// otherwise the default value of . + /// + /// + /// The value assigned to this instance, if a value has been assigned, + /// else the default value of . + /// + public T? ValueOrDefault() => HasValue ? _value : default; + + /// + /// Gets the value assigned to this instance, if a value has been assigned, + /// otherwise a specified default value. + /// + /// The default value. + /// + /// The value assigned to this instance, if a value has been assigned, + /// else . + /// + public T? ValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + public override string? ToString() => HasValue ? _value?.ToString() : "void"; } diff --git a/src/Umbraco.Core/SimpleMainDom.cs b/src/Umbraco.Core/SimpleMainDom.cs index 3f4bd1ce7c..3b3bc1b0c0 100644 --- a/src/Umbraco.Core/SimpleMainDom.cs +++ b/src/Umbraco.Core/SimpleMainDom.cs @@ -1,80 +1,92 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides a simple implementation of . +/// +public class SimpleMainDom : IMainDom, IDisposable { - /// - /// Provides a simple implementation of . - /// - public class SimpleMainDom : IMainDom, IDisposable + private readonly List> _callbacks = new(); + private readonly object _locko = new(); + private bool _disposedValue; + private bool _isStopping; + + /// + public bool IsMainDom { get; private set; } = true; + + public void Dispose() { - private readonly object _locko = new object(); - private readonly List> _callbacks = new List>(); - private bool _isStopping; - private bool _disposedValue; + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + GC.SuppressFinalize(this); + } - /// - public bool IsMainDom { get; private set; } = true; + // always acquire + public bool Acquire(IApplicationShutdownRegistry hostingEnvironment) => true; - // always acquire - public bool Acquire(IApplicationShutdownRegistry hostingEnvironment) => true; - - /// - public bool Register(Action? install, Action? release, int weight = 100) + /// + public bool Register(Action? install, Action? release, int weight = 100) + { + lock (_locko) { - lock (_locko) + if (_isStopping) { - if (_isStopping) return false; - install?.Invoke(); - if (release != null) - _callbacks.Add(new KeyValuePair(weight, release)); - return true; + return false; } + + install?.Invoke(); + if (release != null) + { + _callbacks.Add(new KeyValuePair(weight, release)); + } + + return true; + } + } + + public void Stop() + { + lock (_locko) + { + if (_isStopping) + { + return; + } + + if (IsMainDom == false) + { + return; // probably not needed + } + + _isStopping = true; } - public void Stop() + try { - lock (_locko) + foreach (Action callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) { - if (_isStopping) return; - if (IsMainDom == false) return; // probably not needed - _isStopping = true; - } - - try - { - foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) - { - callback(); // no timeout on callbacks - } - } - finally - { - // in any case... - IsMainDom = false; + callback(); // no timeout on callbacks } } - - protected virtual void Dispose(bool disposing) + finally { - if (!_disposedValue) - { - if (disposing) - { - Stop(); - } - _disposedValue = true; - } + // in any case... + IsMainDom = false; } + } - public void Dispose() + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + if (disposing) + { + Stop(); + } + + _disposedValue = true; } } } diff --git a/src/Umbraco.Core/Snippets/ISnippet.cs b/src/Umbraco.Core/Snippets/ISnippet.cs new file mode 100644 index 0000000000..67c9bf9e7f --- /dev/null +++ b/src/Umbraco.Core/Snippets/ISnippet.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// Defines a partial view macro snippet. + /// + public interface ISnippet + { + /// + /// Gets the name of the snippet. + /// + string Name { get; } + + /// + /// Gets the content of the snippet. + /// + string Content { get; } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollection.cs b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollection.cs new file mode 100644 index 0000000000..b2fb79553c --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollection.cs @@ -0,0 +1,66 @@ +using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Composing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The collection of partial view macro snippets. + /// + public class PartialViewMacroSnippetCollection : BuilderCollectionBase + { + public PartialViewMacroSnippetCollection(Func> items) : base(items) + { + } + + /// + /// Gets the partial view macro snippet names. + /// + /// The names of all partial view macro snippets. + public IEnumerable GetNames() + { + var snippetNames = this.Select(x => Path.GetFileNameWithoutExtension(x.Name)).ToArray(); + + // Ensure the ones that are called 'Empty' are at the top + var empty = snippetNames.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false) + .OrderBy(x => x?.Length).ToArray(); + + return empty.Union(snippetNames.Except(empty)).WhereNotNull(); + } + + /// + /// Gets the content of a partial view macro snippet as a string. + /// + /// The name of the snippet. + /// The content of the partial view macro. + public string GetContentFromName(string snippetName) + { + if (snippetName.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(snippetName)); + } + + string partialViewMacroHeader = "@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage"; + + var snippet = this.Where(x => x.Name.Equals(snippetName + ".cshtml")).FirstOrDefault(); + + // Try and get the snippet path + if (snippet is null) + { + throw new InvalidOperationException("Could not load snippet with name " + snippetName); + } + + // Strip the @inherits if it's there + var snippetContent = StripPartialViewHeader(snippet.Content); + + var content = $"{partialViewMacroHeader}{Environment.NewLine}{snippetContent}"; + return content; + } + + private string StripPartialViewHeader(string contents) + { + var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline); + return headerMatch.Replace(contents, string.Empty); + } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollectionBuilder.cs b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollectionBuilder.cs new file mode 100644 index 0000000000..cf737368ce --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollectionBuilder.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Extensions; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The partial view macro snippet collection builder. + /// + public class PartialViewMacroSnippetCollectionBuilder : LazyCollectionBuilderBase + { + protected override PartialViewMacroSnippetCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) + { + var hostEnvironment = factory.GetRequiredService(); + + var embeddedSnippets = new List(base.CreateItems(factory)); + var snippetProvider = new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets"); + var embeddedFiles = snippetProvider.GetDirectoryContents(string.Empty) + .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml")); + + foreach (var file in embeddedFiles) + { + using var stream = new StreamReader(file.CreateReadStream()); + embeddedSnippets.Add(new Snippet(file.Name, stream.ReadToEnd().Trim())); + } + + var customSnippetsDir = new DirectoryInfo(hostEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.Umbraco}/PartialViewMacros/Templates")); + if (!customSnippetsDir.Exists) + { + return embeddedSnippets; + } + + var customSnippets = customSnippetsDir.GetFiles().Select(f => new Snippet(f.Name, File.ReadAllText(f.FullName))); + var allSnippets = Merge(embeddedSnippets, customSnippets); + + return allSnippets; + } + + private IEnumerable Merge(IEnumerable embeddedSnippets, IEnumerable customSnippets) + { + var allSnippets = embeddedSnippets.Concat(customSnippets); + + var duplicates = allSnippets.GroupBy(s => s.Name) + .Where(gr => gr.Count() > 1) // Finds the snippets with the same name + .Select(s => s.First()); // Takes the first element from a grouping, which is the embeded snippet with that same name, + // since the physical snippet files are placed after the embedded ones in the all snippets colleciton + + // Remove any embedded snippets if a physical file with the same name can be found + return allSnippets.Except(duplicates); + } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs b/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs new file mode 100644 index 0000000000..5a0cda96e9 --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs @@ -0,0 +1,70 @@ +using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Composing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The collection of partial view snippets. + /// + public class PartialViewSnippetCollection : BuilderCollectionBase + { + public PartialViewSnippetCollection(Func> items) : base(items) + { + } + + /// + /// Gets the partial view snippet names. + /// + /// The names of all partial view snippets. + public IEnumerable GetNames() + { + var snippetNames = this.Select(x => Path.GetFileNameWithoutExtension(x.Name)).ToArray(); + + // Ensure the ones that are called 'Empty' are at the top + var empty = snippetNames.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false) + .OrderBy(x => x?.Length).ToArray(); + + return empty.Union(snippetNames.Except(empty)).WhereNotNull(); + } + + /// + /// Gets the content of a partial view snippet as a string. + /// + /// The name of the snippet. + /// The content of the partial view. + public string GetContentFromName(string snippetName) + { + if (snippetName.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(snippetName)); + } + + string partialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"; + + var snippet = this.Where(x => x.Name.Equals(snippetName + ".cshtml")).FirstOrDefault(); + + // Try and get the snippet path + if (snippet is null) + { + throw new InvalidOperationException("Could not load snippet with name " + snippetName); + } + + var snippetContent = CleanUpContents(snippet.Content); + + var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}"; + return content; + } + + private string CleanUpContents(string content) + { + // Strip the @inherits if it's there + var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline); + var newContent = headerMatch.Replace(content, string.Empty); + + return newContent + .Replace("Model.Content.", "Model.") + .Replace("(Model.Content)", "(Model)"); + } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs b/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs new file mode 100644 index 0000000000..730e984d33 --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The partial view snippet collection builder. + /// + public class PartialViewSnippetCollectionBuilder : LazyCollectionBuilderBase + { + protected override PartialViewSnippetCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) + { + var embeddedSnippets = new List(base.CreateItems(factory)); + + // Ignore these + var filterNames = new List + { + "Gallery", + "ListChildPagesFromChangeableSource", + "ListChildPagesOrderedByProperty", + "ListImagesFromMediaFolder" + }; + + var snippetProvider = new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets"); + var embeddedFiles = snippetProvider.GetDirectoryContents(string.Empty) + .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml")); + + foreach (var file in embeddedFiles) + { + if (!filterNames.Contains(Path.GetFileNameWithoutExtension(file.Name))) + { + using var stream = new StreamReader(file.CreateReadStream()); + embeddedSnippets.Add(new Snippet(file.Name, stream.ReadToEnd().Trim())); + } + } + + return embeddedSnippets; + } + } +} diff --git a/src/Umbraco.Core/Snippets/Snippet.cs b/src/Umbraco.Core/Snippets/Snippet.cs new file mode 100644 index 0000000000..bcb03b6d11 --- /dev/null +++ b/src/Umbraco.Core/Snippets/Snippet.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Core.Snippets +{ + public class Snippet : ISnippet + { + public string Name { get; } + public string Content { get; } + + public Snippet(string name, string content) + { + Name = name; + Content = content; + } + } +} diff --git a/src/Umbraco.Core/StaticApplicationLogging.cs b/src/Umbraco.Core/StaticApplicationLogging.cs index f0d01d4073..eac0a3f51b 100644 --- a/src/Umbraco.Core/StaticApplicationLogging.cs +++ b/src/Umbraco.Core/StaticApplicationLogging.cs @@ -1,19 +1,18 @@ -using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class StaticApplicationLogging { - public static class StaticApplicationLogging - { - private static ILoggerFactory? s_loggerFactory; + private static ILoggerFactory? loggerFactory; - public static void Initialize(ILoggerFactory loggerFactory) => s_loggerFactory = loggerFactory; + public static ILogger Logger => CreateLogger(); - public static ILogger Logger => CreateLogger(); + public static void Initialize(ILoggerFactory loggerFactory) => StaticApplicationLogging.loggerFactory = loggerFactory; - public static ILogger CreateLogger() => s_loggerFactory?.CreateLogger() ?? NullLoggerFactory.Instance.CreateLogger(); + public static ILogger CreateLogger() => + loggerFactory?.CreateLogger() ?? NullLoggerFactory.Instance.CreateLogger(); - public static ILogger CreateLogger(Type type) => s_loggerFactory?.CreateLogger(type) ?? NullLogger.Instance; - } + public static ILogger CreateLogger(Type type) => loggerFactory?.CreateLogger(type) ?? NullLogger.Instance; } diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs index 3435c81780..2b1229be77 100644 --- a/src/Umbraco.Core/StringUdi.cs +++ b/src/Umbraco.Core/StringUdi.cs @@ -1,64 +1,51 @@ -using System; using System.ComponentModel; -using System.Linq; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a string-based entity identifier. +/// +[TypeConverter(typeof(UdiTypeConverter))] +public class StringUdi : Udi { /// - /// Represents a string-based entity identifier. + /// Initializes a new instance of the StringUdi class with an entity type and a string id. /// - [TypeConverter(typeof(UdiTypeConverter))] - public class StringUdi : Udi + /// The entity type part of the udi. + /// The string id part of the udi. + public StringUdi(string entityType, string id) + : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) => + Id = id; + + /// + /// Initializes a new instance of the StringUdi class with a uri value. + /// + /// The uri value of the udi. + public StringUdi(Uri uriValue) + : base(uriValue) => + Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash)); + + /// + /// The string part of the identifier. + /// + public string Id { get; } + + /// + public override bool IsRoot => Id == string.Empty; + + public StringUdi EnsureClosed() { - /// - /// The string part of the identifier. - /// - public string Id { get; private set; } - - /// - /// Initializes a new instance of the StringUdi class with an entity type and a string id. - /// - /// The entity type part of the udi. - /// The string id part of the udi. - public StringUdi(string entityType, string id) - : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) - { - Id = id; - } - - /// - /// Initializes a new instance of the StringUdi class with a uri value. - /// - /// The uri value of the udi. - public StringUdi(Uri uriValue) - : base(uriValue) - { - Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash)); - } - - private static string EscapeUriString(string s) - { - // Uri.EscapeUriString preserves / but also [ and ] which is bad - // Uri.EscapeDataString does not preserve / which is bad - - // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = - // unreserved = alpha digit - . _ ~ - - // we want to preserve the / and the unreserved - // so... - return string.Join("/", s.Split(Constants.CharArrays.ForwardSlash).Select(Uri.EscapeDataString)); - } - - /// - public override bool IsRoot - { - get { return Id == string.Empty; } - } - - public StringUdi EnsureClosed() - { - EnsureNotRoot(); - return this; - } + EnsureNotRoot(); + return this; } + + private static string EscapeUriString(string s) => + + // Uri.EscapeUriString preserves / but also [ and ] which is bad + // Uri.EscapeDataString does not preserve / which is bad + // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = + // unreserved = alpha digit - . _ ~ + // we want to preserve the / and the unreserved + // so... + string.Join("/", s.Split(Constants.CharArrays.ForwardSlash).Select(Uri.EscapeDataString)); } diff --git a/src/Umbraco.Core/Strings/CleanStringType.cs b/src/Umbraco.Core/Strings/CleanStringType.cs index 771e834d35..75ad000505 100644 --- a/src/Umbraco.Core/Strings/CleanStringType.cs +++ b/src/Umbraco.Core/Strings/CleanStringType.cs @@ -1,124 +1,121 @@ -using System; +namespace Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Strings +/// +/// Specifies the type of a clean string. +/// +/// +/// Specifies its casing, and its encoding. +/// +[Flags] +public enum CleanStringType { + // note: you have 32 bits at your disposal + // 0xffffffff + + // no value + /// - /// Specifies the type of a clean string. + /// No value. + /// + None = 0x00, + + // casing values + + /// + /// Flag mask for casing. + /// + CaseMask = PascalCase | CamelCase | Unchanged | LowerCase | UpperCase | UmbracoCase, + + /// + /// Pascal casing eg "PascalCase". + /// + PascalCase = 0x01, + + /// + /// Camel casing eg "camelCase". + /// + CamelCase = 0x02, + + /// + /// Unchanged casing eg "UncHanGed". + /// + Unchanged = 0x04, + + /// + /// Lower casing eg "lowercase". + /// + LowerCase = 0x08, + + /// + /// Upper casing eg "UPPERCASE". + /// + UpperCase = 0x10, + + /// + /// Umbraco "safe alias" case. /// /// - /// Specifies its casing, and its encoding. + /// Uppercases the first char of each term except for the first + /// char of the string, everything else including the first char of the + /// string is unchanged. /// - [Flags] - public enum CleanStringType - { - // note: you have 32 bits at your disposal - // 0xffffffff + UmbracoCase = 0x20, - // no value + // encoding values - /// - /// No value. - /// - None = 0x00, + /// + /// Flag mask for encoding. + /// + CodeMask = Utf8 | Ascii | TryAscii, + // Unicode encoding is obsolete, use Utf8 + // Unicode = 0x0100, - // casing values + /// + /// Utf8 encoding. + /// + Utf8 = 0x0200, - /// - /// Flag mask for casing. - /// - CaseMask = PascalCase | CamelCase | Unchanged | LowerCase | UpperCase | UmbracoCase, + /// + /// Ascii encoding. + /// + Ascii = 0x0400, - /// - /// Pascal casing eg "PascalCase". - /// - PascalCase = 0x01, + /// + /// Ascii encoding, if possible. + /// + TryAscii = 0x0800, - /// - /// Camel casing eg "camelCase". - /// - CamelCase = 0x02, + // role values - /// - /// Unchanged casing eg "UncHanGed". - /// - Unchanged = 0x04, + /// + /// Flag mask for role. + /// + RoleMask = UrlSegment | Alias | UnderscoreAlias | FileName | ConvertCase, - /// - /// Lower casing eg "lowercase". - /// - LowerCase = 0x08, + /// + /// Url role. + /// + UrlSegment = 0x010000, - /// - /// Upper casing eg "UPPERCASE". - /// - UpperCase = 0x10, + /// + /// Alias role. + /// + Alias = 0x020000, - /// - /// Umbraco "safe alias" case. - /// - /// Uppercases the first char of each term except for the first - /// char of the string, everything else including the first char of the - /// string is unchanged. - UmbracoCase = 0x20, + /// + /// FileName role. + /// + FileName = 0x040000, + /// + /// ConvertCase role. + /// + ConvertCase = 0x080000, - // encoding values - - /// - /// Flag mask for encoding. - /// - CodeMask = Utf8 | Ascii | TryAscii, - - // Unicode encoding is obsolete, use Utf8 - //Unicode = 0x0100, - - /// - /// Utf8 encoding. - /// - Utf8 = 0x0200, - - /// - /// Ascii encoding. - /// - Ascii = 0x0400, - - /// - /// Ascii encoding, if possible. - /// - TryAscii = 0x0800, - - // role values - - /// - /// Flag mask for role. - /// - RoleMask = UrlSegment | Alias | UnderscoreAlias | FileName | ConvertCase, - - /// - /// Url role. - /// - UrlSegment = 0x010000, - - /// - /// Alias role. - /// - Alias = 0x020000, - - /// - /// FileName role. - /// - FileName = 0x040000, - - /// - /// ConvertCase role. - /// - ConvertCase = 0x080000, - - /// - /// UnderscoreAlias role. - /// - /// This is Alias + leading underscore. - UnderscoreAlias = 0x100000 - } + /// + /// UnderscoreAlias role. + /// + /// This is Alias + leading underscore. + UnderscoreAlias = 0x100000, } diff --git a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs b/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs index a95a3edfc2..e2eb3df7a4 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs @@ -1,63 +1,70 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings.Css +namespace Umbraco.Cms.Core.Strings.Css; + +public class StylesheetHelper { - public class StylesheetHelper + private const string RuleRegexFormat = + @"/\*\*\s*umb_name:\s*(?{0}?)\s*\*/\s*(?[^,{{]*?)\s*{{\s*(?.*?)\s*}}"; + + public static IEnumerable ParseRules(string? input) { - private const string RuleRegexFormat = @"/\*\*\s*umb_name:\s*(?{0}?)\s*\*/\s*(?[^,{{]*?)\s*{{\s*(?.*?)\s*}}"; + var rules = new List(); + var ruleRegex = new Regex( + string.Format(RuleRegexFormat, @"[^\*\r\n]*"), + RegexOptions.IgnoreCase | RegexOptions.Singleline); - public static IEnumerable ParseRules(string? input) + if (input is not null) { - var rules = new List(); - var ruleRegex = new Regex(string.Format(RuleRegexFormat, @"[^\*\r\n]*"), RegexOptions.IgnoreCase | RegexOptions.Singleline); + var contents = input; + MatchCollection ruleMatches = ruleRegex.Matches(contents); - if (input is not null) + foreach (Match match in ruleMatches) { - var contents = input; - var ruleMatches = ruleRegex.Matches(contents); + var name = match.Groups["Name"].Value; - foreach (Match match in ruleMatches) + // If this name already exists, only use the first one + if (rules.Any(x => x.Name == name)) { - var name = match.Groups["Name"].Value; - - //If this name already exists, only use the first one - if (rules.Any(x => x.Name == name)) continue; - - rules.Add(new StylesheetRule - { - Name = match.Groups["Name"].Value, - Selector = match.Groups["Selector"].Value, - // Only match first selector when chained together - Styles = string.Join(Environment.NewLine, match.Groups["Styles"].Value.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None).Select(x => x.Trim()).ToArray()) - }); + continue; } + + rules.Add(new StylesheetRule + { + Name = match.Groups["Name"].Value, + Selector = match.Groups["Selector"].Value, + + // Only match first selector when chained together + Styles = string.Join( + Environment.NewLine, + match.Groups["Styles"].Value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None) + .Select(x => x.Trim()).ToArray()), + }); } - - - return rules; } - public static string? ReplaceRule(string? input, string oldRuleName, StylesheetRule? rule) + return rules; + } + + public static string? ReplaceRule(string? input, string oldRuleName, StylesheetRule? rule) + { + var contents = input; + if (contents is not null) { - var contents = input; - if (contents is not null) - { - var ruleRegex = new Regex(string.Format(RuleRegexFormat, oldRuleName.EscapeRegexSpecialCharacters()), RegexOptions.IgnoreCase | RegexOptions.Singleline); - contents = ruleRegex.Replace(contents, rule != null ? rule.ToString() : ""); - } - - return contents; + var ruleRegex = new Regex( + string.Format(RuleRegexFormat, oldRuleName.EscapeRegexSpecialCharacters()), + RegexOptions.IgnoreCase | RegexOptions.Singleline); + contents = ruleRegex.Replace(contents, rule != null ? rule.ToString() : string.Empty); } - public static string AppendRule(string? input, StylesheetRule rule) - { - var contents = input; - contents += Environment.NewLine + Environment.NewLine + rule; - return contents; - } + return contents; + } + + public static string AppendRule(string? input, StylesheetRule rule) + { + var contents = input; + contents += Environment.NewLine + Environment.NewLine + rule; + return contents; } } diff --git a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs index 06a888c812..4b726f34ef 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs @@ -1,41 +1,43 @@ -using System; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings.Css +namespace Umbraco.Cms.Core.Strings.Css; + +public class StylesheetRule { - public class StylesheetRule + public string Name { get; set; } = null!; + + public string Selector { get; set; } = null!; + + public string Styles { get; set; } = null!; + + public override string ToString() { - public string Name { get; set; } = null!; + var sb = new StringBuilder(); + sb.Append("/**"); + sb.AppendFormat("umb_name:{0}", Name); + sb.Append("*/"); + sb.Append(Environment.NewLine); + sb.Append(Selector); + sb.Append(" {"); + sb.Append(Environment.NewLine); - public string Selector { get; set; } = null!; - - public string Styles { get; set; } = null!; - - public override string ToString() + // append nicely formatted style rules + // - using tabs because the back office code editor uses tabs + if (Styles.IsNullOrWhiteSpace() == false) { - var sb = new StringBuilder(); - sb.Append("/**"); - sb.AppendFormat("umb_name:{0}", Name); - sb.Append("*/"); - sb.Append(Environment.NewLine); - sb.Append(Selector); - sb.Append(" {"); - sb.Append(Environment.NewLine); - // append nicely formatted style rules - // - using tabs because the back office code editor uses tabs - if (Styles.IsNullOrWhiteSpace() == false) + // since we already have a string builder in play here, we'll append to it the "hard" way + // instead of using string interpolation (for increased performance) + foreach (var style in + Styles?.Split(Constants.CharArrays.Semicolon, StringSplitOptions.RemoveEmptyEntries) ?? + Array.Empty()) { - // since we already have a string builder in play here, we'll append to it the "hard" way - // instead of using string interpolation (for increased performance) - foreach (var style in Styles?.Split(Constants.CharArrays.Semicolon, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()) - { - sb.Append("\t").Append(style.StripNewLines().Trim()).Append(";").Append(Environment.NewLine); - } + sb.Append("\t").Append(style.StripNewLines().Trim()).Append(";").Append(Environment.NewLine); } - sb.Append("}"); - - return sb.ToString(); } + + sb.Append("}"); + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index d4d781c9bc..ea93a099f8 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -1,8 +1,5 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; -using System.IO; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; @@ -143,9 +140,11 @@ namespace Umbraco.Cms.Core.Strings public virtual string CleanStringForSafeFileName(string text, string culture) { if (string.IsNullOrWhiteSpace(text)) + { return string.Empty; + } - culture = culture ?? ""; + culture = culture ?? string.Empty; text = text.ReplaceMany(Path.GetInvalidFileNameChars(), '-'); var name = Path.GetFileNameWithoutExtension(text); @@ -153,12 +152,17 @@ namespace Umbraco.Cms.Core.Strings Debug.Assert(name != null, "name != null"); if (name.Length > 0) + { name = CleanString(name, CleanStringType.FileName, culture); + } + Debug.Assert(ext != null, "ext != null"); if (ext.Length > 0) + { ext = CleanString(ext.Substring(1), CleanStringType.FileName, culture); + } - return ext.Length > 0 ? (name + "." + ext) : name; + return ext.Length > 0 ? name + "." + ext : name; } #endregion @@ -190,10 +194,7 @@ namespace Umbraco.Cms.Core.Strings /// strings are cleaned up to camelCase and Ascii. /// The clean string. /// The string is cleaned in the context of the default culture. - public string CleanString(string text, CleanStringType stringType) - { - return CleanString(text, stringType, _config.DefaultCulture, null); - } + public string CleanString(string text, CleanStringType stringType) => CleanString(text, stringType, _config.DefaultCulture, null); /// /// Cleans a string, using a specified separator. @@ -204,10 +205,7 @@ namespace Umbraco.Cms.Core.Strings /// The separator. /// The clean string. /// The string is cleaned in the context of the default culture. - public string CleanString(string text, CleanStringType stringType, char separator) - { - return CleanString(text, stringType, _config.DefaultCulture, separator); - } + public string CleanString(string text, CleanStringType stringType, char separator) => CleanString(text, stringType, _config.DefaultCulture, separator); /// /// Cleans a string in the context of a specified culture. @@ -239,32 +237,43 @@ namespace Umbraco.Cms.Core.Strings protected virtual string CleanString(string text, CleanStringType stringType, string? culture, char? separator) { // be safe - if (text == null) throw new ArgumentNullException(nameof(text)); - culture = culture ?? ""; + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + culture = culture ?? string.Empty; // get config - var config = _config.For(stringType, culture); + DefaultShortStringHelperConfig.Config config = _config.For(stringType, culture); stringType = config.StringTypeExtend(stringType); // apply defaults if ((stringType & CleanStringType.CaseMask) == CleanStringType.None) + { stringType |= CleanStringType.CamelCase; + } + if ((stringType & CleanStringType.CodeMask) == CleanStringType.None) + { stringType |= CleanStringType.Ascii; + } // use configured unless specified separator = separator ?? config.Separator; // apply pre-filter if (config.PreFilter != null) + { text = config.PreFilter(text); + } // apply replacements //if (config.Replacements != null) // text = ReplaceMany(text, config.Replacements); // recode - var codeType = stringType & CleanStringType.CodeMask; + CleanStringType codeType = stringType & CleanStringType.CodeMask; switch (codeType) { case CleanStringType.Ascii: @@ -273,7 +282,11 @@ namespace Umbraco.Cms.Core.Strings case CleanStringType.TryAscii: const char ESC = (char) 27; var ctext = Utf8ToAsciiConverter.ToAsciiString(text, ESC); - if (ctext.Contains(ESC) == false) text = ctext; + if (ctext.Contains(ESC) == false) + { + text = ctext; + } + break; default: text = RemoveSurrogatePairs(text); @@ -285,7 +298,9 @@ namespace Umbraco.Cms.Core.Strings // apply post-filter if (config.PostFilter != null) + { text = config.PostFilter(text); + } return text; } @@ -323,7 +338,7 @@ namespace Umbraco.Cms.Core.Strings int opos = 0, ipos = 0; var state = StateBreak; - culture = culture ?? ""; + culture = culture ?? string.Empty; caseType &= CleanStringType.CaseMask; // if we apply global ToUpper or ToLower to text here @@ -364,9 +379,13 @@ namespace Umbraco.Cms.Core.Strings { ipos = i; if (opos > 0 && separator != char.MinValue) + { output[opos++] = separator; + } + state = isUpper ? StateUp : StateWord; } + break; // within a term / word @@ -379,8 +398,11 @@ namespace Umbraco.Cms.Core.Strings ipos = i; state = isTerm ? StateUp : StateBreak; if (state != StateBreak && separator != char.MinValue) + { output[opos++] = separator; + } } + break; // within a term / acronym @@ -391,14 +413,19 @@ namespace Umbraco.Cms.Core.Strings { // whether it's part of the acronym depends on whether we're greedy if (isTerm && config.GreedyAcronyms == false) + { i -= 1; // handle that char again, in another state - not part of the acronym + } + if (i - ipos > 1) // single-char can't be an acronym { CopyTerm(input, ipos, output, ref opos, i - ipos, caseType, culture, true); ipos = i; state = isTerm ? StateWord : StateBreak; if (state != StateBreak && separator != char.MinValue) + { output[opos++] = separator; + } } else if (isTerm) { @@ -411,6 +438,7 @@ namespace Umbraco.Cms.Core.Strings // keep moving forward as a word state = StateWord; } + break; // within a term / uppercase = could be a word or an acronym @@ -455,18 +483,19 @@ namespace Umbraco.Cms.Core.Strings } // note: supports surrogate pairs in input string - internal void CopyTerm(string input, int ipos, char[] output, ref int opos, int len, - CleanStringType caseType, string culture, bool isAcronym) + internal void CopyTerm(string input, int ipos, char[] output, ref int opos, int len, CleanStringType caseType, string culture, bool isAcronym) { var term = input.Substring(ipos, len); - var cultureInfo = string.IsNullOrEmpty(culture) ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); + CultureInfo cultureInfo = string.IsNullOrEmpty(culture) ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); if (isAcronym) { if ((caseType == CleanStringType.CamelCase && len <= 2 && opos > 0) || (caseType == CleanStringType.PascalCase && len <= 2) || - (caseType == CleanStringType.UmbracoCase)) + caseType == CleanStringType.UmbracoCase) + { caseType = CleanStringType.Unchanged; + } } // note: MSDN seems to imply that ToUpper or ToLower preserve the length @@ -586,7 +615,9 @@ namespace Umbraco.Cms.Core.Strings { // be safe if (text == null) + { throw new ArgumentNullException(nameof(text)); + } var input = text.ToCharArray(); var output = new char[input.Length * 2]; @@ -603,7 +634,10 @@ namespace Umbraco.Cms.Core.Strings if (upos == 0) { if (opos > 0) + { output[opos++] = separator; + } + upos = i + 1; } } @@ -612,15 +646,24 @@ namespace Umbraco.Cms.Core.Strings if (upos > 0) { if (upos < i && opos > 0) + { output[opos++] = separator; + } + upos = 0; } + output[opos++] = a; } + a = c; } + if (a != char.MinValue) + { output[opos++] = a; + } + return new string(output, 0, opos); } diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs index 4f2d202155..ec7ed9d002 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs @@ -1,227 +1,249 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings -{ - public class DefaultShortStringHelperConfig - { - private readonly Dictionary> _configs = new Dictionary>(); +namespace Umbraco.Cms.Core.Strings; - public DefaultShortStringHelperConfig Clone() +public class DefaultShortStringHelperConfig +{ + private readonly Dictionary> _configs = new(); + + public string DefaultCulture { get; set; } = string.Empty; // invariant + + public Dictionary? UrlReplaceCharacters { get; set; } + + public DefaultShortStringHelperConfig Clone() + { + var config = new DefaultShortStringHelperConfig { - var config = new DefaultShortStringHelperConfig + DefaultCulture = DefaultCulture, + UrlReplaceCharacters = UrlReplaceCharacters, + }; + + foreach (KeyValuePair> kvp1 in _configs) + { + Dictionary c = config._configs[kvp1.Key] = + new Dictionary(); + foreach (KeyValuePair kvp2 in _configs[kvp1.Key]) { - DefaultCulture = DefaultCulture, - UrlReplaceCharacters = UrlReplaceCharacters + c[kvp2.Key] = kvp2.Value.Clone(); + } + } + + return config; + } + + public DefaultShortStringHelperConfig WithConfig(Config config) => + WithConfig(DefaultCulture, CleanStringType.RoleMask, config); + + public DefaultShortStringHelperConfig WithConfig(CleanStringType stringRole, Config config) => + WithConfig(DefaultCulture, stringRole, config); + + public DefaultShortStringHelperConfig WithConfig(string? culture, CleanStringType stringRole, Config config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + culture = culture ?? string.Empty; + + if (_configs.ContainsKey(culture) == false) + { + _configs[culture] = new Dictionary(); + } + + _configs[culture][stringRole] = config; + return this; + } + + /// + /// Sets the default configuration. + /// + /// The short string helper. + public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings) + { + IEnumerable charCollection = requestHandlerSettings.GetCharReplacements(); + + UrlReplaceCharacters = charCollection + .Where(x => string.IsNullOrEmpty(x.Char) == false) + .ToDictionary(x => x.Char, x => x.Replacement); + + CleanStringType urlSegmentConvertTo = CleanStringType.Utf8; + if (requestHandlerSettings.ShouldConvertUrlsToAscii) + { + urlSegmentConvertTo = CleanStringType.Ascii; + } + + if (requestHandlerSettings.ShouldTryConvertUrlsToAscii) + { + urlSegmentConvertTo = CleanStringType.TryAscii; + } + + return WithConfig(CleanStringType.UrlSegment, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + PostFilter = x => CutMaxLength(x, 240), + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = urlSegmentConvertTo | CleanStringType.LowerCase, + BreakTermsOnUpper = false, + Separator = '-', + }).WithConfig(CleanStringType.FileName, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Utf8 | CleanStringType.LowerCase, + BreakTermsOnUpper = false, + Separator = '-', + }).WithConfig(CleanStringType.Alias, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + IsTerm = (c, leading) => leading + ? char.IsLetter(c) // only letters + : char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, + BreakTermsOnUpper = false, + }).WithConfig(CleanStringType.UnderscoreAlias, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, + BreakTermsOnUpper = false, + }).WithConfig(CleanStringType.ConvertCase, new Config + { + PreFilter = null, + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Ascii, + BreakTermsOnUpper = true, + }); + } + + // internal: we don't want ppl to retrieve a config and modify it + // (the helper uses a private clone to prevent modifications) + internal Config For(CleanStringType stringType, string? culture) + { + culture = culture ?? string.Empty; + stringType = stringType & CleanStringType.RoleMask; + + Dictionary config; + if (_configs.ContainsKey(culture)) + { + config = _configs[culture]; + + // have we got a config for _that_ role? + if (config.ContainsKey(stringType)) + { + return config[stringType]; + } + + // have we got a generic config for _all_ roles? + if (config.ContainsKey(CleanStringType.RoleMask)) + { + return config[CleanStringType.RoleMask]; + } + } + else if (_configs.ContainsKey(DefaultCulture)) + { + config = _configs[DefaultCulture]; + + // have we got a config for _that_ role? + if (config.ContainsKey(stringType)) + { + return config[stringType]; + } + + // have we got a generic config for _all_ roles? + if (config.ContainsKey(CleanStringType.RoleMask)) + { + return config[CleanStringType.RoleMask]; + } + } + + return Config.NotConfigured; + } + + /// + /// Returns a new string in which characters have been replaced according to the Umbraco settings UrlReplaceCharacters. + /// + /// The string to filter. + /// The filtered string. + public string ApplyUrlReplaceCharacters(string s) => + UrlReplaceCharacters == null ? s : s.ReplaceMany(UrlReplaceCharacters); + + public static string CutMaxLength(string text, int length) => + text.Length <= length ? text : text.Substring(0, length); + + public sealed class Config + { + internal static readonly Config NotConfigured = new(); + + public Config() + { + StringType = CleanStringType.Utf8 | CleanStringType.Unchanged; + PreFilter = null; + PostFilter = null; + IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c); + BreakTermsOnUpper = false; + CutAcronymOnNonUpper = false; + GreedyAcronyms = false; + Separator = char.MinValue; + } + + public Func? PreFilter { get; set; } + + public Func? PostFilter { get; set; } + + public Func IsTerm { get; set; } + + public CleanStringType StringType { get; set; } + + // indicate whether an uppercase within a term eg "fooBar" is to break + // into a new term, or to be considered as part of the current term + public bool BreakTermsOnUpper { get; set; } + + // indicate whether a non-uppercase within an acronym eg "FOOBar" is to cut + // the acronym (at "B" or "a" depending on GreedyAcronyms) or to give + // up the acronym and treat the term as a word + public bool CutAcronymOnNonUpper { get; set; } + + // indicates whether acronyms parsing is greedy ie whether "FOObar" is + // "FOO" + "bar" (greedy) or "FO" + "Obar" (non-greedy) + public bool GreedyAcronyms { get; set; } + + // the separator char + // but then how can we tell we don't want any? + public char Separator { get; set; } + + public Config Clone() => + new Config + { + PreFilter = PreFilter, + PostFilter = PostFilter, + IsTerm = IsTerm, + StringType = StringType, + BreakTermsOnUpper = BreakTermsOnUpper, + CutAcronymOnNonUpper = CutAcronymOnNonUpper, + GreedyAcronyms = GreedyAcronyms, + Separator = Separator, }; - foreach (var kvp1 in _configs) - { - var c = config._configs[kvp1.Key] = new Dictionary(); - foreach (var kvp2 in _configs[kvp1.Key]) - c[kvp2.Key] = kvp2.Value.Clone(); - } - - return config; - } - - public string DefaultCulture { get; set; } = ""; // invariant - - public Dictionary? UrlReplaceCharacters { get; set; } - - public DefaultShortStringHelperConfig WithConfig(Config config) + // extends the config + public CleanStringType StringTypeExtend(CleanStringType stringType) { - return WithConfig(DefaultCulture, CleanStringType.RoleMask, config); - } - - public DefaultShortStringHelperConfig WithConfig(CleanStringType stringRole, Config config) - { - return WithConfig(DefaultCulture, stringRole, config); - } - - public DefaultShortStringHelperConfig WithConfig(string culture, CleanStringType stringRole, Config config) - { - if (config == null) throw new ArgumentNullException(nameof(config)); - - culture = culture ?? ""; - - if (_configs.ContainsKey(culture) == false) - _configs[culture] = new Dictionary(); - _configs[culture][stringRole] = config; - return this; - } - - /// - /// Sets the default configuration. - /// - /// The short string helper. - public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings) - { - IEnumerable charCollection = requestHandlerSettings.GetCharReplacements(); - - UrlReplaceCharacters = charCollection - .Where(x => string.IsNullOrEmpty(x.Char) == false) - .ToDictionary(x => x.Char, x => x.Replacement); - - var urlSegmentConvertTo = CleanStringType.Utf8; - if (requestHandlerSettings.ShouldConvertUrlsToAscii) - urlSegmentConvertTo = CleanStringType.Ascii; - if (requestHandlerSettings.ShouldTryConvertUrlsToAscii) - urlSegmentConvertTo = CleanStringType.TryAscii; - - return WithConfig(CleanStringType.UrlSegment, new Config + CleanStringType st = StringType; + foreach (CleanStringType mask in new[] { CleanStringType.CaseMask, CleanStringType.CodeMask }) { - PreFilter = ApplyUrlReplaceCharacters, - PostFilter = x => CutMaxLength(x, 240), - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = urlSegmentConvertTo | CleanStringType.LowerCase, - BreakTermsOnUpper = false, - Separator = '-' - }).WithConfig(CleanStringType.FileName, new Config - { - PreFilter = ApplyUrlReplaceCharacters, - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = CleanStringType.Utf8 | CleanStringType.LowerCase, - BreakTermsOnUpper = false, - Separator = '-' - }).WithConfig(CleanStringType.Alias, new Config - { - PreFilter = ApplyUrlReplaceCharacters, - IsTerm = (c, leading) => leading - ? char.IsLetter(c) // only letters - : (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore - StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, - BreakTermsOnUpper = false - }).WithConfig(CleanStringType.UnderscoreAlias, new Config - { - PreFilter = ApplyUrlReplaceCharacters, - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, - BreakTermsOnUpper = false - }).WithConfig(CleanStringType.ConvertCase, new Config - { - PreFilter = null, - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = CleanStringType.Ascii, - BreakTermsOnUpper = true - }); - } - - // internal: we don't want ppl to retrieve a config and modify it - // (the helper uses a private clone to prevent modifications) - internal Config For(CleanStringType stringType, string culture) - { - culture = culture ?? ""; - stringType = stringType & CleanStringType.RoleMask; - - Dictionary config; - if (_configs.ContainsKey(culture)) - { - config = _configs[culture]; - if (config.ContainsKey(stringType)) // have we got a config for _that_ role? - return config[stringType]; - if (config.ContainsKey(CleanStringType.RoleMask)) // have we got a generic config for _all_ roles? - return config[CleanStringType.RoleMask]; - } - else if (_configs.ContainsKey(DefaultCulture)) - { - config = _configs[DefaultCulture]; - if (config.ContainsKey(stringType)) // have we got a config for _that_ role? - return config[stringType]; - if (config.ContainsKey(CleanStringType.RoleMask)) // have we got a generic config for _all_ roles? - return config[CleanStringType.RoleMask]; - } - - return Config.NotConfigured; - } - - public sealed class Config - { - public Config() - { - StringType = CleanStringType.Utf8 | CleanStringType.Unchanged; - PreFilter = null; - PostFilter = null; - IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c); - BreakTermsOnUpper = false; - CutAcronymOnNonUpper = false; - GreedyAcronyms = false; - Separator = char.MinValue; - } - - public Config Clone() - { - return new Config + CleanStringType a = stringType & mask; + if (a == 0) { - PreFilter = PreFilter, - PostFilter = PostFilter, - IsTerm = IsTerm, - StringType = StringType, - BreakTermsOnUpper = BreakTermsOnUpper, - CutAcronymOnNonUpper = CutAcronymOnNonUpper, - GreedyAcronyms = GreedyAcronyms, - Separator = Separator - }; - } - - public Func? PreFilter { get; set; } - public Func? PostFilter { get; set; } - public Func IsTerm { get; set; } - - public CleanStringType StringType { get; set; } - - // indicate whether an uppercase within a term eg "fooBar" is to break - // into a new term, or to be considered as part of the current term - public bool BreakTermsOnUpper { get; set; } - - // indicate whether a non-uppercase within an acronym eg "FOOBar" is to cut - // the acronym (at "B" or "a" depending on GreedyAcronyms) or to give - // up the acronym and treat the term as a word - public bool CutAcronymOnNonUpper { get; set; } - - // indicates whether acronyms parsing is greedy ie whether "FOObar" is - // "FOO" + "bar" (greedy) or "FO" + "Obar" (non-greedy) - public bool GreedyAcronyms { get; set; } - - // the separator char - // but then how can we tell we don't want any? - public char Separator { get; set; } - - // extends the config - public CleanStringType StringTypeExtend(CleanStringType stringType) - { - var st = StringType; - foreach (var mask in new[] { CleanStringType.CaseMask, CleanStringType.CodeMask }) - { - var a = stringType & mask; - if (a == 0) continue; - - st = st & ~mask; // clear what we have - st = st | a; // set the new value + continue; } - return st; + + st = st & ~mask; // clear what we have + st = st | a; // set the new value } - internal static readonly Config NotConfigured = new Config(); - } - - /// - /// Returns a new string in which characters have been replaced according to the Umbraco settings UrlReplaceCharacters. - /// - /// The string to filter. - /// The filtered string. - public string ApplyUrlReplaceCharacters(string s) - { - return UrlReplaceCharacters == null ? s : s.ReplaceMany(UrlReplaceCharacters); - } - - public static string CutMaxLength(string text, int length) - { - return text.Length <= length ? text : text.Substring(0, length); + return st; } } } diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 22b8a40c0e..36c0d6e85e 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -1,45 +1,43 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Default implementation of IUrlSegmentProvider. +/// +public class DefaultUrlSegmentProvider : IUrlSegmentProvider { + private readonly IShortStringHelper _shortStringHelper; + + public DefaultUrlSegmentProvider(IShortStringHelper shortStringHelper) => _shortStringHelper = shortStringHelper; + /// - /// Default implementation of IUrlSegmentProvider. + /// Gets the URL segment for a specified content and culture. /// - public class DefaultUrlSegmentProvider : IUrlSegmentProvider + /// The content. + /// The culture. + /// The URL segment. + public string? GetUrlSegment(IContentBase content, string? culture = null) => + GetUrlSegmentSource(content, culture)?.ToUrlSegment(_shortStringHelper, culture); + + private static string? GetUrlSegmentSource(IContentBase content, string? culture) { - private readonly IShortStringHelper _shortStringHelper; - - public DefaultUrlSegmentProvider(IShortStringHelper shortStringHelper) + string? source = null; + if (content.HasProperty(Constants.Conventions.Content.UrlName)) { - _shortStringHelper = shortStringHelper; + source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); } - /// - /// Gets the URL segment for a specified content and culture. - /// - /// The content. - /// The culture. - /// The URL segment. - public string? GetUrlSegment(IContentBase content, string? culture = null) + if (string.IsNullOrWhiteSpace(source)) { - return GetUrlSegmentSource(content, culture)?.ToUrlSegment(_shortStringHelper, culture); + // If the name of a node has been updated, but it has not been published, the url should use the published name, not the current node name + // If this node has never been published (GetPublishName is null), use the unpublished name + source = content is IContent document && document.Edited && document.GetPublishName(culture) != null + ? document.GetPublishName(culture) + : content.GetCultureName(culture); } - private static string? GetUrlSegmentSource(IContentBase content, string? culture) - { - string? source = null; - if (content.HasProperty(Constants.Conventions.Content.UrlName)) - source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); - if (string.IsNullOrWhiteSpace(source)) - { - // If the name of a node has been updated, but it has not been published, the url should use the published name, not the current node name - // If this node has never been published (GetPublishName is null), use the unpublished name - source = (content is IContent document) && document.Edited && document.GetPublishName(culture) != null - ? document.GetPublishName(culture) - : content.GetCultureName(culture); - } - return source; - } + return source; } } diff --git a/src/Umbraco.Core/Strings/Diff.cs b/src/Umbraco.Core/Strings/Diff.cs index 8d7ef9feaa..e8a3fdf84c 100644 --- a/src/Umbraco.Core/Strings/Diff.cs +++ b/src/Umbraco.Core/Strings/Diff.cs @@ -1,507 +1,540 @@ -using System; using System.Collections; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// This Class implements the Difference Algorithm published in +/// "An O(ND) Difference Algorithm and its Variations" by Eugene Myers +/// Algorithmica Vol. 1 No. 2, 1986, p 251. +/// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents +/// each line is converted into a (hash) number. See DiffText(). +/// diff.cs: A port of the algorithm to C# +/// Copyright (c) by Matthias Hertel, http://www.mathertel.de +/// This work is licensed under a BSD style license. See http://www.mathertel.de/License.aspx +/// +internal class Diff { /// - /// This Class implements the Difference Algorithm published in - /// "An O(ND) Difference Algorithm and its Variations" by Eugene Myers - /// Algorithmica Vol. 1 No. 2, 1986, p 251. - /// - /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents - /// each line is converted into a (hash) number. See DiffText(). - /// - /// diff.cs: A port of the algorithm to C# - /// Copyright (c) by Matthias Hertel, http://www.mathertel.de - /// This work is licensed under a BSD style license. See http://www.mathertel.de/License.aspx + /// Find the difference in 2 texts, comparing by text lines. /// - internal class Diff + /// A-version of the text (usually the old one) + /// B-version of the text (usually the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffText(string textA, string textB) => + DiffText(textA, textB, false, false, false); // DiffText + + /// + /// Find the difference in 2 texts, comparing by text lines. + /// This method uses the DiffInt internally by 1st converting the string into char codes + /// then uses the diff int method + /// + /// A-version of the text (usually the old one) + /// B-version of the text (usually the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffText1(string textA, string textB) => + DiffInt(DiffCharCodes(textA, false), DiffCharCodes(textB, false)); + + /// + /// Find the difference in 2 text documents, comparing by text lines. + /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents + /// each line is converted into a (hash) number. This hash-value is computed by storing all + /// text lines into a common Hashtable so i can find duplicates in there, and generating a + /// new number each time a new text line is inserted. + /// + /// A-version of the text (usually the old one) + /// B-version of the text (usually the new one) + /// + /// When set to true, all leading and trailing whitespace characters are stripped out before the + /// comparison is done. + /// + /// + /// When set to true, all whitespace characters are converted to a single space character before + /// the comparison is done. + /// + /// + /// When set to true, all characters are converted to their lowercase equivalence before the + /// comparison is done. + /// + /// Returns a array of Items that describe the differences. + public static Item[] DiffText(string textA, string textB, bool trimSpace, bool ignoreSpace, bool ignoreCase) { - /// Data on one input file being compared. - /// - internal class DiffData + // prepare the input-text and convert to comparable numbers. + var h = new Hashtable(textA.Length + textB.Length); + + // The A-Version of the data (original data) to be compared. + var dataA = new DiffData(DiffCodes(textA, h, trimSpace, ignoreSpace, ignoreCase)); + + // The B-Version of the data (modified data) to be compared. + var dataB = new DiffData(DiffCodes(textB, h, trimSpace, ignoreSpace, ignoreCase)); + + h = null; // free up Hashtable memory (maybe) + + var max = dataA.Length + dataB.Length + 1; + + // vector for the (0,0) to (x,y) search + var downVector = new int[(2 * max) + 2]; + + // vector for the (u,v) to (N,M) search + var upVector = new int[(2 * max) + 2]; + + Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); + + Optimize(dataA); + Optimize(dataB); + return CreateDiffs(dataA, dataB); + } // DiffText + + /// + /// Find the difference in 2 arrays of integers. + /// + /// A-version of the numbers (usually the old one) + /// B-version of the numbers (usually the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffInt(int[] arrayA, int[] arrayB) + { + // The A-Version of the data (original data) to be compared. + var dataA = new DiffData(arrayA); + + // The B-Version of the data (modified data) to be compared. + var dataB = new DiffData(arrayB); + + var max = dataA.Length + dataB.Length + 1; + + // vector for the (0,0) to (x,y) search + var downVector = new int[(2 * max) + 2]; + + // vector for the (u,v) to (N,M) search + var upVector = new int[(2 * max) + 2]; + + Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); + return CreateDiffs(dataA, dataB); + } // Diff + + /// + /// Diffs the char codes. + /// + /// A text. + /// if set to true [ignore case]. + /// + private static int[] DiffCharCodes(string aText, bool ignoreCase) + { + if (ignoreCase) { - - /// Number of elements (lines). - internal int Length; - - /// Buffer of numbers that will be compared. - internal int[] Data; - - /// - /// Array of booleans that flag for modified data. - /// This is the result of the diff. - /// This means deletedA in the first Data or inserted in the second Data. - /// - internal bool[] Modified; - - /// - /// Initialize the Diff-Data buffer. - /// - /// reference to the buffer - internal DiffData(int[] initData) - { - Data = initData; - Length = initData.Length; - Modified = new bool[Length + 2]; - } // DiffData - - } // class DiffData - - /// details of one difference. - public struct Item - { - /// Start Line number in Data A. - public int StartA; - /// Start Line number in Data B. - public int StartB; - - /// Number of changes in Data A. - public int DeletedA; - /// Number of changes in Data B. - public int InsertedB; - } // Item - - /// - /// Shortest Middle Snake Return Data - /// - private struct Smsrd - { - internal int X, Y; - // internal int u, v; // 2002.09.20: no need for 2 points + aText = aText.ToUpperInvariant(); } - /// - /// Find the difference in 2 texts, comparing by text lines. - /// - /// A-version of the text (usually the old one) - /// B-version of the text (usually the new one) - /// Returns a array of Items that describe the differences. - public static Item[] DiffText(string textA, string textB) - { - return (DiffText(textA, textB, false, false, false)); - } // DiffText + var codes = new int[aText.Length]; - /// - /// Find the difference in 2 texts, comparing by text lines. - /// This method uses the DiffInt internally by 1st converting the string into char codes - /// then uses the diff int method - /// - /// A-version of the text (usually the old one) - /// B-version of the text (usually the new one) - /// Returns a array of Items that describe the differences. - public static Item[] DiffText1(string textA, string textB) + for (var n = 0; n < aText.Length; n++) { - return DiffInt(DiffCharCodes(textA, false), DiffCharCodes(textB, false)); + codes[n] = aText[n]; } + return codes; + } // DiffCharCodes - /// - /// Find the difference in 2 text documents, comparing by text lines. - /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents - /// each line is converted into a (hash) number. This hash-value is computed by storing all - /// text lines into a common Hashtable so i can find duplicates in there, and generating a - /// new number each time a new text line is inserted. - /// - /// A-version of the text (usually the old one) - /// B-version of the text (usually the new one) - /// When set to true, all leading and trailing whitespace characters are stripped out before the comparison is done. - /// When set to true, all whitespace characters are converted to a single space character before the comparison is done. - /// When set to true, all characters are converted to their lowercase equivalence before the comparison is done. - /// Returns a array of Items that describe the differences. - public static Item[] DiffText(string textA, string textB, bool trimSpace, bool ignoreSpace, bool ignoreCase) + /// + /// If a sequence of modified lines starts with a line that contains the same content + /// as the line that appends the changes, the difference sequence is modified so that the + /// appended line and not the starting line is marked as modified. + /// This leads to more readable diff sequences when comparing text files. + /// + /// A Diff data buffer containing the identified changes. + private static void Optimize(DiffData data) + { + var startPos = 0; + while (startPos < data.Length) { - // prepare the input-text and convert to comparable numbers. - var h = new Hashtable(textA.Length + textB.Length); - - // The A-Version of the data (original data) to be compared. - var dataA = new DiffData(DiffCodes(textA, h, trimSpace, ignoreSpace, ignoreCase)); - - // The B-Version of the data (modified data) to be compared. - var dataB = new DiffData(DiffCodes(textB, h, trimSpace, ignoreSpace, ignoreCase)); - - h = null; // free up Hashtable memory (maybe) - - var max = dataA.Length + dataB.Length + 1; - // vector for the (0,0) to (x,y) search - var downVector = new int[2 * max + 2]; - // vector for the (u,v) to (N,M) search - var upVector = new int[2 * max + 2]; - - Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); - - Optimize(dataA); - Optimize(dataB); - return CreateDiffs(dataA, dataB); - } // DiffText - - - /// - /// Diffs the char codes. - /// - /// A text. - /// if set to true [ignore case]. - /// - private static int[] DiffCharCodes(string aText, bool ignoreCase) - { - if (ignoreCase) - aText = aText.ToUpperInvariant(); - - var codes = new int[aText.Length]; - - for (int n = 0; n < aText.Length; n++) - codes[n] = (int)aText[n]; - - return (codes); - } // DiffCharCodes - - /// - /// If a sequence of modified lines starts with a line that contains the same content - /// as the line that appends the changes, the difference sequence is modified so that the - /// appended line and not the starting line is marked as modified. - /// This leads to more readable diff sequences when comparing text files. - /// - /// A Diff data buffer containing the identified changes. - private static void Optimize(DiffData data) - { - var startPos = 0; - while (startPos < data.Length) + while (startPos < data.Length && data.Modified[startPos] == false) { - while ((startPos < data.Length) && (data.Modified[startPos] == false)) - startPos++; - int endPos = startPos; - while ((endPos < data.Length) && (data.Modified[endPos] == true)) - endPos++; - - if ((endPos < data.Length) && (data.Data[startPos] == data.Data[endPos])) - { - data.Modified[startPos] = false; - data.Modified[endPos] = true; - } - else - { - startPos = endPos; - } // if - } // while - } // Optimize - - - /// - /// Find the difference in 2 arrays of integers. - /// - /// A-version of the numbers (usually the old one) - /// B-version of the numbers (usually the new one) - /// Returns a array of Items that describe the differences. - public static Item[] DiffInt(int[] arrayA, int[] arrayB) - { - // The A-Version of the data (original data) to be compared. - var dataA = new DiffData(arrayA); - - // The B-Version of the data (modified data) to be compared. - var dataB = new DiffData(arrayB); - - var max = dataA.Length + dataB.Length + 1; - // vector for the (0,0) to (x,y) search - var downVector = new int[2 * max + 2]; - // vector for the (u,v) to (N,M) search - var upVector = new int[2 * max + 2]; - - Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); - return CreateDiffs(dataA, dataB); - } // Diff - - - /// - /// This function converts all text lines of the text into unique numbers for every unique text line - /// so further work can work only with simple numbers. - /// - /// the input text - /// This extern initialized Hashtable is used for storing all ever used text lines. - /// ignore leading and trailing space characters - /// - /// - /// a array of integers. - private static int[] DiffCodes(string aText, IDictionary h, bool trimSpace, bool ignoreSpace, bool ignoreCase) - { - // get all codes of the text - var lastUsedCode = h.Count; - - // strip off all cr, only use lf as text line separator. - aText = aText.Replace("\r", ""); - var lines = aText.Split(Constants.CharArrays.LineFeed); - - var codes = new int[lines.Length]; - - for (int i = 0; i < lines.Length; ++i) - { - string s = lines[i]; - if (trimSpace) - s = s.Trim(); - - if (ignoreSpace) - { - s = Regex.Replace(s, "\\s+", " "); // TODO: optimization: faster blank removal. - } - - if (ignoreCase) - s = s.ToLower(); - - object? aCode = h[s]; - if (aCode == null) - { - lastUsedCode++; - h[s] = lastUsedCode; - codes[i] = lastUsedCode; - } - else - { - codes[i] = (int)aCode; - } // if - } // for - return (codes); - } // DiffCodes - - - /// - /// This is the algorithm to find the Shortest Middle Snake (SMS). - /// - /// sequence A - /// lower bound of the actual range in DataA - /// upper bound of the actual range in DataA (exclusive) - /// sequence B - /// lower bound of the actual range in DataB - /// upper bound of the actual range in DataB (exclusive) - /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. - /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. - /// a MiddleSnakeData record containing x,y and u,v - private static Smsrd Sms(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) - { - int max = dataA.Length + dataB.Length + 1; - - int downK = lowerA - lowerB; // the k-line to start the forward search - int upK = upperA - upperB; // the k-line to start the reverse search - - int delta = (upperA - lowerA) - (upperB - lowerB); - bool oddDelta = (delta & 1) != 0; - - // The vectors in the publication accepts negative indexes. the vectors implemented here are 0-based - // and are access using a specific offset: UpOffset UpVector and DownOffset for DownVektor - int downOffset = max - downK; - int upOffset = max - upK; - - int maxD = ((upperA - lowerA + upperB - lowerB) / 2) + 1; - - // Debug.Write(2, "SMS", String.Format("Search the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); - - // init vectors - downVector[downOffset + downK + 1] = lowerA; - upVector[upOffset + upK - 1] = upperA; - - for (int d = 0; d <= maxD; d++) - { - - // Extend the forward path. - Smsrd ret; - for (int k = downK - d; k <= downK + d; k += 2) - { - // Debug.Write(0, "SMS", "extend forward path " + k.ToString()); - - // find the only or better starting point - int x, y; - if (k == downK - d) - { - x = downVector[downOffset + k + 1]; // down - } - else - { - x = downVector[downOffset + k - 1] + 1; // a step to the right - if ((k < downK + d) && (downVector[downOffset + k + 1] >= x)) - x = downVector[downOffset + k + 1]; // down - } - y = x - k; - - // find the end of the furthest reaching forward D-path in diagonal k. - while ((x < upperA) && (y < upperB) && (dataA.Data[x] == dataB.Data[y])) - { - x++; y++; - } - downVector[downOffset + k] = x; - - // overlap ? - if (oddDelta && (upK - d < k) && (k < upK + d)) - { - if (upVector[upOffset + k] <= downVector[downOffset + k]) - { - ret.X = downVector[downOffset + k]; - ret.Y = downVector[downOffset + k] - k; - // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points - // ret.v = UpVector[UpOffset + k] - k; - return (ret); - } // if - } // if - - } // for k - - // Extend the reverse path. - for (int k = upK - d; k <= upK + d; k += 2) - { - // Debug.Write(0, "SMS", "extend reverse path " + k.ToString()); - - // find the only or better starting point - int x, y; - if (k == upK + d) - { - x = upVector[upOffset + k - 1]; // up - } - else - { - x = upVector[upOffset + k + 1] - 1; // left - if ((k > upK - d) && (upVector[upOffset + k - 1] < x)) - x = upVector[upOffset + k - 1]; // up - } // if - y = x - k; - - while ((x > lowerA) && (y > lowerB) && (dataA.Data[x - 1] == dataB.Data[y - 1])) - { - x--; y--; // diagonal - } - upVector[upOffset + k] = x; - - // overlap ? - if (!oddDelta && (downK - d <= k) && (k <= downK + d)) - { - if (upVector[upOffset + k] <= downVector[downOffset + k]) - { - ret.X = downVector[downOffset + k]; - ret.Y = downVector[downOffset + k] - k; - // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points - // ret.v = UpVector[UpOffset + k] - k; - return (ret); - } // if - } // if - - } // for k - - } // for D - - throw new ApplicationException("the algorithm should never come here."); - } // SMS - - - /// - /// This is the divide-and-conquer implementation of the longest common-subsequence (LCS) - /// algorithm. - /// The published algorithm passes recursively parts of the A and B sequences. - /// To avoid copying these arrays the lower and upper bounds are passed while the sequences stay constant. - /// - /// sequence A - /// lower bound of the actual range in DataA - /// upper bound of the actual range in DataA (exclusive) - /// sequence B - /// lower bound of the actual range in DataB - /// upper bound of the actual range in DataB (exclusive) - /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. - /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. - private static void Lcs(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) - { - // Debug.Write(2, "LCS", String.Format("Analyze the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); - - // Fast walk through equal lines at the start - while (lowerA < upperA && lowerB < upperB && dataA.Data[lowerA] == dataB.Data[lowerB]) - { - lowerA++; lowerB++; + startPos++; } - // Fast walk through equal lines at the end - while (lowerA < upperA && lowerB < upperB && dataA.Data[upperA - 1] == dataB.Data[upperB - 1]) + var endPos = startPos; + while (endPos < data.Length && data.Modified[endPos]) { - --upperA; --upperB; + endPos++; } - if (lowerA == upperA) + if (endPos < data.Length && data.Data[startPos] == data.Data[endPos]) { - // mark as inserted lines. - while (lowerB < upperB) - dataB.Modified[lowerB++] = true; - - } - else if (lowerB == upperB) - { - // mark as deleted lines. - while (lowerA < upperA) - dataA.Modified[lowerA++] = true; - + data.Modified[startPos] = false; + data.Modified[endPos] = true; } else { - // Find the middle snake and length of an optimal path for A and B - Smsrd smsrd = Sms(dataA, lowerA, upperA, dataB, lowerB, upperB, downVector, upVector); - // Debug.Write(2, "MiddleSnakeData", String.Format("{0},{1}", smsrd.x, smsrd.y)); + startPos = endPos; + } // if + } // while + } // Optimize - // The path is from LowerX to (x,y) and (x,y) to UpperX - Lcs(dataA, lowerA, smsrd.X, dataB, lowerB, smsrd.Y, downVector, upVector); - Lcs(dataA, smsrd.X, upperA, dataB, smsrd.Y, upperB, downVector, upVector); // 2002.09.20: no need for 2 points - } - } // LCS() + /// + /// This function converts all text lines of the text into unique numbers for every unique text line + /// so further work can work only with simple numbers. + /// + /// the input text + /// This extern initialized Hashtable is used for storing all ever used text lines. + /// ignore leading and trailing space characters + /// + /// + /// a array of integers. + private static int[] DiffCodes(string aText, IDictionary h, bool trimSpace, bool ignoreSpace, bool ignoreCase) + { + // get all codes of the text + var lastUsedCode = h.Count; + // strip off all cr, only use lf as text line separator. + aText = aText.Replace("\r", string.Empty); + var lines = aText.Split(Constants.CharArrays.LineFeed); - /// Scan the tables of which lines are inserted and deleted, - /// producing an edit script in forward order. - /// - /// dynamic array - private static Item[] CreateDiffs(DiffData dataA, DiffData dataB) + var codes = new int[lines.Length]; + + for (var i = 0; i < lines.Length; ++i) { - ArrayList a = new ArrayList(); - Item aItem; - Item[] result; - - int lineA = 0; - int lineB = 0; - while (lineA < dataA.Length || lineB < dataB.Length) + var s = lines[i]; + if (trimSpace) { - if ((lineA < dataA.Length) && (!dataA.Modified[lineA]) - && (lineB < dataB.Length) && (!dataB.Modified[lineB])) - { - // equal lines - lineA++; - lineB++; + s = s.Trim(); + } + if (ignoreSpace) + { + s = Regex.Replace(s, "\\s+", " "); // TODO: optimization: faster blank removal. + } + + if (ignoreCase) + { + s = s.ToLower(); + } + + var aCode = h[s]; + if (aCode == null) + { + lastUsedCode++; + h[s] = lastUsedCode; + codes[i] = lastUsedCode; + } + else + { + codes[i] = (int)aCode; + } // if + } // for + + return codes; + } // DiffCodes + + /// + /// This is the algorithm to find the Shortest Middle Snake (SMS). + /// + /// sequence A + /// lower bound of the actual range in DataA + /// upper bound of the actual range in DataA (exclusive) + /// sequence B + /// lower bound of the actual range in DataB + /// upper bound of the actual range in DataB (exclusive) + /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. + /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. + /// a MiddleSnakeData record containing x,y and u,v + private static Smsrd Sms(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) + { + var max = dataA.Length + dataB.Length + 1; + + var downK = lowerA - lowerB; // the k-line to start the forward search + var upK = upperA - upperB; // the k-line to start the reverse search + + var delta = upperA - lowerA - (upperB - lowerB); + var oddDelta = (delta & 1) != 0; + + // The vectors in the publication accepts negative indexes. the vectors implemented here are 0-based + // and are access using a specific offset: UpOffset UpVector and DownOffset for DownVektor + var downOffset = max - downK; + var upOffset = max - upK; + + var maxD = ((upperA - lowerA + upperB - lowerB) / 2) + 1; + + // Debug.Write(2, "SMS", String.Format("Search the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); + + // init vectors + downVector[downOffset + downK + 1] = lowerA; + upVector[upOffset + upK - 1] = upperA; + + for (var d = 0; d <= maxD; d++) + { + // Extend the forward path. + Smsrd ret; + for (var k = downK - d; k <= downK + d; k += 2) + { + // Debug.Write(0, "SMS", "extend forward path " + k.ToString()); + + // find the only or better starting point + int x, y; + if (k == downK - d) + { + x = downVector[downOffset + k + 1]; // down } else { - // maybe deleted and/or inserted lines - int startA = lineA; - int startB = lineB; - - while (lineA < dataA.Length && (lineB >= dataB.Length || dataA.Modified[lineA])) - // while (LineA < DataA.Length && DataA.modified[LineA]) - lineA++; - - while (lineB < dataB.Length && (lineA >= dataA.Length || dataB.Modified[lineB])) - // while (LineB < DataB.Length && DataB.modified[LineB]) - lineB++; - - if ((startA < lineA) || (startB < lineB)) + x = downVector[downOffset + k - 1] + 1; // a step to the right + if (k < downK + d && downVector[downOffset + k + 1] >= x) { - // store a new difference-item - aItem = new Item(); - aItem.StartA = startA; - aItem.StartB = startB; - aItem.DeletedA = lineA - startA; - aItem.InsertedB = lineB - startB; - a.Add(aItem); + x = downVector[downOffset + k + 1]; // down + } + } + + y = x - k; + + // find the end of the furthest reaching forward D-path in diagonal k. + while (x < upperA && y < upperB && dataA.Data[x] == dataB.Data[y]) + { + x++; + y++; + } + + downVector[downOffset + k] = x; + + // overlap ? + if (oddDelta && upK - d < k && k < upK + d) + { + if (upVector[upOffset + k] <= downVector[downOffset + k]) + { + ret.X = downVector[downOffset + k]; + ret.Y = downVector[downOffset + k] - k; + + // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points + // ret.v = UpVector[UpOffset + k] - k; + return ret; } // if } // if - } // while + } // for k - result = new Item[a.Count]; - a.CopyTo(result); + // Extend the reverse path. + for (var k = upK - d; k <= upK + d; k += 2) + { + // Debug.Write(0, "SMS", "extend reverse path " + k.ToString()); - return (result); + // find the only or better starting point + int x, y; + if (k == upK + d) + { + x = upVector[upOffset + k - 1]; // up + } + else + { + x = upVector[upOffset + k + 1] - 1; // left + if (k > upK - d && upVector[upOffset + k - 1] < x) + { + x = upVector[upOffset + k - 1]; // up + } + } // if + + y = x - k; + + while (x > lowerA && y > lowerB && dataA.Data[x - 1] == dataB.Data[y - 1]) + { + x--; + y--; // diagonal + } + + upVector[upOffset + k] = x; + + // overlap ? + if (!oddDelta && downK - d <= k && k <= downK + d) + { + if (upVector[upOffset + k] <= downVector[downOffset + k]) + { + ret.X = downVector[downOffset + k]; + ret.Y = downVector[downOffset + k] - k; + + // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points + // ret.v = UpVector[UpOffset + k] - k; + return ret; + } // if + } // if + } // for k + } // for D + + throw new ApplicationException("the algorithm should never come here."); + } // SMS + + /// + /// This is the divide-and-conquer implementation of the longest common-subsequence (LCS) + /// algorithm. + /// The published algorithm passes recursively parts of the A and B sequences. + /// To avoid copying these arrays the lower and upper bounds are passed while the sequences stay constant. + /// + /// sequence A + /// lower bound of the actual range in DataA + /// upper bound of the actual range in DataA (exclusive) + /// sequence B + /// lower bound of the actual range in DataB + /// upper bound of the actual range in DataB (exclusive) + /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. + /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. + private static void Lcs(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) + { + // Debug.Write(2, "LCS", String.Format("Analyze the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); + + // Fast walk through equal lines at the start + while (lowerA < upperA && lowerB < upperB && dataA.Data[lowerA] == dataB.Data[lowerB]) + { + lowerA++; + lowerB++; } - } // class Diff + // Fast walk through equal lines at the end + while (lowerA < upperA && lowerB < upperB && dataA.Data[upperA - 1] == dataB.Data[upperB - 1]) + { + --upperA; + --upperB; + } + if (lowerA == upperA) + { + // mark as inserted lines. + while (lowerB < upperB) + { + dataB.Modified[lowerB++] = true; + } + } + else if (lowerB == upperB) + { + // mark as deleted lines. + while (lowerA < upperA) + { + dataA.Modified[lowerA++] = true; + } + } + else + { + // Find the middle snake and length of an optimal path for A and B + Smsrd smsrd = Sms(dataA, lowerA, upperA, dataB, lowerB, upperB, downVector, upVector); -} + // Debug.Write(2, "MiddleSnakeData", String.Format("{0},{1}", smsrd.x, smsrd.y)); + + // The path is from LowerX to (x,y) and (x,y) to UpperX + Lcs(dataA, lowerA, smsrd.X, dataB, lowerB, smsrd.Y, downVector, upVector); + Lcs(dataA, smsrd.X, upperA, dataB, smsrd.Y, upperB, downVector, upVector); // 2002.09.20: no need for 2 points + } + } // LCS() + + /// + /// Scan the tables of which lines are inserted and deleted, + /// producing an edit script in forward order. + /// + /// dynamic array + private static Item[] CreateDiffs(DiffData dataA, DiffData dataB) + { + var a = new ArrayList(); + Item aItem; + Item[] result; + + var lineA = 0; + var lineB = 0; + while (lineA < dataA.Length || lineB < dataB.Length) + { + if (lineA < dataA.Length && !dataA.Modified[lineA] + && lineB < dataB.Length && !dataB.Modified[lineB]) + { + // equal lines + lineA++; + lineB++; + } + else + { + // maybe deleted and/or inserted lines + var startA = lineA; + var startB = lineB; + + while (lineA < dataA.Length && (lineB >= dataB.Length || dataA.Modified[lineA])) + + // while (LineA < DataA.Length && DataA.modified[LineA]) + { + lineA++; + } + + while (lineB < dataB.Length && (lineA >= dataA.Length || dataB.Modified[lineB])) + + // while (LineB < DataB.Length && DataB.modified[LineB]) + { + lineB++; + } + + if (startA < lineA || startB < lineB) + { + // store a new difference-item + aItem = new Item + { + StartA = startA, + StartB = startB, + DeletedA = lineA - startA, + InsertedB = lineB - startB, + }; + a.Add(aItem); + } // if + } // if + } // while + + result = new Item[a.Count]; + a.CopyTo(result); + + return result; + } + + /// details of one difference. + public struct Item + { + /// Start Line number in Data A. + public int StartA; + + /// Start Line number in Data B. + public int StartB; + + /// Number of changes in Data A. + public int DeletedA; + + /// Number of changes in Data B. + public int InsertedB; + } // Item + + /// + /// Data on one input file being compared. + /// + internal class DiffData + { + /// Buffer of numbers that will be compared. + internal int[] Data; + + /// Number of elements (lines). + internal int Length; + + /// + /// Array of booleans that flag for modified data. + /// This is the result of the diff. + /// This means deletedA in the first Data or inserted in the second Data. + /// + internal bool[] Modified; + + /// + /// Initialize the Diff-Data buffer. + /// + /// reference to the buffer + internal DiffData(int[] initData) + { + Data = initData; + Length = initData.Length; + Modified = new bool[Length + 2]; + } // DiffData + } // class DiffData + + /// + /// Shortest Middle Snake Return Data + /// + private struct Smsrd + { + internal int X; + internal int Y; + + // internal int u, v; // 2002.09.20: no need for 2 points + } +} // class Diff diff --git a/src/Umbraco.Core/Strings/HtmlEncodedString.cs b/src/Umbraco.Core/Strings/HtmlEncodedString.cs index 16941cef48..4477d5436c 100644 --- a/src/Umbraco.Core/Strings/HtmlEncodedString.cs +++ b/src/Umbraco.Core/Strings/HtmlEncodedString.cs @@ -1,33 +1,21 @@ -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Represents an HTML-encoded string that should not be encoded again. +/// +public class HtmlEncodedString : IHtmlEncodedString { - /// - /// Represents an HTML-encoded string that should not be encoded again. - /// - public class HtmlEncodedString : IHtmlEncodedString - { + private readonly string _htmlString; - private string _htmlString; + /// Initializes a new instance of the class. + /// An HTML-encoded string that should not be encoded again. + public HtmlEncodedString(string value) => _htmlString = value; - /// Initializes a new instance of the class. - /// An HTML-encoded string that should not be encoded again. - public HtmlEncodedString(string value) - { - this._htmlString = value; - } + /// Returns an HTML-encoded string. + /// An HTML-encoded string. + public string ToHtmlString() => _htmlString; - /// Returns an HTML-encoded string. - /// An HTML-encoded string. - public string ToHtmlString() - { - return this._htmlString; - } - - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - { - return this._htmlString; - } - - } + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => _htmlString; } diff --git a/src/Umbraco.Core/Strings/IHtmlEncodedString.cs b/src/Umbraco.Core/Strings/IHtmlEncodedString.cs index b7c0c27d2d..bf94f834ad 100644 --- a/src/Umbraco.Core/Strings/IHtmlEncodedString.cs +++ b/src/Umbraco.Core/Strings/IHtmlEncodedString.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Represents an HTML-encoded string that should not be encoded again. +/// +public interface IHtmlEncodedString { /// - /// Represents an HTML-encoded string that should not be encoded again. + /// Returns an HTML-encoded string. /// - public interface IHtmlEncodedString - { - /// - /// Returns an HTML-encoded string. - /// - /// An HTML-encoded string. - string? ToHtmlString(); - } + /// An HTML-encoded string. + string? ToHtmlString(); } diff --git a/src/Umbraco.Core/Strings/IShortStringHelper.cs b/src/Umbraco.Core/Strings/IShortStringHelper.cs index a436758d9a..a5c20f1a09 100644 --- a/src/Umbraco.Core/Strings/IShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/IShortStringHelper.cs @@ -1,114 +1,129 @@ -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Provides string functions for short strings such as aliases or URL segments. +/// +/// Not necessarily optimized to work on large bodies of text. +public interface IShortStringHelper { /// - /// Provides string functions for short strings such as aliases or URL segments. + /// Cleans a string to produce a string that can safely be used in an alias. /// - /// Not necessarily optimized to work on large bodies of text. - public interface IShortStringHelper - { - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The safe alias. - /// - /// The string will be cleaned in the context of the IShortStringHelper default culture. - /// A safe alias is [a-z][a-zA-Z0-9_]* although legacy will also accept '-', and '_' at the beginning. - /// - string CleanStringForSafeAlias(string text); + /// The text to filter. + /// The safe alias. + /// + /// The string will be cleaned in the context of the IShortStringHelper default culture. + /// A safe alias is [a-z][a-zA-Z0-9_]* although legacy will also accept '-', and '_' at the beginning. + /// + string CleanStringForSafeAlias(string text); - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The culture. - /// The safe alias. - string CleanStringForSafeAlias(string text, string culture); + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// The culture. + /// The safe alias. + string CleanStringForSafeAlias(string text, string culture); - /// - /// Cleans a string to produce a string that can safely be used in an URL segment. - /// - /// The text to filter. - /// The safe URL segment. - /// The string will be cleaned in the context of the IShortStringHelper default culture. - string CleanStringForUrlSegment(string text); + /// + /// Cleans a string to produce a string that can safely be used in an URL segment. + /// + /// The text to filter. + /// The safe URL segment. + /// The string will be cleaned in the context of the IShortStringHelper default culture. + string CleanStringForUrlSegment(string text); - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an URL segment. - /// - /// The text to filter. - /// The culture. - /// The safe URL segment. - string CleanStringForUrlSegment(string text, string? culture); + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an URL + /// segment. + /// + /// The text to filter. + /// The culture. + /// The safe URL segment. + string CleanStringForUrlSegment(string text, string? culture); - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a URL). - /// - /// The text to filter. - /// The safe filename. - /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but that issue is not documented. - string CleanStringForSafeFileName(string text); + /// + /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a + /// filename, + /// both internally (on disk) and externally (as a URL). + /// + /// The text to filter. + /// The safe filename. + /// + /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but + /// that issue is not documented. + /// + string CleanStringForSafeFileName(string text); - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a URL). - /// - /// The text to filter. - /// The culture. - /// The safe filename. - /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but that issue is not documented. - string CleanStringForSafeFileName(string text, string culture); + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used as a filename, + /// both internally (on disk) and externally (as a URL). + /// + /// The text to filter. + /// The culture. + /// The safe filename. + /// + /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but + /// that issue is not documented. + /// + string CleanStringForSafeFileName(string text, string culture); - /// - /// Splits a pascal-cased string by inserting a separator in between each term. - /// - /// The text to split. - /// The separator. - /// The split string. - /// Supports Utf8 and Ascii strings, not Unicode strings. - string SplitPascalCasing(string text, char separator); + /// + /// Splits a pascal-cased string by inserting a separator in between each term. + /// + /// The text to split. + /// The separator. + /// The split string. + /// Supports Utf8 and Ascii strings, not Unicode strings. + string SplitPascalCasing(string text, char separator); - /// - /// Cleans a string. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The clean string. - /// The string is cleaned in the context of the IShortStringHelper default culture. - string CleanString(string text, CleanStringType stringType); + /// + /// Cleans a string. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The clean string. + /// The string is cleaned in the context of the IShortStringHelper default culture. + string CleanString(string text, CleanStringType stringType); - /// - /// Cleans a string, using a specified separator. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The clean string. - /// The string is cleaned in the context of the IShortStringHelper default culture. - string CleanString(string text, CleanStringType stringType, char separator); + /// + /// Cleans a string, using a specified separator. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The clean string. + /// The string is cleaned in the context of the IShortStringHelper default culture. + string CleanString(string text, CleanStringType stringType, char separator); - /// - /// Cleans a string in the context of a specified culture. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The culture. - /// The clean string. - string CleanString(string text, CleanStringType stringType, string culture); + /// + /// Cleans a string in the context of a specified culture. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The culture. + /// The clean string. + string CleanString(string text, CleanStringType stringType, string culture); - /// - /// Cleans a string in the context of a specified culture, using a specified separator. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The culture. - /// The clean string. - string CleanString(string text, CleanStringType stringType, char separator, string culture); - } + /// + /// Cleans a string in the context of a specified culture, using a specified separator. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The culture. + /// The clean string. + string CleanString(string text, CleanStringType stringType, char separator, string culture); } diff --git a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs index 74d147173f..c7050050e1 100644 --- a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs @@ -1,26 +1,27 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Provides URL segments for content. +/// +/// Url segments should comply with IETF RFCs regarding content, encoding, etc. +public interface IUrlSegmentProvider { /// - /// Provides URL segments for content. + /// Gets the URL segment for a specified content and culture. /// - /// Url segments should comply with IETF RFCs regarding content, encoding, etc. - public interface IUrlSegmentProvider - { - /// - /// Gets the URL segment for a specified content and culture. - /// - /// The content. - /// The culture. - /// The URL segment. - /// This is for when Umbraco is capable of managing more than one URL - /// per content, in 1-to-1 multilingual configurations. Then there would be one - /// URL per culture. - string? GetUrlSegment(IContentBase content, string? culture = null); + /// The content. + /// The culture. + /// The URL segment. + /// + /// This is for when Umbraco is capable of managing more than one URL + /// per content, in 1-to-1 multilingual configurations. Then there would be one + /// URL per culture. + /// + string? GetUrlSegment(IContentBase content, string? culture = null); - // TODO: For the 301 tracking, we need to add another extended interface to this so that - // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. - // Currently the way it works is very hacky, see notes in: RedirectTrackingEventHandler.ContentService_Publishing - } + // TODO: For the 301 tracking, we need to add another extended interface to this so that + // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. + // Currently the way it works is very hacky, see notes in: RedirectTrackingEventHandler.ContentService_Publishing } diff --git a/src/Umbraco.Core/Strings/PathUtility.cs b/src/Umbraco.Core/Strings/PathUtility.cs index bc88fa8bca..cab7127a6e 100644 --- a/src/Umbraco.Core/Strings/PathUtility.cs +++ b/src/Umbraco.Core/Strings/PathUtility.cs @@ -1,22 +1,29 @@ -namespace Umbraco.Cms.Core.Strings -{ - public static class PathUtility - { +namespace Umbraco.Cms.Core.Strings; - /// - /// Ensures that a path has `~/` as prefix - /// - /// - /// - public static string EnsurePathIsApplicationRootPrefixed(string path) +public static class PathUtility +{ + /// + /// Ensures that a path has `~/` as prefix + /// + /// + /// + public static string EnsurePathIsApplicationRootPrefixed(string path) + { + if (path.StartsWith("~/")) { - if (path.StartsWith("~/")) - return path; - if (path.StartsWith("/") == false && path.StartsWith("\\") == false) - path = string.Format("/{0}", path); - if (path.StartsWith("~") == false) - path = string.Format("~{0}", path); return path; } + + if (path.StartsWith("/") == false && path.StartsWith("\\") == false) + { + path = string.Format("/{0}", path); + } + + if (path.StartsWith("~") == false) + { + path = string.Format("~{0}", path); + } + + return path; } } diff --git a/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs b/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs index 551efc475a..39b826dae9 100644 --- a/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs +++ b/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +public class UrlSegmentProviderCollection : BuilderCollectionBase { - public class UrlSegmentProviderCollection : BuilderCollectionBase + public UrlSegmentProviderCollection(Func> items) + : base(items) { - public UrlSegmentProviderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs b/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs index 60504734f6..f9aa13b335 100644 --- a/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs +++ b/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +public class UrlSegmentProviderCollectionBuilder : OrderedCollectionBuilderBase { - public class UrlSegmentProviderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override UrlSegmentProviderCollectionBuilder This => this; - } + protected override UrlSegmentProviderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs b/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs index 3f492a7b87..4221273150 100644 --- a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs +++ b/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs @@ -1,3627 +1,3624 @@ -using System; +namespace Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Strings +/// +/// Provides methods to convert Utf8 text to Ascii. +/// +/// +/// Tries to match characters such as accented eg "é" to Ascii equivalent eg "e". +/// Converts all "whitespace" characters to a single whitespace. +/// Removes all non-Utf8 (unicode) characters, so in fact it can sort-of "convert" Unicode to Ascii. +/// Replaces symbols with '?'. +/// +public static class Utf8ToAsciiConverter { /// - /// Provides methods to convert Utf8 text to Ascii. + /// Converts an Utf8 string into an Ascii string. /// - /// - /// Tries to match characters such as accented eg "é" to Ascii equivalent eg "e". - /// Converts all "whitespace" characters to a single whitespace. - /// Removes all non-Utf8 (unicode) characters, so in fact it can sort-of "convert" Unicode to Ascii. - /// Replaces symbols with '?'. - /// - public static class Utf8ToAsciiConverter + /// The text to convert. + /// The character to use to replace characters that cannot properly be converted. + /// The converted text. + public static string ToAsciiString(string text, char fail = '?') { - /// - /// Converts an Utf8 string into an Ascii string. - /// - /// The text to convert. - /// The character to use to replace characters that cannot properly be converted. - /// The converted text. - public static string ToAsciiString(string text, char fail = '?') + var input = text.ToCharArray(); + + // this is faster although it uses more memory + // but... we should be filtering short strings only... + var output = new char[input.Length * 3]; // *3 because of things such as OE + var len = ToAscii(input, output, fail); + return new string(output, 0, len); + + // var output = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra + // ToAscii(input, output); + // return output.ToString(); + } + + /// + /// Converts an Utf8 string into an array of Ascii characters. + /// + /// The text to convert. + /// The character to use to replace characters that cannot properly be converted. + /// The converted text. + public static char[] ToAsciiCharArray(string text, char fail = '?') + { + var input = text.ToCharArray(); + + // this is faster although it uses more memory + // but... we should be filtering short strings only... + var output = new char[input.Length * 3]; // *3 because of things such as OE + var len = ToAscii(input, output, fail); + var array = new char[len]; + Array.Copy(output, array, len); + return array; + + // var temp = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra + // ToAscii(input, temp); + // var output = new char[temp.Length]; + // temp.CopyTo(0, output, 0, temp.Length); + // return output; + } + + /// + /// Converts an array of Utf8 characters into an array of Ascii characters. + /// + /// The input array. + /// The output array. + /// The character to use to replace characters that cannot properly be converted. + /// The number of characters in the output array. + /// The caller must ensure that the output array is big enough. + /// The output array is not big enough. + private static int ToAscii(char[] input, char[] output, char fail = '?') + { + var opos = 0; + + for (var ipos = 0; ipos < input.Length; ipos++) { - var input = text.ToCharArray(); - - // this is faster although it uses more memory - // but... we should be filtering short strings only... - - var output = new char[input.Length * 3]; // *3 because of things such as OE - var len = ToAscii(input, output, fail); - return new string(output, 0, len); - - //var output = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra - //ToAscii(input, output); - //return output.ToString(); - } - - /// - /// Converts an Utf8 string into an array of Ascii characters. - /// - /// The text to convert. - /// The character to use to replace characters that cannot properly be converted. - /// The converted text. - public static char[] ToAsciiCharArray(string text, char fail = '?') - { - var input = text.ToCharArray(); - - // this is faster although it uses more memory - // but... we should be filtering short strings only... - - var output = new char[input.Length * 3]; // *3 because of things such as OE - var len = ToAscii(input, output, fail); - var array = new char[len]; - Array.Copy(output, array, len); - return array; - - //var temp = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra - //ToAscii(input, temp); - //var output = new char[temp.Length]; - //temp.CopyTo(0, output, 0, temp.Length); - //return output; - } - - /// - /// Converts an array of Utf8 characters into an array of Ascii characters. - /// - /// The input array. - /// The output array. - /// The character to use to replace characters that cannot properly be converted. - /// The number of characters in the output array. - /// The caller must ensure that the output array is big enough. - /// The output array is not big enough. - private static int ToAscii(char[] input, char[] output, char fail = '?') - { - var opos = 0; - - for (var ipos = 0; ipos < input.Length; ipos++) - if (char.IsSurrogate(input[ipos])) // ignore high surrogate - { - ipos++; // and skip low surrogate - output[opos++] = fail; - } - else - ToAscii(input, ipos, output, ref opos, fail); - - return opos; - } - - //private static void ToAscii(char[] input, StringBuilder output) - //{ - // var chars = new char[5]; - - // for (var ipos = 0; ipos < input.Length; ipos++) - // { - // var opos = 0; - // if (char.IsSurrogate(input[ipos])) - // ipos++; - // else - // { - // ToAscii(input, ipos, chars, ref opos); - // output.Append(chars, 0, opos); - // } - // } - //} - - /// - /// Converts the character at position in input array of Utf8 characters - /// and writes the converted value to output array of Ascii characters at position , - /// and increments that position accordingly. - /// - /// The input array. - /// The input position. - /// The output array. - /// The output position. - /// The character to use to replace characters that cannot properly be converted. - /// - /// Adapted from various sources on the 'net including Lucene.Net.Analysis.ASCIIFoldingFilter. - /// Input should contain Utf8 characters exclusively and NOT Unicode. - /// Removes controls, normalizes whitespaces, replaces symbols by '?'. - /// - private static void ToAscii(char[] input, int ipos, char[] output, ref int opos, char fail = '?') - { - var c = input[ipos]; - - if (char.IsControl(c)) + // ignore high surrogate + if (char.IsSurrogate(input[ipos])) { - // Control characters are non-printing and formatting characters, such as ACK, BEL, CR, FF, LF, and VT. - // The Unicode standard assigns the following code points to control characters: from \U0000 to \U001F, - // \U007F, and from \U0080 to \U009F. According to the Unicode standard, these values are to be - // interpreted as control characters unless their use is otherwise defined by an application. Valid - // control characters are members of the UnicodeCategory.Control category. - - // we don't want them - } - //else if (char.IsSeparator(c)) - //{ - // // The Unicode standard recognizes three subcategories of separators: - // // - Space separators (the UnicodeCategory.SpaceSeparator category), which includes characters such as \u0020. - // // - Line separators (the UnicodeCategory.LineSeparator category), which includes \u2028. - // // - Paragraph separators (the UnicodeCategory.ParagraphSeparator category), which includes \u2029. - // // - // // Note: The Unicode standard classifies the characters \u000A (LF), \u000C (FF), and \u000A (CR) as control - // // characters (members of the UnicodeCategory.Control category), not as separator characters. - - // // better do it via WhiteSpace - //} - else if (char.IsWhiteSpace(c)) - { - // White space characters are the following Unicode characters: - // - Members of the SpaceSeparator category, which includes the characters SPACE (U+0020), - // OGHAM SPACE MARK (U+1680), MONGOLIAN VOWEL SEPARATOR (U+180E), EN QUAD (U+2000), EM QUAD (U+2001), - // EN SPACE (U+2002), EM SPACE (U+2003), THREE-PER-EM SPACE (U+2004), FOUR-PER-EM SPACE (U+2005), - // SIX-PER-EM SPACE (U+2006), FIGURE SPACE (U+2007), PUNCTUATION SPACE (U+2008), THIN SPACE (U+2009), - // HAIR SPACE (U+200A), NARROW NO-BREAK SPACE (U+202F), MEDIUM MATHEMATICAL SPACE (U+205F), - // and IDEOGRAPHIC SPACE (U+3000). - // - Members of the LineSeparator category, which consists solely of the LINE SEPARATOR character (U+2028). - // - Members of the ParagraphSeparator category, which consists solely of the PARAGRAPH SEPARATOR character (U+2029). - // - The characters CHARACTER TABULATION (U+0009), LINE FEED (U+000A), LINE TABULATION (U+000B), - // FORM FEED (U+000C), CARRIAGE RETURN (U+000D), NEXT LINE (U+0085), and NO-BREAK SPACE (U+00A0). - - // make it a whitespace - output[opos++] = ' '; - } - else if (c < '\u0080') - { - // safe - output[opos++] = c; + ipos++; // and skip low surrogate + output[opos++] = fail; } else { - switch (c) - { - - case '\u00C0': - // À [LATIN CAPITAL LETTER A WITH GRAVE] - case '\u00C1': - // � [LATIN CAPITAL LETTER A WITH ACUTE] - case '\u00C2': - //  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] - case '\u00C3': - // à [LATIN CAPITAL LETTER A WITH TILDE] - case '\u00C4': - // Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] - case '\u00C5': - // Ã… [LATIN CAPITAL LETTER A WITH RING ABOVE] - case '\u0100': - // Ä€ [LATIN CAPITAL LETTER A WITH MACRON] - case '\u0102': - // Ä‚ [LATIN CAPITAL LETTER A WITH BREVE] - case '\u0104': - // Ä„ [LATIN CAPITAL LETTER A WITH OGONEK] - case '\u018F': - // � http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] - case '\u01CD': - // � [LATIN CAPITAL LETTER A WITH CARON] - case '\u01DE': - // Çž [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] - case '\u01E0': - // Ç  [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] - case '\u01FA': - // Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] - case '\u0200': - // È€ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] - case '\u0202': - // È‚ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] - case '\u0226': - // Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] - case '\u023A': - // Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] - case '\u1D00': - // á´€ [LATIN LETTER SMALL CAPITAL A] - case '\u1E00': - // Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] - case '\u1EA0': - // Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] - case '\u1EA2': - // Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] - case '\u1EA4': - // Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] - case '\u1EA6': - // Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] - case '\u1EA8': - // Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EAA': - // Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] - case '\u1EAC': - // Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EAE': - // Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] - case '\u1EB0': - // Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] - case '\u1EB2': - // Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] - case '\u1EB4': - // Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] - case '\u1EB6': - // Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] - case '\u24B6': - // â’¶ [CIRCLED LATIN CAPITAL LETTER A] - case '\uFF21': // A [FULLWIDTH LATIN CAPITAL LETTER A] - output[opos++] = 'A'; - break; - - case '\u00E0': - // à [LATIN SMALL LETTER A WITH GRAVE] - case '\u00E1': - // á [LATIN SMALL LETTER A WITH ACUTE] - case '\u00E2': - // â [LATIN SMALL LETTER A WITH CIRCUMFLEX] - case '\u00E3': - // ã [LATIN SMALL LETTER A WITH TILDE] - case '\u00E4': - // ä [LATIN SMALL LETTER A WITH DIAERESIS] - case '\u00E5': - // Ã¥ [LATIN SMALL LETTER A WITH RING ABOVE] - case '\u0101': - // � [LATIN SMALL LETTER A WITH MACRON] - case '\u0103': - // ă [LATIN SMALL LETTER A WITH BREVE] - case '\u0105': - // Ä… [LATIN SMALL LETTER A WITH OGONEK] - case '\u01CE': - // ÇŽ [LATIN SMALL LETTER A WITH CARON] - case '\u01DF': - // ÇŸ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] - case '\u01E1': - // Ç¡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] - case '\u01FB': - // Ç» [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] - case '\u0201': - // � [LATIN SMALL LETTER A WITH DOUBLE GRAVE] - case '\u0203': - // ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] - case '\u0227': - // ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] - case '\u0250': - // � [LATIN SMALL LETTER TURNED A] - case '\u0259': - // É™ [LATIN SMALL LETTER SCHWA] - case '\u025A': - // Éš [LATIN SMALL LETTER SCHWA WITH HOOK] - case '\u1D8F': - // � [LATIN SMALL LETTER A WITH RETROFLEX HOOK] - case '\u1D95': - // á¶• [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] - case '\u1E01': - // ạ [LATIN SMALL LETTER A WITH RING BELOW] - case '\u1E9A': - // ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] - case '\u1EA1': - // ạ [LATIN SMALL LETTER A WITH DOT BELOW] - case '\u1EA3': - // ả [LATIN SMALL LETTER A WITH HOOK ABOVE] - case '\u1EA5': - // ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] - case '\u1EA7': - // ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] - case '\u1EA9': - // ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EAB': - // ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] - case '\u1EAD': - // ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EAF': - // ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] - case '\u1EB1': - // ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] - case '\u1EB3': - // ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] - case '\u1EB5': - // ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] - case '\u1EB7': - // ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] - case '\u2090': - // � [LATIN SUBSCRIPT SMALL LETTER A] - case '\u2094': - // �? [LATIN SUBSCRIPT SMALL LETTER SCHWA] - case '\u24D0': - // � [CIRCLED LATIN SMALL LETTER A] - case '\u2C65': - // â±¥ [LATIN SMALL LETTER A WITH STROKE] - case '\u2C6F': - // Ɐ [LATIN CAPITAL LETTER TURNED A] - case '\uFF41': // � [FULLWIDTH LATIN SMALL LETTER A] - output[opos++] = 'a'; - break; - - case '\uA732': // Ꜳ [LATIN CAPITAL LETTER AA] - output[opos++] = 'A'; - output[opos++] = 'A'; - break; - - case '\u00C6': - // Æ [LATIN CAPITAL LETTER AE] - case '\u01E2': - // Ç¢ [LATIN CAPITAL LETTER AE WITH MACRON] - case '\u01FC': - // Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] - case '\u1D01': // á´� [LATIN LETTER SMALL CAPITAL AE] - output[opos++] = 'A'; - output[opos++] = 'E'; - break; - - case '\uA734': // Ꜵ [LATIN CAPITAL LETTER AO] - output[opos++] = 'A'; - output[opos++] = 'O'; - break; - - case '\uA736': // Ꜷ [LATIN CAPITAL LETTER AU] - output[opos++] = 'A'; - output[opos++] = 'U'; - break; - - case '\uA738': - // Ꜹ [LATIN CAPITAL LETTER AV] - case '\uA73A': // Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] - output[opos++] = 'A'; - output[opos++] = 'V'; - break; - - case '\uA73C': // Ꜽ [LATIN CAPITAL LETTER AY] - output[opos++] = 'A'; - output[opos++] = 'Y'; - break; - - case '\u249C': // â’œ [PARENTHESIZED LATIN SMALL LETTER A] - output[opos++] = '('; - output[opos++] = 'a'; - output[opos++] = ')'; - break; - - case '\uA733': // ꜳ [LATIN SMALL LETTER AA] - output[opos++] = 'a'; - output[opos++] = 'a'; - break; - - case '\u00E6': - // æ [LATIN SMALL LETTER AE] - case '\u01E3': - // Ç£ [LATIN SMALL LETTER AE WITH MACRON] - case '\u01FD': - // ǽ [LATIN SMALL LETTER AE WITH ACUTE] - case '\u1D02': // á´‚ [LATIN SMALL LETTER TURNED AE] - output[opos++] = 'a'; - output[opos++] = 'e'; - break; - - case '\uA735': // ꜵ [LATIN SMALL LETTER AO] - output[opos++] = 'a'; - output[opos++] = 'o'; - break; - - case '\uA737': // ꜷ [LATIN SMALL LETTER AU] - output[opos++] = 'a'; - output[opos++] = 'u'; - break; - - case '\uA739': - // ꜹ [LATIN SMALL LETTER AV] - case '\uA73B': // ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] - output[opos++] = 'a'; - output[opos++] = 'v'; - break; - - case '\uA73D': // ꜽ [LATIN SMALL LETTER AY] - output[opos++] = 'a'; - output[opos++] = 'y'; - break; - - case '\u0181': - // � [LATIN CAPITAL LETTER B WITH HOOK] - case '\u0182': - // Æ‚ [LATIN CAPITAL LETTER B WITH TOPBAR] - case '\u0243': - // Ƀ [LATIN CAPITAL LETTER B WITH STROKE] - case '\u0299': - // Ê™ [LATIN LETTER SMALL CAPITAL B] - case '\u1D03': - // á´ƒ [LATIN LETTER SMALL CAPITAL BARRED B] - case '\u1E02': - // Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] - case '\u1E04': - // Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] - case '\u1E06': - // Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] - case '\u24B7': - // â’· [CIRCLED LATIN CAPITAL LETTER B] - case '\uFF22': // ï¼¢ [FULLWIDTH LATIN CAPITAL LETTER B] - output[opos++] = 'B'; - break; - - case '\u0180': - // Æ€ [LATIN SMALL LETTER B WITH STROKE] - case '\u0183': - // ƃ [LATIN SMALL LETTER B WITH TOPBAR] - case '\u0253': - // É“ [LATIN SMALL LETTER B WITH HOOK] - case '\u1D6C': - // ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] - case '\u1D80': - // á¶€ [LATIN SMALL LETTER B WITH PALATAL HOOK] - case '\u1E03': - // ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] - case '\u1E05': - // ḅ [LATIN SMALL LETTER B WITH DOT BELOW] - case '\u1E07': - // ḇ [LATIN SMALL LETTER B WITH LINE BELOW] - case '\u24D1': - // â“‘ [CIRCLED LATIN SMALL LETTER B] - case '\uFF42': // b [FULLWIDTH LATIN SMALL LETTER B] - output[opos++] = 'b'; - break; - - case '\u249D': // â’� [PARENTHESIZED LATIN SMALL LETTER B] - output[opos++] = '('; - output[opos++] = 'b'; - output[opos++] = ')'; - break; - - case '\u00C7': - // Ç [LATIN CAPITAL LETTER C WITH CEDILLA] - case '\u0106': - // Ć [LATIN CAPITAL LETTER C WITH ACUTE] - case '\u0108': - // Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] - case '\u010A': - // ÄŠ [LATIN CAPITAL LETTER C WITH DOT ABOVE] - case '\u010C': - // ÄŒ [LATIN CAPITAL LETTER C WITH CARON] - case '\u0187': - // Ƈ [LATIN CAPITAL LETTER C WITH HOOK] - case '\u023B': - // È» [LATIN CAPITAL LETTER C WITH STROKE] - case '\u0297': - // Ê— [LATIN LETTER STRETCHED C] - case '\u1D04': - // á´„ [LATIN LETTER SMALL CAPITAL C] - case '\u1E08': - // Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] - case '\u24B8': - // â’¸ [CIRCLED LATIN CAPITAL LETTER C] - case '\uFF23': // ï¼£ [FULLWIDTH LATIN CAPITAL LETTER C] - output[opos++] = 'C'; - break; - - case '\u00E7': - // ç [LATIN SMALL LETTER C WITH CEDILLA] - case '\u0107': - // ć [LATIN SMALL LETTER C WITH ACUTE] - case '\u0109': - // ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] - case '\u010B': - // Ä‹ [LATIN SMALL LETTER C WITH DOT ABOVE] - case '\u010D': - // � [LATIN SMALL LETTER C WITH CARON] - case '\u0188': - // ƈ [LATIN SMALL LETTER C WITH HOOK] - case '\u023C': - // ȼ [LATIN SMALL LETTER C WITH STROKE] - case '\u0255': - // É• [LATIN SMALL LETTER C WITH CURL] - case '\u1E09': - // ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] - case '\u2184': - // ↄ [LATIN SMALL LETTER REVERSED C] - case '\u24D2': - // â“’ [CIRCLED LATIN SMALL LETTER C] - case '\uA73E': - // Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] - case '\uA73F': - // ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] - case '\uFF43': // c [FULLWIDTH LATIN SMALL LETTER C] - output[opos++] = 'c'; - break; - - case '\u249E': // â’ž [PARENTHESIZED LATIN SMALL LETTER C] - output[opos++] = '('; - output[opos++] = 'c'; - output[opos++] = ')'; - break; - - case '\u00D0': - // � [LATIN CAPITAL LETTER ETH] - case '\u010E': - // ÄŽ [LATIN CAPITAL LETTER D WITH CARON] - case '\u0110': - // � [LATIN CAPITAL LETTER D WITH STROKE] - case '\u0189': - // Ɖ [LATIN CAPITAL LETTER AFRICAN D] - case '\u018A': - // ÆŠ [LATIN CAPITAL LETTER D WITH HOOK] - case '\u018B': - // Æ‹ [LATIN CAPITAL LETTER D WITH TOPBAR] - case '\u1D05': - // á´… [LATIN LETTER SMALL CAPITAL D] - case '\u1D06': - // á´† [LATIN LETTER SMALL CAPITAL ETH] - case '\u1E0A': - // Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] - case '\u1E0C': - // Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] - case '\u1E0E': - // Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] - case '\u1E10': - // � [LATIN CAPITAL LETTER D WITH CEDILLA] - case '\u1E12': - // Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] - case '\u24B9': - // â’¹ [CIRCLED LATIN CAPITAL LETTER D] - case '\uA779': - // � [LATIN CAPITAL LETTER INSULAR D] - case '\uFF24': // D [FULLWIDTH LATIN CAPITAL LETTER D] - output[opos++] = 'D'; - break; - - case '\u00F0': - // ð [LATIN SMALL LETTER ETH] - case '\u010F': - // � [LATIN SMALL LETTER D WITH CARON] - case '\u0111': - // Ä‘ [LATIN SMALL LETTER D WITH STROKE] - case '\u018C': - // ÆŒ [LATIN SMALL LETTER D WITH TOPBAR] - case '\u0221': - // È¡ [LATIN SMALL LETTER D WITH CURL] - case '\u0256': - // É– [LATIN SMALL LETTER D WITH TAIL] - case '\u0257': - // É— [LATIN SMALL LETTER D WITH HOOK] - case '\u1D6D': - // áµ­ [LATIN SMALL LETTER D WITH MIDDLE TILDE] - case '\u1D81': - // � [LATIN SMALL LETTER D WITH PALATAL HOOK] - case '\u1D91': - // á¶‘ [LATIN SMALL LETTER D WITH HOOK AND TAIL] - case '\u1E0B': - // ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] - case '\u1E0D': - // � [LATIN SMALL LETTER D WITH DOT BELOW] - case '\u1E0F': - // � [LATIN SMALL LETTER D WITH LINE BELOW] - case '\u1E11': - // ḑ [LATIN SMALL LETTER D WITH CEDILLA] - case '\u1E13': - // ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] - case '\u24D3': - // â““ [CIRCLED LATIN SMALL LETTER D] - case '\uA77A': - // � [LATIN SMALL LETTER INSULAR D] - case '\uFF44': // d [FULLWIDTH LATIN SMALL LETTER D] - output[opos++] = 'd'; - break; - - case '\u01C4': - // Ç„ [LATIN CAPITAL LETTER DZ WITH CARON] - case '\u01F1': // DZ [LATIN CAPITAL LETTER DZ] - output[opos++] = 'D'; - output[opos++] = 'Z'; - break; - - case '\u01C5': - // Ç… [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] - case '\u01F2': // Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] - output[opos++] = 'D'; - output[opos++] = 'z'; - break; - - case '\u249F': // â’Ÿ [PARENTHESIZED LATIN SMALL LETTER D] - output[opos++] = '('; - output[opos++] = 'd'; - output[opos++] = ')'; - break; - - case '\u0238': // ȸ [LATIN SMALL LETTER DB DIGRAPH] - output[opos++] = 'd'; - output[opos++] = 'b'; - break; - - case '\u01C6': - // dž [LATIN SMALL LETTER DZ WITH CARON] - case '\u01F3': - // dz [LATIN SMALL LETTER DZ] - case '\u02A3': - // Ê£ [LATIN SMALL LETTER DZ DIGRAPH] - case '\u02A5': // Ê¥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] - output[opos++] = 'd'; - output[opos++] = 'z'; - break; - - case '\u00C8': - // È [LATIN CAPITAL LETTER E WITH GRAVE] - case '\u00C9': - // É [LATIN CAPITAL LETTER E WITH ACUTE] - case '\u00CA': - // Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] - case '\u00CB': - // Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] - case '\u0112': - // Ä’ [LATIN CAPITAL LETTER E WITH MACRON] - case '\u0114': - // �? [LATIN CAPITAL LETTER E WITH BREVE] - case '\u0116': - // Ä– [LATIN CAPITAL LETTER E WITH DOT ABOVE] - case '\u0118': - // Ę [LATIN CAPITAL LETTER E WITH OGONEK] - case '\u011A': - // Äš [LATIN CAPITAL LETTER E WITH CARON] - case '\u018E': - // ÆŽ [LATIN CAPITAL LETTER REVERSED E] - case '\u0190': - // � [LATIN CAPITAL LETTER OPEN E] - case '\u0204': - // È„ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] - case '\u0206': - // Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] - case '\u0228': - // Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] - case '\u0246': - // Ɇ [LATIN CAPITAL LETTER E WITH STROKE] - case '\u1D07': - // á´‡ [LATIN LETTER SMALL CAPITAL E] - case '\u1E14': - // �? [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] - case '\u1E16': - // Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] - case '\u1E18': - // Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] - case '\u1E1A': - // Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] - case '\u1E1C': - // Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] - case '\u1EB8': - // Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] - case '\u1EBA': - // Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] - case '\u1EBC': - // Ẽ [LATIN CAPITAL LETTER E WITH TILDE] - case '\u1EBE': - // Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] - case '\u1EC0': - // Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] - case '\u1EC2': - // Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EC4': - // Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] - case '\u1EC6': - // Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] - case '\u24BA': - // â’º [CIRCLED LATIN CAPITAL LETTER E] - case '\u2C7B': - // â±» [LATIN LETTER SMALL CAPITAL TURNED E] - case '\uFF25': // ï¼¥ [FULLWIDTH LATIN CAPITAL LETTER E] - output[opos++] = 'E'; - break; - - case '\u00E8': - // è [LATIN SMALL LETTER E WITH GRAVE] - case '\u00E9': - // é [LATIN SMALL LETTER E WITH ACUTE] - case '\u00EA': - // ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] - case '\u00EB': - // ë [LATIN SMALL LETTER E WITH DIAERESIS] - case '\u0113': - // Ä“ [LATIN SMALL LETTER E WITH MACRON] - case '\u0115': - // Ä• [LATIN SMALL LETTER E WITH BREVE] - case '\u0117': - // Ä— [LATIN SMALL LETTER E WITH DOT ABOVE] - case '\u0119': - // Ä™ [LATIN SMALL LETTER E WITH OGONEK] - case '\u011B': - // Ä› [LATIN SMALL LETTER E WITH CARON] - case '\u01DD': - // � [LATIN SMALL LETTER TURNED E] - case '\u0205': - // È… [LATIN SMALL LETTER E WITH DOUBLE GRAVE] - case '\u0207': - // ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] - case '\u0229': - // È© [LATIN SMALL LETTER E WITH CEDILLA] - case '\u0247': - // ɇ [LATIN SMALL LETTER E WITH STROKE] - case '\u0258': - // ɘ [LATIN SMALL LETTER REVERSED E] - case '\u025B': - // É› [LATIN SMALL LETTER OPEN E] - case '\u025C': - // Éœ [LATIN SMALL LETTER REVERSED OPEN E] - case '\u025D': - // � [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] - case '\u025E': - // Éž [LATIN SMALL LETTER CLOSED REVERSED OPEN E] - case '\u029A': - // Êš [LATIN SMALL LETTER CLOSED OPEN E] - case '\u1D08': - // á´ˆ [LATIN SMALL LETTER TURNED OPEN E] - case '\u1D92': - // á¶’ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] - case '\u1D93': - // á¶“ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] - case '\u1D94': - // �? [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] - case '\u1E15': - // ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] - case '\u1E17': - // ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] - case '\u1E19': - // ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] - case '\u1E1B': - // ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] - case '\u1E1D': - // � [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] - case '\u1EB9': - // ẹ [LATIN SMALL LETTER E WITH DOT BELOW] - case '\u1EBB': - // ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] - case '\u1EBD': - // ẽ [LATIN SMALL LETTER E WITH TILDE] - case '\u1EBF': - // ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] - case '\u1EC1': - // � [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] - case '\u1EC3': - // ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EC5': - // á»… [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] - case '\u1EC7': - // ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] - case '\u2091': - // â‚‘ [LATIN SUBSCRIPT SMALL LETTER E] - case '\u24D4': - // �? [CIRCLED LATIN SMALL LETTER E] - case '\u2C78': - // ⱸ [LATIN SMALL LETTER E WITH NOTCH] - case '\uFF45': // ï½… [FULLWIDTH LATIN SMALL LETTER E] - output[opos++] = 'e'; - break; - - case '\u24A0': // â’  [PARENTHESIZED LATIN SMALL LETTER E] - output[opos++] = '('; - output[opos++] = 'e'; - output[opos++] = ')'; - break; - - case '\u0191': - // Æ‘ [LATIN CAPITAL LETTER F WITH HOOK] - case '\u1E1E': - // Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] - case '\u24BB': - // â’» [CIRCLED LATIN CAPITAL LETTER F] - case '\uA730': - // ꜰ [LATIN LETTER SMALL CAPITAL F] - case '\uA77B': - // � [LATIN CAPITAL LETTER INSULAR F] - case '\uA7FB': - // ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] - case '\uFF26': // F [FULLWIDTH LATIN CAPITAL LETTER F] - output[opos++] = 'F'; - break; - - case '\u0192': - // Æ’ [LATIN SMALL LETTER F WITH HOOK] - case '\u1D6E': - // áµ® [LATIN SMALL LETTER F WITH MIDDLE TILDE] - case '\u1D82': - // á¶‚ [LATIN SMALL LETTER F WITH PALATAL HOOK] - case '\u1E1F': - // ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] - case '\u1E9B': - // ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] - case '\u24D5': - // â“• [CIRCLED LATIN SMALL LETTER F] - case '\uA77C': - // � [LATIN SMALL LETTER INSULAR F] - case '\uFF46': // f [FULLWIDTH LATIN SMALL LETTER F] - output[opos++] = 'f'; - break; - - case '\u24A1': // â’¡ [PARENTHESIZED LATIN SMALL LETTER F] - output[opos++] = '('; - output[opos++] = 'f'; - output[opos++] = ')'; - break; - - case '\uFB00': // ff [LATIN SMALL LIGATURE FF] - output[opos++] = 'f'; - output[opos++] = 'f'; - break; - - case '\uFB03': // ffi [LATIN SMALL LIGATURE FFI] - output[opos++] = 'f'; - output[opos++] = 'f'; - output[opos++] = 'i'; - break; - - case '\uFB04': // ffl [LATIN SMALL LIGATURE FFL] - output[opos++] = 'f'; - output[opos++] = 'f'; - output[opos++] = 'l'; - break; - - case '\uFB01': // � [LATIN SMALL LIGATURE FI] - output[opos++] = 'f'; - output[opos++] = 'i'; - break; - - case '\uFB02': // fl [LATIN SMALL LIGATURE FL] - output[opos++] = 'f'; - output[opos++] = 'l'; - break; - - case '\u011C': - // Äœ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] - case '\u011E': - // Äž [LATIN CAPITAL LETTER G WITH BREVE] - case '\u0120': - // Ä  [LATIN CAPITAL LETTER G WITH DOT ABOVE] - case '\u0122': - // Ä¢ [LATIN CAPITAL LETTER G WITH CEDILLA] - case '\u0193': - // Æ“ [LATIN CAPITAL LETTER G WITH HOOK] - case '\u01E4': - // Ǥ [LATIN CAPITAL LETTER G WITH STROKE] - case '\u01E5': - // Ç¥ [LATIN SMALL LETTER G WITH STROKE] - case '\u01E6': - // Ǧ [LATIN CAPITAL LETTER G WITH CARON] - case '\u01E7': - // ǧ [LATIN SMALL LETTER G WITH CARON] - case '\u01F4': - // Ç´ [LATIN CAPITAL LETTER G WITH ACUTE] - case '\u0262': - // É¢ [LATIN LETTER SMALL CAPITAL G] - case '\u029B': - // Ê› [LATIN LETTER SMALL CAPITAL G WITH HOOK] - case '\u1E20': - // Ḡ [LATIN CAPITAL LETTER G WITH MACRON] - case '\u24BC': - // â’¼ [CIRCLED LATIN CAPITAL LETTER G] - case '\uA77D': - // � [LATIN CAPITAL LETTER INSULAR G] - case '\uA77E': - // � [LATIN CAPITAL LETTER TURNED INSULAR G] - case '\uFF27': // ï¼§ [FULLWIDTH LATIN CAPITAL LETTER G] - output[opos++] = 'G'; - break; - - case '\u011D': - // � [LATIN SMALL LETTER G WITH CIRCUMFLEX] - case '\u011F': - // ÄŸ [LATIN SMALL LETTER G WITH BREVE] - case '\u0121': - // Ä¡ [LATIN SMALL LETTER G WITH DOT ABOVE] - case '\u0123': - // Ä£ [LATIN SMALL LETTER G WITH CEDILLA] - case '\u01F5': - // ǵ [LATIN SMALL LETTER G WITH ACUTE] - case '\u0260': - // É  [LATIN SMALL LETTER G WITH HOOK] - case '\u0261': - // É¡ [LATIN SMALL LETTER SCRIPT G] - case '\u1D77': - // áµ· [LATIN SMALL LETTER TURNED G] - case '\u1D79': - // áµ¹ [LATIN SMALL LETTER INSULAR G] - case '\u1D83': - // ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] - case '\u1E21': - // ḡ [LATIN SMALL LETTER G WITH MACRON] - case '\u24D6': - // â“– [CIRCLED LATIN SMALL LETTER G] - case '\uA77F': - // � [LATIN SMALL LETTER TURNED INSULAR G] - case '\uFF47': // g [FULLWIDTH LATIN SMALL LETTER G] - output[opos++] = 'g'; - break; - - case '\u24A2': // â’¢ [PARENTHESIZED LATIN SMALL LETTER G] - output[opos++] = '('; - output[opos++] = 'g'; - output[opos++] = ')'; - break; - - case '\u0124': - // Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] - case '\u0126': - // Ħ [LATIN CAPITAL LETTER H WITH STROKE] - case '\u021E': - // Èž [LATIN CAPITAL LETTER H WITH CARON] - case '\u029C': - // Êœ [LATIN LETTER SMALL CAPITAL H] - case '\u1E22': - // Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] - case '\u1E24': - // Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] - case '\u1E26': - // Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] - case '\u1E28': - // Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] - case '\u1E2A': - // Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] - case '\u24BD': - // â’½ [CIRCLED LATIN CAPITAL LETTER H] - case '\u2C67': - // â±§ [LATIN CAPITAL LETTER H WITH DESCENDER] - case '\u2C75': - // â±µ [LATIN CAPITAL LETTER HALF H] - case '\uFF28': // H [FULLWIDTH LATIN CAPITAL LETTER H] - output[opos++] = 'H'; - break; - - case '\u0125': - // Ä¥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] - case '\u0127': - // ħ [LATIN SMALL LETTER H WITH STROKE] - case '\u021F': - // ÈŸ [LATIN SMALL LETTER H WITH CARON] - case '\u0265': - // É¥ [LATIN SMALL LETTER TURNED H] - case '\u0266': - // ɦ [LATIN SMALL LETTER H WITH HOOK] - case '\u02AE': - // Ê® [LATIN SMALL LETTER TURNED H WITH FISHHOOK] - case '\u02AF': - // ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] - case '\u1E23': - // ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] - case '\u1E25': - // ḥ [LATIN SMALL LETTER H WITH DOT BELOW] - case '\u1E27': - // ḧ [LATIN SMALL LETTER H WITH DIAERESIS] - case '\u1E29': - // ḩ [LATIN SMALL LETTER H WITH CEDILLA] - case '\u1E2B': - // ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] - case '\u1E96': - // ẖ [LATIN SMALL LETTER H WITH LINE BELOW] - case '\u24D7': - // â“— [CIRCLED LATIN SMALL LETTER H] - case '\u2C68': - // ⱨ [LATIN SMALL LETTER H WITH DESCENDER] - case '\u2C76': - // â±¶ [LATIN SMALL LETTER HALF H] - case '\uFF48': // h [FULLWIDTH LATIN SMALL LETTER H] - output[opos++] = 'h'; - break; - - case '\u01F6': // Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] - output[opos++] = 'H'; - output[opos++] = 'V'; - break; - - case '\u24A3': // â’£ [PARENTHESIZED LATIN SMALL LETTER H] - output[opos++] = '('; - output[opos++] = 'h'; - output[opos++] = ')'; - break; - - case '\u0195': // Æ• [LATIN SMALL LETTER HV] - output[opos++] = 'h'; - output[opos++] = 'v'; - break; - - case '\u00CC': - // ÃŒ [LATIN CAPITAL LETTER I WITH GRAVE] - case '\u00CD': - // � [LATIN CAPITAL LETTER I WITH ACUTE] - case '\u00CE': - // ÃŽ [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] - case '\u00CF': - // � [LATIN CAPITAL LETTER I WITH DIAERESIS] - case '\u0128': - // Ĩ [LATIN CAPITAL LETTER I WITH TILDE] - case '\u012A': - // Ī [LATIN CAPITAL LETTER I WITH MACRON] - case '\u012C': - // Ĭ [LATIN CAPITAL LETTER I WITH BREVE] - case '\u012E': - // Ä® [LATIN CAPITAL LETTER I WITH OGONEK] - case '\u0130': - // İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] - case '\u0196': - // Æ– [LATIN CAPITAL LETTER IOTA] - case '\u0197': - // Æ— [LATIN CAPITAL LETTER I WITH STROKE] - case '\u01CF': - // � [LATIN CAPITAL LETTER I WITH CARON] - case '\u0208': - // Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] - case '\u020A': - // ÈŠ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] - case '\u026A': - // ɪ [LATIN LETTER SMALL CAPITAL I] - case '\u1D7B': - // áµ» [LATIN SMALL CAPITAL LETTER I WITH STROKE] - case '\u1E2C': - // Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] - case '\u1E2E': - // Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] - case '\u1EC8': - // Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] - case '\u1ECA': - // Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] - case '\u24BE': - // â’¾ [CIRCLED LATIN CAPITAL LETTER I] - case '\uA7FE': - // ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] - case '\uFF29': // I [FULLWIDTH LATIN CAPITAL LETTER I] - output[opos++] = 'I'; - break; - - case '\u00EC': - // ì [LATIN SMALL LETTER I WITH GRAVE] - case '\u00ED': - // í [LATIN SMALL LETTER I WITH ACUTE] - case '\u00EE': - // î [LATIN SMALL LETTER I WITH CIRCUMFLEX] - case '\u00EF': - // ï [LATIN SMALL LETTER I WITH DIAERESIS] - case '\u0129': - // Ä© [LATIN SMALL LETTER I WITH TILDE] - case '\u012B': - // Ä« [LATIN SMALL LETTER I WITH MACRON] - case '\u012D': - // Ä­ [LATIN SMALL LETTER I WITH BREVE] - case '\u012F': - // į [LATIN SMALL LETTER I WITH OGONEK] - case '\u0131': - // ı [LATIN SMALL LETTER DOTLESS I] - case '\u01D0': - // � [LATIN SMALL LETTER I WITH CARON] - case '\u0209': - // ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] - case '\u020B': - // È‹ [LATIN SMALL LETTER I WITH INVERTED BREVE] - case '\u0268': - // ɨ [LATIN SMALL LETTER I WITH STROKE] - case '\u1D09': - // á´‰ [LATIN SMALL LETTER TURNED I] - case '\u1D62': - // áµ¢ [LATIN SUBSCRIPT SMALL LETTER I] - case '\u1D7C': - // áµ¼ [LATIN SMALL LETTER IOTA WITH STROKE] - case '\u1D96': - // á¶– [LATIN SMALL LETTER I WITH RETROFLEX HOOK] - case '\u1E2D': - // ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] - case '\u1E2F': - // ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] - case '\u1EC9': - // ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] - case '\u1ECB': - // ị [LATIN SMALL LETTER I WITH DOT BELOW] - case '\u2071': - // � [SUPERSCRIPT LATIN SMALL LETTER I] - case '\u24D8': - // ⓘ [CIRCLED LATIN SMALL LETTER I] - case '\uFF49': // i [FULLWIDTH LATIN SMALL LETTER I] - output[opos++] = 'i'; - break; - - case '\u0132': // IJ [LATIN CAPITAL LIGATURE IJ] - output[opos++] = 'I'; - output[opos++] = 'J'; - break; - - case '\u24A4': // â’¤ [PARENTHESIZED LATIN SMALL LETTER I] - output[opos++] = '('; - output[opos++] = 'i'; - output[opos++] = ')'; - break; - - case '\u0133': // ij [LATIN SMALL LIGATURE IJ] - output[opos++] = 'i'; - output[opos++] = 'j'; - break; - - case '\u0134': - // Ä´ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] - case '\u0248': - // Ɉ [LATIN CAPITAL LETTER J WITH STROKE] - case '\u1D0A': - // á´Š [LATIN LETTER SMALL CAPITAL J] - case '\u24BF': - // â’¿ [CIRCLED LATIN CAPITAL LETTER J] - case '\uFF2A': // J [FULLWIDTH LATIN CAPITAL LETTER J] - output[opos++] = 'J'; - break; - - case '\u0135': - // ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] - case '\u01F0': - // ǰ [LATIN SMALL LETTER J WITH CARON] - case '\u0237': - // È· [LATIN SMALL LETTER DOTLESS J] - case '\u0249': - // ɉ [LATIN SMALL LETTER J WITH STROKE] - case '\u025F': - // ÉŸ [LATIN SMALL LETTER DOTLESS J WITH STROKE] - case '\u0284': - // Ê„ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] - case '\u029D': - // � [LATIN SMALL LETTER J WITH CROSSED-TAIL] - case '\u24D9': - // â“™ [CIRCLED LATIN SMALL LETTER J] - case '\u2C7C': - // â±¼ [LATIN SUBSCRIPT SMALL LETTER J] - case '\uFF4A': // j [FULLWIDTH LATIN SMALL LETTER J] - output[opos++] = 'j'; - break; - - case '\u24A5': // â’¥ [PARENTHESIZED LATIN SMALL LETTER J] - output[opos++] = '('; - output[opos++] = 'j'; - output[opos++] = ')'; - break; - - case '\u0136': - // Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] - case '\u0198': - // Ƙ [LATIN CAPITAL LETTER K WITH HOOK] - case '\u01E8': - // Ǩ [LATIN CAPITAL LETTER K WITH CARON] - case '\u1D0B': - // á´‹ [LATIN LETTER SMALL CAPITAL K] - case '\u1E30': - // Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] - case '\u1E32': - // Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] - case '\u1E34': - // Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] - case '\u24C0': - // â“€ [CIRCLED LATIN CAPITAL LETTER K] - case '\u2C69': - // Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] - case '\uA740': - // � [LATIN CAPITAL LETTER K WITH STROKE] - case '\uA742': - // � [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] - case '\uA744': - // � [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] - case '\uFF2B': // K [FULLWIDTH LATIN CAPITAL LETTER K] - output[opos++] = 'K'; - break; - - case '\u0137': - // Ä· [LATIN SMALL LETTER K WITH CEDILLA] - case '\u0199': - // Æ™ [LATIN SMALL LETTER K WITH HOOK] - case '\u01E9': - // Ç© [LATIN SMALL LETTER K WITH CARON] - case '\u029E': - // Êž [LATIN SMALL LETTER TURNED K] - case '\u1D84': - // á¶„ [LATIN SMALL LETTER K WITH PALATAL HOOK] - case '\u1E31': - // ḱ [LATIN SMALL LETTER K WITH ACUTE] - case '\u1E33': - // ḳ [LATIN SMALL LETTER K WITH DOT BELOW] - case '\u1E35': - // ḵ [LATIN SMALL LETTER K WITH LINE BELOW] - case '\u24DA': - // ⓚ [CIRCLED LATIN SMALL LETTER K] - case '\u2C6A': - // ⱪ [LATIN SMALL LETTER K WITH DESCENDER] - case '\uA741': - // � [LATIN SMALL LETTER K WITH STROKE] - case '\uA743': - // � [LATIN SMALL LETTER K WITH DIAGONAL STROKE] - case '\uA745': - // � [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] - case '\uFF4B': // k [FULLWIDTH LATIN SMALL LETTER K] - output[opos++] = 'k'; - break; - - case '\u24A6': // â’¦ [PARENTHESIZED LATIN SMALL LETTER K] - output[opos++] = '('; - output[opos++] = 'k'; - output[opos++] = ')'; - break; - - case '\u0139': - // Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] - case '\u013B': - // Ä» [LATIN CAPITAL LETTER L WITH CEDILLA] - case '\u013D': - // Ľ [LATIN CAPITAL LETTER L WITH CARON] - case '\u013F': - // Ä¿ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] - case '\u0141': - // � [LATIN CAPITAL LETTER L WITH STROKE] - case '\u023D': - // Ƚ [LATIN CAPITAL LETTER L WITH BAR] - case '\u029F': - // ÊŸ [LATIN LETTER SMALL CAPITAL L] - case '\u1D0C': - // á´Œ [LATIN LETTER SMALL CAPITAL L WITH STROKE] - case '\u1E36': - // Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] - case '\u1E38': - // Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] - case '\u1E3A': - // Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] - case '\u1E3C': - // Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] - case '\u24C1': - // � [CIRCLED LATIN CAPITAL LETTER L] - case '\u2C60': - // â±  [LATIN CAPITAL LETTER L WITH DOUBLE BAR] - case '\u2C62': - // â±¢ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] - case '\uA746': - // � [LATIN CAPITAL LETTER BROKEN L] - case '\uA748': - // � [LATIN CAPITAL LETTER L WITH HIGH STROKE] - case '\uA780': - // Ꞁ [LATIN CAPITAL LETTER TURNED L] - case '\uFF2C': // L [FULLWIDTH LATIN CAPITAL LETTER L] - output[opos++] = 'L'; - break; - - case '\u013A': - // ĺ [LATIN SMALL LETTER L WITH ACUTE] - case '\u013C': - // ļ [LATIN SMALL LETTER L WITH CEDILLA] - case '\u013E': - // ľ [LATIN SMALL LETTER L WITH CARON] - case '\u0140': - // Å€ [LATIN SMALL LETTER L WITH MIDDLE DOT] - case '\u0142': - // Å‚ [LATIN SMALL LETTER L WITH STROKE] - case '\u019A': - // Æš [LATIN SMALL LETTER L WITH BAR] - case '\u0234': - // È´ [LATIN SMALL LETTER L WITH CURL] - case '\u026B': - // É« [LATIN SMALL LETTER L WITH MIDDLE TILDE] - case '\u026C': - // ɬ [LATIN SMALL LETTER L WITH BELT] - case '\u026D': - // É­ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] - case '\u1D85': - // á¶… [LATIN SMALL LETTER L WITH PALATAL HOOK] - case '\u1E37': - // ḷ [LATIN SMALL LETTER L WITH DOT BELOW] - case '\u1E39': - // ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] - case '\u1E3B': - // ḻ [LATIN SMALL LETTER L WITH LINE BELOW] - case '\u1E3D': - // ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] - case '\u24DB': - // â“› [CIRCLED LATIN SMALL LETTER L] - case '\u2C61': - // ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] - case '\uA747': - // � [LATIN SMALL LETTER BROKEN L] - case '\uA749': - // � [LATIN SMALL LETTER L WITH HIGH STROKE] - case '\uA781': - // � [LATIN SMALL LETTER TURNED L] - case '\uFF4C': // l [FULLWIDTH LATIN SMALL LETTER L] - output[opos++] = 'l'; - break; - - case '\u01C7': // LJ [LATIN CAPITAL LETTER LJ] - output[opos++] = 'L'; - output[opos++] = 'J'; - break; - - case '\u1EFA': // Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] - output[opos++] = 'L'; - output[opos++] = 'L'; - break; - - case '\u01C8': // Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] - output[opos++] = 'L'; - output[opos++] = 'j'; - break; - - case '\u24A7': // â’§ [PARENTHESIZED LATIN SMALL LETTER L] - output[opos++] = '('; - output[opos++] = 'l'; - output[opos++] = ')'; - break; - - case '\u01C9': // lj [LATIN SMALL LETTER LJ] - output[opos++] = 'l'; - output[opos++] = 'j'; - break; - - case '\u1EFB': // á»» [LATIN SMALL LETTER MIDDLE-WELSH LL] - output[opos++] = 'l'; - output[opos++] = 'l'; - break; - - case '\u02AA': // ʪ [LATIN SMALL LETTER LS DIGRAPH] - output[opos++] = 'l'; - output[opos++] = 's'; - break; - - case '\u02AB': // Ê« [LATIN SMALL LETTER LZ DIGRAPH] - output[opos++] = 'l'; - output[opos++] = 'z'; - break; - - case '\u019C': - // Æœ [LATIN CAPITAL LETTER TURNED M] - case '\u1D0D': - // á´� [LATIN LETTER SMALL CAPITAL M] - case '\u1E3E': - // Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] - case '\u1E40': - // á¹€ [LATIN CAPITAL LETTER M WITH DOT ABOVE] - case '\u1E42': - // Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] - case '\u24C2': - // â“‚ [CIRCLED LATIN CAPITAL LETTER M] - case '\u2C6E': - // â±® [LATIN CAPITAL LETTER M WITH HOOK] - case '\uA7FD': - // ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] - case '\uA7FF': - // ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] - case '\uFF2D': // ï¼­ [FULLWIDTH LATIN CAPITAL LETTER M] - output[opos++] = 'M'; - break; - - case '\u026F': - // ɯ [LATIN SMALL LETTER TURNED M] - case '\u0270': - // ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] - case '\u0271': - // ɱ [LATIN SMALL LETTER M WITH HOOK] - case '\u1D6F': - // ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] - case '\u1D86': - // ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] - case '\u1E3F': - // ḿ [LATIN SMALL LETTER M WITH ACUTE] - case '\u1E41': - // � [LATIN SMALL LETTER M WITH DOT ABOVE] - case '\u1E43': - // ṃ [LATIN SMALL LETTER M WITH DOT BELOW] - case '\u24DC': - // ⓜ [CIRCLED LATIN SMALL LETTER M] - case '\uFF4D': // � [FULLWIDTH LATIN SMALL LETTER M] - output[opos++] = 'm'; - break; - - case '\u24A8': // â’¨ [PARENTHESIZED LATIN SMALL LETTER M] - output[opos++] = '('; - output[opos++] = 'm'; - output[opos++] = ')'; - break; - - case '\u00D1': - // Ñ [LATIN CAPITAL LETTER N WITH TILDE] - case '\u0143': - // Ã…Æ’ [LATIN CAPITAL LETTER N WITH ACUTE] - case '\u0145': - // Å… [LATIN CAPITAL LETTER N WITH CEDILLA] - case '\u0147': - // Ň [LATIN CAPITAL LETTER N WITH CARON] - case '\u014A': - // Ã…Å  http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] - case '\u019D': - // � [LATIN CAPITAL LETTER N WITH LEFT HOOK] - case '\u01F8': - // Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] - case '\u0220': - // È  [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] - case '\u0274': - // É´ [LATIN LETTER SMALL CAPITAL N] - case '\u1D0E': - // á´Ž [LATIN LETTER SMALL CAPITAL REVERSED N] - case '\u1E44': - // Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] - case '\u1E46': - // Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] - case '\u1E48': - // Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] - case '\u1E4A': - // Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] - case '\u24C3': - // Ⓝ [CIRCLED LATIN CAPITAL LETTER N] - case '\uFF2E': // ï¼® [FULLWIDTH LATIN CAPITAL LETTER N] - output[opos++] = 'N'; - break; - - case '\u00F1': - // ñ [LATIN SMALL LETTER N WITH TILDE] - case '\u0144': - // Å„ [LATIN SMALL LETTER N WITH ACUTE] - case '\u0146': - // ņ [LATIN SMALL LETTER N WITH CEDILLA] - case '\u0148': - // ň [LATIN SMALL LETTER N WITH CARON] - case '\u0149': - // ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] - case '\u014B': - // Å‹ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] - case '\u019E': - // Æž [LATIN SMALL LETTER N WITH LONG RIGHT LEG] - case '\u01F9': - // ǹ [LATIN SMALL LETTER N WITH GRAVE] - case '\u0235': - // ȵ [LATIN SMALL LETTER N WITH CURL] - case '\u0272': - // ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] - case '\u0273': - // ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] - case '\u1D70': - // áµ° [LATIN SMALL LETTER N WITH MIDDLE TILDE] - case '\u1D87': - // ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] - case '\u1E45': - // á¹… [LATIN SMALL LETTER N WITH DOT ABOVE] - case '\u1E47': - // ṇ [LATIN SMALL LETTER N WITH DOT BELOW] - case '\u1E49': - // ṉ [LATIN SMALL LETTER N WITH LINE BELOW] - case '\u1E4B': - // ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] - case '\u207F': - // � [SUPERSCRIPT LATIN SMALL LETTER N] - case '\u24DD': - // � [CIRCLED LATIN SMALL LETTER N] - case '\uFF4E': // n [FULLWIDTH LATIN SMALL LETTER N] - output[opos++] = 'n'; - break; - - case '\u01CA': // ÇŠ [LATIN CAPITAL LETTER NJ] - output[opos++] = 'N'; - output[opos++] = 'J'; - break; - - case '\u01CB': // Ç‹ [LATIN CAPITAL LETTER N WITH SMALL LETTER J] - output[opos++] = 'N'; - output[opos++] = 'j'; - break; - - case '\u24A9': // â’© [PARENTHESIZED LATIN SMALL LETTER N] - output[opos++] = '('; - output[opos++] = 'n'; - output[opos++] = ')'; - break; - - case '\u01CC': // ÇŒ [LATIN SMALL LETTER NJ] - output[opos++] = 'n'; - output[opos++] = 'j'; - break; - - case '\u00D2': - // Ã’ [LATIN CAPITAL LETTER O WITH GRAVE] - case '\u00D3': - // Ó [LATIN CAPITAL LETTER O WITH ACUTE] - case '\u00D4': - // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] - case '\u00D5': - // Õ [LATIN CAPITAL LETTER O WITH TILDE] - case '\u00D6': - // Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] - case '\u00D8': - // Ø [LATIN CAPITAL LETTER O WITH STROKE] - case '\u014C': - // Ã…Å’ [LATIN CAPITAL LETTER O WITH MACRON] - case '\u014E': - // ÅŽ [LATIN CAPITAL LETTER O WITH BREVE] - case '\u0150': - // � [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] - case '\u0186': - // Ɔ [LATIN CAPITAL LETTER OPEN O] - case '\u019F': - // ÆŸ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] - case '\u01A0': - // Æ  [LATIN CAPITAL LETTER O WITH HORN] - case '\u01D1': - // Ç‘ [LATIN CAPITAL LETTER O WITH CARON] - case '\u01EA': - // Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] - case '\u01EC': - // Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] - case '\u01FE': - // Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] - case '\u020C': - // ÈŒ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] - case '\u020E': - // ÈŽ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] - case '\u022A': - // Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] - case '\u022C': - // Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] - case '\u022E': - // È® [LATIN CAPITAL LETTER O WITH DOT ABOVE] - case '\u0230': - // Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] - case '\u1D0F': - // á´� [LATIN LETTER SMALL CAPITAL O] - case '\u1D10': - // á´� [LATIN LETTER SMALL CAPITAL OPEN O] - case '\u1E4C': - // Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] - case '\u1E4E': - // Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] - case '\u1E50': - // � [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] - case '\u1E52': - // á¹’ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] - case '\u1ECC': - // Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] - case '\u1ECE': - // Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] - case '\u1ED0': - // � [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] - case '\u1ED2': - // á»’ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] - case '\u1ED4': - // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1ED6': - // á»– [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] - case '\u1ED8': - // Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EDA': - // Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] - case '\u1EDC': - // Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] - case '\u1EDE': - // Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] - case '\u1EE0': - // á»  [LATIN CAPITAL LETTER O WITH HORN AND TILDE] - case '\u1EE2': - // Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] - case '\u24C4': - // â“„ [CIRCLED LATIN CAPITAL LETTER O] - case '\uA74A': - // � [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] - case '\uA74C': - // � [LATIN CAPITAL LETTER O WITH LOOP] - case '\uFF2F': // O [FULLWIDTH LATIN CAPITAL LETTER O] - output[opos++] = 'O'; - break; - - case '\u00F2': - // ò [LATIN SMALL LETTER O WITH GRAVE] - case '\u00F3': - // ó [LATIN SMALL LETTER O WITH ACUTE] - case '\u00F4': - // ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] - case '\u00F5': - // õ [LATIN SMALL LETTER O WITH TILDE] - case '\u00F6': - // ö [LATIN SMALL LETTER O WITH DIAERESIS] - case '\u00F8': - // ø [LATIN SMALL LETTER O WITH STROKE] - case '\u014D': - // � [LATIN SMALL LETTER O WITH MACRON] - case '\u014F': - // � [LATIN SMALL LETTER O WITH BREVE] - case '\u0151': - // Å‘ [LATIN SMALL LETTER O WITH DOUBLE ACUTE] - case '\u01A1': - // Æ¡ [LATIN SMALL LETTER O WITH HORN] - case '\u01D2': - // Ç’ [LATIN SMALL LETTER O WITH CARON] - case '\u01EB': - // Ç« [LATIN SMALL LETTER O WITH OGONEK] - case '\u01ED': - // Ç­ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] - case '\u01FF': - // Ç¿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] - case '\u020D': - // � [LATIN SMALL LETTER O WITH DOUBLE GRAVE] - case '\u020F': - // � [LATIN SMALL LETTER O WITH INVERTED BREVE] - case '\u022B': - // È« [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] - case '\u022D': - // È­ [LATIN SMALL LETTER O WITH TILDE AND MACRON] - case '\u022F': - // ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] - case '\u0231': - // ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] - case '\u0254': - // �? [LATIN SMALL LETTER OPEN O] - case '\u0275': - // ɵ [LATIN SMALL LETTER BARRED O] - case '\u1D16': - // á´– [LATIN SMALL LETTER TOP HALF O] - case '\u1D17': - // á´— [LATIN SMALL LETTER BOTTOM HALF O] - case '\u1D97': - // á¶— [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] - case '\u1E4D': - // � [LATIN SMALL LETTER O WITH TILDE AND ACUTE] - case '\u1E4F': - // � [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] - case '\u1E51': - // ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] - case '\u1E53': - // ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] - case '\u1ECD': - // � [LATIN SMALL LETTER O WITH DOT BELOW] - case '\u1ECF': - // � [LATIN SMALL LETTER O WITH HOOK ABOVE] - case '\u1ED1': - // ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] - case '\u1ED3': - // ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] - case '\u1ED5': - // ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1ED7': - // á»— [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] - case '\u1ED9': - // á»™ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EDB': - // á»› [LATIN SMALL LETTER O WITH HORN AND ACUTE] - case '\u1EDD': - // � [LATIN SMALL LETTER O WITH HORN AND GRAVE] - case '\u1EDF': - // ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] - case '\u1EE1': - // ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] - case '\u1EE3': - // ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] - case '\u2092': - // â‚’ [LATIN SUBSCRIPT SMALL LETTER O] - case '\u24DE': - // ⓞ [CIRCLED LATIN SMALL LETTER O] - case '\u2C7A': - // ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] - case '\uA74B': - // � [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] - case '\uA74D': - // � [LATIN SMALL LETTER O WITH LOOP] - case '\uFF4F': // � [FULLWIDTH LATIN SMALL LETTER O] - output[opos++] = 'o'; - break; - - case '\u0152': - // Å’ [LATIN CAPITAL LIGATURE OE] - case '\u0276': // ɶ [LATIN LETTER SMALL CAPITAL OE] - output[opos++] = 'O'; - output[opos++] = 'E'; - break; - - case '\uA74E': // � [LATIN CAPITAL LETTER OO] - output[opos++] = 'O'; - output[opos++] = 'O'; - break; - - case '\u0222': - // È¢ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] - case '\u1D15': // á´• [LATIN LETTER SMALL CAPITAL OU] - output[opos++] = 'O'; - output[opos++] = 'U'; - break; - - case '\u24AA': // â’ª [PARENTHESIZED LATIN SMALL LETTER O] - output[opos++] = '('; - output[opos++] = 'o'; - output[opos++] = ')'; - break; - - case '\u0153': - // Å“ [LATIN SMALL LIGATURE OE] - case '\u1D14': // á´�? [LATIN SMALL LETTER TURNED OE] - output[opos++] = 'o'; - output[opos++] = 'e'; - break; - - case '\uA74F': // � [LATIN SMALL LETTER OO] - output[opos++] = 'o'; - output[opos++] = 'o'; - break; - - case '\u0223': // È£ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] - output[opos++] = 'o'; - output[opos++] = 'u'; - break; - - case '\u01A4': - // Ƥ [LATIN CAPITAL LETTER P WITH HOOK] - case '\u1D18': - // á´˜ [LATIN LETTER SMALL CAPITAL P] - case '\u1E54': - // �? [LATIN CAPITAL LETTER P WITH ACUTE] - case '\u1E56': - // á¹– [LATIN CAPITAL LETTER P WITH DOT ABOVE] - case '\u24C5': - // â“… [CIRCLED LATIN CAPITAL LETTER P] - case '\u2C63': - // â±£ [LATIN CAPITAL LETTER P WITH STROKE] - case '\uA750': - // � [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] - case '\uA752': - // � [LATIN CAPITAL LETTER P WITH FLOURISH] - case '\uA754': - // �? [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] - case '\uFF30': // ï¼° [FULLWIDTH LATIN CAPITAL LETTER P] - output[opos++] = 'P'; - break; - - case '\u01A5': - // Æ¥ [LATIN SMALL LETTER P WITH HOOK] - case '\u1D71': - // áµ± [LATIN SMALL LETTER P WITH MIDDLE TILDE] - case '\u1D7D': - // áµ½ [LATIN SMALL LETTER P WITH STROKE] - case '\u1D88': - // ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] - case '\u1E55': - // ṕ [LATIN SMALL LETTER P WITH ACUTE] - case '\u1E57': - // á¹— [LATIN SMALL LETTER P WITH DOT ABOVE] - case '\u24DF': - // ⓟ [CIRCLED LATIN SMALL LETTER P] - case '\uA751': - // � [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] - case '\uA753': - // � [LATIN SMALL LETTER P WITH FLOURISH] - case '\uA755': - // � [LATIN SMALL LETTER P WITH SQUIRREL TAIL] - case '\uA7FC': - // ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] - case '\uFF50': // � [FULLWIDTH LATIN SMALL LETTER P] - output[opos++] = 'p'; - break; - - case '\u24AB': // â’« [PARENTHESIZED LATIN SMALL LETTER P] - output[opos++] = '('; - output[opos++] = 'p'; - output[opos++] = ')'; - break; - - case '\u024A': - // ÉŠ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] - case '\u24C6': - // Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] - case '\uA756': - // � [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] - case '\uA758': - // � [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] - case '\uFF31': // ï¼± [FULLWIDTH LATIN CAPITAL LETTER Q] - output[opos++] = 'Q'; - break; - - case '\u0138': - // ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] - case '\u024B': - // É‹ [LATIN SMALL LETTER Q WITH HOOK TAIL] - case '\u02A0': - // Ê  [LATIN SMALL LETTER Q WITH HOOK] - case '\u24E0': - // â“  [CIRCLED LATIN SMALL LETTER Q] - case '\uA757': - // � [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] - case '\uA759': - // � [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] - case '\uFF51': // q [FULLWIDTH LATIN SMALL LETTER Q] - output[opos++] = 'q'; - break; - - case '\u24AC': // â’¬ [PARENTHESIZED LATIN SMALL LETTER Q] - output[opos++] = '('; - output[opos++] = 'q'; - output[opos++] = ')'; - break; - - case '\u0239': // ȹ [LATIN SMALL LETTER QP DIGRAPH] - output[opos++] = 'q'; - output[opos++] = 'p'; - break; - - case '\u0154': - // �? [LATIN CAPITAL LETTER R WITH ACUTE] - case '\u0156': - // Å– [LATIN CAPITAL LETTER R WITH CEDILLA] - case '\u0158': - // Ã…Ëœ [LATIN CAPITAL LETTER R WITH CARON] - case '\u0210': - // È’ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] - case '\u0212': - // È’ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] - case '\u024C': - // ÉŒ [LATIN CAPITAL LETTER R WITH STROKE] - case '\u0280': - // Ê€ [LATIN LETTER SMALL CAPITAL R] - case '\u0281': - // � [LATIN LETTER SMALL CAPITAL INVERTED R] - case '\u1D19': - // á´™ [LATIN LETTER SMALL CAPITAL REVERSED R] - case '\u1D1A': - // á´š [LATIN LETTER SMALL CAPITAL TURNED R] - case '\u1E58': - // Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] - case '\u1E5A': - // Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] - case '\u1E5C': - // Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] - case '\u1E5E': - // Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] - case '\u24C7': - // Ⓡ [CIRCLED LATIN CAPITAL LETTER R] - case '\u2C64': - // Ɽ [LATIN CAPITAL LETTER R WITH TAIL] - case '\uA75A': - // � [LATIN CAPITAL LETTER R ROTUNDA] - case '\uA782': - // êž‚ [LATIN CAPITAL LETTER INSULAR R] - case '\uFF32': // ï¼² [FULLWIDTH LATIN CAPITAL LETTER R] - output[opos++] = 'R'; - break; - - case '\u0155': - // Å• [LATIN SMALL LETTER R WITH ACUTE] - case '\u0157': - // Å— [LATIN SMALL LETTER R WITH CEDILLA] - case '\u0159': - // Ã…â„¢ [LATIN SMALL LETTER R WITH CARON] - case '\u0211': - // È‘ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] - case '\u0213': - // È“ [LATIN SMALL LETTER R WITH INVERTED BREVE] - case '\u024D': - // � [LATIN SMALL LETTER R WITH STROKE] - case '\u027C': - // ɼ [LATIN SMALL LETTER R WITH LONG LEG] - case '\u027D': - // ɽ [LATIN SMALL LETTER R WITH TAIL] - case '\u027E': - // ɾ [LATIN SMALL LETTER R WITH FISHHOOK] - case '\u027F': - // É¿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] - case '\u1D63': - // áµ£ [LATIN SUBSCRIPT SMALL LETTER R] - case '\u1D72': - // áµ² [LATIN SMALL LETTER R WITH MIDDLE TILDE] - case '\u1D73': - // áµ³ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] - case '\u1D89': - // ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] - case '\u1E59': - // á¹™ [LATIN SMALL LETTER R WITH DOT ABOVE] - case '\u1E5B': - // á¹› [LATIN SMALL LETTER R WITH DOT BELOW] - case '\u1E5D': - // � [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] - case '\u1E5F': - // ṟ [LATIN SMALL LETTER R WITH LINE BELOW] - case '\u24E1': - // â“¡ [CIRCLED LATIN SMALL LETTER R] - case '\uA75B': - // � [LATIN SMALL LETTER R ROTUNDA] - case '\uA783': - // ꞃ [LATIN SMALL LETTER INSULAR R] - case '\uFF52': // ï½’ [FULLWIDTH LATIN SMALL LETTER R] - output[opos++] = 'r'; - break; - - case '\u24AD': // â’­ [PARENTHESIZED LATIN SMALL LETTER R] - output[opos++] = '('; - output[opos++] = 'r'; - output[opos++] = ')'; - break; - - case '\u015A': - // Ã…Å¡ [LATIN CAPITAL LETTER S WITH ACUTE] - case '\u015C': - // Ã…Å“ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] - case '\u015E': - // Åž [LATIN CAPITAL LETTER S WITH CEDILLA] - case '\u0160': - // Å  [LATIN CAPITAL LETTER S WITH CARON] - case '\u0218': - // Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] - case '\u1E60': - // á¹  [LATIN CAPITAL LETTER S WITH DOT ABOVE] - case '\u1E62': - // á¹¢ [LATIN CAPITAL LETTER S WITH DOT BELOW] - case '\u1E64': - // Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] - case '\u1E66': - // Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] - case '\u1E68': - // Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] - case '\u24C8': - // Ⓢ [CIRCLED LATIN CAPITAL LETTER S] - case '\uA731': - // ꜱ [LATIN LETTER SMALL CAPITAL S] - case '\uA785': - // êž… [LATIN SMALL LETTER INSULAR S] - case '\uFF33': // ï¼³ [FULLWIDTH LATIN CAPITAL LETTER S] - output[opos++] = 'S'; - break; - - case '\u015B': - // Å› [LATIN SMALL LETTER S WITH ACUTE] - case '\u015D': - // � [LATIN SMALL LETTER S WITH CIRCUMFLEX] - case '\u015F': - // ÅŸ [LATIN SMALL LETTER S WITH CEDILLA] - case '\u0161': - // Å¡ [LATIN SMALL LETTER S WITH CARON] - case '\u017F': - // Å¿ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] - case '\u0219': - // È™ [LATIN SMALL LETTER S WITH COMMA BELOW] - case '\u023F': - // È¿ [LATIN SMALL LETTER S WITH SWASH TAIL] - case '\u0282': - // Ê‚ [LATIN SMALL LETTER S WITH HOOK] - case '\u1D74': - // áµ´ [LATIN SMALL LETTER S WITH MIDDLE TILDE] - case '\u1D8A': - // á¶Š [LATIN SMALL LETTER S WITH PALATAL HOOK] - case '\u1E61': - // ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] - case '\u1E63': - // á¹£ [LATIN SMALL LETTER S WITH DOT BELOW] - case '\u1E65': - // á¹¥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] - case '\u1E67': - // á¹§ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] - case '\u1E69': - // ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] - case '\u1E9C': - // ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] - case '\u1E9D': - // � [LATIN SMALL LETTER LONG S WITH HIGH STROKE] - case '\u24E2': - // â“¢ [CIRCLED LATIN SMALL LETTER S] - case '\uA784': - // êž„ [LATIN CAPITAL LETTER INSULAR S] - case '\uFF53': // s [FULLWIDTH LATIN SMALL LETTER S] - output[opos++] = 's'; - break; - - case '\u1E9E': // ẞ [LATIN CAPITAL LETTER SHARP S] - output[opos++] = 'S'; - output[opos++] = 'S'; - break; - - case '\u24AE': // â’® [PARENTHESIZED LATIN SMALL LETTER S] - output[opos++] = '('; - output[opos++] = 's'; - output[opos++] = ')'; - break; - - case '\u00DF': // ß [LATIN SMALL LETTER SHARP S] - output[opos++] = 's'; - output[opos++] = 's'; - break; - - case '\uFB06': // st [LATIN SMALL LIGATURE ST] - output[opos++] = 's'; - output[opos++] = 't'; - break; - - case '\u0162': - // Å¢ [LATIN CAPITAL LETTER T WITH CEDILLA] - case '\u0164': - // Ť [LATIN CAPITAL LETTER T WITH CARON] - case '\u0166': - // Ŧ [LATIN CAPITAL LETTER T WITH STROKE] - case '\u01AC': - // Ƭ [LATIN CAPITAL LETTER T WITH HOOK] - case '\u01AE': - // Æ® [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] - case '\u021A': - // Èš [LATIN CAPITAL LETTER T WITH COMMA BELOW] - case '\u023E': - // Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] - case '\u1D1B': - // á´› [LATIN LETTER SMALL CAPITAL T] - case '\u1E6A': - // Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] - case '\u1E6C': - // Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] - case '\u1E6E': - // á¹® [LATIN CAPITAL LETTER T WITH LINE BELOW] - case '\u1E70': - // á¹° [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] - case '\u24C9': - // Ⓣ [CIRCLED LATIN CAPITAL LETTER T] - case '\uA786': - // Ꞇ [LATIN CAPITAL LETTER INSULAR T] - case '\uFF34': // ï¼´ [FULLWIDTH LATIN CAPITAL LETTER T] - output[opos++] = 'T'; - break; - - case '\u0163': - // Å£ [LATIN SMALL LETTER T WITH CEDILLA] - case '\u0165': - // Ã…Â¥ [LATIN SMALL LETTER T WITH CARON] - case '\u0167': - // ŧ [LATIN SMALL LETTER T WITH STROKE] - case '\u01AB': - // Æ« [LATIN SMALL LETTER T WITH PALATAL HOOK] - case '\u01AD': - // Æ­ [LATIN SMALL LETTER T WITH HOOK] - case '\u021B': - // È› [LATIN SMALL LETTER T WITH COMMA BELOW] - case '\u0236': - // ȶ [LATIN SMALL LETTER T WITH CURL] - case '\u0287': - // ʇ [LATIN SMALL LETTER TURNED T] - case '\u0288': - // ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] - case '\u1D75': - // áµµ [LATIN SMALL LETTER T WITH MIDDLE TILDE] - case '\u1E6B': - // ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] - case '\u1E6D': - // á¹­ [LATIN SMALL LETTER T WITH DOT BELOW] - case '\u1E6F': - // ṯ [LATIN SMALL LETTER T WITH LINE BELOW] - case '\u1E71': - // á¹± [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] - case '\u1E97': - // ẗ [LATIN SMALL LETTER T WITH DIAERESIS] - case '\u24E3': - // â“£ [CIRCLED LATIN SMALL LETTER T] - case '\u2C66': - // ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] - case '\uFF54': // �? [FULLWIDTH LATIN SMALL LETTER T] - output[opos++] = 't'; - break; - - case '\u00DE': - // Þ [LATIN CAPITAL LETTER THORN] - case '\uA766': // � [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] - output[opos++] = 'T'; - output[opos++] = 'H'; - break; - - case '\uA728': // Ꜩ [LATIN CAPITAL LETTER TZ] - output[opos++] = 'T'; - output[opos++] = 'Z'; - break; - - case '\u24AF': // â’¯ [PARENTHESIZED LATIN SMALL LETTER T] - output[opos++] = '('; - output[opos++] = 't'; - output[opos++] = ')'; - break; - - case '\u02A8': // ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] - output[opos++] = 't'; - output[opos++] = 'c'; - break; - - case '\u00FE': - // þ [LATIN SMALL LETTER THORN] - case '\u1D7A': - // ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] - case '\uA767': // � [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] - output[opos++] = 't'; - output[opos++] = 'h'; - break; - - case '\u02A6': // ʦ [LATIN SMALL LETTER TS DIGRAPH] - output[opos++] = 't'; - output[opos++] = 's'; - break; - - case '\uA729': // ꜩ [LATIN SMALL LETTER TZ] - output[opos++] = 't'; - output[opos++] = 'z'; - break; - - case '\u00D9': - // Ù [LATIN CAPITAL LETTER U WITH GRAVE] - case '\u00DA': - // Ú [LATIN CAPITAL LETTER U WITH ACUTE] - case '\u00DB': - // Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] - case '\u00DC': - // Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] - case '\u0168': - // Ũ [LATIN CAPITAL LETTER U WITH TILDE] - case '\u016A': - // Ū [LATIN CAPITAL LETTER U WITH MACRON] - case '\u016C': - // Ŭ [LATIN CAPITAL LETTER U WITH BREVE] - case '\u016E': - // Å® [LATIN CAPITAL LETTER U WITH RING ABOVE] - case '\u0170': - // Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] - case '\u0172': - // Ų [LATIN CAPITAL LETTER U WITH OGONEK] - case '\u01AF': - // Ư [LATIN CAPITAL LETTER U WITH HORN] - case '\u01D3': - // Ç“ [LATIN CAPITAL LETTER U WITH CARON] - case '\u01D5': - // Ç• [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] - case '\u01D7': - // Ç— [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] - case '\u01D9': - // Ç™ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] - case '\u01DB': - // Ç› [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] - case '\u0214': - // �? [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] - case '\u0216': - // È– [LATIN CAPITAL LETTER U WITH INVERTED BREVE] - case '\u0244': - // É„ [LATIN CAPITAL LETTER U BAR] - case '\u1D1C': - // á´œ [LATIN LETTER SMALL CAPITAL U] - case '\u1D7E': - // áµ¾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] - case '\u1E72': - // á¹² [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] - case '\u1E74': - // á¹´ [LATIN CAPITAL LETTER U WITH TILDE BELOW] - case '\u1E76': - // á¹¶ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] - case '\u1E78': - // Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] - case '\u1E7A': - // Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] - case '\u1EE4': - // Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] - case '\u1EE6': - // Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] - case '\u1EE8': - // Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] - case '\u1EEA': - // Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] - case '\u1EEC': - // Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] - case '\u1EEE': - // á»® [LATIN CAPITAL LETTER U WITH HORN AND TILDE] - case '\u1EF0': - // á»° [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] - case '\u24CA': - // Ⓤ [CIRCLED LATIN CAPITAL LETTER U] - case '\uFF35': // ï¼µ [FULLWIDTH LATIN CAPITAL LETTER U] - output[opos++] = 'U'; - break; - - case '\u00F9': - // ù [LATIN SMALL LETTER U WITH GRAVE] - case '\u00FA': - // ú [LATIN SMALL LETTER U WITH ACUTE] - case '\u00FB': - // û [LATIN SMALL LETTER U WITH CIRCUMFLEX] - case '\u00FC': - // ü [LATIN SMALL LETTER U WITH DIAERESIS] - case '\u0169': - // Å© [LATIN SMALL LETTER U WITH TILDE] - case '\u016B': - // Å« [LATIN SMALL LETTER U WITH MACRON] - case '\u016D': - // Å­ [LATIN SMALL LETTER U WITH BREVE] - case '\u016F': - // ů [LATIN SMALL LETTER U WITH RING ABOVE] - case '\u0171': - // ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] - case '\u0173': - // ų [LATIN SMALL LETTER U WITH OGONEK] - case '\u01B0': - // ư [LATIN SMALL LETTER U WITH HORN] - case '\u01D4': - // �? [LATIN SMALL LETTER U WITH CARON] - case '\u01D6': - // Ç– [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] - case '\u01D8': - // ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] - case '\u01DA': - // Çš [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] - case '\u01DC': - // Çœ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] - case '\u0215': - // È• [LATIN SMALL LETTER U WITH DOUBLE GRAVE] - case '\u0217': - // È— [LATIN SMALL LETTER U WITH INVERTED BREVE] - case '\u0289': - // ʉ [LATIN SMALL LETTER U BAR] - case '\u1D64': - // ᵤ [LATIN SUBSCRIPT SMALL LETTER U] - case '\u1D99': - // á¶™ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] - case '\u1E73': - // á¹³ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] - case '\u1E75': - // á¹µ [LATIN SMALL LETTER U WITH TILDE BELOW] - case '\u1E77': - // á¹· [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] - case '\u1E79': - // á¹¹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] - case '\u1E7B': - // á¹» [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] - case '\u1EE5': - // ụ [LATIN SMALL LETTER U WITH DOT BELOW] - case '\u1EE7': - // á»§ [LATIN SMALL LETTER U WITH HOOK ABOVE] - case '\u1EE9': - // ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] - case '\u1EEB': - // ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] - case '\u1EED': - // á»­ [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] - case '\u1EEF': - // ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] - case '\u1EF1': - // á»± [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] - case '\u24E4': - // ⓤ [CIRCLED LATIN SMALL LETTER U] - case '\uFF55': // u [FULLWIDTH LATIN SMALL LETTER U] - output[opos++] = 'u'; - break; - - case '\u24B0': // â’° [PARENTHESIZED LATIN SMALL LETTER U] - output[opos++] = '('; - output[opos++] = 'u'; - output[opos++] = ')'; - break; - - case '\u1D6B': // ᵫ [LATIN SMALL LETTER UE] - output[opos++] = 'u'; - output[opos++] = 'e'; - break; - - case '\u01B2': - // Ʋ [LATIN CAPITAL LETTER V WITH HOOK] - case '\u0245': - // É… [LATIN CAPITAL LETTER TURNED V] - case '\u1D20': - // á´  [LATIN LETTER SMALL CAPITAL V] - case '\u1E7C': - // á¹¼ [LATIN CAPITAL LETTER V WITH TILDE] - case '\u1E7E': - // á¹¾ [LATIN CAPITAL LETTER V WITH DOT BELOW] - case '\u1EFC': - // Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] - case '\u24CB': - // â“‹ [CIRCLED LATIN CAPITAL LETTER V] - case '\uA75E': - // � [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] - case '\uA768': - // � [LATIN CAPITAL LETTER VEND] - case '\uFF36': // ï¼¶ [FULLWIDTH LATIN CAPITAL LETTER V] - output[opos++] = 'V'; - break; - - case '\u028B': - // Ê‹ [LATIN SMALL LETTER V WITH HOOK] - case '\u028C': - // ÊŒ [LATIN SMALL LETTER TURNED V] - case '\u1D65': - // áµ¥ [LATIN SUBSCRIPT SMALL LETTER V] - case '\u1D8C': - // á¶Œ [LATIN SMALL LETTER V WITH PALATAL HOOK] - case '\u1E7D': - // á¹½ [LATIN SMALL LETTER V WITH TILDE] - case '\u1E7F': - // ṿ [LATIN SMALL LETTER V WITH DOT BELOW] - case '\u24E5': - // â“¥ [CIRCLED LATIN SMALL LETTER V] - case '\u2C71': - // â±± [LATIN SMALL LETTER V WITH RIGHT HOOK] - case '\u2C74': - // â±´ [LATIN SMALL LETTER V WITH CURL] - case '\uA75F': - // � [LATIN SMALL LETTER V WITH DIAGONAL STROKE] - case '\uFF56': // ï½– [FULLWIDTH LATIN SMALL LETTER V] - output[opos++] = 'v'; - break; - - case '\uA760': // � [LATIN CAPITAL LETTER VY] - output[opos++] = 'V'; - output[opos++] = 'Y'; - break; - - case '\u24B1': // â’± [PARENTHESIZED LATIN SMALL LETTER V] - output[opos++] = '('; - output[opos++] = 'v'; - output[opos++] = ')'; - break; - - case '\uA761': // � [LATIN SMALL LETTER VY] - output[opos++] = 'v'; - output[opos++] = 'y'; - break; - - case '\u0174': - // Å´ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] - case '\u01F7': - // Ç· http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] - case '\u1D21': - // á´¡ [LATIN LETTER SMALL CAPITAL W] - case '\u1E80': - // Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] - case '\u1E82': - // Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] - case '\u1E84': - // Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] - case '\u1E86': - // Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] - case '\u1E88': - // Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] - case '\u24CC': - // Ⓦ [CIRCLED LATIN CAPITAL LETTER W] - case '\u2C72': - // â±² [LATIN CAPITAL LETTER W WITH HOOK] - case '\uFF37': // ï¼· [FULLWIDTH LATIN CAPITAL LETTER W] - output[opos++] = 'W'; - break; - - case '\u0175': - // ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] - case '\u01BF': - // Æ¿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] - case '\u028D': - // � [LATIN SMALL LETTER TURNED W] - case '\u1E81': - // � [LATIN SMALL LETTER W WITH GRAVE] - case '\u1E83': - // ẃ [LATIN SMALL LETTER W WITH ACUTE] - case '\u1E85': - // ẅ [LATIN SMALL LETTER W WITH DIAERESIS] - case '\u1E87': - // ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] - case '\u1E89': - // ẉ [LATIN SMALL LETTER W WITH DOT BELOW] - case '\u1E98': - // ẘ [LATIN SMALL LETTER W WITH RING ABOVE] - case '\u24E6': - // ⓦ [CIRCLED LATIN SMALL LETTER W] - case '\u2C73': - // â±³ [LATIN SMALL LETTER W WITH HOOK] - case '\uFF57': // ï½— [FULLWIDTH LATIN SMALL LETTER W] - output[opos++] = 'w'; - break; - - case '\u24B2': // â’² [PARENTHESIZED LATIN SMALL LETTER W] - output[opos++] = '('; - output[opos++] = 'w'; - output[opos++] = ')'; - break; - - case '\u1E8A': - // Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] - case '\u1E8C': - // Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] - case '\u24CD': - // � [CIRCLED LATIN CAPITAL LETTER X] - case '\uFF38': // X [FULLWIDTH LATIN CAPITAL LETTER X] - output[opos++] = 'X'; - break; - - case '\u1D8D': - // � [LATIN SMALL LETTER X WITH PALATAL HOOK] - case '\u1E8B': - // ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] - case '\u1E8D': - // � [LATIN SMALL LETTER X WITH DIAERESIS] - case '\u2093': - // â‚“ [LATIN SUBSCRIPT SMALL LETTER X] - case '\u24E7': - // â“§ [CIRCLED LATIN SMALL LETTER X] - case '\uFF58': // x [FULLWIDTH LATIN SMALL LETTER X] - output[opos++] = 'x'; - break; - - case '\u24B3': // â’³ [PARENTHESIZED LATIN SMALL LETTER X] - output[opos++] = '('; - output[opos++] = 'x'; - output[opos++] = ')'; - break; - - case '\u00DD': - // � [LATIN CAPITAL LETTER Y WITH ACUTE] - case '\u0176': - // Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] - case '\u0178': - // Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] - case '\u01B3': - // Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] - case '\u0232': - // Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] - case '\u024E': - // ÉŽ [LATIN CAPITAL LETTER Y WITH STROKE] - case '\u028F': - // � [LATIN LETTER SMALL CAPITAL Y] - case '\u1E8E': - // Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] - case '\u1EF2': - // Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] - case '\u1EF4': - // á»´ [LATIN CAPITAL LETTER Y WITH DOT BELOW] - case '\u1EF6': - // á»¶ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] - case '\u1EF8': - // Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] - case '\u1EFE': - // Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] - case '\u24CE': - // Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] - case '\uFF39': // ï¼¹ [FULLWIDTH LATIN CAPITAL LETTER Y] - output[opos++] = 'Y'; - break; - - case '\u00FD': - // ý [LATIN SMALL LETTER Y WITH ACUTE] - case '\u00FF': - // ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] - case '\u0177': - // Å· [LATIN SMALL LETTER Y WITH CIRCUMFLEX] - case '\u01B4': - // Æ´ [LATIN SMALL LETTER Y WITH HOOK] - case '\u0233': - // ȳ [LATIN SMALL LETTER Y WITH MACRON] - case '\u024F': - // � [LATIN SMALL LETTER Y WITH STROKE] - case '\u028E': - // ÊŽ [LATIN SMALL LETTER TURNED Y] - case '\u1E8F': - // � [LATIN SMALL LETTER Y WITH DOT ABOVE] - case '\u1E99': - // ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] - case '\u1EF3': - // ỳ [LATIN SMALL LETTER Y WITH GRAVE] - case '\u1EF5': - // ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] - case '\u1EF7': - // á»· [LATIN SMALL LETTER Y WITH HOOK ABOVE] - case '\u1EF9': - // ỹ [LATIN SMALL LETTER Y WITH TILDE] - case '\u1EFF': - // ỿ [LATIN SMALL LETTER Y WITH LOOP] - case '\u24E8': - // ⓨ [CIRCLED LATIN SMALL LETTER Y] - case '\uFF59': // ï½™ [FULLWIDTH LATIN SMALL LETTER Y] - output[opos++] = 'y'; - break; - - case '\u24B4': // â’´ [PARENTHESIZED LATIN SMALL LETTER Y] - output[opos++] = '('; - output[opos++] = 'y'; - output[opos++] = ')'; - break; - - case '\u0179': - // Ź [LATIN CAPITAL LETTER Z WITH ACUTE] - case '\u017B': - // Å» [LATIN CAPITAL LETTER Z WITH DOT ABOVE] - case '\u017D': - // Ž [LATIN CAPITAL LETTER Z WITH CARON] - case '\u01B5': - // Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] - case '\u021C': - // Èœ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] - case '\u0224': - // Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] - case '\u1D22': - // á´¢ [LATIN LETTER SMALL CAPITAL Z] - case '\u1E90': - // � [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] - case '\u1E92': - // Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] - case '\u1E94': - // �? [LATIN CAPITAL LETTER Z WITH LINE BELOW] - case '\u24CF': - // � [CIRCLED LATIN CAPITAL LETTER Z] - case '\u2C6B': - // Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] - case '\uA762': - // � [LATIN CAPITAL LETTER VISIGOTHIC Z] - case '\uFF3A': // Z [FULLWIDTH LATIN CAPITAL LETTER Z] - output[opos++] = 'Z'; - break; - - case '\u017A': - // ź [LATIN SMALL LETTER Z WITH ACUTE] - case '\u017C': - // ż [LATIN SMALL LETTER Z WITH DOT ABOVE] - case '\u017E': - // ž [LATIN SMALL LETTER Z WITH CARON] - case '\u01B6': - // ƶ [LATIN SMALL LETTER Z WITH STROKE] - case '\u021D': - // � http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] - case '\u0225': - // È¥ [LATIN SMALL LETTER Z WITH HOOK] - case '\u0240': - // É€ [LATIN SMALL LETTER Z WITH SWASH TAIL] - case '\u0290': - // � [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] - case '\u0291': - // Ê‘ [LATIN SMALL LETTER Z WITH CURL] - case '\u1D76': - // áµ¶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] - case '\u1D8E': - // á¶Ž [LATIN SMALL LETTER Z WITH PALATAL HOOK] - case '\u1E91': - // ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] - case '\u1E93': - // ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] - case '\u1E95': - // ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] - case '\u24E9': - // â“© [CIRCLED LATIN SMALL LETTER Z] - case '\u2C6C': - // ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] - case '\uA763': - // � [LATIN SMALL LETTER VISIGOTHIC Z] - case '\uFF5A': // z [FULLWIDTH LATIN SMALL LETTER Z] - output[opos++] = 'z'; - break; - - case '\u24B5': // â’µ [PARENTHESIZED LATIN SMALL LETTER Z] - output[opos++] = '('; - output[opos++] = 'z'; - output[opos++] = ')'; - break; - - case '\u2070': - // � [SUPERSCRIPT ZERO] - case '\u2080': - // â‚€ [SUBSCRIPT ZERO] - case '\u24EA': - // ⓪ [CIRCLED DIGIT ZERO] - case '\u24FF': - // â“¿ [NEGATIVE CIRCLED DIGIT ZERO] - case '\uFF10': // � [FULLWIDTH DIGIT ZERO] - output[opos++] = '0'; - break; - - case '\u00B9': - // ¹ [SUPERSCRIPT ONE] - case '\u2081': - // � [SUBSCRIPT ONE] - case '\u2460': - // â‘  [CIRCLED DIGIT ONE] - case '\u24F5': - // ⓵ [DOUBLE CIRCLED DIGIT ONE] - case '\u2776': - // � [DINGBAT NEGATIVE CIRCLED DIGIT ONE] - case '\u2780': - // ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] - case '\u278A': - // ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] - case '\uFF11': // 1 [FULLWIDTH DIGIT ONE] - output[opos++] = '1'; - break; - - case '\u2488': // â’ˆ [DIGIT ONE FULL STOP] - output[opos++] = '1'; - output[opos++] = '.'; - break; - - case '\u2474': // â‘´ [PARENTHESIZED DIGIT ONE] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = ')'; - break; - - case '\u00B2': - // ² [SUPERSCRIPT TWO] - case '\u2082': - // â‚‚ [SUBSCRIPT TWO] - case '\u2461': - // â‘¡ [CIRCLED DIGIT TWO] - case '\u24F6': - // â“¶ [DOUBLE CIRCLED DIGIT TWO] - case '\u2777': - // � [DINGBAT NEGATIVE CIRCLED DIGIT TWO] - case '\u2781': - // � [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] - case '\u278B': - // âž‹ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] - case '\uFF12': // ï¼’ [FULLWIDTH DIGIT TWO] - output[opos++] = '2'; - break; - - case '\u2489': // â’‰ [DIGIT TWO FULL STOP] - output[opos++] = '2'; - output[opos++] = '.'; - break; - - case '\u2475': // ⑵ [PARENTHESIZED DIGIT TWO] - output[opos++] = '('; - output[opos++] = '2'; - output[opos++] = ')'; - break; - - case '\u00B3': - // ³ [SUPERSCRIPT THREE] - case '\u2083': - // ₃ [SUBSCRIPT THREE] - case '\u2462': - // â‘¢ [CIRCLED DIGIT THREE] - case '\u24F7': - // â“· [DOUBLE CIRCLED DIGIT THREE] - case '\u2778': - // � [DINGBAT NEGATIVE CIRCLED DIGIT THREE] - case '\u2782': - // âž‚ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] - case '\u278C': - // ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] - case '\uFF13': // 3 [FULLWIDTH DIGIT THREE] - output[opos++] = '3'; - break; - - case '\u248A': // â’Š [DIGIT THREE FULL STOP] - output[opos++] = '3'; - output[opos++] = '.'; - break; - - case '\u2476': // â‘¶ [PARENTHESIZED DIGIT THREE] - output[opos++] = '('; - output[opos++] = '3'; - output[opos++] = ')'; - break; - - case '\u2074': - // � [SUPERSCRIPT FOUR] - case '\u2084': - // â‚„ [SUBSCRIPT FOUR] - case '\u2463': - // â‘£ [CIRCLED DIGIT FOUR] - case '\u24F8': - // ⓸ [DOUBLE CIRCLED DIGIT FOUR] - case '\u2779': - // � [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] - case '\u2783': - // ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] - case '\u278D': - // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] - case '\uFF14': // �? [FULLWIDTH DIGIT FOUR] - output[opos++] = '4'; - break; - - case '\u248B': // â’‹ [DIGIT FOUR FULL STOP] - output[opos++] = '4'; - output[opos++] = '.'; - break; - - case '\u2477': // â‘· [PARENTHESIZED DIGIT FOUR] - output[opos++] = '('; - output[opos++] = '4'; - output[opos++] = ')'; - break; - - case '\u2075': - // � [SUPERSCRIPT FIVE] - case '\u2085': - // â‚… [SUBSCRIPT FIVE] - case '\u2464': - // ⑤ [CIRCLED DIGIT FIVE] - case '\u24F9': - // ⓹ [DOUBLE CIRCLED DIGIT FIVE] - case '\u277A': - // � [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] - case '\u2784': - // âž„ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] - case '\u278E': - // ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] - case '\uFF15': // 5 [FULLWIDTH DIGIT FIVE] - output[opos++] = '5'; - break; - - case '\u248C': // â’Œ [DIGIT FIVE FULL STOP] - output[opos++] = '5'; - output[opos++] = '.'; - break; - - case '\u2478': // ⑸ [PARENTHESIZED DIGIT FIVE] - output[opos++] = '('; - output[opos++] = '5'; - output[opos++] = ')'; - break; - - case '\u2076': - // � [SUPERSCRIPT SIX] - case '\u2086': - // ₆ [SUBSCRIPT SIX] - case '\u2465': - // â‘¥ [CIRCLED DIGIT SIX] - case '\u24FA': - // ⓺ [DOUBLE CIRCLED DIGIT SIX] - case '\u277B': - // � [DINGBAT NEGATIVE CIRCLED DIGIT SIX] - case '\u2785': - // âž… [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] - case '\u278F': - // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] - case '\uFF16': // ï¼– [FULLWIDTH DIGIT SIX] - output[opos++] = '6'; - break; - - case '\u248D': // â’� [DIGIT SIX FULL STOP] - output[opos++] = '6'; - output[opos++] = '.'; - break; - - case '\u2479': // ⑹ [PARENTHESIZED DIGIT SIX] - output[opos++] = '('; - output[opos++] = '6'; - output[opos++] = ')'; - break; - - case '\u2077': - // � [SUPERSCRIPT SEVEN] - case '\u2087': - // ₇ [SUBSCRIPT SEVEN] - case '\u2466': - // ⑦ [CIRCLED DIGIT SEVEN] - case '\u24FB': - // â“» [DOUBLE CIRCLED DIGIT SEVEN] - case '\u277C': - // � [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] - case '\u2786': - // ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] - case '\u2790': - // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] - case '\uFF17': // ï¼— [FULLWIDTH DIGIT SEVEN] - output[opos++] = '7'; - break; - - case '\u248E': // â’Ž [DIGIT SEVEN FULL STOP] - output[opos++] = '7'; - output[opos++] = '.'; - break; - - case '\u247A': // ⑺ [PARENTHESIZED DIGIT SEVEN] - output[opos++] = '('; - output[opos++] = '7'; - output[opos++] = ')'; - break; - - case '\u2078': - // � [SUPERSCRIPT EIGHT] - case '\u2088': - // ₈ [SUBSCRIPT EIGHT] - case '\u2467': - // â‘§ [CIRCLED DIGIT EIGHT] - case '\u24FC': - // ⓼ [DOUBLE CIRCLED DIGIT EIGHT] - case '\u277D': - // � [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] - case '\u2787': - // ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] - case '\u2791': - // âž‘ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] - case '\uFF18': // 8 [FULLWIDTH DIGIT EIGHT] - output[opos++] = '8'; - break; - - case '\u248F': // â’� [DIGIT EIGHT FULL STOP] - output[opos++] = '8'; - output[opos++] = '.'; - break; - - case '\u247B': // â‘» [PARENTHESIZED DIGIT EIGHT] - output[opos++] = '('; - output[opos++] = '8'; - output[opos++] = ')'; - break; - - case '\u2079': - // � [SUPERSCRIPT NINE] - case '\u2089': - // ₉ [SUBSCRIPT NINE] - case '\u2468': - // ⑨ [CIRCLED DIGIT NINE] - case '\u24FD': - // ⓽ [DOUBLE CIRCLED DIGIT NINE] - case '\u277E': - // � [DINGBAT NEGATIVE CIRCLED DIGIT NINE] - case '\u2788': - // ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] - case '\u2792': - // âž’ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] - case '\uFF19': // ï¼™ [FULLWIDTH DIGIT NINE] - output[opos++] = '9'; - break; - - case '\u2490': // â’� [DIGIT NINE FULL STOP] - output[opos++] = '9'; - output[opos++] = '.'; - break; - - case '\u247C': // ⑼ [PARENTHESIZED DIGIT NINE] - output[opos++] = '('; - output[opos++] = '9'; - output[opos++] = ')'; - break; - - case '\u2469': - // â‘© [CIRCLED NUMBER TEN] - case '\u24FE': - // ⓾ [DOUBLE CIRCLED NUMBER TEN] - case '\u277F': - // � [DINGBAT NEGATIVE CIRCLED NUMBER TEN] - case '\u2789': - // ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] - case '\u2793': // âž“ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] - output[opos++] = '1'; - output[opos++] = '0'; - break; - - case '\u2491': // â’‘ [NUMBER TEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '0'; - output[opos++] = '.'; - break; - - case '\u247D': // ⑽ [PARENTHESIZED NUMBER TEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '0'; - output[opos++] = ')'; - break; - - case '\u246A': - // ⑪ [CIRCLED NUMBER ELEVEN] - case '\u24EB': // â“« [NEGATIVE CIRCLED NUMBER ELEVEN] - output[opos++] = '1'; - output[opos++] = '1'; - break; - - case '\u2492': // â’’ [NUMBER ELEVEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '1'; - output[opos++] = '.'; - break; - - case '\u247E': // ⑾ [PARENTHESIZED NUMBER ELEVEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '1'; - output[opos++] = ')'; - break; - - case '\u246B': - // â‘« [CIRCLED NUMBER TWELVE] - case '\u24EC': // ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] - output[opos++] = '1'; - output[opos++] = '2'; - break; - - case '\u2493': // â’“ [NUMBER TWELVE FULL STOP] - output[opos++] = '1'; - output[opos++] = '2'; - output[opos++] = '.'; - break; - - case '\u247F': // â‘¿ [PARENTHESIZED NUMBER TWELVE] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '2'; - output[opos++] = ')'; - break; - - case '\u246C': - // ⑬ [CIRCLED NUMBER THIRTEEN] - case '\u24ED': // â“­ [NEGATIVE CIRCLED NUMBER THIRTEEN] - output[opos++] = '1'; - output[opos++] = '3'; - break; - - case '\u2494': // â’�? [NUMBER THIRTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '3'; - output[opos++] = '.'; - break; - - case '\u2480': // â’€ [PARENTHESIZED NUMBER THIRTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '3'; - output[opos++] = ')'; - break; - - case '\u246D': - // â‘­ [CIRCLED NUMBER FOURTEEN] - case '\u24EE': // â“® [NEGATIVE CIRCLED NUMBER FOURTEEN] - output[opos++] = '1'; - output[opos++] = '4'; - break; - - case '\u2495': // â’• [NUMBER FOURTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '4'; - output[opos++] = '.'; - break; - - case '\u2481': // â’� [PARENTHESIZED NUMBER FOURTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '4'; - output[opos++] = ')'; - break; - - case '\u246E': - // â‘® [CIRCLED NUMBER FIFTEEN] - case '\u24EF': // ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] - output[opos++] = '1'; - output[opos++] = '5'; - break; - - case '\u2496': // â’– [NUMBER FIFTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '5'; - output[opos++] = '.'; - break; - - case '\u2482': // â’‚ [PARENTHESIZED NUMBER FIFTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '5'; - output[opos++] = ')'; - break; - - case '\u246F': - // ⑯ [CIRCLED NUMBER SIXTEEN] - case '\u24F0': // â“° [NEGATIVE CIRCLED NUMBER SIXTEEN] - output[opos++] = '1'; - output[opos++] = '6'; - break; - - case '\u2497': // â’— [NUMBER SIXTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '6'; - output[opos++] = '.'; - break; - - case '\u2483': // â’ƒ [PARENTHESIZED NUMBER SIXTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '6'; - output[opos++] = ')'; - break; - - case '\u2470': - // â‘° [CIRCLED NUMBER SEVENTEEN] - case '\u24F1': // ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] - output[opos++] = '1'; - output[opos++] = '7'; - break; - - case '\u2498': // â’˜ [NUMBER SEVENTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '7'; - output[opos++] = '.'; - break; - - case '\u2484': // â’„ [PARENTHESIZED NUMBER SEVENTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '7'; - output[opos++] = ')'; - break; - - case '\u2471': - // ⑱ [CIRCLED NUMBER EIGHTEEN] - case '\u24F2': // ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] - output[opos++] = '1'; - output[opos++] = '8'; - break; - - case '\u2499': // â’™ [NUMBER EIGHTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '8'; - output[opos++] = '.'; - break; - - case '\u2485': // â’… [PARENTHESIZED NUMBER EIGHTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '8'; - output[opos++] = ')'; - break; - - case '\u2472': - // ⑲ [CIRCLED NUMBER NINETEEN] - case '\u24F3': // ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] - output[opos++] = '1'; - output[opos++] = '9'; - break; - - case '\u249A': // â’š [NUMBER NINETEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '9'; - output[opos++] = '.'; - break; - - case '\u2486': // â’† [PARENTHESIZED NUMBER NINETEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '9'; - output[opos++] = ')'; - break; - - case '\u2473': - // ⑳ [CIRCLED NUMBER TWENTY] - case '\u24F4': // â“´ [NEGATIVE CIRCLED NUMBER TWENTY] - output[opos++] = '2'; - output[opos++] = '0'; - break; - - case '\u249B': // â’› [NUMBER TWENTY FULL STOP] - output[opos++] = '2'; - output[opos++] = '0'; - output[opos++] = '.'; - break; - - case '\u2487': // â’‡ [PARENTHESIZED NUMBER TWENTY] - output[opos++] = '('; - output[opos++] = '2'; - output[opos++] = '0'; - output[opos++] = ')'; - break; - - case '\u00AB': - // « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] - case '\u00BB': - // » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] - case '\u201C': - // “ [LEFT DOUBLE QUOTATION MARK] - case '\u201D': - // � [RIGHT DOUBLE QUOTATION MARK] - case '\u201E': - // „ [DOUBLE LOW-9 QUOTATION MARK] - case '\u2033': - // ″ [DOUBLE PRIME] - case '\u2036': - // ‶ [REVERSED DOUBLE PRIME] - case '\u275D': - // � [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] - case '\u275E': - // � [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] - case '\u276E': - // � [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] - case '\u276F': - // � [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] - case '\uFF02': // " [FULLWIDTH QUOTATION MARK] - output[opos++] = '"'; - break; - - case '\u2018': - // ‘ [LEFT SINGLE QUOTATION MARK] - case '\u2019': - // ’ [RIGHT SINGLE QUOTATION MARK] - case '\u201A': - // ‚ [SINGLE LOW-9 QUOTATION MARK] - case '\u201B': - // ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] - case '\u2032': - // ′ [PRIME] - case '\u2035': - // ‵ [REVERSED PRIME] - case '\u2039': - // ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] - case '\u203A': - // › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] - case '\u275B': - // � [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] - case '\u275C': - // � [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] - case '\uFF07': // ' [FULLWIDTH APOSTROPHE] - output[opos++] = '\''; - break; - - case '\u2010': - // � [HYPHEN] - case '\u2011': - // ‑ [NON-BREAKING HYPHEN] - case '\u2012': - // ‒ [FIGURE DASH] - case '\u2013': - // – [EN DASH] - case '\u2014': - // �? [EM DASH] - case '\u207B': - // � [SUPERSCRIPT MINUS] - case '\u208B': - // â‚‹ [SUBSCRIPT MINUS] - case '\uFF0D': // � [FULLWIDTH HYPHEN-MINUS] - output[opos++] = '-'; - break; - - case '\u2045': - // � [LEFT SQUARE BRACKET WITH QUILL] - case '\u2772': - // � [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] - case '\uFF3B': // ï¼» [FULLWIDTH LEFT SQUARE BRACKET] - output[opos++] = '['; - break; - - case '\u2046': - // � [RIGHT SQUARE BRACKET WITH QUILL] - case '\u2773': - // � [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] - case '\uFF3D': // ï¼½ [FULLWIDTH RIGHT SQUARE BRACKET] - output[opos++] = ']'; - break; - - case '\u207D': - // � [SUPERSCRIPT LEFT PARENTHESIS] - case '\u208D': - // � [SUBSCRIPT LEFT PARENTHESIS] - case '\u2768': - // � [MEDIUM LEFT PARENTHESIS ORNAMENT] - case '\u276A': - // � [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] - case '\uFF08': // ( [FULLWIDTH LEFT PARENTHESIS] - output[opos++] = '('; - break; - - case '\u2E28': // ⸨ [LEFT DOUBLE PARENTHESIS] - output[opos++] = '('; - output[opos++] = '('; - break; - - case '\u207E': - // � [SUPERSCRIPT RIGHT PARENTHESIS] - case '\u208E': - // ₎ [SUBSCRIPT RIGHT PARENTHESIS] - case '\u2769': - // � [MEDIUM RIGHT PARENTHESIS ORNAMENT] - case '\u276B': - // � [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] - case '\uFF09': // ) [FULLWIDTH RIGHT PARENTHESIS] - output[opos++] = ')'; - break; - - case '\u2E29': // ⸩ [RIGHT DOUBLE PARENTHESIS] - output[opos++] = ')'; - output[opos++] = ')'; - break; - - case '\u276C': - // � [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] - case '\u2770': - // � [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] - case '\uFF1C': // < [FULLWIDTH LESS-THAN SIGN] - output[opos++] = '<'; - break; - - case '\u276D': - // � [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] - case '\u2771': - // � [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] - case '\uFF1E': // > [FULLWIDTH GREATER-THAN SIGN] - output[opos++] = '>'; - break; - - case '\u2774': - // � [MEDIUM LEFT CURLY BRACKET ORNAMENT] - case '\uFF5B': // ï½› [FULLWIDTH LEFT CURLY BRACKET] - output[opos++] = '{'; - break; - - case '\u2775': - // � [MEDIUM RIGHT CURLY BRACKET ORNAMENT] - case '\uFF5D': // � [FULLWIDTH RIGHT CURLY BRACKET] - output[opos++] = '}'; - break; - - case '\u207A': - // � [SUPERSCRIPT PLUS SIGN] - case '\u208A': - // ₊ [SUBSCRIPT PLUS SIGN] - case '\uFF0B': // + [FULLWIDTH PLUS SIGN] - output[opos++] = '+'; - break; - - case '\u207C': - // � [SUPERSCRIPT EQUALS SIGN] - case '\u208C': - // ₌ [SUBSCRIPT EQUALS SIGN] - case '\uFF1D': // � [FULLWIDTH EQUALS SIGN] - output[opos++] = '='; - break; - - case '\uFF01': // � [FULLWIDTH EXCLAMATION MARK] - output[opos++] = '!'; - break; - - case '\u203C': // ‼ [DOUBLE EXCLAMATION MARK] - output[opos++] = '!'; - output[opos++] = '!'; - break; - - case '\u2049': // � [EXCLAMATION QUESTION MARK] - output[opos++] = '!'; - output[opos++] = '?'; - break; - - case '\uFF03': // # [FULLWIDTH NUMBER SIGN] - output[opos++] = '#'; - break; - - case '\uFF04': // $ [FULLWIDTH DOLLAR SIGN] - output[opos++] = '$'; - break; - - case '\u2052': - // � [COMMERCIAL MINUS SIGN] - case '\uFF05': // ï¼… [FULLWIDTH PERCENT SIGN] - output[opos++] = '%'; - break; - - case '\uFF06': // & [FULLWIDTH AMPERSAND] - output[opos++] = '&'; - break; - - case '\u204E': - // � [LOW ASTERISK] - case '\uFF0A': // * [FULLWIDTH ASTERISK] - output[opos++] = '*'; - break; - - case '\uFF0C': // , [FULLWIDTH COMMA] - output[opos++] = ','; - break; - - case '\uFF0E': // . [FULLWIDTH FULL STOP] - output[opos++] = '.'; - break; - - case '\u2044': - // � [FRACTION SLASH] - case '\uFF0F': // � [FULLWIDTH SOLIDUS] - output[opos++] = '/'; - break; - - case '\uFF1A': // : [FULLWIDTH COLON] - output[opos++] = ':'; - break; - - case '\u204F': - // � [REVERSED SEMICOLON] - case '\uFF1B': // ï¼› [FULLWIDTH SEMICOLON] - output[opos++] = ';'; - break; - - case '\uFF1F': // ? [FULLWIDTH QUESTION MARK] - output[opos++] = '?'; - break; - - case '\u2047': // � [DOUBLE QUESTION MARK] - output[opos++] = '?'; - output[opos++] = '?'; - break; - - case '\u2048': // � [QUESTION EXCLAMATION MARK] - output[opos++] = '?'; - output[opos++] = '!'; - break; - - case '\uFF20': // ï¼  [FULLWIDTH COMMERCIAL AT] - output[opos++] = '@'; - break; - - case '\uFF3C': // ï¼¼ [FULLWIDTH REVERSE SOLIDUS] - output[opos++] = '\\'; - break; - - case '\u2038': - // ‸ [CARET] - case '\uFF3E': // ï¼¾ [FULLWIDTH CIRCUMFLEX ACCENT] - output[opos++] = '^'; - break; - - case '\uFF3F': // _ [FULLWIDTH LOW LINE] - output[opos++] = '_'; - break; - - case '\u2053': - // � [SWUNG DASH] - case '\uFF5E': // ~ [FULLWIDTH TILDE] - output[opos++] = '~'; - break; - - // BEGIN CUSTOM TRANSLITERATION OF CYRILIC CHARS - - #region Cyrillic chars - - // russian uppercase "А Б В Г Д Е Ё Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я" - // russian lowercase "а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я" - - // notes - // read http://www.vesic.org/english/blog/c-sharp/transliteration-easy-way-microsoft-transliteration-utility/ - // should we look into MS Transliteration Utility (http://msdn.microsoft.com/en-US/goglobal/bb688104.aspx) - // also UnicodeSharpFork https://bitbucket.org/DimaStefantsov/unidecodesharpfork - // also Transliterator http://transliterator.codeplex.com/ - // - // in any case it would be good to generate all those "case" statements instead of writing them by hand - // time for a T4 template? - // also we should support extensibility so ppl can register more cases in external code - - // TODO: transliterates Анастасия as Anastasiya, and not Anastasia - // Ольга --> Ol'ga, Татьяна --> Tat'yana -- that's bad (?) - // Note: should ä (German umlaut) become a or ae ? - - case '\u0410': // А - output[opos++] = 'A'; - break; - case '\u0430': // а - output[opos++] = 'a'; - break; - case '\u0411': // Б - output[opos++] = 'B'; - break; - case '\u0431': // б - output[opos++] = 'b'; - break; - case '\u0412': // В - output[opos++] = 'V'; - break; - case '\u0432': // в - output[opos++] = 'v'; - break; - case '\u0413': // Г - output[opos++] = 'G'; - break; - case '\u0433': // г - output[opos++] = 'g'; - break; - case '\u0414': // Д - output[opos++] = 'D'; - break; - case '\u0434': // д - output[opos++] = 'd'; - break; - case '\u0415': // Е - output[opos++] = 'E'; - break; - case '\u0435': // е - output[opos++] = 'e'; - break; - case '\u0401': // Ё - output[opos++] = 'E'; // alt. Yo - break; - case '\u0451': // ё - output[opos++] = 'e'; // alt. yo - break; - case '\u0416': // Ж - output[opos++] = 'Z'; - output[opos++] = 'h'; - break; - case '\u0436': // ж - output[opos++] = 'z'; - output[opos++] = 'h'; - break; - case '\u0417': // З - output[opos++] = 'Z'; - break; - case '\u0437': // з - output[opos++] = 'z'; - break; - case '\u0418': // И - output[opos++] = 'I'; - break; - case '\u0438': // и - output[opos++] = 'i'; - break; - case '\u0419': // Й - output[opos++] = 'I'; // alt. Y, J - break; - case '\u0439': // й - output[opos++] = 'i'; // alt. y, j - break; - case '\u041A': // К - output[opos++] = 'K'; - break; - case '\u043A': // к - output[opos++] = 'k'; - break; - case '\u041B': // Л - output[opos++] = 'L'; - break; - case '\u043B': // л - output[opos++] = 'l'; - break; - case '\u041C': // М - output[opos++] = 'M'; - break; - case '\u043C': // м - output[opos++] = 'm'; - break; - case '\u041D': // Н - output[opos++] = 'N'; - break; - case '\u043D': // н - output[opos++] = 'n'; - break; - case '\u041E': // О - output[opos++] = 'O'; - break; - case '\u043E': // о - output[opos++] = 'o'; - break; - case '\u041F': // П - output[opos++] = 'P'; - break; - case '\u043F': // п - output[opos++] = 'p'; - break; - case '\u0420': // Р - output[opos++] = 'R'; - break; - case '\u0440': // р - output[opos++] = 'r'; - break; - case '\u0421': // С - output[opos++] = 'S'; - break; - case '\u0441': // с - output[opos++] = 's'; - break; - case '\u0422': // Т - output[opos++] = 'T'; - break; - case '\u0442': // т - output[opos++] = 't'; - break; - case '\u0423': // У - output[opos++] = 'U'; - break; - case '\u0443': // у - output[opos++] = 'u'; - break; - case '\u0424': // Ф - output[opos++] = 'F'; - break; - case '\u0444': // ф - output[opos++] = 'f'; - break; - case '\u0425': // Х - output[opos++] = 'K'; // alt. X - output[opos++] = 'h'; - break; - case '\u0445': // х - output[opos++] = 'k'; // alt. x - output[opos++] = 'h'; - break; - case '\u0426': // Ц - output[opos++] = 'F'; - break; - case '\u0446': // ц - output[opos++] = 'f'; - break; - case '\u0427': // Ч - output[opos++] = 'C'; // alt. Ts, C - output[opos++] = 'h'; - break; - case '\u0447': // ч - output[opos++] = 'c'; // alt. ts, c - output[opos++] = 'h'; - break; - case '\u0428': // Ш - output[opos++] = 'S'; // alt. Ch, S - output[opos++] = 'h'; - break; - case '\u0448': // ш - output[opos++] = 's'; // alt. ch, s - output[opos++] = 'h'; - break; - case '\u0429': // Щ - output[opos++] = 'S'; // alt. Shch, Sc - output[opos++] = 'h'; - break; - case '\u0449': // щ - output[opos++] = 's'; // alt. shch, sc - output[opos++] = 'h'; - break; - case '\u042A': // Ъ - output[opos++] = '"'; // " - break; - case '\u044A': // ъ - output[opos++] = '"'; // " - break; - case '\u042B': // Ы - output[opos++] = 'Y'; - break; - case '\u044B': // ы - output[opos++] = 'y'; - break; - case '\u042C': // Ь - output[opos++] = '\''; // ' - break; - case '\u044C': // ь - output[opos++] = '\''; // ' - break; - case '\u042D': // Э - output[opos++] = 'E'; - break; - case '\u044D': // э - output[opos++] = 'e'; - break; - case '\u042E': // Ю - output[opos++] = 'Y'; // alt. Ju - output[opos++] = 'u'; - break; - case '\u044E': // ю - output[opos++] = 'y'; // alt. ju - output[opos++] = 'u'; - break; - case '\u042F': // Я - output[opos++] = 'Y'; // alt. Ja - output[opos++] = 'a'; - break; - case '\u044F': // я - output[opos++] = 'y'; // alt. ja - output[opos++] = 'a'; - break; - - #endregion - - // BEGIN EXTRA - /* - case '£': - output[opos++] = 'G'; - output[opos++] = 'B'; - output[opos++] = 'P'; - break; - - case '€': - output[opos++] = 'E'; - output[opos++] = 'U'; - output[opos++] = 'R'; - break; - - case '©': - output[opos++] = '('; - output[opos++] = 'C'; - output[opos++] = ')'; - break; - */ - default: - //if (ToMoreAscii(input, ipos, output, ref opos)) - // break; - - //if (!char.IsLetterOrDigit(c)) // that would not catch eg 汉 unfortunately - // output[opos++] = '?'; - //else - // output[opos++] = c; - - // strict ASCII - output[opos++] = fail; - - break; - } + ToAscii(input, ipos, output, ref opos, fail); } } - //private static bool ToMoreAscii(char[] input, int ipos, char[] output, ref int opos) - //{ - // var c = input[ipos]; - - // switch (c) - // { - // case '£': - // output[opos++] = 'G'; - // output[opos++] = 'B'; - // output[opos++] = 'P'; - // break; - - // case '€': - // output[opos++] = 'E'; - // output[opos++] = 'U'; - // output[opos++] = 'R'; - // break; - - // case '©': - // output[opos++] = '('; - // output[opos++] = 'C'; - // output[opos++] = ')'; - // break; - - // default: - // return false; - // } - - // return true; - //} + return opos; } + + // private static void ToAscii(char[] input, StringBuilder output) + // { + // var chars = new char[5]; + + // for (var ipos = 0; ipos < input.Length; ipos++) + // { + // var opos = 0; + // if (char.IsSurrogate(input[ipos])) + // ipos++; + // else + // { + // ToAscii(input, ipos, chars, ref opos); + // output.Append(chars, 0, opos); + // } + // } + // } + + /// + /// Converts the character at position in input array of Utf8 characters + /// + /// and writes the converted value to output array of Ascii characters at position + /// , + /// and increments that position accordingly. + /// + /// The input array. + /// The input position. + /// The output array. + /// The output position. + /// The character to use to replace characters that cannot properly be converted. + /// + /// Adapted from various sources on the 'net including Lucene.Net.Analysis.ASCIIFoldingFilter. + /// Input should contain Utf8 characters exclusively and NOT Unicode. + /// Removes controls, normalizes whitespaces, replaces symbols by '?'. + /// + private static void ToAscii(char[] input, int ipos, char[] output, ref int opos, char fail = '?') + { + var c = input[ipos]; + + if (char.IsControl(c)) + { + // Control characters are non-printing and formatting characters, such as ACK, BEL, CR, FF, LF, and VT. + // The Unicode standard assigns the following code points to control characters: from \U0000 to \U001F, + // \U007F, and from \U0080 to \U009F. According to the Unicode standard, these values are to be + // interpreted as control characters unless their use is otherwise defined by an application. Valid + // control characters are members of the UnicodeCategory.Control category. + + // we don't want them + } + + // else if (char.IsSeparator(c)) + // { + // // The Unicode standard recognizes three subcategories of separators: + // // - Space separators (the UnicodeCategory.SpaceSeparator category), which includes characters such as \u0020. + // // - Line separators (the UnicodeCategory.LineSeparator category), which includes \u2028. + // // - Paragraph separators (the UnicodeCategory.ParagraphSeparator category), which includes \u2029. + // // + // // Note: The Unicode standard classifies the characters \u000A (LF), \u000C (FF), and \u000A (CR) as control + // // characters (members of the UnicodeCategory.Control category), not as separator characters. + + // // better do it via WhiteSpace + // } + else if (char.IsWhiteSpace(c)) + { + // White space characters are the following Unicode characters: + // - Members of the SpaceSeparator category, which includes the characters SPACE (U+0020), + // OGHAM SPACE MARK (U+1680), MONGOLIAN VOWEL SEPARATOR (U+180E), EN QUAD (U+2000), EM QUAD (U+2001), + // EN SPACE (U+2002), EM SPACE (U+2003), THREE-PER-EM SPACE (U+2004), FOUR-PER-EM SPACE (U+2005), + // SIX-PER-EM SPACE (U+2006), FIGURE SPACE (U+2007), PUNCTUATION SPACE (U+2008), THIN SPACE (U+2009), + // HAIR SPACE (U+200A), NARROW NO-BREAK SPACE (U+202F), MEDIUM MATHEMATICAL SPACE (U+205F), + // and IDEOGRAPHIC SPACE (U+3000). + // - Members of the LineSeparator category, which consists solely of the LINE SEPARATOR character (U+2028). + // - Members of the ParagraphSeparator category, which consists solely of the PARAGRAPH SEPARATOR character (U+2029). + // - The characters CHARACTER TABULATION (U+0009), LINE FEED (U+000A), LINE TABULATION (U+000B), + // FORM FEED (U+000C), CARRIAGE RETURN (U+000D), NEXT LINE (U+0085), and NO-BREAK SPACE (U+00A0). + + // make it a whitespace + output[opos++] = ' '; + } + else if (c < '\u0080') + { + // safe + output[opos++] = c; + } + else + { + switch (c) + { + case '\u00C0': + // À [LATIN CAPITAL LETTER A WITH GRAVE] + case '\u00C1': + // � [LATIN CAPITAL LETTER A WITH ACUTE] + case '\u00C2': + //  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] + case '\u00C3': + // à [LATIN CAPITAL LETTER A WITH TILDE] + case '\u00C4': + // Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] + case '\u00C5': + // Ã… [LATIN CAPITAL LETTER A WITH RING ABOVE] + case '\u0100': + // Ä€ [LATIN CAPITAL LETTER A WITH MACRON] + case '\u0102': + // Ä‚ [LATIN CAPITAL LETTER A WITH BREVE] + case '\u0104': + // Ä„ [LATIN CAPITAL LETTER A WITH OGONEK] + case '\u018F': + // � http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] + case '\u01CD': + // � [LATIN CAPITAL LETTER A WITH CARON] + case '\u01DE': + // Çž [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] + case '\u01E0': + // Ç  [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] + case '\u01FA': + // Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] + case '\u0200': + // È€ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] + case '\u0202': + // È‚ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] + case '\u0226': + // Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] + case '\u023A': + // Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] + case '\u1D00': + // á´€ [LATIN LETTER SMALL CAPITAL A] + case '\u1E00': + // Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] + case '\u1EA0': + // Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] + case '\u1EA2': + // Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] + case '\u1EA4': + // Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] + case '\u1EA6': + // Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] + case '\u1EA8': + // Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EAA': + // Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] + case '\u1EAC': + // Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EAE': + // Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] + case '\u1EB0': + // Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] + case '\u1EB2': + // Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] + case '\u1EB4': + // Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] + case '\u1EB6': + // Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] + case '\u24B6': + // â’¶ [CIRCLED LATIN CAPITAL LETTER A] + case '\uFF21': // A [FULLWIDTH LATIN CAPITAL LETTER A] + output[opos++] = 'A'; + break; + + case '\u00E0': + // à [LATIN SMALL LETTER A WITH GRAVE] + case '\u00E1': + // á [LATIN SMALL LETTER A WITH ACUTE] + case '\u00E2': + // â [LATIN SMALL LETTER A WITH CIRCUMFLEX] + case '\u00E3': + // ã [LATIN SMALL LETTER A WITH TILDE] + case '\u00E4': + // ä [LATIN SMALL LETTER A WITH DIAERESIS] + case '\u00E5': + // Ã¥ [LATIN SMALL LETTER A WITH RING ABOVE] + case '\u0101': + // � [LATIN SMALL LETTER A WITH MACRON] + case '\u0103': + // ă [LATIN SMALL LETTER A WITH BREVE] + case '\u0105': + // Ä… [LATIN SMALL LETTER A WITH OGONEK] + case '\u01CE': + // ÇŽ [LATIN SMALL LETTER A WITH CARON] + case '\u01DF': + // ÇŸ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] + case '\u01E1': + // Ç¡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] + case '\u01FB': + // Ç» [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] + case '\u0201': + // � [LATIN SMALL LETTER A WITH DOUBLE GRAVE] + case '\u0203': + // ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] + case '\u0227': + // ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] + case '\u0250': + // � [LATIN SMALL LETTER TURNED A] + case '\u0259': + // É™ [LATIN SMALL LETTER SCHWA] + case '\u025A': + // Éš [LATIN SMALL LETTER SCHWA WITH HOOK] + case '\u1D8F': + // � [LATIN SMALL LETTER A WITH RETROFLEX HOOK] + case '\u1D95': + // á¶• [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] + case '\u1E01': + // ạ [LATIN SMALL LETTER A WITH RING BELOW] + case '\u1E9A': + // ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] + case '\u1EA1': + // ạ [LATIN SMALL LETTER A WITH DOT BELOW] + case '\u1EA3': + // ả [LATIN SMALL LETTER A WITH HOOK ABOVE] + case '\u1EA5': + // ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] + case '\u1EA7': + // ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] + case '\u1EA9': + // ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EAB': + // ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] + case '\u1EAD': + // ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EAF': + // ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] + case '\u1EB1': + // ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] + case '\u1EB3': + // ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] + case '\u1EB5': + // ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] + case '\u1EB7': + // ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] + case '\u2090': + // � [LATIN SUBSCRIPT SMALL LETTER A] + case '\u2094': + // �? [LATIN SUBSCRIPT SMALL LETTER SCHWA] + case '\u24D0': + // � [CIRCLED LATIN SMALL LETTER A] + case '\u2C65': + // â±¥ [LATIN SMALL LETTER A WITH STROKE] + case '\u2C6F': + // Ɐ [LATIN CAPITAL LETTER TURNED A] + case '\uFF41': // � [FULLWIDTH LATIN SMALL LETTER A] + output[opos++] = 'a'; + break; + + case '\uA732': // Ꜳ [LATIN CAPITAL LETTER AA] + output[opos++] = 'A'; + output[opos++] = 'A'; + break; + + case '\u00C6': + // Æ [LATIN CAPITAL LETTER AE] + case '\u01E2': + // Ç¢ [LATIN CAPITAL LETTER AE WITH MACRON] + case '\u01FC': + // Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] + case '\u1D01': // á´� [LATIN LETTER SMALL CAPITAL AE] + output[opos++] = 'A'; + output[opos++] = 'E'; + break; + + case '\uA734': // Ꜵ [LATIN CAPITAL LETTER AO] + output[opos++] = 'A'; + output[opos++] = 'O'; + break; + + case '\uA736': // Ꜷ [LATIN CAPITAL LETTER AU] + output[opos++] = 'A'; + output[opos++] = 'U'; + break; + + case '\uA738': + // Ꜹ [LATIN CAPITAL LETTER AV] + case '\uA73A': // Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] + output[opos++] = 'A'; + output[opos++] = 'V'; + break; + + case '\uA73C': // Ꜽ [LATIN CAPITAL LETTER AY] + output[opos++] = 'A'; + output[opos++] = 'Y'; + break; + + case '\u249C': // â’œ [PARENTHESIZED LATIN SMALL LETTER A] + output[opos++] = '('; + output[opos++] = 'a'; + output[opos++] = ')'; + break; + + case '\uA733': // ꜳ [LATIN SMALL LETTER AA] + output[opos++] = 'a'; + output[opos++] = 'a'; + break; + + case '\u00E6': + // æ [LATIN SMALL LETTER AE] + case '\u01E3': + // Ç£ [LATIN SMALL LETTER AE WITH MACRON] + case '\u01FD': + // ǽ [LATIN SMALL LETTER AE WITH ACUTE] + case '\u1D02': // á´‚ [LATIN SMALL LETTER TURNED AE] + output[opos++] = 'a'; + output[opos++] = 'e'; + break; + + case '\uA735': // ꜵ [LATIN SMALL LETTER AO] + output[opos++] = 'a'; + output[opos++] = 'o'; + break; + + case '\uA737': // ꜷ [LATIN SMALL LETTER AU] + output[opos++] = 'a'; + output[opos++] = 'u'; + break; + + case '\uA739': + // ꜹ [LATIN SMALL LETTER AV] + case '\uA73B': // ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] + output[opos++] = 'a'; + output[opos++] = 'v'; + break; + + case '\uA73D': // ꜽ [LATIN SMALL LETTER AY] + output[opos++] = 'a'; + output[opos++] = 'y'; + break; + + case '\u0181': + // � [LATIN CAPITAL LETTER B WITH HOOK] + case '\u0182': + // Æ‚ [LATIN CAPITAL LETTER B WITH TOPBAR] + case '\u0243': + // Ƀ [LATIN CAPITAL LETTER B WITH STROKE] + case '\u0299': + // Ê™ [LATIN LETTER SMALL CAPITAL B] + case '\u1D03': + // á´ƒ [LATIN LETTER SMALL CAPITAL BARRED B] + case '\u1E02': + // Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] + case '\u1E04': + // Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] + case '\u1E06': + // Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] + case '\u24B7': + // â’· [CIRCLED LATIN CAPITAL LETTER B] + case '\uFF22': // ï¼¢ [FULLWIDTH LATIN CAPITAL LETTER B] + output[opos++] = 'B'; + break; + + case '\u0180': + // Æ€ [LATIN SMALL LETTER B WITH STROKE] + case '\u0183': + // ƃ [LATIN SMALL LETTER B WITH TOPBAR] + case '\u0253': + // É“ [LATIN SMALL LETTER B WITH HOOK] + case '\u1D6C': + // ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] + case '\u1D80': + // á¶€ [LATIN SMALL LETTER B WITH PALATAL HOOK] + case '\u1E03': + // ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] + case '\u1E05': + // ḅ [LATIN SMALL LETTER B WITH DOT BELOW] + case '\u1E07': + // ḇ [LATIN SMALL LETTER B WITH LINE BELOW] + case '\u24D1': + // â“‘ [CIRCLED LATIN SMALL LETTER B] + case '\uFF42': // b [FULLWIDTH LATIN SMALL LETTER B] + output[opos++] = 'b'; + break; + + case '\u249D': // â’� [PARENTHESIZED LATIN SMALL LETTER B] + output[opos++] = '('; + output[opos++] = 'b'; + output[opos++] = ')'; + break; + + case '\u00C7': + // Ç [LATIN CAPITAL LETTER C WITH CEDILLA] + case '\u0106': + // Ć [LATIN CAPITAL LETTER C WITH ACUTE] + case '\u0108': + // Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] + case '\u010A': + // ÄŠ [LATIN CAPITAL LETTER C WITH DOT ABOVE] + case '\u010C': + // ÄŒ [LATIN CAPITAL LETTER C WITH CARON] + case '\u0187': + // Ƈ [LATIN CAPITAL LETTER C WITH HOOK] + case '\u023B': + // È» [LATIN CAPITAL LETTER C WITH STROKE] + case '\u0297': + // Ê— [LATIN LETTER STRETCHED C] + case '\u1D04': + // á´„ [LATIN LETTER SMALL CAPITAL C] + case '\u1E08': + // Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] + case '\u24B8': + // â’¸ [CIRCLED LATIN CAPITAL LETTER C] + case '\uFF23': // ï¼£ [FULLWIDTH LATIN CAPITAL LETTER C] + output[opos++] = 'C'; + break; + + case '\u00E7': + // ç [LATIN SMALL LETTER C WITH CEDILLA] + case '\u0107': + // ć [LATIN SMALL LETTER C WITH ACUTE] + case '\u0109': + // ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] + case '\u010B': + // Ä‹ [LATIN SMALL LETTER C WITH DOT ABOVE] + case '\u010D': + // � [LATIN SMALL LETTER C WITH CARON] + case '\u0188': + // ƈ [LATIN SMALL LETTER C WITH HOOK] + case '\u023C': + // ȼ [LATIN SMALL LETTER C WITH STROKE] + case '\u0255': + // É• [LATIN SMALL LETTER C WITH CURL] + case '\u1E09': + // ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] + case '\u2184': + // ↄ [LATIN SMALL LETTER REVERSED C] + case '\u24D2': + // â“’ [CIRCLED LATIN SMALL LETTER C] + case '\uA73E': + // Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] + case '\uA73F': + // ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] + case '\uFF43': // c [FULLWIDTH LATIN SMALL LETTER C] + output[opos++] = 'c'; + break; + + case '\u249E': // â’ž [PARENTHESIZED LATIN SMALL LETTER C] + output[opos++] = '('; + output[opos++] = 'c'; + output[opos++] = ')'; + break; + + case '\u00D0': + // � [LATIN CAPITAL LETTER ETH] + case '\u010E': + // ÄŽ [LATIN CAPITAL LETTER D WITH CARON] + case '\u0110': + // � [LATIN CAPITAL LETTER D WITH STROKE] + case '\u0189': + // Ɖ [LATIN CAPITAL LETTER AFRICAN D] + case '\u018A': + // ÆŠ [LATIN CAPITAL LETTER D WITH HOOK] + case '\u018B': + // Æ‹ [LATIN CAPITAL LETTER D WITH TOPBAR] + case '\u1D05': + // á´… [LATIN LETTER SMALL CAPITAL D] + case '\u1D06': + // á´† [LATIN LETTER SMALL CAPITAL ETH] + case '\u1E0A': + // Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] + case '\u1E0C': + // Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] + case '\u1E0E': + // Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] + case '\u1E10': + // � [LATIN CAPITAL LETTER D WITH CEDILLA] + case '\u1E12': + // Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] + case '\u24B9': + // â’¹ [CIRCLED LATIN CAPITAL LETTER D] + case '\uA779': + // � [LATIN CAPITAL LETTER INSULAR D] + case '\uFF24': // D [FULLWIDTH LATIN CAPITAL LETTER D] + output[opos++] = 'D'; + break; + + case '\u00F0': + // ð [LATIN SMALL LETTER ETH] + case '\u010F': + // � [LATIN SMALL LETTER D WITH CARON] + case '\u0111': + // Ä‘ [LATIN SMALL LETTER D WITH STROKE] + case '\u018C': + // ÆŒ [LATIN SMALL LETTER D WITH TOPBAR] + case '\u0221': + // È¡ [LATIN SMALL LETTER D WITH CURL] + case '\u0256': + // É– [LATIN SMALL LETTER D WITH TAIL] + case '\u0257': + // É— [LATIN SMALL LETTER D WITH HOOK] + case '\u1D6D': + // áµ­ [LATIN SMALL LETTER D WITH MIDDLE TILDE] + case '\u1D81': + // � [LATIN SMALL LETTER D WITH PALATAL HOOK] + case '\u1D91': + // á¶‘ [LATIN SMALL LETTER D WITH HOOK AND TAIL] + case '\u1E0B': + // ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] + case '\u1E0D': + // � [LATIN SMALL LETTER D WITH DOT BELOW] + case '\u1E0F': + // � [LATIN SMALL LETTER D WITH LINE BELOW] + case '\u1E11': + // ḑ [LATIN SMALL LETTER D WITH CEDILLA] + case '\u1E13': + // ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] + case '\u24D3': + // â““ [CIRCLED LATIN SMALL LETTER D] + case '\uA77A': + // � [LATIN SMALL LETTER INSULAR D] + case '\uFF44': // d [FULLWIDTH LATIN SMALL LETTER D] + output[opos++] = 'd'; + break; + + case '\u01C4': + // Ç„ [LATIN CAPITAL LETTER DZ WITH CARON] + case '\u01F1': // DZ [LATIN CAPITAL LETTER DZ] + output[opos++] = 'D'; + output[opos++] = 'Z'; + break; + + case '\u01C5': + // Ç… [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] + case '\u01F2': // Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] + output[opos++] = 'D'; + output[opos++] = 'z'; + break; + + case '\u249F': // â’Ÿ [PARENTHESIZED LATIN SMALL LETTER D] + output[opos++] = '('; + output[opos++] = 'd'; + output[opos++] = ')'; + break; + + case '\u0238': // ȸ [LATIN SMALL LETTER DB DIGRAPH] + output[opos++] = 'd'; + output[opos++] = 'b'; + break; + + case '\u01C6': + // dž [LATIN SMALL LETTER DZ WITH CARON] + case '\u01F3': + // dz [LATIN SMALL LETTER DZ] + case '\u02A3': + // Ê£ [LATIN SMALL LETTER DZ DIGRAPH] + case '\u02A5': // Ê¥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] + output[opos++] = 'd'; + output[opos++] = 'z'; + break; + + case '\u00C8': + // È [LATIN CAPITAL LETTER E WITH GRAVE] + case '\u00C9': + // É [LATIN CAPITAL LETTER E WITH ACUTE] + case '\u00CA': + // Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] + case '\u00CB': + // Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] + case '\u0112': + // Ä’ [LATIN CAPITAL LETTER E WITH MACRON] + case '\u0114': + // �? [LATIN CAPITAL LETTER E WITH BREVE] + case '\u0116': + // Ä– [LATIN CAPITAL LETTER E WITH DOT ABOVE] + case '\u0118': + // Ę [LATIN CAPITAL LETTER E WITH OGONEK] + case '\u011A': + // Äš [LATIN CAPITAL LETTER E WITH CARON] + case '\u018E': + // ÆŽ [LATIN CAPITAL LETTER REVERSED E] + case '\u0190': + // � [LATIN CAPITAL LETTER OPEN E] + case '\u0204': + // È„ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] + case '\u0206': + // Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] + case '\u0228': + // Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] + case '\u0246': + // Ɇ [LATIN CAPITAL LETTER E WITH STROKE] + case '\u1D07': + // á´‡ [LATIN LETTER SMALL CAPITAL E] + case '\u1E14': + // �? [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] + case '\u1E16': + // Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] + case '\u1E18': + // Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] + case '\u1E1A': + // Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] + case '\u1E1C': + // Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] + case '\u1EB8': + // Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] + case '\u1EBA': + // Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] + case '\u1EBC': + // Ẽ [LATIN CAPITAL LETTER E WITH TILDE] + case '\u1EBE': + // Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] + case '\u1EC0': + // Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] + case '\u1EC2': + // Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EC4': + // Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] + case '\u1EC6': + // Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + case '\u24BA': + // â’º [CIRCLED LATIN CAPITAL LETTER E] + case '\u2C7B': + // â±» [LATIN LETTER SMALL CAPITAL TURNED E] + case '\uFF25': // ï¼¥ [FULLWIDTH LATIN CAPITAL LETTER E] + output[opos++] = 'E'; + break; + + case '\u00E8': + // è [LATIN SMALL LETTER E WITH GRAVE] + case '\u00E9': + // é [LATIN SMALL LETTER E WITH ACUTE] + case '\u00EA': + // ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] + case '\u00EB': + // ë [LATIN SMALL LETTER E WITH DIAERESIS] + case '\u0113': + // Ä“ [LATIN SMALL LETTER E WITH MACRON] + case '\u0115': + // Ä• [LATIN SMALL LETTER E WITH BREVE] + case '\u0117': + // Ä— [LATIN SMALL LETTER E WITH DOT ABOVE] + case '\u0119': + // Ä™ [LATIN SMALL LETTER E WITH OGONEK] + case '\u011B': + // Ä› [LATIN SMALL LETTER E WITH CARON] + case '\u01DD': + // � [LATIN SMALL LETTER TURNED E] + case '\u0205': + // È… [LATIN SMALL LETTER E WITH DOUBLE GRAVE] + case '\u0207': + // ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] + case '\u0229': + // È© [LATIN SMALL LETTER E WITH CEDILLA] + case '\u0247': + // ɇ [LATIN SMALL LETTER E WITH STROKE] + case '\u0258': + // ɘ [LATIN SMALL LETTER REVERSED E] + case '\u025B': + // É› [LATIN SMALL LETTER OPEN E] + case '\u025C': + // Éœ [LATIN SMALL LETTER REVERSED OPEN E] + case '\u025D': + // � [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] + case '\u025E': + // Éž [LATIN SMALL LETTER CLOSED REVERSED OPEN E] + case '\u029A': + // Êš [LATIN SMALL LETTER CLOSED OPEN E] + case '\u1D08': + // á´ˆ [LATIN SMALL LETTER TURNED OPEN E] + case '\u1D92': + // á¶’ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] + case '\u1D93': + // á¶“ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] + case '\u1D94': + // �? [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] + case '\u1E15': + // ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] + case '\u1E17': + // ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] + case '\u1E19': + // ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] + case '\u1E1B': + // ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] + case '\u1E1D': + // � [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] + case '\u1EB9': + // ẹ [LATIN SMALL LETTER E WITH DOT BELOW] + case '\u1EBB': + // ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] + case '\u1EBD': + // ẽ [LATIN SMALL LETTER E WITH TILDE] + case '\u1EBF': + // ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] + case '\u1EC1': + // � [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] + case '\u1EC3': + // ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EC5': + // á»… [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] + case '\u1EC7': + // ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + case '\u2091': + // â‚‘ [LATIN SUBSCRIPT SMALL LETTER E] + case '\u24D4': + // �? [CIRCLED LATIN SMALL LETTER E] + case '\u2C78': + // ⱸ [LATIN SMALL LETTER E WITH NOTCH] + case '\uFF45': // ï½… [FULLWIDTH LATIN SMALL LETTER E] + output[opos++] = 'e'; + break; + + case '\u24A0': // â’  [PARENTHESIZED LATIN SMALL LETTER E] + output[opos++] = '('; + output[opos++] = 'e'; + output[opos++] = ')'; + break; + + case '\u0191': + // Æ‘ [LATIN CAPITAL LETTER F WITH HOOK] + case '\u1E1E': + // Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] + case '\u24BB': + // â’» [CIRCLED LATIN CAPITAL LETTER F] + case '\uA730': + // ꜰ [LATIN LETTER SMALL CAPITAL F] + case '\uA77B': + // � [LATIN CAPITAL LETTER INSULAR F] + case '\uA7FB': + // ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] + case '\uFF26': // F [FULLWIDTH LATIN CAPITAL LETTER F] + output[opos++] = 'F'; + break; + + case '\u0192': + // Æ’ [LATIN SMALL LETTER F WITH HOOK] + case '\u1D6E': + // áµ® [LATIN SMALL LETTER F WITH MIDDLE TILDE] + case '\u1D82': + // á¶‚ [LATIN SMALL LETTER F WITH PALATAL HOOK] + case '\u1E1F': + // ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] + case '\u1E9B': + // ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] + case '\u24D5': + // â“• [CIRCLED LATIN SMALL LETTER F] + case '\uA77C': + // � [LATIN SMALL LETTER INSULAR F] + case '\uFF46': // f [FULLWIDTH LATIN SMALL LETTER F] + output[opos++] = 'f'; + break; + + case '\u24A1': // â’¡ [PARENTHESIZED LATIN SMALL LETTER F] + output[opos++] = '('; + output[opos++] = 'f'; + output[opos++] = ')'; + break; + + case '\uFB00': // ff [LATIN SMALL LIGATURE FF] + output[opos++] = 'f'; + output[opos++] = 'f'; + break; + + case '\uFB03': // ffi [LATIN SMALL LIGATURE FFI] + output[opos++] = 'f'; + output[opos++] = 'f'; + output[opos++] = 'i'; + break; + + case '\uFB04': // ffl [LATIN SMALL LIGATURE FFL] + output[opos++] = 'f'; + output[opos++] = 'f'; + output[opos++] = 'l'; + break; + + case '\uFB01': // � [LATIN SMALL LIGATURE FI] + output[opos++] = 'f'; + output[opos++] = 'i'; + break; + + case '\uFB02': // fl [LATIN SMALL LIGATURE FL] + output[opos++] = 'f'; + output[opos++] = 'l'; + break; + + case '\u011C': + // Äœ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] + case '\u011E': + // Äž [LATIN CAPITAL LETTER G WITH BREVE] + case '\u0120': + // Ä  [LATIN CAPITAL LETTER G WITH DOT ABOVE] + case '\u0122': + // Ä¢ [LATIN CAPITAL LETTER G WITH CEDILLA] + case '\u0193': + // Æ“ [LATIN CAPITAL LETTER G WITH HOOK] + case '\u01E4': + // Ǥ [LATIN CAPITAL LETTER G WITH STROKE] + case '\u01E5': + // Ç¥ [LATIN SMALL LETTER G WITH STROKE] + case '\u01E6': + // Ǧ [LATIN CAPITAL LETTER G WITH CARON] + case '\u01E7': + // ǧ [LATIN SMALL LETTER G WITH CARON] + case '\u01F4': + // Ç´ [LATIN CAPITAL LETTER G WITH ACUTE] + case '\u0262': + // É¢ [LATIN LETTER SMALL CAPITAL G] + case '\u029B': + // Ê› [LATIN LETTER SMALL CAPITAL G WITH HOOK] + case '\u1E20': + // Ḡ [LATIN CAPITAL LETTER G WITH MACRON] + case '\u24BC': + // â’¼ [CIRCLED LATIN CAPITAL LETTER G] + case '\uA77D': + // � [LATIN CAPITAL LETTER INSULAR G] + case '\uA77E': + // � [LATIN CAPITAL LETTER TURNED INSULAR G] + case '\uFF27': // ï¼§ [FULLWIDTH LATIN CAPITAL LETTER G] + output[opos++] = 'G'; + break; + + case '\u011D': + // � [LATIN SMALL LETTER G WITH CIRCUMFLEX] + case '\u011F': + // ÄŸ [LATIN SMALL LETTER G WITH BREVE] + case '\u0121': + // Ä¡ [LATIN SMALL LETTER G WITH DOT ABOVE] + case '\u0123': + // Ä£ [LATIN SMALL LETTER G WITH CEDILLA] + case '\u01F5': + // ǵ [LATIN SMALL LETTER G WITH ACUTE] + case '\u0260': + // É  [LATIN SMALL LETTER G WITH HOOK] + case '\u0261': + // É¡ [LATIN SMALL LETTER SCRIPT G] + case '\u1D77': + // áµ· [LATIN SMALL LETTER TURNED G] + case '\u1D79': + // áµ¹ [LATIN SMALL LETTER INSULAR G] + case '\u1D83': + // ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] + case '\u1E21': + // ḡ [LATIN SMALL LETTER G WITH MACRON] + case '\u24D6': + // â“– [CIRCLED LATIN SMALL LETTER G] + case '\uA77F': + // � [LATIN SMALL LETTER TURNED INSULAR G] + case '\uFF47': // g [FULLWIDTH LATIN SMALL LETTER G] + output[opos++] = 'g'; + break; + + case '\u24A2': // â’¢ [PARENTHESIZED LATIN SMALL LETTER G] + output[opos++] = '('; + output[opos++] = 'g'; + output[opos++] = ')'; + break; + + case '\u0124': + // Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] + case '\u0126': + // Ħ [LATIN CAPITAL LETTER H WITH STROKE] + case '\u021E': + // Èž [LATIN CAPITAL LETTER H WITH CARON] + case '\u029C': + // Êœ [LATIN LETTER SMALL CAPITAL H] + case '\u1E22': + // Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] + case '\u1E24': + // Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] + case '\u1E26': + // Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] + case '\u1E28': + // Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] + case '\u1E2A': + // Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] + case '\u24BD': + // â’½ [CIRCLED LATIN CAPITAL LETTER H] + case '\u2C67': + // â±§ [LATIN CAPITAL LETTER H WITH DESCENDER] + case '\u2C75': + // â±µ [LATIN CAPITAL LETTER HALF H] + case '\uFF28': // H [FULLWIDTH LATIN CAPITAL LETTER H] + output[opos++] = 'H'; + break; + + case '\u0125': + // Ä¥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] + case '\u0127': + // ħ [LATIN SMALL LETTER H WITH STROKE] + case '\u021F': + // ÈŸ [LATIN SMALL LETTER H WITH CARON] + case '\u0265': + // É¥ [LATIN SMALL LETTER TURNED H] + case '\u0266': + // ɦ [LATIN SMALL LETTER H WITH HOOK] + case '\u02AE': + // Ê® [LATIN SMALL LETTER TURNED H WITH FISHHOOK] + case '\u02AF': + // ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] + case '\u1E23': + // ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] + case '\u1E25': + // ḥ [LATIN SMALL LETTER H WITH DOT BELOW] + case '\u1E27': + // ḧ [LATIN SMALL LETTER H WITH DIAERESIS] + case '\u1E29': + // ḩ [LATIN SMALL LETTER H WITH CEDILLA] + case '\u1E2B': + // ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] + case '\u1E96': + // ẖ [LATIN SMALL LETTER H WITH LINE BELOW] + case '\u24D7': + // â“— [CIRCLED LATIN SMALL LETTER H] + case '\u2C68': + // ⱨ [LATIN SMALL LETTER H WITH DESCENDER] + case '\u2C76': + // â±¶ [LATIN SMALL LETTER HALF H] + case '\uFF48': // h [FULLWIDTH LATIN SMALL LETTER H] + output[opos++] = 'h'; + break; + + case '\u01F6': // Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] + output[opos++] = 'H'; + output[opos++] = 'V'; + break; + + case '\u24A3': // â’£ [PARENTHESIZED LATIN SMALL LETTER H] + output[opos++] = '('; + output[opos++] = 'h'; + output[opos++] = ')'; + break; + + case '\u0195': // Æ• [LATIN SMALL LETTER HV] + output[opos++] = 'h'; + output[opos++] = 'v'; + break; + + case '\u00CC': + // ÃŒ [LATIN CAPITAL LETTER I WITH GRAVE] + case '\u00CD': + // � [LATIN CAPITAL LETTER I WITH ACUTE] + case '\u00CE': + // ÃŽ [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] + case '\u00CF': + // � [LATIN CAPITAL LETTER I WITH DIAERESIS] + case '\u0128': + // Ĩ [LATIN CAPITAL LETTER I WITH TILDE] + case '\u012A': + // Ī [LATIN CAPITAL LETTER I WITH MACRON] + case '\u012C': + // Ĭ [LATIN CAPITAL LETTER I WITH BREVE] + case '\u012E': + // Ä® [LATIN CAPITAL LETTER I WITH OGONEK] + case '\u0130': + // İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] + case '\u0196': + // Æ– [LATIN CAPITAL LETTER IOTA] + case '\u0197': + // Æ— [LATIN CAPITAL LETTER I WITH STROKE] + case '\u01CF': + // � [LATIN CAPITAL LETTER I WITH CARON] + case '\u0208': + // Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] + case '\u020A': + // ÈŠ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] + case '\u026A': + // ɪ [LATIN LETTER SMALL CAPITAL I] + case '\u1D7B': + // áµ» [LATIN SMALL CAPITAL LETTER I WITH STROKE] + case '\u1E2C': + // Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] + case '\u1E2E': + // Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] + case '\u1EC8': + // Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] + case '\u1ECA': + // Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] + case '\u24BE': + // â’¾ [CIRCLED LATIN CAPITAL LETTER I] + case '\uA7FE': + // ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] + case '\uFF29': // I [FULLWIDTH LATIN CAPITAL LETTER I] + output[opos++] = 'I'; + break; + + case '\u00EC': + // ì [LATIN SMALL LETTER I WITH GRAVE] + case '\u00ED': + // í [LATIN SMALL LETTER I WITH ACUTE] + case '\u00EE': + // î [LATIN SMALL LETTER I WITH CIRCUMFLEX] + case '\u00EF': + // ï [LATIN SMALL LETTER I WITH DIAERESIS] + case '\u0129': + // Ä© [LATIN SMALL LETTER I WITH TILDE] + case '\u012B': + // Ä« [LATIN SMALL LETTER I WITH MACRON] + case '\u012D': + // Ä­ [LATIN SMALL LETTER I WITH BREVE] + case '\u012F': + // į [LATIN SMALL LETTER I WITH OGONEK] + case '\u0131': + // ı [LATIN SMALL LETTER DOTLESS I] + case '\u01D0': + // � [LATIN SMALL LETTER I WITH CARON] + case '\u0209': + // ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] + case '\u020B': + // È‹ [LATIN SMALL LETTER I WITH INVERTED BREVE] + case '\u0268': + // ɨ [LATIN SMALL LETTER I WITH STROKE] + case '\u1D09': + // á´‰ [LATIN SMALL LETTER TURNED I] + case '\u1D62': + // áµ¢ [LATIN SUBSCRIPT SMALL LETTER I] + case '\u1D7C': + // áµ¼ [LATIN SMALL LETTER IOTA WITH STROKE] + case '\u1D96': + // á¶– [LATIN SMALL LETTER I WITH RETROFLEX HOOK] + case '\u1E2D': + // ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] + case '\u1E2F': + // ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] + case '\u1EC9': + // ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] + case '\u1ECB': + // ị [LATIN SMALL LETTER I WITH DOT BELOW] + case '\u2071': + // � [SUPERSCRIPT LATIN SMALL LETTER I] + case '\u24D8': + // ⓘ [CIRCLED LATIN SMALL LETTER I] + case '\uFF49': // i [FULLWIDTH LATIN SMALL LETTER I] + output[opos++] = 'i'; + break; + + case '\u0132': // IJ [LATIN CAPITAL LIGATURE IJ] + output[opos++] = 'I'; + output[opos++] = 'J'; + break; + + case '\u24A4': // â’¤ [PARENTHESIZED LATIN SMALL LETTER I] + output[opos++] = '('; + output[opos++] = 'i'; + output[opos++] = ')'; + break; + + case '\u0133': // ij [LATIN SMALL LIGATURE IJ] + output[opos++] = 'i'; + output[opos++] = 'j'; + break; + + case '\u0134': + // Ä´ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] + case '\u0248': + // Ɉ [LATIN CAPITAL LETTER J WITH STROKE] + case '\u1D0A': + // á´Š [LATIN LETTER SMALL CAPITAL J] + case '\u24BF': + // â’¿ [CIRCLED LATIN CAPITAL LETTER J] + case '\uFF2A': // J [FULLWIDTH LATIN CAPITAL LETTER J] + output[opos++] = 'J'; + break; + + case '\u0135': + // ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] + case '\u01F0': + // ǰ [LATIN SMALL LETTER J WITH CARON] + case '\u0237': + // È· [LATIN SMALL LETTER DOTLESS J] + case '\u0249': + // ɉ [LATIN SMALL LETTER J WITH STROKE] + case '\u025F': + // ÉŸ [LATIN SMALL LETTER DOTLESS J WITH STROKE] + case '\u0284': + // Ê„ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] + case '\u029D': + // � [LATIN SMALL LETTER J WITH CROSSED-TAIL] + case '\u24D9': + // â“™ [CIRCLED LATIN SMALL LETTER J] + case '\u2C7C': + // â±¼ [LATIN SUBSCRIPT SMALL LETTER J] + case '\uFF4A': // j [FULLWIDTH LATIN SMALL LETTER J] + output[opos++] = 'j'; + break; + + case '\u24A5': // â’¥ [PARENTHESIZED LATIN SMALL LETTER J] + output[opos++] = '('; + output[opos++] = 'j'; + output[opos++] = ')'; + break; + + case '\u0136': + // Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] + case '\u0198': + // Ƙ [LATIN CAPITAL LETTER K WITH HOOK] + case '\u01E8': + // Ǩ [LATIN CAPITAL LETTER K WITH CARON] + case '\u1D0B': + // á´‹ [LATIN LETTER SMALL CAPITAL K] + case '\u1E30': + // Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] + case '\u1E32': + // Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] + case '\u1E34': + // Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] + case '\u24C0': + // â“€ [CIRCLED LATIN CAPITAL LETTER K] + case '\u2C69': + // Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] + case '\uA740': + // � [LATIN CAPITAL LETTER K WITH STROKE] + case '\uA742': + // � [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] + case '\uA744': + // � [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] + case '\uFF2B': // K [FULLWIDTH LATIN CAPITAL LETTER K] + output[opos++] = 'K'; + break; + + case '\u0137': + // Ä· [LATIN SMALL LETTER K WITH CEDILLA] + case '\u0199': + // Æ™ [LATIN SMALL LETTER K WITH HOOK] + case '\u01E9': + // Ç© [LATIN SMALL LETTER K WITH CARON] + case '\u029E': + // Êž [LATIN SMALL LETTER TURNED K] + case '\u1D84': + // á¶„ [LATIN SMALL LETTER K WITH PALATAL HOOK] + case '\u1E31': + // ḱ [LATIN SMALL LETTER K WITH ACUTE] + case '\u1E33': + // ḳ [LATIN SMALL LETTER K WITH DOT BELOW] + case '\u1E35': + // ḵ [LATIN SMALL LETTER K WITH LINE BELOW] + case '\u24DA': + // ⓚ [CIRCLED LATIN SMALL LETTER K] + case '\u2C6A': + // ⱪ [LATIN SMALL LETTER K WITH DESCENDER] + case '\uA741': + // � [LATIN SMALL LETTER K WITH STROKE] + case '\uA743': + // � [LATIN SMALL LETTER K WITH DIAGONAL STROKE] + case '\uA745': + // � [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] + case '\uFF4B': // k [FULLWIDTH LATIN SMALL LETTER K] + output[opos++] = 'k'; + break; + + case '\u24A6': // â’¦ [PARENTHESIZED LATIN SMALL LETTER K] + output[opos++] = '('; + output[opos++] = 'k'; + output[opos++] = ')'; + break; + + case '\u0139': + // Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] + case '\u013B': + // Ä» [LATIN CAPITAL LETTER L WITH CEDILLA] + case '\u013D': + // Ľ [LATIN CAPITAL LETTER L WITH CARON] + case '\u013F': + // Ä¿ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] + case '\u0141': + // � [LATIN CAPITAL LETTER L WITH STROKE] + case '\u023D': + // Ƚ [LATIN CAPITAL LETTER L WITH BAR] + case '\u029F': + // ÊŸ [LATIN LETTER SMALL CAPITAL L] + case '\u1D0C': + // á´Œ [LATIN LETTER SMALL CAPITAL L WITH STROKE] + case '\u1E36': + // Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] + case '\u1E38': + // Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] + case '\u1E3A': + // Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] + case '\u1E3C': + // Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] + case '\u24C1': + // � [CIRCLED LATIN CAPITAL LETTER L] + case '\u2C60': + // â±  [LATIN CAPITAL LETTER L WITH DOUBLE BAR] + case '\u2C62': + // â±¢ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] + case '\uA746': + // � [LATIN CAPITAL LETTER BROKEN L] + case '\uA748': + // � [LATIN CAPITAL LETTER L WITH HIGH STROKE] + case '\uA780': + // Ꞁ [LATIN CAPITAL LETTER TURNED L] + case '\uFF2C': // L [FULLWIDTH LATIN CAPITAL LETTER L] + output[opos++] = 'L'; + break; + + case '\u013A': + // ĺ [LATIN SMALL LETTER L WITH ACUTE] + case '\u013C': + // ļ [LATIN SMALL LETTER L WITH CEDILLA] + case '\u013E': + // ľ [LATIN SMALL LETTER L WITH CARON] + case '\u0140': + // Å€ [LATIN SMALL LETTER L WITH MIDDLE DOT] + case '\u0142': + // Å‚ [LATIN SMALL LETTER L WITH STROKE] + case '\u019A': + // Æš [LATIN SMALL LETTER L WITH BAR] + case '\u0234': + // È´ [LATIN SMALL LETTER L WITH CURL] + case '\u026B': + // É« [LATIN SMALL LETTER L WITH MIDDLE TILDE] + case '\u026C': + // ɬ [LATIN SMALL LETTER L WITH BELT] + case '\u026D': + // É­ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] + case '\u1D85': + // á¶… [LATIN SMALL LETTER L WITH PALATAL HOOK] + case '\u1E37': + // ḷ [LATIN SMALL LETTER L WITH DOT BELOW] + case '\u1E39': + // ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] + case '\u1E3B': + // ḻ [LATIN SMALL LETTER L WITH LINE BELOW] + case '\u1E3D': + // ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] + case '\u24DB': + // â“› [CIRCLED LATIN SMALL LETTER L] + case '\u2C61': + // ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] + case '\uA747': + // � [LATIN SMALL LETTER BROKEN L] + case '\uA749': + // � [LATIN SMALL LETTER L WITH HIGH STROKE] + case '\uA781': + // � [LATIN SMALL LETTER TURNED L] + case '\uFF4C': // l [FULLWIDTH LATIN SMALL LETTER L] + output[opos++] = 'l'; + break; + + case '\u01C7': // LJ [LATIN CAPITAL LETTER LJ] + output[opos++] = 'L'; + output[opos++] = 'J'; + break; + + case '\u1EFA': // Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] + output[opos++] = 'L'; + output[opos++] = 'L'; + break; + + case '\u01C8': // Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] + output[opos++] = 'L'; + output[opos++] = 'j'; + break; + + case '\u24A7': // â’§ [PARENTHESIZED LATIN SMALL LETTER L] + output[opos++] = '('; + output[opos++] = 'l'; + output[opos++] = ')'; + break; + + case '\u01C9': // lj [LATIN SMALL LETTER LJ] + output[opos++] = 'l'; + output[opos++] = 'j'; + break; + + case '\u1EFB': // á»» [LATIN SMALL LETTER MIDDLE-WELSH LL] + output[opos++] = 'l'; + output[opos++] = 'l'; + break; + + case '\u02AA': // ʪ [LATIN SMALL LETTER LS DIGRAPH] + output[opos++] = 'l'; + output[opos++] = 's'; + break; + + case '\u02AB': // Ê« [LATIN SMALL LETTER LZ DIGRAPH] + output[opos++] = 'l'; + output[opos++] = 'z'; + break; + + case '\u019C': + // Æœ [LATIN CAPITAL LETTER TURNED M] + case '\u1D0D': + // á´� [LATIN LETTER SMALL CAPITAL M] + case '\u1E3E': + // Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] + case '\u1E40': + // á¹€ [LATIN CAPITAL LETTER M WITH DOT ABOVE] + case '\u1E42': + // Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] + case '\u24C2': + // â“‚ [CIRCLED LATIN CAPITAL LETTER M] + case '\u2C6E': + // â±® [LATIN CAPITAL LETTER M WITH HOOK] + case '\uA7FD': + // ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] + case '\uA7FF': + // ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] + case '\uFF2D': // ï¼­ [FULLWIDTH LATIN CAPITAL LETTER M] + output[opos++] = 'M'; + break; + + case '\u026F': + // ɯ [LATIN SMALL LETTER TURNED M] + case '\u0270': + // ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] + case '\u0271': + // ɱ [LATIN SMALL LETTER M WITH HOOK] + case '\u1D6F': + // ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] + case '\u1D86': + // ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] + case '\u1E3F': + // ḿ [LATIN SMALL LETTER M WITH ACUTE] + case '\u1E41': + // � [LATIN SMALL LETTER M WITH DOT ABOVE] + case '\u1E43': + // ṃ [LATIN SMALL LETTER M WITH DOT BELOW] + case '\u24DC': + // ⓜ [CIRCLED LATIN SMALL LETTER M] + case '\uFF4D': // � [FULLWIDTH LATIN SMALL LETTER M] + output[opos++] = 'm'; + break; + + case '\u24A8': // â’¨ [PARENTHESIZED LATIN SMALL LETTER M] + output[opos++] = '('; + output[opos++] = 'm'; + output[opos++] = ')'; + break; + + case '\u00D1': + // Ñ [LATIN CAPITAL LETTER N WITH TILDE] + case '\u0143': + // Ã…Æ’ [LATIN CAPITAL LETTER N WITH ACUTE] + case '\u0145': + // Å… [LATIN CAPITAL LETTER N WITH CEDILLA] + case '\u0147': + // Ň [LATIN CAPITAL LETTER N WITH CARON] + case '\u014A': + // Ã…Å  http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] + case '\u019D': + // � [LATIN CAPITAL LETTER N WITH LEFT HOOK] + case '\u01F8': + // Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] + case '\u0220': + // È  [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] + case '\u0274': + // É´ [LATIN LETTER SMALL CAPITAL N] + case '\u1D0E': + // á´Ž [LATIN LETTER SMALL CAPITAL REVERSED N] + case '\u1E44': + // Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] + case '\u1E46': + // Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] + case '\u1E48': + // Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] + case '\u1E4A': + // Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] + case '\u24C3': + // Ⓝ [CIRCLED LATIN CAPITAL LETTER N] + case '\uFF2E': // ï¼® [FULLWIDTH LATIN CAPITAL LETTER N] + output[opos++] = 'N'; + break; + + case '\u00F1': + // ñ [LATIN SMALL LETTER N WITH TILDE] + case '\u0144': + // Å„ [LATIN SMALL LETTER N WITH ACUTE] + case '\u0146': + // ņ [LATIN SMALL LETTER N WITH CEDILLA] + case '\u0148': + // ň [LATIN SMALL LETTER N WITH CARON] + case '\u0149': + // ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] + case '\u014B': + // Å‹ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] + case '\u019E': + // Æž [LATIN SMALL LETTER N WITH LONG RIGHT LEG] + case '\u01F9': + // ǹ [LATIN SMALL LETTER N WITH GRAVE] + case '\u0235': + // ȵ [LATIN SMALL LETTER N WITH CURL] + case '\u0272': + // ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] + case '\u0273': + // ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] + case '\u1D70': + // áµ° [LATIN SMALL LETTER N WITH MIDDLE TILDE] + case '\u1D87': + // ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] + case '\u1E45': + // á¹… [LATIN SMALL LETTER N WITH DOT ABOVE] + case '\u1E47': + // ṇ [LATIN SMALL LETTER N WITH DOT BELOW] + case '\u1E49': + // ṉ [LATIN SMALL LETTER N WITH LINE BELOW] + case '\u1E4B': + // ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] + case '\u207F': + // � [SUPERSCRIPT LATIN SMALL LETTER N] + case '\u24DD': + // � [CIRCLED LATIN SMALL LETTER N] + case '\uFF4E': // n [FULLWIDTH LATIN SMALL LETTER N] + output[opos++] = 'n'; + break; + + case '\u01CA': // ÇŠ [LATIN CAPITAL LETTER NJ] + output[opos++] = 'N'; + output[opos++] = 'J'; + break; + + case '\u01CB': // Ç‹ [LATIN CAPITAL LETTER N WITH SMALL LETTER J] + output[opos++] = 'N'; + output[opos++] = 'j'; + break; + + case '\u24A9': // â’© [PARENTHESIZED LATIN SMALL LETTER N] + output[opos++] = '('; + output[opos++] = 'n'; + output[opos++] = ')'; + break; + + case '\u01CC': // ÇŒ [LATIN SMALL LETTER NJ] + output[opos++] = 'n'; + output[opos++] = 'j'; + break; + + case '\u00D2': + // Ã’ [LATIN CAPITAL LETTER O WITH GRAVE] + case '\u00D3': + // Ó [LATIN CAPITAL LETTER O WITH ACUTE] + case '\u00D4': + // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] + case '\u00D5': + // Õ [LATIN CAPITAL LETTER O WITH TILDE] + case '\u00D6': + // Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] + case '\u00D8': + // Ø [LATIN CAPITAL LETTER O WITH STROKE] + case '\u014C': + // Ã…Å’ [LATIN CAPITAL LETTER O WITH MACRON] + case '\u014E': + // ÅŽ [LATIN CAPITAL LETTER O WITH BREVE] + case '\u0150': + // � [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] + case '\u0186': + // Ɔ [LATIN CAPITAL LETTER OPEN O] + case '\u019F': + // ÆŸ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] + case '\u01A0': + // Æ  [LATIN CAPITAL LETTER O WITH HORN] + case '\u01D1': + // Ç‘ [LATIN CAPITAL LETTER O WITH CARON] + case '\u01EA': + // Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] + case '\u01EC': + // Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] + case '\u01FE': + // Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] + case '\u020C': + // ÈŒ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] + case '\u020E': + // ÈŽ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] + case '\u022A': + // Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] + case '\u022C': + // Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] + case '\u022E': + // È® [LATIN CAPITAL LETTER O WITH DOT ABOVE] + case '\u0230': + // Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] + case '\u1D0F': + // á´� [LATIN LETTER SMALL CAPITAL O] + case '\u1D10': + // á´� [LATIN LETTER SMALL CAPITAL OPEN O] + case '\u1E4C': + // Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] + case '\u1E4E': + // Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] + case '\u1E50': + // � [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] + case '\u1E52': + // á¹’ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] + case '\u1ECC': + // Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] + case '\u1ECE': + // Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] + case '\u1ED0': + // � [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] + case '\u1ED2': + // á»’ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] + case '\u1ED4': + // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1ED6': + // á»– [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] + case '\u1ED8': + // Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EDA': + // Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] + case '\u1EDC': + // Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] + case '\u1EDE': + // Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] + case '\u1EE0': + // á»  [LATIN CAPITAL LETTER O WITH HORN AND TILDE] + case '\u1EE2': + // Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] + case '\u24C4': + // â“„ [CIRCLED LATIN CAPITAL LETTER O] + case '\uA74A': + // � [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] + case '\uA74C': + // � [LATIN CAPITAL LETTER O WITH LOOP] + case '\uFF2F': // O [FULLWIDTH LATIN CAPITAL LETTER O] + output[opos++] = 'O'; + break; + + case '\u00F2': + // ò [LATIN SMALL LETTER O WITH GRAVE] + case '\u00F3': + // ó [LATIN SMALL LETTER O WITH ACUTE] + case '\u00F4': + // ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] + case '\u00F5': + // õ [LATIN SMALL LETTER O WITH TILDE] + case '\u00F6': + // ö [LATIN SMALL LETTER O WITH DIAERESIS] + case '\u00F8': + // ø [LATIN SMALL LETTER O WITH STROKE] + case '\u014D': + // � [LATIN SMALL LETTER O WITH MACRON] + case '\u014F': + // � [LATIN SMALL LETTER O WITH BREVE] + case '\u0151': + // Å‘ [LATIN SMALL LETTER O WITH DOUBLE ACUTE] + case '\u01A1': + // Æ¡ [LATIN SMALL LETTER O WITH HORN] + case '\u01D2': + // Ç’ [LATIN SMALL LETTER O WITH CARON] + case '\u01EB': + // Ç« [LATIN SMALL LETTER O WITH OGONEK] + case '\u01ED': + // Ç­ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] + case '\u01FF': + // Ç¿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] + case '\u020D': + // � [LATIN SMALL LETTER O WITH DOUBLE GRAVE] + case '\u020F': + // � [LATIN SMALL LETTER O WITH INVERTED BREVE] + case '\u022B': + // È« [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] + case '\u022D': + // È­ [LATIN SMALL LETTER O WITH TILDE AND MACRON] + case '\u022F': + // ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] + case '\u0231': + // ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] + case '\u0254': + // �? [LATIN SMALL LETTER OPEN O] + case '\u0275': + // ɵ [LATIN SMALL LETTER BARRED O] + case '\u1D16': + // á´– [LATIN SMALL LETTER TOP HALF O] + case '\u1D17': + // á´— [LATIN SMALL LETTER BOTTOM HALF O] + case '\u1D97': + // á¶— [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] + case '\u1E4D': + // � [LATIN SMALL LETTER O WITH TILDE AND ACUTE] + case '\u1E4F': + // � [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] + case '\u1E51': + // ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] + case '\u1E53': + // ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] + case '\u1ECD': + // � [LATIN SMALL LETTER O WITH DOT BELOW] + case '\u1ECF': + // � [LATIN SMALL LETTER O WITH HOOK ABOVE] + case '\u1ED1': + // ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] + case '\u1ED3': + // ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] + case '\u1ED5': + // ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1ED7': + // á»— [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] + case '\u1ED9': + // á»™ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EDB': + // á»› [LATIN SMALL LETTER O WITH HORN AND ACUTE] + case '\u1EDD': + // � [LATIN SMALL LETTER O WITH HORN AND GRAVE] + case '\u1EDF': + // ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] + case '\u1EE1': + // ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] + case '\u1EE3': + // ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] + case '\u2092': + // â‚’ [LATIN SUBSCRIPT SMALL LETTER O] + case '\u24DE': + // ⓞ [CIRCLED LATIN SMALL LETTER O] + case '\u2C7A': + // ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] + case '\uA74B': + // � [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] + case '\uA74D': + // � [LATIN SMALL LETTER O WITH LOOP] + case '\uFF4F': // � [FULLWIDTH LATIN SMALL LETTER O] + output[opos++] = 'o'; + break; + + case '\u0152': + // Å’ [LATIN CAPITAL LIGATURE OE] + case '\u0276': // ɶ [LATIN LETTER SMALL CAPITAL OE] + output[opos++] = 'O'; + output[opos++] = 'E'; + break; + + case '\uA74E': // � [LATIN CAPITAL LETTER OO] + output[opos++] = 'O'; + output[opos++] = 'O'; + break; + + case '\u0222': + // È¢ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] + case '\u1D15': // á´• [LATIN LETTER SMALL CAPITAL OU] + output[opos++] = 'O'; + output[opos++] = 'U'; + break; + + case '\u24AA': // â’ª [PARENTHESIZED LATIN SMALL LETTER O] + output[opos++] = '('; + output[opos++] = 'o'; + output[opos++] = ')'; + break; + + case '\u0153': + // Å“ [LATIN SMALL LIGATURE OE] + case '\u1D14': // á´�? [LATIN SMALL LETTER TURNED OE] + output[opos++] = 'o'; + output[opos++] = 'e'; + break; + + case '\uA74F': // � [LATIN SMALL LETTER OO] + output[opos++] = 'o'; + output[opos++] = 'o'; + break; + + case '\u0223': // È£ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] + output[opos++] = 'o'; + output[opos++] = 'u'; + break; + + case '\u01A4': + // Ƥ [LATIN CAPITAL LETTER P WITH HOOK] + case '\u1D18': + // á´˜ [LATIN LETTER SMALL CAPITAL P] + case '\u1E54': + // �? [LATIN CAPITAL LETTER P WITH ACUTE] + case '\u1E56': + // á¹– [LATIN CAPITAL LETTER P WITH DOT ABOVE] + case '\u24C5': + // â“… [CIRCLED LATIN CAPITAL LETTER P] + case '\u2C63': + // â±£ [LATIN CAPITAL LETTER P WITH STROKE] + case '\uA750': + // � [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] + case '\uA752': + // � [LATIN CAPITAL LETTER P WITH FLOURISH] + case '\uA754': + // �? [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] + case '\uFF30': // ï¼° [FULLWIDTH LATIN CAPITAL LETTER P] + output[opos++] = 'P'; + break; + + case '\u01A5': + // Æ¥ [LATIN SMALL LETTER P WITH HOOK] + case '\u1D71': + // áµ± [LATIN SMALL LETTER P WITH MIDDLE TILDE] + case '\u1D7D': + // áµ½ [LATIN SMALL LETTER P WITH STROKE] + case '\u1D88': + // ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] + case '\u1E55': + // ṕ [LATIN SMALL LETTER P WITH ACUTE] + case '\u1E57': + // á¹— [LATIN SMALL LETTER P WITH DOT ABOVE] + case '\u24DF': + // ⓟ [CIRCLED LATIN SMALL LETTER P] + case '\uA751': + // � [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] + case '\uA753': + // � [LATIN SMALL LETTER P WITH FLOURISH] + case '\uA755': + // � [LATIN SMALL LETTER P WITH SQUIRREL TAIL] + case '\uA7FC': + // ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] + case '\uFF50': // � [FULLWIDTH LATIN SMALL LETTER P] + output[opos++] = 'p'; + break; + + case '\u24AB': // â’« [PARENTHESIZED LATIN SMALL LETTER P] + output[opos++] = '('; + output[opos++] = 'p'; + output[opos++] = ')'; + break; + + case '\u024A': + // ÉŠ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] + case '\u24C6': + // Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] + case '\uA756': + // � [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] + case '\uA758': + // � [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] + case '\uFF31': // ï¼± [FULLWIDTH LATIN CAPITAL LETTER Q] + output[opos++] = 'Q'; + break; + + case '\u0138': + // ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] + case '\u024B': + // É‹ [LATIN SMALL LETTER Q WITH HOOK TAIL] + case '\u02A0': + // Ê  [LATIN SMALL LETTER Q WITH HOOK] + case '\u24E0': + // â“  [CIRCLED LATIN SMALL LETTER Q] + case '\uA757': + // � [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] + case '\uA759': + // � [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] + case '\uFF51': // q [FULLWIDTH LATIN SMALL LETTER Q] + output[opos++] = 'q'; + break; + + case '\u24AC': // â’¬ [PARENTHESIZED LATIN SMALL LETTER Q] + output[opos++] = '('; + output[opos++] = 'q'; + output[opos++] = ')'; + break; + + case '\u0239': // ȹ [LATIN SMALL LETTER QP DIGRAPH] + output[opos++] = 'q'; + output[opos++] = 'p'; + break; + + case '\u0154': + // �? [LATIN CAPITAL LETTER R WITH ACUTE] + case '\u0156': + // Å– [LATIN CAPITAL LETTER R WITH CEDILLA] + case '\u0158': + // Ã…Ëœ [LATIN CAPITAL LETTER R WITH CARON] + case '\u0210': + // È’ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] + case '\u0212': + // È’ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] + case '\u024C': + // ÉŒ [LATIN CAPITAL LETTER R WITH STROKE] + case '\u0280': + // Ê€ [LATIN LETTER SMALL CAPITAL R] + case '\u0281': + // � [LATIN LETTER SMALL CAPITAL INVERTED R] + case '\u1D19': + // á´™ [LATIN LETTER SMALL CAPITAL REVERSED R] + case '\u1D1A': + // á´š [LATIN LETTER SMALL CAPITAL TURNED R] + case '\u1E58': + // Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] + case '\u1E5A': + // Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] + case '\u1E5C': + // Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] + case '\u1E5E': + // Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] + case '\u24C7': + // Ⓡ [CIRCLED LATIN CAPITAL LETTER R] + case '\u2C64': + // Ɽ [LATIN CAPITAL LETTER R WITH TAIL] + case '\uA75A': + // � [LATIN CAPITAL LETTER R ROTUNDA] + case '\uA782': + // êž‚ [LATIN CAPITAL LETTER INSULAR R] + case '\uFF32': // ï¼² [FULLWIDTH LATIN CAPITAL LETTER R] + output[opos++] = 'R'; + break; + + case '\u0155': + // Å• [LATIN SMALL LETTER R WITH ACUTE] + case '\u0157': + // Å— [LATIN SMALL LETTER R WITH CEDILLA] + case '\u0159': + // Ã…â„¢ [LATIN SMALL LETTER R WITH CARON] + case '\u0211': + // È‘ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] + case '\u0213': + // È“ [LATIN SMALL LETTER R WITH INVERTED BREVE] + case '\u024D': + // � [LATIN SMALL LETTER R WITH STROKE] + case '\u027C': + // ɼ [LATIN SMALL LETTER R WITH LONG LEG] + case '\u027D': + // ɽ [LATIN SMALL LETTER R WITH TAIL] + case '\u027E': + // ɾ [LATIN SMALL LETTER R WITH FISHHOOK] + case '\u027F': + // É¿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] + case '\u1D63': + // áµ£ [LATIN SUBSCRIPT SMALL LETTER R] + case '\u1D72': + // áµ² [LATIN SMALL LETTER R WITH MIDDLE TILDE] + case '\u1D73': + // áµ³ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] + case '\u1D89': + // ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] + case '\u1E59': + // á¹™ [LATIN SMALL LETTER R WITH DOT ABOVE] + case '\u1E5B': + // á¹› [LATIN SMALL LETTER R WITH DOT BELOW] + case '\u1E5D': + // � [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] + case '\u1E5F': + // ṟ [LATIN SMALL LETTER R WITH LINE BELOW] + case '\u24E1': + // â“¡ [CIRCLED LATIN SMALL LETTER R] + case '\uA75B': + // � [LATIN SMALL LETTER R ROTUNDA] + case '\uA783': + // ꞃ [LATIN SMALL LETTER INSULAR R] + case '\uFF52': // ï½’ [FULLWIDTH LATIN SMALL LETTER R] + output[opos++] = 'r'; + break; + + case '\u24AD': // â’­ [PARENTHESIZED LATIN SMALL LETTER R] + output[opos++] = '('; + output[opos++] = 'r'; + output[opos++] = ')'; + break; + + case '\u015A': + // Ã…Å¡ [LATIN CAPITAL LETTER S WITH ACUTE] + case '\u015C': + // Ã…Å“ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] + case '\u015E': + // Åž [LATIN CAPITAL LETTER S WITH CEDILLA] + case '\u0160': + // Å  [LATIN CAPITAL LETTER S WITH CARON] + case '\u0218': + // Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] + case '\u1E60': + // á¹  [LATIN CAPITAL LETTER S WITH DOT ABOVE] + case '\u1E62': + // á¹¢ [LATIN CAPITAL LETTER S WITH DOT BELOW] + case '\u1E64': + // Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] + case '\u1E66': + // Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] + case '\u1E68': + // Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] + case '\u24C8': + // Ⓢ [CIRCLED LATIN CAPITAL LETTER S] + case '\uA731': + // ꜱ [LATIN LETTER SMALL CAPITAL S] + case '\uA785': + // êž… [LATIN SMALL LETTER INSULAR S] + case '\uFF33': // ï¼³ [FULLWIDTH LATIN CAPITAL LETTER S] + output[opos++] = 'S'; + break; + + case '\u015B': + // Å› [LATIN SMALL LETTER S WITH ACUTE] + case '\u015D': + // � [LATIN SMALL LETTER S WITH CIRCUMFLEX] + case '\u015F': + // ÅŸ [LATIN SMALL LETTER S WITH CEDILLA] + case '\u0161': + // Å¡ [LATIN SMALL LETTER S WITH CARON] + case '\u017F': + // Å¿ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] + case '\u0219': + // È™ [LATIN SMALL LETTER S WITH COMMA BELOW] + case '\u023F': + // È¿ [LATIN SMALL LETTER S WITH SWASH TAIL] + case '\u0282': + // Ê‚ [LATIN SMALL LETTER S WITH HOOK] + case '\u1D74': + // áµ´ [LATIN SMALL LETTER S WITH MIDDLE TILDE] + case '\u1D8A': + // á¶Š [LATIN SMALL LETTER S WITH PALATAL HOOK] + case '\u1E61': + // ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] + case '\u1E63': + // á¹£ [LATIN SMALL LETTER S WITH DOT BELOW] + case '\u1E65': + // á¹¥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] + case '\u1E67': + // á¹§ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] + case '\u1E69': + // ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] + case '\u1E9C': + // ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] + case '\u1E9D': + // � [LATIN SMALL LETTER LONG S WITH HIGH STROKE] + case '\u24E2': + // â“¢ [CIRCLED LATIN SMALL LETTER S] + case '\uA784': + // êž„ [LATIN CAPITAL LETTER INSULAR S] + case '\uFF53': // s [FULLWIDTH LATIN SMALL LETTER S] + output[opos++] = 's'; + break; + + case '\u1E9E': // ẞ [LATIN CAPITAL LETTER SHARP S] + output[opos++] = 'S'; + output[opos++] = 'S'; + break; + + case '\u24AE': // â’® [PARENTHESIZED LATIN SMALL LETTER S] + output[opos++] = '('; + output[opos++] = 's'; + output[opos++] = ')'; + break; + + case '\u00DF': // ß [LATIN SMALL LETTER SHARP S] + output[opos++] = 's'; + output[opos++] = 's'; + break; + + case '\uFB06': // st [LATIN SMALL LIGATURE ST] + output[opos++] = 's'; + output[opos++] = 't'; + break; + + case '\u0162': + // Å¢ [LATIN CAPITAL LETTER T WITH CEDILLA] + case '\u0164': + // Ť [LATIN CAPITAL LETTER T WITH CARON] + case '\u0166': + // Ŧ [LATIN CAPITAL LETTER T WITH STROKE] + case '\u01AC': + // Ƭ [LATIN CAPITAL LETTER T WITH HOOK] + case '\u01AE': + // Æ® [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] + case '\u021A': + // Èš [LATIN CAPITAL LETTER T WITH COMMA BELOW] + case '\u023E': + // Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] + case '\u1D1B': + // á´› [LATIN LETTER SMALL CAPITAL T] + case '\u1E6A': + // Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] + case '\u1E6C': + // Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] + case '\u1E6E': + // á¹® [LATIN CAPITAL LETTER T WITH LINE BELOW] + case '\u1E70': + // á¹° [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] + case '\u24C9': + // Ⓣ [CIRCLED LATIN CAPITAL LETTER T] + case '\uA786': + // Ꞇ [LATIN CAPITAL LETTER INSULAR T] + case '\uFF34': // ï¼´ [FULLWIDTH LATIN CAPITAL LETTER T] + output[opos++] = 'T'; + break; + + case '\u0163': + // Å£ [LATIN SMALL LETTER T WITH CEDILLA] + case '\u0165': + // Ã…Â¥ [LATIN SMALL LETTER T WITH CARON] + case '\u0167': + // ŧ [LATIN SMALL LETTER T WITH STROKE] + case '\u01AB': + // Æ« [LATIN SMALL LETTER T WITH PALATAL HOOK] + case '\u01AD': + // Æ­ [LATIN SMALL LETTER T WITH HOOK] + case '\u021B': + // È› [LATIN SMALL LETTER T WITH COMMA BELOW] + case '\u0236': + // ȶ [LATIN SMALL LETTER T WITH CURL] + case '\u0287': + // ʇ [LATIN SMALL LETTER TURNED T] + case '\u0288': + // ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] + case '\u1D75': + // áµµ [LATIN SMALL LETTER T WITH MIDDLE TILDE] + case '\u1E6B': + // ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] + case '\u1E6D': + // á¹­ [LATIN SMALL LETTER T WITH DOT BELOW] + case '\u1E6F': + // ṯ [LATIN SMALL LETTER T WITH LINE BELOW] + case '\u1E71': + // á¹± [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] + case '\u1E97': + // ẗ [LATIN SMALL LETTER T WITH DIAERESIS] + case '\u24E3': + // â“£ [CIRCLED LATIN SMALL LETTER T] + case '\u2C66': + // ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] + case '\uFF54': // �? [FULLWIDTH LATIN SMALL LETTER T] + output[opos++] = 't'; + break; + + case '\u00DE': + // Þ [LATIN CAPITAL LETTER THORN] + case '\uA766': // � [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] + output[opos++] = 'T'; + output[opos++] = 'H'; + break; + + case '\uA728': // Ꜩ [LATIN CAPITAL LETTER TZ] + output[opos++] = 'T'; + output[opos++] = 'Z'; + break; + + case '\u24AF': // â’¯ [PARENTHESIZED LATIN SMALL LETTER T] + output[opos++] = '('; + output[opos++] = 't'; + output[opos++] = ')'; + break; + + case '\u02A8': // ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] + output[opos++] = 't'; + output[opos++] = 'c'; + break; + + case '\u00FE': + // þ [LATIN SMALL LETTER THORN] + case '\u1D7A': + // ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] + case '\uA767': // � [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] + output[opos++] = 't'; + output[opos++] = 'h'; + break; + + case '\u02A6': // ʦ [LATIN SMALL LETTER TS DIGRAPH] + output[opos++] = 't'; + output[opos++] = 's'; + break; + + case '\uA729': // ꜩ [LATIN SMALL LETTER TZ] + output[opos++] = 't'; + output[opos++] = 'z'; + break; + + case '\u00D9': + // Ù [LATIN CAPITAL LETTER U WITH GRAVE] + case '\u00DA': + // Ú [LATIN CAPITAL LETTER U WITH ACUTE] + case '\u00DB': + // Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] + case '\u00DC': + // Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] + case '\u0168': + // Ũ [LATIN CAPITAL LETTER U WITH TILDE] + case '\u016A': + // Ū [LATIN CAPITAL LETTER U WITH MACRON] + case '\u016C': + // Ŭ [LATIN CAPITAL LETTER U WITH BREVE] + case '\u016E': + // Å® [LATIN CAPITAL LETTER U WITH RING ABOVE] + case '\u0170': + // Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] + case '\u0172': + // Ų [LATIN CAPITAL LETTER U WITH OGONEK] + case '\u01AF': + // Ư [LATIN CAPITAL LETTER U WITH HORN] + case '\u01D3': + // Ç“ [LATIN CAPITAL LETTER U WITH CARON] + case '\u01D5': + // Ç• [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] + case '\u01D7': + // Ç— [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] + case '\u01D9': + // Ç™ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] + case '\u01DB': + // Ç› [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] + case '\u0214': + // �? [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] + case '\u0216': + // È– [LATIN CAPITAL LETTER U WITH INVERTED BREVE] + case '\u0244': + // É„ [LATIN CAPITAL LETTER U BAR] + case '\u1D1C': + // á´œ [LATIN LETTER SMALL CAPITAL U] + case '\u1D7E': + // áµ¾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] + case '\u1E72': + // á¹² [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] + case '\u1E74': + // á¹´ [LATIN CAPITAL LETTER U WITH TILDE BELOW] + case '\u1E76': + // á¹¶ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] + case '\u1E78': + // Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] + case '\u1E7A': + // Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] + case '\u1EE4': + // Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] + case '\u1EE6': + // Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] + case '\u1EE8': + // Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] + case '\u1EEA': + // Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] + case '\u1EEC': + // Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] + case '\u1EEE': + // á»® [LATIN CAPITAL LETTER U WITH HORN AND TILDE] + case '\u1EF0': + // á»° [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] + case '\u24CA': + // Ⓤ [CIRCLED LATIN CAPITAL LETTER U] + case '\uFF35': // ï¼µ [FULLWIDTH LATIN CAPITAL LETTER U] + output[opos++] = 'U'; + break; + + case '\u00F9': + // ù [LATIN SMALL LETTER U WITH GRAVE] + case '\u00FA': + // ú [LATIN SMALL LETTER U WITH ACUTE] + case '\u00FB': + // û [LATIN SMALL LETTER U WITH CIRCUMFLEX] + case '\u00FC': + // ü [LATIN SMALL LETTER U WITH DIAERESIS] + case '\u0169': + // Å© [LATIN SMALL LETTER U WITH TILDE] + case '\u016B': + // Å« [LATIN SMALL LETTER U WITH MACRON] + case '\u016D': + // Å­ [LATIN SMALL LETTER U WITH BREVE] + case '\u016F': + // ů [LATIN SMALL LETTER U WITH RING ABOVE] + case '\u0171': + // ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] + case '\u0173': + // ų [LATIN SMALL LETTER U WITH OGONEK] + case '\u01B0': + // ư [LATIN SMALL LETTER U WITH HORN] + case '\u01D4': + // �? [LATIN SMALL LETTER U WITH CARON] + case '\u01D6': + // Ç– [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] + case '\u01D8': + // ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] + case '\u01DA': + // Çš [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] + case '\u01DC': + // Çœ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] + case '\u0215': + // È• [LATIN SMALL LETTER U WITH DOUBLE GRAVE] + case '\u0217': + // È— [LATIN SMALL LETTER U WITH INVERTED BREVE] + case '\u0289': + // ʉ [LATIN SMALL LETTER U BAR] + case '\u1D64': + // ᵤ [LATIN SUBSCRIPT SMALL LETTER U] + case '\u1D99': + // á¶™ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] + case '\u1E73': + // á¹³ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] + case '\u1E75': + // á¹µ [LATIN SMALL LETTER U WITH TILDE BELOW] + case '\u1E77': + // á¹· [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] + case '\u1E79': + // á¹¹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] + case '\u1E7B': + // á¹» [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] + case '\u1EE5': + // ụ [LATIN SMALL LETTER U WITH DOT BELOW] + case '\u1EE7': + // á»§ [LATIN SMALL LETTER U WITH HOOK ABOVE] + case '\u1EE9': + // ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] + case '\u1EEB': + // ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] + case '\u1EED': + // á»­ [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] + case '\u1EEF': + // ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] + case '\u1EF1': + // á»± [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] + case '\u24E4': + // ⓤ [CIRCLED LATIN SMALL LETTER U] + case '\uFF55': // u [FULLWIDTH LATIN SMALL LETTER U] + output[opos++] = 'u'; + break; + + case '\u24B0': // â’° [PARENTHESIZED LATIN SMALL LETTER U] + output[opos++] = '('; + output[opos++] = 'u'; + output[opos++] = ')'; + break; + + case '\u1D6B': // ᵫ [LATIN SMALL LETTER UE] + output[opos++] = 'u'; + output[opos++] = 'e'; + break; + + case '\u01B2': + // Ʋ [LATIN CAPITAL LETTER V WITH HOOK] + case '\u0245': + // É… [LATIN CAPITAL LETTER TURNED V] + case '\u1D20': + // á´  [LATIN LETTER SMALL CAPITAL V] + case '\u1E7C': + // á¹¼ [LATIN CAPITAL LETTER V WITH TILDE] + case '\u1E7E': + // á¹¾ [LATIN CAPITAL LETTER V WITH DOT BELOW] + case '\u1EFC': + // Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] + case '\u24CB': + // â“‹ [CIRCLED LATIN CAPITAL LETTER V] + case '\uA75E': + // � [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] + case '\uA768': + // � [LATIN CAPITAL LETTER VEND] + case '\uFF36': // ï¼¶ [FULLWIDTH LATIN CAPITAL LETTER V] + output[opos++] = 'V'; + break; + + case '\u028B': + // Ê‹ [LATIN SMALL LETTER V WITH HOOK] + case '\u028C': + // ÊŒ [LATIN SMALL LETTER TURNED V] + case '\u1D65': + // áµ¥ [LATIN SUBSCRIPT SMALL LETTER V] + case '\u1D8C': + // á¶Œ [LATIN SMALL LETTER V WITH PALATAL HOOK] + case '\u1E7D': + // á¹½ [LATIN SMALL LETTER V WITH TILDE] + case '\u1E7F': + // ṿ [LATIN SMALL LETTER V WITH DOT BELOW] + case '\u24E5': + // â“¥ [CIRCLED LATIN SMALL LETTER V] + case '\u2C71': + // â±± [LATIN SMALL LETTER V WITH RIGHT HOOK] + case '\u2C74': + // â±´ [LATIN SMALL LETTER V WITH CURL] + case '\uA75F': + // � [LATIN SMALL LETTER V WITH DIAGONAL STROKE] + case '\uFF56': // ï½– [FULLWIDTH LATIN SMALL LETTER V] + output[opos++] = 'v'; + break; + + case '\uA760': // � [LATIN CAPITAL LETTER VY] + output[opos++] = 'V'; + output[opos++] = 'Y'; + break; + + case '\u24B1': // â’± [PARENTHESIZED LATIN SMALL LETTER V] + output[opos++] = '('; + output[opos++] = 'v'; + output[opos++] = ')'; + break; + + case '\uA761': // � [LATIN SMALL LETTER VY] + output[opos++] = 'v'; + output[opos++] = 'y'; + break; + + case '\u0174': + // Å´ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] + case '\u01F7': + // Ç· http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] + case '\u1D21': + // á´¡ [LATIN LETTER SMALL CAPITAL W] + case '\u1E80': + // Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] + case '\u1E82': + // Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] + case '\u1E84': + // Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] + case '\u1E86': + // Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] + case '\u1E88': + // Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] + case '\u24CC': + // Ⓦ [CIRCLED LATIN CAPITAL LETTER W] + case '\u2C72': + // â±² [LATIN CAPITAL LETTER W WITH HOOK] + case '\uFF37': // ï¼· [FULLWIDTH LATIN CAPITAL LETTER W] + output[opos++] = 'W'; + break; + + case '\u0175': + // ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] + case '\u01BF': + // Æ¿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] + case '\u028D': + // � [LATIN SMALL LETTER TURNED W] + case '\u1E81': + // � [LATIN SMALL LETTER W WITH GRAVE] + case '\u1E83': + // ẃ [LATIN SMALL LETTER W WITH ACUTE] + case '\u1E85': + // ẅ [LATIN SMALL LETTER W WITH DIAERESIS] + case '\u1E87': + // ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] + case '\u1E89': + // ẉ [LATIN SMALL LETTER W WITH DOT BELOW] + case '\u1E98': + // ẘ [LATIN SMALL LETTER W WITH RING ABOVE] + case '\u24E6': + // ⓦ [CIRCLED LATIN SMALL LETTER W] + case '\u2C73': + // â±³ [LATIN SMALL LETTER W WITH HOOK] + case '\uFF57': // ï½— [FULLWIDTH LATIN SMALL LETTER W] + output[opos++] = 'w'; + break; + + case '\u24B2': // â’² [PARENTHESIZED LATIN SMALL LETTER W] + output[opos++] = '('; + output[opos++] = 'w'; + output[opos++] = ')'; + break; + + case '\u1E8A': + // Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] + case '\u1E8C': + // Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] + case '\u24CD': + // � [CIRCLED LATIN CAPITAL LETTER X] + case '\uFF38': // X [FULLWIDTH LATIN CAPITAL LETTER X] + output[opos++] = 'X'; + break; + + case '\u1D8D': + // � [LATIN SMALL LETTER X WITH PALATAL HOOK] + case '\u1E8B': + // ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] + case '\u1E8D': + // � [LATIN SMALL LETTER X WITH DIAERESIS] + case '\u2093': + // â‚“ [LATIN SUBSCRIPT SMALL LETTER X] + case '\u24E7': + // â“§ [CIRCLED LATIN SMALL LETTER X] + case '\uFF58': // x [FULLWIDTH LATIN SMALL LETTER X] + output[opos++] = 'x'; + break; + + case '\u24B3': // â’³ [PARENTHESIZED LATIN SMALL LETTER X] + output[opos++] = '('; + output[opos++] = 'x'; + output[opos++] = ')'; + break; + + case '\u00DD': + // � [LATIN CAPITAL LETTER Y WITH ACUTE] + case '\u0176': + // Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] + case '\u0178': + // Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] + case '\u01B3': + // Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] + case '\u0232': + // Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] + case '\u024E': + // ÉŽ [LATIN CAPITAL LETTER Y WITH STROKE] + case '\u028F': + // � [LATIN LETTER SMALL CAPITAL Y] + case '\u1E8E': + // Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] + case '\u1EF2': + // Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] + case '\u1EF4': + // á»´ [LATIN CAPITAL LETTER Y WITH DOT BELOW] + case '\u1EF6': + // á»¶ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] + case '\u1EF8': + // Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] + case '\u1EFE': + // Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] + case '\u24CE': + // Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] + case '\uFF39': // ï¼¹ [FULLWIDTH LATIN CAPITAL LETTER Y] + output[opos++] = 'Y'; + break; + + case '\u00FD': + // ý [LATIN SMALL LETTER Y WITH ACUTE] + case '\u00FF': + // ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] + case '\u0177': + // Å· [LATIN SMALL LETTER Y WITH CIRCUMFLEX] + case '\u01B4': + // Æ´ [LATIN SMALL LETTER Y WITH HOOK] + case '\u0233': + // ȳ [LATIN SMALL LETTER Y WITH MACRON] + case '\u024F': + // � [LATIN SMALL LETTER Y WITH STROKE] + case '\u028E': + // ÊŽ [LATIN SMALL LETTER TURNED Y] + case '\u1E8F': + // � [LATIN SMALL LETTER Y WITH DOT ABOVE] + case '\u1E99': + // ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] + case '\u1EF3': + // ỳ [LATIN SMALL LETTER Y WITH GRAVE] + case '\u1EF5': + // ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] + case '\u1EF7': + // á»· [LATIN SMALL LETTER Y WITH HOOK ABOVE] + case '\u1EF9': + // ỹ [LATIN SMALL LETTER Y WITH TILDE] + case '\u1EFF': + // ỿ [LATIN SMALL LETTER Y WITH LOOP] + case '\u24E8': + // ⓨ [CIRCLED LATIN SMALL LETTER Y] + case '\uFF59': // ï½™ [FULLWIDTH LATIN SMALL LETTER Y] + output[opos++] = 'y'; + break; + + case '\u24B4': // â’´ [PARENTHESIZED LATIN SMALL LETTER Y] + output[opos++] = '('; + output[opos++] = 'y'; + output[opos++] = ')'; + break; + + case '\u0179': + // Ź [LATIN CAPITAL LETTER Z WITH ACUTE] + case '\u017B': + // Å» [LATIN CAPITAL LETTER Z WITH DOT ABOVE] + case '\u017D': + // Ž [LATIN CAPITAL LETTER Z WITH CARON] + case '\u01B5': + // Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] + case '\u021C': + // Èœ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] + case '\u0224': + // Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] + case '\u1D22': + // á´¢ [LATIN LETTER SMALL CAPITAL Z] + case '\u1E90': + // � [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] + case '\u1E92': + // Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] + case '\u1E94': + // �? [LATIN CAPITAL LETTER Z WITH LINE BELOW] + case '\u24CF': + // � [CIRCLED LATIN CAPITAL LETTER Z] + case '\u2C6B': + // Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] + case '\uA762': + // � [LATIN CAPITAL LETTER VISIGOTHIC Z] + case '\uFF3A': // Z [FULLWIDTH LATIN CAPITAL LETTER Z] + output[opos++] = 'Z'; + break; + + case '\u017A': + // ź [LATIN SMALL LETTER Z WITH ACUTE] + case '\u017C': + // ż [LATIN SMALL LETTER Z WITH DOT ABOVE] + case '\u017E': + // ž [LATIN SMALL LETTER Z WITH CARON] + case '\u01B6': + // ƶ [LATIN SMALL LETTER Z WITH STROKE] + case '\u021D': + // � http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] + case '\u0225': + // È¥ [LATIN SMALL LETTER Z WITH HOOK] + case '\u0240': + // É€ [LATIN SMALL LETTER Z WITH SWASH TAIL] + case '\u0290': + // � [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] + case '\u0291': + // Ê‘ [LATIN SMALL LETTER Z WITH CURL] + case '\u1D76': + // áµ¶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] + case '\u1D8E': + // á¶Ž [LATIN SMALL LETTER Z WITH PALATAL HOOK] + case '\u1E91': + // ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] + case '\u1E93': + // ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] + case '\u1E95': + // ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] + case '\u24E9': + // â“© [CIRCLED LATIN SMALL LETTER Z] + case '\u2C6C': + // ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] + case '\uA763': + // � [LATIN SMALL LETTER VISIGOTHIC Z] + case '\uFF5A': // z [FULLWIDTH LATIN SMALL LETTER Z] + output[opos++] = 'z'; + break; + + case '\u24B5': // â’µ [PARENTHESIZED LATIN SMALL LETTER Z] + output[opos++] = '('; + output[opos++] = 'z'; + output[opos++] = ')'; + break; + + case '\u2070': + // � [SUPERSCRIPT ZERO] + case '\u2080': + // â‚€ [SUBSCRIPT ZERO] + case '\u24EA': + // ⓪ [CIRCLED DIGIT ZERO] + case '\u24FF': + // â“¿ [NEGATIVE CIRCLED DIGIT ZERO] + case '\uFF10': // � [FULLWIDTH DIGIT ZERO] + output[opos++] = '0'; + break; + + case '\u00B9': + // ¹ [SUPERSCRIPT ONE] + case '\u2081': + // � [SUBSCRIPT ONE] + case '\u2460': + // â‘  [CIRCLED DIGIT ONE] + case '\u24F5': + // ⓵ [DOUBLE CIRCLED DIGIT ONE] + case '\u2776': + // � [DINGBAT NEGATIVE CIRCLED DIGIT ONE] + case '\u2780': + // ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] + case '\u278A': + // ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] + case '\uFF11': // 1 [FULLWIDTH DIGIT ONE] + output[opos++] = '1'; + break; + + case '\u2488': // â’ˆ [DIGIT ONE FULL STOP] + output[opos++] = '1'; + output[opos++] = '.'; + break; + + case '\u2474': // â‘´ [PARENTHESIZED DIGIT ONE] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = ')'; + break; + + case '\u00B2': + // ² [SUPERSCRIPT TWO] + case '\u2082': + // â‚‚ [SUBSCRIPT TWO] + case '\u2461': + // â‘¡ [CIRCLED DIGIT TWO] + case '\u24F6': + // â“¶ [DOUBLE CIRCLED DIGIT TWO] + case '\u2777': + // � [DINGBAT NEGATIVE CIRCLED DIGIT TWO] + case '\u2781': + // � [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] + case '\u278B': + // âž‹ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] + case '\uFF12': // ï¼’ [FULLWIDTH DIGIT TWO] + output[opos++] = '2'; + break; + + case '\u2489': // â’‰ [DIGIT TWO FULL STOP] + output[opos++] = '2'; + output[opos++] = '.'; + break; + + case '\u2475': // ⑵ [PARENTHESIZED DIGIT TWO] + output[opos++] = '('; + output[opos++] = '2'; + output[opos++] = ')'; + break; + + case '\u00B3': + // ³ [SUPERSCRIPT THREE] + case '\u2083': + // ₃ [SUBSCRIPT THREE] + case '\u2462': + // â‘¢ [CIRCLED DIGIT THREE] + case '\u24F7': + // â“· [DOUBLE CIRCLED DIGIT THREE] + case '\u2778': + // � [DINGBAT NEGATIVE CIRCLED DIGIT THREE] + case '\u2782': + // âž‚ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] + case '\u278C': + // ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] + case '\uFF13': // 3 [FULLWIDTH DIGIT THREE] + output[opos++] = '3'; + break; + + case '\u248A': // â’Š [DIGIT THREE FULL STOP] + output[opos++] = '3'; + output[opos++] = '.'; + break; + + case '\u2476': // â‘¶ [PARENTHESIZED DIGIT THREE] + output[opos++] = '('; + output[opos++] = '3'; + output[opos++] = ')'; + break; + + case '\u2074': + // � [SUPERSCRIPT FOUR] + case '\u2084': + // â‚„ [SUBSCRIPT FOUR] + case '\u2463': + // â‘£ [CIRCLED DIGIT FOUR] + case '\u24F8': + // ⓸ [DOUBLE CIRCLED DIGIT FOUR] + case '\u2779': + // � [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] + case '\u2783': + // ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] + case '\u278D': + // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] + case '\uFF14': // �? [FULLWIDTH DIGIT FOUR] + output[opos++] = '4'; + break; + + case '\u248B': // â’‹ [DIGIT FOUR FULL STOP] + output[opos++] = '4'; + output[opos++] = '.'; + break; + + case '\u2477': // â‘· [PARENTHESIZED DIGIT FOUR] + output[opos++] = '('; + output[opos++] = '4'; + output[opos++] = ')'; + break; + + case '\u2075': + // � [SUPERSCRIPT FIVE] + case '\u2085': + // â‚… [SUBSCRIPT FIVE] + case '\u2464': + // ⑤ [CIRCLED DIGIT FIVE] + case '\u24F9': + // ⓹ [DOUBLE CIRCLED DIGIT FIVE] + case '\u277A': + // � [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] + case '\u2784': + // âž„ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] + case '\u278E': + // ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] + case '\uFF15': // 5 [FULLWIDTH DIGIT FIVE] + output[opos++] = '5'; + break; + + case '\u248C': // â’Œ [DIGIT FIVE FULL STOP] + output[opos++] = '5'; + output[opos++] = '.'; + break; + + case '\u2478': // ⑸ [PARENTHESIZED DIGIT FIVE] + output[opos++] = '('; + output[opos++] = '5'; + output[opos++] = ')'; + break; + + case '\u2076': + // � [SUPERSCRIPT SIX] + case '\u2086': + // ₆ [SUBSCRIPT SIX] + case '\u2465': + // â‘¥ [CIRCLED DIGIT SIX] + case '\u24FA': + // ⓺ [DOUBLE CIRCLED DIGIT SIX] + case '\u277B': + // � [DINGBAT NEGATIVE CIRCLED DIGIT SIX] + case '\u2785': + // âž… [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] + case '\u278F': + // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] + case '\uFF16': // ï¼– [FULLWIDTH DIGIT SIX] + output[opos++] = '6'; + break; + + case '\u248D': // â’� [DIGIT SIX FULL STOP] + output[opos++] = '6'; + output[opos++] = '.'; + break; + + case '\u2479': // ⑹ [PARENTHESIZED DIGIT SIX] + output[opos++] = '('; + output[opos++] = '6'; + output[opos++] = ')'; + break; + + case '\u2077': + // � [SUPERSCRIPT SEVEN] + case '\u2087': + // ₇ [SUBSCRIPT SEVEN] + case '\u2466': + // ⑦ [CIRCLED DIGIT SEVEN] + case '\u24FB': + // â“» [DOUBLE CIRCLED DIGIT SEVEN] + case '\u277C': + // � [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] + case '\u2786': + // ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] + case '\u2790': + // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] + case '\uFF17': // ï¼— [FULLWIDTH DIGIT SEVEN] + output[opos++] = '7'; + break; + + case '\u248E': // â’Ž [DIGIT SEVEN FULL STOP] + output[opos++] = '7'; + output[opos++] = '.'; + break; + + case '\u247A': // ⑺ [PARENTHESIZED DIGIT SEVEN] + output[opos++] = '('; + output[opos++] = '7'; + output[opos++] = ')'; + break; + + case '\u2078': + // � [SUPERSCRIPT EIGHT] + case '\u2088': + // ₈ [SUBSCRIPT EIGHT] + case '\u2467': + // â‘§ [CIRCLED DIGIT EIGHT] + case '\u24FC': + // ⓼ [DOUBLE CIRCLED DIGIT EIGHT] + case '\u277D': + // � [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] + case '\u2787': + // ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] + case '\u2791': + // âž‘ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] + case '\uFF18': // 8 [FULLWIDTH DIGIT EIGHT] + output[opos++] = '8'; + break; + + case '\u248F': // â’� [DIGIT EIGHT FULL STOP] + output[opos++] = '8'; + output[opos++] = '.'; + break; + + case '\u247B': // â‘» [PARENTHESIZED DIGIT EIGHT] + output[opos++] = '('; + output[opos++] = '8'; + output[opos++] = ')'; + break; + + case '\u2079': + // � [SUPERSCRIPT NINE] + case '\u2089': + // ₉ [SUBSCRIPT NINE] + case '\u2468': + // ⑨ [CIRCLED DIGIT NINE] + case '\u24FD': + // ⓽ [DOUBLE CIRCLED DIGIT NINE] + case '\u277E': + // � [DINGBAT NEGATIVE CIRCLED DIGIT NINE] + case '\u2788': + // ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] + case '\u2792': + // âž’ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] + case '\uFF19': // ï¼™ [FULLWIDTH DIGIT NINE] + output[opos++] = '9'; + break; + + case '\u2490': // â’� [DIGIT NINE FULL STOP] + output[opos++] = '9'; + output[opos++] = '.'; + break; + + case '\u247C': // ⑼ [PARENTHESIZED DIGIT NINE] + output[opos++] = '('; + output[opos++] = '9'; + output[opos++] = ')'; + break; + + case '\u2469': + // â‘© [CIRCLED NUMBER TEN] + case '\u24FE': + // ⓾ [DOUBLE CIRCLED NUMBER TEN] + case '\u277F': + // � [DINGBAT NEGATIVE CIRCLED NUMBER TEN] + case '\u2789': + // ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] + case '\u2793': // âž“ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] + output[opos++] = '1'; + output[opos++] = '0'; + break; + + case '\u2491': // â’‘ [NUMBER TEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '0'; + output[opos++] = '.'; + break; + + case '\u247D': // ⑽ [PARENTHESIZED NUMBER TEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '0'; + output[opos++] = ')'; + break; + + case '\u246A': + // ⑪ [CIRCLED NUMBER ELEVEN] + case '\u24EB': // â“« [NEGATIVE CIRCLED NUMBER ELEVEN] + output[opos++] = '1'; + output[opos++] = '1'; + break; + + case '\u2492': // â’’ [NUMBER ELEVEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '1'; + output[opos++] = '.'; + break; + + case '\u247E': // ⑾ [PARENTHESIZED NUMBER ELEVEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '1'; + output[opos++] = ')'; + break; + + case '\u246B': + // â‘« [CIRCLED NUMBER TWELVE] + case '\u24EC': // ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] + output[opos++] = '1'; + output[opos++] = '2'; + break; + + case '\u2493': // â’“ [NUMBER TWELVE FULL STOP] + output[opos++] = '1'; + output[opos++] = '2'; + output[opos++] = '.'; + break; + + case '\u247F': // â‘¿ [PARENTHESIZED NUMBER TWELVE] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '2'; + output[opos++] = ')'; + break; + + case '\u246C': + // ⑬ [CIRCLED NUMBER THIRTEEN] + case '\u24ED': // â“­ [NEGATIVE CIRCLED NUMBER THIRTEEN] + output[opos++] = '1'; + output[opos++] = '3'; + break; + + case '\u2494': // â’�? [NUMBER THIRTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '3'; + output[opos++] = '.'; + break; + + case '\u2480': // â’€ [PARENTHESIZED NUMBER THIRTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '3'; + output[opos++] = ')'; + break; + + case '\u246D': + // â‘­ [CIRCLED NUMBER FOURTEEN] + case '\u24EE': // â“® [NEGATIVE CIRCLED NUMBER FOURTEEN] + output[opos++] = '1'; + output[opos++] = '4'; + break; + + case '\u2495': // â’• [NUMBER FOURTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '4'; + output[opos++] = '.'; + break; + + case '\u2481': // â’� [PARENTHESIZED NUMBER FOURTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '4'; + output[opos++] = ')'; + break; + + case '\u246E': + // â‘® [CIRCLED NUMBER FIFTEEN] + case '\u24EF': // ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] + output[opos++] = '1'; + output[opos++] = '5'; + break; + + case '\u2496': // â’– [NUMBER FIFTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '5'; + output[opos++] = '.'; + break; + + case '\u2482': // â’‚ [PARENTHESIZED NUMBER FIFTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '5'; + output[opos++] = ')'; + break; + + case '\u246F': + // ⑯ [CIRCLED NUMBER SIXTEEN] + case '\u24F0': // â“° [NEGATIVE CIRCLED NUMBER SIXTEEN] + output[opos++] = '1'; + output[opos++] = '6'; + break; + + case '\u2497': // â’— [NUMBER SIXTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '6'; + output[opos++] = '.'; + break; + + case '\u2483': // â’ƒ [PARENTHESIZED NUMBER SIXTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '6'; + output[opos++] = ')'; + break; + + case '\u2470': + // â‘° [CIRCLED NUMBER SEVENTEEN] + case '\u24F1': // ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] + output[opos++] = '1'; + output[opos++] = '7'; + break; + + case '\u2498': // â’˜ [NUMBER SEVENTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '7'; + output[opos++] = '.'; + break; + + case '\u2484': // â’„ [PARENTHESIZED NUMBER SEVENTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '7'; + output[opos++] = ')'; + break; + + case '\u2471': + // ⑱ [CIRCLED NUMBER EIGHTEEN] + case '\u24F2': // ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] + output[opos++] = '1'; + output[opos++] = '8'; + break; + + case '\u2499': // â’™ [NUMBER EIGHTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '8'; + output[opos++] = '.'; + break; + + case '\u2485': // â’… [PARENTHESIZED NUMBER EIGHTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '8'; + output[opos++] = ')'; + break; + + case '\u2472': + // ⑲ [CIRCLED NUMBER NINETEEN] + case '\u24F3': // ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] + output[opos++] = '1'; + output[opos++] = '9'; + break; + + case '\u249A': // â’š [NUMBER NINETEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '9'; + output[opos++] = '.'; + break; + + case '\u2486': // â’† [PARENTHESIZED NUMBER NINETEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '9'; + output[opos++] = ')'; + break; + + case '\u2473': + // ⑳ [CIRCLED NUMBER TWENTY] + case '\u24F4': // â“´ [NEGATIVE CIRCLED NUMBER TWENTY] + output[opos++] = '2'; + output[opos++] = '0'; + break; + + case '\u249B': // â’› [NUMBER TWENTY FULL STOP] + output[opos++] = '2'; + output[opos++] = '0'; + output[opos++] = '.'; + break; + + case '\u2487': // â’‡ [PARENTHESIZED NUMBER TWENTY] + output[opos++] = '('; + output[opos++] = '2'; + output[opos++] = '0'; + output[opos++] = ')'; + break; + + case '\u00AB': + // « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] + case '\u00BB': + // » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] + case '\u201C': + // “ [LEFT DOUBLE QUOTATION MARK] + case '\u201D': + // � [RIGHT DOUBLE QUOTATION MARK] + case '\u201E': + // „ [DOUBLE LOW-9 QUOTATION MARK] + case '\u2033': + // ″ [DOUBLE PRIME] + case '\u2036': + // ‶ [REVERSED DOUBLE PRIME] + case '\u275D': + // � [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] + case '\u275E': + // � [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] + case '\u276E': + // � [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] + case '\u276F': + // � [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] + case '\uFF02': // " [FULLWIDTH QUOTATION MARK] + output[opos++] = '"'; + break; + + case '\u2018': + // ‘ [LEFT SINGLE QUOTATION MARK] + case '\u2019': + // ’ [RIGHT SINGLE QUOTATION MARK] + case '\u201A': + // ‚ [SINGLE LOW-9 QUOTATION MARK] + case '\u201B': + // ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] + case '\u2032': + // ′ [PRIME] + case '\u2035': + // ‵ [REVERSED PRIME] + case '\u2039': + // ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] + case '\u203A': + // › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] + case '\u275B': + // � [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] + case '\u275C': + // � [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] + case '\uFF07': // ' [FULLWIDTH APOSTROPHE] + output[opos++] = '\''; + break; + + case '\u2010': + // � [HYPHEN] + case '\u2011': + // ‑ [NON-BREAKING HYPHEN] + case '\u2012': + // ‒ [FIGURE DASH] + case '\u2013': + // – [EN DASH] + case '\u2014': + // �? [EM DASH] + case '\u207B': + // � [SUPERSCRIPT MINUS] + case '\u208B': + // â‚‹ [SUBSCRIPT MINUS] + case '\uFF0D': // � [FULLWIDTH HYPHEN-MINUS] + output[opos++] = '-'; + break; + + case '\u2045': + // � [LEFT SQUARE BRACKET WITH QUILL] + case '\u2772': + // � [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] + case '\uFF3B': // ï¼» [FULLWIDTH LEFT SQUARE BRACKET] + output[opos++] = '['; + break; + + case '\u2046': + // � [RIGHT SQUARE BRACKET WITH QUILL] + case '\u2773': + // � [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] + case '\uFF3D': // ï¼½ [FULLWIDTH RIGHT SQUARE BRACKET] + output[opos++] = ']'; + break; + + case '\u207D': + // � [SUPERSCRIPT LEFT PARENTHESIS] + case '\u208D': + // � [SUBSCRIPT LEFT PARENTHESIS] + case '\u2768': + // � [MEDIUM LEFT PARENTHESIS ORNAMENT] + case '\u276A': + // � [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] + case '\uFF08': // ( [FULLWIDTH LEFT PARENTHESIS] + output[opos++] = '('; + break; + + case '\u2E28': // ⸨ [LEFT DOUBLE PARENTHESIS] + output[opos++] = '('; + output[opos++] = '('; + break; + + case '\u207E': + // � [SUPERSCRIPT RIGHT PARENTHESIS] + case '\u208E': + // ₎ [SUBSCRIPT RIGHT PARENTHESIS] + case '\u2769': + // � [MEDIUM RIGHT PARENTHESIS ORNAMENT] + case '\u276B': + // � [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] + case '\uFF09': // ) [FULLWIDTH RIGHT PARENTHESIS] + output[opos++] = ')'; + break; + + case '\u2E29': // ⸩ [RIGHT DOUBLE PARENTHESIS] + output[opos++] = ')'; + output[opos++] = ')'; + break; + + case '\u276C': + // � [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] + case '\u2770': + // � [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] + case '\uFF1C': // < [FULLWIDTH LESS-THAN SIGN] + output[opos++] = '<'; + break; + + case '\u276D': + // � [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] + case '\u2771': + // � [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] + case '\uFF1E': // > [FULLWIDTH GREATER-THAN SIGN] + output[opos++] = '>'; + break; + + case '\u2774': + // � [MEDIUM LEFT CURLY BRACKET ORNAMENT] + case '\uFF5B': // ï½› [FULLWIDTH LEFT CURLY BRACKET] + output[opos++] = '{'; + break; + + case '\u2775': + // � [MEDIUM RIGHT CURLY BRACKET ORNAMENT] + case '\uFF5D': // � [FULLWIDTH RIGHT CURLY BRACKET] + output[opos++] = '}'; + break; + + case '\u207A': + // � [SUPERSCRIPT PLUS SIGN] + case '\u208A': + // ₊ [SUBSCRIPT PLUS SIGN] + case '\uFF0B': // + [FULLWIDTH PLUS SIGN] + output[opos++] = '+'; + break; + + case '\u207C': + // � [SUPERSCRIPT EQUALS SIGN] + case '\u208C': + // ₌ [SUBSCRIPT EQUALS SIGN] + case '\uFF1D': // � [FULLWIDTH EQUALS SIGN] + output[opos++] = '='; + break; + + case '\uFF01': // � [FULLWIDTH EXCLAMATION MARK] + output[opos++] = '!'; + break; + + case '\u203C': // ‼ [DOUBLE EXCLAMATION MARK] + output[opos++] = '!'; + output[opos++] = '!'; + break; + + case '\u2049': // � [EXCLAMATION QUESTION MARK] + output[opos++] = '!'; + output[opos++] = '?'; + break; + + case '\uFF03': // # [FULLWIDTH NUMBER SIGN] + output[opos++] = '#'; + break; + + case '\uFF04': // $ [FULLWIDTH DOLLAR SIGN] + output[opos++] = '$'; + break; + + case '\u2052': + // � [COMMERCIAL MINUS SIGN] + case '\uFF05': // ï¼… [FULLWIDTH PERCENT SIGN] + output[opos++] = '%'; + break; + + case '\uFF06': // & [FULLWIDTH AMPERSAND] + output[opos++] = '&'; + break; + + case '\u204E': + // � [LOW ASTERISK] + case '\uFF0A': // * [FULLWIDTH ASTERISK] + output[opos++] = '*'; + break; + + case '\uFF0C': // , [FULLWIDTH COMMA] + output[opos++] = ','; + break; + + case '\uFF0E': // . [FULLWIDTH FULL STOP] + output[opos++] = '.'; + break; + + case '\u2044': + // � [FRACTION SLASH] + case '\uFF0F': // � [FULLWIDTH SOLIDUS] + output[opos++] = '/'; + break; + + case '\uFF1A': // : [FULLWIDTH COLON] + output[opos++] = ':'; + break; + + case '\u204F': + // � [REVERSED SEMICOLON] + case '\uFF1B': // ï¼› [FULLWIDTH SEMICOLON] + output[opos++] = ';'; + break; + + case '\uFF1F': // ? [FULLWIDTH QUESTION MARK] + output[opos++] = '?'; + break; + + case '\u2047': // � [DOUBLE QUESTION MARK] + output[opos++] = '?'; + output[opos++] = '?'; + break; + + case '\u2048': // � [QUESTION EXCLAMATION MARK] + output[opos++] = '?'; + output[opos++] = '!'; + break; + + case '\uFF20': // ï¼  [FULLWIDTH COMMERCIAL AT] + output[opos++] = '@'; + break; + + case '\uFF3C': // ï¼¼ [FULLWIDTH REVERSE SOLIDUS] + output[opos++] = '\\'; + break; + + case '\u2038': + // ‸ [CARET] + case '\uFF3E': // ï¼¾ [FULLWIDTH CIRCUMFLEX ACCENT] + output[opos++] = '^'; + break; + + case '\uFF3F': // _ [FULLWIDTH LOW LINE] + output[opos++] = '_'; + break; + + case '\u2053': + // � [SWUNG DASH] + case '\uFF5E': // ~ [FULLWIDTH TILDE] + output[opos++] = '~'; + break; + + // BEGIN CUSTOM TRANSLITERATION OF CYRILIC CHARS + + // russian uppercase "А Б В Г Д Е Ё Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я" + // russian lowercase "а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я" + + // notes + // read http://www.vesic.org/english/blog/c-sharp/transliteration-easy-way-microsoft-transliteration-utility/ + // should we look into MS Transliteration Utility (http://msdn.microsoft.com/en-US/goglobal/bb688104.aspx) + // also UnicodeSharpFork https://bitbucket.org/DimaStefantsov/unidecodesharpfork + // also Transliterator http://transliterator.codeplex.com/ + // + // in any case it would be good to generate all those "case" statements instead of writing them by hand + // time for a T4 template? + // also we should support extensibility so ppl can register more cases in external code + + // TODO: transliterates Анастасия as Anastasiya, and not Anastasia + // Ольга --> Ol'ga, Татьяна --> Tat'yana -- that's bad (?) + // Note: should ä (German umlaut) become a or ae ? + case '\u0410': // А + output[opos++] = 'A'; + break; + case '\u0430': // а + output[opos++] = 'a'; + break; + case '\u0411': // Б + output[opos++] = 'B'; + break; + case '\u0431': // б + output[opos++] = 'b'; + break; + case '\u0412': // В + output[opos++] = 'V'; + break; + case '\u0432': // в + output[opos++] = 'v'; + break; + case '\u0413': // Г + output[opos++] = 'G'; + break; + case '\u0433': // г + output[opos++] = 'g'; + break; + case '\u0414': // Д + output[opos++] = 'D'; + break; + case '\u0434': // д + output[opos++] = 'd'; + break; + case '\u0415': // Е + output[opos++] = 'E'; + break; + case '\u0435': // е + output[opos++] = 'e'; + break; + case '\u0401': // Ё + output[opos++] = 'E'; // alt. Yo + break; + case '\u0451': // ё + output[opos++] = 'e'; // alt. yo + break; + case '\u0416': // Ж + output[opos++] = 'Z'; + output[opos++] = 'h'; + break; + case '\u0436': // ж + output[opos++] = 'z'; + output[opos++] = 'h'; + break; + case '\u0417': // З + output[opos++] = 'Z'; + break; + case '\u0437': // з + output[opos++] = 'z'; + break; + case '\u0418': // И + output[opos++] = 'I'; + break; + case '\u0438': // и + output[opos++] = 'i'; + break; + case '\u0419': // Й + output[opos++] = 'I'; // alt. Y, J + break; + case '\u0439': // й + output[opos++] = 'i'; // alt. y, j + break; + case '\u041A': // К + output[opos++] = 'K'; + break; + case '\u043A': // к + output[opos++] = 'k'; + break; + case '\u041B': // Л + output[opos++] = 'L'; + break; + case '\u043B': // л + output[opos++] = 'l'; + break; + case '\u041C': // М + output[opos++] = 'M'; + break; + case '\u043C': // м + output[opos++] = 'm'; + break; + case '\u041D': // Н + output[opos++] = 'N'; + break; + case '\u043D': // н + output[opos++] = 'n'; + break; + case '\u041E': // О + output[opos++] = 'O'; + break; + case '\u043E': // о + output[opos++] = 'o'; + break; + case '\u041F': // П + output[opos++] = 'P'; + break; + case '\u043F': // п + output[opos++] = 'p'; + break; + case '\u0420': // Р + output[opos++] = 'R'; + break; + case '\u0440': // р + output[opos++] = 'r'; + break; + case '\u0421': // С + output[opos++] = 'S'; + break; + case '\u0441': // с + output[opos++] = 's'; + break; + case '\u0422': // Т + output[opos++] = 'T'; + break; + case '\u0442': // т + output[opos++] = 't'; + break; + case '\u0423': // У + output[opos++] = 'U'; + break; + case '\u0443': // у + output[opos++] = 'u'; + break; + case '\u0424': // Ф + output[opos++] = 'F'; + break; + case '\u0444': // ф + output[opos++] = 'f'; + break; + case '\u0425': // Х + output[opos++] = 'K'; // alt. X + output[opos++] = 'h'; + break; + case '\u0445': // х + output[opos++] = 'k'; // alt. x + output[opos++] = 'h'; + break; + case '\u0426': // Ц + output[opos++] = 'F'; + break; + case '\u0446': // ц + output[opos++] = 'f'; + break; + case '\u0427': // Ч + output[opos++] = 'C'; // alt. Ts, C + output[opos++] = 'h'; + break; + case '\u0447': // ч + output[opos++] = 'c'; // alt. ts, c + output[opos++] = 'h'; + break; + case '\u0428': // Ш + output[opos++] = 'S'; // alt. Ch, S + output[opos++] = 'h'; + break; + case '\u0448': // ш + output[opos++] = 's'; // alt. ch, s + output[opos++] = 'h'; + break; + case '\u0429': // Щ + output[opos++] = 'S'; // alt. Shch, Sc + output[opos++] = 'h'; + break; + case '\u0449': // щ + output[opos++] = 's'; // alt. shch, sc + output[opos++] = 'h'; + break; + case '\u042A': // Ъ + output[opos++] = '"'; // " + break; + case '\u044A': // ъ + output[opos++] = '"'; // " + break; + case '\u042B': // Ы + output[opos++] = 'Y'; + break; + case '\u044B': // ы + output[opos++] = 'y'; + break; + case '\u042C': // Ь + output[opos++] = '\''; // ' + break; + case '\u044C': // ь + output[opos++] = '\''; // ' + break; + case '\u042D': // Э + output[opos++] = 'E'; + break; + case '\u044D': // э + output[opos++] = 'e'; + break; + case '\u042E': // Ю + output[opos++] = 'Y'; // alt. Ju + output[opos++] = 'u'; + break; + case '\u044E': // ю + output[opos++] = 'y'; // alt. ju + output[opos++] = 'u'; + break; + case '\u042F': // Я + output[opos++] = 'Y'; // alt. Ja + output[opos++] = 'a'; + break; + case '\u044F': // я + output[opos++] = 'y'; // alt. ja + output[opos++] = 'a'; + break; + + // BEGIN EXTRA + /* + case '£': + output[opos++] = 'G'; + output[opos++] = 'B'; + output[opos++] = 'P'; + break; + + case '€': + output[opos++] = 'E'; + output[opos++] = 'U'; + output[opos++] = 'R'; + break; + + case '©': + output[opos++] = '('; + output[opos++] = 'C'; + output[opos++] = ')'; + break; + */ + default: + // if (ToMoreAscii(input, ipos, output, ref opos)) + // break; + + // if (!char.IsLetterOrDigit(c)) // that would not catch eg 汉 unfortunately + // output[opos++] = '?'; + // else + // output[opos++] = c; + + // strict ASCII + output[opos++] = fail; + + break; + } + } + } + + // private static bool ToMoreAscii(char[] input, int ipos, char[] output, ref int opos) + // { + // var c = input[ipos]; + + // switch (c) + // { + // case '£': + // output[opos++] = 'G'; + // output[opos++] = 'B'; + // output[opos++] = 'P'; + // break; + + // case '€': + // output[opos++] = 'E'; + // output[opos++] = 'U'; + // output[opos++] = 'R'; + // break; + + // case '©': + // output[opos++] = '('; + // output[opos++] = 'C'; + // output[opos++] = ')'; + // break; + + // default: + // return false; + // } + + // return true; + // } } diff --git a/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs b/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs index 340de80c96..09c904b7bc 100644 --- a/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs +++ b/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs @@ -1,29 +1,30 @@ -using System; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Gets the current server's based on active servers registered with +/// +/// +/// +/// This is the default service which determines a server's role by using a master election process. +/// The scheduling publisher election process doesn't occur until just after startup so this election process doesn't +/// really affect the primary startup phase. +/// +public sealed class ElectedServerRoleAccessor : IServerRoleAccessor { + private readonly IServerRegistrationService _registrationService; + /// - /// Gets the current server's based on active servers registered with + /// Initializes a new instance of the class. /// - /// - /// This is the default service which determines a server's role by using a master election process. - /// The scheduling publisher election process doesn't occur until just after startup so this election process doesn't really affect the primary startup phase. - /// - public sealed class ElectedServerRoleAccessor : IServerRoleAccessor - { - private readonly IServerRegistrationService _registrationService; + /// The registration service. + /// Some options. + public ElectedServerRoleAccessor(IServerRegistrationService registrationService) => _registrationService = + registrationService ?? throw new ArgumentNullException(nameof(registrationService)); - /// - /// Initializes a new instance of the class. - /// - /// The registration service. - /// Some options. - public ElectedServerRoleAccessor(IServerRegistrationService registrationService) => _registrationService = registrationService ?? throw new ArgumentNullException(nameof(registrationService)); - - /// - /// Gets the role of the current server in the application environment. - /// - public ServerRole CurrentServerRole => _registrationService.GetCurrentServerRole(); - } + /// + /// Gets the role of the current server in the application environment. + /// + public ServerRole CurrentServerRole => _registrationService.GetCurrentServerRole(); } diff --git a/src/Umbraco.Core/Sync/IServerAddress.cs b/src/Umbraco.Core/Sync/IServerAddress.cs index 4de7490d8f..cc9da01db0 100644 --- a/src/Umbraco.Core/Sync/IServerAddress.cs +++ b/src/Umbraco.Core/Sync/IServerAddress.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Provides the address of a server. +/// +public interface IServerAddress { /// - /// Provides the address of a server. + /// Gets the server address. /// - public interface IServerAddress - { - /// - /// Gets the server address. - /// - string? ServerAddress { get; } + string? ServerAddress { get; } - // TODO: Should probably add things like port, protocol, server name, app id - } + // TODO: Should probably add things like port, protocol, server name, app id } diff --git a/src/Umbraco.Core/Sync/IServerMessenger.cs b/src/Umbraco.Core/Sync/IServerMessenger.cs index e58cfe9bc0..49cd397e2d 100644 --- a/src/Umbraco.Core/Sync/IServerMessenger.cs +++ b/src/Umbraco.Core/Sync/IServerMessenger.cs @@ -1,83 +1,81 @@ -using System; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Transmits distributed cache notifications for all servers of a load balanced environment. +/// +/// Also ensures that the notification is processed on the local environment. +public interface IServerMessenger { /// - /// Transmits distributed cache notifications for all servers of a load balanced environment. + /// Called to synchronize a server with queued notifications /// - /// Also ensures that the notification is processed on the local environment. - public interface IServerMessenger - { - /// - /// Called to synchronize a server with queued notifications - /// - void Sync(); + void Sync(); - /// - /// Called to send/commit the queued messages created with the Perform methods - /// - void SendMessages(); + /// + /// Called to send/commit the queued messages created with the Perform methods + /// + void SendMessages(); - /// - /// Notifies the distributed cache, for a specified . - /// - /// The ICacheRefresher. - /// The notification content. - void QueueRefresh(ICacheRefresher refresher, TPayload[] payload); + /// + /// Notifies the distributed cache, for a specified . + /// + /// The ICacheRefresher. + /// The notification content. + void QueueRefresh(ICacheRefresher refresher, TPayload[] payload); - /// - /// Notifies the distributed cache of specified item invalidation, for a specified . - /// - /// The type of the invalidated items. - /// The ICacheRefresher. - /// A function returning the unique identifier of items. - /// The invalidated items. - void QueueRefresh(ICacheRefresher refresher, Func getNumericId, params T[] instances); + /// + /// Notifies the distributed cache of specified item invalidation, for a specified . + /// + /// The type of the invalidated items. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. + void QueueRefresh(ICacheRefresher refresher, Func getNumericId, params T[] instances); - /// - /// Notifies the distributed cache of specified item invalidation, for a specified . - /// - /// The type of the invalidated items. - /// The ICacheRefresher. - /// A function returning the unique identifier of items. - /// The invalidated items. - void QueueRefresh(ICacheRefresher refresher, Func getGuidId, params T[] instances); + /// + /// Notifies the distributed cache of specified item invalidation, for a specified . + /// + /// The type of the invalidated items. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. + void QueueRefresh(ICacheRefresher refresher, Func getGuidId, params T[] instances); - /// - /// Notifies all servers of specified items removal, for a specified . - /// - /// The type of the removed items. - /// The ICacheRefresher. - /// A function returning the unique identifier of items. - /// The removed items. - void QueueRemove(ICacheRefresher refresher, Func getNumericId, params T[] instances); + /// + /// Notifies all servers of specified items removal, for a specified . + /// + /// The type of the removed items. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. + void QueueRemove(ICacheRefresher refresher, Func getNumericId, params T[] instances); - /// - /// Notifies all servers of specified items removal, for a specified . - /// - /// The ICacheRefresher. - /// The unique identifiers of the removed items. - void QueueRemove(ICacheRefresher refresher, params int[] numericIds); + /// + /// Notifies all servers of specified items removal, for a specified . + /// + /// The ICacheRefresher. + /// The unique identifiers of the removed items. + void QueueRemove(ICacheRefresher refresher, params int[] numericIds); - /// - /// Notifies all servers of specified items invalidation, for a specified . - /// - /// The ICacheRefresher. - /// The unique identifiers of the invalidated items. - void QueueRefresh(ICacheRefresher refresher, params int[] numericIds); + /// + /// Notifies all servers of specified items invalidation, for a specified . + /// + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. + void QueueRefresh(ICacheRefresher refresher, params int[] numericIds); - /// - /// Notifies all servers of specified items invalidation, for a specified . - /// - /// The ICacheRefresher. - /// The unique identifiers of the invalidated items. - void QueueRefresh(ICacheRefresher refresher, params Guid[] guidIds); + /// + /// Notifies all servers of specified items invalidation, for a specified . + /// + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. + void QueueRefresh(ICacheRefresher refresher, params Guid[] guidIds); - /// - /// Notifies all servers of a global invalidation for a specified . - /// - /// The ICacheRefresher. - void QueueRefreshAll(ICacheRefresher refresher); - } + /// + /// Notifies all servers of a global invalidation for a specified . + /// + /// The ICacheRefresher. + void QueueRefreshAll(ICacheRefresher refresher); } diff --git a/src/Umbraco.Core/Sync/IServerRoleAccessor.cs b/src/Umbraco.Core/Sync/IServerRoleAccessor.cs index 1ebd59b26d..aed70b0f50 100644 --- a/src/Umbraco.Core/Sync/IServerRoleAccessor.cs +++ b/src/Umbraco.Core/Sync/IServerRoleAccessor.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Gets the current server's +/// +public interface IServerRoleAccessor { /// - /// Gets the current server's + /// Gets the role of the current server in the application environment. /// - public interface IServerRoleAccessor - { - /// - /// Gets the role of the current server in the application environment. - /// - ServerRole CurrentServerRole { get; } - } + ServerRole CurrentServerRole { get; } } diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index 0c616a4e68..1d7d085f90 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Retrieve the for the application during startup +/// +public interface ISyncBootStateAccessor { /// - /// Retrieve the for the application during startup + /// Get the /// - public interface ISyncBootStateAccessor - { - /// - /// Get the - /// - /// - SyncBootState GetSyncBootState(); - } + /// + SyncBootState GetSyncBootState(); } diff --git a/src/Umbraco.Core/Sync/MessageType.cs b/src/Umbraco.Core/Sync/MessageType.cs index 5164428632..282aebeb54 100644 --- a/src/Umbraco.Core/Sync/MessageType.cs +++ b/src/Umbraco.Core/Sync/MessageType.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// The message type to be used for syncing across servers. +/// +public enum MessageType { - /// - /// The message type to be used for syncing across servers. - /// - public enum MessageType - { - RefreshAll, - RefreshById, - RefreshByJson, - RemoveById, - RefreshByInstance, - RemoveByInstance, - RefreshByPayload - } + RefreshAll, + RefreshById, + RefreshByJson, + RemoveById, + RefreshByInstance, + RemoveByInstance, + RefreshByPayload, } diff --git a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs index 0dcfa471db..4040edd8f7 100644 --- a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Boot state implementation for when umbraco is not in the run state +/// +public sealed class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor { - /// - /// Boot state implementation for when umbraco is not in the run state - /// - public sealed class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor - { - public SyncBootState GetSyncBootState() => SyncBootState.Unknown; - } + public SyncBootState GetSyncBootState() => SyncBootState.Unknown; } diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index b8609410ab..2a80dbf95f 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -1,217 +1,220 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +[Serializable] +public class RefreshInstruction { - [Serializable] - public class RefreshInstruction + // NOTE + // that class should be refactored + // but at the moment it is exposed in CacheRefresher webservice + // so for the time being we keep it as-is for backward compatibility reasons + + // need this public, parameter-less constructor so the web service messenger + // can de-serialize the instructions it receives + + /// + /// Initializes a new instance of the class. + /// + /// + /// Need this public, parameter-less constructor so the web service messenger can de-serialize the instructions it + /// receives. + /// + public RefreshInstruction() => + + // Set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db + JsonIdCount = 1; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Need this public one so it can be de-serialized - used by the Json thing + /// otherwise, should use GetInstructions(...) + /// + public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload) + : this() { - // NOTE - // that class should be refactored - // but at the moment it is exposed in CacheRefresher webservice - // so for the time being we keep it as-is for backward compatibility reasons - - // need this public, parameter-less constructor so the web service messenger - // can de-serialize the instructions it receives - - /// - /// Initializes a new instance of the class. - /// - /// - /// Need this public, parameter-less constructor so the web service messenger can de-serialize the instructions it receives. - /// - public RefreshInstruction() => - - // Set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db - JsonIdCount = 1; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Need this public one so it can be de-serialized - used by the Json thing - /// otherwise, should use GetInstructions(...) - /// - public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload) - : this() - { - RefresherId = refresherId; - RefreshType = refreshType; - GuidId = guidId; - IntId = intId; - JsonIds = jsonIds; - JsonPayload = jsonPayload; - } - - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) - : this() - { - RefresherId = refresher.RefresherUniqueId; - RefreshType = refreshType; - } - - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId) - : this(refresher, refreshType) => GuidId = guidId; - - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, int intId) - : this(refresher, refreshType) => IntId = intId; - - /// - /// A private constructor to create a new instance - /// - /// - /// When the refresh method is we know how many Ids are being refreshed so we know the instruction - /// count which will be taken into account when we store this count in the database. - /// - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string? json, int idCount = 1) - : this(refresher, refreshType) - { - JsonIdCount = idCount; - - if (refreshType == RefreshMethodType.RefreshByJson) - { - JsonPayload = json; - } - else - { - JsonIds = json; - } - } - - public static IEnumerable GetInstructions( - ICacheRefresher refresher, - IJsonSerializer jsonSerializer, - MessageType messageType, - IEnumerable? ids, - Type? idType, - string? json) - { - switch (messageType) - { - case MessageType.RefreshAll: - return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshAll) }; - - case MessageType.RefreshByJson: - return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByJson, json) }; - - case MessageType.RefreshById: - if (idType == null) - { - throw new InvalidOperationException("Cannot refresh by id if idType is null."); - } - - if (idType == typeof(int)) - { - // Bulk of ints is supported - var intIds = ids?.Cast().ToArray(); - return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, jsonSerializer.Serialize(intIds), intIds?.Length ?? 0) }; - } - - // Else must be guids, bulk of guids is not supported, so iterate. - return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid) x)) ?? Enumerable.Empty(); - - case MessageType.RemoveById: - if (idType == null) - { - throw new InvalidOperationException("Cannot remove by id if idType is null."); - } - - // Must be ints, bulk-remove is not supported, so iterate. - return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RemoveById, (int) x)) ?? Enumerable.Empty(); - //return new[] { new RefreshInstruction(refresher, RefreshMethodType.RemoveByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; - - default: - //case MessageType.RefreshByInstance: - //case MessageType.RemoveByInstance: - throw new ArgumentOutOfRangeException("messageType"); - } - } - - /// - /// Gets or sets the refresh action type. - /// - public RefreshMethodType RefreshType { get; set; } - - /// - /// Gets or sets the refresher unique identifier. - /// - public Guid RefresherId { get; set; } - - /// - /// Gets or sets the Guid data value. - /// - public Guid GuidId { get; set; } - - /// - /// Gets or sets the int data value. - /// - public int IntId { get; set; } - - /// - /// Gets or sets the ids data value. - /// - public string? JsonIds { get; set; } - - /// - /// Gets or sets the number of Ids contained in the JsonIds json value. - /// - /// - /// This is used to determine the instruction count per row. - /// - public int JsonIdCount { get; set; } - - /// - /// Gets or sets the payload data value. - /// - public string? JsonPayload { get; set; } - - protected bool Equals(RefreshInstruction other) => - RefreshType == other.RefreshType - && RefresherId.Equals(other.RefresherId) - && GuidId.Equals(other.GuidId) - && IntId == other.IntId - && string.Equals(JsonIds, other.JsonIds) - && string.Equals(JsonPayload, other.JsonPayload); - - public override bool Equals(object? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - if (other.GetType() != GetType()) - { - return false; - } - - return Equals((RefreshInstruction) other); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (int) RefreshType; - hashCode = (hashCode*397) ^ RefresherId.GetHashCode(); - hashCode = (hashCode*397) ^ GuidId.GetHashCode(); - hashCode = (hashCode*397) ^ IntId; - hashCode = (hashCode*397) ^ (JsonIds != null ? JsonIds.GetHashCode() : 0); - hashCode = (hashCode*397) ^ (JsonPayload != null ? JsonPayload.GetHashCode() : 0); - return hashCode; - } - } - - public static bool operator ==(RefreshInstruction left, RefreshInstruction right) => Equals(left, right); - - public static bool operator !=(RefreshInstruction left, RefreshInstruction right) => Equals(left, right) == false; + RefresherId = refresherId; + RefreshType = refreshType; + GuidId = guidId; + IntId = intId; + JsonIds = jsonIds; + JsonPayload = jsonPayload; } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) + : this() + { + RefresherId = refresher.RefresherUniqueId; + RefreshType = refreshType; + } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId) + : this(refresher, refreshType) => GuidId = guidId; + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, int intId) + : this(refresher, refreshType) => IntId = intId; + + /// + /// A private constructor to create a new instance + /// + /// + /// When the refresh method is we know how many Ids are being refreshed + /// so we know the instruction + /// count which will be taken into account when we store this count in the database. + /// + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string? json, int idCount = 1) + : this(refresher, refreshType) + { + JsonIdCount = idCount; + + if (refreshType == RefreshMethodType.RefreshByJson) + { + JsonPayload = json; + } + else + { + JsonIds = json; + } + } + + /// + /// Gets or sets the refresh action type. + /// + public RefreshMethodType RefreshType { get; set; } + + /// + /// Gets or sets the refresher unique identifier. + /// + public Guid RefresherId { get; set; } + + /// + /// Gets or sets the Guid data value. + /// + public Guid GuidId { get; set; } + + /// + /// Gets or sets the int data value. + /// + public int IntId { get; set; } + + /// + /// Gets or sets the ids data value. + /// + public string? JsonIds { get; set; } + + /// + /// Gets or sets the number of Ids contained in the JsonIds json value. + /// + /// + /// This is used to determine the instruction count per row. + /// + public int JsonIdCount { get; set; } + + /// + /// Gets or sets the payload data value. + /// + public string? JsonPayload { get; set; } + + public static bool operator ==(RefreshInstruction left, RefreshInstruction right) => Equals(left, right); + + public static IEnumerable GetInstructions( + ICacheRefresher refresher, + IJsonSerializer jsonSerializer, + MessageType messageType, + IEnumerable? ids, + Type? idType, + string? json) + { + switch (messageType) + { + case MessageType.RefreshAll: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshAll) }; + + case MessageType.RefreshByJson: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByJson, json) }; + + case MessageType.RefreshById: + if (idType == null) + { + throw new InvalidOperationException("Cannot refresh by id if idType is null."); + } + + if (idType == typeof(int)) + { + // Bulk of ints is supported + var intIds = ids?.Cast().ToArray(); + return new[] + { + new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, jsonSerializer.Serialize(intIds), intIds?.Length ?? 0), + }; + } + + // Else must be guids, bulk of guids is not supported, so iterate. + return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid)x)) ?? + Enumerable.Empty(); + + case MessageType.RemoveById: + if (idType == null) + { + throw new InvalidOperationException("Cannot remove by id if idType is null."); + } + + // Must be ints, bulk-remove is not supported, so iterate. + return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RemoveById, (int)x)) ?? + Enumerable.Empty(); + + // return new[] { new RefreshInstruction(refresher, RefreshMethodType.RemoveByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; + default: + // case MessageType.RefreshByInstance: + // case MessageType.RemoveByInstance: + throw new ArgumentOutOfRangeException("messageType"); + } + } + + public override bool Equals(object? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + + return Equals((RefreshInstruction)other); + } + + protected bool Equals(RefreshInstruction other) => + RefreshType == other.RefreshType + && RefresherId.Equals(other.RefresherId) + && GuidId.Equals(other.GuidId) + && IntId == other.IntId + && string.Equals(JsonIds, other.JsonIds) + && string.Equals(JsonPayload, other.JsonPayload); + + public override int GetHashCode() + { + unchecked + { + var hashCode = (int)RefreshType; + hashCode = (hashCode * 397) ^ RefresherId.GetHashCode(); + hashCode = (hashCode * 397) ^ GuidId.GetHashCode(); + hashCode = (hashCode * 397) ^ IntId; + hashCode = (hashCode * 397) ^ (JsonIds != null ? JsonIds.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (JsonPayload != null ? JsonPayload.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator !=(RefreshInstruction left, RefreshInstruction right) => Equals(left, right) == false; } diff --git a/src/Umbraco.Core/Sync/RefreshMethodType.cs b/src/Umbraco.Core/Sync/RefreshMethodType.cs index bf72423c1f..f249a4701e 100644 --- a/src/Umbraco.Core/Sync/RefreshMethodType.cs +++ b/src/Umbraco.Core/Sync/RefreshMethodType.cs @@ -1,44 +1,40 @@ -using System; +namespace Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Sync +/// +/// Describes refresh action type. +/// +[Serializable] +public enum RefreshMethodType { - /// - /// Describes refresh action type. - /// - [Serializable] - public enum RefreshMethodType - { - // NOTE - // that enum should get merged somehow with MessageType and renamed somehow - // but at the moment it is exposed in CacheRefresher webservice through RefreshInstruction - // so for the time being we keep it as-is for backward compatibility reasons + // NOTE + // that enum should get merged somehow with MessageType and renamed somehow + // but at the moment it is exposed in CacheRefresher webservice through RefreshInstruction + // so for the time being we keep it as-is for backward compatibility reasons + RefreshAll, + RefreshByGuid, + RefreshById, + RefreshByIds, + RefreshByJson, + RemoveById, - RefreshAll, - RefreshByGuid, - RefreshById, - RefreshByIds, - RefreshByJson, - RemoveById, + // would adding values break backward compatibility? + // RemoveByIds - // would adding values break backward compatibility? - //RemoveByIds + // these are MessageType values + // note that AnythingByInstance are local messages and cannot be distributed + /* + RefreshAll, + RefreshById, + RefreshByJson, + RemoveById, + RefreshByInstance, + RemoveByInstance + */ - // these are MessageType values - // note that AnythingByInstance are local messages and cannot be distributed - /* - RefreshAll, - RefreshById, - RefreshByJson, - RemoveById, - RefreshByInstance, - RemoveByInstance - */ - - // NOTE - // in the future we want - // RefreshAll - // RefreshById / ByInstance (support enumeration of int or guid) - // RemoveById / ByInstance (support enumeration of int or guid) - // Notify (for everything JSON) - } + // NOTE + // in the future we want + // RefreshAll + // RefreshById / ByInstance (support enumeration of int or guid) + // RemoveById / ByInstance (support enumeration of int or guid) + // Notify (for everything JSON) } diff --git a/src/Umbraco.Core/Sync/ServerRole.cs b/src/Umbraco.Core/Sync/ServerRole.cs index 9bfd4469b3..15f546fc35 100644 --- a/src/Umbraco.Core/Sync/ServerRole.cs +++ b/src/Umbraco.Core/Sync/ServerRole.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// The role of a server in an application environment. +/// +public enum ServerRole : byte { /// - /// The role of a server in an application environment. + /// The server role is unknown. /// - public enum ServerRole : byte - { - /// - /// The server role is unknown. - /// - Unknown = 0, + Unknown = 0, - /// - /// The server is the single server of a single-server environment. - /// - Single = 1, + /// + /// The server is the single server of a single-server environment. + /// + Single = 1, - /// - /// In a multi-servers environment, the server is a Subscriber server. - /// - Subscriber = 2, + /// + /// In a multi-servers environment, the server is a Subscriber server. + /// + Subscriber = 2, - /// - /// In a multi-servers environment, the server is the Scheduling Publisher. - /// - SchedulingPublisher = 3 - } + /// + /// In a multi-servers environment, the server is the Scheduling Publisher. + /// + SchedulingPublisher = 3, } diff --git a/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs b/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs index 2f4e85c5b1..f03f27d9e7 100644 --- a/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs +++ b/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Can be used when Umbraco is definitely not operating in a Load Balanced scenario to micro-optimize some startup +/// performance +/// +/// +/// The micro optimization is specifically to avoid a DB query just after the app starts up to determine the +/// +/// which by default is done with scheduling publisher election by a database query. The master election process +/// doesn't occur until just after startup +/// so this micro optimization doesn't really affect the primary startup phase. +/// +public class SingleServerRoleAccessor : IServerRoleAccessor { - /// - /// Can be used when Umbraco is definitely not operating in a Load Balanced scenario to micro-optimize some startup performance - /// - /// - /// The micro optimization is specifically to avoid a DB query just after the app starts up to determine the - /// which by default is done with scheduling publisher election by a database query. The master election process doesn't occur until just after startup - /// so this micro optimization doesn't really affect the primary startup phase. - /// - public class SingleServerRoleAccessor : IServerRoleAccessor - { - public ServerRole CurrentServerRole => ServerRole.Single; - } + public ServerRole CurrentServerRole => ServerRole.Single; } diff --git a/src/Umbraco.Core/Sync/SyncBootState.cs b/src/Umbraco.Core/Sync/SyncBootState.cs index 670930de31..6233ace01a 100644 --- a/src/Umbraco.Core/Sync/SyncBootState.cs +++ b/src/Umbraco.Core/Sync/SyncBootState.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +public enum SyncBootState { - public enum SyncBootState - { - /// - /// Unknown state. Treat as WarmBoot - /// - Unknown = 0, + /// + /// Unknown state. Treat as WarmBoot + /// + Unknown = 0, - /// - /// Cold boot. No Sync state - /// - ColdBoot = 1, + /// + /// Cold boot. No Sync state + /// + ColdBoot = 1, - /// - /// Warm boot. Sync state present - /// - WarmBoot = 2 - } + /// + /// Warm boot. Sync state present + /// + WarmBoot = 2, } diff --git a/src/Umbraco.Core/SystemLock.cs b/src/Umbraco.Core/SystemLock.cs index d39d6ecbce..0e47096c2e 100644 --- a/src/Umbraco.Core/SystemLock.cs +++ b/src/Umbraco.Core/SystemLock.cs @@ -1,194 +1,181 @@ -using System; -using System.Runtime.ConstrainedExecution; -using System.Threading; -using System.Threading.Tasks; +using System.Runtime.ConstrainedExecution; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-6-asynclock/ +// +// notes: +// - this is NOT a reader/writer lock +// - this is NOT a recursive lock +// +// using a named Semaphore here and not a Mutex because mutexes have thread +// affinity which does not work with async situations +// +// it is important that managed code properly release the Semaphore before +// going down else it will maintain the lock - however note that when the +// whole process (w3wp.exe) goes down and all handles to the Semaphore have +// been closed, the Semaphore system object is destroyed - so in any case +// an iisreset should clean up everything +// +public class SystemLock { - // https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-6-asynclock/ - // - // notes: - // - this is NOT a reader/writer lock - // - this is NOT a recursive lock - // - // using a named Semaphore here and not a Mutex because mutexes have thread - // affinity which does not work with async situations - // - // it is important that managed code properly release the Semaphore before - // going down else it will maintain the lock - however note that when the - // whole process (w3wp.exe) goes down and all handles to the Semaphore have - // been closed, the Semaphore system object is destroyed - so in any case - // an iisreset should clean up everything - // - public class SystemLock + private readonly IDisposable? _releaser; + private readonly Task? _releaserTask; + private readonly SemaphoreSlim? _semaphore; + private readonly Semaphore? _semaphore2; + + public SystemLock() + : this(null) { - private readonly SemaphoreSlim? _semaphore; - private readonly Semaphore? _semaphore2; - private readonly IDisposable? _releaser; - private readonly Task? _releaserTask; + } - public SystemLock() - : this(null) - { } - - public SystemLock(string? name) + public SystemLock(string? name) + { + // WaitOne() waits until count > 0 then decrements count + // Release() increments count + // initial count: the initial count value + // maximum count: the max value of count, and then Release() throws + if (string.IsNullOrWhiteSpace(name)) { - // WaitOne() waits until count > 0 then decrements count - // Release() increments count - // initial count: the initial count value - // maximum count: the max value of count, and then Release() throws + // anonymous semaphore + // use one unique releaser, that will not release the semaphore when finalized + // because the semaphore is destroyed anyway if the app goes down + _semaphore = new SemaphoreSlim(1, 1); // create a local (to the app domain) semaphore + _releaser = new SemaphoreSlimReleaser(_semaphore); + _releaserTask = Task.FromResult(_releaser); + } + else + { + // named semaphore + // use dedicated releasers, that will release the semaphore when finalized + // because the semaphore is system-wide and we cannot leak counts + _semaphore2 = new Semaphore(1, 1, name); // create a system-wide named semaphore + } + } - if (string.IsNullOrWhiteSpace(name)) + public IDisposable? Lock() + { + if (_semaphore != null) + { + _semaphore.Wait(); + } + else + { + _semaphore2?.WaitOne(); + } + + return _releaser ?? CreateReleaser(); // anonymous vs named + } + + private IDisposable? CreateReleaser() => + + // for anonymous semaphore, use the unique releaser, else create a new one + _semaphore != null + ? _releaser // (IDisposable)new SemaphoreSlimReleaser(_semaphore) + : new NamedSemaphoreReleaser(_semaphore2); + + public IDisposable? Lock(int millisecondsTimeout) + { + var entered = _semaphore != null + ? _semaphore.Wait(millisecondsTimeout) + : _semaphore2?.WaitOne(millisecondsTimeout); + if (entered == false) + { + throw new TimeoutException("Failed to enter the lock within timeout."); + } + + return _releaser ?? CreateReleaser(); // anonymous vs named + } + + // note - before making those classes some structs, read + // about "impure methods" and mutating readonly structs... + private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable + { + private readonly Semaphore? _semaphore; + + // This code added to correctly implement the disposable pattern. + private bool _disposedValue; // To detect redundant calls + + internal NamedSemaphoreReleaser(Semaphore? semaphore) => _semaphore = semaphore; + + // we WANT to release the semaphore because it's a system object, ie a critical + // non-managed resource - and if it is not released then noone else can acquire + // the lock - so we inherit from CriticalFinalizerObject which means that the + // finalizer "should" run in all situations - there is always a chance that it + // does not run and the semaphore remains "acquired" but then chances are the + // whole process (w3wp.exe...) is going down, at which point the semaphore will + // be destroyed by Windows. + + // however, the semaphore is a managed object, and so when the finalizer runs it + // might have been finalized already, and then we get a, ObjectDisposedException + // in the finalizer - which is bad. + + // in order to prevent this we do two things + // - use a GCHandler to ensure the semaphore is still there when the finalizer + // runs, so we can actually release it + // - wrap the finalizer code in a try...catch to make sure it never throws + ~NamedSemaphoreReleaser() + { + try { - // anonymous semaphore - // use one unique releaser, that will not release the semaphore when finalized - // because the semaphore is destroyed anyway if the app goes down - - _semaphore = new SemaphoreSlim(1, 1); // create a local (to the app domain) semaphore - _releaser = new SemaphoreSlimReleaser(_semaphore); - _releaserTask = Task.FromResult(_releaser); + Dispose(false); } - else + catch { - // named semaphore - // use dedicated releasers, that will release the semaphore when finalized - // because the semaphore is system-wide and we cannot leak counts - - _semaphore2 = new Semaphore(1, 1, name); // create a system-wide named semaphore + // we do NOT want the finalizer to throw - never ever } } - private IDisposable? CreateReleaser() + public void Dispose() { - // for anonymous semaphore, use the unique releaser, else create a new one - return _semaphore != null - ? _releaser // (IDisposable)new SemaphoreSlimReleaser(_semaphore) - : new NamedSemaphoreReleaser(_semaphore2); + Dispose(true); + GC.SuppressFinalize(this); // finalize will not run } - public IDisposable? Lock() + private void Dispose(bool disposing) { - if (_semaphore != null) - _semaphore.Wait(); - else - _semaphore2?.WaitOne(); - return _releaser ?? CreateReleaser(); // anonymous vs named - } - - public IDisposable? Lock(int millisecondsTimeout) - { - var entered = _semaphore != null - ? _semaphore.Wait(millisecondsTimeout) - : _semaphore2?.WaitOne(millisecondsTimeout); - if (entered == false) - throw new TimeoutException("Failed to enter the lock within timeout."); - return _releaser ?? CreateReleaser(); // anonymous vs named - } - - // note - before making those classes some structs, read - // about "impure methods" and mutating readonly structs... - - private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable - { - private readonly Semaphore? _semaphore; - - internal NamedSemaphoreReleaser(Semaphore? semaphore) - { - _semaphore = semaphore; - } - - #region IDisposable Support - - // This code added to correctly implement the disposable pattern. - - private bool disposedValue = false; // To detect redundant calls - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); // finalize will not run - } - - private void Dispose(bool disposing) - { - if (!disposedValue) - { - try - { - _semaphore?.Release(); - } - finally - { - try - { - _semaphore?.Dispose(); - } - catch { } - } - disposedValue = true; - } - } - - // we WANT to release the semaphore because it's a system object, ie a critical - // non-managed resource - and if it is not released then noone else can acquire - // the lock - so we inherit from CriticalFinalizerObject which means that the - // finalizer "should" run in all situations - there is always a chance that it - // does not run and the semaphore remains "acquired" but then chances are the - // whole process (w3wp.exe...) is going down, at which point the semaphore will - // be destroyed by Windows. - - // however, the semaphore is a managed object, and so when the finalizer runs it - // might have been finalized already, and then we get a, ObjectDisposedException - // in the finalizer - which is bad. - - // in order to prevent this we do two things - // - use a GCHandler to ensure the semaphore is still there when the finalizer - // runs, so we can actually release it - // - wrap the finalizer code in a try...catch to make sure it never throws - - ~NamedSemaphoreReleaser() + if (!_disposedValue) { try { - Dispose(false); + _semaphore?.Release(); } - catch + finally { - // we do NOT want the finalizer to throw - never ever + try + { + _semaphore?.Dispose(); + } + catch + { + } } + + _disposedValue = true; } + } + } - #endregion + private class SemaphoreSlimReleaser : IDisposable + { + private readonly SemaphoreSlim _semaphore; + internal SemaphoreSlimReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore; + + ~SemaphoreSlimReleaser() => Dispose(false); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } - private class SemaphoreSlimReleaser : IDisposable + private void Dispose(bool disposing) { - private readonly SemaphoreSlim _semaphore; - - internal SemaphoreSlimReleaser(SemaphoreSlim semaphore) + if (disposing) { - _semaphore = semaphore; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - // normal - _semaphore.Release(); - } - } - - ~SemaphoreSlimReleaser() - { - Dispose(false); + // normal + _semaphore.Release(); } } } diff --git a/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs index 7fd0ee5a85..bd41914010 100644 --- a/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs +++ b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs @@ -1,31 +1,27 @@ -using System; +namespace Umbraco.Cms.Core.Telemetry; -namespace Umbraco.Cms.Core.Telemetry +/// +/// Used to get and create the site identifier +/// +public interface ISiteIdentifierService { /// - /// Used to get and create the site identifier + /// Tries to get the site identifier /// - public interface ISiteIdentifierService - { + /// True if success. + bool TryGetSiteIdentifier(out Guid siteIdentifier); - /// - /// Tries to get the site identifier - /// - /// True if success. - bool TryGetSiteIdentifier(out Guid siteIdentifier); + /// + /// Creates the site identifier and writes it to config. + /// + /// asd. + /// True if success. + bool TryCreateSiteIdentifier(out Guid createdGuid); - /// - /// Creates the site identifier and writes it to config. - /// - /// asd. - /// True if success. - bool TryCreateSiteIdentifier(out Guid createdGuid); - - /// - /// Tries to get the site identifier or otherwise create it if it doesn't exist. - /// - /// The out parameter for the existing or create site identifier. - /// True if success. - bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier); - } + /// + /// Tries to get the site identifier or otherwise create it if it doesn't exist. + /// + /// The out parameter for the existing or create site identifier. + /// True if success. + bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier); } diff --git a/src/Umbraco.Core/Telemetry/ITelemetryService.cs b/src/Umbraco.Core/Telemetry/ITelemetryService.cs index bb832bfd7e..23b0d154a4 100644 --- a/src/Umbraco.Core/Telemetry/ITelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/ITelemetryService.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Core.Telemetry.Models; -namespace Umbraco.Cms.Core.Telemetry +namespace Umbraco.Cms.Core.Telemetry; + +/// +/// Service which gathers the data for telemetry reporting +/// +public interface ITelemetryService { /// - /// Service which gathers the data for telemetry reporting + /// Try and get the /// - public interface ITelemetryService - { - /// - /// Try and get the - /// - bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData); - } + bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData); } diff --git a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs index 53dc6d1a6e..53c07766e8 100644 --- a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs +++ b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs @@ -1,26 +1,25 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Telemetry.Models +namespace Umbraco.Cms.Core.Telemetry.Models; + +/// +/// Serializable class containing information about an installed package. +/// +[DataContract(Name = "packageTelemetry")] +public class PackageTelemetry { /// - /// Serializable class containing information about an installed package. + /// Gets or sets the name of the installed package. /// - [DataContract(Name = "packageTelemetry")] - public class PackageTelemetry - { - /// - /// Gets or sets the name of the installed package. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// Gets or sets the version of the installed package. - /// - /// - /// This may be an empty string if no version is specified, or if package telemetry has been restricted. - /// - [DataMember(Name = "version")] - public string? Version { get; set; } - } + /// + /// Gets or sets the version of the installed package. + /// + /// + /// This may be an empty string if no version is specified, or if package telemetry has been restricted. + /// + [DataMember(Name = "version")] + public string? Version { get; set; } } diff --git a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs index ea6ff63f91..31bab02f1c 100644 --- a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs +++ b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs @@ -1,38 +1,35 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Telemetry.Models +namespace Umbraco.Cms.Core.Telemetry.Models; + +/// +/// Serializable class containing telemetry information. +/// +[DataContract] +public class TelemetryReportData { /// - /// Serializable class containing telemetry information. + /// Gets or sets a random GUID to prevent an instance posting multiple times pr. day. /// - [DataContract] - public class TelemetryReportData - { - /// - /// Gets or sets a random GUID to prevent an instance posting multiple times pr. day. - /// - [DataMember(Name = "id")] - public Guid Id { get; set; } + [DataMember(Name = "id")] + public Guid Id { get; set; } - /// - /// Gets or sets the Umbraco CMS version. - /// - [DataMember(Name = "version")] - public string? Version { get; set; } + /// + /// Gets or sets the Umbraco CMS version. + /// + [DataMember(Name = "version")] + public string? Version { get; set; } - /// - /// Gets or sets an enumerable containing information about packages. - /// - /// - /// Contains only the name and version of the packages, unless no version is specified. - /// - [DataMember(Name = "packages")] - public IEnumerable? Packages { get; set; } + /// + /// Gets or sets an enumerable containing information about packages. + /// + /// + /// Contains only the name and version of the packages, unless no version is specified. + /// + [DataMember(Name = "packages")] + public IEnumerable? Packages { get; set; } - [DataMember(Name = "detailed")] - public IEnumerable? Detailed { get; set; } - } + [DataMember(Name = "detailed")] + public IEnumerable? Detailed { get; set; } } diff --git a/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs index b6e40665c1..a7b5882ecc 100644 --- a/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs +++ b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs @@ -1,81 +1,79 @@ -using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Telemetry +namespace Umbraco.Cms.Core.Telemetry; + +/// +internal class SiteIdentifierService : ISiteIdentifierService { - /// - internal class SiteIdentifierService : ISiteIdentifierService + private readonly IConfigManipulator _configManipulator; + private readonly ILogger _logger; + private GlobalSettings _globalSettings; + + public SiteIdentifierService( + IOptionsMonitor optionsMonitor, + IConfigManipulator configManipulator, + ILogger logger) { - private GlobalSettings _globalSettings; - private readonly IConfigManipulator _configManipulator; - private readonly ILogger _logger; + _globalSettings = optionsMonitor.CurrentValue; + optionsMonitor.OnChange(globalSettings => _globalSettings = globalSettings); + _configManipulator = configManipulator; + _logger = logger; + } - public SiteIdentifierService( - IOptionsMonitor optionsMonitor, - IConfigManipulator configManipulator, - ILogger logger) + /// + public bool TryGetSiteIdentifier(out Guid siteIdentifier) + { + // Parse telemetry string as a GUID & verify its a GUID and not some random string + // since users may have messed with or decided to empty the app setting or put in something random + if (Guid.TryParse(_globalSettings.Id, out Guid parsedTelemetryId) is false + || parsedTelemetryId == Guid.Empty) { - _globalSettings = optionsMonitor.CurrentValue; - optionsMonitor.OnChange(globalSettings => _globalSettings = globalSettings); - _configManipulator = configManipulator; - _logger = logger; - } - - /// - public bool TryGetSiteIdentifier(out Guid siteIdentifier) - { - // Parse telemetry string as a GUID & verify its a GUID and not some random string - // since users may have messed with or decided to empty the app setting or put in something random - if (Guid.TryParse(_globalSettings.Id, out var parsedTelemetryId) is false - || parsedTelemetryId == Guid.Empty) - { - siteIdentifier = Guid.Empty; - return false; - } - - siteIdentifier = parsedTelemetryId; - return true; - } - - /// - public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier) - { - if (TryGetSiteIdentifier(out Guid existingId)) - { - siteIdentifier = existingId; - return true; - } - - if (TryCreateSiteIdentifier(out Guid createdId)) - { - siteIdentifier = createdId; - return true; - } - siteIdentifier = Guid.Empty; return false; } - /// - public bool TryCreateSiteIdentifier(out Guid createdGuid) + siteIdentifier = parsedTelemetryId; + return true; + } + + /// + public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier) + { + if (TryGetSiteIdentifier(out Guid existingId)) { - createdGuid = Guid.NewGuid(); - - try - { - _configManipulator.SetGlobalId(createdGuid.ToString()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier"); - createdGuid = Guid.Empty; - return false; - } - + siteIdentifier = existingId; return true; } + + if (TryCreateSiteIdentifier(out Guid createdId)) + { + siteIdentifier = createdId; + return true; + } + + siteIdentifier = Guid.Empty; + return false; + } + + /// + public bool TryCreateSiteIdentifier(out Guid createdGuid) + { + createdGuid = Guid.NewGuid(); + + try + { + _configManipulator.SetGlobalId(createdGuid.ToString()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier"); + createdGuid = Guid.Empty; + return false; + } + + return true; } } diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index bcc6076d24..4ebf1ba0b9 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; @@ -10,88 +8,87 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Telemetry +namespace Umbraco.Cms.Core.Telemetry; + +/// +internal class TelemetryService : ITelemetryService { - /// - internal class TelemetryService : ITelemetryService + private readonly IManifestParser _manifestParser; + private readonly IMetricsConsentService _metricsConsentService; + private readonly ISiteIdentifierService _siteIdentifierService; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IUsageInformationService _usageInformationService; + + /// + /// Initializes a new instance of the class. + /// + public TelemetryService( + IManifestParser manifestParser, + IUmbracoVersion umbracoVersion, + ISiteIdentifierService siteIdentifierService, + IUsageInformationService usageInformationService, + IMetricsConsentService metricsConsentService) { - private readonly IManifestParser _manifestParser; - private readonly IUmbracoVersion _umbracoVersion; - private readonly ISiteIdentifierService _siteIdentifierService; - private readonly IUsageInformationService _usageInformationService; - private readonly IMetricsConsentService _metricsConsentService; + _manifestParser = manifestParser; + _umbracoVersion = umbracoVersion; + _siteIdentifierService = siteIdentifierService; + _usageInformationService = usageInformationService; + _metricsConsentService = metricsConsentService; + } - /// - /// Initializes a new instance of the class. - /// - public TelemetryService( - IManifestParser manifestParser, - IUmbracoVersion umbracoVersion, - ISiteIdentifierService siteIdentifierService, - IUsageInformationService usageInformationService, - IMetricsConsentService metricsConsentService) + /// + public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) + { + if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false) { - _manifestParser = manifestParser; - _umbracoVersion = umbracoVersion; - _siteIdentifierService = siteIdentifierService; - _usageInformationService = usageInformationService; - _metricsConsentService = metricsConsentService; + telemetryReportData = null; + return false; } - /// - public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) + telemetryReportData = new TelemetryReportData { - if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false) - { - telemetryReportData = null; - return false; - } + Id = telemetryId, + Version = GetVersion(), + Packages = GetPackageTelemetry(), + Detailed = _usageInformationService.GetDetailed(), + }; + return true; + } - telemetryReportData = new TelemetryReportData - { - Id = telemetryId, - Version = GetVersion(), - Packages = GetPackageTelemetry(), - Detailed = _usageInformationService.GetDetailed(), - }; - return true; + private string? GetVersion() + { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; } - private string? GetVersion() - { - if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) - { - return null; - } + return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + } - return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + private IEnumerable? GetPackageTelemetry() + { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; } - private IEnumerable? GetPackageTelemetry() + List packages = new(); + IEnumerable manifests = _manifestParser.GetManifests(); + + foreach (PackageManifest manifest in manifests) { - if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + if (manifest.AllowPackageTelemetry is false) { - return null; + continue; } - List packages = new(); - IEnumerable manifests = _manifestParser.GetManifests(); - - foreach (PackageManifest manifest in manifests) + packages.Add(new PackageTelemetry { - if (manifest.AllowPackageTelemetry is false) - { - continue; - } - - packages.Add(new PackageTelemetry - { - Name = manifest.PackageName, - Version = manifest.Version ?? string.Empty, - }); - } - - return packages; + Name = manifest.PackageName, + Version = manifest.Version ?? string.Empty, + }); } + + return packages; } } diff --git a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs index 46ac9fb6e7..aa0e9a09bf 100644 --- a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs @@ -1,95 +1,96 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Umbraco.Cms.Core.Routing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +public sealed class HtmlImageSourceParser { + private static readonly Regex ResolveImgPattern = new( + @"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - public sealed class HtmlImageSourceParser + private static readonly Regex DataUdiAttributeRegex = new( + @"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + private readonly IPublishedUrlProvider? _publishedUrlProvider; + + private Func? _getMediaUrl; + + public HtmlImageSourceParser(Func getMediaUrl) => _getMediaUrl = getMediaUrl; + + public HtmlImageSourceParser(IPublishedUrlProvider publishedUrlProvider) => + _publishedUrlProvider = publishedUrlProvider; + + /// + /// Parses out media UDIs from an html string based on 'data-udi' html attributes + /// + /// + /// + public IEnumerable FindUdisFromDataAttributes(string text) { - public HtmlImageSourceParser(Func getMediaUrl) + MatchCollection matches = DataUdiAttributeRegex.Matches(text); + if (matches.Count == 0) { - this._getMediaUrl = getMediaUrl; + yield break; } - private readonly IPublishedUrlProvider? _publishedUrlProvider; - - public HtmlImageSourceParser(IPublishedUrlProvider publishedUrlProvider) + foreach (Match match in matches) { - _publishedUrlProvider = publishedUrlProvider; - } - - private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - private Func? _getMediaUrl; - - /// - /// Parses out media UDIs from an html string based on 'data-udi' html attributes - /// - /// - /// - public IEnumerable FindUdisFromDataAttributes(string text) - { - var matches = DataUdiAttributeRegex.Matches(text); - if (matches.Count == 0) - yield break; - - foreach (Match match in matches) + if (match.Groups.Count == 2 && UdiParser.TryParse(match.Groups[1].Value, out Udi? udi)) { - if (match.Groups.Count == 2 && UdiParser.TryParse(match.Groups[1].Value, out var udi)) - yield return udi; + yield return udi; } } + } - /// - /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. - /// - /// - /// - /// Umbraco image tags are identified by their data-udi attributes - public string EnsureImageSources(string text) + /// + /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. + /// + /// + /// + /// Umbraco image tags are identified by their data-udi attributes + public string EnsureImageSources(string text) + { + if (_getMediaUrl == null) { - if(_getMediaUrl == null) - _getMediaUrl = (guid) => _publishedUrlProvider?.GetMediaUrl(guid); - - return ResolveImgPattern.Replace(text, match => - { - // match groups: - // - 1 = from the beginning of the image tag until src attribute value begins - // - 2 = the src attribute value excluding the querystring (if present) - // - 3 = anything after group 2 and before the data-udi attribute value begins - // - 4 = the data-udi attribute value - // - 5 = anything after group 4 until the image tag is closed - var udi = match.Groups[4].Value; - if (udi.IsNullOrWhiteSpace() ||UdiParser.TryParse(udi, out var guidUdi) == false) - { - return match.Value; - } - var mediaUrl = _getMediaUrl(guidUdi.Guid); - if (mediaUrl == null) - { - // image does not exist - we could choose to remove the image entirely here (return empty string), - // but that would leave the editors completely in the dark as to why the image doesn't show - return match.Value; - } - - return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; - }); + _getMediaUrl = guid => _publishedUrlProvider?.GetMediaUrl(guid); } - /// - /// Removes media URLs from <img> tags where a data-udi attribute is present - /// - /// - /// - public string RemoveImageSources(string text) - // see comment in ResolveMediaFromTextString for group reference - => ResolveImgPattern.Replace(text, "$1$3$4$5"); + return ResolveImgPattern.Replace(text, match => + { + // match groups: + // - 1 = from the beginning of the image tag until src attribute value begins + // - 2 = the src attribute value excluding the querystring (if present) + // - 3 = anything after group 2 and before the data-udi attribute value begins + // - 4 = the data-udi attribute value + // - 5 = anything after group 4 until the image tag is closed + var udi = match.Groups[4].Value; + if (udi.IsNullOrWhiteSpace() || UdiParser.TryParse(udi, out GuidUdi? guidUdi) == false) + { + return match.Value; + } + + var mediaUrl = _getMediaUrl(guidUdi.Guid); + if (mediaUrl == null) + { + // image does not exist - we could choose to remove the image entirely here (return empty string), + // but that would leave the editors completely in the dark as to why the image doesn't show + return match.Value; + } + + return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; + }); } + + /// + /// Removes media URLs from <img> tags where a data-udi attribute is present + /// + /// + /// + public string RemoveImageSources(string text) + + // see comment in ResolveMediaFromTextString for group reference + => ResolveImgPattern.Replace(text, "$1$3$4$5"); } diff --git a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs index 4317f05cc9..1030705051 100644 --- a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs +++ b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs @@ -1,127 +1,135 @@ -using System; -using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +/// +/// Utility class used to parse internal links +/// +public sealed class HtmlLocalLinkParser { - /// - /// Utility class used to parse internal links - /// - public sealed class HtmlLocalLinkParser + internal static readonly Regex LocalLinkPattern = new( + @"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private readonly IPublishedUrlProvider _publishedUrlProvider; + + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public HtmlLocalLinkParser( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedUrlProvider publishedUrlProvider) { + _umbracoContextAccessor = umbracoContextAccessor; + _publishedUrlProvider = publishedUrlProvider; + } - internal static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedUrlProvider _publishedUrlProvider; - - public HtmlLocalLinkParser(IUmbracoContextAccessor umbracoContextAccessor, IPublishedUrlProvider publishedUrlProvider) + public IEnumerable FindUdisFromLocalLinks(string text) + { + foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text)) { - _umbracoContextAccessor = umbracoContextAccessor; - _publishedUrlProvider = publishedUrlProvider; - } - - public IEnumerable FindUdisFromLocalLinks(string text) - { - foreach ((int? intId, GuidUdi? udi, string tagValue) in FindLocalLinkIds(text)) + if (udi is not null) { - if (udi is not null) - yield return udi; // In v8, we only care abuot UDIs + yield return udi; // In v8, we only care abuot UDIs } } + } - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - /// - public string EnsureInternalLinks(string text, bool preview) + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text, bool preview) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); - } - - if (!preview) - { - return EnsureInternalLinks(text); - } - - using (umbracoContext!.ForcedPreview(preview)) // force for URL provider - { - return EnsureInternalLinks(text); - } + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); } - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - /// - public string EnsureInternalLinks(string text) + if (!preview) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out _)) - { - throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); - } + return EnsureInternalLinks(text); + } - foreach((int? intId, GuidUdi? udi, string tagValue) in FindLocalLinkIds(text)) + using (umbracoContext.ForcedPreview(preview)) // force for URL provider + { + return EnsureInternalLinks(text); + } + } + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out _)) + { + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + } + + foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text)) + { + if (udi is not null) { - if (udi is not null) + var newLink = "#"; + if (udi?.EntityType == Constants.UdiEntityType.Document) { - var newLink = "#"; - if (udi?.EntityType == Constants.UdiEntityType.Document) - newLink = _publishedUrlProvider.GetUrl(udi.Guid); - else if (udi?.EntityType == Constants.UdiEntityType.Media) - newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid); - - if (newLink == null) - newLink = "#"; - - text = text.Replace(tagValue, "href=\"" + newLink); + newLink = _publishedUrlProvider.GetUrl(udi.Guid); } - else if (intId.HasValue) + else if (udi?.EntityType == Constants.UdiEntityType.Media) { - var newLink = _publishedUrlProvider.GetUrl(intId.Value); - text = text.Replace(tagValue, "href=\"" + newLink); + newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid); } - } - return text; + if (newLink == null) + { + newLink = "#"; + } + + text = text.Replace(tagValue, "href=\"" + newLink); + } + else if (intId.HasValue) + { + var newLink = _publishedUrlProvider.GetUrl(intId.Value); + text = text.Replace(tagValue, "href=\"" + newLink); + } } - private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLocalLinkIds(string text) + return text; + } + + private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLocalLinkIds(string text) + { + // Parse internal links + MatchCollection tags = LocalLinkPattern.Matches(text); + foreach (Match tag in tags) { - // Parse internal links - var tags = LocalLinkPattern.Matches(text); - foreach (Match tag in tags) + if (tag.Groups.Count > 0) { - if (tag.Groups.Count > 0) + var id = tag.Groups[1].Value; // .Remove(tag.Groups[1].Value.Length - 1, 1); + + // The id could be an int or a UDI + if (UdiParser.TryParse(id, out Udi? udi)) { - var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - - //The id could be an int or a UDI - if (UdiParser.TryParse(id, out var udi)) + var guidUdi = udi as GuidUdi; + if (guidUdi is not null) { - var guidUdi = udi as GuidUdi; - if (guidUdi is not null) - yield return (null, guidUdi, tag.Value); - } - - if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) - { - yield return (intId, null, tag.Value); + yield return (null, guidUdi, tag.Value); } } - } + if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) + { + yield return (intId, null, tag.Value); + } + } } } } diff --git a/src/Umbraco.Core/Templates/HtmlUrlParser.cs b/src/Umbraco.Core/Templates/HtmlUrlParser.cs index 39c82f00ab..f4a817485d 100644 --- a/src/Umbraco.Core/Templates/HtmlUrlParser.cs +++ b/src/Umbraco.Core/Templates/HtmlUrlParser.cs @@ -5,66 +5,77 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +public sealed class HtmlUrlParser { - public sealed class HtmlUrlParser + private static readonly Regex ResolveUrlPattern = new( + "(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private readonly IIOHelper _ioHelper; + private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; + private ContentSettings _contentSettings; + + public HtmlUrlParser(IOptionsMonitor contentSettings, ILogger logger, IProfilingLogger profilingLogger, IIOHelper ioHelper) { - private ContentSettings _contentSettings; - private readonly ILogger _logger; - private readonly IIOHelper _ioHelper; - private readonly IProfilingLogger _profilingLogger; + _contentSettings = contentSettings.CurrentValue; + _logger = logger; + _ioHelper = ioHelper; + _profilingLogger = profilingLogger; - private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + contentSettings.OnChange(x => _contentSettings = x); + } - public HtmlUrlParser(IOptionsMonitor contentSettings, ILogger logger, IProfilingLogger profilingLogger, IIOHelper ioHelper) + /// + /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl + /// to replace the tilde with the application path. + /// + /// + /// + /// + /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. + /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for + /// non-Virtual-Directory installs. + /// + public string EnsureUrls(string text) + { + if (_contentSettings.ResolveUrlsFromTextString == false) { - _contentSettings = contentSettings.CurrentValue; - _logger = logger; - _ioHelper = ioHelper; - _profilingLogger = profilingLogger; - - contentSettings.OnChange(x => _contentSettings = x); - } - - /// - /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. - /// - /// - /// - /// - /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. - /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. - /// - public string EnsureUrls(string text) - { - if (_contentSettings.ResolveUrlsFromTextString == false) - return text; - - using (var timer = _profilingLogger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) - { - // find all relative URLs (ie. URLs that contain ~) - var tags = ResolveUrlPattern.Matches(text); - _logger.LogDebug("After regex: {Duration} matched: {TagsCount}", timer?.Stopwatch.ElapsedMilliseconds, tags.Count); - foreach (Match tag in tags) - { - var url = ""; - if (tag.Groups[1].Success) - url = tag.Groups[1].Value; - - // The richtext editor inserts a slash in front of the URL. That's why we need this little fix - // if (url.StartsWith("/")) - // text = text.Replace(url, ResolveUrl(url.Substring(1))); - // else - if (string.IsNullOrEmpty(url) == false) - { - var resolvedUrl = (url.Substring(0, 1) == "/") ? _ioHelper.ResolveUrl(url.Substring(1)) : _ioHelper.ResolveUrl(url); - text = text.Replace(url, resolvedUrl); - } - } - } - return text; } + + using (DisposableTimer? timer = _profilingLogger.DebugDuration( + typeof(IOHelper), + "ResolveUrlsFromTextString starting", + "ResolveUrlsFromTextString complete")) + { + // find all relative URLs (ie. URLs that contain ~) + MatchCollection tags = ResolveUrlPattern.Matches(text); + _logger.LogDebug("After regex: {Duration} matched: {TagsCount}", timer?.Stopwatch.ElapsedMilliseconds, tags.Count); + foreach (Match tag in tags) + { + var url = string.Empty; + if (tag.Groups[1].Success) + { + url = tag.Groups[1].Value; + } + + // The richtext editor inserts a slash in front of the URL. That's why we need this little fix + // if (url.StartsWith("/")) + // text = text.Replace(url, ResolveUrl(url.Substring(1))); + // else + if (string.IsNullOrEmpty(url) == false) + { + var resolvedUrl = url[..1] == "/" + ? _ioHelper.ResolveUrl(url[1..]) + : _ioHelper.ResolveUrl(url); + text = text.Replace(url, resolvedUrl); + } + } + } + + return text; } } diff --git a/src/Umbraco.Core/Templates/ITemplateRenderer.cs b/src/Umbraco.Core/Templates/ITemplateRenderer.cs index f6e6435a8a..17d16168ec 100644 --- a/src/Umbraco.Core/Templates/ITemplateRenderer.cs +++ b/src/Umbraco.Core/Templates/ITemplateRenderer.cs @@ -1,13 +1,9 @@ -using System.IO; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Templates; -namespace Umbraco.Cms.Core.Templates +/// +/// This is used purely for the RenderTemplate functionality in Umbraco +/// +public interface ITemplateRenderer { - /// - /// This is used purely for the RenderTemplate functionality in Umbraco - /// - public interface ITemplateRenderer - { - Task RenderAsync(int pageId, int? altTemplateId, StringWriter writer); - } + Task RenderAsync(int pageId, int? altTemplateId, StringWriter writer); } diff --git a/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs b/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs index 1239f22877..b94d575b9f 100644 --- a/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs @@ -1,57 +1,53 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +/// +/// Methods used to render umbraco components as HTML in templates +/// +public interface IUmbracoComponentRenderer { /// - /// Methods used to render umbraco components as HTML in templates + /// Renders the template for the specified pageId and an optional altTemplateId /// - public interface IUmbracoComponentRenderer - { - /// - /// Renders the template for the specified pageId and an optional altTemplateId - /// - /// The content id - /// If not specified, will use the template assigned to the node - Task RenderTemplateAsync(int contentId, int? altTemplateId = null); + /// The content id + /// If not specified, will use the template assigned to the node + Task RenderTemplateAsync(int contentId, int? altTemplateId = null); - /// - /// Renders the macro with the specified alias. - /// - /// The content id - /// The alias. - Task RenderMacroAsync(int contentId, string alias); + /// + /// Renders the macro with the specified alias. + /// + /// The content id + /// The alias. + Task RenderMacroAsync(int contentId, string alias); - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// The content id - /// The alias. - /// The parameters. - Task RenderMacroAsync(int contentId, string alias, object parameters); + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The content id + /// The alias. + /// The parameters. + Task RenderMacroAsync(int contentId, string alias, object parameters); - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// The content id - /// The alias. - /// The parameters. - Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters); + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The content id + /// The alias. + /// The parameters. + Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters); - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// An IPublishedContent to use for the context for the macro rendering - /// The alias. - /// The parameters. - /// A raw HTML string of the macro output - /// - /// Currently only used when the node is unpublished and unable to get the contentId item from the - /// content cache as its unpublished. This deals with taking in a preview/draft version of the content node - /// - Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters); - - } + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// An IPublishedContent to use for the context for the macro rendering + /// The alias. + /// The parameters. + /// A raw HTML string of the macro output + /// + /// Currently only used when the node is unpublished and unable to get the contentId item from the + /// content cache as its unpublished. This deals with taking in a preview/draft version of the content node + /// + Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters); } diff --git a/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs b/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs index 407f85ad60..e419bd5be3 100644 --- a/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs @@ -1,112 +1,108 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; -using System.Threading.Tasks; using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +/// +/// Methods used to render umbraco components as HTML in templates +/// +/// +/// Used by UmbracoHelper +/// +public class UmbracoComponentRenderer : IUmbracoComponentRenderer { + private readonly IMacroRenderer _macroRenderer; + private readonly ITemplateRenderer _templateRenderer; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; /// - /// Methods used to render umbraco components as HTML in templates + /// Initializes a new instance of the class. /// - /// - /// Used by UmbracoHelper - /// - public class UmbracoComponentRenderer : IUmbracoComponentRenderer + public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IMacroRenderer _macroRenderer; - private readonly ITemplateRenderer _templateRenderer; + _umbracoContextAccessor = umbracoContextAccessor; + _macroRenderer = macroRenderer; + _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); + } - /// - /// Initializes a new instance of the class. - /// - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) + /// + public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) + { + using (var sw = new StringWriter()) { - _umbracoContextAccessor = umbracoContextAccessor; - _macroRenderer = macroRenderer; - _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); - } - - /// - public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) - { - using (var sw = new StringWriter()) + try { - try - { - await _templateRenderer.RenderAsync(contentId, altTemplateId, sw); - } - catch (Exception ex) - { - sw.Write("", contentId, ex); - } - - return new HtmlEncodedString(sw.ToString()); + await _templateRenderer.RenderAsync(contentId, altTemplateId, sw); } - } - - /// - public async Task RenderMacroAsync(int contentId, string alias) => await RenderMacroAsync(contentId, alias, new { }); - - /// - public async Task RenderMacroAsync(int contentId, string alias, object parameters) => await RenderMacroAsync(contentId, alias, parameters.ToDictionary()); - - /// - public async Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters) - { - if (contentId == default) + catch (Exception ex) { - throw new ArgumentException("Invalid content id " + contentId); - } - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var content = umbracoContext.Content?.GetById(contentId); - - if (content == null) - { - throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); + sw.Write("", contentId, ex); } - return await RenderMacroAsync(content, alias, parameters); - } - - /// - public async Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters) - { - if(content == null) - { - throw new InvalidOperationException("Cannot render a macro, IPublishedContent is null"); - } - - return await RenderMacroAsync(content, alias, parameters); - } - - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - private async Task RenderMacroAsync(IPublishedContent content, string alias, IDictionary? parameters) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - // TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method looks for a lower case match. the whole macro concept needs to be rewritten. - // NOTE: the value could have HTML encoded values, so we need to deal with that - var macroProps = parameters?.ToDictionary( - x => x.Key.ToLowerInvariant(), - i => (i.Value is string) ? WebUtility.HtmlDecode(i.Value.ToString()) : i.Value); - - var html = (await _macroRenderer.RenderAsync(alias, content, macroProps)).Text; - - return new HtmlEncodedString(html!); + return new HtmlEncodedString(sw.ToString()); } } + + /// + public async Task RenderMacroAsync(int contentId, string alias) => + await RenderMacroAsync(contentId, alias, new { }); + + /// + public async Task RenderMacroAsync(int contentId, string alias, object parameters) => + await RenderMacroAsync(contentId, alias, parameters.ToDictionary()); + + /// + public async Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters) + { + if (contentId == default) + { + throw new ArgumentException("Invalid content id " + contentId); + } + + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? content = umbracoContext.Content?.GetById(contentId); + + if (content == null) + { + throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); + } + + return await RenderMacroAsync(content, alias, parameters); + } + + /// + public async Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters) + { + if (content == null) + { + throw new InvalidOperationException("Cannot render a macro, IPublishedContent is null"); + } + + return await RenderMacroAsync(content, alias, parameters); + } + + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + private async Task RenderMacroAsync(IPublishedContent content, string alias, IDictionary? parameters) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + // TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method looks for a lower case match. the whole macro concept needs to be rewritten. + // NOTE: the value could have HTML encoded values, so we need to deal with that + var macroProps = parameters?.ToDictionary( + x => x.Key.ToLowerInvariant(), + i => i.Value is string ? WebUtility.HtmlDecode(i.Value.ToString()) : i.Value); + + var html = (await _macroRenderer.RenderAsync(alias, content, macroProps)).Text; + + return new HtmlEncodedString(html!); + } } diff --git a/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs b/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs index 3fba765f83..d1d8384502 100644 --- a/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs +++ b/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs @@ -1,63 +1,65 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.Tour +namespace Umbraco.Cms.Core.Tour; + +/// +/// Represents a back-office tour filter. +/// +public class BackOfficeTourFilter { /// - /// Represents a back-office tour filter. + /// Initializes a new instance of the class. /// - public class BackOfficeTourFilter + /// Value to filter out tours by a plugin, can be null + /// Value to filter out a tour file, can be null + /// Value to filter out a tour alias, can be null + /// + /// Depending on what is null will depend on how the filter is applied. + /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check + /// tour alias is not NULL and then match it, + /// if any steps is NULL then the filters upstream are applied. + /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from + /// the plugin "hello" but not from other plugins if the same file name exists. + /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the + /// plugin or file name + /// + public BackOfficeTourFilter(Regex? pluginName, Regex? tourFileName, Regex? tourAlias) { - /// - /// Initializes a new instance of the class. - /// - /// Value to filter out tours by a plugin, can be null - /// Value to filter out a tour file, can be null - /// Value to filter out a tour alias, can be null - /// - /// Depending on what is null will depend on how the filter is applied. - /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check tour alias is not NULL and then match it, - /// if any steps is NULL then the filters upstream are applied. - /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from the plugin "hello" but not from other plugins if the same file name exists. - /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the plugin or file name - /// - public BackOfficeTourFilter(Regex? pluginName, Regex? tourFileName, Regex? tourAlias) - { - PluginName = pluginName; - TourFileName = tourFileName; - TourAlias = tourAlias; - } - - /// - /// Gets the plugin name filtering regex. - /// - public Regex? PluginName { get; } - - /// - /// Gets the tour filename filtering regex. - /// - public Regex? TourFileName { get; } - - /// - /// Gets the tour alias filtering regex. - /// - public Regex? TourAlias { get; } - - /// - /// Creates a filter to filter on the plugin name. - /// - public static BackOfficeTourFilter FilterPlugin(Regex pluginName) - => new BackOfficeTourFilter(pluginName, null, null); - - /// - /// Creates a filter to filter on the tour filename. - /// - public static BackOfficeTourFilter FilterFile(Regex tourFileName) - => new BackOfficeTourFilter(null, tourFileName, null); - - /// - /// Creates a filter to filter on the tour alias. - /// - public static BackOfficeTourFilter FilterAlias(Regex tourAlias) - => new BackOfficeTourFilter(null, null, tourAlias); + PluginName = pluginName; + TourFileName = tourFileName; + TourAlias = tourAlias; } + + /// + /// Gets the plugin name filtering regex. + /// + public Regex? PluginName { get; } + + /// + /// Gets the tour filename filtering regex. + /// + public Regex? TourFileName { get; } + + /// + /// Gets the tour alias filtering regex. + /// + public Regex? TourAlias { get; } + + /// + /// Creates a filter to filter on the plugin name. + /// + public static BackOfficeTourFilter FilterPlugin(Regex pluginName) + => new(pluginName, null, null); + + /// + /// Creates a filter to filter on the tour filename. + /// + public static BackOfficeTourFilter FilterFile(Regex tourFileName) + => new(null, tourFileName, null); + + /// + /// Creates a filter to filter on the tour alias. + /// + public static BackOfficeTourFilter FilterAlias(Regex tourAlias) + => new(null, null, tourAlias); } diff --git a/src/Umbraco.Core/Tour/TourFilterCollection.cs b/src/Umbraco.Core/Tour/TourFilterCollection.cs index 2864abbced..44905f9127 100644 --- a/src/Umbraco.Core/Tour/TourFilterCollection.cs +++ b/src/Umbraco.Core/Tour/TourFilterCollection.cs @@ -1,16 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Tour +namespace Umbraco.Cms.Core.Tour; + +/// +/// Represents a collection of items. +/// +public class TourFilterCollection : BuilderCollectionBase { - /// - /// Represents a collection of items. - /// - public class TourFilterCollection : BuilderCollectionBase + public TourFilterCollection(Func> items) + : base(items) { - public TourFilterCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs b/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs index 61f10cc96d..b39bcede46 100644 --- a/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs +++ b/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs @@ -1,73 +1,57 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Tour +namespace Umbraco.Cms.Core.Tour; + +/// +/// Builds a collection of items. +/// +public class TourFilterCollectionBuilder : CollectionBuilderBase { + private readonly HashSet _instances = new(); + /// - /// Builds a collection of items. + /// Adds a filter instance. /// - public class TourFilterCollectionBuilder : CollectionBuilderBase + public void AddFilter(BackOfficeTourFilter filter) => _instances.Add(filter); + + /// + protected override IEnumerable CreateItems(IServiceProvider factory) => + base.CreateItems(factory).Concat(_instances); + + /// + /// Removes a filter instance. + /// + public void RemoveFilter(BackOfficeTourFilter filter) => _instances.Remove(filter); + + /// + /// Removes all filter instances. + /// + public void RemoveAllFilters() => _instances.Clear(); + + /// + /// Removes filters matching a condition. + /// + public void RemoveFilter(Func predicate) => + _instances.RemoveWhere(new Predicate(predicate)); + + /// + /// Creates and adds a filter instance filtering by plugin name. + /// + public void AddFilterByPlugin(string pluginName) { - private readonly HashSet _instances = new HashSet(); + pluginName = pluginName.EnsureStartsWith("^").EnsureEndsWith("$"); + _instances.Add(BackOfficeTourFilter.FilterPlugin(new Regex(pluginName, RegexOptions.IgnoreCase))); + } - /// - protected override IEnumerable CreateItems(IServiceProvider factory) - { - return base.CreateItems(factory).Concat(_instances); - } - - /// - /// Adds a filter instance. - /// - public void AddFilter(BackOfficeTourFilter filter) - { - _instances.Add(filter); - } - - /// - /// Removes a filter instance. - /// - public void RemoveFilter(BackOfficeTourFilter filter) - { - _instances.Remove(filter); - } - - /// - /// Removes all filter instances. - /// - public void RemoveAllFilters() - { - _instances.Clear(); - } - - /// - /// Removes filters matching a condition. - /// - public void RemoveFilter(Func predicate) - { - _instances.RemoveWhere(new Predicate(predicate)); - } - - /// - /// Creates and adds a filter instance filtering by plugin name. - /// - public void AddFilterByPlugin(string pluginName) - { - pluginName = pluginName.EnsureStartsWith("^").EnsureEndsWith("$"); - _instances.Add(BackOfficeTourFilter.FilterPlugin(new Regex(pluginName, RegexOptions.IgnoreCase))); - } - - /// - /// Creates and adds a filter instance filtering by tour filename. - /// - public void AddFilterByFile(string filename) - { - filename = filename.EnsureStartsWith("^").EnsureEndsWith("$"); - _instances.Add(BackOfficeTourFilter.FilterFile(new Regex(filename, RegexOptions.IgnoreCase))); - } + /// + /// Creates and adds a filter instance filtering by tour filename. + /// + public void AddFilterByFile(string filename) + { + filename = filename.EnsureStartsWith("^").EnsureEndsWith("$"); + _instances.Add(BackOfficeTourFilter.FilterFile(new Regex(filename, RegexOptions.IgnoreCase))); } } diff --git a/src/Umbraco.Core/Trees/ActionUrlMethod.cs b/src/Umbraco.Core/Trees/ActionUrlMethod.cs index fcf455c6ad..c2be2cea54 100644 --- a/src/Umbraco.Core/Trees/ActionUrlMethod.cs +++ b/src/Umbraco.Core/Trees/ActionUrlMethod.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// Specifies the action to take for a menu item when a URL is specified +/// +public enum ActionUrlMethod { - /// - /// Specifies the action to take for a menu item when a URL is specified - /// - public enum ActionUrlMethod - { - Dialog, - BlankWindow - } + Dialog, + BlankWindow, } diff --git a/src/Umbraco.Core/Trees/CoreTreeAttribute.cs b/src/Umbraco.Core/Trees/CoreTreeAttribute.cs index eedad5b600..b1c29ccb63 100644 --- a/src/Umbraco.Core/Trees/CoreTreeAttribute.cs +++ b/src/Umbraco.Core/Trees/CoreTreeAttribute.cs @@ -1,14 +1,12 @@ -using System; +namespace Umbraco.Cms.Core.Trees; -namespace Umbraco.Cms.Core.Trees +/// +/// Indicates that a tree is a core tree and should not be treated as a plugin tree. +/// +/// +/// This ensures that umbraco will look in the umbraco folders for views for this tree. +/// +[AttributeUsage(AttributeTargets.Class)] +public class CoreTreeAttribute : Attribute { - /// - /// Indicates that a tree is a core tree and should not be treated as a plugin tree. - /// - /// - /// This ensures that umbraco will look in the umbraco folders for views for this tree. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CoreTreeAttribute : Attribute - { } } diff --git a/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs b/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs index fca82ca18b..bba1bfc2dc 100644 --- a/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs +++ b/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs @@ -1,15 +1,13 @@ -namespace Umbraco.Cms.Core.Trees -{ +namespace Umbraco.Cms.Core.Trees; +/// +/// Represents a factory to create . +/// +public interface IMenuItemCollectionFactory +{ /// - /// Represents a factory to create . + /// Creates an empty . /// - public interface IMenuItemCollectionFactory - { - /// - /// Creates an empty . - /// - /// An empty . - MenuItemCollection Create(); - } + /// An empty . + MenuItemCollection Create(); } diff --git a/src/Umbraco.Core/Trees/ISearchableTree.cs b/src/Umbraco.Core/Trees/ISearchableTree.cs index dd61ba0cdb..42883d0f87 100644 --- a/src/Umbraco.Core/Trees/ISearchableTree.cs +++ b/src/Umbraco.Core/Trees/ISearchableTree.cs @@ -1,28 +1,25 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Trees -{ - public interface ISearchableTree : IDiscoverable - { - /// - /// The alias of the tree that the belongs to - /// - string TreeAlias { get; } +namespace Umbraco.Cms.Core.Trees; - /// - /// Searches for results based on the entity type - /// - /// - /// - /// - /// - /// - /// A starting point for the search, generally a node id, but for members this is a member type alias - /// - /// - Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null); - } +public interface ISearchableTree : IDiscoverable +{ + /// + /// The alias of the tree that the belongs to + /// + string TreeAlias { get; } + + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// + Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null); } diff --git a/src/Umbraco.Core/Trees/ISearchableTreeWithCulture.cs b/src/Umbraco.Core/Trees/ISearchableTreeWithCulture.cs new file mode 100644 index 0000000000..6ac4ea3be1 --- /dev/null +++ b/src/Umbraco.Core/Trees/ISearchableTreeWithCulture.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Core.Trees +{ + [Obsolete("This interface will be merged into ISearchableTree in Umbraco 12")] + public interface ISearchableTreeWithCulture : ISearchableTree + { + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// + /// + Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null, string? culture = null); + } +} diff --git a/src/Umbraco.Core/Trees/ITree.cs b/src/Umbraco.Core/Trees/ITree.cs index 106b3eef37..efb3cfab97 100644 --- a/src/Umbraco.Core/Trees/ITree.cs +++ b/src/Umbraco.Core/Trees/ITree.cs @@ -1,44 +1,43 @@ -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +// TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used +// leave as internal for now, maybe we'll use in the future, means we could pass around ITree +// TODO: We should make this a thing, a tree should just be an interface *not* a controller +public interface ITree { - // TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used - // leave as internal for now, maybe we'll use in the future, means we could pass around ITree - // TODO: We should make this a thing, a tree should just be an interface *not* a controller - public interface ITree - { - /// - /// Gets or sets the sort order. - /// - /// The sort order. - int SortOrder { get; } + /// + /// Gets or sets the sort order. + /// + /// The sort order. + int SortOrder { get; } - /// - /// Gets the section alias. - /// - string SectionAlias { get; } + /// + /// Gets the section alias. + /// + string SectionAlias { get; } - /// - /// Gets the tree group. - /// - string? TreeGroup { get; } + /// + /// Gets the tree group. + /// + string? TreeGroup { get; } - /// - /// Gets the tree alias. - /// - string TreeAlias { get; } + /// + /// Gets the tree alias. + /// + string TreeAlias { get; } - /// - /// Gets or sets the tree title (fallback if the tree alias isn't localized) - /// - string? TreeTitle { get; } + /// + /// Gets or sets the tree title (fallback if the tree alias isn't localized) + /// + string? TreeTitle { get; } - /// - /// Gets the tree use. - /// - TreeUse TreeUse { get; } + /// + /// Gets the tree use. + /// + TreeUse TreeUse { get; } - /// - /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app) - /// - bool IsSingleNodeTree { get; } - } + /// + /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app) + /// + bool IsSingleNodeTree { get; } } diff --git a/src/Umbraco.Core/Trees/MenuItemCollection.cs b/src/Umbraco.Core/Trees/MenuItemCollection.cs index 66bdba55d4..aaace2cbd3 100644 --- a/src/Umbraco.Core/Trees/MenuItemCollection.cs +++ b/src/Umbraco.Core/Trees/MenuItemCollection.cs @@ -1,44 +1,33 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models.Trees; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// A menu item collection for a given tree node +/// +[DataContract(Name = "menuItems", Namespace = "")] +public class MenuItemCollection { + public MenuItemCollection(ActionCollection actionCollection) => Items = new MenuItemList(actionCollection); + + public MenuItemCollection(ActionCollection actionCollection, IEnumerable items) => + Items = new MenuItemList(actionCollection, items); + /// - /// A menu item collection for a given tree node + /// Sets the default menu item alias to be shown when the menu is launched - this is optional and if not set then the + /// menu will just be shown normally. /// - [DataContract(Name = "menuItems", Namespace = "")] - public class MenuItemCollection - { - private readonly MenuItemList _menuItems; + [DataMember(Name = "defaultAlias")] + public string? DefaultMenuAlias { get; set; } - public MenuItemCollection(ActionCollection actionCollection) - { - _menuItems = new MenuItemList(actionCollection); - } - - public MenuItemCollection(ActionCollection actionCollection, IEnumerable items) - { - _menuItems = new MenuItemList(actionCollection, items); - } - - /// - /// Sets the default menu item alias to be shown when the menu is launched - this is optional and if not set then the menu will just be shown normally. - /// - [DataMember(Name = "defaultAlias")] - public string? DefaultMenuAlias { get; set; } - - /// - /// The list of menu items - /// - /// - /// We require this so the json serialization works correctly - /// - [DataMember(Name = "menuItems")] - public MenuItemList Items - { - get { return _menuItems; } - } - } + /// + /// The list of menu items + /// + /// + /// We require this so the json serialization works correctly + /// + [DataMember(Name = "menuItems")] + public MenuItemList Items { get; } } diff --git a/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs b/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs index 112b8b6240..da24b0d933 100644 --- a/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs +++ b/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs @@ -1,20 +1,12 @@ using Umbraco.Cms.Core.Actions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +public class MenuItemCollectionFactory : IMenuItemCollectionFactory { - public class MenuItemCollectionFactory: IMenuItemCollectionFactory - { - private readonly ActionCollection _actionCollection; + private readonly ActionCollection _actionCollection; - public MenuItemCollectionFactory(ActionCollection actionCollection) - { - _actionCollection = actionCollection; - } + public MenuItemCollectionFactory(ActionCollection actionCollection) => _actionCollection = actionCollection; - public MenuItemCollection Create() - { - return new MenuItemCollection(_actionCollection); - } - - } + public MenuItemCollection Create() => new MenuItemCollection(_actionCollection); } diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index b3fe420602..cb912a112b 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -1,72 +1,80 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Threading; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models.Trees; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// A custom menu list +/// +/// +/// NOTE: We need a sub collection to the MenuItemCollection object due to how json serialization works. +/// +public class MenuItemList : List { + private readonly ActionCollection _actionCollection; + + public MenuItemList(ActionCollection actionCollection) => _actionCollection = actionCollection; + + public MenuItemList(ActionCollection actionCollection, IEnumerable items) + : base(items) => + _actionCollection = actionCollection; + /// - /// A custom menu list + /// Adds a menu item with a dictionary which is merged to the AdditionalData bag /// - /// - /// NOTE: We need a sub collection to the MenuItemCollection object due to how json serialization works. - /// - public class MenuItemList : List + /// + /// + /// The used to localize the action name based on its alias + /// Whether or not this action opens a dialog + public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) + where T : IAction => Add(textService, hasSeparator, opensDialog, useLegacyIcon: true); + + /// + /// Adds a menu item with a dictionary which is merged to the AdditionalData bag + /// + /// + /// + /// The used to localize the action name based on its alias + /// Whether or not this action opens a dialog + /// Whether or not this action should use legacy icon prefixed with "icon-" or full icon name is specified. + public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false, bool useLegacyIcon = true) + where T : IAction { - private readonly ActionCollection _actionCollection; - - public MenuItemList(ActionCollection actionCollection) + MenuItem? item = CreateMenuItem(textService, hasSeparator, opensDialog, useLegacyIcon); + if (item != null) { - _actionCollection = actionCollection; + Add(item); + return item; } - public MenuItemList(ActionCollection actionCollection, IEnumerable items) - : base(items) - { - _actionCollection = actionCollection; - } + return null; + } - /// - /// Adds a menu item with a dictionary which is merged to the AdditionalData bag - /// - /// - /// - /// The used to localize the action name based on its alias - /// Whether or not this action opens a dialog - public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) - where T : IAction + private MenuItem? CreateMenuItem(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false, bool useLegacyIcon = true) + where T : IAction + { + T? item = _actionCollection.GetAction(); + if (item == null) { - var item = CreateMenuItem(textService, hasSeparator, opensDialog); - if (item != null) - { - Add(item); - return item; - } return null; } - private MenuItem? CreateMenuItem(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) - where T : IAction + IDictionary values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); + values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); + + var menuItem = new MenuItem(item, textService.Localize("actions", item.Alias)) { - var item = _actionCollection.GetAction(); - if (item == null) return null; + SeparatorBefore = hasSeparator, + OpensDialog = opensDialog, + TextDescription = textDescription, + UseLegacyIcon = useLegacyIcon, + }; - var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); - values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); - - var menuItem = new MenuItem(item, textService.Localize($"actions", item.Alias)) - { - SeparatorBefore = hasSeparator, - OpensDialog = opensDialog, - TextDescription = textDescription, - }; - - return menuItem; - } + return menuItem; } } diff --git a/src/Umbraco.Core/Trees/SearchableApplicationTree.cs b/src/Umbraco.Core/Trees/SearchableApplicationTree.cs index 33104cb8c7..44b0a896ac 100644 --- a/src/Umbraco.Core/Trees/SearchableApplicationTree.cs +++ b/src/Umbraco.Core/Trees/SearchableApplicationTree.cs @@ -1,22 +1,26 @@ -namespace Umbraco.Cms.Core.Trees -{ - public class SearchableApplicationTree - { - public SearchableApplicationTree(string appAlias, string treeAlias, int sortOrder, string formatterService, string formatterMethod, ISearchableTree searchableTree) - { - AppAlias = appAlias; - TreeAlias = treeAlias; - SortOrder = sortOrder; - FormatterService = formatterService; - FormatterMethod = formatterMethod; - SearchableTree = searchableTree; - } +namespace Umbraco.Cms.Core.Trees; - public string AppAlias { get; } - public string TreeAlias { get; } - public int SortOrder { get; } - public string FormatterService { get; } - public string FormatterMethod { get; } - public ISearchableTree SearchableTree { get; } +public class SearchableApplicationTree +{ + public SearchableApplicationTree(string appAlias, string treeAlias, int sortOrder, string formatterService, string formatterMethod, ISearchableTree searchableTree) + { + AppAlias = appAlias; + TreeAlias = treeAlias; + SortOrder = sortOrder; + FormatterService = formatterService; + FormatterMethod = formatterMethod; + SearchableTree = searchableTree; } + + public string AppAlias { get; } + + public string TreeAlias { get; } + + public int SortOrder { get; } + + public string FormatterService { get; } + + public string FormatterMethod { get; } + + public ISearchableTree SearchableTree { get; } } diff --git a/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs b/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs index ca5cfef02a..f3a92fe82f 100644 --- a/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs +++ b/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs @@ -1,53 +1,64 @@ -using System; +namespace Umbraco.Cms.Core.Trees; -namespace Umbraco.Cms.Core.Trees +[AttributeUsage(AttributeTargets.Class)] +public sealed class SearchableTreeAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class)] - public sealed class SearchableTreeAttribute : Attribute + public const int DefaultSortOrder = 1000; + + /// + /// This constructor will assume that the method name equals `format(searchResult, appAlias, treeAlias)`. + /// + /// Name of the service. + public SearchableTreeAttribute(string serviceName) + : this(serviceName, string.Empty) { - public const int DefaultSortOrder = 1000; - - public string ServiceName { get; } - - public string MethodName { get; } - - public int SortOrder { get; } - - /// - /// This constructor will assume that the method name equals `format(searchResult, appAlias, treeAlias)`. - /// - /// Name of the service. - public SearchableTreeAttribute(string serviceName) - : this(serviceName, string.Empty) - { } - - /// - /// This constructor defines both the Angular service and method name to use. - /// - /// Name of the service. - /// Name of the method. - public SearchableTreeAttribute(string serviceName, string methodName) - : this(serviceName, methodName, DefaultSortOrder) - { } - - /// - /// This constructor defines both the Angular service and method name to use and explicitly defines a sort order for the results - /// - /// Name of the service. - /// Name of the method. - /// The sort order. - /// serviceName - /// or - /// methodName - /// Value can't be empty or consist only of white-space characters. - serviceName - public SearchableTreeAttribute(string serviceName, string methodName, int sortOrder) - { - if (serviceName == null) throw new ArgumentNullException(nameof(serviceName)); - if (string.IsNullOrWhiteSpace(serviceName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(serviceName)); - - ServiceName = serviceName; - MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); - SortOrder = sortOrder; - } } + + /// + /// This constructor defines both the Angular service and method name to use. + /// + /// Name of the service. + /// Name of the method. + public SearchableTreeAttribute(string serviceName, string methodName) + : this(serviceName, methodName, DefaultSortOrder) + { + } + + /// + /// This constructor defines both the Angular service and method name to use and explicitly defines a sort order for + /// the results + /// + /// Name of the service. + /// Name of the method. + /// The sort order. + /// + /// serviceName + /// or + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - serviceName + public SearchableTreeAttribute(string serviceName, string methodName, int sortOrder) + { + if (serviceName == null) + { + throw new ArgumentNullException(nameof(serviceName)); + } + + if (string.IsNullOrWhiteSpace(serviceName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(serviceName)); + } + + ServiceName = serviceName; + MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); + SortOrder = sortOrder; + } + + public string ServiceName { get; } + + public string MethodName { get; } + + public int SortOrder { get; } } diff --git a/src/Umbraco.Core/Trees/SearchableTreeCollection.cs b/src/Umbraco.Core/Trees/SearchableTreeCollection.cs index ff42b5e8c3..fdf2c8124b 100644 --- a/src/Umbraco.Core/Trees/SearchableTreeCollection.cs +++ b/src/Umbraco.Core/Trees/SearchableTreeCollection.cs @@ -1,50 +1,45 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +public class SearchableTreeCollection : BuilderCollectionBase { - public class SearchableTreeCollection : BuilderCollectionBase + private readonly Dictionary _dictionary; + + public SearchableTreeCollection(Func> items, ITreeService treeService) + : base(items) => + _dictionary = CreateDictionary(treeService); + + public IReadOnlyDictionary SearchableApplicationTrees => _dictionary; + + public SearchableApplicationTree this[string key] => _dictionary[key]; + + private Dictionary CreateDictionary(ITreeService treeService) { - private readonly Dictionary _dictionary; - - public SearchableTreeCollection(Func> items, ITreeService treeService) - : base(items) + Tree[] appTrees = treeService.GetAll() + .OrderBy(x => x.SortOrder) + .ToArray(); + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + ISearchableTree[] searchableTrees = this.ToArray(); + foreach (Tree appTree in appTrees) { - _dictionary = CreateDictionary(treeService); - } - - private Dictionary CreateDictionary(ITreeService treeService) - { - var appTrees = treeService.GetAll() - .OrderBy(x => x.SortOrder) - .ToArray(); - var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - var searchableTrees = this.ToArray(); - foreach (var appTree in appTrees) + ISearchableTree? found = searchableTrees.FirstOrDefault(x => x.TreeAlias.InvariantEquals(appTree.TreeAlias)); + if (found != null) { - var found = searchableTrees.FirstOrDefault(x => x.TreeAlias.InvariantEquals(appTree.TreeAlias)); - if (found != null) - { - var searchableTreeAttribute = found.GetType().GetCustomAttribute(false); - dictionary[found.TreeAlias] = new SearchableApplicationTree( - appTree.SectionAlias, - appTree.TreeAlias, - searchableTreeAttribute?.SortOrder ?? SearchableTreeAttribute.DefaultSortOrder, - searchableTreeAttribute?.ServiceName ?? string.Empty, - searchableTreeAttribute?.MethodName ?? string.Empty, - found - ); - } + SearchableTreeAttribute? searchableTreeAttribute = + found.GetType().GetCustomAttribute(false); + dictionary[found.TreeAlias] = new SearchableApplicationTree( + appTree.SectionAlias, + appTree.TreeAlias, + searchableTreeAttribute?.SortOrder ?? SearchableTreeAttribute.DefaultSortOrder, + searchableTreeAttribute?.ServiceName ?? string.Empty, + searchableTreeAttribute?.MethodName ?? string.Empty, + found); } - return dictionary; } - public IReadOnlyDictionary SearchableApplicationTrees => _dictionary; - - public SearchableApplicationTree this[string key] => _dictionary[key]; + return dictionary; } } diff --git a/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs b/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs index dca2839558..372866ba68 100644 --- a/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs +++ b/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Trees -{ - public class SearchableTreeCollectionBuilder : LazyCollectionBuilderBase - { - protected override SearchableTreeCollectionBuilder This => this; +namespace Umbraco.Cms.Core.Trees; - //per request because generally an instance of ISearchableTree is a controller - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; - } +public class SearchableTreeCollectionBuilder : LazyCollectionBuilderBase +{ + protected override SearchableTreeCollectionBuilder This => this; + + // per request because generally an instance of ISearchableTree is a controller + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; } diff --git a/src/Umbraco.Core/Trees/Tree.cs b/src/Umbraco.Core/Trees/Tree.cs index f229dd8019..47ee0b234b 100644 --- a/src/Umbraco.Core/Trees/Tree.cs +++ b/src/Umbraco.Core/Trees/Tree.cs @@ -1,74 +1,74 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Diagnostics; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +[DebuggerDisplay("Tree - {SectionAlias}/{TreeAlias}")] +public class Tree : ITree { - [DebuggerDisplay("Tree - {SectionAlias}/{TreeAlias}")] - public class Tree : ITree + public Tree(int sortOrder, string applicationAlias, string? group, string alias, string? title, TreeUse use, Type treeControllerType, bool isSingleNodeTree) { - public Tree(int sortOrder, string applicationAlias, string? group, string alias, string? title, TreeUse use, Type treeControllerType, bool isSingleNodeTree) + SortOrder = sortOrder; + SectionAlias = applicationAlias ?? throw new ArgumentNullException(nameof(applicationAlias)); + TreeGroup = group; + TreeAlias = alias ?? throw new ArgumentNullException(nameof(alias)); + TreeTitle = title; + TreeUse = use; + TreeControllerType = treeControllerType ?? throw new ArgumentNullException(nameof(treeControllerType)); + IsSingleNodeTree = isSingleNodeTree; + } + + /// + /// Gets the tree controller type. + /// + public Type TreeControllerType { get; } + + /// + public int SortOrder { get; set; } + + /// + public string SectionAlias { get; set; } + + /// + public string? TreeGroup { get; } + + /// + public string TreeAlias { get; } + + /// + public string? TreeTitle { get; set; } + + /// + public TreeUse TreeUse { get; set; } + + /// + public bool IsSingleNodeTree { get; } + + public static string? GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) + { + var label = $"[{tree.TreeAlias}]"; + + // try to look up a the localized tree header matching the tree alias + var localizedLabel = textService.Localize("treeHeader", tree.TreeAlias); + + // if the localizedLabel returns [alias] then return the title if it's defined + if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) { - SortOrder = sortOrder; - SectionAlias = applicationAlias ?? throw new ArgumentNullException(nameof(applicationAlias)); - TreeGroup = group; - TreeAlias = alias ?? throw new ArgumentNullException(nameof(alias)); - TreeTitle = title; - TreeUse = use; - TreeControllerType = treeControllerType ?? throw new ArgumentNullException(nameof(treeControllerType)); - IsSingleNodeTree = isSingleNodeTree; + if (string.IsNullOrEmpty(tree.TreeTitle) == false) + { + label = tree.TreeTitle; + } + } + else + { + // the localizedLabel translated into something that's not just [alias], so use the translation + label = localizedLabel; } - /// - public int SortOrder { get; set; } - - /// - public string SectionAlias { get; set; } - - /// - public string? TreeGroup { get; } - - /// - public string TreeAlias { get; } - - /// - public string? TreeTitle { get; set; } - - /// - public TreeUse TreeUse { get; set; } - - /// - public bool IsSingleNodeTree { get; } - - /// - /// Gets the tree controller type. - /// - public Type TreeControllerType { get; } - - public static string? GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) - { - var label = $"[{tree.TreeAlias}]"; - - // try to look up a the localized tree header matching the tree alias - var localizedLabel = textService.Localize("treeHeader", tree.TreeAlias); - - // if the localizedLabel returns [alias] then return the title if it's defined - if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) - { - if (string.IsNullOrEmpty(tree.TreeTitle) == false) - label = tree.TreeTitle; - } - else - { - // the localizedLabel translated into something that's not just [alias], so use the translation - label = localizedLabel; - } - - return label; - } + return label; } } diff --git a/src/Umbraco.Core/Trees/TreeCollection.cs b/src/Umbraco.Core/Trees/TreeCollection.cs index 59fa99819c..fa6283753a 100644 --- a/src/Umbraco.Core/Trees/TreeCollection.cs +++ b/src/Umbraco.Core/Trees/TreeCollection.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Trees -{ - /// - /// Represents the collection of section trees. - /// - public class TreeCollection : BuilderCollectionBase - { +namespace Umbraco.Cms.Core.Trees; - public TreeCollection(Func> items) : base(items) - { - } +/// +/// Represents the collection of section trees. +/// +public class TreeCollection : BuilderCollectionBase +{ + public TreeCollection(Func> items) + : base(items) + { } } diff --git a/src/Umbraco.Core/Trees/TreeNode.cs b/src/Umbraco.Core/Trees/TreeNode.cs index 3c166c9fdd..dde66bd3a3 100644 --- a/src/Umbraco.Core/Trees/TreeNode.cs +++ b/src/Umbraco.Core/Trees/TreeNode.cs @@ -1,128 +1,130 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// Represents a model in the tree +/// +/// +/// TreeNode is sealed to prevent developers from adding additional json data to the response +/// +[DataContract(Name = "node", Namespace = "")] +public class TreeNode : EntityBasic { /// - /// Represents a model in the tree + /// Internal constructor, to create a tree node use the CreateTreeNode methods of the TreeApiController. + /// + /// + /// The parent id for the current node + /// + /// + public TreeNode(string nodeId, string? parentId, string? getChildNodesUrl, string? menuUrl) + { + if (nodeId == null) + { + throw new ArgumentNullException(nameof(nodeId)); + } + + if (string.IsNullOrWhiteSpace(nodeId)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(nodeId)); + } + + Id = nodeId; + ParentId = parentId; + ChildNodesUrl = getChildNodesUrl; + MenuUrl = menuUrl; + CssClasses = new List(); + + // default + Icon = "icon-folder-close"; + Path = "-1"; + } + + [DataMember(Name = "parentId", IsRequired = true)] + public new object? ParentId { get; set; } + + /// + /// A flag to set whether or not this node has children + /// + [DataMember(Name = "hasChildren")] + public bool HasChildren { get; set; } + + /// + /// The tree nodetype which refers to the type of node rendered in the tree + /// + [DataMember(Name = "nodeType")] + public string? NodeType { get; set; } + + /// + /// Optional: The Route path for the editor for this node /// /// - /// TreeNode is sealed to prevent developers from adding additional json data to the response + /// If this is not set, then the route path will be automatically determined by: {section}/edit/{id} /// - [DataContract(Name = "node", Namespace = "")] - public class TreeNode : EntityBasic + [DataMember(Name = "routePath")] + public string? RoutePath { get; set; } + + /// + /// The JSON URL to load the nodes children + /// + [DataMember(Name = "childNodesUrl")] + public string? ChildNodesUrl { get; set; } + + /// + /// The JSON URL to load the menu from + /// + [DataMember(Name = "menuUrl")] + public string? MenuUrl { get; set; } + + /// + /// Returns true if the icon represents a CSS class instead of a file path + /// + [DataMember(Name = "iconIsClass")] + public bool IconIsClass { - /// - /// Internal constructor, to create a tree node use the CreateTreeNode methods of the TreeApiController. - /// - /// - /// The parent id for the current node - /// - /// - public TreeNode(string nodeId, string? parentId, string? getChildNodesUrl, string? menuUrl) + get { - if (nodeId == null) throw new ArgumentNullException(nameof(nodeId)); - if (string.IsNullOrWhiteSpace(nodeId)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(nodeId)); - - Id = nodeId; - ParentId = parentId; - ChildNodesUrl = getChildNodesUrl; - MenuUrl = menuUrl; - CssClasses = new List(); - //default - Icon = "icon-folder-close"; - Path = "-1"; - } - - [DataMember(Name = "parentId", IsRequired = true)] - public new object? ParentId { get; set; } - - /// - /// A flag to set whether or not this node has children - /// - [DataMember(Name = "hasChildren")] - public bool HasChildren { get; set; } - - /// - /// The tree nodetype which refers to the type of node rendered in the tree - /// - [DataMember(Name = "nodeType")] - public string? NodeType { get; set; } - - /// - /// Optional: The Route path for the editor for this node - /// - /// - /// If this is not set, then the route path will be automatically determined by: {section}/edit/{id} - /// - [DataMember(Name = "routePath")] - public string? RoutePath { get; set; } - - /// - /// The JSON URL to load the nodes children - /// - [DataMember(Name = "childNodesUrl")] - public string? ChildNodesUrl { get; set; } - - /// - /// The JSON URL to load the menu from - /// - [DataMember(Name = "menuUrl")] - public string? MenuUrl { get; set; } - - /// - /// Returns true if the icon represents a CSS class instead of a file path - /// - [DataMember(Name = "iconIsClass")] - public bool IconIsClass - { - get + if (Icon.IsNullOrWhiteSpace()) { - if (Icon.IsNullOrWhiteSpace()) - { - return true; - } - - if (Icon!.StartsWith("..")) - return false; - - - //if it starts with a '.' or doesn't contain a '.' at all then it is a class - return Icon.StartsWith(".") || Icon.Contains(".") == false; + return true; } - } - /// - /// Returns the icon file path if the icon is not a class, otherwise returns an empty string - /// - [DataMember(Name = "iconFilePath")] - public string IconFilePath - { - get + if (Icon!.StartsWith("..")) { - return string.Empty; - - //TODO Figure out how to do this, without the model has to know a bout services and config. - // - // if (IconIsClass) - // return string.Empty; - // - // //absolute path with or without tilde - // if (Icon.StartsWith("~") || Icon.StartsWith("/")) - // return IOHelper.ResolveUrl("~" + Icon.TrimStart(Constants.CharArrays.Tilde)); - // - // //legacy icon path - // return string.Format("{0}images/umbraco/{1}", Current.Configs.Global().Path.EnsureEndsWith("/"), Icon); + return false; } - } - /// - /// A list of additional/custom css classes to assign to the node - /// - [DataMember(Name = "cssClasses")] - public IList CssClasses { get; private set; } + // if it starts with a '.' or doesn't contain a '.' at all then it is a class + return Icon.StartsWith(".") || Icon.Contains(".") == false; + } } + + /// + /// Returns the icon file path if the icon is not a class, otherwise returns an empty string + /// + [DataMember(Name = "iconFilePath")] + public string IconFilePath => string.Empty; + + // TODO Figure out how to do this, without the model has to know a bout services and config. + // + // if (IconIsClass) + // return string.Empty; + // + // //absolute path with or without tilde + // if (Icon.StartsWith("~") || Icon.StartsWith("/")) + // return IOHelper.ResolveUrl("~" + Icon.TrimStart(Constants.CharArrays.Tilde)); + // + // //legacy icon path + // return string.Format("{0}images/umbraco/{1}", Current.Configs.Global().Path.EnsureEndsWith("/"), Icon); + + /// + /// A list of additional/custom css classes to assign to the node + /// + [DataMember(Name = "cssClasses")] + public IList CssClasses { get; private set; } } diff --git a/src/Umbraco.Core/Trees/TreeNodeCollection.cs b/src/Umbraco.Core/Trees/TreeNodeCollection.cs index 545b6881aa..b76fcc41ce 100644 --- a/src/Umbraco.Core/Trees/TreeNodeCollection.cs +++ b/src/Umbraco.Core/Trees/TreeNodeCollection.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +[CollectionDataContract(Name = "nodes", Namespace = "")] +public sealed class TreeNodeCollection : List { - [CollectionDataContract(Name = "nodes", Namespace = "")] - public sealed class TreeNodeCollection : List + public TreeNodeCollection() { - public static TreeNodeCollection Empty => new TreeNodeCollection(); - - public TreeNodeCollection() - { - } - - public TreeNodeCollection(IEnumerable nodes) - : base(nodes) - { - } } + + public TreeNodeCollection(IEnumerable nodes) + : base(nodes) + { + } + + public static TreeNodeCollection Empty => new(); } diff --git a/src/Umbraco.Core/Trees/TreeNodeExtensions.cs b/src/Umbraco.Core/Trees/TreeNodeExtensions.cs index 9e887f68ec..7fdc8ef480 100644 --- a/src/Umbraco.Core/Trees/TreeNodeExtensions.cs +++ b/src/Umbraco.Core/Trees/TreeNodeExtensions.cs @@ -1,82 +1,79 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Trees; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TreeNodeExtensions { - public static class TreeNodeExtensions + internal const string LegacyJsCallbackKey = "jsClickCallback"; + + /// + /// Sets the node style to show that it is a container type + /// + /// + public static void SetContainerStyle(this TreeNode treeNode) { - internal const string LegacyJsCallbackKey = "jsClickCallback"; - - /// - /// Legacy tree node's assign a JS method callback for when an item is clicked, this method facilitates that. - /// - /// - /// - internal static void AssignLegacyJsCallback(this TreeNode treeNode, string jsCallback) + if (treeNode.CssClasses.Contains("is-container") == false) { - treeNode.AdditionalData[LegacyJsCallbackKey] = jsCallback; + treeNode.CssClasses.Add("is-container"); } + } - /// - /// Sets the node style to show that it is a container type - /// - /// - public static void SetContainerStyle(this TreeNode treeNode) + /// + /// Legacy tree node's assign a JS method callback for when an item is clicked, this method facilitates that. + /// + /// + /// + internal static void AssignLegacyJsCallback(this TreeNode treeNode, string jsCallback) => + treeNode.AdditionalData[LegacyJsCallbackKey] = jsCallback; + + /// + /// Sets the node style to show that it is currently protected publicly + /// + /// + public static void SetProtectedStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("protected") == false) { - if (treeNode.CssClasses.Contains("is-container") == false) - { - treeNode.CssClasses.Add("is-container"); - } + treeNode.CssClasses.Add("protected"); } + } - /// - /// Sets the node style to show that it is currently protected publicly - /// - /// - public static void SetProtectedStyle(this TreeNode treeNode) + /// + /// Sets the node style to show that it is currently locked / non-deletable + /// + /// + public static void SetLockedStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("locked") == false) { - if (treeNode.CssClasses.Contains("protected") == false) - { - treeNode.CssClasses.Add("protected"); - } + treeNode.CssClasses.Add("locked"); } + } - /// - /// Sets the node style to show that it is currently locked / non-deletable - /// - /// - public static void SetLockedStyle(this TreeNode treeNode) + /// + /// Sets the node style to show that it is has unpublished versions (but is currently published) + /// + /// + public static void SetHasPendingVersionStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("has-unpublished-version") == false) { - if (treeNode.CssClasses.Contains("locked") == false) - { - treeNode.CssClasses.Add("locked"); - } + treeNode.CssClasses.Add("has-unpublished-version"); } + } - /// - /// Sets the node style to show that it is has unpublished versions (but is currently published) - /// - /// - public static void SetHasPendingVersionStyle(this TreeNode treeNode) + /// + /// Sets the node style to show that it is not published + /// + /// + public static void SetNotPublishedStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("not-published") == false) { - if (treeNode.CssClasses.Contains("has-unpublished-version") == false) - { - treeNode.CssClasses.Add("has-unpublished-version"); - } - } - - /// - /// Sets the node style to show that it is not published - /// - /// - public static void SetNotPublishedStyle(this TreeNode treeNode) - { - if (treeNode.CssClasses.Contains("not-published") == false) - { - treeNode.CssClasses.Add("not-published"); - } + treeNode.CssClasses.Add("not-published"); } } } diff --git a/src/Umbraco.Core/Trees/TreeUse.cs b/src/Umbraco.Core/Trees/TreeUse.cs index 55be24d54d..ff06bc1dea 100644 --- a/src/Umbraco.Core/Trees/TreeUse.cs +++ b/src/Umbraco.Core/Trees/TreeUse.cs @@ -1,26 +1,23 @@ -using System; +namespace Umbraco.Cms.Core.Trees; -namespace Umbraco.Cms.Core.Trees +/// +/// Defines tree uses. +/// +[Flags] +public enum TreeUse { /// - /// Defines tree uses. + /// The tree is not used. /// - [Flags] - public enum TreeUse - { - /// - /// The tree is not used. - /// - None = 0, + None = 0, - /// - /// The tree is used as a main (section) tree. - /// - Main = 1, + /// + /// The tree is used as a main (section) tree. + /// + Main = 1, - /// - /// The tree is used as a dialog. - /// - Dialog = 2, - } + /// + /// The tree is used as a dialog. + /// + Dialog = 2, } diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index 2e141e2e66..bc6c1ab6ac 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -1,171 +1,186 @@ -using System; using System.ComponentModel; -using System.Linq; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents an entity identifier. +/// +/// An Udi can be fully qualified or "closed" eg umb://document/{guid} or "open" eg umb://document. +[TypeConverter(typeof(UdiTypeConverter))] +public abstract class Udi : IComparable { + /// + /// Initializes a new instance of the Udi class. + /// + /// The entity type part of the identifier. + /// The string value of the identifier. + protected Udi(string entityType, string stringValue) + { + EntityType = entityType; + UriValue = new Uri(stringValue); + } /// - /// Represents an entity identifier. + /// Initializes a new instance of the Udi class. /// - /// An Udi can be fully qualified or "closed" eg umb://document/{guid} or "open" eg umb://document. - [TypeConverter(typeof(UdiTypeConverter))] - public abstract class Udi : IComparable + /// The uri value of the identifier. + protected Udi(Uri uriValue) { - public Uri UriValue { get; } - - /// - /// Initializes a new instance of the Udi class. - /// - /// The entity type part of the identifier. - /// The string value of the identifier. - protected Udi(string entityType, string stringValue) - { - EntityType = entityType; - UriValue = new Uri(stringValue); - } - - /// - /// Initializes a new instance of the Udi class. - /// - /// The uri value of the identifier. - protected Udi(Uri uriValue) - { - EntityType = uriValue.Host; - UriValue = uriValue; - } - - - - /// - /// Gets the entity type part of the identifier. - /// - public string EntityType { get; private set; } - - public int CompareTo(Udi? other) - { - return string.Compare(UriValue.ToString(), other?.UriValue.ToString(), StringComparison.OrdinalIgnoreCase); - } - - public override string ToString() - { - // UriValue is created in the ctor and is never null - // use AbsoluteUri here and not ToString else it's not encoded! - return UriValue.AbsoluteUri; - } - - - - /// - /// Creates a root Udi for an entity type. - /// - /// The entity type. - /// The root Udi for the entity type. - public static Udi Create(string entityType) - { - return UdiParser.GetRootUdi(entityType); - } - - /// - /// Creates a string Udi. - /// - /// The entity type. - /// The identifier. - /// The string Udi for the entity type and identifier. - public static Udi Create(string entityType, string id) - { - if (UdiParser.UdiTypes.TryGetValue(entityType, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); - - if (string.IsNullOrWhiteSpace(id)) - throw new ArgumentException("Value cannot be null or whitespace.", "id"); - if (udiType != UdiType.StringUdi) - throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have string udis.", entityType)); - - return new StringUdi(entityType, id); - } - - /// - /// Creates a Guid Udi. - /// - /// The entity type. - /// The identifier. - /// The Guid Udi for the entity type and identifier. - public static Udi Create(string? entityType, Guid id) - { - if (entityType is null || UdiParser.UdiTypes.TryGetValue(entityType, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); - - if (udiType != UdiType.GuidUdi) - throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have guid udis.", entityType)); - if (id == default(Guid)) - throw new ArgumentException("Cannot be an empty guid.", "id"); - - return new GuidUdi(entityType, id); - } - - public static Udi Create(Uri uri) - { - // if it's a know type go fast and use ctors - // else fallback to parsing the string (and guess the type) - - if (UdiParser.UdiTypes.TryGetValue(uri.Host, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); - - if (udiType == UdiType.GuidUdi) - return new GuidUdi(uri); - if (udiType == UdiType.StringUdi) - return new StringUdi(uri); - - throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); - } - - public void EnsureType(params string[] validTypes) - { - if (validTypes.Contains(EntityType) == false) - throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); - } - - /// - /// Gets a value indicating whether this Udi is a root Udi. - /// - /// A root Udi points to the "root of all things" for a given entity type, e.g. the content tree root. - public abstract bool IsRoot { get; } - - /// - /// Ensures that this Udi is not a root Udi. - /// - /// This Udi. - /// When this Udi is a Root Udi. - public Udi EnsureNotRoot() - { - if (IsRoot) throw new Exception("Root Udi."); - return this; - } - - public override bool Equals(object? obj) - { - var other = obj as Udi; - return other is not null && GetType() == other.GetType() && UriValue == other.UriValue; - } - - public override int GetHashCode() - { - return UriValue.GetHashCode(); - } - - public static bool operator ==(Udi? udi1, Udi? udi2) - { - if (ReferenceEquals(udi1, udi2)) return true; - if ((object?)udi1 == null || (object?)udi2 == null) return false; - return udi1.Equals(udi2); - } - - public static bool operator !=(Udi? udi1, Udi? udi2) - { - return (udi1 == udi2) == false; - } - - + EntityType = uriValue.Host; + UriValue = uriValue; } + + public Uri UriValue { get; } + + /// + /// Gets the entity type part of the identifier. + /// + public string EntityType { get; } + + /// + /// Gets a value indicating whether this Udi is a root Udi. + /// + /// A root Udi points to the "root of all things" for a given entity type, e.g. the content tree root. + public abstract bool IsRoot { get; } + + public static bool operator ==(Udi? udi1, Udi? udi2) + { + if (ReferenceEquals(udi1, udi2)) + { + return true; + } + + if (udi1 is null || udi2 is null) + { + return false; + } + + return udi1.Equals(udi2); + } + + /// + /// Creates a root Udi for an entity type. + /// + /// The entity type. + /// The root Udi for the entity type. + public static Udi Create(string entityType) => UdiParser.GetRootUdi(entityType); + + public int CompareTo(Udi? other) => string.Compare(UriValue.ToString(), other?.UriValue.ToString(), StringComparison.OrdinalIgnoreCase); + + public override string ToString() => + + // UriValue is created in the ctor and is never null + // use AbsoluteUri here and not ToString else it's not encoded! + UriValue.AbsoluteUri; + + /// + /// Creates a string Udi. + /// + /// The entity type. + /// The identifier. + /// The string Udi for the entity type and identifier. + public static Udi Create(string entityType, string id) + { + if (UdiParser.UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) + { + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); + } + + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "id"); + } + + if (udiType != UdiType.StringUdi) + { + throw new InvalidOperationException(string.Format( + "Entity type \"{0}\" does not have string udis.", + entityType)); + } + + return new StringUdi(entityType, id); + } + + /// + /// Creates a Guid Udi. + /// + /// The entity type. + /// The identifier. + /// The Guid Udi for the entity type and identifier. + public static Udi Create(string? entityType, Guid id) + { + if (entityType is null || UdiParser.UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) + { + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); + } + + if (udiType != UdiType.GuidUdi) + { + throw new InvalidOperationException(string.Format( + "Entity type \"{0}\" does not have guid udis.", + entityType)); + } + + if (id == default) + { + throw new ArgumentException("Cannot be an empty guid.", "id"); + } + + return new GuidUdi(entityType, id); + } + + public static Udi Create(Uri uri) + { + // if it's a know type go fast and use ctors + // else fallback to parsing the string (and guess the type) + if (UdiParser.UdiTypes.TryGetValue(uri.Host, out UdiType udiType) == false) + { + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); + } + + if (udiType == UdiType.GuidUdi) + { + return new GuidUdi(uri); + } + + if (udiType == UdiType.StringUdi) + { + return new StringUdi(uri); + } + + throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); + } + + public void EnsureType(params string[] validTypes) + { + if (validTypes.Contains(EntityType) == false) + { + throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); + } + } + + /// + /// Ensures that this Udi is not a root Udi. + /// + /// This Udi. + /// When this Udi is a Root Udi. + public Udi EnsureNotRoot() + { + if (IsRoot) + { + throw new Exception("Root Udi."); + } + + return this; + } + + public override bool Equals(object? obj) + { + var other = obj as Udi; + return other is not null && GetType() == other.GetType() && UriValue == other.UriValue; + } + + public override int GetHashCode() => UriValue.GetHashCode(); + + public static bool operator !=(Udi? udi1, Udi? udi2) => udi1 == udi2 == false; } diff --git a/src/Umbraco.Core/UdiDefinitionAttribute.cs b/src/Umbraco.Core/UdiDefinitionAttribute.cs index 9139ef4188..fe96909f78 100644 --- a/src/Umbraco.Core/UdiDefinitionAttribute.cs +++ b/src/Umbraco.Core/UdiDefinitionAttribute.cs @@ -1,20 +1,25 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public sealed class UdiDefinitionAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class UdiDefinitionAttribute : Attribute + public UdiDefinitionAttribute(string entityType, UdiType udiType) { - public UdiDefinitionAttribute(string entityType, UdiType udiType) + if (string.IsNullOrWhiteSpace(entityType)) { - if (string.IsNullOrWhiteSpace(entityType)) throw new ArgumentNullException("entityType"); - if (udiType != UdiType.GuidUdi && udiType != UdiType.StringUdi) throw new ArgumentException("Invalid value.", "udiType"); - EntityType = entityType; - UdiType = udiType; + throw new ArgumentNullException("entityType"); } - public string EntityType { get; private set; } + if (udiType != UdiType.GuidUdi && udiType != UdiType.StringUdi) + { + throw new ArgumentException("Invalid value.", "udiType"); + } - public UdiType UdiType { get; private set; } + EntityType = entityType; + UdiType = udiType; } + + public string EntityType { get; } + + public UdiType UdiType { get; } } diff --git a/src/Umbraco.Core/UdiEntityTypeHelper.cs b/src/Umbraco.Core/UdiEntityTypeHelper.cs index 781c084785..f0e8774cf8 100644 --- a/src/Umbraco.Core/UdiEntityTypeHelper.cs +++ b/src/Umbraco.Core/UdiEntityTypeHelper.cs @@ -1,102 +1,98 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class UdiEntityTypeHelper { - public static class UdiEntityTypeHelper + public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) { - - - public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) + switch (umbracoObjectType) { - switch (umbracoObjectType) - { - case UmbracoObjectTypes.Document: - return Constants.UdiEntityType.Document; - case UmbracoObjectTypes.DocumentBlueprint: - return Constants.UdiEntityType.DocumentBlueprint; - case UmbracoObjectTypes.Media: - return Constants.UdiEntityType.Media; - case UmbracoObjectTypes.Member: - return Constants.UdiEntityType.Member; - case UmbracoObjectTypes.Template: - return Constants.UdiEntityType.Template; - case UmbracoObjectTypes.DocumentType: - return Constants.UdiEntityType.DocumentType; - case UmbracoObjectTypes.DocumentTypeContainer: - return Constants.UdiEntityType.DocumentTypeContainer; - case UmbracoObjectTypes.MediaType: - return Constants.UdiEntityType.MediaType; - case UmbracoObjectTypes.MediaTypeContainer: - return Constants.UdiEntityType.MediaTypeContainer; - case UmbracoObjectTypes.DataType: - return Constants.UdiEntityType.DataType; - case UmbracoObjectTypes.DataTypeContainer: - return Constants.UdiEntityType.DataTypeContainer; - case UmbracoObjectTypes.MemberType: - return Constants.UdiEntityType.MemberType; - case UmbracoObjectTypes.MemberGroup: - return Constants.UdiEntityType.MemberGroup; - case UmbracoObjectTypes.RelationType: - return Constants.UdiEntityType.RelationType; - case UmbracoObjectTypes.FormsForm: - return Constants.UdiEntityType.FormsForm; - case UmbracoObjectTypes.FormsPreValue: - return Constants.UdiEntityType.FormsPreValue; - case UmbracoObjectTypes.FormsDataSource: - return Constants.UdiEntityType.FormsDataSource; - case UmbracoObjectTypes.Language: - return Constants.UdiEntityType.Language; - } - - throw new NotSupportedException( - $"UmbracoObjectType \"{umbracoObjectType}\" does not have a matching EntityType."); + case UmbracoObjectTypes.Document: + return Constants.UdiEntityType.Document; + case UmbracoObjectTypes.DocumentBlueprint: + return Constants.UdiEntityType.DocumentBlueprint; + case UmbracoObjectTypes.Media: + return Constants.UdiEntityType.Media; + case UmbracoObjectTypes.Member: + return Constants.UdiEntityType.Member; + case UmbracoObjectTypes.Template: + return Constants.UdiEntityType.Template; + case UmbracoObjectTypes.DocumentType: + return Constants.UdiEntityType.DocumentType; + case UmbracoObjectTypes.DocumentTypeContainer: + return Constants.UdiEntityType.DocumentTypeContainer; + case UmbracoObjectTypes.MediaType: + return Constants.UdiEntityType.MediaType; + case UmbracoObjectTypes.MediaTypeContainer: + return Constants.UdiEntityType.MediaTypeContainer; + case UmbracoObjectTypes.DataType: + return Constants.UdiEntityType.DataType; + case UmbracoObjectTypes.DataTypeContainer: + return Constants.UdiEntityType.DataTypeContainer; + case UmbracoObjectTypes.MemberType: + return Constants.UdiEntityType.MemberType; + case UmbracoObjectTypes.MemberGroup: + return Constants.UdiEntityType.MemberGroup; + case UmbracoObjectTypes.RelationType: + return Constants.UdiEntityType.RelationType; + case UmbracoObjectTypes.FormsForm: + return Constants.UdiEntityType.FormsForm; + case UmbracoObjectTypes.FormsPreValue: + return Constants.UdiEntityType.FormsPreValue; + case UmbracoObjectTypes.FormsDataSource: + return Constants.UdiEntityType.FormsDataSource; + case UmbracoObjectTypes.Language: + return Constants.UdiEntityType.Language; } - public static UmbracoObjectTypes ToUmbracoObjectType(string entityType) - { - switch (entityType) - { - case Constants.UdiEntityType.Document: - return UmbracoObjectTypes.Document; - case Constants.UdiEntityType.DocumentBlueprint: - return UmbracoObjectTypes.DocumentBlueprint; - case Constants.UdiEntityType.Media: - return UmbracoObjectTypes.Media; - case Constants.UdiEntityType.Member: - return UmbracoObjectTypes.Member; - case Constants.UdiEntityType.Template: - return UmbracoObjectTypes.Template; - case Constants.UdiEntityType.DocumentType: - return UmbracoObjectTypes.DocumentType; - case Constants.UdiEntityType.DocumentTypeContainer: - return UmbracoObjectTypes.DocumentTypeContainer; - case Constants.UdiEntityType.MediaType: - return UmbracoObjectTypes.MediaType; - case Constants.UdiEntityType.MediaTypeContainer: - return UmbracoObjectTypes.MediaTypeContainer; - case Constants.UdiEntityType.DataType: - return UmbracoObjectTypes.DataType; - case Constants.UdiEntityType.DataTypeContainer: - return UmbracoObjectTypes.DataTypeContainer; - case Constants.UdiEntityType.MemberType: - return UmbracoObjectTypes.MemberType; - case Constants.UdiEntityType.MemberGroup: - return UmbracoObjectTypes.MemberGroup; - case Constants.UdiEntityType.RelationType: - return UmbracoObjectTypes.RelationType; - case Constants.UdiEntityType.FormsForm: - return UmbracoObjectTypes.FormsForm; - case Constants.UdiEntityType.FormsPreValue: - return UmbracoObjectTypes.FormsPreValue; - case Constants.UdiEntityType.FormsDataSource: - return UmbracoObjectTypes.FormsDataSource; - case Constants.UdiEntityType.Language: - return UmbracoObjectTypes.Language; - } + throw new NotSupportedException( + $"UmbracoObjectType \"{umbracoObjectType}\" does not have a matching EntityType."); + } - throw new NotSupportedException( - $"EntityType \"{entityType}\" does not have a matching UmbracoObjectType."); + public static UmbracoObjectTypes ToUmbracoObjectType(string entityType) + { + switch (entityType) + { + case Constants.UdiEntityType.Document: + return UmbracoObjectTypes.Document; + case Constants.UdiEntityType.DocumentBlueprint: + return UmbracoObjectTypes.DocumentBlueprint; + case Constants.UdiEntityType.Media: + return UmbracoObjectTypes.Media; + case Constants.UdiEntityType.Member: + return UmbracoObjectTypes.Member; + case Constants.UdiEntityType.Template: + return UmbracoObjectTypes.Template; + case Constants.UdiEntityType.DocumentType: + return UmbracoObjectTypes.DocumentType; + case Constants.UdiEntityType.DocumentTypeContainer: + return UmbracoObjectTypes.DocumentTypeContainer; + case Constants.UdiEntityType.MediaType: + return UmbracoObjectTypes.MediaType; + case Constants.UdiEntityType.MediaTypeContainer: + return UmbracoObjectTypes.MediaTypeContainer; + case Constants.UdiEntityType.DataType: + return UmbracoObjectTypes.DataType; + case Constants.UdiEntityType.DataTypeContainer: + return UmbracoObjectTypes.DataTypeContainer; + case Constants.UdiEntityType.MemberType: + return UmbracoObjectTypes.MemberType; + case Constants.UdiEntityType.MemberGroup: + return UmbracoObjectTypes.MemberGroup; + case Constants.UdiEntityType.RelationType: + return UmbracoObjectTypes.RelationType; + case Constants.UdiEntityType.FormsForm: + return UmbracoObjectTypes.FormsForm; + case Constants.UdiEntityType.FormsPreValue: + return UmbracoObjectTypes.FormsPreValue; + case Constants.UdiEntityType.FormsDataSource: + return UmbracoObjectTypes.FormsDataSource; + case Constants.UdiEntityType.Language: + return UmbracoObjectTypes.Language; } + + throw new NotSupportedException( + $"EntityType \"{entityType}\" does not have a matching UmbracoObjectType."); } } diff --git a/src/Umbraco.Core/UdiParser.cs b/src/Umbraco.Core/UdiParser.cs index 907880db13..30448e1b45 100644 --- a/src/Umbraco.Core/UdiParser.cs +++ b/src/Umbraco.Core/UdiParser.cs @@ -1,222 +1,235 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public sealed class UdiParser { - public sealed class UdiParser + private static readonly ConcurrentDictionary RootUdis = new(); + + static UdiParser() => + + // initialize with known (built-in) Udi types + // we will add scanned types later on + UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); + + internal static ConcurrentDictionary UdiTypes { get; private set; } + + /// + /// Internal API for tests to resets all udi types back to only the known udi types. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void ResetUdiTypes() => UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); + + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// An Udi instance that contains the value that was parsed. + public static Udi Parse(string s) { - private static readonly ConcurrentDictionary RootUdis = new ConcurrentDictionary(); - internal static ConcurrentDictionary UdiTypes { get; private set; } + ParseInternal(s, false, false, out Udi? udi); + return udi!; + } - static UdiParser() + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// A value indicating whether to only deal with known types. + /// An Udi instance that contains the value that was parsed. + /// + /// + /// If is true, and the string could not be parsed because + /// the entity type was not known, the method succeeds but sets udito an + /// value. + /// + /// + /// If is true, assemblies are not scanned for types, + /// and therefore only builtin types may be known. Unless scanning already took place. + /// + /// + public static Udi Parse(string s, bool knownTypes) + { + ParseInternal(s, false, knownTypes, out Udi? udi); + return udi!; + } + + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// An Udi instance that contains the value that was parsed. + /// A boolean value indicating whether the string could be parsed. + public static bool TryParse(string s, [MaybeNullWhen(false)] out Udi udi) => ParseInternal(s, true, false, out udi); + + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// An Udi instance that contains the value that was parsed. + /// A boolean value indicating whether the string could be parsed. + public static bool TryParse(string? s, [MaybeNullWhen(false)] out T udi) + where T : Udi? + { + var result = ParseInternal(s, true, false, out Udi? parsed); + if (result && parsed is T) { - // initialize with known (built-in) Udi types - // we will add scanned types later on - UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); + udi = (T)parsed; + return true; } - /// - /// Internal API for tests to resets all udi types back to only the known udi types. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static void ResetUdiTypes() - { - UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); - } + udi = null; + return false; + } - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// An Udi instance that contains the value that was parsed. - public static Udi Parse(string s) - { - ParseInternal(s, false, false, out var udi); - return udi!; - } + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// A value indicating whether to only deal with known types. + /// An Udi instance that contains the value that was parsed. + /// A boolean value indicating whether the string could be parsed. + /// + /// + /// If is true, and the string could not be parsed because + /// the entity type was not known, the method returns false but still sets udi + /// to an value. + /// + /// + /// If is true, assemblies are not scanned for types, + /// and therefore only builtin types may be known. Unless scanning already took place. + /// + /// + public static bool TryParse(string? s, bool knownTypes, [MaybeNullWhen(false)] out Udi udi) => + ParseInternal(s, true, knownTypes, out udi); - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// A value indicating whether to only deal with known types. - /// An Udi instance that contains the value that was parsed. - /// - /// If is true, and the string could not be parsed because - /// the entity type was not known, the method succeeds but sets udito an - /// value. - /// If is true, assemblies are not scanned for types, - /// and therefore only builtin types may be known. Unless scanning already took place. - /// - public static Udi Parse(string s, bool knownTypes) - { - ParseInternal(s, false, knownTypes, out var udi); - return udi!; - } + /// + /// Registers a custom entity type. + /// + /// + /// + public static void RegisterUdiType(string entityType, UdiType udiType) => UdiTypes.TryAdd(entityType, udiType); - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// An Udi instance that contains the value that was parsed. - /// A boolean value indicating whether the string could be parsed. - public static bool TryParse(string s, [MaybeNullWhen(returnValue: false)] out Udi udi) + internal static Udi GetRootUdi(string entityType) => + RootUdis.GetOrAdd(entityType, x => { - return ParseInternal(s, true, false, out udi); - } - - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// An Udi instance that contains the value that was parsed. - /// A boolean value indicating whether the string could be parsed. - public static bool TryParse(string? s, [MaybeNullWhen(returnValue: false)] out T udi) - where T : Udi? - { - var result = ParseInternal(s, true, false, out var parsed); - if (result && parsed is T) + if (UdiTypes.TryGetValue(x, out UdiType udiType) == false) { - udi = (T)parsed; + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); + } + + return udiType == UdiType.StringUdi + ? new StringUdi(entityType, string.Empty) + : new GuidUdi(entityType, Guid.Empty); + }); + + private static bool ParseInternal(string? s, bool tryParse, bool knownTypes, [MaybeNullWhen(false)] out Udi udi) + { + udi = null; + if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false + || Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false) + { + if (tryParse) + { + return false; + } + + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); + } + + var entityType = uri.Host; + if (UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) + { + if (knownTypes) + { + // not knowing the type is not an error + // just return the unknown type udi + udi = UnknownTypeUdi.Instance; + return false; + } + + if (tryParse) + { + return false; + } + + throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); + } + + var path = uri.AbsolutePath.TrimStart('/'); + + if (udiType == UdiType.GuidUdi) + { + if (path == string.Empty) + { + udi = GetRootUdi(uri.Host); return true; } - udi = null; - return false; - } - - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// A value indicating whether to only deal with known types. - /// An Udi instance that contains the value that was parsed. - /// A boolean value indicating whether the string could be parsed. - /// - /// If is true, and the string could not be parsed because - /// the entity type was not known, the method returns false but still sets udi - /// to an value. - /// If is true, assemblies are not scanned for types, - /// and therefore only builtin types may be known. Unless scanning already took place. - /// - public static bool TryParse(string? s, bool knownTypes, [MaybeNullWhen(returnValue: false)] out Udi udi) - { - return ParseInternal(s, true, knownTypes, out udi); - } - - private static bool ParseInternal(string? s, bool tryParse, bool knownTypes,[MaybeNullWhen(returnValue: false)] out Udi udi) - { - udi = null; - if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false - || Uri.TryCreate(s, UriKind.Absolute, out var uri) == false) + if (Guid.TryParse(path, out Guid guid) == false) { - if (tryParse) return false; + if (tryParse) + { + return false; + } + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } - var entityType = uri.Host; - if (UdiTypes.TryGetValue(entityType, out var udiType) == false) - { - if (knownTypes) - { - // not knowing the type is not an error - // just return the unknown type udi - udi = UnknownTypeUdi.Instance; - return false; - } - if (tryParse) return false; - throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); - } - - var path = uri.AbsolutePath.TrimStart('/'); - - if (udiType == UdiType.GuidUdi) - { - if (path == string.Empty) - { - udi = GetRootUdi(uri.Host); - return true; - } - if (Guid.TryParse(path, out var guid) == false) - { - if (tryParse) return false; - throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); - } - udi = new GuidUdi(uri.Host, guid); - return true; - } - - if (udiType == UdiType.StringUdi) - { - udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path)); - return true; - } - - if (tryParse) return false; - throw new InvalidOperationException(string.Format("Invalid udi type \"{0}\".", udiType)); + udi = new GuidUdi(uri.Host, guid); + return true; } - internal static Udi GetRootUdi(string entityType) + if (udiType == UdiType.StringUdi) { - return RootUdis.GetOrAdd(entityType, x => - { - if (UdiTypes.TryGetValue(x, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); - return udiType == UdiType.StringUdi - ? (Udi)new StringUdi(entityType, string.Empty) - : new GuidUdi(entityType, Guid.Empty); - }); + udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path)); + return true; } + if (tryParse) + { + return false; + } - - /// - /// Registers a custom entity type. - /// - /// - /// - public static void RegisterUdiType(string entityType, UdiType udiType) => UdiTypes.TryAdd(entityType, udiType); - - public static Dictionary GetKnownUdiTypes() => - new Dictionary - { - { Constants.UdiEntityType.Unknown, UdiType.Unknown }, - - { Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi }, - { Constants.UdiEntityType.Element, UdiType.GuidUdi }, - { Constants.UdiEntityType.Document, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi }, - { Constants.UdiEntityType.Media, UdiType.GuidUdi }, - { Constants.UdiEntityType.Member, UdiType.GuidUdi }, - { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, - { Constants.UdiEntityType.Macro, UdiType.GuidUdi }, - { Constants.UdiEntityType.Template, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentType, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentTypeBluePrints, UdiType.GuidUdi }, - { Constants.UdiEntityType.MediaType, UdiType.GuidUdi }, - { Constants.UdiEntityType.MediaTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, - { Constants.UdiEntityType.DataTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.MemberType, UdiType.GuidUdi }, - { Constants.UdiEntityType.MemberGroup, UdiType.GuidUdi }, - { Constants.UdiEntityType.RelationType, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsForm, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsPreValue, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsDataSource, UdiType.GuidUdi }, - - { Constants.UdiEntityType.AnyString, UdiType.StringUdi }, - { Constants.UdiEntityType.Language, UdiType.StringUdi }, - { Constants.UdiEntityType.MacroScript, UdiType.StringUdi }, - { Constants.UdiEntityType.MediaFile, UdiType.StringUdi }, - { Constants.UdiEntityType.TemplateFile, UdiType.StringUdi }, - { Constants.UdiEntityType.Script, UdiType.StringUdi }, - { Constants.UdiEntityType.PartialView, UdiType.StringUdi }, - { Constants.UdiEntityType.PartialViewMacro, UdiType.StringUdi }, - { Constants.UdiEntityType.Stylesheet, UdiType.StringUdi } - }; + throw new InvalidOperationException(string.Format("Invalid udi type \"{0}\".", udiType)); } + + public static Dictionary GetKnownUdiTypes() => + new() + { + { Constants.UdiEntityType.Unknown, UdiType.Unknown }, + { Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi }, + { Constants.UdiEntityType.Element, UdiType.GuidUdi }, + { Constants.UdiEntityType.Document, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi }, + { Constants.UdiEntityType.Media, UdiType.GuidUdi }, + { Constants.UdiEntityType.Member, UdiType.GuidUdi }, + { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, + { Constants.UdiEntityType.Macro, UdiType.GuidUdi }, + { Constants.UdiEntityType.Template, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentType, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentTypeBluePrints, UdiType.GuidUdi }, + { Constants.UdiEntityType.MediaType, UdiType.GuidUdi }, + { Constants.UdiEntityType.MediaTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, + { Constants.UdiEntityType.DataTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.MemberType, UdiType.GuidUdi }, + { Constants.UdiEntityType.MemberGroup, UdiType.GuidUdi }, + { Constants.UdiEntityType.RelationType, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsForm, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsPreValue, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsDataSource, UdiType.GuidUdi }, + { Constants.UdiEntityType.AnyString, UdiType.StringUdi }, + { Constants.UdiEntityType.Language, UdiType.StringUdi }, + { Constants.UdiEntityType.MacroScript, UdiType.StringUdi }, + { Constants.UdiEntityType.MediaFile, UdiType.StringUdi }, + { Constants.UdiEntityType.TemplateFile, UdiType.StringUdi }, + { Constants.UdiEntityType.Script, UdiType.StringUdi }, + { Constants.UdiEntityType.PartialView, UdiType.StringUdi }, + { Constants.UdiEntityType.PartialViewMacro, UdiType.StringUdi }, + { Constants.UdiEntityType.Stylesheet, UdiType.StringUdi }, + }; } diff --git a/src/Umbraco.Core/UdiParserServiceConnectors.cs b/src/Umbraco.Core/UdiParserServiceConnectors.cs index 320cc9a901..4c307435de 100644 --- a/src/Umbraco.Core/UdiParserServiceConnectors.cs +++ b/src/Umbraco.Core/UdiParserServiceConnectors.cs @@ -1,82 +1,99 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Deploy; using Umbraco.Extensions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class UdiParserServiceConnectors { - public static class UdiParserServiceConnectors + private static readonly object ScanLocker = new(); + + // notes - see U4-10409 + // if this class is used during application pre-start it cannot scans the assemblies, + // this is addressed by lazily-scanning, with the following caveats: + // - parsing a root udi still requires a scan and therefore still breaks + // - parsing an invalid udi ("umb://should-be-guid/") corrupts KnowUdiTypes + private static volatile bool _scanned; + + /// + /// Scan for deploy in assemblies for known UDI types. + /// + /// + public static void ScanDeployServiceConnectorsForUdiTypes(TypeLoader typeLoader) { - // notes - see U4-10409 - // if this class is used during application pre-start it cannot scans the assemblies, - // this is addressed by lazily-scanning, with the following caveats: - // - parsing a root udi still requires a scan and therefore still breaks - // - parsing an invalid udi ("umb://should-be-guid/") corrupts KnowUdiTypes - - private static volatile bool _scanned; - private static readonly object ScanLocker = new object(); - - /// - /// Scan for deploy in assemblies for known UDI types. - /// - /// - public static void ScanDeployServiceConnectorsForUdiTypes(TypeLoader typeLoader) + if (typeLoader is null) { - if (typeLoader is null) - throw new ArgumentNullException(nameof(typeLoader)); - - if (_scanned) return; - - lock (ScanLocker) - { - // Scan for unknown UDI types - // there is no way we can get the "registered" service connectors, as registration - // happens in Deploy, not in Core, and the Udi class belongs to Core - therefore, we - // just pick every service connectors - just making sure that not two of them - // would register the same entity type, with different udi types (would not make - // much sense anyways) - var connectors = typeLoader.GetTypes(); - var result = new Dictionary(); - foreach (var connector in connectors) - { - var attrs = connector.GetCustomAttributes(false); - foreach (var attr in attrs) - { - if (result.TryGetValue(attr.EntityType, out var udiType) && udiType != attr.UdiType) - throw new Exception(string.Format("Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", attr.EntityType)); - result[attr.EntityType] = attr.UdiType; - } - } - - // merge these into the known list - foreach (var item in result) - UdiParser.RegisterUdiType(item.Key, item.Value); - - _scanned = true; - } + throw new ArgumentNullException(nameof(typeLoader)); } - /// - /// Registers a single to add it's UDI type. - /// - /// - public static void RegisterServiceConnector() - where T: IServiceConnector + if (_scanned) { + return; + } + + lock (ScanLocker) + { + // Scan for unknown UDI types + // there is no way we can get the "registered" service connectors, as registration + // happens in Deploy, not in Core, and the Udi class belongs to Core - therefore, we + // just pick every service connectors - just making sure that not two of them + // would register the same entity type, with different udi types (would not make + // much sense anyways) + IEnumerable connectors = typeLoader.GetTypes(); var result = new Dictionary(); - var connector = typeof(T); - var attrs = connector.GetCustomAttributes(false); - foreach (var attr in attrs) + foreach (Type connector in connectors) { - if (result.TryGetValue(attr.EntityType, out var udiType) && udiType != attr.UdiType) - throw new Exception(string.Format("Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", attr.EntityType)); - result[attr.EntityType] = attr.UdiType; + IEnumerable + attrs = connector.GetCustomAttributes(false); + foreach (UdiDefinitionAttribute attr in attrs) + { + if (result.TryGetValue(attr.EntityType, out UdiType udiType) && udiType != attr.UdiType) + { + throw new Exception(string.Format( + "Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", + attr.EntityType)); + } + + result[attr.EntityType] = attr.UdiType; + } } // merge these into the known list - foreach (var item in result) + foreach (KeyValuePair item in result) + { UdiParser.RegisterUdiType(item.Key, item.Value); + } + + _scanned = true; + } + } + + /// + /// Registers a single to add it's UDI type. + /// + /// + public static void RegisterServiceConnector() + where T : IServiceConnector + { + var result = new Dictionary(); + Type connector = typeof(T); + IEnumerable attrs = connector.GetCustomAttributes(false); + foreach (UdiDefinitionAttribute attr in attrs) + { + if (result.TryGetValue(attr.EntityType, out UdiType udiType) && udiType != attr.UdiType) + { + throw new Exception(string.Format( + "Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", + attr.EntityType)); + } + + result[attr.EntityType] = attr.UdiType; + } + + // merge these into the known list + foreach (KeyValuePair item in result) + { + UdiParser.RegisterUdiType(item.Key, item.Value); } } } diff --git a/src/Umbraco.Core/UdiRange.cs b/src/Umbraco.Core/UdiRange.cs index ca5b07bf36..5d98664a3e 100644 --- a/src/Umbraco.Core/UdiRange.cs +++ b/src/Umbraco.Core/UdiRange.cs @@ -1,103 +1,96 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents a range. +/// +/// +/// +/// A Udi range is composed of a which represents the base of the range, +/// plus a selector that can be "." (the Udi), ".*" (the Udi and its children), ".**" (the udi and +/// its descendants, "*" (the children of the Udi), and "**" (the descendants of the Udi). +/// +/// The Udi here can be a closed entity, or an open entity. +/// +public class UdiRange { + private readonly Uri _uriValue; + /// - /// Represents a range. + /// Initializes a new instance of the class with a and an optional + /// selector. /// - /// - /// A Udi range is composed of a which represents the base of the range, - /// plus a selector that can be "." (the Udi), ".*" (the Udi and its children), ".**" (the udi and - /// its descendants, "*" (the children of the Udi), and "**" (the descendants of the Udi). - /// The Udi here can be a closed entity, or an open entity. - public class UdiRange + /// A . + /// An optional selector. + public UdiRange(Udi udi, string selector = Constants.DeploySelector.This) { - private readonly Uri _uriValue; - - /// - /// Initializes a new instance of the class with a and an optional selector. - /// - /// A . - /// An optional selector. - public UdiRange(Udi udi, string selector = Constants.DeploySelector.This) + Udi = udi; + switch (selector) { - Udi = udi; - switch (selector) - { - case Constants.DeploySelector.This: - Selector = selector; - _uriValue = udi.UriValue; - break; - case Constants.DeploySelector.ChildrenOfThis: - case Constants.DeploySelector.DescendantsOfThis: - case Constants.DeploySelector.ThisAndChildren: - case Constants.DeploySelector.ThisAndDescendants: - Selector = selector; - _uriValue = new Uri(Udi + "?" + selector); - break; - default: - throw new ArgumentException(string.Format("Invalid selector \"{0}\".", selector)); - } - } - - /// - /// Gets the for this range. - /// - public Udi Udi { get; private set; } - - /// - /// Gets or sets the selector for this range. - /// - public string Selector { get; private set; } - - /// - /// Gets the entity type of the for this range. - /// - public string EntityType - { - get { return Udi.EntityType; } - } - - public static UdiRange Parse(string s) - { - Uri? uri; - - if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false - || Uri.TryCreate(s, UriKind.Absolute, out uri) == false) - { - //if (tryParse) return false; - throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s)); - } - - var udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri; - return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart(Constants.CharArrays.QuestionMark)); - } - - public override string ToString() - { - return _uriValue.ToString(); - } - - public override bool Equals(object? obj) - { - return obj is UdiRange other && GetType() == other.GetType() && _uriValue == other._uriValue; - } - - public override int GetHashCode() - { - return _uriValue.GetHashCode(); - } - - public static bool operator ==(UdiRange range1, UdiRange range2) - { - if (ReferenceEquals(range1, range2)) return true; - if ((object)range1 == null || (object)range2 == null) return false; - return range1.Equals(range2); - } - - public static bool operator !=(UdiRange range1, UdiRange range2) - { - return !(range1 == range2); + case Constants.DeploySelector.This: + Selector = selector; + _uriValue = udi.UriValue; + break; + case Constants.DeploySelector.ChildrenOfThis: + case Constants.DeploySelector.DescendantsOfThis: + case Constants.DeploySelector.ThisAndChildren: + case Constants.DeploySelector.ThisAndDescendants: + Selector = selector; + _uriValue = new Uri(Udi + "?" + selector); + break; + default: + throw new ArgumentException(string.Format("Invalid selector \"{0}\".", selector)); } } + + /// + /// Gets the for this range. + /// + public Udi Udi { get; } + + /// + /// Gets or sets the selector for this range. + /// + public string Selector { get; } + + /// + /// Gets the entity type of the for this range. + /// + public string EntityType => Udi.EntityType; + + public static bool operator ==(UdiRange? range1, UdiRange? range2) + { + if (ReferenceEquals(range1, range2)) + { + return true; + } + + if (range1 is null || range2 is null) + { + return false; + } + + return range1.Equals(range2); + } + + public static bool operator !=(UdiRange range1, UdiRange range2) => !(range1 == range2); + + public static UdiRange Parse(string s) + { + if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false + || Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false) + { + // if (tryParse) return false; + throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s)); + } + + Uri udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri; + return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart(Constants.CharArrays.QuestionMark)); + } + + public override string ToString() => _uriValue.ToString(); + + public override bool Equals(object? obj) => + obj is UdiRange other && GetType() == other.GetType() && _uriValue == other._uriValue; + + public override int GetHashCode() => _uriValue.GetHashCode(); } diff --git a/src/Umbraco.Core/UdiType.cs b/src/Umbraco.Core/UdiType.cs index 572c36de95..e5ebd2f7ce 100644 --- a/src/Umbraco.Core/UdiType.cs +++ b/src/Umbraco.Core/UdiType.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines Udi types. +/// +public enum UdiType { - /// - /// Defines Udi types. - /// - public enum UdiType - { - Unknown, - GuidUdi, - StringUdi - } + Unknown, + GuidUdi, + StringUdi, } diff --git a/src/Umbraco.Core/UdiTypeConverter.cs b/src/Umbraco.Core/UdiTypeConverter.cs index c443b1817b..2a52a1e093 100644 --- a/src/Umbraco.Core/UdiTypeConverter.cs +++ b/src/Umbraco.Core/UdiTypeConverter.cs @@ -1,37 +1,36 @@ -using System; using System.ComponentModel; using System.Globalization; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// A custom type converter for UDI +/// +/// +/// Primarily this is used so that WebApi can auto-bind a string parameter to a UDI instance +/// +internal class UdiTypeConverter : TypeConverter { - /// - /// A custom type converter for UDI - /// - /// - /// Primarily this is used so that WebApi can auto-bind a string parameter to a UDI instance - /// - internal class UdiTypeConverter : TypeConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + if (sourceType == typeof(string)) { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); + return true; } - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string) { - if (value is string) + if (UdiParser.TryParse((string)value, out Udi? udi)) { - Udi? udi; - if (UdiParser.TryParse((string)value, out udi)) - { - return udi; - } + return udi; } - return base.ConvertFrom(context, culture, value); } + + return base.ConvertFrom(context, culture, value); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8bcead30bb..01183739d5 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -16,10 +16,10 @@ - + - + diff --git a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs index 66ad608881..afd6183b54 100644 --- a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs +++ b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public class UmbracoApiControllerTypeCollection : BuilderCollectionBase { - public class UmbracoApiControllerTypeCollection : BuilderCollectionBase + public UmbracoApiControllerTypeCollection(Func> items) + : base(items) { - public UmbracoApiControllerTypeCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/UmbracoContextReference.cs b/src/Umbraco.Core/UmbracoContextReference.cs index 89959c3b32..d17012e0f9 100644 --- a/src/Umbraco.Core/UmbracoContextReference.cs +++ b/src/Umbraco.Core/UmbracoContextReference.cs @@ -1,61 +1,61 @@ -using System; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a reference to an instance. +/// +/// +/// +/// A reference points to an and it may own it (when it +/// is a root reference) or just reference it. A reference must be disposed after it has +/// been used. Disposing does nothing if the reference is not a root reference. Otherwise, +/// it disposes the and clears the +/// . +/// +/// +public class UmbracoContextReference : IDisposable { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private bool _disposedValue; + /// - /// Represents a reference to an instance. + /// Initializes a new instance of the class. /// - /// - /// A reference points to an and it may own it (when it - /// is a root reference) or just reference it. A reference must be disposed after it has - /// been used. Disposing does nothing if the reference is not a root reference. Otherwise, - /// it disposes the and clears the - /// . - /// - public class UmbracoContextReference : IDisposable + public UmbracoContextReference(IUmbracoContext umbracoContext, bool isRoot, IUmbracoContextAccessor umbracoContextAccessor) { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private bool _disposedValue; + IsRoot = isRoot; - /// - /// Initializes a new instance of the class. - /// - public UmbracoContextReference(IUmbracoContext umbracoContext, bool isRoot, IUmbracoContextAccessor umbracoContextAccessor) + UmbracoContext = umbracoContext; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Gets the . + /// + public IUmbracoContext UmbracoContext { get; } + + /// + /// Gets a value indicating whether the reference is a root reference. + /// + public bool IsRoot { get; } + + public void Dispose() => Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - IsRoot = isRoot; - - UmbracoContext = umbracoContext; - _umbracoContextAccessor = umbracoContextAccessor; - } - - /// - /// Gets the . - /// - public IUmbracoContext UmbracoContext { get; } - - /// - /// Gets a value indicating whether the reference is a root reference. - /// - public bool IsRoot { get; } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) + if (disposing) { - if (disposing) + if (IsRoot) { - if (IsRoot) - { - UmbracoContext.Dispose(); - _umbracoContextAccessor.Clear(); - } + UmbracoContext.Dispose(); + _umbracoContextAccessor.Clear(); } - - _disposedValue = true; } - } - public void Dispose() => Dispose(disposing: true); + _disposedValue = true; + } } } diff --git a/src/Umbraco.Core/UnknownTypeUdi.cs b/src/Umbraco.Core/UnknownTypeUdi.cs index 4131eae053..3c38418f0e 100644 --- a/src/Umbraco.Core/UnknownTypeUdi.cs +++ b/src/Umbraco.Core/UnknownTypeUdi.cs @@ -1,16 +1,13 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public class UnknownTypeUdi : Udi { - public class UnknownTypeUdi : Udi + public static readonly UnknownTypeUdi Instance = new(); + + private UnknownTypeUdi() + : base("unknown", "umb://unknown/") { - private UnknownTypeUdi() - : base("unknown", "umb://unknown/") - { } - - public static readonly UnknownTypeUdi Instance = new UnknownTypeUdi(); - - public override bool IsRoot - { - get { return false; } - } } + + public override bool IsRoot => false; } diff --git a/src/Umbraco.Core/UpgradeResult.cs b/src/Umbraco.Core/UpgradeResult.cs index 25431a5983..7f27e503fe 100644 --- a/src/Umbraco.Core/UpgradeResult.cs +++ b/src/Umbraco.Core/UpgradeResult.cs @@ -1,16 +1,17 @@ -namespace Umbraco.Cms.Core -{ - public class UpgradeResult - { - public string UpgradeType { get; } - public string Comment { get; } - public string UpgradeUrl { get; } +namespace Umbraco.Cms.Core; - public UpgradeResult(string upgradeType, string comment, string upgradeUrl) - { - UpgradeType = upgradeType; - Comment = comment; - UpgradeUrl = upgradeUrl; - } +public class UpgradeResult +{ + public UpgradeResult(string upgradeType, string comment, string upgradeUrl) + { + UpgradeType = upgradeType; + Comment = comment; + UpgradeUrl = upgradeUrl; } + + public string UpgradeType { get; } + + public string Comment { get; } + + public string UpgradeUrl { get; } } diff --git a/src/Umbraco.Core/UriUtilityCore.cs b/src/Umbraco.Core/UriUtilityCore.cs index 68b6234a0f..3599a7c16a 100644 --- a/src/Umbraco.Core/UriUtilityCore.cs +++ b/src/Umbraco.Core/UriUtilityCore.cs @@ -1,59 +1,51 @@ -using System; -using Umbraco.Extensions; +using Umbraco.Extensions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class UriUtilityCore { - public static class UriUtilityCore + #region Uri string utilities + + public static bool HasScheme(string uri) => uri.IndexOf("://") > 0; + + public static string StartWithScheme(string uri) => StartWithScheme(uri, null); + + public static string StartWithScheme(string uri, string? scheme) => + HasScheme(uri) ? uri : string.Format("{0}://{1}", scheme ?? Uri.UriSchemeHttp, uri); + + public static string EndPathWithSlash(string uri) { + var pos1 = Math.Max(0, uri.IndexOf('?')); + var pos2 = Math.Max(0, uri.IndexOf('#')); + var pos = Math.Min(pos1, pos2); - #region Uri string utilities + var path = pos > 0 ? uri.Substring(0, pos) : uri; + path = path.EnsureEndsWith('/'); - public static bool HasScheme(string uri) + if (pos > 0) { - return uri.IndexOf("://") > 0; + path += uri.Substring(pos); } - public static string StartWithScheme(string uri) - { - return StartWithScheme(uri, null); - } - - public static string StartWithScheme(string uri, string? scheme) - { - return HasScheme(uri) ? uri : String.Format("{0}://{1}", scheme ?? Uri.UriSchemeHttp, uri); - } - - public static string EndPathWithSlash(string uri) - { - var pos1 = Math.Max(0, uri.IndexOf('?')); - var pos2 = Math.Max(0, uri.IndexOf('#')); - var pos = Math.Min(pos1, pos2); - - var path = pos > 0 ? uri.Substring(0, pos) : uri; - path = path.EnsureEndsWith('/'); - - if (pos > 0) - path += uri.Substring(pos); - - return path; - } - - public static string TrimPathEndSlash(string uri) - { - var pos1 = Math.Max(0, uri.IndexOf('?')); - var pos2 = Math.Max(0, uri.IndexOf('#')); - var pos = Math.Min(pos1, pos2); - - var path = pos > 0 ? uri.Substring(0, pos) : uri; - path = path.TrimEnd(Constants.CharArrays.ForwardSlash); - - if (pos > 0) - path += uri.Substring(pos); - - return path; - } - - #endregion - + return path; } + + public static string TrimPathEndSlash(string uri) + { + var pos1 = Math.Max(0, uri.IndexOf('?')); + var pos2 = Math.Max(0, uri.IndexOf('#')); + var pos = Math.Min(pos1, pos2); + + var path = pos > 0 ? uri[..pos] : uri; + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); + + if (pos > 0) + { + path += uri.Substring(pos); + } + + return path; + } + + #endregion } diff --git a/src/Umbraco.Core/Web/CookieManagerExtensions.cs b/src/Umbraco.Core/Web/CookieManagerExtensions.cs index 75014000bb..2e399ac8c1 100644 --- a/src/Umbraco.Core/Web/CookieManagerExtensions.cs +++ b/src/Umbraco.Core/Web/CookieManagerExtensions.cs @@ -1,18 +1,13 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core; using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class CookieManagerExtensions { - public static class CookieManagerExtensions - { - public static string? GetPreviewCookieValue(this ICookieManager cookieManager) - { - return cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); - } - - } - + public static string? GetPreviewCookieValue(this ICookieManager cookieManager) => + cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); } diff --git a/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs index 94710429f0..509a746b30 100644 --- a/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs +++ b/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs @@ -1,39 +1,39 @@ using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +/// +/// Implements a hybrid . +/// +public class HybridUmbracoContextAccessor : HybridAccessorBase, IUmbracoContextAccessor { /// - /// Implements a hybrid . + /// Initializes a new instance of the class. /// - public class HybridUmbracoContextAccessor : HybridAccessorBase, IUmbracoContextAccessor + public HybridUmbracoContextAccessor(IRequestCache requestCache) + : base(requestCache) { - /// - /// Initializes a new instance of the class. - /// - public HybridUmbracoContextAccessor(IRequestCache requestCache) - : base(requestCache) - { } - - /// - /// Tries to get the object. - /// - public bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext) - { - umbracoContext = Value; - - return umbracoContext is not null; - } - - /// - /// Clears the current object. - /// - public void Clear() => Value = null; - - /// - /// Sets the object. - /// - /// - public void Set(IUmbracoContext umbracoContext) => Value = umbracoContext; } + + /// + /// Tries to get the object. + /// + public bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext) + { + umbracoContext = Value; + + return umbracoContext is not null; + } + + /// + /// Clears the current object. + /// + public void Clear() => Value = null; + + /// + /// Sets the object. + /// + /// + public void Set(IUmbracoContext umbracoContext) => Value = umbracoContext; } diff --git a/src/Umbraco.Core/Web/ICookieManager.cs b/src/Umbraco.Core/Web/ICookieManager.cs index 730b78a705..2815675f5d 100644 --- a/src/Umbraco.Core/Web/ICookieManager.cs +++ b/src/Umbraco.Core/Web/ICookieManager.cs @@ -1,12 +1,12 @@ -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +public interface ICookieManager { + void ExpireCookie(string cookieName); - public interface ICookieManager - { - void ExpireCookie(string cookieName); - string? GetCookieValue(string cookieName); - void SetCookieValue(string cookieName, string value); - bool HasCookie(string cookieName); - } + string? GetCookieValue(string cookieName); + void SetCookieValue(string cookieName, string value); + + bool HasCookie(string cookieName); } diff --git a/src/Umbraco.Core/Web/IRequestAccessor.cs b/src/Umbraco.Core/Web/IRequestAccessor.cs index 9fb4e99d5c..a72ec5bc72 100644 --- a/src/Umbraco.Core/Web/IRequestAccessor.cs +++ b/src/Umbraco.Core/Web/IRequestAccessor.cs @@ -1,22 +1,19 @@ -using System; +namespace Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Web +public interface IRequestAccessor { - public interface IRequestAccessor - { - /// - /// Returns the request/form/querystring value for the given name - /// - string GetRequestValue(string name); + /// + /// Returns the request/form/querystring value for the given name + /// + string GetRequestValue(string name); - /// - /// Returns the query string value for the given name - /// - string GetQueryStringValue(string name); + /// + /// Returns the query string value for the given name + /// + string GetQueryStringValue(string name); - /// - /// Returns the current request uri - /// - Uri? GetRequestUrl(); - } + /// + /// Returns the current request uri + /// + Uri? GetRequestUrl(); } diff --git a/src/Umbraco.Core/Web/ISessionManager.cs b/src/Umbraco.Core/Web/ISessionManager.cs index 3ba691e222..a37bebcfa7 100644 --- a/src/Umbraco.Core/Web/ISessionManager.cs +++ b/src/Umbraco.Core/Web/ISessionManager.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +public interface ISessionManager { - public interface ISessionManager - { - string? GetSessionValue(string key); + string? GetSessionValue(string key); - void SetSessionValue(string key, string value); + void SetSessionValue(string key, string value); - void ClearSessionValue(string key); - } + void ClearSessionValue(string key); } diff --git a/src/Umbraco.Core/Web/IUmbracoContext.cs b/src/Umbraco.Core/Web/IUmbracoContext.cs index 7cfa3876c0..17ffc515a2 100644 --- a/src/Umbraco.Core/Web/IUmbracoContext.cs +++ b/src/Umbraco.Core/Web/IUmbracoContext.cs @@ -1,74 +1,72 @@ -using System; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +public interface IUmbracoContext : IDisposable { - public interface IUmbracoContext : IDisposable - { - /// - /// Gets the DateTime this instance was created. - /// - /// - /// Used internally for performance calculations, the ObjectCreated DateTime is set as soon as this - /// object is instantiated which in the web site is created during the BeginRequest phase. - /// We can then determine complete rendering time from that. - /// - DateTime ObjectCreated { get; } + /// + /// Gets the DateTime this instance was created. + /// + /// + /// Used internally for performance calculations, the ObjectCreated DateTime is set as soon as this + /// object is instantiated which in the web site is created during the BeginRequest phase. + /// We can then determine complete rendering time from that. + /// + DateTime ObjectCreated { get; } - /// - /// Gets the uri that is handled by ASP.NET after server-side rewriting took place. - /// - Uri OriginalRequestUrl { get; } + /// + /// Gets the uri that is handled by ASP.NET after server-side rewriting took place. + /// + Uri OriginalRequestUrl { get; } - /// - /// Gets the cleaned up url that is handled by Umbraco. - /// - /// That is, lowercase, no trailing slash after path, no .aspx... - Uri CleanedUmbracoUrl { get; } + /// + /// Gets the cleaned up url that is handled by Umbraco. + /// + /// That is, lowercase, no trailing slash after path, no .aspx... + Uri CleanedUmbracoUrl { get; } - /// - /// Gets the published snapshot. - /// - IPublishedSnapshot PublishedSnapshot { get; } + /// + /// Gets the published snapshot. + /// + IPublishedSnapshot PublishedSnapshot { get; } - /// - /// Gets the published content cache. - /// - IPublishedContentCache? Content { get; } + /// + /// Gets the published content cache. + /// + IPublishedContentCache? Content { get; } - /// - /// Gets the published media cache. - /// - IPublishedMediaCache? Media { get; } + /// + /// Gets the published media cache. + /// + IPublishedMediaCache? Media { get; } - /// - /// Gets the domains cache. - /// - IDomainCache? Domains { get; } + /// + /// Gets the domains cache. + /// + IDomainCache? Domains { get; } - /// - /// Gets or sets the PublishedRequest object - /// - //// TODO: Can we refactor this so it's not a settable thing required for routing? - //// The only nicer way would be to have a RouteRequest method directly on IUmbracoContext but that means adding another dep to the ctx/factory. - IPublishedRequest? PublishedRequest { get; set; } + /// + /// Gets or sets the PublishedRequest object + /// + //// TODO: Can we refactor this so it's not a settable thing required for routing? + //// The only nicer way would be to have a RouteRequest method directly on IUmbracoContext but that means adding another dep to the ctx/factory. + IPublishedRequest? PublishedRequest { get; set; } - /// - /// Gets a value indicating whether the request has debugging enabled - /// - /// true if this instance is debug; otherwise, false. - bool IsDebug { get; } + /// + /// Gets a value indicating whether the request has debugging enabled + /// + /// true if this instance is debug; otherwise, false. + bool IsDebug { get; } - /// - /// Gets a value indicating whether the current user is in a preview mode and browsing the site (ie. not in the admin UI) - /// - bool InPreviewMode { get; } + /// + /// Gets a value indicating whether the current user is in a preview mode and browsing the site (ie. not in the admin UI) + /// + bool InPreviewMode { get; } - /// - /// Forces the context into preview - /// - /// A instance to be disposed to exit the preview context - IDisposable ForcedPreview(bool preview); - } + /// + /// Forces the context into preview + /// + /// A instance to be disposed to exit the preview context + IDisposable ForcedPreview(bool preview); } diff --git a/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs index d8e6793f89..370412b281 100644 --- a/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs +++ b/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs @@ -1,16 +1,17 @@ using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +/// +/// Provides access to a TryGetUmbracoContext bool method that will return true if the "current" is not null. +/// Provides a Clear() method that will clear the current object. +/// Provides a Set() method that til set the current object. +/// +public interface IUmbracoContextAccessor { - /// - /// Provides access to a TryGetUmbracoContext bool method that will return true if the "current" is not null. - /// Provides a Clear() method that will clear the current object. - /// Provides a Set() method that til set the current object. - /// - public interface IUmbracoContextAccessor - { - bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext); - void Clear(); - void Set(IUmbracoContext umbracoContext); - } + bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext); + + void Clear(); + + void Set(IUmbracoContext umbracoContext); } diff --git a/src/Umbraco.Core/Web/IUmbracoContextFactory.cs b/src/Umbraco.Core/Web/IUmbracoContextFactory.cs index 68ebcf8b2b..d8d5475841 100644 --- a/src/Umbraco.Core/Web/IUmbracoContextFactory.cs +++ b/src/Umbraco.Core/Web/IUmbracoContextFactory.cs @@ -1,30 +1,27 @@ - -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +/// +/// Creates and manages instances. +/// +public interface IUmbracoContextFactory { /// - /// Creates and manages instances. + /// Ensures that a current exists. /// - public interface IUmbracoContextFactory - { - /// - /// Ensures that a current exists. - /// - /// - /// If an is already registered in the - /// , returns a non-root reference to it. - /// Otherwise, create a new instance, registers it, and return a root reference - /// to it. - /// If is null, the factory tries to use - /// if it exists. Otherwise, it uses a dummy - /// . - /// - /// - /// using (var contextReference = contextFactory.EnsureUmbracoContext()) - /// { - /// var umbracoContext = contextReference.UmbracoContext; - /// // use umbracoContext... - /// } - /// - UmbracoContextReference EnsureUmbracoContext(); - } + /// + /// + /// If an is already registered in the + /// , returns a non-root reference to it. + /// Otherwise, create a new instance, registers it, and return a root reference + /// to it. + /// + /// + /// + /// using (var contextReference = contextFactory.EnsureUmbracoContext()) + /// { + /// var umbracoContext = contextReference.UmbracoContext; + /// // use umbracoContext... + /// } + /// + UmbracoContextReference EnsureUmbracoContext(); } diff --git a/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs b/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs index efc162a9a3..5f484c8fe0 100644 --- a/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs +++ b/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs @@ -1,21 +1,21 @@ -using System; +namespace Umbraco.Cms.Core.Web.Mvc; -namespace Umbraco.Cms.Core.Web.Mvc +/// +/// Represents some metadata about the controller +/// +public class PluginControllerMetadata { - /// - /// Represents some metadata about the controller - /// - public class PluginControllerMetadata - { - public Type ControllerType { get; set; } = null!; - public string? ControllerName { get; set; } - public string? ControllerNamespace { get; set; } - public string? AreaName { get; set; } + public Type ControllerType { get; set; } = null!; - /// - /// This is determined by another attribute [IsBackOffice] which slightly modifies the route path - /// allowing us to determine if it is indeed a back office request or not - /// - public bool IsBackOffice { get; set; } - } + public string? ControllerName { get; set; } + + public string? ControllerNamespace { get; set; } + + public string? AreaName { get; set; } + + /// + /// This is determined by another attribute [IsBackOffice] which slightly modifies the route path + /// allowing us to determine if it is indeed a back office request or not + /// + public bool IsBackOffice { get; set; } } diff --git a/src/Umbraco.Core/WebAssets/AssetFile.cs b/src/Umbraco.Core/WebAssets/AssetFile.cs index c10a423a99..a0ad298302 100644 --- a/src/Umbraco.Core/WebAssets/AssetFile.cs +++ b/src/Umbraco.Core/WebAssets/AssetFile.cs @@ -1,23 +1,20 @@ -using System.Diagnostics; +using System.Diagnostics; -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +/// +/// Represents a dependency file +/// +[DebuggerDisplay("Type: {DependencyType}, File: {FilePath}")] +public class AssetFile : IAssetFile { - /// - /// Represents a dependency file - /// - [DebuggerDisplay("Type: {DependencyType}, File: {FilePath}")] - public class AssetFile : IAssetFile - { - #region IAssetFile Members + public AssetFile(AssetType type) => DependencyType = type; - public string? FilePath { get; set; } - public AssetType DependencyType { get; } + #region IAssetFile Members - #endregion + public string? FilePath { get; set; } - public AssetFile(AssetType type) - { - DependencyType = type; - } - } + public AssetType DependencyType { get; } + + #endregion } diff --git a/src/Umbraco.Core/WebAssets/AssetType.cs b/src/Umbraco.Core/WebAssets/AssetType.cs index f40a592588..e04caa80a2 100644 --- a/src/Umbraco.Core/WebAssets/AssetType.cs +++ b/src/Umbraco.Core/WebAssets/AssetType.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public enum AssetType { - public enum AssetType - { - Javascript, - Css - } + Javascript, + Css, } diff --git a/src/Umbraco.Core/WebAssets/BundlingOptions.cs b/src/Umbraco.Core/WebAssets/BundlingOptions.cs index 64b9e72e17..99236494f3 100644 --- a/src/Umbraco.Core/WebAssets/BundlingOptions.cs +++ b/src/Umbraco.Core/WebAssets/BundlingOptions.cs @@ -1,44 +1,46 @@ -using System; +namespace Umbraco.Cms.Core.WebAssets; -namespace Umbraco.Cms.Core.WebAssets +public struct BundlingOptions : IEquatable { - public struct BundlingOptions : IEquatable + public BundlingOptions(bool optimizeOutput = true, bool enabledCompositeFiles = true) { - public static BundlingOptions OptimizedAndComposite => new BundlingOptions(true, true); - public static BundlingOptions OptimizedNotComposite => new BundlingOptions(true, false); - public static BundlingOptions NotOptimizedNotComposite => new BundlingOptions(false, false); - public static BundlingOptions NotOptimizedAndComposite => new BundlingOptions(false, true); - - public BundlingOptions(bool optimizeOutput = true, bool enabledCompositeFiles = true) - { - OptimizeOutput = optimizeOutput; - EnabledCompositeFiles = enabledCompositeFiles; - } - - /// - /// If true, the files in the bundle will be minified - /// - public bool OptimizeOutput { get; } - - /// - /// If true, the files in the bundle will be combined, if false the files - /// will be served as individual files. - /// - public bool EnabledCompositeFiles { get; } - - public override bool Equals(object? obj) => obj is BundlingOptions options && Equals(options); - public bool Equals(BundlingOptions other) => OptimizeOutput == other.OptimizeOutput && EnabledCompositeFiles == other.EnabledCompositeFiles; - - public override int GetHashCode() - { - int hashCode = 2130304063; - hashCode = hashCode * -1521134295 + OptimizeOutput.GetHashCode(); - hashCode = hashCode * -1521134295 + EnabledCompositeFiles.GetHashCode(); - return hashCode; - } - - public static bool operator ==(BundlingOptions left, BundlingOptions right) => left.Equals(right); - - public static bool operator !=(BundlingOptions left, BundlingOptions right) => !(left == right); + OptimizeOutput = optimizeOutput; + EnabledCompositeFiles = enabledCompositeFiles; } + + public static BundlingOptions OptimizedAndComposite => new(true); + + public static BundlingOptions OptimizedNotComposite => new(true, false); + + public static BundlingOptions NotOptimizedNotComposite => new(false, false); + + public static BundlingOptions NotOptimizedAndComposite => new(false); + + /// + /// If true, the files in the bundle will be minified + /// + public bool OptimizeOutput { get; } + + /// + /// If true, the files in the bundle will be combined, if false the files + /// will be served as individual files. + /// + public bool EnabledCompositeFiles { get; } + + public static bool operator ==(BundlingOptions left, BundlingOptions right) => left.Equals(right); + + public override bool Equals(object? obj) => obj is BundlingOptions options && Equals(options); + + public bool Equals(BundlingOptions other) => OptimizeOutput == other.OptimizeOutput && + EnabledCompositeFiles == other.EnabledCompositeFiles; + + public override int GetHashCode() + { + var hashCode = 2130304063; + hashCode = (hashCode * -1521134295) + OptimizeOutput.GetHashCode(); + hashCode = (hashCode * -1521134295) + EnabledCompositeFiles.GetHashCode(); + return hashCode; + } + + public static bool operator !=(BundlingOptions left, BundlingOptions right) => !(left == right); } diff --git a/src/Umbraco.Core/WebAssets/CssFile.cs b/src/Umbraco.Core/WebAssets/CssFile.cs index 101ff22763..9ba30c83de 100644 --- a/src/Umbraco.Core/WebAssets/CssFile.cs +++ b/src/Umbraco.Core/WebAssets/CssFile.cs @@ -1,14 +1,11 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +/// +/// Represents a CSS asset file +/// +public class CssFile : AssetFile { - /// - /// Represents a CSS asset file - /// - public class CssFile : AssetFile - { - public CssFile(string filePath) - : base(AssetType.Css) - { - FilePath = filePath; - } - } + public CssFile(string filePath) + : base(AssetType.Css) => + FilePath = filePath; } diff --git a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs index 2595afe40e..523b186c9a 100644 --- a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs +++ b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public class CustomBackOfficeAssetsCollection : BuilderCollectionBase { - public class CustomBackOfficeAssetsCollection : BuilderCollectionBase + public CustomBackOfficeAssetsCollection(Func> items) + : base(items) { - public CustomBackOfficeAssetsCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs index df84bf013d..bdfebf128a 100644 --- a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs +++ b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public class CustomBackOfficeAssetsCollectionBuilder : OrderedCollectionBuilderBase { - public class CustomBackOfficeAssetsCollectionBuilder : OrderedCollectionBuilderBase - { - protected override CustomBackOfficeAssetsCollectionBuilder This => this; - } + protected override CustomBackOfficeAssetsCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/WebAssets/IAssetFile.cs b/src/Umbraco.Core/WebAssets/IAssetFile.cs index dd66afe4a7..f3e5516f45 100644 --- a/src/Umbraco.Core/WebAssets/IAssetFile.cs +++ b/src/Umbraco.Core/WebAssets/IAssetFile.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public interface IAssetFile { - public interface IAssetFile - { - string? FilePath { get; set; } - AssetType DependencyType { get; } - } + string? FilePath { get; set; } + + AssetType DependencyType { get; } } diff --git a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs index 92b9e1e423..813618738b 100644 --- a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs +++ b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs @@ -1,88 +1,83 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.WebAssets; -namespace Umbraco.Cms.Core.WebAssets +/// +/// Used for bundling and minifying web assets at runtime +/// +public interface IRuntimeMinifier { /// - /// Used for bundling and minifying web assets at runtime + /// Returns the cache buster value /// - public interface IRuntimeMinifier - { - /// - /// Returns the cache buster value - /// - string CacheBuster { get; } + string CacheBuster { get; } - /// - /// Creates a css bundle - /// - /// - /// - /// - /// All files must be absolute paths, relative paths will throw - /// - /// - /// Thrown if any of the paths specified are not absolute - /// - void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); + /// + /// Creates a css bundle + /// + /// + /// + /// + /// All files must be absolute paths, relative paths will throw + /// + /// + /// Thrown if any of the paths specified are not absolute + /// + void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); - /// - /// Renders the html link tag for the bundle - /// - /// - /// - /// An html encoded string - /// - Task RenderCssHereAsync(string bundleName); + /// + /// Renders the html link tag for the bundle + /// + /// + /// + /// An html encoded string + /// + Task RenderCssHereAsync(string bundleName); - /// - /// Creates a JS bundle - /// - /// - /// - /// - /// - /// All files must be absolute paths, relative paths will throw - /// - /// - /// Thrown if any of the paths specified are not absolute - /// - void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); + /// + /// Creates a JS bundle + /// + /// + /// + /// + /// + /// All files must be absolute paths, relative paths will throw + /// + /// + /// Thrown if any of the paths specified are not absolute + /// + void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); - /// - /// Renders the html script tag for the bundle - /// - /// - /// - /// An html encoded string - /// - Task RenderJsHereAsync(string bundleName); + /// + /// Renders the html script tag for the bundle + /// + /// + /// + /// An html encoded string + /// + Task RenderJsHereAsync(string bundleName); - /// - /// Returns the asset paths for the JS bundle name - /// - /// - /// - /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL - /// - Task> GetJsAssetPathsAsync(string bundleName); + /// + /// Returns the asset paths for the JS bundle name + /// + /// + /// + /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL + /// + Task> GetJsAssetPathsAsync(string bundleName); - /// - /// Returns the asset paths for the css bundle name - /// - /// - /// - /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL - /// - Task> GetCssAssetPathsAsync(string bundleName); + /// + /// Returns the asset paths for the css bundle name + /// + /// + /// + /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL + /// + Task> GetCssAssetPathsAsync(string bundleName); - /// - /// Minify the file content, of a given type - /// - /// - /// - /// - Task MinifyAsync(string? fileContent, AssetType assetType); - } + /// + /// Minify the file content, of a given type + /// + /// + /// + /// + Task MinifyAsync(string? fileContent, AssetType assetType); } diff --git a/src/Umbraco.Core/WebAssets/JavascriptFile.cs b/src/Umbraco.Core/WebAssets/JavascriptFile.cs index 2dccbf2a07..e7f4ea239f 100644 --- a/src/Umbraco.Core/WebAssets/JavascriptFile.cs +++ b/src/Umbraco.Core/WebAssets/JavascriptFile.cs @@ -1,14 +1,11 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +/// +/// Represents a JS asset file +/// +public class JavaScriptFile : AssetFile { - /// - /// Represents a JS asset file - /// - public class JavaScriptFile : AssetFile - { - public JavaScriptFile(string filePath) - : base(AssetType.Javascript) - { - FilePath = filePath; - } - } + public JavaScriptFile(string filePath) + : base(AssetType.Javascript) => + FilePath = filePath; } diff --git a/src/Umbraco.Core/Xml/DynamicContext.cs b/src/Umbraco.Core/Xml/DynamicContext.cs index 7547b7cc31..fd86866348 100644 --- a/src/Umbraco.Core/Xml/DynamicContext.cs +++ b/src/Umbraco.Core/Xml/DynamicContext.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Xml; +using System.Xml; using System.Xml.XPath; using System.Xml.Xsl; @@ -66,16 +64,25 @@ namespace Umbraco.Cms.Core.Xml object xml = table.Add(XmlNamespaces.Xml); object xmlns = table.Add(XmlNamespaces.XmlNs); - if (context == null) return; + if (context == null) + { + return; + } foreach (string prefix in context) { var uri = context.LookupNamespace(prefix); // Use fast object reference comparison to omit forbidden namespace declarations. if (Equals(uri, xml) || Equals(uri, xmlns)) + { continue; + } + if (uri == null) + { continue; + } + base.AddNamespace(prefix, uri); } } @@ -87,10 +94,8 @@ namespace Umbraco.Cms.Core.Xml /// /// Implementation equal to . /// - public override int CompareDocument(string baseUri, string nextbaseUri) - { - return String.Compare(baseUri, nextbaseUri, false, System.Globalization.CultureInfo.InvariantCulture); - } + public override int CompareDocument(string baseUri, string nextbaseUri) => + String.Compare(baseUri, nextbaseUri, false, System.Globalization.CultureInfo.InvariantCulture); /// /// Same as . @@ -187,7 +192,11 @@ namespace Umbraco.Cms.Core.Xml /// The is null. public void AddVariable(string name, object value) { - if (value == null) throw new ArgumentNullException("value"); + if (value == null) + { + throw new ArgumentNullException("value"); + } + _variables[name] = new DynamicVariable(name, value); } @@ -203,7 +212,7 @@ namespace Umbraco.Cms.Core.Xml { IXsltContextVariable var; _variables.TryGetValue(name, out var!); - return var!; + return var; } #endregion Variable Handling Code @@ -215,8 +224,8 @@ namespace Umbraco.Cms.Core.Xml /// internal class DynamicVariable : IXsltContextVariable { - readonly string _name; - readonly object _value; + private readonly string _name; + private readonly object _value; #region Public Members @@ -234,13 +243,21 @@ namespace Umbraco.Cms.Core.Xml _value = value; if (value is string) + { _type = XPathResultType.String; + } else if (value is bool) + { _type = XPathResultType.Boolean; + } else if (value is XPathNavigator) + { _type = XPathResultType.Navigator; + } else if (value is XPathNodeIterator) + { _type = XPathResultType.NodeSet; + } else { // Try to convert to double (native XPath numeric type) @@ -284,7 +301,7 @@ namespace Umbraco.Cms.Core.Xml get { return _type; } } - readonly XPathResultType _type; + private readonly XPathResultType _type; object IXsltContextVariable.Evaluate(XsltContext context) { diff --git a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs index 4285f9c97f..bb5c186ca6 100644 --- a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs +++ b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs @@ -1,122 +1,134 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// This is used to parse our customize Umbraco XPath expressions (i.e. that include special tokens like $site) into +/// a real XPath statement +/// +public class UmbracoXPathPathSyntaxParser { /// - /// This is used to parse our customize Umbraco XPath expressions (i.e. that include special tokens like $site) into - /// a real XPath statement + /// Parses custom umbraco xpath expression /// - public class UmbracoXPathPathSyntaxParser + /// The Xpath expression + /// + /// The current node id context of executing the query - null if there is no current node, in which case + /// some of the parameters like $current, $parent, $site will be disabled + /// + /// The callback to create the nodeId path, given a node Id + /// The callback to return whether a published node exists based on Id + /// + public static string ParseXPathQuery( + string xpathExpression, + int? nodeContextId, + Func?> getPath, + Func publishedContentExists) { - /// - /// Parses custom umbraco xpath expression - /// - /// The Xpath expression - /// - /// The current node id context of executing the query - null if there is no current node, in which case - /// some of the parameters like $current, $parent, $site will be disabled - /// - /// The callback to create the nodeId path, given a node Id - /// The callback to return whether a published node exists based on Id - /// - public static string ParseXPathQuery( - string xpathExpression, - int? nodeContextId, - Func?> getPath, - Func publishedContentExists) + // TODO: This should probably support some of the old syntax and token replacements, currently + // it does not, there is a ticket raised here about it: http://issues.umbraco.org/issue/U4-6364 + // previous tokens were: "$currentPage", "$ancestorOrSelf", "$parentPage" and I believe they were + // allowed 'inline', not just at the beginning... whether or not we want to support that is up + // for discussion. + if (xpathExpression == null) { + throw new ArgumentNullException(nameof(xpathExpression)); + } - // TODO: This should probably support some of the old syntax and token replacements, currently - // it does not, there is a ticket raised here about it: http://issues.umbraco.org/issue/U4-6364 - // previous tokens were: "$currentPage", "$ancestorOrSelf", "$parentPage" and I believe they were - // allowed 'inline', not just at the beginning... whether or not we want to support that is up - // for discussion. + if (string.IsNullOrWhiteSpace(xpathExpression)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(xpathExpression)); + } - if (xpathExpression == null) throw new ArgumentNullException(nameof(xpathExpression)); - if (string.IsNullOrWhiteSpace(xpathExpression)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(xpathExpression)); - if (getPath == null) throw new ArgumentNullException(nameof(getPath)); - if (publishedContentExists == null) throw new ArgumentNullException(nameof(publishedContentExists)); + if (getPath == null) + { + throw new ArgumentNullException(nameof(getPath)); + } - //no need to parse it - if (xpathExpression.StartsWith("$") == false) - return xpathExpression; - - //get nearest published item - Func?, int> getClosestPublishedAncestor = path => - { - if (path is not null) - { - foreach (var i in path) - { - int idAsInt; - if (int.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out idAsInt)) - { - var exists = publishedContentExists(int.Parse(i, CultureInfo.InvariantCulture)); - if (exists) - return idAsInt; - } - } - } - - return -1; - }; - - const string rootXpath = "id({0})"; - - //parseable items: - var vars = new Dictionary>(); - - //These parameters must have a node id context - if (nodeContextId.HasValue) - { - vars.Add("$current", q => - { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); - return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); - }); - - vars.Add("$parent", q => - { - //remove the first item in the array if its the current node - //this happens when current is published, but we are looking for its parent specifically - var path = getPath(nodeContextId.Value)?.ToArray(); - if (path?[0] == nodeContextId.ToString()) - { - path = path?.Skip(1).ToArray(); - } - - var closestPublishedAncestorId = getClosestPublishedAncestor(path); - return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId)); - }); - - - vars.Add("$site", q => - { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); - return q.Replace("$site", string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]"); - }); - } - - // TODO: This used to just replace $root with string.Empty BUT, that would never work - // the root is always "/root . Need to confirm with Per why this was string.Empty before! - vars.Add("$root", q => q.Replace("$root", "/root")); - - - foreach (var varible in vars) - { - if (xpathExpression.StartsWith(varible.Key)) - { - xpathExpression = varible.Value(xpathExpression); - break; - } - } + if (publishedContentExists == null) + { + throw new ArgumentNullException(nameof(publishedContentExists)); + } + // no need to parse it + if (xpathExpression.StartsWith("$") == false) + { return xpathExpression; } + // get nearest published item + Func?, int> getClosestPublishedAncestor = path => + { + if (path is not null) + { + foreach (var i in path) + { + if (int.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out int idAsInt)) + { + var exists = publishedContentExists(int.Parse(i, CultureInfo.InvariantCulture)); + if (exists) + { + return idAsInt; + } + } + } + } + + return -1; + }; + + const string rootXpath = "id({0})"; + + // parseable items: + var vars = new Dictionary>(); + + // These parameters must have a node id context + if (nodeContextId.HasValue) + { + vars.Add("$current", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); + }); + + vars.Add("$parent", q => + { + // remove the first item in the array if its the current node + // this happens when current is published, but we are looking for its parent specifically + var path = getPath(nodeContextId.Value)?.ToArray(); + if (path?[0] == nodeContextId.ToString()) + { + path = path?.Skip(1).ToArray(); + } + + var closestPublishedAncestorId = getClosestPublishedAncestor(path); + return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId)); + }); + + vars.Add("$site", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace( + "$site", + string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]"); + }); + } + + // TODO: This used to just replace $root with string.Empty BUT, that would never work + // the root is always "/root . Need to confirm with Per why this was string.Empty before! + vars.Add("$root", q => q.Replace("$root", "/root")); + + foreach (KeyValuePair> varible in vars) + { + if (xpathExpression.StartsWith(varible.Key)) + { + xpathExpression = varible.Value(xpathExpression); + break; + } + } + + return xpathExpression; } } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs index c1a4e6c3e4..b9359b4fef 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs @@ -1,59 +1,62 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +/// +/// Represents a content that can be navigated via XPath. +/// +public interface INavigableContent { /// - /// Represents a content that can be navigated via XPath. + /// Gets the unique identifier of the navigable content. /// - public interface INavigableContent - { - /// - /// Gets the unique identifier of the navigable content. - /// - /// The root node identifier should be -1. - int Id { get; } + /// The root node identifier should be -1. + int Id { get; } - /// - /// Gets the unique identifier of parent of the navigable content. - /// - /// The top-level content parent identifiers should be -1 ie the identifier - /// of the root node, whose parent identifier should in turn be -1. - int ParentId { get; } + /// + /// Gets the unique identifier of parent of the navigable content. + /// + /// + /// The top-level content parent identifiers should be -1 ie the identifier + /// of the root node, whose parent identifier should in turn be -1. + /// + int ParentId { get; } - /// - /// Gets the type of the navigable content. - /// - INavigableContentType Type { get; } + /// + /// Gets the type of the navigable content. + /// + INavigableContentType Type { get; } - /// - /// Gets the unique identifiers of the children of the navigable content. - /// - IList? ChildIds { get; } + /// + /// Gets the unique identifiers of the children of the navigable content. + /// + IList? ChildIds { get; } - /// - /// Gets the value of a field of the navigable content for XPath navigation use. - /// - /// The field index. - /// The value of the field for XPath navigation use. - /// - /// Fields are attributes or elements depending on their relative index value compared - /// to source.LastAttributeIndex. - /// For attributes, the value must be a string. - /// For elements, the value should an XPathNavigator instance if the field is xml - /// and has content (is not empty), null to indicate that the element is empty, or a string - /// which can be empty, whitespace... depending on what the data type wants to expose. - /// - object? Value(int index); + /// + /// Gets the value of a field of the navigable content for XPath navigation use. + /// + /// The field index. + /// The value of the field for XPath navigation use. + /// + /// + /// Fields are attributes or elements depending on their relative index value compared + /// to source.LastAttributeIndex. + /// + /// For attributes, the value must be a string. + /// + /// For elements, the value should an XPathNavigator instance if the field is xml + /// and has content (is not empty), null to indicate that the element is empty, or a string + /// which can be empty, whitespace... depending on what the data type wants to expose. + /// + /// + object? Value(int index); - // TODO: implement the following one + // TODO: implement the following one - ///// - ///// Gets the value of a field of the navigable content, for a specified language. - ///// - ///// The field index. - ///// The language key. - ///// The value of the field for the specified language. - ///// ... - //object Value(int index, string languageKey); - } + ///// + ///// Gets the value of a field of the navigable content, for a specified language. + ///// + ///// The field index. + ///// The language key. + ///// The value of the field for the specified language. + ///// ... + // object Value(int index, string languageKey); } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs index 2e214d5e9a..08a7c1a0f6 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +/// +/// Represents the type of a content that can be navigated via XPath. +/// +public interface INavigableContentType { /// - /// Represents the type of a content that can be navigated via XPath. + /// Gets the name of the content type. /// - public interface INavigableContentType - { - /// - /// Gets the name of the content type. - /// - string? Name { get; } + string? Name { get; } - /// - /// Gets the field types of the content type. - /// - /// This includes the attributes and the properties. - INavigableFieldType[] FieldTypes { get; } - } + /// + /// Gets the field types of the content type. + /// + /// This includes the attributes and the properties. + INavigableFieldType[] FieldTypes { get; } } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs index 0b66cc0626..28fa46e84b 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs @@ -1,23 +1,22 @@ -using System; +namespace Umbraco.Cms.Core.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +/// +/// Represents the type of a field of a content that can be navigated via XPath. +/// +/// A field can be an attribute or a property. +public interface INavigableFieldType { /// - /// Represents the type of a field of a content that can be navigated via XPath. + /// Gets the name of the field type. /// - /// A field can be an attribute or a property. - public interface INavigableFieldType - { - /// - /// Gets the name of the field type. - /// - string Name { get; } + string Name { get; } - /// - /// Gets a method to convert the field value to a string. - /// - /// This is for built-in properties, ie attributes. User-defined properties have their - /// own way to convert their value for XPath. - Func? XmlStringConverter { get; } - } + /// + /// Gets a method to convert the field value to a string. + /// + /// + /// This is for built-in properties, ie attributes. User-defined properties have their + /// own way to convert their value for XPath. + /// + Func? XmlStringConverter { get; } } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableSource.cs b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs index 76b43b618c..1f8500725b 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableSource.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs @@ -1,29 +1,30 @@ -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +/// +/// Represents a source of content that can be navigated via XPath. +/// +public interface INavigableSource { /// - /// Represents a source of content that can be navigated via XPath. + /// Gets the index of the last attribute in the fields collections. /// - public interface INavigableSource - { - /// - /// Gets a content identified by its unique identifier. - /// - /// The unique identifier. - /// The content identified by the unique identifier, or null. - /// When id is -1 (root content) implementations should return null. - INavigableContent? Get(int id); + int LastAttributeIndex { get; } - /// - /// Gets the index of the last attribute in the fields collections. - /// - int LastAttributeIndex { get; } + /// + /// Gets the content at the root of the source. + /// + /// + /// That content should have unique identifier -1 and should not be gettable, + /// ie Get(-1) should return null. Its ParentId should be -1. It should provide + /// values for the attribute fields. + /// + INavigableContent Root { get; } - /// - /// Gets the content at the root of the source. - /// - /// That content should have unique identifier -1 and should not be gettable, - /// ie Get(-1) should return null. Its ParentId should be -1. It should provide - /// values for the attribute fields. - INavigableContent Root { get; } - } + /// + /// Gets a content identified by its unique identifier. + /// + /// The unique identifier. + /// The content identified by the unique identifier, or null. + /// When id is -1 (root content) implementations should return null. + INavigableContent? Get(int id); } diff --git a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs index 2e2819066b..dd27e6124c 100644 --- a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Diagnostics; using System.Xml; using System.Xml.XPath; @@ -66,10 +63,10 @@ namespace Umbraco.Cms.Core.Xml.XPath #endif [Conditional("DEBUG")] - void DebugEnter(string name) + private void DebugEnter(string name) { #if DEBUG - Debug(""); + Debug(string.Empty); DebugState(":"); Debug(name); _tabs = Math.Min(Tabs.Length, _tabs + 2); @@ -77,7 +74,7 @@ namespace Umbraco.Cms.Core.Xml.XPath } [Conditional("DEBUG")] - void DebugCreate(MacroNavigator nav) + private void DebugCreate(MacroNavigator nav) { #if DEBUG Debug("Create: [MacroNavigator::{0}]", nav._uid); @@ -103,16 +100,19 @@ namespace Umbraco.Cms.Core.Xml.XPath } [Conditional("DEBUG")] - void DebugReturn(string format, params object[] args) + private void DebugReturn(string format, params object[] args) { #if DEBUG Debug("=> " + format, args); - if (_tabs > 0) _tabs -= 2; + if (_tabs > 0) + { + _tabs -= 2; + } #endif } [Conditional("DEBUG")] - void DebugState(string s = " =>") + private void DebugState(string s = " =>") { #if DEBUG string position; @@ -123,25 +123,28 @@ namespace Umbraco.Cms.Core.Xml.XPath position = "At macro."; break; case StatePosition.Parameter: - position = string.Format("At parameter '{0}'.", - _macro.Parameters[_state.ParameterIndex].Name); + position = string.Format("At parameter '{0}'.", _macro.Parameters[_state.ParameterIndex].Name); break; case StatePosition.ParameterAttribute: - position = string.Format("At parameter attribute '{0}/{1}'.", + position = string.Format( + "At parameter attribute '{0}/{1}'.", _macro.Parameters[_state.ParameterIndex].Name, _macro.Parameters[_state.ParameterIndex].Attributes?[_state.ParameterAttributeIndex].Key); break; case StatePosition.ParameterNavigator: - position = string.Format("In parameter '{0}{1}' navigator.", + position = string.Format( + "In parameter '{0}{1}' navigator.", _macro.Parameters[_state.ParameterIndex].Name, - _macro.Parameters[_state.ParameterIndex].WrapNavigatorInNodes ? "/nodes" : ""); + _macro.Parameters[_state.ParameterIndex].WrapNavigatorInNodes ? "/nodes" : string.Empty); break; case StatePosition.ParameterNodes: - position = string.Format("At parameter '{0}/nodes'.", + position = string.Format( + "At parameter '{0}/nodes'.", _macro.Parameters[_state.ParameterIndex].Name); break; case StatePosition.ParameterText: - position = string.Format("In parameter '{0}' text.", + position = string.Format( + "In parameter '{0}' text.", _macro.Parameters[_state.ParameterIndex].Name); break; case StatePosition.Root: @@ -156,7 +159,7 @@ namespace Umbraco.Cms.Core.Xml.XPath } #if DEBUG - void Debug(string format, params object[] args) + private void Debug(string format, params object[] args) { // remove comments to write @@ -192,7 +195,9 @@ namespace Umbraco.Cms.Core.Xml.XPath StringValue = value; } - public MacroParameter(string name, XPathNavigator navigator, + public MacroParameter( + string name, + XPathNavigator navigator, int maxNavigatorDepth = int.MaxValue, bool wrapNavigatorInNodes = false, IEnumerable>? attributes = null) @@ -202,10 +207,13 @@ namespace Umbraco.Cms.Core.Xml.XPath WrapNavigatorInNodes = wrapNavigatorInNodes; if (attributes != null) { - var a = attributes.ToArray(); + KeyValuePair[] a = attributes.ToArray(); if (a.Length > 0) + { Attributes = a; + } } + NavigatorValue = navigator; // should not be empty } @@ -248,8 +256,8 @@ namespace Umbraco.Cms.Core.Xml.XPath isEmpty = _macro.Parameters.Length == 0; break; case StatePosition.Parameter: - var parameter = _macro.Parameters[_state.ParameterIndex]; - var nav = parameter.NavigatorValue; + MacroParameter parameter = _macro.Parameters[_state.ParameterIndex]; + XPathNavigator? nav = parameter.NavigatorValue; if (parameter.WrapNavigatorInNodes || nav != null) { isEmpty = false; @@ -259,6 +267,7 @@ namespace Umbraco.Cms.Core.Xml.XPath var s = _macro.Parameters[_state.ParameterIndex].StringValue; isEmpty = s == null; } + break; case StatePosition.ParameterNavigator: isEmpty = _state.ParameterNavigator?.IsEmptyElement ?? true; @@ -410,7 +419,11 @@ namespace Umbraco.Cms.Core.Xml.XPath succ = true; DebugState(); } - else succ = false; + else + { + succ = false; + } + break; case StatePosition.ParameterAttribute: case StatePosition.ParameterNodes: @@ -452,8 +465,8 @@ namespace Umbraco.Cms.Core.Xml.XPath } break; case StatePosition.Parameter: - var parameter = _macro.Parameters[_state.ParameterIndex]; - var nav = parameter.NavigatorValue; + MacroParameter parameter = _macro.Parameters[_state.ParameterIndex]; + XPathNavigator? nav = parameter.NavigatorValue; if (parameter.WrapNavigatorInNodes) { _state.Position = StatePosition.ParameterNodes; @@ -479,8 +492,12 @@ namespace Umbraco.Cms.Core.Xml.XPath DebugState(); succ = true; } - else succ = false; + else + { + succ = false; + } } + break; case StatePosition.ParameterNavigator: if (_state.ParameterNavigatorDepth == _macro.Parameters[_state.ParameterIndex].MaxNavigatorDepth) @@ -507,7 +524,11 @@ namespace Umbraco.Cms.Core.Xml.XPath succ = true; DebugState(); } - else succ = false; + else + { + succ = false; + } + break; case StatePosition.ParameterAttribute: case StatePosition.ParameterText: @@ -692,7 +713,9 @@ namespace Umbraco.Cms.Core.Xml.XPath break; case StatePosition.ParameterAttribute: if (_state.ParameterAttributeIndex == _macro.Parameters[_state.ParameterIndex].Attributes?.Length - 1) + { succ = false; + } else { ++_state.ParameterAttributeIndex; @@ -914,7 +937,9 @@ namespace Umbraco.Cms.Core.Xml.XPath case StatePosition.ParameterNodes: nav = _macro.Parameters[_state.ParameterIndex].NavigatorValue; if (nav == null) + { value = string.Empty; + } else { nav = nav.Clone(); // never use the raw parameter's navigator @@ -945,16 +970,24 @@ namespace Umbraco.Cms.Core.Xml.XPath return false; } if (nav.NodeType != XPathNodeType.Element) + { return false; + } - var clone = nav.Clone(); + XPathNavigator clone = nav.Clone(); if (!clone.MoveToFirstAttribute()) + { return false; + } + do { if (clone.Name == "isDoc") + { return true; - } while (clone.MoveToNextAttribute()); + } + } + while (clone.MoveToNextAttribute()); return false; } @@ -971,7 +1004,7 @@ namespace Umbraco.Cms.Core.Xml.XPath ParameterText, ParameterNodes, ParameterNavigator - }; + } // gets the state // for unit tests only diff --git a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs index a575ee86f8..3529f55922 100644 --- a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs @@ -5,140 +5,141 @@ // but by default nothing is written, unless some lines are un-commented in Debug(...) below. // // Beware! Diagnostics are extremely verbose and can overflow logging pretty easily. - #if DEBUG // define to enable diagnostics code #undef DEBUGNAVIGATOR #endif -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Xml; using System.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +/// +/// Provides a cursor model for navigating Umbraco data as if it were XML. +/// +public class NavigableNavigator : XPathNavigator { + // "The XmlNameTable stores atomized strings of any local name, namespace URI, + // and prefix used by the XPathNavigator. This means that when the same Name is + // returned multiple times (like "book"), the same String object is returned for + // that Name. This makes it possible to write efficient code that does object + // comparisons on these strings, instead of expensive string comparisons." + // + // "When an element or attribute name occurs multiple times in an XML document, + // it is stored only once in the NameTable. The names are stored as common + // language runtime (CLR) object types. This enables you to do object comparisons + // on these strings rather than a more expensive string comparison. These + // string objects are referred to as atomized strings." + // + // But... "Any instance members are not guaranteed to be thread safe." + // + // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx + // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx + // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx + // + // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to + // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI, + // and Prefix properties are returned, the string returned should come from the + // NameTable. Comparisons between names are done by object comparisons rather + // than by string comparisons, which are significantly slower."" + // + // So what shall we do? Well, here we have no namespace, no prefix, and all + // local names come from cached instances of INavigableContentType or + // INavigableFieldType and are already unique. So... create a one nametable + // because we need one, and share it amongst all clones. + private readonly XmlNameTable _nameTable; + private readonly INavigableSource _source; + private readonly int _lastAttributeIndex; // last index of attributes in the fields collection + private readonly int _maxDepth; + + #region Constructor + + ///// + ///// Initializes a new instance of the class with a content source. + ///// + ///// The content source. + ///// The maximum depth. + // private NavigableNavigator(INavigableSource source, int maxDepth) + // { + // _source = source; + // _lastAttributeIndex = source.LastAttributeIndex; + // _maxDepth = maxDepth; + // } + /// - /// Provides a cursor model for navigating Umbraco data as if it were XML. + /// Initializes a new instance of the class with a content source, + /// and an optional root content. /// - public class NavigableNavigator : XPathNavigator + /// The content source. + /// The root content identifier. + /// The maximum depth. + /// When no root content is supplied then the root of the source is used. + public NavigableNavigator(INavigableSource source, int rootId = 0, int maxDepth = int.MaxValue) + + // : this(source, maxDepth) { - // "The XmlNameTable stores atomized strings of any local name, namespace URI, - // and prefix used by the XPathNavigator. This means that when the same Name is - // returned multiple times (like "book"), the same String object is returned for - // that Name. This makes it possible to write efficient code that does object - // comparisons on these strings, instead of expensive string comparisons." - // - // "When an element or attribute name occurs multiple times in an XML document, - // it is stored only once in the NameTable. The names are stored as common - // language runtime (CLR) object types. This enables you to do object comparisons - // on these strings rather than a more expensive string comparison. These - // string objects are referred to as atomized strings." - // - // But... "Any instance members are not guaranteed to be thread safe." - // - // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx - // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx - // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx - // - // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to - // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI, - // and Prefix properties are returned, the string returned should come from the - // NameTable. Comparisons between names are done by object comparisons rather - // than by string comparisons, which are significantly slower."" - // - // So what shall we do? Well, here we have no namespace, no prefix, and all - // local names come from cached instances of INavigableContentType or - // INavigableFieldType and are already unique. So... create a one nametable - // because we need one, and share it amongst all clones. + _source = source; + _lastAttributeIndex = source.LastAttributeIndex; + _maxDepth = maxDepth; - private readonly XmlNameTable _nameTable; - private readonly INavigableSource _source; - private readonly int _lastAttributeIndex; // last index of attributes in the fields collection - private State _state; - private readonly int _maxDepth; - - #region Constructor - - ///// - ///// Initializes a new instance of the class with a content source. - ///// - ///// The content source. - ///// The maximum depth. - //private NavigableNavigator(INavigableSource source, int maxDepth) - //{ - // _source = source; - // _lastAttributeIndex = source.LastAttributeIndex; - // _maxDepth = maxDepth; - //} - - /// - /// Initializes a new instance of the class with a content source, - /// and an optional root content. - /// - /// The content source. - /// The root content identifier. - /// The maximum depth. - /// When no root content is supplied then the root of the source is used. - public NavigableNavigator(INavigableSource source, int rootId = 0, int maxDepth = int.MaxValue) - //: this(source, maxDepth) + _nameTable = new NameTable(); + _lastAttributeIndex = source.LastAttributeIndex; + INavigableContent? content = rootId <= 0 ? source.Root : source.Get(rootId); + if (content == null) { - _source = source; - _lastAttributeIndex = source.LastAttributeIndex; - _maxDepth = maxDepth; - - _nameTable = new NameTable(); - _lastAttributeIndex = source.LastAttributeIndex; - var content = rootId <= 0 ? source.Root : source.Get(rootId); - if (content == null) - throw new ArgumentException("Not the identifier of a content within the source.", nameof(rootId)); - _state = new State(content, null, null, 0, StatePosition.Root); - - _contents = new ConcurrentDictionary(); + throw new ArgumentException("Not the identifier of a content within the source.", nameof(rootId)); } - ///// - ///// Initializes a new instance of the class with a content source, a name table and a state. - ///// - ///// The content source. - ///// The name table. - ///// The state. - ///// The maximum depth. - ///// Privately used for cloning a navigator. - //private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state, int maxDepth) - // : this(source, rootId: 0, maxDepth: maxDepth) - //{ - // _nameTable = nameTable; - // _state = state; - //} + InternalState = new State(content, null, null, 0, StatePosition.Root); - /// - /// Initializes a new instance of the class as a clone. - /// - /// The cloned navigator. - /// The clone state. - /// The clone maximum depth. - /// Privately used for cloning a navigator. - private NavigableNavigator(NavigableNavigator orig, State? state = null, int maxDepth = -1) - : this(orig._source, rootId: 0, maxDepth: orig._maxDepth) + _contents = new ConcurrentDictionary(); + } + + ///// + ///// Initializes a new instance of the class with a content source, a name table and a state. + ///// + ///// The content source. + ///// The name table. + ///// The state. + ///// The maximum depth. + ///// Privately used for cloning a navigator. + // private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state, int maxDepth) + // : this(source, rootId: 0, maxDepth: maxDepth) + // { + // _nameTable = nameTable; + // _state = state; + // } + + /// + /// Initializes a new instance of the class as a clone. + /// + /// The cloned navigator. + /// The clone state. + /// The clone maximum depth. + /// Privately used for cloning a navigator. + private NavigableNavigator(NavigableNavigator orig, State? state = null, int maxDepth = -1) + : this(orig._source, 0, orig._maxDepth) + { + _nameTable = orig._nameTable; + + InternalState = state ?? orig.InternalState.Clone(); + if (state != null && maxDepth < 0) { - _nameTable = orig._nameTable; - - _state = state ?? orig._state.Clone(); - if (state != null && maxDepth < 0) - throw new ArgumentException("Both state and maxDepth are required."); - _maxDepth = maxDepth < 0 ? orig._maxDepth : maxDepth; - - _contents = orig._contents; + throw new ArgumentException("Both state and maxDepth are required."); } - #endregion + _maxDepth = maxDepth < 0 ? orig._maxDepth : maxDepth; - #region Diagnostics + _contents = orig._contents; + } + + #endregion + + #region Diagnostics #if DEBUGNAVIGATOR private const string Tabs = " "; @@ -155,60 +156,59 @@ namespace Umbraco.Cms.Core.Xml.XPath } #endif - // About conditional methods: marking a method with the [Conditional] attribute ensures - // that no calls to the method will be generated by the compiler. However, the method - // does exist. Wrapping the method body with #if/endif ensures that no IL is generated - // and so it's only an empty method. - - [Conditional("DEBUGNAVIGATOR")] - void DebugEnter(string name) - { + // About conditional methods: marking a method with the [Conditional] attribute ensures + // that no calls to the method will be generated by the compiler. However, the method + // does exist. Wrapping the method body with #if/endif ensures that no IL is generated + // and so it's only an empty method. + [Conditional("DEBUGNAVIGATOR")] + private void DebugEnter(string name) + { #if DEBUGNAVIGATOR Debug(""); DebugState(":"); Debug(name); _tabs = Math.Min(Tabs.Length, _tabs + 2); #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - void DebugCreate(NavigableNavigator nav) - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugCreate(NavigableNavigator nav) + { #if DEBUGNAVIGATOR Debug("Create: [NavigableNavigator::{0}]", nav._uid); #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - private void DebugReturn() - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugReturn() + { #if DEBUGNAVIGATOR // ReSharper disable IntroduceOptionalParameters.Local DebugReturn("(void)"); // ReSharper restore IntroduceOptionalParameters.Local #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - private void DebugReturn(bool value) - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugReturn(bool value) + { #if DEBUGNAVIGATOR DebugReturn(value ? "true" : "false"); #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - void DebugReturn(string format, params object[] args) - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugReturn(string format, params object[] args) + { #if DEBUGNAVIGATOR Debug("=> " + format, args); if (_tabs > 0) _tabs -= 2; #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - void DebugState(string s = " =>") - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugState(string s = " =>") + { #if DEBUGNAVIGATOR string position; @@ -245,7 +245,7 @@ namespace Umbraco.Cms.Core.Xml.XPath Debug("State{0} {1}", s, position); #endif - } + } #if DEBUGNAVIGATOR void Debug(string format, params object[] args) @@ -257,980 +257,1035 @@ namespace Umbraco.Cms.Core.Xml.XPath } #endif - #endregion + #endregion - #region Source management + #region Source management - private readonly ConcurrentDictionary _contents; + private readonly ConcurrentDictionary _contents; - private INavigableContent? SourceGet(int id) + private INavigableContent? SourceGet(int id) => + + // original version, would keep creating INavigableContent objects + // return _source.Get(id); + // improved version, uses a cache, shared with clones + _contents.GetOrAdd(id, x => _source.Get(x)); + + #endregion + + /// + /// Gets the underlying content object. + /// + public override object? UnderlyingObject => InternalState.Content; + + /// + /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. + /// + /// A new XPathNavigator positioned at the same node as this XPathNavigator. + public override XPathNavigator Clone() + { + DebugEnter("Clone"); + var nav = new NavigableNavigator(this); + DebugCreate(nav); + DebugReturn("[XPathNavigator]"); + return nav; + } + + /// + /// Creates a new XPathNavigator using the same source but positioned at a new root. + /// + /// A new XPathNavigator using the same source and positioned at a new root. + /// The new root can be above this navigator's root. + public XPathNavigator CloneWithNewRoot(string id, int maxDepth = int.MaxValue) + { + int i; + if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out i) == false) { - // original version, would keep creating INavigableContent objects - //return _source.Get(id); - - // improved version, uses a cache, shared with clones - return _contents.GetOrAdd(id, x => _source.Get(x)); + throw new ArgumentException("Not a valid identifier.", nameof(id)); } - #endregion + return CloneWithNewRoot(id); + } - /// - /// Gets the underlying content object. - /// - public override object? UnderlyingObject => _state.Content; + /// + /// Creates a new XPathNavigator using the same source but positioned at a new root. + /// + /// A new XPathNavigator using the same source and positioned at a new root. + /// The new root can be above this navigator's root. + public XPathNavigator? CloneWithNewRoot(int id, int maxDepth = int.MaxValue) + { + DebugEnter("CloneWithNewRoot"); - /// - /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. - /// - /// A new XPathNavigator positioned at the same node as this XPathNavigator. - public override XPathNavigator Clone() + State? state = null; + + if (id <= 0) { - DebugEnter("Clone"); - var nav = new NavigableNavigator(this); - DebugCreate(nav); + state = new State(_source.Root, null, null, 0, StatePosition.Root); + } + else + { + INavigableContent? content = SourceGet(id); + if (content != null) + { + state = new State(content, null, null, 0, StatePosition.Root); + } + } + + NavigableNavigator? clone = null; + + if (state != null) + { + clone = new NavigableNavigator(this, state, maxDepth); + DebugCreate(clone); DebugReturn("[XPathNavigator]"); - return nav; + } + else + { + DebugReturn("[null]"); } - /// - /// Creates a new XPathNavigator using the same source but positioned at a new root. - /// - /// A new XPathNavigator using the same source and positioned at a new root. - /// The new root can be above this navigator's root. - public XPathNavigator CloneWithNewRoot(string id, int maxDepth = int.MaxValue) + return clone; + } + + /// + /// Gets a value indicating whether the current node is an empty element without an end element tag. + /// + public override bool IsEmptyElement + { + get { - int i; - if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out i) == false) - throw new ArgumentException("Not a valid identifier.", nameof(id)); - return CloneWithNewRoot(id); - } + DebugEnter("IsEmptyElement"); + bool isEmpty; - /// - /// Creates a new XPathNavigator using the same source but positioned at a new root. - /// - /// A new XPathNavigator using the same source and positioned at a new root. - /// The new root can be above this navigator's root. - public XPathNavigator? CloneWithNewRoot(int id, int maxDepth = int.MaxValue) - { - DebugEnter("CloneWithNewRoot"); - - State? state = null; - - if (id <= 0) - { - state = new State(_source.Root, null, null, 0, StatePosition.Root); - } - else - { - var content = SourceGet(id); - if (content != null) - { - state = new State(content, null, null, 0, StatePosition.Root); - } - } - - NavigableNavigator? clone = null; - - if (state != null) - { - clone = new NavigableNavigator(this, state, maxDepth); - DebugCreate(clone); - DebugReturn("[XPathNavigator]"); - } - else - { - DebugReturn("[null]"); - } - - return clone; - } - - /// - /// Gets a value indicating whether the current node is an empty element without an end element tag. - /// - public override bool IsEmptyElement - { - get - { - DebugEnter("IsEmptyElement"); - bool isEmpty; - - switch (_state.Position) - { - case StatePosition.Element: - // must go through source because of preview/published ie there may be - // ids but corresponding to preview elements that we don't see here - var hasContentChild = _state.GetContentChildIds(_maxDepth).Any(x => SourceGet(x) != null); - isEmpty = (hasContentChild == false) // no content child - && _state.FieldsCount - 1 == _lastAttributeIndex; // no property element child - break; - case StatePosition.PropertyElement: - // value should be - // - an XPathNavigator over a non-empty XML fragment - // - a non-Xml-whitespace string - // - null - isEmpty = _state.Content?.Value(_state.FieldIndex) == null; - break; - case StatePosition.PropertyXml: - isEmpty = _state.XmlFragmentNavigator?.IsEmptyElement ?? true; - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.Root: - throw new InvalidOperationException("Not an element."); - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(isEmpty); - return isEmpty; - } - } - - /// - /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. - /// - /// The XPathNavigator to compare to this XPathNavigator. - /// true if the two XPathNavigator objects have the same position; otherwise, false. - public override bool IsSamePosition(XPathNavigator nav) - { - DebugEnter("IsSamePosition"); - bool isSame; - - switch (_state.Position) + switch (InternalState.Position) { + case StatePosition.Element: + // must go through source because of preview/published ie there may be + // ids but corresponding to preview elements that we don't see here + var hasContentChild = InternalState.GetContentChildIds(_maxDepth).Any(x => SourceGet(x) != null); + isEmpty = hasContentChild == false // no content child + && InternalState.FieldsCount - 1 == _lastAttributeIndex; // no property element child + break; + case StatePosition.PropertyElement: + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + isEmpty = InternalState.Content?.Value(InternalState.FieldIndex) == null; + break; case StatePosition.PropertyXml: - isSame = _state.XmlFragmentNavigator?.IsSamePosition(nav) ?? false; + isEmpty = InternalState.XmlFragmentNavigator?.IsEmptyElement ?? true; break; case StatePosition.Attribute: - case StatePosition.Element: - case StatePosition.PropertyElement: case StatePosition.PropertyText: case StatePosition.Root: - var other = nav as NavigableNavigator; - isSame = other != null && other._source == _source && _state.IsSamePosition(other._state); + throw new InvalidOperationException("Not an element."); + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isEmpty); + return isEmpty; + } + } + + /// + /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. + /// + /// The XPathNavigator to compare to this XPathNavigator. + /// true if the two XPathNavigator objects have the same position; otherwise, false. + public override bool IsSamePosition(XPathNavigator nav) + { + DebugEnter("IsSamePosition"); + bool isSame; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + isSame = InternalState.XmlFragmentNavigator?.IsSamePosition(nav) ?? false; + break; + case StatePosition.Attribute: + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + var other = nav as NavigableNavigator; + isSame = other != null && other._source == _source && InternalState.IsSamePosition(other.InternalState); + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isSame); + return isSame; + } + + /// + /// Gets the qualified name of the current node. + /// + public override string Name + { + get + { + DebugEnter("Name"); + string name; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + name = InternalState.XmlFragmentNavigator?.Name ?? string.Empty; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + name = InternalState.FieldIndex == -1 ? "id" : InternalState.CurrentFieldType?.Name ?? string.Empty; + break; + case StatePosition.Element: + name = InternalState.Content?.Type.Name ?? string.Empty; + break; + case StatePosition.PropertyText: + name = string.Empty; + break; + case StatePosition.Root: + name = string.Empty; break; default: throw new InvalidOperationException("Invalid position."); } - DebugReturn(isSame); - return isSame; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Gets the Name of the current node without any namespace prefix. + /// + public override string LocalName + { + get + { + DebugEnter("LocalName"); + var name = Name; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Moves the XPathNavigator to the same position as the specified XPathNavigator. + /// + /// The XPathNavigator positioned on the node that you want to move to. + /// + /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveTo(XPathNavigator nav) + { + DebugEnter("MoveTo"); + + var other = nav as NavigableNavigator; + var succ = false; + + if (other != null && other._source == _source) + { + InternalState = other.InternalState.Clone(); + DebugState(); + succ = true; } - /// - /// Gets the qualified name of the current node. - /// - public override string Name + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first attribute of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToFirstAttribute() + { + DebugEnter("MoveToFirstAttribute"); + bool succ; + + switch (InternalState.Position) { - get - { - DebugEnter("Name"); - string name; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - name = _state.XmlFragmentNavigator?.Name ?? string.Empty; - break; - case StatePosition.Attribute: - case StatePosition.PropertyElement: - name = _state.FieldIndex == -1 ? "id" : _state.CurrentFieldType?.Name ?? string.Empty; - break; - case StatePosition.Element: - name = _state.Content?.Type.Name ?? string.Empty; - break; - case StatePosition.PropertyText: - name = string.Empty; - break; - case StatePosition.Root: - name = string.Empty; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn("\"{0}\"", name); - return name; - } - } - - /// - /// Gets the Name of the current node without any namespace prefix. - /// - public override string LocalName - { - get - { - DebugEnter("LocalName"); - var name = Name; - DebugReturn("\"{0}\"", name); - return name; - } - } - - /// - /// Moves the XPathNavigator to the same position as the specified XPathNavigator. - /// - /// The XPathNavigator positioned on the node that you want to move to. - /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveTo(XPathNavigator nav) - { - DebugEnter("MoveTo"); - - var other = nav as NavigableNavigator; - var succ = false; - - if (other != null && other._source == _source) - { - _state = other._state.Clone(); + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToFirstAttribute() ?? false; + break; + case StatePosition.Element: + InternalState.FieldIndex = -1; + InternalState.Position = StatePosition.Attribute; DebugState(); succ = true; - } - - DebugReturn(succ); - return succ; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); } - /// - /// Moves the XPathNavigator to the first attribute of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToFirstAttribute() + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first child node of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToFirstChild() + { + DebugEnter("MoveToFirstChild"); + bool succ; + + switch (InternalState.Position) { - DebugEnter("MoveToFirstAttribute"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToFirstAttribute() ?? false; - break; - case StatePosition.Element: - _state.FieldIndex = -1; - _state.Position = StatePosition.Attribute; - DebugState(); - succ = true; - break; - case StatePosition.Attribute: - case StatePosition.PropertyElement: - case StatePosition.PropertyText: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the first child node of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToFirstChild() - { - DebugEnter("MoveToFirstChild"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToFirstChild() ?? false; - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - succ = false; - break; - case StatePosition.Element: - var firstPropertyIndex = _lastAttributeIndex + 1; - if (_state.FieldsCount > firstPropertyIndex) - { - _state.Position = StatePosition.PropertyElement; - _state.FieldIndex = firstPropertyIndex; - DebugState(); - succ = true; - } - else succ = MoveToFirstChildElement(); - break; - case StatePosition.PropertyElement: - succ = MoveToFirstChildProperty(); - break; - case StatePosition.Root: - _state.Position = StatePosition.Element; - DebugState(); - succ = true; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - private bool MoveToFirstChildElement() - { - var children = _state.GetContentChildIds(_maxDepth); - - if (children.Count > 0) - { - // children may contain IDs that does not correspond to some content in source - // because children contains all child IDs including unpublished children - and - // then if we're not previewing, the source will return null. - var child = children.Select(id => SourceGet(id)).FirstOrDefault(c => c != null); - if (child != null) + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToFirstChild() ?? false; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + succ = false; + break; + case StatePosition.Element: + var firstPropertyIndex = _lastAttributeIndex + 1; + if (InternalState.FieldsCount > firstPropertyIndex) { - _state.Position = StatePosition.Element; - _state.FieldIndex = -1; - _state = new State(child, _state, children, 0, StatePosition.Element); + InternalState.Position = StatePosition.PropertyElement; + InternalState.FieldIndex = firstPropertyIndex; DebugState(); - return true; - } - } - - return false; - } - - private bool MoveToFirstChildProperty() - { - var valueForXPath = _state.Content?.Value(_state.FieldIndex); - - // value should be - // - an XPathNavigator over a non-empty XML fragment - // - a non-Xml-whitespace string - // - null - - var nav = valueForXPath as XPathNavigator; - if (nav != null) - { - nav = nav.Clone(); // never use the one we got - nav.MoveToFirstChild(); - _state.XmlFragmentNavigator = nav; - _state.Position = StatePosition.PropertyXml; - DebugState(); - return true; - } - - if (valueForXPath == null) - return false; - - if (valueForXPath is string) - { - _state.Position = StatePosition.PropertyText; - DebugState(); - return true; - } - - throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); - } - - /// - /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. - /// - /// An XPathNamespaceScope value describing the namespace scope. - /// Returns true if the XPathNavigator is successful moving to the first namespace node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) - { - DebugEnter("MoveToFirstNamespace"); - DebugReturn(false); - return false; - } - - /// - /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. - /// - /// An XPathNamespaceScope value describing the namespace scope. - /// Returns true if the XPathNavigator is successful moving to the next namespace node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) - { - DebugEnter("MoveToNextNamespace"); - DebugReturn(false); - return false; - } - - /// - /// Moves to the node that has an attribute of type ID whose value matches the specified String. - /// - /// A String representing the ID value of the node to which you want to move. - /// true if the XPathNavigator is successful moving; otherwise, false. - /// If false, the position of the navigator is unchanged. - public override bool MoveToId(string id) - { - DebugEnter("MoveToId"); - var succ = false; - - // don't look into fragments, just look for element identifiers - // not sure we actually need to implement it... think of it as - // as exercise of style, always better than throwing NotImplemented. - - // navigator may be rooted below source root - // find the navigator root id - var state = _state; - while (state.Parent != null) // root state has no parent - state = state.Parent; - var navRootId = state.Content?.Id; - - int contentId; - if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentId)) - { - if (contentId == navRootId) - { - _state = new State(state.Content, null, null, 0, StatePosition.Element); succ = true; } else { - var content = SourceGet(contentId); - if (content != null) - { - // walk up to the navigator's root - or the source's root - var s = new Stack(); - while (content != null && content.ParentId != navRootId) - { - s.Push(content); - content = SourceGet(content.ParentId); - } - - if (content != null && s.Count < _maxDepth) - { - _state = new State(state.Content, null, null, 0, StatePosition.Element); - while (content != null) - { - _state = new State(content, _state, _state.Content?.ChildIds, _state.Content?.ChildIds?.IndexOf(content.Id) ?? -1, StatePosition.Element); - content = s.Count == 0 ? null : s.Pop(); - } - DebugState(); - succ = true; - } - } + succ = MoveToFirstChildElement(); } - } - DebugReturn(succ); - return succ; + break; + case StatePosition.PropertyElement: + succ = MoveToFirstChildProperty(); + break; + case StatePosition.Root: + InternalState.Position = StatePosition.Element; + DebugState(); + succ = true; + break; + default: + throw new InvalidOperationException("Invalid position."); } - /// - /// Moves the XPathNavigator to the next sibling node of the current node. - /// - /// true if the XPathNavigator is successful moving to the next sibling node; - /// otherwise, false if there are no more siblings or if the XPathNavigator is currently - /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToNext() + DebugReturn(succ); + return succ; + } + + private bool MoveToFirstChildElement() + { + IList children = InternalState.GetContentChildIds(_maxDepth); + + if (children.Count > 0) { - DebugEnter("MoveToNext"); - bool succ; - - switch (_state.Position) + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + INavigableContent? child = children.Select(id => SourceGet(id)).FirstOrDefault(c => c != null); + if (child != null) { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToNext() ?? false; - break; - case StatePosition.Element: - succ = false; - while (_state.Siblings != null && _state.SiblingIndex < _state.Siblings.Count - 1) - { - // Siblings may contain IDs that does not correspond to some content in source - // because children contains all child IDs including unpublished children - and - // then if we're not previewing, the source will return null. - var node = SourceGet(_state.Siblings[++_state.SiblingIndex]); - if (node == null) continue; - - _state.Content = node; - DebugState(); - succ = true; - break; - } - break; - case StatePosition.PropertyElement: - if (_state.FieldIndex == _state.FieldsCount - 1) - { - // after property elements may come some children elements - // if successful, will push a new state - succ = MoveToFirstChildElement(); - } - else - { - ++_state.FieldIndex; - DebugState(); - succ = true; - } - break; - case StatePosition.PropertyText: - case StatePosition.Attribute: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the previous sibling node of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the previous sibling node; - /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently - /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToPrevious() - { - DebugEnter("MoveToPrevious"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToPrevious() ?? false; - break; - case StatePosition.Element: - succ = false; - while (_state.Siblings != null && _state.SiblingIndex > 0) - { - // children may contain IDs that does not correspond to some content in source - // because children contains all child IDs including unpublished children - and - // then if we're not previewing, the source will return null. - var content = SourceGet(_state.Siblings[--_state.SiblingIndex]); - if (content == null) continue; - - _state.Content = content; - DebugState(); - succ = true; - break; - } - if (succ == false && _state.SiblingIndex == 0 && _state.FieldsCount - 1 > _lastAttributeIndex) - { - // before children elements may come some property elements - if (MoveToParentElement()) // pops the state - { - _state.FieldIndex = _state.FieldsCount - 1; - DebugState(); - succ = true; - } - } - break; - case StatePosition.PropertyElement: - succ = false; - if (_state.FieldIndex > _lastAttributeIndex) - { - --_state.FieldIndex; - DebugState(); - succ = true; - } - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the next attribute. - /// - /// Returns true if the XPathNavigator is successful moving to the next attribute; - /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToNextAttribute() - { - DebugEnter("MoveToNextAttribute"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToNextAttribute() ?? false; - break; - case StatePosition.Attribute: - if (_state.FieldIndex == _lastAttributeIndex) - succ = false; - else - { - ++_state.FieldIndex; - DebugState(); - succ = true; - } - break; - case StatePosition.Element: - case StatePosition.PropertyElement: - case StatePosition.PropertyText: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the parent node of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToParent() - { - DebugEnter("MoveToParent"); - bool succ; - - switch (_state.Position) - { - case StatePosition.Attribute: - case StatePosition.PropertyElement: - _state.Position = StatePosition.Element; - _state.FieldIndex = -1; - DebugState(); - succ = true; - break; - case StatePosition.Element: - succ = MoveToParentElement(); - if (succ == false) - { - _state.Position = StatePosition.Root; - succ = true; - } - break; - case StatePosition.PropertyText: - _state.Position = StatePosition.PropertyElement; - DebugState(); - succ = true; - break; - case StatePosition.PropertyXml: - if (_state.XmlFragmentNavigator?.MoveToParent() == false) - throw new InvalidOperationException("Could not move to parent in fragment."); - if (_state.XmlFragmentNavigator?.NodeType == XPathNodeType.Root) - { - _state.XmlFragmentNavigator = null; - _state.Position = StatePosition.PropertyElement; - DebugState(); - } - succ = true; - break; - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - private bool MoveToParentElement() - { - var p = _state.Parent; - if (p != null) - { - _state = p; + InternalState.Position = StatePosition.Element; + InternalState.FieldIndex = -1; + InternalState = new State(child, InternalState, children, 0, StatePosition.Element); DebugState(); return true; } + } + return false; + } + + private bool MoveToFirstChildProperty() + { + var valueForXPath = InternalState.Content?.Value(InternalState.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + var nav = valueForXPath as XPathNavigator; + if (nav != null) + { + nav = nav.Clone(); // never use the one we got + nav.MoveToFirstChild(); + InternalState.XmlFragmentNavigator = nav; + InternalState.Position = StatePosition.PropertyXml; + DebugState(); + return true; + } + + if (valueForXPath == null) + { return false; } - /// - /// Moves the XPathNavigator to the root node that the current node belongs to. - /// - public override void MoveToRoot() + if (valueForXPath is string) { - DebugEnter("MoveToRoot"); - - while (_state.Parent != null) - _state = _state.Parent; + InternalState.Position = StatePosition.PropertyText; DebugState(); - - DebugReturn(); + return true; } - /// - /// Gets the base URI for the current node. - /// - public override string BaseURI => string.Empty; + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); + } - /// - /// Gets the XmlNameTable of the XPathNavigator. - /// - public override XmlNameTable NameTable => _nameTable; + /// + /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// + /// Returns true if the XPathNavigator is successful moving to the first namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToFirstNamespace"); + DebugReturn(false); + return false; + } - /// - /// Gets the namespace URI of the current node. - /// - public override string NamespaceURI => string.Empty; + /// + /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// + /// Returns true if the XPathNavigator is successful moving to the next namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToNextNamespace"); + DebugReturn(false); + return false; + } - /// - /// Gets the XPathNodeType of the current node. - /// - public override XPathNodeType NodeType + /// + /// Moves to the node that has an attribute of type ID whose value matches the specified String. + /// + /// A String representing the ID value of the node to which you want to move. + /// + /// true if the XPathNavigator is successful moving; otherwise, false. + /// If false, the position of the navigator is unchanged. + /// + public override bool MoveToId(string id) + { + DebugEnter("MoveToId"); + var succ = false; + + // don't look into fragments, just look for element identifiers + // not sure we actually need to implement it... think of it as + // as exercise of style, always better than throwing NotImplemented. + + // navigator may be rooted below source root + // find the navigator root id + State state = InternalState; + + // root state has no parent + while (state.Parent != null) { - get + state = state.Parent; + } + + var navRootId = state.Content?.Id; + + int contentId; + if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentId)) + { + if (contentId == navRootId) { - DebugEnter("NodeType"); - XPathNodeType type; - - switch (_state.Position) + InternalState = new State(state.Content, null, null, 0, StatePosition.Element); + succ = true; + } + else + { + INavigableContent? content = SourceGet(contentId); + if (content != null) { - case StatePosition.PropertyXml: - type = _state.XmlFragmentNavigator?.NodeType ?? XPathNodeType.Root; - break; - case StatePosition.Attribute: - type = XPathNodeType.Attribute; - break; - case StatePosition.Element: - case StatePosition.PropertyElement: - type = XPathNodeType.Element; - break; - case StatePosition.PropertyText: - type = XPathNodeType.Text; - break; - case StatePosition.Root: - type = XPathNodeType.Root; - break; - default: - throw new InvalidOperationException("Invalid position."); - } + // walk up to the navigator's root - or the source's root + var s = new Stack(); + while (content != null && content.ParentId != navRootId) + { + s.Push(content); + content = SourceGet(content.ParentId); + } - DebugReturn("\'{0}\'", type); - return type; + if (content != null && s.Count < _maxDepth) + { + InternalState = new State(state.Content, null, null, 0, StatePosition.Element); + while (content != null) + { + InternalState = new State(content, InternalState, InternalState.Content?.ChildIds, InternalState.Content?.ChildIds?.IndexOf(content.Id) ?? -1, StatePosition.Element); + content = s.Count == 0 ? null : s.Pop(); + } + + DebugState(); + succ = true; + } + } } } - /// - /// Gets the namespace prefix associated with the current node. - /// - public override string Prefix => string.Empty; + DebugReturn(succ); + return succ; + } - /// - /// Gets the string value of the item. - /// - /// Does not fully behave as per the specs, as we report empty value on content elements, and we start - /// reporting values only on property elements. This is because, otherwise, we would dump the whole database - /// and it probably does not make sense at Umbraco level. - public override string Value + /// + /// Moves the XPathNavigator to the next sibling node of the current node. + /// + /// + /// true if the XPathNavigator is successful moving to the next sibling node; + /// otherwise, false if there are no more siblings or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToNext() + { + DebugEnter("MoveToNext"); + bool succ; + + switch (InternalState.Position) { - get - { - DebugEnter("Value"); - string value; - - switch (_state.Position) + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToNext() ?? false; + break; + case StatePosition.Element: + succ = false; + while (InternalState.Siblings != null && InternalState.SiblingIndex < InternalState.Siblings.Count - 1) { - case StatePosition.PropertyXml: - value = _state.XmlFragmentNavigator?.Value ?? string.Empty; - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.PropertyElement: - if (_state.FieldIndex == -1) + // Siblings may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + INavigableContent? node = SourceGet(InternalState.Siblings[++InternalState.SiblingIndex]); + if (node == null) + { + continue; + } + + InternalState.Content = node; + DebugState(); + succ = true; + break; + } + + break; + case StatePosition.PropertyElement: + if (InternalState.FieldIndex == InternalState.FieldsCount - 1) + { + // after property elements may come some children elements + // if successful, will push a new state + succ = MoveToFirstChildElement(); + } + else + { + ++InternalState.FieldIndex; + DebugState(); + succ = true; + } + + break; + case StatePosition.PropertyText: + case StatePosition.Attribute: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the previous sibling node of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the previous sibling node; + /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToPrevious() + { + DebugEnter("MoveToPrevious"); + bool succ; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToPrevious() ?? false; + break; + case StatePosition.Element: + succ = false; + while (InternalState.Siblings != null && InternalState.SiblingIndex > 0) + { + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + INavigableContent? content = SourceGet(InternalState.Siblings[--InternalState.SiblingIndex]); + if (content == null) + { + continue; + } + + InternalState.Content = content; + DebugState(); + succ = true; + break; + } + + if (succ == false && InternalState.SiblingIndex == 0 && + InternalState.FieldsCount - 1 > _lastAttributeIndex) + { + // before children elements may come some property elements + // pops the state + if (MoveToParentElement()) + { + InternalState.FieldIndex = InternalState.FieldsCount - 1; + DebugState(); + succ = true; + } + } + + break; + case StatePosition.PropertyElement: + succ = false; + if (InternalState.FieldIndex > _lastAttributeIndex) + { + --InternalState.FieldIndex; + DebugState(); + succ = true; + } + + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next attribute. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the next attribute; + /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToNextAttribute() + { + DebugEnter("MoveToNextAttribute"); + bool succ; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToNextAttribute() ?? false; + break; + case StatePosition.Attribute: + if (InternalState.FieldIndex == _lastAttributeIndex) + { + succ = false; + } + else + { + ++InternalState.FieldIndex; + DebugState(); + succ = true; + } + + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the parent node of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToParent() + { + DebugEnter("MoveToParent"); + bool succ; + + switch (InternalState.Position) + { + case StatePosition.Attribute: + case StatePosition.PropertyElement: + InternalState.Position = StatePosition.Element; + InternalState.FieldIndex = -1; + DebugState(); + succ = true; + break; + case StatePosition.Element: + succ = MoveToParentElement(); + if (succ == false) + { + InternalState.Position = StatePosition.Root; + succ = true; + } + + break; + case StatePosition.PropertyText: + InternalState.Position = StatePosition.PropertyElement; + DebugState(); + succ = true; + break; + case StatePosition.PropertyXml: + if (InternalState.XmlFragmentNavigator?.MoveToParent() == false) + { + throw new InvalidOperationException("Could not move to parent in fragment."); + } + + if (InternalState.XmlFragmentNavigator?.NodeType == XPathNodeType.Root) + { + InternalState.XmlFragmentNavigator = null; + InternalState.Position = StatePosition.PropertyElement; + DebugState(); + } + + succ = true; + break; + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + private bool MoveToParentElement() + { + State? p = InternalState.Parent; + if (p != null) + { + InternalState = p; + DebugState(); + return true; + } + + return false; + } + + /// + /// Moves the XPathNavigator to the root node that the current node belongs to. + /// + public override void MoveToRoot() + { + DebugEnter("MoveToRoot"); + + while (InternalState.Parent != null) + { + InternalState = InternalState.Parent; + } + + DebugState(); + + DebugReturn(); + } + + /// + /// Gets the base URI for the current node. + /// + public override string BaseURI => string.Empty; + + /// + /// Gets the XmlNameTable of the XPathNavigator. + /// + public override XmlNameTable NameTable => _nameTable; + + /// + /// Gets the namespace URI of the current node. + /// + public override string NamespaceURI => string.Empty; + + /// + /// Gets the XPathNodeType of the current node. + /// + public override XPathNodeType NodeType + { + get + { + DebugEnter("NodeType"); + XPathNodeType type; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + type = InternalState.XmlFragmentNavigator?.NodeType ?? XPathNodeType.Root; + break; + case StatePosition.Attribute: + type = XPathNodeType.Attribute; + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + type = XPathNodeType.Element; + break; + case StatePosition.PropertyText: + type = XPathNodeType.Text; + break; + case StatePosition.Root: + type = XPathNodeType.Root; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\'{0}\'", type); + return type; + } + } + + /// + /// Gets the namespace prefix associated with the current node. + /// + public override string Prefix => string.Empty; + + /// + /// Gets the string value of the item. + /// + /// + /// Does not fully behave as per the specs, as we report empty value on content elements, and we start + /// reporting values only on property elements. This is because, otherwise, we would dump the whole database + /// and it probably does not make sense at Umbraco level. + /// + public override string Value + { + get + { + DebugEnter("Value"); + string value; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + value = InternalState.XmlFragmentNavigator?.Value ?? string.Empty; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.PropertyElement: + if (InternalState.FieldIndex == -1) + { + value = InternalState.Content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + } + else + { + var valueForXPath = InternalState.Content?.Value(InternalState.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + var nav = valueForXPath as XPathNavigator; + var s = valueForXPath as string; + if (valueForXPath == null) { - value = _state.Content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + value = string.Empty; + } + else if (nav != null) + { + nav = nav.Clone(); // never use the one we got + value = nav.Value; + } + else if (s != null) + { + value = s; } else { - var valueForXPath = _state.Content?.Value(_state.FieldIndex); - - // value should be - // - an XPathNavigator over a non-empty XML fragment - // - a non-Xml-whitespace string - // - null - - var nav = valueForXPath as XPathNavigator; - var s = valueForXPath as string; - if (valueForXPath == null) - { - value = string.Empty; - } - else if (nav != null) - { - nav = nav.Clone(); // never use the one we got - value = nav.Value; - } - else if (s != null) - { - value = s; - } - else - { - throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); - } + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); } - break; - case StatePosition.Element: - case StatePosition.Root: - value = string.Empty; - break; - default: - throw new InvalidOperationException("Invalid position."); - } + } - DebugReturn("\"{0}\"", value); - return value; + break; + case StatePosition.Element: + case StatePosition.Root: + value = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); } + + DebugReturn("\"{0}\"", value); + return value; } - - #region State management - - // the possible state positions - public enum StatePosition - { - Root, - Element, - Attribute, - PropertyElement, - PropertyText, - PropertyXml - }; - - // gets the state - // for unit tests only - public State InternalState => _state; - - // represents the XPathNavigator state - public class State - { - public StatePosition Position { get; set; } - - // initialize a new state - private State(StatePosition position) - { - Position = position; - FieldIndex = -1; - } - - // initialize a new state - // used for creating the very first state - // and also when moving to a child element - public State(INavigableContent? content, State? parent, IList? siblings, int siblingIndex, StatePosition position) - : this(position) - { - Content = content; - Parent = parent; - Depth = parent?.Depth + 1 ?? 0; - Siblings = siblings; - SiblingIndex = siblingIndex; - } - - // initialize a clone state - private State(State other, bool recurse = false) - { - Position = other.Position; - - _content = other._content; - SiblingIndex = other.SiblingIndex; - Siblings = other.Siblings; - FieldsCount = other.FieldsCount; - FieldIndex = other.FieldIndex; - Depth = other.Depth; - - if (Position == StatePosition.PropertyXml) - XmlFragmentNavigator = other.XmlFragmentNavigator?.Clone(); - - // NielsK did - //Parent = other.Parent; - // but that creates corrupted stacks of states when cloning - // because clones share the parents : have to clone the whole - // stack of states. Avoid recursion. - - if (recurse) return; - - var clone = this; - while (other.Parent != null) - { - clone.Parent = new State(other.Parent, true); - clone = clone.Parent; - other = other.Parent; - } - } - - public State Clone() - { - return new State(this); - } - - // the parent state - public State? Parent { get; private set; } - - // the depth - public int Depth { get; } - - // the current content - private INavigableContent? _content; - - // the current content - public INavigableContent? Content - { - get - { - return _content; - } - set - { - FieldsCount = value?.Type.FieldTypes.Length ?? 0; - _content = value; - } - } - - private static readonly int[] NoChildIds = new int[0]; - - // the current content child ids - public IList GetContentChildIds(int maxDepth) - { - return Depth < maxDepth && _content?.ChildIds != null ? _content.ChildIds : NoChildIds; - } - - // the index of the current content within Siblings - public int SiblingIndex { get; set; } - - // the list of content identifiers for all children of the current content's parent - public IList? Siblings { get; } - - // the number of fields of the current content - // properties include attributes and properties - public int FieldsCount { get; private set; } - - // the index of the current field - // index -1 means special attribute "id" - public int FieldIndex { get; set; } - - // the current field type - // beware, no check on the index - public INavigableFieldType? CurrentFieldType => Content?.Type.FieldTypes[FieldIndex]; - - // gets or sets the xml fragment navigator - public XPathNavigator? XmlFragmentNavigator { get; set; } - - // gets a value indicating whether this state is at the same position as another one. - public bool IsSamePosition(State other) - { - if (other.XmlFragmentNavigator is null || XmlFragmentNavigator is null) - { - return false; - } - return other.Position == Position - && (Position != StatePosition.PropertyXml || other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator)) - && other.Content == Content - && other.FieldIndex == FieldIndex; - } - } - - #endregion } + + #region State management + + // the possible state positions + public enum StatePosition + { + Root, + Element, + Attribute, + PropertyElement, + PropertyText, + PropertyXml, + } + + // gets the state + // for unit tests only + public State InternalState { get; private set; } + + // represents the XPathNavigator state + public class State + { + private static readonly int[] NoChildIds = new int[0]; + + // the current content + private INavigableContent? _content; + + // initialize a new state + private State(StatePosition position) + { + Position = position; + FieldIndex = -1; + } + + // initialize a new state + // used for creating the very first state + // and also when moving to a child element + public State(INavigableContent? content, State? parent, IList? siblings, int siblingIndex, StatePosition position) + : this(position) + { + Content = content; + Parent = parent; + Depth = parent?.Depth + 1 ?? 0; + Siblings = siblings; + SiblingIndex = siblingIndex; + } + + // initialize a clone state + private State(State other, bool recurse = false) + { + Position = other.Position; + + _content = other._content; + SiblingIndex = other.SiblingIndex; + Siblings = other.Siblings; + FieldsCount = other.FieldsCount; + FieldIndex = other.FieldIndex; + Depth = other.Depth; + + if (Position == StatePosition.PropertyXml) + { + XmlFragmentNavigator = other.XmlFragmentNavigator?.Clone(); + } + + // NielsK did + // Parent = other.Parent; + // but that creates corrupted stacks of states when cloning + // because clones share the parents : have to clone the whole + // stack of states. Avoid recursion. + if (recurse) + { + return; + } + + State clone = this; + while (other.Parent != null) + { + clone.Parent = new State(other.Parent, true); + clone = clone.Parent; + other = other.Parent; + } + } + + public StatePosition Position { get; set; } + + // the parent state + public State? Parent { get; private set; } + + // the depth + public int Depth { get; } + + // the current content + public INavigableContent? Content + { + get => _content; + set + { + FieldsCount = value?.Type.FieldTypes.Length ?? 0; + _content = value; + } + } + + // the index of the current content within Siblings + public int SiblingIndex { get; set; } + + // the list of content identifiers for all children of the current content's parent + public IList? Siblings { get; } + + // the number of fields of the current content + // properties include attributes and properties + public int FieldsCount { get; private set; } + + // the index of the current field + // index -1 means special attribute "id" + public int FieldIndex { get; set; } + + // the current field type + // beware, no check on the index + public INavigableFieldType? CurrentFieldType => Content?.Type.FieldTypes[FieldIndex]; + + // gets or sets the xml fragment navigator + public XPathNavigator? XmlFragmentNavigator { get; set; } + + public State Clone() => new State(this); + + // the current content child ids + public IList GetContentChildIds(int maxDepth) => + Depth < maxDepth && _content?.ChildIds != null ? _content.ChildIds : NoChildIds; + + // gets a value indicating whether this state is at the same position as another one. + public bool IsSamePosition(State other) + { + if (other.XmlFragmentNavigator is null || XmlFragmentNavigator is null) + { + return false; + } + + return other.Position == Position + && (Position != StatePosition.PropertyXml || + other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator)) + && other.Content == Content + && other.FieldIndex == FieldIndex; + } + } + + #endregion } diff --git a/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs b/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs index 364560ebee..1b710c8db5 100644 --- a/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs @@ -1,119 +1,88 @@ -using System.Xml; +using System.Xml; using System.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +public class RenamedRootNavigator : XPathNavigator { - public class RenamedRootNavigator : XPathNavigator + private readonly XPathNavigator _navigator; + private readonly string _rootName; + + public RenamedRootNavigator(XPathNavigator navigator, string rootName) { - private readonly XPathNavigator _navigator; - private readonly string _rootName; - - public RenamedRootNavigator(XPathNavigator navigator, string rootName) - { - _navigator = navigator; - _rootName = rootName; - } - - public override string BaseURI => _navigator.BaseURI; - - public override XPathNavigator Clone() - { - return new RenamedRootNavigator(_navigator.Clone(), _rootName); - } - - public override bool IsEmptyElement => _navigator.IsEmptyElement; - - public override bool IsSamePosition(XPathNavigator other) - { - return _navigator.IsSamePosition(other); - } - - public override string LocalName - { - get - { - // local name without prefix - - var nav = _navigator.Clone(); - if (nav.MoveToParent() && nav.MoveToParent()) - return _navigator.LocalName; - return _rootName; - } - } - - public override bool MoveTo(XPathNavigator other) - { - return _navigator.MoveTo(other); - } - - public override bool MoveToFirstAttribute() - { - return _navigator.MoveToFirstAttribute(); - } - - public override bool MoveToFirstChild() - { - return _navigator.MoveToFirstChild(); - } - - public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) - { - return _navigator.MoveToFirstNamespace(namespaceScope); - } - - public override bool MoveToId(string id) - { - return _navigator.MoveToId(id); - } - - public override bool MoveToNext() - { - return _navigator.MoveToNext(); - } - - public override bool MoveToNextAttribute() - { - return _navigator.MoveToNextAttribute(); - } - - public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) - { - return _navigator.MoveToNextNamespace(namespaceScope); - } - - public override bool MoveToParent() - { - return _navigator.MoveToParent(); - } - - public override bool MoveToPrevious() - { - return _navigator.MoveToPrevious(); - } - - public override string Name - { - get - { - // qualified name with prefix - - var nav = _navigator.Clone(); - if (nav.MoveToParent() && nav.MoveToParent()) - return _navigator.Name; - var name = _navigator.Name; - var pos = name.IndexOf(':'); - return pos < 0 ? _rootName : (name.Substring(0, pos + 1) + _rootName); - } - } - - public override XmlNameTable NameTable => _navigator.NameTable; - - public override string NamespaceURI => _navigator.NamespaceURI; - - public override XPathNodeType NodeType => _navigator.NodeType; - - public override string Prefix => _navigator.Prefix; - - public override string Value => _navigator.Value; + _navigator = navigator; + _rootName = rootName; } + + public override string BaseURI => _navigator.BaseURI; + + public override bool IsEmptyElement => _navigator.IsEmptyElement; + + public override string LocalName + { + get + { + // local name without prefix + XPathNavigator nav = _navigator.Clone(); + if (nav.MoveToParent() && nav.MoveToParent()) + { + return _navigator.LocalName; + } + + return _rootName; + } + } + + public override string Name + { + get + { + // qualified name with prefix + XPathNavigator nav = _navigator.Clone(); + if (nav.MoveToParent() && nav.MoveToParent()) + { + return _navigator.Name; + } + + var name = _navigator.Name; + var pos = name.IndexOf(':'); + return pos < 0 ? _rootName : name[..(pos + 1)] + _rootName; + } + } + + public override XmlNameTable NameTable => _navigator.NameTable; + + public override string NamespaceURI => _navigator.NamespaceURI; + + public override XPathNodeType NodeType => _navigator.NodeType; + + public override string Prefix => _navigator.Prefix; + + public override string Value => _navigator.Value; + + public override XPathNavigator Clone() => new RenamedRootNavigator(_navigator.Clone(), _rootName); + + public override bool IsSamePosition(XPathNavigator other) => _navigator.IsSamePosition(other); + + public override bool MoveTo(XPathNavigator other) => _navigator.MoveTo(other); + + public override bool MoveToFirstAttribute() => _navigator.MoveToFirstAttribute(); + + public override bool MoveToFirstChild() => _navigator.MoveToFirstChild(); + + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) => + _navigator.MoveToFirstNamespace(namespaceScope); + + public override bool MoveToId(string id) => _navigator.MoveToId(id); + + public override bool MoveToNext() => _navigator.MoveToNext(); + + public override bool MoveToNextAttribute() => _navigator.MoveToNextAttribute(); + + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) => + _navigator.MoveToNextNamespace(namespaceScope); + + public override bool MoveToParent() => _navigator.MoveToParent(); + + public override bool MoveToPrevious() => _navigator.MoveToPrevious(); } diff --git a/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs b/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs index 8006d26da6..44cda2c691 100644 --- a/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs +++ b/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs @@ -1,61 +1,70 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Xml.XPath; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions to XPathNavigator. +/// +public static class XPathNavigatorExtensions { /// - /// Provides extensions to XPathNavigator. + /// Selects a node set, using the specified XPath expression. /// - public static class XPathNavigatorExtensions + /// A source XPathNavigator. + /// An XPath expression. + /// A set of XPathVariables. + /// An iterator over the nodes matching the specified expression. + public static XPathNodeIterator Select(this XPathNavigator navigator, string expression, params XPathVariable[] variables) { - /// - /// Selects a node set, using the specified XPath expression. - /// - /// A source XPathNavigator. - /// An XPath expression. - /// A set of XPathVariables. - /// An iterator over the nodes matching the specified expression. - public static XPathNodeIterator Select(this XPathNavigator navigator, string expression, params XPathVariable[] variables) + if (variables == null || variables.Length == 0 || variables[0] == null) { - if (variables == null || variables.Length == 0 || variables[0] == null) - return navigator.Select(expression); - - // Reflector shows that the standard XPathNavigator.Compile method just does - // return XPathExpression.Compile(xpath); - // only difference is, XPathNavigator.Compile is virtual so it could be overridden - // by a class inheriting from XPathNavigator... there does not seem to be any - // doing it in the Framework, though... so we'll assume it's much cleaner to use - // the static compile: - var compiled = XPathExpression.Compile(expression); - - var context = new DynamicContext(); - foreach (var variable in variables) - context.AddVariable(variable.Name, variable.Value); - compiled.SetContext(context); - return navigator.Select(compiled); + return navigator.Select(expression); } - /// - /// Selects a node set, using the specified XPath expression. - /// - /// A source XPathNavigator. - /// An XPath expression. - /// A set of XPathVariables. - /// An iterator over the nodes matching the specified expression. - public static XPathNodeIterator Select(this XPathNavigator navigator, XPathExpression expression, params XPathVariable[] variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return navigator.Select(expression); + // Reflector shows that the standard XPathNavigator.Compile method just does + // return XPathExpression.Compile(xpath); + // only difference is, XPathNavigator.Compile is virtual so it could be overridden + // by a class inheriting from XPathNavigator... there does not seem to be any + // doing it in the Framework, though... so we'll assume it's much cleaner to use + // the static compile: + var compiled = XPathExpression.Compile(expression); - var compiled = expression.Clone(); // clone for thread-safety - var context = new DynamicContext(); - foreach (var variable in variables) - context.AddVariable(variable.Name, variable.Value); - compiled.SetContext(context); - return navigator.Select(compiled); + var context = new DynamicContext(); + foreach (XPathVariable variable in variables) + { + context.AddVariable(variable.Name, variable.Value); } + + compiled.SetContext(context); + return navigator.Select(compiled); + } + + /// + /// Selects a node set, using the specified XPath expression. + /// + /// A source XPathNavigator. + /// An XPath expression. + /// A set of XPathVariables. + /// An iterator over the nodes matching the specified expression. + public static XPathNodeIterator Select(this XPathNavigator navigator, XPathExpression expression, params XPathVariable[] variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) + { + return navigator.Select(expression); + } + + XPathExpression compiled = expression.Clone(); // clone for thread-safety + var context = new DynamicContext(); + foreach (XPathVariable variable in variables) + { + context.AddVariable(variable.Name, variable.Value); + } + + compiled.SetContext(context); + return navigator.Select(compiled); } } diff --git a/src/Umbraco.Core/Xml/XPathVariable.cs b/src/Umbraco.Core/Xml/XPathVariable.cs index 9bfed8e98d..4c2d2d0f4e 100644 --- a/src/Umbraco.Core/Xml/XPathVariable.cs +++ b/src/Umbraco.Core/Xml/XPathVariable.cs @@ -1,32 +1,31 @@ -// source: mvpxml.codeplex.com +// source: mvpxml.codeplex.com -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// Represents a variable in an XPath query. +/// +/// The name must be foo in the constructor and $foo in the XPath query. +public class XPathVariable { /// - /// Represents a variable in an XPath query. + /// Initializes a new instance of the class with a name and a value. /// - /// The name must be foo in the constructor and $foo in the XPath query. - public class XPathVariable + /// + /// + public XPathVariable(string name, string value) { - /// - /// Gets or sets the name of the variable. - /// - public string Name { get; private set; } - - /// - /// Gets or sets the value of the variable. - /// - public string Value { get; private set; } - - /// - /// Initializes a new instance of the class with a name and a value. - /// - /// - /// - public XPathVariable(string name, string value) - { - Name = name; - Value = value; - } + Name = name; + Value = value; } + + /// + /// Gets or sets the name of the variable. + /// + public string Name { get; } + + /// + /// Gets or sets the value of the variable. + /// + public string Value { get; } } diff --git a/src/Umbraco.Core/Xml/XmlHelper.cs b/src/Umbraco.Core/Xml/XmlHelper.cs index 4de056e223..ad97120c93 100644 --- a/src/Umbraco.Core/Xml/XmlHelper.cs +++ b/src/Umbraco.Core/Xml/XmlHelper.cs @@ -1,392 +1,527 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using System.Xml; using System.Xml.XPath; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// The XmlHelper class contains general helper methods for working with xml in umbraco. +/// +public class XmlHelper { /// - /// The XmlHelper class contains general helper methods for working with xml in umbraco. + /// Creates or sets an attribute on the XmlNode if an Attributes collection is available /// - public class XmlHelper + /// + /// + /// + /// + public static void SetAttribute(XmlDocument xml, XmlNode n, string name, string value) { - /// - /// Creates or sets an attribute on the XmlNode if an Attributes collection is available - /// - /// - /// - /// - /// - public static void SetAttribute(XmlDocument xml, XmlNode n, string name, string value) + if (xml == null) { - if (xml == null) throw new ArgumentNullException(nameof(xml)); - if (n == null) throw new ArgumentNullException(nameof(n)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - if (n.Attributes == null) - { - return; - } - if (n.Attributes[name] == null) - { - var a = xml.CreateAttribute(name); - a.Value = value; - n.Attributes.Append(a); - } - else - { - n.Attributes[name]!.Value = value; - } + throw new ArgumentNullException(nameof(xml)); } - /// - /// Gets a value indicating whether a specified string contains only xml whitespace characters. - /// - /// The string. - /// true if the string contains only xml whitespace characters. - /// As per XML 1.1 specs, space, \t, \r and \n. - public static bool IsXmlWhitespace(string s) + if (n == null) { - // as per xml 1.1 specs - anything else is significant whitespace - s = s.Trim(Constants.CharArrays.XmlWhitespaceChars); - return s.Length == 0; + throw new ArgumentNullException(nameof(n)); } - /// - /// Creates a new XPathDocument from an xml string. - /// - /// The xml string. - /// An XPathDocument created from the xml string. - public static XPathDocument CreateXPathDocument(string xml) + if (name == null) { - return new XPathDocument(new XmlTextReader(new StringReader(xml))); + throw new ArgumentNullException(nameof(name)); } - /// - /// Tries to create a new XPathDocument from an xml string. - /// - /// The xml string. - /// The XPath document. - /// A value indicating whether it has been possible to create the document. - public static bool TryCreateXPathDocument(string xml, out XPathDocument? doc) + if (string.IsNullOrWhiteSpace(name)) { - try - { - doc = CreateXPathDocument(xml); - return true; - } - catch (Exception) - { - doc = null; - return false; - } + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Tries to create a new XPathDocument from a property value. - /// - /// The value of the property. - /// The XPath document. - /// A value indicating whether it has been possible to create the document. - /// The value can be anything... Performance-wise, this is bad. - public static bool TryCreateXPathDocumentFromPropertyValue(object value, out XPathDocument? doc) + if (n.Attributes == null) { - // DynamicNode.ConvertPropertyValueByDataType first cleans the value by calling - // XmlHelper.StripDashesInElementOrAttributeName - this is because the XML is - // to be returned as a DynamicXml and element names such as "value-item" are - // invalid and must be converted to "valueitem". But we don't have that sort of - // problem here - and we don't need to bother with dashes nor dots, etc. + return; + } - doc = null; - var xml = value as string; - if (xml == null) return false; // no a string - if (CouldItBeXml(xml) == false) return false; // string does not look like it's xml - if (IsXmlWhitespace(xml)) return false; // string is whitespace, xml-wise - if (TryCreateXPathDocument(xml, out doc) == false) return false; // string can't be parsed into xml + if (n.Attributes[name] == null) + { + XmlAttribute a = xml.CreateAttribute(name); + a.Value = value; + n.Attributes.Append(a); + } + else + { + n.Attributes[name]!.Value = value; + } + } - var nav = doc!.CreateNavigator(); - if (nav.MoveToFirstChild()) - { - //SD: This used to do this but the razor macros and the entire razor macros section is gone, it was all legacy, it seems this method isn't even - // used apart from for tests so don't think this matters. In any case, we no longer check for this! + /// + /// Gets a value indicating whether a specified string contains only xml whitespace characters. + /// + /// The string. + /// true if the string contains only xml whitespace characters. + /// As per XML 1.1 specs, space, \t, \r and \n. + public static bool IsXmlWhitespace(string s) + { + // as per xml 1.1 specs - anything else is significant whitespace + s = s.Trim(Constants.CharArrays.XmlWhitespaceChars); + return s.Length == 0; + } - //var name = nav.LocalName; // must not match an excluded tag - //if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) return true; - - return true; - } + /// + /// Creates a new XPathDocument from an xml string. + /// + /// The xml string. + /// An XPathDocument created from the xml string. + public static XPathDocument CreateXPathDocument(string xml) => + new XPathDocument(new XmlTextReader(new StringReader(xml))); + /// + /// Tries to create a new XPathDocument from an xml string. + /// + /// The xml string. + /// The XPath document. + /// A value indicating whether it has been possible to create the document. + public static bool TryCreateXPathDocument(string xml, out XPathDocument? doc) + { + try + { + doc = CreateXPathDocument(xml); + return true; + } + catch (Exception) + { doc = null; return false; } + } - - /// - /// Sorts the children of a parentNode. - /// - /// The parent node. - /// An XPath expression to select children of to sort. - /// A function returning the value to order the nodes by. - public static void SortNodes( - XmlNode parentNode, - string childNodesXPath, - Func orderBy) + /// + /// Tries to create a new XPathDocument from a property value. + /// + /// The value of the property. + /// The XPath document. + /// A value indicating whether it has been possible to create the document. + /// The value can be anything... Performance-wise, this is bad. + public static bool TryCreateXPathDocumentFromPropertyValue(object value, out XPathDocument? doc) + { + // DynamicNode.ConvertPropertyValueByDataType first cleans the value by calling + // XmlHelper.StripDashesInElementOrAttributeName - this is because the XML is + // to be returned as a DynamicXml and element names such as "value-item" are + // invalid and must be converted to "valueitem". But we don't have that sort of + // problem here - and we don't need to bother with dashes nor dots, etc. + doc = null; + if (value is not string xml) { - var sortedChildNodes = parentNode.SelectNodes(childNodesXPath)?.Cast() - .OrderBy(orderBy) - .ToArray(); - - // append child nodes to last position, in sort-order - // so all child nodes will go after the property nodes - if (sortedChildNodes is not null) - { - foreach (var node in sortedChildNodes) - parentNode.AppendChild(node); // moves the node to the last position - } + return false; // no a string } - - /// - /// Sorts a single child node of a parentNode. - /// - /// The parent node. - /// An XPath expression to select children of to sort. - /// The child node to sort. - /// A function returning the value to order the nodes by. - /// A value indicating whether sorting was needed. - /// Assuming all nodes but are sorted, this will move the node to - /// the right position without moving all the nodes (as SortNodes would do) - should improve perfs. - public static bool SortNode( - XmlNode parentNode, - string childNodesXPath, - XmlNode node, - Func orderBy) + if (CouldItBeXml(xml) == false) { - var nodeSortOrder = orderBy(node); - var childNodesAndOrder = parentNode.SelectNodes(childNodesXPath)?.Cast() - .Select(x => Tuple.Create(x, orderBy(x))).ToArray(); + return false; // string does not look like it's xml + } - // only one node = node is in the right place already, obviously - if (childNodesAndOrder is null || childNodesAndOrder.Length == 1) return false; + if (IsXmlWhitespace(xml)) + { + return false; // string is whitespace, xml-wise + } - // find the first node with a sortOrder > node.sortOrder - var i = 0; - while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder) - i++; + if (TryCreateXPathDocument(xml, out doc) == false) + { + return false; // string can't be parsed into xml + } - // if one was found - if (i < childNodesAndOrder.Length) + XPathNavigator nav = doc!.CreateNavigator(); + if (nav.MoveToFirstChild()) + { + // SD: This used to do this but the razor macros and the entire razor macros section is gone, it was all legacy, it seems this method isn't even + // used apart from for tests so don't think this matters. In any case, we no longer check for this! + + // var name = nav.LocalName; // must not match an excluded tag + // if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) return true; + return true; + } + + doc = null; + return false; + } + + /// + /// Sorts the children of a parentNode. + /// + /// The parent node. + /// An XPath expression to select children of to sort. + /// A function returning the value to order the nodes by. + public static void SortNodes( + XmlNode parentNode, + string childNodesXPath, + Func orderBy) + { + XmlNode[]? sortedChildNodes = parentNode.SelectNodes(childNodesXPath)?.Cast() + .OrderBy(orderBy) + .ToArray(); + + // append child nodes to last position, in sort-order + // so all child nodes will go after the property nodes + if (sortedChildNodes is not null) + { + foreach (XmlNode node in sortedChildNodes) { - // and node is just before, we're done already - // else we need to move it right before the node that was found - if (i == 0 || childNodesAndOrder[i - 1].Item1 != node) - { - parentNode.InsertBefore(node, childNodesAndOrder[i].Item1); - return true; - } - } - else // i == childNodesAndOrder.Length && childNodesAndOrder.Length > 1 - { - // and node is the last one, we're done already - // else we need to append it as the last one - // (and i > 1, see above) - if (childNodesAndOrder[i - 1].Item1 != node) - { - parentNode.AppendChild(node); - return true; - } + parentNode.AppendChild(node); // moves the node to the last position } + } + } + + /// + /// Sorts a single child node of a parentNode. + /// + /// The parent node. + /// An XPath expression to select children of to sort. + /// The child node to sort. + /// A function returning the value to order the nodes by. + /// A value indicating whether sorting was needed. + /// + /// Assuming all nodes but are sorted, this will move the node to + /// the right position without moving all the nodes (as SortNodes would do) - should improve perfs. + /// + public static bool SortNode( + XmlNode parentNode, + string childNodesXPath, + XmlNode node, + Func orderBy) + { + var nodeSortOrder = orderBy(node); + Tuple[]? childNodesAndOrder = parentNode.SelectNodes(childNodesXPath)?.Cast() + .Select(x => Tuple.Create(x, orderBy(x))).ToArray(); + + // only one node = node is in the right place already, obviously + if (childNodesAndOrder is null || childNodesAndOrder.Length == 1) + { return false; } - - /// - /// Opens a file as a XmlDocument. - /// - /// The relative file path. ie. /config/umbraco.config - /// - /// Returns a XmlDocument class - public static XmlDocument OpenAsXmlDocument(string filePath, IHostingEnvironment hostingEnvironment) + // find the first node with a sortOrder > node.sortOrder + var i = 0; + while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder) { - using (var reader = new XmlTextReader(hostingEnvironment.MapPathContentRoot(filePath)) {WhitespaceHandling = WhitespaceHandling.All}) - { - var xmlDoc = new XmlDocument(); - //Load the file into the XmlDocument - xmlDoc.Load(reader); + i++; + } - return xmlDoc; + // if one was found + if (i < childNodesAndOrder.Length) + { + // and node is just before, we're done already + // else we need to move it right before the node that was found + if (i == 0 || childNodesAndOrder[i - 1].Item1 != node) + { + parentNode.InsertBefore(node, childNodesAndOrder[i].Item1); + return true; + } + } + else // i == childNodesAndOrder.Length && childNodesAndOrder.Length > 1 + { + // and node is the last one, we're done already + // else we need to append it as the last one + // (and i > 1, see above) + if (childNodesAndOrder[i - 1].Item1 != node) + { + parentNode.AppendChild(node); + return true; } } - /// - /// creates a XmlAttribute with the specified name and value - /// - /// The xmldocument. - /// The name of the attribute. - /// The value of the attribute. - /// a XmlAttribute - public static XmlAttribute AddAttribute(XmlDocument xd, string name, string value) - { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + return false; + } - var temp = xd.CreateAttribute(name); - temp.Value = value; - return temp; + /// + /// Opens a file as a XmlDocument. + /// + /// The relative file path. ie. /config/umbraco.config + /// + /// Returns a XmlDocument class + public static XmlDocument OpenAsXmlDocument(string filePath, IHostingEnvironment hostingEnvironment) + { + using (var reader = + new XmlTextReader(hostingEnvironment.MapPathContentRoot(filePath)) + { + WhitespaceHandling = WhitespaceHandling.All, + }) + { + var xmlDoc = new XmlDocument(); + + // Load the file into the XmlDocument + xmlDoc.Load(reader); + + return xmlDoc; + } + } + + /// + /// creates a XmlAttribute with the specified name and value + /// + /// The xmldocument. + /// The name of the attribute. + /// The value of the attribute. + /// a XmlAttribute + public static XmlAttribute AddAttribute(XmlDocument xd, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); } - /// - /// Creates a text XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node name. - /// The node value. - /// a XmlNode - public static XmlNode AddTextNode(XmlDocument xd, string name, string value) + if (name == null) { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - var temp = xd.CreateNode(XmlNodeType.Element, name, ""); - temp.AppendChild(xd.CreateTextNode(value)); - return temp; + throw new ArgumentNullException(nameof(name)); } - /// - /// Sets or Creates a text XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node to set or create the child text node on - /// The node name. - /// The node value. - /// a XmlNode - public static XmlNode SetTextNode(XmlDocument xd, XmlNode parent, string name, string value) + if (string.IsNullOrWhiteSpace(name)) { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (parent == null) throw new ArgumentNullException(nameof(parent)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - var child = parent.SelectSingleNode(name); - if (child != null) - { - child.InnerText = value; - return child; - } - return AddTextNode(xd, name, value); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Sets or creates an Xml node from its inner Xml. - /// - /// The xmldocument. - /// The node to set or create the child text node on - /// The node name. - /// The node inner Xml. - /// a XmlNode - public static XmlNode SetInnerXmlNode(XmlDocument xd, XmlNode parent, string name, string value) - { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (parent == null) throw new ArgumentNullException(nameof(parent)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + XmlAttribute temp = xd.CreateAttribute(name); + temp.Value = value; + return temp; + } - var child = parent.SelectSingleNode(name) ?? xd.CreateNode(XmlNodeType.Element, name, ""); - child.InnerXml = value; + /// + /// Creates a text XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node name. + /// The node value. + /// a XmlNode + public static XmlNode AddTextNode(XmlDocument xd, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + XmlNode temp = xd.CreateNode(XmlNodeType.Element, name, string.Empty); + temp.AppendChild(xd.CreateTextNode(value)); + return temp; + } + + /// + /// Sets or Creates a text XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node to set or create the child text node on + /// The node name. + /// The node value. + /// a XmlNode + public static XmlNode SetTextNode(XmlDocument xd, XmlNode parent, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); + } + + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + XmlNode? child = parent.SelectSingleNode(name); + if (child != null) + { + child.InnerText = value; return child; } - /// - /// Creates a cdata XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node name. - /// The node value. - /// A XmlNode - public static XmlNode AddCDataNode(XmlDocument xd, string name, string value) - { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + return AddTextNode(xd, name, value); + } - var temp = xd.CreateNode(XmlNodeType.Element, name, ""); - temp.AppendChild(xd.CreateCDataSection(value)); - return temp; + /// + /// Sets or creates an Xml node from its inner Xml. + /// + /// The xmldocument. + /// The node to set or create the child text node on + /// The node name. + /// The node inner Xml. + /// a XmlNode + public static XmlNode SetInnerXmlNode(XmlDocument xd, XmlNode parent, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); } - /// - /// Sets or Creates a cdata XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node to set or create the child text node on - /// The node name. - /// The node value. - /// a XmlNode - public static XmlNode SetCDataNode(XmlDocument xd, XmlNode parent, string name, string value) + if (parent == null) { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (parent == null) throw new ArgumentNullException(nameof(parent)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - var child = parent.SelectSingleNode(name); - if (child != null) - { - child.InnerXml = ""; - return child; - } - return AddCDataNode(xd, name, value); + throw new ArgumentNullException(nameof(parent)); } - /// - /// Gets the value of a XmlNode - /// - /// The XmlNode. - /// the value as a string - public static string GetNodeValue(XmlNode n) + if (name == null) { - var value = string.Empty; - if (n == null || n.FirstChild == null) - return value; - value = n.FirstChild.Value ?? n.InnerXml; - return value.Replace("", "", "]]>"); + throw new ArgumentNullException(nameof(name)); } - /// - /// Determines whether the specified string appears to be XML. - /// - /// The XML string. - /// - /// true if the specified string appears to be XML; otherwise, false. - /// - public static bool CouldItBeXml(string? xml) + if (string.IsNullOrWhiteSpace(name)) { - if (string.IsNullOrEmpty(xml)) return false; - - xml = xml.Trim(); - return xml.StartsWith("<") && xml.EndsWith(">") && xml.Contains('/'); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Return a dictionary of attributes found for a string based tag - /// - /// - /// - public static Dictionary GetAttributesFromElement(string tag) + XmlNode child = parent.SelectSingleNode(name) ?? xd.CreateNode(XmlNodeType.Element, name, string.Empty); + child.InnerXml = value; + return child; + } + + /// + /// Creates a cdata XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node name. + /// The node value. + /// A XmlNode + public static XmlNode AddCDataNode(XmlDocument xd, string name, string value) + { + if (xd == null) { - var m = - Regex.Matches(tag, "(?\\S*)=\"(?[^\"]*)\"", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - // fix for issue 14862: return lowercase attributes for case insensitive matching - var d = m.Cast().ToDictionary(attributeSet => attributeSet.Groups["attributeName"].Value.ToString().ToLower(), attributeSet => attributeSet.Groups["attributeValue"].Value.ToString()); - return d; + throw new ArgumentNullException(nameof(xd)); } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + XmlNode temp = xd.CreateNode(XmlNodeType.Element, name, string.Empty); + temp.AppendChild(xd.CreateCDataSection(value)); + return temp; + } + + /// + /// Sets or Creates a cdata XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node to set or create the child text node on + /// The node name. + /// The node value. + /// a XmlNode + public static XmlNode SetCDataNode(XmlDocument xd, XmlNode parent, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); + } + + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + XmlNode? child = parent.SelectSingleNode(name); + if (child != null) + { + child.InnerXml = ""; + return child; + } + + return AddCDataNode(xd, name, value); + } + + /// + /// Gets the value of a XmlNode + /// + /// The XmlNode. + /// the value as a string + public static string GetNodeValue(XmlNode n) + { + var value = string.Empty; + if (n == null || n.FirstChild == null) + { + return value; + } + + value = n.FirstChild.Value ?? n.InnerXml; + return value.Replace("", "", "]]>"); + } + + /// + /// Determines whether the specified string appears to be XML. + /// + /// The XML string. + /// + /// true if the specified string appears to be XML; otherwise, false. + /// + public static bool CouldItBeXml(string? xml) + { + if (string.IsNullOrEmpty(xml)) + { + return false; + } + + xml = xml.Trim(); + return xml.StartsWith("<") && xml.EndsWith(">") && xml.Contains('/'); + } + + /// + /// Return a dictionary of attributes found for a string based tag + /// + /// + /// + public static Dictionary GetAttributesFromElement(string tag) + { + MatchCollection m = + Regex.Matches(tag, "(?\\S*)=\"(?[^\"]*)\"", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + // fix for issue 14862: return lowercase attributes for case insensitive matching + var d = m.ToDictionary( + attributeSet => attributeSet.Groups["attributeName"].Value.ToString().ToLower(), + attributeSet => attributeSet.Groups["attributeValue"].Value.ToString()); + return d; } } diff --git a/src/Umbraco.Core/Xml/XmlNamespaces.cs b/src/Umbraco.Core/Xml/XmlNamespaces.cs index 1721de253f..55a23736ff 100644 --- a/src/Umbraco.Core/Xml/XmlNamespaces.cs +++ b/src/Umbraco.Core/Xml/XmlNamespaces.cs @@ -1,41 +1,40 @@ -// source: mvpxml.codeplex.com +// source: mvpxml.codeplex.com -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// Provides public constants for wellknown XML namespaces. +/// +/// Author: Daniel Cazzulino, blog +public static class XmlNamespaces { /// - /// Provides public constants for wellknown XML namespaces. + /// The public XML 1.0 namespace. /// - /// Author: Daniel Cazzulino, blog - public static class XmlNamespaces - { - /// - /// The public XML 1.0 namespace. - /// - /// See http://www.w3.org/TR/2004/REC-xml-20040204/ - public const string Xml = "http://www.w3.org/XML/1998/namespace"; + /// See http://www.w3.org/TR/2004/REC-xml-20040204/ + public const string Xml = "http://www.w3.org/XML/1998/namespace"; - /// - /// Public Xml Namespaces specification namespace. - /// - /// See http://www.w3.org/TR/REC-xml-names/ - public const string XmlNs = "http://www.w3.org/2000/xmlns/"; + /// + /// Public Xml Namespaces specification namespace. + /// + /// See http://www.w3.org/TR/REC-xml-names/ + public const string XmlNs = "http://www.w3.org/2000/xmlns/"; - /// - /// Public Xml Namespaces prefix. - /// - /// See http://www.w3.org/TR/REC-xml-names/ - public const string XmlNsPrefix = "xmlns"; + /// + /// Public Xml Namespaces prefix. + /// + /// See http://www.w3.org/TR/REC-xml-names/ + public const string XmlNsPrefix = "xmlns"; - /// - /// XML Schema instance namespace. - /// - /// See http://www.w3.org/TR/xmlschema-1/ - public const string Xsi = "http://www.w3.org/2001/XMLSchema-instance"; + /// + /// XML Schema instance namespace. + /// + /// See http://www.w3.org/TR/xmlschema-1/ + public const string Xsi = "http://www.w3.org/2001/XMLSchema-instance"; - /// - /// XML 1.0 Schema namespace. - /// - /// See http://www.w3.org/TR/xmlschema-1/ - public const string Xsd = "http://www.w3.org/2001/XMLSchema"; - } + /// + /// XML 1.0 Schema namespace. + /// + /// See http://www.w3.org/TR/xmlschema-1/ + public const string Xsd = "http://www.w3.org/2001/XMLSchema"; } diff --git a/src/Umbraco.Core/Xml/XmlNodeListFactory.cs b/src/Umbraco.Core/Xml/XmlNodeListFactory.cs index 29797fc59a..17c2f41843 100644 --- a/src/Umbraco.Core/Xml/XmlNodeListFactory.cs +++ b/src/Umbraco.Core/Xml/XmlNodeListFactory.cs @@ -1,178 +1,166 @@ -using System; -using System.Collections.Generic; +using System.Collections; using System.Xml; using System.Xml.XPath; // source: mvpxml.codeplex.com +namespace Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core.Xml +public class XmlNodeListFactory { - public class XmlNodeListFactory + private XmlNodeListFactory() { - private XmlNodeListFactory() { } + } - #region Public members + #region Public members + + /// + /// Creates an instance of a that allows + /// enumerating elements in the iterator. + /// + /// + /// The result of a previous node selection + /// through an query. + /// + /// An initialized list ready to be enumerated. + /// + /// The underlying XML store used to issue the query must be + /// an object inheriting , such as + /// . + /// + public static XmlNodeList CreateNodeList(XPathNodeIterator? iterator) => new XmlNodeListIterator(iterator); + + #endregion Public members + + #region XmlNodeListIterator + + private class XmlNodeListIterator : XmlNodeList + { + private readonly XPathNodeIterator? _iterator; + private readonly IList _nodes = new List(); + + public XmlNodeListIterator(XPathNodeIterator? iterator) => _iterator = iterator?.Clone(); + + public override int Count + { + get + { + if (!Done) + { + ReadToEnd(); + } + + return _nodes.Count; + } + } /// - /// Creates an instance of a that allows - /// enumerating elements in the iterator. + /// Flags that the iterator has been consumed. /// - /// The result of a previous node selection - /// through an query. - /// An initialized list ready to be enumerated. - /// The underlying XML store used to issue the query must be - /// an object inheriting , such as - /// . - public static XmlNodeList CreateNodeList(XPathNodeIterator? iterator) + private bool Done { get; set; } + + /// + /// Current count of nodes in the iterator (read so far). + /// + private int CurrentPosition => _nodes.Count; + + public override IEnumerator GetEnumerator() => new XmlNodeListEnumerator(this); + + public override XmlNode? Item(int index) { - return new XmlNodeListIterator(iterator); + if (index >= _nodes.Count) + { + ReadTo(index); + } + + // Compatible behavior with .NET + if (index >= _nodes.Count || index < 0) + { + return null; + } + + return _nodes[index]; } - #endregion Public members - - #region XmlNodeListIterator - - private class XmlNodeListIterator : XmlNodeList + /// + /// Reads the entire iterator. + /// + private void ReadToEnd() { - readonly XPathNodeIterator? _iterator; - readonly IList _nodes = new List(); - - public XmlNodeListIterator(XPathNodeIterator? iterator) + while (_iterator is not null && _iterator.MoveNext()) { - _iterator = iterator?.Clone(); - } - - public override System.Collections.IEnumerator GetEnumerator() - { - return new XmlNodeListEnumerator(this); - } - - public override XmlNode? Item(int index) - { - - if (index >= _nodes.Count) - ReadTo(index); - // Compatible behavior with .NET - if (index >= _nodes.Count || index < 0) - return null; - return _nodes[index]; - } - - public override int Count - { - get + // Check IHasXmlNode interface. + if (_iterator.Current is not IHasXmlNode node) { - if (!_done) ReadToEnd(); - return _nodes.Count; + throw new ArgumentException("IHasXmlNode is missing."); } + + _nodes.Add(node.GetNode()); } + Done = true; + } - /// - /// Reads the entire iterator. - /// - private void ReadToEnd() + /// + /// Reads up to the specified index, or until the + /// iterator is consumed. + /// + private void ReadTo(int to) + { + while (_nodes.Count <= to) { - while (_iterator is not null && _iterator.MoveNext()) + if (_iterator is not null && _iterator.MoveNext()) { - var node = _iterator.Current as IHasXmlNode; // Check IHasXmlNode interface. - if (node == null) + if (_iterator.Current is not IHasXmlNode node) + { throw new ArgumentException("IHasXmlNode is missing."); + } + _nodes.Add(node.GetNode()); } - _done = true; - } - - /// - /// Reads up to the specified index, or until the - /// iterator is consumed. - /// - private void ReadTo(int to) - { - while (_nodes.Count <= to) + else { - if (_iterator is not null && _iterator.MoveNext()) - { - var node = _iterator.Current as IHasXmlNode; - // Check IHasXmlNode interface. - if (node == null) - throw new ArgumentException("IHasXmlNode is missing."); - _nodes.Add(node.GetNode()); - } - else - { - _done = true; - return; - } + Done = true; + return; } } - - /// - /// Flags that the iterator has been consumed. - /// - private bool Done - { - get { return _done; } - } - - bool _done; - - /// - /// Current count of nodes in the iterator (read so far). - /// - private int CurrentPosition - { - get { return _nodes.Count; } - } - - #region XmlNodeListEnumerator - - private class XmlNodeListEnumerator : System.Collections.IEnumerator - { - readonly XmlNodeListIterator _iterator; - int _position = -1; - - public XmlNodeListEnumerator(XmlNodeListIterator iterator) - { - _iterator = iterator; - } - - #region IEnumerator Members - - void System.Collections.IEnumerator.Reset() - { - _position = -1; - } - - - bool System.Collections.IEnumerator.MoveNext() - { - _position++; - _iterator.ReadTo(_position); - - // If we reached the end and our index is still - // bigger, there are no more items. - if (_iterator.Done && _position >= _iterator.CurrentPosition) - return false; - - return true; - } - - object? System.Collections.IEnumerator.Current - { - get - { - return _iterator[_position]; - } - } - - #endregion - } - - #endregion XmlNodeListEnumerator } - #endregion XmlNodeListIterator + #region XmlNodeListEnumerator + + private class XmlNodeListEnumerator : IEnumerator + { + private readonly XmlNodeListIterator _iterator; + private int _position = -1; + + public XmlNodeListEnumerator(XmlNodeListIterator iterator) => _iterator = iterator; + + object? IEnumerator.Current => _iterator[_position]; + + #region IEnumerator Members + + void IEnumerator.Reset() => _position = -1; + + bool IEnumerator.MoveNext() + { + _position++; + _iterator.ReadTo(_position); + + // If we reached the end and our index is still + // bigger, there are no more items. + if (_iterator.Done && _position >= _iterator.CurrentPosition) + { + return false; + } + + return true; + } + + #endregion + } + + #endregion XmlNodeListEnumerator } + + #endregion XmlNodeListIterator } diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs index 9cb667ad44..71c0929e39 100644 --- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs +++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs @@ -1,384 +1,414 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Examine; using Examine.Search; -using Examine.Search; using Lucene.Net.QueryParsers.Classic; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher { - public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher + private readonly AppCaches _appCaches; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IEntityService _entityService; + private readonly IExamineManager _examineManager; + private readonly ILocalizationService _languageService; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IUmbracoTreeSearcherFields _treeSearcherFields; + private readonly IUmbracoMapper _umbracoMapper; + + public BackOfficeExamineSearcher( + IExamineManager examineManager, + ILocalizationService languageService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IEntityService entityService, + IUmbracoTreeSearcherFields treeSearcherFields, + AppCaches appCaches, + IUmbracoMapper umbracoMapper, + IPublishedUrlProvider publishedUrlProvider) { - private readonly IExamineManager _examineManager; - private readonly ILocalizationService _languageService; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IEntityService _entityService; - private readonly IUmbracoTreeSearcherFields _treeSearcherFields; - private readonly AppCaches _appCaches; - private readonly IUmbracoMapper _umbracoMapper; - private readonly IPublishedUrlProvider _publishedUrlProvider; + _examineManager = examineManager; + _languageService = languageService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _entityService = entityService; + _treeSearcherFields = treeSearcherFields; + _appCaches = appCaches; + _umbracoMapper = umbracoMapper; + _publishedUrlProvider = publishedUrlProvider; + } - public BackOfficeExamineSearcher(IExamineManager examineManager, - ILocalizationService languageService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IEntityService entityService, - IUmbracoTreeSearcherFields treeSearcherFields, - AppCaches appCaches, - IUmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) + public IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string? searchFrom = null, + bool ignoreUserStartNodes = false) + { + var sb = new StringBuilder(); + + string type; + var indexName = Constants.UmbracoIndexes.InternalIndexName; + var fields = _treeSearcherFields.GetBackOfficeFields().ToList(); + + ISet fieldsToLoad = new HashSet(_treeSearcherFields.GetBackOfficeFieldsToLoad()); + + // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string + // manipulation for things like start paths, member types, etc... + //if (Examine.ExamineExtensions.TryParseLuceneQuery(query)) + //{ + + //} + + //special GUID check since if a user searches on one specifically we need to escape it + if (Guid.TryParse(query, out Guid g)) { - _examineManager = examineManager; - _languageService = languageService; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _entityService = entityService; - _treeSearcherFields = treeSearcherFields; - _appCaches = appCaches; - _umbracoMapper = umbracoMapper; - _publishedUrlProvider = publishedUrlProvider; + query = "\"" + g + "\""; } - public IEnumerable Search(string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false) + IUser? currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; + + switch (entityType) { - var sb = new StringBuilder(); - - string type; - var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _treeSearcherFields.GetBackOfficeFields().ToList(); - - ISet fieldsToLoad = new HashSet(_treeSearcherFields.GetBackOfficeFieldsToLoad()); - - // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string - // manipulation for things like start paths, member types, etc... - //if (Examine.ExamineExtensions.TryParseLuceneQuery(query)) - //{ - - //} - - //special GUID check since if a user searches on one specifically we need to escape it - if (Guid.TryParse(query, out var g)) - { - query = "\"" + g.ToString() + "\""; - } - - var currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - - switch (entityType) - { - case UmbracoEntityTypes.Member: - indexName = Constants.UmbracoIndexes.MembersIndexName; - type = "member"; - fields.AddRange(_treeSearcherFields.GetBackOfficeMembersFields()); - foreach(var field in _treeSearcherFields.GetBackOfficeMembersFieldsToLoad()) - { - fieldsToLoad.Add(field); - } - - if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") - { - sb.Append("+__NodeTypeAlias:"); - sb.Append(searchFrom); - sb.Append(" "); - } - break; - case UmbracoEntityTypes.Media: - type = "media"; - fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields()); - foreach (var field in _treeSearcherFields.GetBackOfficeMediaFieldsToLoad()) - { - fieldsToLoad.Add(field); - } - - var allMediaStartNodes = currentUser != null - ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches) - : Array.Empty(); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); - break; - case UmbracoEntityTypes.Document: - type = "content"; - fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields()); - foreach (var field in _treeSearcherFields.GetBackOfficeDocumentFieldsToLoad()) - { - fieldsToLoad.Add(field); - } - var allContentStartNodes = currentUser != null - ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches) - : Array.Empty(); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); - break; - default: - throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) + " currently does not support searching against object type " + entityType); - } - - if (!_examineManager.TryGetIndex(indexName, out var index)) - throw new InvalidOperationException("No index found by name " + indexName); - - if (!BuildQuery(sb, query, searchFrom, fields, type)) - { - totalFound = 0; - return Enumerable.Empty(); - } - - var result = index.Searcher - .CreateQuery() - .NativeQuery(sb.ToString()) - .SelectFields(fieldsToLoad) - //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested - .Execute(QueryOptions.SkipTake(Convert.ToInt32(pageSize * pageIndex), pageSize)); - - totalFound = result.TotalItemCount; - - return result; - } - - private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List fields, string type) - { - //build a lucene query: - // the nodeName will be boosted 10x without wildcards - // then nodeName will be matched normally with wildcards - // the rest will be normal without wildcards - - var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode.ToLowerInvariant()).ToList(); - - // the chars [*-_] in the query will mess everything up so let's remove those - query = Regex.Replace(query, "[\\*\\-_]", ""); - - //check if text is surrounded by single or double quotes, if so, then exact match - var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$") - || Regex.IsMatch(query, "^\'.*?\'$"); - - if (surroundedByQuotes) - { - //strip quotes, escape string, the replace again - query = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); - - query = QueryParser.Escape(query); - - //nothing to search - if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) + case UmbracoEntityTypes.Member: + indexName = Constants.UmbracoIndexes.MembersIndexName; + type = "member"; + fields.AddRange(_treeSearcherFields.GetBackOfficeMembersFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeMembersFieldsToLoad()) { - return false; + fieldsToLoad.Add(field); } - //update the query with the query term - if (query.IsNullOrWhiteSpace() == false) + if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && + searchFrom.Trim() != "-1") { - //add back the surrounding quotes - query = string.Format("{0}{1}{0}", "\"", query); + sb.Append("+__NodeTypeAlias:"); + sb.Append(searchFrom); + sb.Append(" "); + } - sb.Append("+("); + break; + case UmbracoEntityTypes.Media: + type = "media"; + fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeMediaFieldsToLoad()) + { + fieldsToLoad.Add(field); + } - AppendNodeNamePhraseWithBoost(sb, query, allLangs); + var allMediaStartNodes = currentUser != null + ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches) + : Array.Empty(); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); + break; + case UmbracoEntityTypes.Document: + type = "content"; + fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeDocumentFieldsToLoad()) + { + fieldsToLoad.Add(field); + } - foreach (var f in fields) - { - //additional fields normally - sb.Append(f); - sb.Append(": ("); - sb.Append(query); - sb.Append(") "); - } + var allContentStartNodes = currentUser != null + ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches) + : Array.Empty(); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); + break; + default: + throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) + + " currently does not support searching against object type " + + entityType); + } + if (!_examineManager.TryGetIndex(indexName, out IIndex? index)) + { + throw new InvalidOperationException("No index found by name " + indexName); + } + + if (!BuildQuery(sb, query, searchFrom, fields, type)) + { + totalFound = 0; + return Enumerable.Empty(); + } + + ISearchResults? result = index.Searcher + .CreateQuery() + .NativeQuery(sb.ToString()) + .SelectFields(fieldsToLoad) + //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested + .Execute(QueryOptions.SkipTake(Convert.ToInt32(pageSize * pageIndex), pageSize)); + + totalFound = result.TotalItemCount; + + return result; + } + + private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List fields, string type) + { + //build a lucene query: + // the nodeName will be boosted 10x without wildcards + // then nodeName will be matched normally with wildcards + // the rest will be normal without wildcards + + var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode.ToLowerInvariant()).ToList(); + + // the chars [*-_] in the query will mess everything up so let's remove those + query = Regex.Replace(query, "[\\*\\-_]", string.Empty); + + //check if text is surrounded by single or double quotes, if so, then exact match + var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$") + || Regex.IsMatch(query, "^\'.*?\'$"); + + if (surroundedByQuotes) + { + //strip quotes, escape string, the replace again + query = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); + + query = QueryParserBase.Escape(query); + + //nothing to search + if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) + { + return false; + } + + //update the query with the query term + if (query.IsNullOrWhiteSpace() == false) + { + //add back the surrounding quotes + query = string.Format("{0}{1}{0}", "\"", query); + + sb.Append("+("); + + AppendNodeNamePhraseWithBoost(sb, query, allLangs); + + foreach (var f in fields) + { + //additional fields normally + sb.Append(f); + sb.Append(": ("); + sb.Append(query); sb.Append(") "); } + + sb.Append(") "); } - else + } + else + { + var trimmed = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); + + //nothing to search + if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) { - var trimmed = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote); + return false; + } - //nothing to search - if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) + //update the query with the query term + if (trimmed.IsNullOrWhiteSpace() == false) + { + query = QueryParserBase.Escape(query); + + var querywords = query.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries); + + sb.Append("+("); + + AppendNodeNameExactWithBoost(sb, query, allLangs); + + AppendNodeNameWithWildcards(sb, querywords, allLangs); + + foreach (var f in fields) { - return false; - } + var queryWordsReplaced = new string[querywords.Length]; - //update the query with the query term - if (trimmed.IsNullOrWhiteSpace() == false) - { - query = QueryParser.Escape(query); - - var querywords = query.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries); - - sb.Append("+("); - - AppendNodeNameExactWithBoost(sb, query, allLangs); - - AppendNodeNameWithWildcards(sb, querywords, allLangs); - - foreach (var f in fields) + // when searching file names containing hyphens we need to replace the hyphens with spaces + if (f.Equals(UmbracoExamineFieldNames.UmbracoFileFieldName)) { - var queryWordsReplaced = new string[querywords.Length]; - - // when searching file names containing hyphens we need to replace the hyphens with spaces - if (f.Equals(UmbracoExamineFieldNames.UmbracoFileFieldName)) + for (var index = 0; index < querywords.Length; index++) { - for (var index = 0; index < querywords.Length; index++) - { - queryWordsReplaced[index] = querywords[index].Replace("\\-", " ").Replace("_", " ").Trim(" "); - } + queryWordsReplaced[index] = + querywords[index].Replace("\\-", " ").Replace("_", " ").Trim(" "); } - else - { - queryWordsReplaced = querywords; - } - - //additional fields normally - sb.Append(f); - sb.Append(":"); - sb.Append("("); - foreach (var w in queryWordsReplaced) - { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(")"); - sb.Append(" "); + } + else + { + queryWordsReplaced = querywords; } - sb.Append(") "); + //additional fields normally + sb.Append(f); + sb.Append(":"); + sb.Append("("); + foreach (var w in queryWordsReplaced) + { + sb.Append(w.ToLower()); + sb.Append("* "); + } + + sb.Append(")"); + sb.Append(" "); } + + sb.Append(") "); } - - //must match index type - sb.Append("+__IndexType:"); - sb.Append(type); - - return true; } - private void AppendNodeNamePhraseWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + //must match index type + sb.Append("+__IndexType:"); + sb.Append(type); + + return true; + } + + private void AppendNodeNamePhraseWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + { + //node name exactly boost x 10 + sb.Append("nodeName: ("); + sb.Append(query.ToLower()); + sb.Append(")^10.0 "); + + //also search on all variant node names + foreach (var lang in allLangs) { //node name exactly boost x 10 - sb.Append("nodeName: ("); + sb.Append($"nodeName_{lang}: ("); sb.Append(query.ToLower()); sb.Append(")^10.0 "); - - //also search on all variant node names - foreach (var lang in allLangs) - { - //node name exactly boost x 10 - sb.Append($"nodeName_{lang}: ("); - sb.Append(query.ToLower()); - sb.Append(")^10.0 "); - } } + } - private void AppendNodeNameExactWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + private void AppendNodeNameExactWithBoost(StringBuilder sb, string query, IEnumerable allLangs) + { + //node name exactly boost x 10 + sb.Append("nodeName:"); + sb.Append("\""); + sb.Append(query.ToLower()); + sb.Append("\""); + sb.Append("^10.0 "); + //also search on all variant node names + foreach (var lang in allLangs) { //node name exactly boost x 10 - sb.Append("nodeName:"); + sb.Append($"nodeName_{lang}:"); sb.Append("\""); sb.Append(query.ToLower()); sb.Append("\""); sb.Append("^10.0 "); - //also search on all variant node names - foreach (var lang in allLangs) - { - //node name exactly boost x 10 - sb.Append($"nodeName_{lang}:"); - sb.Append("\""); - sb.Append(query.ToLower()); - sb.Append("\""); - sb.Append("^10.0 "); - } + } + } + + private void AppendNodeNameWithWildcards(StringBuilder sb, string[] querywords, IEnumerable allLangs) + { + //node name normally with wildcards + sb.Append("nodeName:"); + sb.Append("("); + foreach (var w in querywords) + { + sb.Append(w.ToLower()); + sb.Append("* "); } - private void AppendNodeNameWithWildcards(StringBuilder sb, string[] querywords, IEnumerable allLangs) + sb.Append(") "); + //also search on all variant node names + foreach (var lang in allLangs) { //node name normally with wildcards - sb.Append("nodeName:"); + sb.Append($"nodeName_{lang}:"); sb.Append("("); foreach (var w in querywords) { sb.Append(w.ToLower()); sb.Append("* "); } + sb.Append(") "); - //also search on all variant node names - foreach (var lang in allLangs) - { - //node name normally with wildcards - sb.Append($"nodeName_{lang}:"); - sb.Append("("); - foreach (var w in querywords) - { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(") "); - } - } - - private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[]? startNodeIds, string? searchFrom, bool ignoreUserStartNodes, IEntityService entityService) - { - if (sb == null) throw new ArgumentNullException(nameof(sb)); - if (entityService == null) throw new ArgumentNullException(nameof(entityService)); - - UdiParser.TryParse(searchFrom, true, out var udi); - searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString(); - - var entityPath = int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) && searchFromId > 0 - ? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault() - : null; - if (entityPath != null) - { - // find... only what's underneath - sb.Append("+__Path:"); - AppendPath(sb, entityPath.Path, false); - sb.Append(" "); - } - else if (startNodeIds?.Length == 0) - { - // make sure we don't find anything - sb.Append("+__Path:none "); - } - else if (startNodeIds?.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction - { - var entityPaths = entityService.GetAllPaths(objectType, startNodeIds); - - // for each start node, find the start node, and what's underneath - // +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...) - sb.Append("+__Path:("); - var first = true; - foreach (var ep in entityPaths) - { - if (first) - first = false; - else - sb.Append(" "); - AppendPath(sb, ep.Path, true); - } - sb.Append(") "); - } - } - - private void AppendPath(StringBuilder sb, string path, bool includeThisNode) - { - path = path.Replace("-", "\\-").Replace(",", "\\,"); - if (includeThisNode) - { - sb.Append(path); - sb.Append(" "); - } - sb.Append(path); - sb.Append("\\,*"); } } + + private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[]? startNodeIds, string? searchFrom, bool ignoreUserStartNodes, IEntityService entityService) + { + if (sb == null) + { + throw new ArgumentNullException(nameof(sb)); + } + + if (entityService == null) + { + throw new ArgumentNullException(nameof(entityService)); + } + + UdiParser.TryParse(searchFrom, true, out Udi? udi); + searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString(); + + TreeEntityPath? entityPath = + int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) && + searchFromId > 0 + ? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault() + : null; + if (entityPath != null) + { + // find... only what's underneath + sb.Append("+__Path:"); + AppendPath(sb, entityPath.Path, false); + sb.Append(" "); + } + else if (startNodeIds?.Length == 0) + { + // make sure we don't find anything + sb.Append("+__Path:none "); + } + else if (startNodeIds?.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction + { + IEnumerable entityPaths = entityService.GetAllPaths(objectType, startNodeIds); + + // for each start node, find the start node, and what's underneath + // +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...) + sb.Append("+__Path:("); + var first = true; + foreach (TreeEntityPath ep in entityPaths) + { + if (first) + { + first = false; + } + else + { + sb.Append(" "); + } + + AppendPath(sb, ep.Path, true); + } + + sb.Append(") "); + } + } + + private void AppendPath(StringBuilder sb, string path, bool includeThisNode) + { + path = path.Replace("-", "\\-").Replace(",", "\\,"); + if (includeThisNode) + { + sb.Append(path); + sb.Append(" "); + } + + sb.Append(path); + sb.Append("\\,*"); + } } diff --git a/src/Umbraco.Examine.Lucene/CompatibilitySuppressions.xml b/src/Umbraco.Examine.Lucene/CompatibilitySuppressions.xml new file mode 100644 index 0000000000..681c7cdab5 --- /dev/null +++ b/src/Umbraco.Examine.Lucene/CompatibilitySuppressions.xml @@ -0,0 +1,10 @@ + + + + CP0008 + T:Umbraco.Cms.Infrastructure.Examine.UmbracoContentIndex + lib/net6.0/Umbraco.Examine.Lucene.dll + lib/net6.0/Umbraco.Examine.Lucene.dll + true + + \ No newline at end of file diff --git a/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs b/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs index 108aad06bc..f58dffacac 100644 --- a/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs +++ b/src/Umbraco.Examine.Lucene/ConfigurationEnabledDirectoryFactory.cs @@ -1,65 +1,63 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; using Examine; using Examine.Lucene.Directories; using Examine.Lucene.Providers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Directory = Lucene.Net.Store.Directory; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An Examine directory factory implementation based on configured values +/// +public class ConfigurationEnabledDirectoryFactory : DirectoryFactoryBase { - /// - /// An Examine directory factory implementation based on configured values - /// - public class ConfigurationEnabledDirectoryFactory : DirectoryFactoryBase + private readonly IApplicationRoot _applicationRoot; + private readonly IServiceProvider _services; + private readonly IndexCreatorSettings _settings; + private IDirectoryFactory? _directoryFactory; + + public ConfigurationEnabledDirectoryFactory( + IServiceProvider services, + IOptions settings, + IApplicationRoot applicationRoot) { - private readonly IServiceProvider _services; - private readonly IApplicationRoot _applicationRoot; - private readonly IndexCreatorSettings _settings; - private IDirectoryFactory? _directoryFactory; + _services = services; + _applicationRoot = applicationRoot; + _settings = settings.Value; + } - public ConfigurationEnabledDirectoryFactory( - IServiceProvider services, - IOptions settings, - IApplicationRoot applicationRoot) + protected override Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) + { + _directoryFactory = CreateFactory(); + return _directoryFactory.CreateDirectory(luceneIndex, forceUnlock); + } + + /// + /// Creates a directory factory based on the configured value and ensures that + /// + private IDirectoryFactory CreateFactory() + { + DirectoryInfo dirInfo = _applicationRoot.ApplicationRoot; + + if (!dirInfo.Exists) { - _services = services; - _applicationRoot = applicationRoot; - _settings = settings.Value; + System.IO.Directory.CreateDirectory(dirInfo.FullName); } - protected override Lucene.Net.Store.Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) + switch (_settings.LuceneDirectoryFactory) { - _directoryFactory = CreateFactory(); - return _directoryFactory.CreateDirectory(luceneIndex, forceUnlock); - } - - /// - /// Creates a directory factory based on the configured value and ensures that - /// - private IDirectoryFactory CreateFactory() - { - DirectoryInfo dirInfo = _applicationRoot.ApplicationRoot; - - if (!dirInfo.Exists) - { - Directory.CreateDirectory(dirInfo.FullName); - } - - switch (_settings.LuceneDirectoryFactory) - { - case LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory: - return _services.GetRequiredService(); - case LuceneDirectoryFactory.TempFileSystemDirectoryFactory: - return _services.GetRequiredService(); - case LuceneDirectoryFactory.Default: - default: - return _services.GetRequiredService(); - } + case LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory: + return _services.GetRequiredService(); + case LuceneDirectoryFactory.TempFileSystemDirectoryFactory: + return _services.GetRequiredService(); + case LuceneDirectoryFactory.Default: + default: + return _services.GetRequiredService(); } } } diff --git a/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs b/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs index 48c9fbb5ff..e6306ab444 100644 --- a/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs +++ b/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs @@ -1,4 +1,3 @@ -using System; using Examine; using Examine.Lucene; using Examine.Lucene.Analyzers; @@ -8,58 +7,55 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection +namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection; + +/// +/// Configures the index options to construct the Examine indexes +/// +public sealed class ConfigureIndexOptions : IConfigureNamedOptions { - /// - /// Configures the index options to construct the Examine indexes - /// - public sealed class ConfigureIndexOptions : IConfigureNamedOptions + private readonly IndexCreatorSettings _settings; + private readonly IUmbracoIndexConfig _umbracoIndexConfig; + + public ConfigureIndexOptions( + IUmbracoIndexConfig umbracoIndexConfig, + IOptions settings) { - private readonly IUmbracoIndexConfig _umbracoIndexConfig; - private readonly IndexCreatorSettings _settings; - - public ConfigureIndexOptions( - IUmbracoIndexConfig umbracoIndexConfig, - IOptions settings) - { - _umbracoIndexConfig = umbracoIndexConfig; - _settings = settings.Value; - } - - public void Configure(string name, LuceneDirectoryIndexOptions options) - { - switch (name) - { - case Constants.UmbracoIndexes.InternalIndexName: - options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); - options.Validator = _umbracoIndexConfig.GetContentValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); - break; - case Constants.UmbracoIndexes.ExternalIndexName: - options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion); - options.Validator = _umbracoIndexConfig.GetPublishedContentValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); - break; - case Constants.UmbracoIndexes.MembersIndexName: - options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); - options.Validator = _umbracoIndexConfig.GetMemberValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); - break; - } - - // ensure indexes are unlocked on startup - options.UnlockIndex = true; - - if (_settings.LuceneDirectoryFactory == LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory) - { - // if this directory factory is enabled then a snapshot deletion policy is required - options.IndexDeletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); - } - - - } - - public void Configure(LuceneDirectoryIndexOptions options) - => throw new NotImplementedException("This is never called and is just part of the interface"); + _umbracoIndexConfig = umbracoIndexConfig; + _settings = settings.Value; } + + public void Configure(string name, LuceneDirectoryIndexOptions options) + { + switch (name) + { + case Constants.UmbracoIndexes.InternalIndexName: + options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); + options.Validator = _umbracoIndexConfig.GetContentValueSetValidator(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + break; + case Constants.UmbracoIndexes.ExternalIndexName: + options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion); + options.Validator = _umbracoIndexConfig.GetPublishedContentValueSetValidator(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + break; + case Constants.UmbracoIndexes.MembersIndexName: + options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); + options.Validator = _umbracoIndexConfig.GetMemberValueSetValidator(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + break; + } + + // ensure indexes are unlocked on startup + options.UnlockIndex = true; + + if (_settings.LuceneDirectoryFactory == LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory) + { + // if this directory factory is enabled then a snapshot deletion policy is required + options.IndexDeletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + } + } + + public void Configure(LuceneDirectoryIndexOptions options) + => throw new NotImplementedException("This is never called and is just part of the interface"); } diff --git a/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs index 8eafde1a38..dac930964e 100644 --- a/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Examine.Lucene/DependencyInjection/UmbracoBuilderExtensions.cs @@ -3,38 +3,39 @@ using Examine.Lucene.Directories; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Infrastructure.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection +namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection; + +public static class UmbracoBuilderExtensions { - public static class UmbracoBuilderExtensions + /// + /// Adds the Examine indexes for Umbraco + /// + /// + /// + public static IUmbracoBuilder AddExamineIndexes(this IUmbracoBuilder umbracoBuilder) { - /// - /// Adds the Examine indexes for Umbraco - /// - /// - /// - public static IUmbracoBuilder AddExamineIndexes(this IUmbracoBuilder umbracoBuilder) - { - IServiceCollection services = umbracoBuilder.Services; + IServiceCollection services = umbracoBuilder.Services; - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - services.AddExamine(); + services.AddExamine(); - // Create the indexes - services - .AddExamineLuceneIndex(Constants.UmbracoIndexes.InternalIndexName) - .AddExamineLuceneIndex(Constants.UmbracoIndexes.ExternalIndexName) - .AddExamineLuceneIndex(Constants.UmbracoIndexes.MembersIndexName) - .ConfigureOptions(); + // Create the indexes + services + .AddExamineLuceneIndex(Constants.UmbracoIndexes + .InternalIndexName) + .AddExamineLuceneIndex(Constants.UmbracoIndexes + .ExternalIndexName) + .AddExamineLuceneIndex(Constants.UmbracoIndexes + .MembersIndexName) + .ConfigureOptions(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - return umbracoBuilder; - } + return umbracoBuilder; } } diff --git a/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs b/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs index 02a9ea85e0..99a31a0513 100644 --- a/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs +++ b/src/Umbraco.Examine.Lucene/Extensions/ExamineExtensions.cs @@ -1,75 +1,66 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; using Examine; using Examine.Lucene.Providers; using Lucene.Net.Analysis.Core; -using Lucene.Net.Index; using Lucene.Net.QueryParsers.Classic; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Runtime; +using Lucene.Net.Search; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions -{ - /// - /// Extension methods for the LuceneIndex - /// - public static class ExamineExtensions - { - internal static bool TryParseLuceneQuery(string query) - { - // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll - // also do this rudimentary check - if (!query.Contains(":")) - { - return false; - } +namespace Umbraco.Extensions; - try +/// +/// Extension methods for the LuceneIndex +/// +public static class ExamineExtensions +{ + internal static bool TryParseLuceneQuery(string query) + { + // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll + // also do this rudimentary check + if (!query.Contains(":")) + { + return false; + } + + try + { + //This will pass with a plain old string without any fields, need to figure out a way to have it properly parse + Query? parsed = new QueryParser(LuceneInfo.CurrentVersion, UmbracoExamineFieldNames.NodeNameFieldName, new KeywordAnalyzer()).Parse(query); + return true; + } + catch (ParseException) + { + return false; + } + catch (Exception) + { + return false; + } + } + + /// + /// Checks if the index can be read/opened + /// + /// + /// The exception returned if there was an error + /// + public static bool IsHealthy(this LuceneIndex indexer, [MaybeNullWhen(true)] out Exception ex) + { + try + { + using (indexer.IndexWriter.IndexWriter.GetReader(false)) { - //This will pass with a plain old string without any fields, need to figure out a way to have it properly parse - var parsed = new QueryParser(LuceneInfo.CurrentVersion, UmbracoExamineFieldNames.NodeNameFieldName, new KeywordAnalyzer()).Parse(query); + ex = null; return true; } - catch (ParseException) - { - return false; - } - catch (Exception) - { - return false; - } } - - /// - /// Checks if the index can be read/opened - /// - /// - /// The exception returned if there was an error - /// - public static bool IsHealthy(this LuceneIndex indexer, [MaybeNullWhen(true)] out Exception ex) + catch (Exception e) { - try - { - using (indexer.IndexWriter.IndexWriter.GetReader(false)) - { - ex = null; - return true; - } - } - catch (Exception e) - { - ex = e; - return false; - } + ex = e; + return false; } - } } diff --git a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs index 4d7ec2f23d..00f5be31a3 100644 --- a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs +++ b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs @@ -1,9 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Examine.Lucene; using Examine.Lucene.Providers; using Lucene.Net.Store; @@ -14,81 +11,77 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; using Directory = Lucene.Net.Store.Directory; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class LuceneIndexDiagnostics : IIndexDiagnostics { - public class LuceneIndexDiagnostics : IIndexDiagnostics + private readonly IHostingEnvironment _hostingEnvironment; + private readonly LuceneDirectoryIndexOptions? _indexOptions; + + public LuceneIndexDiagnostics( + LuceneIndex index, + ILogger logger, + IHostingEnvironment hostingEnvironment, + IOptionsMonitor? indexOptions) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly LuceneDirectoryIndexOptions? _indexOptions; - - public LuceneIndexDiagnostics( - LuceneIndex index, - ILogger logger, - IHostingEnvironment hostingEnvironment, - IOptionsMonitor? indexOptions) + _hostingEnvironment = hostingEnvironment; + if (indexOptions != null) { - _hostingEnvironment = hostingEnvironment; - if (indexOptions != null) - { - _indexOptions = indexOptions.Get(index.Name); + _indexOptions = indexOptions.Get(index.Name); + } + Index = index; + Logger = logger; + } + + public LuceneIndex Index { get; } + public ILogger Logger { get; } + + + public Attempt IsHealthy() + { + var isHealthy = Index.IsHealthy(out Exception? indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError?.Message); + } + + public long GetDocumentCount() => Index.GetDocumentCount(); + + public IEnumerable GetFieldNames() => Index.GetFieldNames(); + + public virtual IReadOnlyDictionary Metadata + { + get + { + Directory luceneDir = Index.GetLuceneDirectory(); + var d = new Dictionary + { + [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, + ["LuceneDirectory"] = luceneDir.GetType().Name + }; + + if (luceneDir is FSDirectory fsDir) + { + var rootDir = _hostingEnvironment.ApplicationPhysicalPath; + d["LuceneIndexFolder"] = fsDir.Directory.ToString().ToLowerInvariant() + .TrimStart(rootDir.ToLowerInvariant()).Replace("\\", " /").EnsureStartsWith('/'); } - Index = index; - Logger = logger; - } - public LuceneIndex Index { get; } - public ILogger Logger { get; } - - - - public Attempt IsHealthy() - { - var isHealthy = Index.IsHealthy(out var indexError); - return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError?.Message); - } - - public long GetDocumentCount() => Index.GetDocumentCount(); - - public IEnumerable GetFieldNames() => Index.GetFieldNames(); - - public virtual IReadOnlyDictionary Metadata - { - get + if (_indexOptions != null) { - Directory luceneDir = Index.GetLuceneDirectory(); - var d = new Dictionary + if (_indexOptions.DirectoryFactory != null) { - [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, - [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, - ["LuceneDirectory"] = luceneDir.GetType().Name - }; - - if (luceneDir is FSDirectory fsDir) - { - - var rootDir = _hostingEnvironment.ApplicationPhysicalPath; - d["LuceneIndexFolder"] = fsDir.Directory.ToString().ToLowerInvariant().TrimStart(rootDir.ToLowerInvariant()).Replace("\\", " /").EnsureStartsWith('/'); + d[nameof(LuceneDirectoryIndexOptions.DirectoryFactory)] = _indexOptions.DirectoryFactory.GetType(); } - if (_indexOptions != null) + if (_indexOptions.IndexDeletionPolicy != null) { - if (_indexOptions.DirectoryFactory != null) - { - d[nameof(LuceneDirectoryIndexOptions.DirectoryFactory)] = _indexOptions.DirectoryFactory.GetType(); - } - - if (_indexOptions.IndexDeletionPolicy != null) - { - d[nameof(LuceneDirectoryIndexOptions.IndexDeletionPolicy)] = _indexOptions.IndexDeletionPolicy.GetType(); - } - + d[nameof(LuceneDirectoryIndexOptions.IndexDeletionPolicy)] = + _indexOptions.IndexDeletionPolicy.GetType(); } - - return d; } + + return d; } - - } } diff --git a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs index fb8b082d15..971e515228 100644 --- a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs +++ b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnosticsFactory.cs @@ -1,52 +1,49 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Examine; -using Examine.Lucene; using Examine.Lucene.Providers; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Implementation of which returns +/// for lucene based indexes that don't have an implementation else fallsback to the default +/// implementation. +/// +public class LuceneIndexDiagnosticsFactory : IndexDiagnosticsFactory { - /// - /// Implementation of which returns - /// for lucene based indexes that don't have an implementation else fallsback to the default implementation. - /// - public class LuceneIndexDiagnosticsFactory : IndexDiagnosticsFactory + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILoggerFactory _loggerFactory; + + public LuceneIndexDiagnosticsFactory( + ILoggerFactory loggerFactory, + IHostingEnvironment hostingEnvironment) { - private readonly ILoggerFactory _loggerFactory; - private readonly IHostingEnvironment _hostingEnvironment; + _loggerFactory = loggerFactory; + _hostingEnvironment = hostingEnvironment; + } - public LuceneIndexDiagnosticsFactory( - ILoggerFactory loggerFactory, - IHostingEnvironment hostingEnvironment) + public override IIndexDiagnostics Create(IIndex index) + { + if (!(index is IIndexDiagnostics indexDiag)) { - _loggerFactory = loggerFactory; - _hostingEnvironment = hostingEnvironment; - } - - public override IIndexDiagnostics Create(IIndex index) - { - if (!(index is IIndexDiagnostics indexDiag)) + if (index is LuceneIndex luceneIndex) { - if (index is LuceneIndex luceneIndex) - { - indexDiag = new LuceneIndexDiagnostics( - luceneIndex, - _loggerFactory.CreateLogger(), - _hostingEnvironment, - null); - } - else - { - indexDiag = base.Create(index); - } + indexDiag = new LuceneIndexDiagnostics( + luceneIndex, + _loggerFactory.CreateLogger(), + _hostingEnvironment, + null); + } + else + { + indexDiag = base.Create(index); } - return indexDiag; } + + return indexDiag; } } diff --git a/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs b/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs index 1c7127b9d5..84c1b3c4e4 100644 --- a/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs +++ b/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs @@ -1,30 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Threading; using Examine.Lucene.Directories; using Examine.Lucene.Providers; using Lucene.Net.Store; using Directory = Lucene.Net.Store.Directory; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class LuceneRAMDirectoryFactory : DirectoryFactoryBase { - public class LuceneRAMDirectoryFactory : DirectoryFactoryBase + protected override Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) + => new RandomIdRAMDirectory(); + + private class RandomIdRAMDirectory : RAMDirectory { - - public LuceneRAMDirectoryFactory() - { - } - - protected override Directory CreateDirectory(LuceneIndex luceneIndex, bool forceUnlock) - => new RandomIdRAMDirectory(); - - private class RandomIdRAMDirectory : RAMDirectory - { - private readonly string _lockId = Guid.NewGuid().ToString(); - public override string GetLockID() => _lockId; - } + private readonly string _lockId = Guid.NewGuid().ToString(); + public override string GetLockID() => _lockId; } } diff --git a/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs b/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs index ed6f47c882..e10374387e 100644 --- a/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs +++ b/src/Umbraco.Examine.Lucene/NoPrefixSimpleFsLockFactory.cs @@ -1,29 +1,26 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.IO; using Lucene.Net.Store; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// A custom that ensures a prefixless lock prefix +/// +/// +/// This is a work around for the Lucene APIs. By default Lucene will use a null prefix however when we set a custom +/// lock factory the null prefix is overwritten. +/// +public class NoPrefixSimpleFsLockFactory : SimpleFSLockFactory { - - /// - /// A custom that ensures a prefixless lock prefix - /// - /// - /// This is a work around for the Lucene APIs. By default Lucene will use a null prefix however when we set a custom - /// lock factory the null prefix is overwritten. - /// - public class NoPrefixSimpleFsLockFactory : SimpleFSLockFactory + public NoPrefixSimpleFsLockFactory(DirectoryInfo lockDir) : base(lockDir) { - public NoPrefixSimpleFsLockFactory(DirectoryInfo lockDir) : base(lockDir) - { - } + } - public override string LockPrefix - { - get => base.LockPrefix; - set => base.LockPrefix = null; //always set to null - } + public override string LockPrefix + { + get => base.LockPrefix; + set => base.LockPrefix = null; //always set to null } } diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj index db5a303efd..833e01be50 100644 --- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj +++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj @@ -21,7 +21,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs b/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs index e99f986176..d27d673a1f 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoApplicationRoot.cs @@ -1,23 +1,22 @@ -using System.IO; using Examine; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Sets the Examine to be ExamineIndexes sub directory of the Umbraco TEMP folder +/// +public class UmbracoApplicationRoot : IApplicationRoot { - /// - /// Sets the Examine to be ExamineIndexes sub directory of the Umbraco TEMP folder - /// - public class UmbracoApplicationRoot : IApplicationRoot - { - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHostingEnvironment _hostingEnvironment; - public UmbracoApplicationRoot(IHostingEnvironment hostingEnvironment) - => _hostingEnvironment = hostingEnvironment; + public UmbracoApplicationRoot(IHostingEnvironment hostingEnvironment) + => _hostingEnvironment = hostingEnvironment; - public DirectoryInfo ApplicationRoot - => new DirectoryInfo( - Path.Combine( - _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempData), - "ExamineIndexes")); - } + public DirectoryInfo ApplicationRoot + => new( + Path.Combine( + _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData), + "ExamineIndexes")); } diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs index ec6c8e35a4..a2c4dedafe 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs @@ -1,154 +1,151 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Examine; using Examine.Lucene; +using Examine.Search; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An indexer for Umbraco content and media +/// +public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex { - /// - /// An indexer for Umbraco content and media - /// - public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex, IDisposable + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; + private readonly ILogger _logger; + + public UmbracoContentIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState, + ILocalizationService? languageService = null) + : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) { - private readonly ILogger _logger; - private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; - public UmbracoContentIndex( - ILoggerFactory loggerFactory, - string name, - IOptionsMonitor indexOptions, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState, - ILocalizationService? languageService = null) - : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) - { - LanguageService = languageService; - _logger = loggerFactory.CreateLogger(); + LanguageService = languageService; + _logger = loggerFactory.CreateLogger(); - LuceneDirectoryIndexOptions namedOptions = indexOptions.Get(name); - if (namedOptions == null) + LuceneDirectoryIndexOptions namedOptions = indexOptions.Get(name); + if (namedOptions == null) + { + throw new InvalidOperationException( + $"No named {typeof(LuceneDirectoryIndexOptions)} options with name {name}"); + } + + if (namedOptions.Validator is IContentValueSetValidator contentValueSetValidator) + { + PublishedValuesOnly = contentValueSetValidator.PublishedValuesOnly; + } + } + + protected ILocalizationService? LanguageService { get; } + + /// + /// Explicitly override because we need to do validation differently than the underlying logic + /// + /// + void IIndex.IndexItems(IEnumerable values) => PerformIndexItems(values, OnIndexOperationComplete); + + /// + /// Special check for invalid paths + /// + /// + /// + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + // We don't want to re-enumerate this list, but we need to split it into 2x enumerables: invalid and valid items. + // The Invalid items will be deleted, these are items that have invalid paths (i.e. moved to the recycle bin, etc...) + // Then we'll index the Value group all together. + var invalidOrValid = values.GroupBy(v => + { + if (!v.Values.TryGetValue("path", out IReadOnlyList? paths) || paths.Count <= 0 || paths[0] == null) { - throw new InvalidOperationException($"No named {typeof(LuceneDirectoryIndexOptions)} options with name {name}"); + return ValueSetValidationStatus.Failed; } - if (namedOptions.Validator is IContentValueSetValidator contentValueSetValidator) + ValueSetValidationResult validationResult = ValueSetValidator.Validate(v); + + return validationResult.Status; + }).ToList(); + + var hasDeletes = false; + var hasUpdates = false; + + // ordering by descending so that Filtered/Failed processes first + foreach (IGrouping group in invalidOrValid.OrderByDescending(x => x.Key)) + { + switch (group.Key) { - PublishedValuesOnly = contentValueSetValidator.PublishedValuesOnly; + case ValueSetValidationStatus.Valid: + hasUpdates = true; + + //these are the valid ones, so just index them all at once + base.PerformIndexItems(group.ToList(), onComplete); + break; + case ValueSetValidationStatus.Failed: + // don't index anything that is invalid + break; + case ValueSetValidationStatus.Filtered: + hasDeletes = true; + + // these are the invalid/filtered items so we'll delete them + // since the path is not valid we need to delete this item in + // case it exists in the index already and has now + // been moved to an invalid parent. + base.PerformDeleteFromIndex(group.Select(x => x.Id), null); + break; } } - protected ILocalizationService? LanguageService { get; } - - /// - /// Explicitly override because we need to do validation differently than the underlying logic - /// - /// - void IIndex.IndexItems(IEnumerable values) => PerformIndexItems(values, OnIndexOperationComplete); - - /// - /// Special check for invalid paths - /// - /// - /// - protected override void PerformIndexItems(IEnumerable values, Action onComplete) + if ((hasDeletes && !hasUpdates) || (!hasDeletes && !hasUpdates)) { - // We don't want to re-enumerate this list, but we need to split it into 2x enumerables: invalid and valid items. - // The Invalid items will be deleted, these are items that have invalid paths (i.e. moved to the recycle bin, etc...) - // Then we'll index the Value group all together. - var invalidOrValid = values.GroupBy(v => - { - if (!v.Values.TryGetValue("path", out IReadOnlyList? paths) || paths.Count <= 0 || paths[0] == null) - { - return ValueSetValidationStatus.Failed; - } + //we need to manually call the completed method + onComplete(new IndexOperationEventArgs(this, 0)); + } + } - ValueSetValidationResult validationResult = ValueSetValidator.Validate(v); + /// + /// + /// Deletes a node from the index. + /// + /// + /// When a content node is deleted, we also need to delete it's children from the index so we need to perform a + /// custom Lucene search to find all decendents and create Delete item queues for them too. + /// + /// ID of the node to delete + /// + protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) + { + var idsAsList = itemIds.ToList(); - return validationResult.Status; - }).ToList(); + for (var i = 0; i < idsAsList.Count; i++) + { + var nodeId = idsAsList[i]; - var hasDeletes = false; - var hasUpdates = false; + //find all descendants based on path + var descendantPath = $@"\-1\,*{nodeId}\,*"; + var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}"; + IQuery? c = Searcher.CreateQuery(); + IBooleanOperation? filtered = c.NativeQuery(rawQuery); + IOrdering? selectedFields = filtered.SelectFields(_idOnlyFieldSet); + ISearchResults? results = selectedFields.Execute(); - // ordering by descending so that Filtered/Failed processes first - foreach (IGrouping group in invalidOrValid.OrderByDescending(x => x.Key)) - { - switch (group.Key) - { - case ValueSetValidationStatus.Valid: - hasUpdates = true; + _logger.LogDebug("DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); - //these are the valid ones, so just index them all at once - base.PerformIndexItems(group.ToList(), onComplete); - break; - case ValueSetValidationStatus.Failed: - // don't index anything that is invalid - break; - case ValueSetValidationStatus.Filtered: - hasDeletes = true; + var toRemove = results.Select(x => x.Id).ToList(); + // delete those descendants (ensure base. is used here so we aren't calling ourselves!) + base.PerformDeleteFromIndex(toRemove, null); - // these are the invalid/filtered items so we'll delete them - // since the path is not valid we need to delete this item in - // case it exists in the index already and has now - // been moved to an invalid parent. - base.PerformDeleteFromIndex(group.Select(x => x.Id), null); - break; - } - } - - if ((hasDeletes && !hasUpdates) || (!hasDeletes && !hasUpdates)) - { - //we need to manually call the completed method - onComplete(new IndexOperationEventArgs(this, 0)); - } - } - - /// - /// - /// Deletes a node from the index. - /// - /// - /// When a content node is deleted, we also need to delete it's children from the index so we need to perform a - /// custom Lucene search to find all decendents and create Delete item queues for them too. - /// - /// ID of the node to delete - /// - protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) - { - var idsAsList = itemIds.ToList(); - - for (int i = 0; i < idsAsList.Count; i++) - { - string nodeId = idsAsList[i]; - - //find all descendants based on path - var descendantPath = $@"\-1\,*{nodeId}\,*"; - var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}"; - var c = Searcher.CreateQuery(); - var filtered = c.NativeQuery(rawQuery); - var selectedFields = filtered.SelectFields(_idOnlyFieldSet); - var results = selectedFields.Execute(); - - _logger. - LogDebug("DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); - - var toRemove = results.Select(x => x.Id).ToList(); - // delete those descendants (ensure base. is used here so we aren't calling ourselves!) - base.PerformDeleteFromIndex(toRemove, null); - - // remove any ids from our list that were part of the descendants - idsAsList.RemoveAll(x => toRemove.Contains(x)); - } - - base.PerformDeleteFromIndex(idsAsList, onComplete); + // remove any ids from our list that were part of the descendants + idsAsList.RemoveAll(x => toRemove.Contains(x)); } + base.PerformDeleteFromIndex(idsAsList, onComplete); } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs index 9b94482a74..f2b94215a5 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs @@ -1,9 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Examine; using Examine.Lucene; using Examine.Lucene.Providers; @@ -15,123 +12,123 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An abstract provider containing the basic functionality to be able to query against Umbraco data. +/// +public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDiagnostics { + private readonly UmbracoExamineIndexDiagnostics _diagnostics; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private bool _hasLoggedInitLog; + + protected UmbracoExamineIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState) + : base(loggerFactory, name, indexOptions) + { + _runtimeState = runtimeState; + _diagnostics = new UmbracoExamineIndexDiagnostics(this, loggerFactory.CreateLogger(), hostingEnvironment, indexOptions); + _logger = loggerFactory.CreateLogger(); + } + + public Attempt IsHealthy() => _diagnostics.IsHealthy(); + public virtual IReadOnlyDictionary Metadata => _diagnostics.Metadata; /// - /// An abstract provider containing the basic functionality to be able to query against Umbraco data. + /// When set to true Umbraco will keep the index in sync with Umbraco data automatically /// - public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDiagnostics + public bool EnableDefaultEventHandler { get; set; } = true; + + public bool PublishedValuesOnly { get; protected set; } = false; + + /// + /// override to check if we can actually initialize. + /// + /// + /// This check is required since the base examine lib will try to rebuild on startup + /// + protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) { - private readonly UmbracoExamineIndexDiagnostics _diagnostics; - private readonly IRuntimeState _runtimeState; - private bool _hasLoggedInitLog = false; - private readonly ILogger _logger; - - protected UmbracoExamineIndex( - ILoggerFactory loggerFactory, - string name, - IOptionsMonitor indexOptions, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState) - : base(loggerFactory, name, indexOptions) + if (CanInitialize()) { - _runtimeState = runtimeState; - _diagnostics = new UmbracoExamineIndexDiagnostics(this, loggerFactory.CreateLogger(), hostingEnvironment, indexOptions); - _logger = loggerFactory.CreateLogger(); + base.PerformDeleteFromIndex(itemIds, onComplete); + } + } + + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + if (CanInitialize()) + { + base.PerformIndexItems(values, onComplete); + } + } + + /// + /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes + /// + /// + protected bool CanInitialize() + { + var canInit = _runtimeState.Level == RuntimeLevel.Run; + + if (!canInit && !_hasLoggedInitLog) + { + _hasLoggedInitLog = true; + _logger.LogWarning("Runtime state is not " + RuntimeLevel.Run + ", no indexing will occur"); } - /// - /// When set to true Umbraco will keep the index in sync with Umbraco data automatically - /// - public bool EnableDefaultEventHandler { get; set; } = true; + return canInit; + } - public bool PublishedValuesOnly { get; protected set; } = false; + /// + /// This ensures that the special __Raw_ fields are indexed correctly + /// + /// + protected override void OnDocumentWriting(DocumentWritingEventArgs docArgs) + { + Document? d = docArgs.Document; - /// - /// override to check if we can actually initialize. - /// - /// - /// This check is required since the base examine lib will try to rebuild on startup - /// - protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action? onComplete) + foreach (KeyValuePair> f in docArgs.ValueSet.Values + .Where(x => x.Key.StartsWith(UmbracoExamineFieldNames.RawFieldPrefix)).ToList()) { - if (CanInitialize()) + if (f.Value.Count > 0) { - base.PerformDeleteFromIndex(itemIds, onComplete); + //remove the original value so we can store it the correct way + d.RemoveField(f.Key); + + d.Add(new StoredField(f.Key, f.Value[0].ToString())); } } - protected override void PerformIndexItems(IEnumerable values, Action onComplete) + base.OnDocumentWriting(docArgs); + } + + protected override void OnTransformingIndexValues(IndexingItemEventArgs e) + { + base.OnTransformingIndexValues(e); + + var updatedValues = e.ValueSet.Values.ToDictionary(x => x.Key, x => (IEnumerable)x.Value); + + //ensure special __Path field + var path = e.ValueSet.GetValue("path"); + if (path != null) { - if (CanInitialize()) - { - base.PerformIndexItems(values, onComplete); - } + updatedValues[UmbracoExamineFieldNames.IndexPathFieldName] = path.Yield(); } - /// - /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes - /// - /// - protected bool CanInitialize() + //icon + if (e.ValueSet.Values.TryGetValue("icon", out IReadOnlyList? icon) && + e.ValueSet.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) == false) { - var canInit = _runtimeState.Level == RuntimeLevel.Run; - - if (!canInit && !_hasLoggedInitLog) - { - _hasLoggedInitLog = true; - _logger.LogWarning("Runtime state is not " + RuntimeLevel.Run + ", no indexing will occur"); - } - - return canInit; + updatedValues[UmbracoExamineFieldNames.IconFieldName] = icon; } - /// - /// This ensures that the special __Raw_ fields are indexed correctly - /// - /// - protected override void OnDocumentWriting(DocumentWritingEventArgs docArgs) - { - var d = docArgs.Document; - - foreach (var f in docArgs.ValueSet.Values.Where(x => x.Key.StartsWith(UmbracoExamineFieldNames.RawFieldPrefix)).ToList()) - { - if (f.Value.Count > 0) - { - //remove the original value so we can store it the correct way - d.RemoveField(f.Key); - - d.Add(new StoredField(f.Key, f.Value[0].ToString())); - } - } - - base.OnDocumentWriting(docArgs); - } - - protected override void OnTransformingIndexValues(IndexingItemEventArgs e) - { - base.OnTransformingIndexValues(e); - - var updatedValues = e.ValueSet.Values.ToDictionary(x => x.Key, x => (IEnumerable) x.Value); - - //ensure special __Path field - var path = e.ValueSet.GetValue("path"); - if (path != null) - { - updatedValues[UmbracoExamineFieldNames.IndexPathFieldName] = path.Yield(); - } - - //icon - if (e.ValueSet.Values.TryGetValue("icon", out var icon) && e.ValueSet.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) == false) - { - updatedValues[UmbracoExamineFieldNames.IconFieldName] = icon; - } - - e.SetValues(updatedValues); - } - - public Attempt IsHealthy() => _diagnostics.IsHealthy(); - public virtual IReadOnlyDictionary Metadata => _diagnostics.Metadata; + e.SetValues(updatedValues); } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs index 6a18884dc2..9e06d1c98e 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoExamineIndexDiagnostics.cs @@ -1,55 +1,50 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Examine.Lucene; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics { - public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics + private readonly UmbracoExamineIndex _index; + + public UmbracoExamineIndexDiagnostics( + UmbracoExamineIndex index, + ILogger logger, + IHostingEnvironment hostingEnvironment, + IOptionsMonitor indexOptions) + : base(index, logger, hostingEnvironment, indexOptions) => + _index = index; + + public override IReadOnlyDictionary Metadata { - private readonly UmbracoExamineIndex _index; - - public UmbracoExamineIndexDiagnostics( - UmbracoExamineIndex index, - ILogger logger, - IHostingEnvironment hostingEnvironment, - IOptionsMonitor indexOptions) - : base(index, logger, hostingEnvironment, indexOptions) + get { - _index = index; - } + var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - public override IReadOnlyDictionary Metadata - { - get + d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; + d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; + + if (_index.ValueSetValidator is ValueSetValidator vsv) { - var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - - d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; - d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; - - if (_index.ValueSetValidator is ValueSetValidator vsv) - { - d[nameof(ValueSetValidator.IncludeItemTypes)] = vsv.IncludeItemTypes; - d[nameof(ContentValueSetValidator.ExcludeItemTypes)] = vsv.ExcludeItemTypes; - d[nameof(ContentValueSetValidator.IncludeFields)] = vsv.IncludeFields; - d[nameof(ContentValueSetValidator.ExcludeFields)] = vsv.ExcludeFields; - } - - if (_index.ValueSetValidator is ContentValueSetValidator cvsv) - { - d[nameof(ContentValueSetValidator.PublishedValuesOnly)] = cvsv.PublishedValuesOnly; - d[nameof(ContentValueSetValidator.SupportProtectedContent)] = cvsv.SupportProtectedContent; - d[nameof(ContentValueSetValidator.ParentId)] = cvsv.ParentId; - } - - return d.Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value); + d[nameof(ValueSetValidator.IncludeItemTypes)] = vsv.IncludeItemTypes; + d[nameof(ContentValueSetValidator.ExcludeItemTypes)] = vsv.ExcludeItemTypes; + d[nameof(ContentValueSetValidator.IncludeFields)] = vsv.IncludeFields; + d[nameof(ContentValueSetValidator.ExcludeFields)] = vsv.ExcludeFields; } + + if (_index.ValueSetValidator is ContentValueSetValidator cvsv) + { + d[nameof(ContentValueSetValidator.PublishedValuesOnly)] = cvsv.PublishedValuesOnly; + d[nameof(ContentValueSetValidator.SupportProtectedContent)] = cvsv.SupportProtectedContent; + d[nameof(ContentValueSetValidator.ParentId)] = cvsv.ParentId; + } + + return d.Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value); } } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs b/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs index 89f61c1e53..4f45e3513e 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoLockFactory.cs @@ -1,15 +1,13 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.IO; using Examine.Lucene.Directories; using Lucene.Net.Store; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class UmbracoLockFactory : ILockFactory { - public class UmbracoLockFactory : ILockFactory - { - public LockFactory GetLockFactory(DirectoryInfo directory) - => new NoPrefixSimpleFsLockFactory(directory); - } + public LockFactory GetLockFactory(DirectoryInfo directory) + => new NoPrefixSimpleFsLockFactory(directory); } diff --git a/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs index a6e000ff43..f40ca76e84 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoMemberIndex.cs @@ -7,21 +7,20 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Custom indexer for members +/// +public class UmbracoMemberIndex : UmbracoExamineIndex, IUmbracoMemberIndex { - /// - /// Custom indexer for members - /// - public class UmbracoMemberIndex : UmbracoExamineIndex, IUmbracoMemberIndex + public UmbracoMemberIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState) + : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) { - public UmbracoMemberIndex( - ILoggerFactory loggerFactory, - string name, - IOptionsMonitor indexOptions, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState) - : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) - { - } } } diff --git a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs index fc59d06016..4657c8a68a 100644 --- a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs @@ -8,54 +8,55 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Ensures that distributed cache events are setup and the is initialized +/// +public sealed class DatabaseServerMessengerNotificationHandler : + INotificationHandler, INotificationHandler { + private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly ILogger _logger; + private readonly IServerMessenger _messenger; + private readonly IRuntimeState _runtimeState; + /// - /// Ensures that distributed cache events are setup and the is initialized + /// Initializes a new instance of the class. /// - public sealed class DatabaseServerMessengerNotificationHandler : INotificationHandler, INotificationHandler + public DatabaseServerMessengerNotificationHandler( + IServerMessenger serverMessenger, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + IRuntimeState runtimeState) { - private readonly IServerMessenger _messenger; - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly ILogger _logger; - private readonly IRuntimeState _runtimeState; - - /// - /// Initializes a new instance of the class. - /// - public DatabaseServerMessengerNotificationHandler( - IServerMessenger serverMessenger, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - IRuntimeState runtimeState) - { - _databaseFactory = databaseFactory; - _logger = logger; - _messenger = serverMessenger; - _runtimeState = runtimeState; - } - - /// - public void Handle(UmbracoApplicationStartingNotification notification) - { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return; - } - - if (_databaseFactory.CanConnect == false) - { - _logger.LogWarning("Cannot connect to the database, distributed calls will not be enabled for this server."); - return; - } - - // Sync on startup, this will run through the messenger's initialization sequence - _messenger?.Sync(); - } - - /// - /// Clear the batch on end request - /// - public void Handle(UmbracoRequestEndNotification notification) => _messenger?.SendMessages(); + _databaseFactory = databaseFactory; + _logger = logger; + _messenger = serverMessenger; + _runtimeState = runtimeState; } + + /// + public void Handle(UmbracoApplicationStartingNotification notification) + { + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } + + if (_databaseFactory.CanConnect == false) + { + _logger.LogWarning( + "Cannot connect to the database, distributed calls will not be enabled for this server."); + return; + } + + // Sync on startup, this will run through the messenger's initialization sequence + _messenger?.Sync(); + } + + /// + /// Clear the batch on end request + /// + public void Handle(UmbracoRequestEndNotification notification) => _messenger?.SendMessages(); } diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index b18cce9b3d..7f7f8d6784 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -1,268 +1,273 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents the default cache policy. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// The default cache policy caches entities with a 5 minutes sliding expiration. +/// Each entity is cached individually. +/// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. +/// If options.GetAllCacheValidateCount then we check against the db when getting many entities. +/// +public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IEntity { - /// - /// Represents the default cache policy. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// The default cache policy caches entities with a 5 minutes sliding expiration. - /// Each entity is cached individually. - /// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. - /// If options.GetAllCacheValidateCount then we check against the db when getting many entities. - /// - public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase - where TEntity : class, IEntity + private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const + private readonly RepositoryCachePolicyOptions _options; + + public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) + : base(cache, scopeAccessor) => + _options = options ?? throw new ArgumentNullException(nameof(options)); + + protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; + + /// + public override void Create(TEntity entity, Action persistNew) { - private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const - private readonly RepositoryCachePolicyOptions _options; - - public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) - : base(cache, scopeAccessor) + if (entity == null) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + throw new ArgumentNullException(nameof(entity)); } - protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; - - protected string GetEntityCacheKey(TId? id) + try { - if (EqualityComparer.Default.Equals(id, default)) + persistNew(entity); + + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - return string.Empty; + Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); } - if (typeof(TId).IsValueType) - { - return EntityTypeCacheKey + id; - } - else - { - return EntityTypeCacheKey + id?.ToString()?.ToUpperInvariant(); - } + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + catch + { + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.Clear(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + + throw; + } + } + + /// + public override void Update(TEntity entity, Action persistUpdated) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); } - protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; - - protected virtual void InsertEntity(string cacheKey, TEntity entity) - => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); - - protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities) + try { - if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount) + persistUpdated(entity); + + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - // getting all of them, and finding nothing. - // if we can cache a zero count, cache an empty array, - // for as long as the cache is not cleared (no expiration) - Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); + Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); } - else + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + catch + { + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.Clear(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + + throw; + } + } + + /// + public override void Delete(TEntity entity, Action persistDeleted) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + try + { + persistDeleted(entity); + } + finally + { + // whatever happens, clear the cache + var cacheKey = GetEntityCacheKey(entity.Id); + Cache.Clear(cacheKey); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + } + + /// + public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) + { + var cacheKey = GetEntityCacheKey(id); + TEntity? fromCache = Cache.GetCacheItem(cacheKey); + + // if found in cache then return else fetch and cache + if (fromCache != null) + { + return fromCache; + } + + TEntity? entity = performGet(id); + + if (entity != null && entity.HasIdentity) + { + InsertEntity(cacheKey, entity); + } + + return entity; + } + + /// + public override TEntity? GetCached(TId id) + { + var cacheKey = GetEntityCacheKey(id); + return Cache.GetCacheItem(cacheKey); + } + + /// + public override bool Exists(TId id, Func performExists, Func?> performGetAll) + { + // if found in cache the return else check + var cacheKey = GetEntityCacheKey(id); + TEntity? fromCache = Cache.GetCacheItem(cacheKey); + return fromCache != null || performExists(id); + } + + /// + public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + { + if (ids?.Length > 0) + { + // try to get each entity from the cache + // if we can find all of them, return + TEntity[] entities = ids.Select(GetCached).WhereNotNull().ToArray(); + if (ids.Length.Equals(entities.Length)) { - if (entities is not null) + return entities; // no need for null checks, we are not caching nulls + } + } + else + { + // get everything we have + TEntity?[] entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey) + .ToArray(); // no need for null checks, we are not caching nulls + + if (entities.Length > 0) + { + // if some of them were in the cache... + if (_options.GetAllCacheValidateCount) { - // individually cache each item - foreach (var entity in entities) + // need to validate the count, get the actual count and return if ok + if (_options.PerformCount is not null) { - var capture = entity; - Cache.Insert(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); - } - } - } - } - - /// - public override void Create(TEntity entity, Action persistNew) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistNew(entity); - - // just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); - } - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - catch - { - // if an exception is thrown we need to remove the entry from cache, - // this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - Cache.Clear(GetEntityCacheKey(entity.Id)); - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - - throw; - } - } - - /// - public override void Update(TEntity entity, Action persistUpdated) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistUpdated(entity); - - // just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); - } - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - catch - { - // if an exception is thrown we need to remove the entry from cache, - // this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - Cache.Clear(GetEntityCacheKey(entity.Id)); - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - - throw; - } - } - - /// - public override void Delete(TEntity entity, Action persistDeleted) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistDeleted(entity); - } - finally - { - // whatever happens, clear the cache - var cacheKey = GetEntityCacheKey(entity.Id); - Cache.Clear(cacheKey); - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - } - - /// - public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) - { - var cacheKey = GetEntityCacheKey(id); - var fromCache = Cache.GetCacheItem(cacheKey); - - // if found in cache then return else fetch and cache - if (fromCache != null) - { - return fromCache; - } - - var entity = performGet(id); - - if (entity != null && entity.HasIdentity) - { - InsertEntity(cacheKey, entity); - } - - return entity; - } - - /// - public override TEntity? GetCached(TId id) - { - var cacheKey = GetEntityCacheKey(id); - return Cache.GetCacheItem(cacheKey); - } - - /// - public override bool Exists(TId id, Func performExists, Func?> performGetAll) - { - // if found in cache the return else check - var cacheKey = GetEntityCacheKey(id); - var fromCache = Cache.GetCacheItem(cacheKey); - return fromCache != null || performExists(id); - } - - /// - public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) - { - if (ids?.Length > 0) - { - // try to get each entity from the cache - // if we can find all of them, return - var entities = ids.Select(GetCached).WhereNotNull().ToArray(); - if (ids.Length.Equals(entities.Length)) - return entities; // no need for null checks, we are not caching nulls - } - else - { - // get everything we have - var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey)? - .ToArray(); // no need for null checks, we are not caching nulls - - if (entities?.Length > 0) - { - // if some of them were in the cache... - if (_options.GetAllCacheValidateCount) - { - // need to validate the count, get the actual count and return if ok - if (_options.PerformCount is not null) + var totalCount = _options.PerformCount(); + if (entities.Length == totalCount) { - var totalCount = _options.PerformCount(); - if (entities.Length == totalCount) - return entities.WhereNotNull().ToArray(); + return entities.WhereNotNull().ToArray(); } } - else - { - // no need to validate, just return what we have and assume it's all there is - return entities.WhereNotNull().ToArray(); - } } - else if (_options.GetAllCacheAllowZeroCount) + else { - // if none of them were in the cache - // and we allow zero count - check for the special (empty) entry - var empty = Cache.GetCacheItem(EntityTypeCacheKey); - if (empty != null) return empty; + // no need to validate, just return what we have and assume it's all there is + return entities.WhereNotNull().ToArray(); + } + } + else if (_options.GetAllCacheAllowZeroCount) + { + // if none of them were in the cache + // and we allow zero count - check for the special (empty) entry + TEntity[]? empty = Cache.GetCacheItem(EntityTypeCacheKey); + if (empty != null) + { + return empty; } } - - // cache failed, get from repo and cache - var repoEntities = performGetAll(ids)? - .WhereNotNull() // exclude nulls! - .Where(x => x.HasIdentity) // be safe, though would be weird... - .ToArray(); - - // note: if empty & allow zero count, will cache a special (empty) entry - InsertEntities(ids, repoEntities); - - return repoEntities ?? Array.Empty(); } - /// - public override void ClearAll() + // cache failed, get from repo and cache + TEntity[]? repoEntities = performGetAll(ids)? + .WhereNotNull() // exclude nulls! + .Where(x => x.HasIdentity) // be safe, though would be weird... + .ToArray(); + + // note: if empty & allow zero count, will cache a special (empty) entry + InsertEntities(ids, repoEntities); + + return repoEntities ?? Array.Empty(); + } + + /// + public override void ClearAll() => Cache.ClearByKey(EntityTypeCacheKey); + + protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; + + protected string GetEntityCacheKey(TId? id) + { + if (EqualityComparer.Default.Equals(id, default)) { - Cache.ClearByKey(EntityTypeCacheKey); + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return EntityTypeCacheKey + id; + } + + return EntityTypeCacheKey + id?.ToString()?.ToUpperInvariant(); + } + + protected virtual void InsertEntity(string cacheKey, TEntity entity) + => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + + protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities) + { + if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount) + { + // getting all of them, and finding nothing. + // if we can cache a zero count, cache an empty array, + // for as long as the cache is not cleared (no expiration) + Cache.Insert(EntityTypeCacheKey, () => _emptyEntities); + } + else + { + if (entities is not null) + { + // individually cache each item + foreach (TEntity entity in entities) + { + TEntity capture = entity; + Cache.Insert(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); + } + } } } } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 6e6f549b03..11119aaf66 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -1,365 +1,341 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; +public class DistributedCacheBinder : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { + private readonly DistributedCache _distributedCache; + /// - /// Default implementation. + /// Initializes a new instance of the class. /// - public class DistributedCacheBinder : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + public DistributedCacheBinder(DistributedCache distributedCache) { - private readonly DistributedCache _distributedCache; - - /// - /// Initializes a new instance of the class. - /// - public DistributedCacheBinder(DistributedCache distributedCache) - { - _distributedCache = distributedCache; - } - - #region PublicAccessService - - public void Handle(PublicAccessEntrySavedNotification notification) - { - _distributedCache.RefreshPublicAccess(); - } - - public void Handle(PublicAccessEntryDeletedNotification notification) - { - _distributedCache.RefreshPublicAccess(); - - } - - #endregion - - #region ContentService - - /// - /// Handles cache refreshing for when content is copied - /// - /// - /// - /// - /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user permissions cache. - /// - private void ContentService_Copied(IContentService sender, CopyEventArgs e) - { - } - - - public void Handle(ContentTreeChangeNotification notification) - { - _distributedCache.RefreshContentCache(notification.Changes.ToArray()); - } - - //private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs e) - //{ - // _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray()); - //} - - //private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs e) - //{ - // _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); - //} - - #endregion - - #region LocalizationService / Dictionary - public void Handle(DictionaryItemSavedNotification notification) - { - foreach (IDictionaryItem entity in notification.SavedEntities) - { - _distributedCache.RefreshDictionaryCache(entity.Id); - } - } - - public void Handle(DictionaryItemDeletedNotification notification) - { - foreach (IDictionaryItem entity in notification.DeletedEntities) - { - _distributedCache.RemoveDictionaryCache(entity.Id); - } - } - - #endregion - - #region DataTypeService - - public void Handle(DataTypeSavedNotification notification) - { - foreach (IDataType entity in notification.SavedEntities) - { - _distributedCache.RefreshDataTypeCache(entity); - } - _distributedCache.RefreshValueEditorCache(notification.SavedEntities); - } - - public void Handle(DataTypeDeletedNotification notification) - { - foreach (IDataType entity in notification.DeletedEntities) - { - _distributedCache.RemoveDataTypeCache(entity); - } - _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); - } - - #endregion - - #region DomainService - - public void Handle(DomainSavedNotification notification) - { - foreach (IDomain entity in notification.SavedEntities) - { - _distributedCache.RefreshDomainCache(entity); - } - } - - public void Handle(DomainDeletedNotification notification) - { - foreach (IDomain entity in notification.DeletedEntities) - { - _distributedCache.RemoveDomainCache(entity); - } - } - - #endregion - - #region LocalizationService / Language - - /// - /// Fires when a language is deleted - /// - /// - public void Handle(LanguageDeletedNotification notification) - { - foreach (ILanguage entity in notification.DeletedEntities) - { - _distributedCache.RemoveLanguageCache(entity); - } - } - - /// - /// Fires when a language is saved - /// - /// - public void Handle(LanguageSavedNotification notification) - { - foreach (ILanguage entity in notification.SavedEntities) - { - _distributedCache.RefreshLanguageCache(entity); - } - } - - #endregion - - #region Content|Media|MemberTypeService - - public void Handle(ContentTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - public void Handle(MediaTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - public void Handle(MemberTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - #endregion - - #region UserService - - public void Handle(UserSavedNotification notification) - { - foreach (IUser entity in notification.SavedEntities) - { - _distributedCache.RefreshUserCache(entity.Id); - } - } - - public void Handle(UserDeletedNotification notification) - { - foreach (IUser entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserCache(entity.Id); - } - } - - public void Handle(UserGroupWithUsersSavedNotification notification) - { - foreach (UserGroupWithUsers entity in notification.SavedEntities) - { - _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); - } - } - - public void Handle(UserGroupDeletedNotification notification) - { - foreach (IUserGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserGroupCache(entity.Id); - } - } - - #endregion - - #region FileService - - /// - /// Removes cache for template - /// - /// - public void Handle(TemplateDeletedNotification notification) - { - foreach (ITemplate entity in notification.DeletedEntities) - { - _distributedCache.RemoveTemplateCache(entity.Id); - } - } - - /// - /// Refresh cache for template - /// - /// - public void Handle(TemplateSavedNotification notification) - { - foreach (ITemplate entity in notification.SavedEntities) - { - _distributedCache.RefreshTemplateCache(entity.Id); - } - } - - #endregion - - #region MacroService - - public void Handle(MacroDeletedNotification notification) - { - foreach (IMacro entity in notification.DeletedEntities) - { - _distributedCache.RemoveMacroCache(entity); - } - } - - public void Handle(MacroSavedNotification notification) - { - foreach (IMacro entity in notification.SavedEntities) - { - _distributedCache.RefreshMacroCache(entity); - } - } - - #endregion - - #region MediaService - - public void Handle(MediaTreeChangeNotification notification) - { - _distributedCache.RefreshMediaCache(notification.Changes.ToArray()); - } - - #endregion - - #region MemberService - - public void Handle(MemberDeletedNotification notification) - { - _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); - } - - public void Handle(MemberSavedNotification notification) - { - _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); - } - - #endregion - - #region MemberGroupService - - /// - /// Fires when a member group is deleted - /// - /// - public void Handle(MemberGroupDeletedNotification notification) - { - foreach (IMemberGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } - } - - /// - /// Fires when a member group is saved - /// - /// - public void Handle(MemberGroupSavedNotification notification) - { - foreach (IMemberGroup entity in notification.SavedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } - } - - #endregion - - #region RelationType - - public void Handle(RelationTypeSavedNotification notification) - { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.SavedEntities) - { - dc.RefreshRelationTypeCache(entity.Id); - } - } - - public void Handle(RelationTypeDeletedNotification notification) - { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.DeletedEntities) - { - dc.RemoveRelationTypeCache(entity.Id); - } - } - - #endregion + _distributedCache = distributedCache; } + + #region PublicAccessService + + public void Handle(PublicAccessEntrySavedNotification notification) + { + _distributedCache.RefreshPublicAccess(); + } + + public void Handle(PublicAccessEntryDeletedNotification notification) => _distributedCache.RefreshPublicAccess(); + + #endregion + + #region ContentService + + public void Handle(ContentTreeChangeNotification notification) + { + _distributedCache.RefreshContentCache(notification.Changes.ToArray()); + } + + // private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs e) + // { + // _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray()); + // } + + // private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs e) + // { + // _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); + // } + #endregion + + #region LocalizationService / Dictionary + public void Handle(DictionaryItemSavedNotification notification) + { + foreach (IDictionaryItem entity in notification.SavedEntities) + { + _distributedCache.RefreshDictionaryCache(entity.Id); + } + } + + public void Handle(DictionaryItemDeletedNotification notification) + { + foreach (IDictionaryItem entity in notification.DeletedEntities) + { + _distributedCache.RemoveDictionaryCache(entity.Id); + } + } + + #endregion + + #region DataTypeService + + public void Handle(DataTypeSavedNotification notification) + { + foreach (IDataType entity in notification.SavedEntities) + { + _distributedCache.RefreshDataTypeCache(entity); + } + + _distributedCache.RefreshValueEditorCache(notification.SavedEntities); + } + + public void Handle(DataTypeDeletedNotification notification) + { + foreach (IDataType entity in notification.DeletedEntities) + { + _distributedCache.RemoveDataTypeCache(entity); + } + + _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); + } + + #endregion + + #region DomainService + + public void Handle(DomainSavedNotification notification) + { + foreach (IDomain entity in notification.SavedEntities) + { + _distributedCache.RefreshDomainCache(entity); + } + } + + public void Handle(DomainDeletedNotification notification) + { + foreach (IDomain entity in notification.DeletedEntities) + { + _distributedCache.RemoveDomainCache(entity); + } + } + + #endregion + + #region LocalizationService / Language + + /// + /// Fires when a language is deleted + /// + /// + public void Handle(LanguageDeletedNotification notification) + { + foreach (ILanguage entity in notification.DeletedEntities) + { + _distributedCache.RemoveLanguageCache(entity); + } + } + + /// + /// Fires when a language is saved + /// + /// + public void Handle(LanguageSavedNotification notification) + { + foreach (ILanguage entity in notification.SavedEntities) + { + _distributedCache.RefreshLanguageCache(entity); + } + } + + #endregion + + #region Content|Media|MemberTypeService + + public void Handle(ContentTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + + public void Handle(MediaTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + + public void Handle(MemberTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + + #endregion + + #region UserService + + public void Handle(UserSavedNotification notification) + { + foreach (IUser entity in notification.SavedEntities) + { + _distributedCache.RefreshUserCache(entity.Id); + } + } + + public void Handle(UserDeletedNotification notification) + { + foreach (IUser entity in notification.DeletedEntities) + { + _distributedCache.RemoveUserCache(entity.Id); + } + } + + public void Handle(UserGroupWithUsersSavedNotification notification) + { + foreach (UserGroupWithUsers entity in notification.SavedEntities) + { + _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); + } + } + + public void Handle(UserGroupDeletedNotification notification) + { + foreach (IUserGroup entity in notification.DeletedEntities) + { + _distributedCache.RemoveUserGroupCache(entity.Id); + } + } + + #endregion + + #region FileService + + /// + /// Removes cache for template + /// + /// + public void Handle(TemplateDeletedNotification notification) + { + foreach (ITemplate entity in notification.DeletedEntities) + { + _distributedCache.RemoveTemplateCache(entity.Id); + } + } + + /// + /// Refresh cache for template + /// + /// + public void Handle(TemplateSavedNotification notification) + { + foreach (ITemplate entity in notification.SavedEntities) + { + _distributedCache.RefreshTemplateCache(entity.Id); + } + } + + #endregion + + #region MacroService + + public void Handle(MacroDeletedNotification notification) + { + foreach (IMacro entity in notification.DeletedEntities) + { + _distributedCache.RemoveMacroCache(entity); + } + } + + public void Handle(MacroSavedNotification notification) + { + foreach (IMacro entity in notification.SavedEntities) + { + _distributedCache.RefreshMacroCache(entity); + } + } + + #endregion + + #region MediaService + + public void Handle(MediaTreeChangeNotification notification) + { + _distributedCache.RefreshMediaCache(notification.Changes.ToArray()); + } + + #endregion + + #region MemberService + + public void Handle(MemberDeletedNotification notification) + { + _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); + } + + public void Handle(MemberSavedNotification notification) + { + _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); + } + + #endregion + + #region MemberGroupService + + /// + /// Fires when a member group is deleted + /// + /// + public void Handle(MemberGroupDeletedNotification notification) + { + foreach (IMemberGroup entity in notification.DeletedEntities) + { + _distributedCache.RemoveMemberGroupCache(entity.Id); + } + } + + /// + /// Fires when a member group is saved + /// + /// + public void Handle(MemberGroupSavedNotification notification) + { + foreach (IMemberGroup entity in notification.SavedEntities) + { + _distributedCache.RemoveMemberGroupCache(entity.Id); + } + } + + #endregion + + #region RelationType + + public void Handle(RelationTypeSavedNotification notification) + { + DistributedCache dc = _distributedCache; + foreach (IRelationType entity in notification.SavedEntities) + { + dc.RefreshRelationTypeCache(entity.Id); + } + } + + public void Handle(RelationTypeDeletedNotification notification) + { + DistributedCache dc = _distributedCache; + foreach (IRelationType entity in notification.DeletedEntities) + { + dc.RemoveRelationTypeCache(entity.Id); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs index ceac767a8c..dfa7d9b605 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs @@ -1,327 +1,376 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for . +/// +public static class DistributedCacheExtensions { - /// - /// Extension methods for . - /// - public static class DistributedCacheExtensions + #region PublicAccessCache + + public static void RefreshPublicAccess(this DistributedCache dc) { - #region PublicAccessCache - - public static void RefreshPublicAccess(this DistributedCache dc) - { - dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); - } - - #endregion - - #region User cache - - public static void RemoveUserCache(this DistributedCache dc, int userId) - { - dc.Remove(UserCacheRefresher.UniqueId, userId); - } - - public static void RefreshUserCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserCacheRefresher.UniqueId, userId); - } - - public static void RefreshAllUserCache(this DistributedCache dc) - { - dc.RefreshAll(UserCacheRefresher.UniqueId); - } - - #endregion - - #region User group cache - - public static void RemoveUserGroupCache(this DistributedCache dc, int userId) - { - dc.Remove(UserGroupCacheRefresher.UniqueId, userId); - } - - public static void RefreshUserGroupCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); - } - - public static void RefreshAllUserGroupCache(this DistributedCache dc) - { - dc.RefreshAll(UserGroupCacheRefresher.UniqueId); - } - - #endregion - - #region TemplateCache - - public static void RefreshTemplateCache(this DistributedCache dc, int templateId) - { - dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); - } - - public static void RemoveTemplateCache(this DistributedCache dc, int templateId) - { - dc.Remove(TemplateCacheRefresher.UniqueId, templateId); - } - - #endregion - - #region DictionaryCache - - public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } - - public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } - - #endregion - - #region DataTypeCache - - public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) return; - var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } - - public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) return; - var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region ValueEditorCache - - public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) - { - if (dataTypes is null) - { - return; - } - - var payloads = dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); - dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region ContentCache - - public static void RefreshAllContentCache(this DistributedCache dc) - { - var payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - - // note: refresh all content cache does refresh content types too - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); - - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region MemberCache - - public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) - { - if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); - } - - public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) - { - if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); - } - - #endregion - - #region MemberGroupCache - - public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); - } - - public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); - } - - #endregion - - #region MediaCache - - public static void RefreshAllMediaCache(this DistributedCache dc) - { - var payloads = new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - - // note: refresh all media cache does refresh content types too - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } - - public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); - - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Published Snapshot - - public static void RefreshAllPublishedSnapshot(this DistributedCache dc) - { - // note: refresh all content & media caches does refresh content types too - dc.RefreshAllContentCache(); - dc.RefreshAllMediaCache(); - dc.RefreshAllDomainCache(); - } - - #endregion - - #region MacroCache - - public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) return; - var payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); - } - - public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) return; - var payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Content/Media/Member type cache - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof (IContentType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Domain Cache - - public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) return; - var payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Refresh) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) return; - var payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Remove) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - public static void RefreshAllDomainCache(this DistributedCache dc) - { - var payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Language Cache - - public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) return; - - var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, - language.WasPropertyDirty(nameof(ILanguage.IsoCode)) - ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture - : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); - - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); - } - - public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) return; - - var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); - } - - #endregion - - #region Relation type cache - - public static void RefreshRelationTypeCache(this DistributedCache dc, int id) - { - dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); - } - - public static void RemoveRelationTypeCache(this DistributedCache dc, int id) - { - dc.Remove(RelationTypeCacheRefresher.UniqueId, id); - } - - #endregion - - + dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); } + + #endregion + + #region User cache + + public static void RemoveUserCache(this DistributedCache dc, int userId) + { + dc.Remove(UserCacheRefresher.UniqueId, userId); + } + + public static void RefreshUserCache(this DistributedCache dc, int userId) + { + dc.Refresh(UserCacheRefresher.UniqueId, userId); + } + + public static void RefreshAllUserCache(this DistributedCache dc) + { + dc.RefreshAll(UserCacheRefresher.UniqueId); + } + + #endregion + + #region User group cache + + public static void RemoveUserGroupCache(this DistributedCache dc, int userId) + { + dc.Remove(UserGroupCacheRefresher.UniqueId, userId); + } + + public static void RefreshUserGroupCache(this DistributedCache dc, int userId) + { + dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); + } + + public static void RefreshAllUserGroupCache(this DistributedCache dc) + { + dc.RefreshAll(UserGroupCacheRefresher.UniqueId); + } + + #endregion + + #region TemplateCache + + public static void RefreshTemplateCache(this DistributedCache dc, int templateId) + { + dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); + } + + public static void RemoveTemplateCache(this DistributedCache dc, int templateId) + { + dc.Remove(TemplateCacheRefresher.UniqueId, templateId); + } + + #endregion + + #region DictionaryCache + + public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) + { + dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); + } + + public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) + { + dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); + } + + #endregion + + #region DataTypeCache + + public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) + { + if (dataType == null) + { + return; + } + + DataTypeCacheRefresher.JsonPayload[] payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) }; + dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); + } + + public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) + { + if (dataType == null) + { + return; + } + + DataTypeCacheRefresher.JsonPayload[] payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) }; + dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region ValueEditorCache + + public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) + { + if (dataTypes is null) + { + return; + } + + IEnumerable payloads = dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); + dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region ContentCache + + public static void RefreshAllContentCache(this DistributedCache dc) + { + ContentCacheRefresher.JsonPayload[] payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + + // note: refresh all content cache does refresh content types too + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } + + public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region MemberCache + + public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + { + if (members.Length == 0) + { + return; + } + + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); + } + + public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) + { + if (members.Length == 0) + { + return; + } + + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); + } + + #endregion + + #region MemberGroupCache + + public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) + { + dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); + } + + public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) + { + dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); + } + + #endregion + + #region MediaCache + + public static void RefreshAllMediaCache(this DistributedCache dc) + { + MediaCacheRefresher.JsonPayload[] payloads = new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + + // note: refresh all media cache does refresh content types too + dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } + + public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + + dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Published Snapshot + + public static void RefreshAllPublishedSnapshot(this DistributedCache dc) + { + // note: refresh all content & media caches does refresh content types too + dc.RefreshAllContentCache(); + dc.RefreshAllMediaCache(); + dc.RefreshAllDomainCache(); + } + + #endregion + + #region MacroCache + + public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) + { + return; + } + + MacroCacheRefresher.JsonPayload[] payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; + dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + } + + public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) + { + return; + } + + MacroCacheRefresher.JsonPayload[] payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; + dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Content/Media/Member type cache + + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IContentType).Name, x.Item.Id, x.ChangeTypes)); + + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } + + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes)); + + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } + + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes)); + + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Domain Cache + + public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) + { + return; + } + + DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Refresh) }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } + + public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) + { + return; + } + + DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Remove) }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } + + public static void RefreshAllDomainCache(this DistributedCache dc) + { + DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Language Cache + + public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) + { + return; + } + + var payload = new LanguageCacheRefresher.JsonPayload( + language.Id, + language.IsoCode, + language.WasPropertyDirty(nameof(ILanguage.IsoCode)) ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); + + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); + } + + public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) + { + return; + } + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); + } + + #endregion + + #region Relation type cache + + public static void RefreshRelationTypeCache(this DistributedCache dc, int id) + { + dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); + } + + public static void RemoveRelationTypeCache(this DistributedCache dc, int id) + { + dc.Remove(RelationTypeCacheRefresher.UniqueId, id); + } + + #endregion + } diff --git a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs index 34cdd3ce0c..91ae85c84f 100644 --- a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs @@ -1,187 +1,191 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents a caching policy that caches the entire entities set as a single collection. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// Caches the entire set of entities as a single collection. +/// +/// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, +/// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to +/// keep as a whole in memory. +/// +/// +internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IEntity { - /// - /// Represents a caching policy that caches the entire entities set as a single collection. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// Caches the entire set of entities as a single collection. - /// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, - /// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to - /// keep as a whole in memory. - /// - internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase - where TEntity : class, IEntity + protected static readonly TId[] EmptyIds = new TId[0]; // const + private readonly Func _entityGetId; + private readonly bool _expires; + + public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, Func entityGetId, bool expires) + : base(cache, scopeAccessor) { - private readonly Func _entityGetId; - private readonly bool _expires; + _entityGetId = entityGetId; + _expires = expires; + } - public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, Func entityGetId, bool expires) - : base(cache, scopeAccessor) + /// + public override void Create(TEntity entity, Action persistNew) + { + if (entity == null) { - _entityGetId = entityGetId; - _expires = expires; + throw new ArgumentNullException(nameof(entity)); } - protected static readonly TId[] EmptyIds = new TId[0]; // const - - protected string GetEntityTypeCacheKey() + try { - return $"uRepo_{typeof (TEntity).Name}_"; + persistNew(entity); } - - protected void InsertEntities(TEntity[]? entities) + finally { - if (entities is null) - { - return; - } - - // cache is expected to be a deep-cloning cache ie it deep-clones whatever is - // IDeepCloneable when it goes in, and out. it also resets dirty properties, - // making sure that no 'dirty' entity is cached. - // - // this policy is caching the entire list of entities. to ensure that entities - // are properly deep-clones when cached, it uses a DeepCloneableList. however, - // we don't want to deep-clone *each* entity in the list when fetching it from - // cache as that would not be efficient for Get(id). so the DeepCloneableList is - // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, - // and then will *not* clone when retrieving. - - var key = GetEntityTypeCacheKey(); - - if (_expires) - { - Cache.Insert(key, () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); - } - else - { - Cache.Insert(key, () => new DeepCloneableList(entities)); - } - } - - /// - public override void Create(TEntity entity, Action persistNew) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistNew(entity); - } - finally - { - ClearAll(); - } - } - - /// - public override void Update(TEntity entity, Action persistUpdated) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistUpdated(entity); - } - finally - { - ClearAll(); - } - } - - /// - public override void Delete(TEntity entity, Action persistDeleted) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistDeleted(entity); - } - finally - { - ClearAll(); - } - } - - /// - public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) - { - // get all from the cache, then look for the entity - var all = GetAllCached(performGetAll); - var entity = all.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); - - // see note in InsertEntities - what we get here is the original - // cached entity, not a clone, so we need to manually ensure it is deep-cloned. - return (TEntity?)entity?.DeepClone(); - } - - /// - public override TEntity? GetCached(TId id) - { - // get all from the cache -- and only the cache, then look for the entity - var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); - var entity = all?.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); - - // see note in InsertEntities - what we get here is the original - // cached entity, not a clone, so we need to manually ensure it is deep-cloned. - return (TEntity?) entity?.DeepClone(); - } - - /// - public override bool Exists(TId id, Func performExits, Func?> performGetAll) - { - // get all as one set, then look for the entity - var all = GetAllCached(performGetAll); - return all.Any(x => _entityGetId(x)?.Equals(id) ?? false); - } - - /// - public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) - { - // get all as one set, from cache if possible, else repo - var all = GetAllCached(performGetAll); - - // if ids have been specified, filter - if (ids?.Length > 0) all = all.Where(x => ids.Contains(_entityGetId(x))); - - // and return - // see note in SetCacheActionToInsertEntities - what we get here is the original - // cached entities, not clones, so we need to manually ensure they are deep-cloned. - return all.Select(x => (TEntity) x.DeepClone()).ToArray(); - } - - // does NOT clone anything, so be nice with the returned values - internal IEnumerable GetAllCached(Func?> performGetAll) - { - // try the cache first - var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); - if (all != null) return all.ToArray(); - - // else get from repo and cache - var entities = performGetAll(EmptyIds)?.WhereNotNull().ToArray(); - InsertEntities(entities); // may be an empty array... - return entities ?? Enumerable.Empty(); - } - - /// - public override void ClearAll() - { - Cache.Clear(GetEntityTypeCacheKey()); + ClearAll(); } } + + protected string GetEntityTypeCacheKey() => $"uRepo_{typeof(TEntity).Name}_"; + + protected void InsertEntities(TEntity[]? entities) + { + if (entities is null) + { + return; + } + + // cache is expected to be a deep-cloning cache ie it deep-clones whatever is + // IDeepCloneable when it goes in, and out. it also resets dirty properties, + // making sure that no 'dirty' entity is cached. + // + // this policy is caching the entire list of entities. to ensure that entities + // are properly deep-clones when cached, it uses a DeepCloneableList. however, + // we don't want to deep-clone *each* entity in the list when fetching it from + // cache as that would not be efficient for Get(id). so the DeepCloneableList is + // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, + // and then will *not* clone when retrieving. + var key = GetEntityTypeCacheKey(); + + if (_expires) + { + Cache.Insert(key, () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); + } + else + { + Cache.Insert(key, () => new DeepCloneableList(entities)); + } + } + + /// + public override void Update(TEntity entity, Action persistUpdated) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + try + { + persistUpdated(entity); + } + finally + { + ClearAll(); + } + } + + /// + public override void Delete(TEntity entity, Action persistDeleted) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + try + { + persistDeleted(entity); + } + finally + { + ClearAll(); + } + } + + /// + public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) + { + // get all from the cache, then look for the entity + IEnumerable all = GetAllCached(performGetAll); + TEntity? entity = all.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); + + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return (TEntity?)entity?.DeepClone(); + } + + /// + public override TEntity? GetCached(TId id) + { + // get all from the cache -- and only the cache, then look for the entity + DeepCloneableList? all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + TEntity? entity = all?.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); + + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return (TEntity?)entity?.DeepClone(); + } + + /// + public override bool Exists(TId id, Func performExits, Func?> performGetAll) + { + // get all as one set, then look for the entity + IEnumerable all = GetAllCached(performGetAll); + return all.Any(x => _entityGetId(x)?.Equals(id) ?? false); + } + + /// + public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + { + // get all as one set, from cache if possible, else repo + IEnumerable all = GetAllCached(performGetAll); + + // if ids have been specified, filter + if (ids?.Length > 0) + { + all = all.Where(x => ids.Contains(_entityGetId(x))); + } + + // and return + // see note in SetCacheActionToInsertEntities - what we get here is the original + // cached entities, not clones, so we need to manually ensure they are deep-cloned. + return all.Select(x => (TEntity)x.DeepClone()).ToArray(); + } + + /// + public override void ClearAll() => Cache.Clear(GetEntityTypeCacheKey()); + + // does NOT clone anything, so be nice with the returned values + internal IEnumerable GetAllCached(Func?> performGetAll) + { + // try the cache first + DeepCloneableList? all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + if (all != null) + { + return all.ToArray(); + } + + // else get from repo and cache + TEntity[]? entities = performGetAll(EmptyIds)?.WhereNotNull().ToArray(); + InsertEntities(entities); // may be an empty array... + return entities ?? Enumerable.Empty(); + } } diff --git a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs index 900ff02921..7a43071b81 100644 --- a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs +++ b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs @@ -1,72 +1,71 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for repository cache policies. +/// +/// The type of the entity. +/// The type of the identifier. +public abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy + where TEntity : class, IEntity { - /// - /// A base class for repository cache policies. - /// - /// The type of the entity. - /// The type of the identifier. - public abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy - where TEntity : class, IEntity + private readonly IAppPolicyCache _globalCache; + private readonly IScopeAccessor _scopeAccessor; + + protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor) { - private readonly IAppPolicyCache _globalCache; - private readonly IScopeAccessor _scopeAccessor; + _globalCache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); + _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + } - protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor) + protected IAppPolicyCache Cache + { + get { - _globalCache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); - _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - } - - protected IAppPolicyCache Cache - { - get + IScope? ambientScope = _scopeAccessor.AmbientScope; + switch (ambientScope?.RepositoryCacheMode) { - var ambientScope = _scopeAccessor.AmbientScope; - switch (ambientScope?.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - return _globalCache; - case RepositoryCacheMode.Scoped: - return ambientScope.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.None: - return NoAppCache.Instance; - default: - throw new NotSupportedException($"Repository cache mode {ambientScope?.RepositoryCacheMode} is not supported."); - } + case RepositoryCacheMode.Default: + return _globalCache; + case RepositoryCacheMode.Scoped: + return ambientScope.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.None: + return NoAppCache.Instance; + default: + throw new NotSupportedException( + $"Repository cache mode {ambientScope?.RepositoryCacheMode} is not supported."); } } - - /// - public abstract TEntity? Get(TId? id, Func performGet, Func?> performGetAll); - - /// - public abstract TEntity? GetCached(TId id); - - /// - public abstract bool Exists(TId id, Func performExists, Func?> performGetAll); - - /// - public abstract void Create(TEntity entity, Action persistNew); - - /// - public abstract void Update(TEntity entity, Action persistUpdated); - - /// - public abstract void Delete(TEntity entity, Action persistDeleted); - - /// - public abstract TEntity[] GetAll(TId[]? ids, Func?> performGetAll); - - /// - public abstract void ClearAll(); } + + /// + public abstract TEntity? Get(TId? id, Func performGet, Func?> performGetAll); + + /// + public abstract TEntity? GetCached(TId id); + + /// + public abstract bool Exists(TId id, Func performExists, Func?> performGetAll); + + /// + public abstract void Create(TEntity entity, Action persistNew); + + /// + public abstract void Update(TEntity entity, Action persistUpdated); + + /// + public abstract void Delete(TEntity entity, Action persistDeleted); + + /// + public abstract TEntity[] GetAll(TId[]? ids, Func?> performGetAll); + + /// + public abstract void ClearAll(); } diff --git a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index d23960c903..16079d059a 100644 --- a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -2,31 +2,32 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Core.Cache -{ - /// - /// Represents a special policy that does not cache the result of GetAll. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// Overrides the default repository cache policy and does not writes the result of GetAll - /// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. - /// Used by DictionaryRepository. - /// - internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy - where TEntity : class, IEntity - { - public SingleItemsOnlyRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) - : base(cache, scopeAccessor, options) - { } +namespace Umbraco.Cms.Core.Cache; - protected override void InsertEntities(TId[]? ids, TEntity[]? entities) - { - // nop - } +/// +/// Represents a special policy that does not cache the result of GetAll. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// +/// Overrides the default repository cache policy and does not writes the result of GetAll +/// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. +/// +/// Used by DictionaryRepository. +/// +internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy + where TEntity : class, IEntity +{ + public SingleItemsOnlyRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) + : base(cache, scopeAccessor, options) + { + } + + protected override void InsertEntities(TId[]? ids, TEntity[]? entities) + { + // nop } } diff --git a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs index c39ab7c8c6..9481eb9958 100644 --- a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs +++ b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; @@ -14,22 +12,26 @@ namespace Umbraco.Cms.Core.Configuration { public class JsonConfigManipulator : IConfigManipulator { + private const string UmbracoConnectionStringPath = $"ConnectionStrings:{Cms.Core.Constants.System.UmbracoConnectionName}"; + private const string UmbracoConnectionStringProviderNamePath = UmbracoConnectionStringPath + ConnectionStrings.ProviderNamePostfix; + private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly object _locker = new object(); - public JsonConfigManipulator( - IConfiguration configuration, - ILogger logger) + public JsonConfigManipulator(IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; } - public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{Cms.Core.Constants.System.UmbracoConnectionName}"; + [Obsolete] + public string UmbracoConnectionPath { get; } = UmbracoConnectionStringPath; + public void RemoveConnectionString() { - var provider = GetJsonConfigurationProvider(UmbracoConnectionPath); + // Remove keys from JSON + var provider = GetJsonConfigurationProvider(UmbracoConnectionStringPath); var json = GetJson(provider); if (json is null) @@ -38,13 +40,15 @@ namespace Umbraco.Cms.Core.Configuration return; } - RemoveJsonKey(json, UmbracoConnectionPath); + RemoveJsonKey(json, UmbracoConnectionStringPath); + RemoveJsonKey(json, UmbracoConnectionStringProviderNamePath); SaveJson(provider, json); } public void SaveConnectionString(string connectionString, string? providerName) { + // Save keys to JSON var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); @@ -55,18 +59,17 @@ namespace Umbraco.Cms.Core.Configuration } var item = GetConnectionItem(connectionString, providerName); - if (item is not null) { - json?.Merge(item, new JsonMergeSettings()); + json.Merge(item, new JsonMergeSettings()); } SaveJson(provider, json); } - public void SaveConfigValue(string key, object value) { + // Save key to JSON var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); @@ -96,11 +99,11 @@ namespace Umbraco.Cms.Core.Configuration } SaveJson(provider, json); - } public void SaveDisableRedirectUrlTracking(bool disable) { + // Save key to JSON var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); @@ -111,10 +114,9 @@ namespace Umbraco.Cms.Core.Configuration } var item = GetDisableRedirectUrlItem(disable); - if (item is not null) { - json?.Merge(item, new JsonMergeSettings()); + json.Merge(item, new JsonMergeSettings()); } SaveJson(provider, json); @@ -122,6 +124,7 @@ namespace Umbraco.Cms.Core.Configuration public void SetGlobalId(string id) { + // Save key to JSON var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); @@ -132,10 +135,9 @@ namespace Umbraco.Cms.Core.Configuration } var item = GetGlobalIdItem(id); - if (item is not null) { - json?.Merge(item, new JsonMergeSettings()); + json.Merge(item, new JsonMergeSettings()); } SaveJson(provider, json); @@ -192,8 +194,13 @@ namespace Umbraco.Cms.Core.Configuration writer.WriteStartObject(); writer.WritePropertyName(Constants.System.UmbracoConnectionName); writer.WriteValue(connectionString); - writer.WritePropertyName($"{Constants.System.UmbracoConnectionName}{ConnectionStrings.ProviderNamePostfix}"); - writer.WriteValue(providerName); + + if (!string.IsNullOrEmpty(providerName)) + { + writer.WritePropertyName(Constants.System.UmbracoConnectionName + ConnectionStrings.ProviderNamePostfix); + writer.WriteValue(providerName); + } + writer.WriteEndObject(); writer.WriteEndObject(); @@ -211,8 +218,13 @@ namespace Umbraco.Cms.Core.Configuration token?.Parent?.Remove(); } - private void SaveJson(JsonConfigurationProvider provider, JObject? json) + private void SaveJson(JsonConfigurationProvider? provider, JObject? json) { + if (provider is null) + { + return; + } + lock (_locker) { if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider) @@ -238,8 +250,13 @@ namespace Umbraco.Cms.Core.Configuration } } - private JObject? GetJson(JsonConfigurationProvider provider) + private JObject? GetJson(JsonConfigurationProvider? provider) { + if (provider is null) + { + return null; + } + lock (_locker) { if (provider.Source.FileProvider is not PhysicalFileProvider physicalFileProvider) @@ -264,22 +281,21 @@ namespace Umbraco.Cms.Core.Configuration } } - private JsonConfigurationProvider GetJsonConfigurationProvider(string? requiredKey = null) + private JsonConfigurationProvider? GetJsonConfigurationProvider(string? requiredKey = null) { if (_configuration is IConfigurationRoot configurationRoot) { foreach (var provider in configurationRoot.Providers) { - if (provider is JsonConfigurationProvider jsonConfigurationProvider) + if (provider is JsonConfigurationProvider jsonConfigurationProvider && + (requiredKey is null || provider.TryGet(requiredKey, out _))) { - if (requiredKey is null || provider.TryGet(requiredKey, out _)) - { - return jsonConfigurationProvider; - } + return jsonConfigurationProvider; } } } - throw new InvalidOperationException("Could not find a writable json config source"); + + return null; } /// @@ -293,17 +309,21 @@ namespace Umbraco.Cms.Core.Configuration { if (token is JObject obj) { - foreach (var property in obj.Properties()) { if (name is null) + { return property.Value; + } + if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) + { return property.Value; + } } } + return null; } - } } diff --git a/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs b/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs index fe21141636..5feee83f23 100644 --- a/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs +++ b/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs @@ -1,24 +1,25 @@ -using System; using NCrontab; -using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Implements using the NCrontab library +/// +public class NCronTabParser : ICronTabParser { - public class NCronTabParser : ICronTabParser + /// + public bool IsValidCronTab(string cronTab) { - public bool IsValidCronTab(string cronTab) - { - var result = CrontabSchedule.TryParse(cronTab); + var result = CrontabSchedule.TryParse(cronTab); - return !(result is null); - } - - public DateTime GetNextOccurrence(string cronTab, DateTime time) - { - var result = CrontabSchedule.Parse(cronTab); - - return result.GetNextOccurrence(time); - } + return !(result is null); } + /// + public DateTime GetNextOccurrence(string cronTab, DateTime time) + { + var result = CrontabSchedule.Parse(cronTab); + + return result.GetNextOccurrence(time); + } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs index 7091b89cad..609c5305dc 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs @@ -2,31 +2,40 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Runtime; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Gets the mappers collection builder. /// - public static partial class UmbracoBuilderExtensions - { - /// - /// Gets the mappers collection builder. - /// - /// The builder. - public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); + /// The builder. + public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); - public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); + /// + /// Gets the NPoco mappers collection builder. + /// + /// The builder. + public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + /// + /// Gets the package migration plans collection builder. + /// + /// The builder. + public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); - /// - /// Gets the package migration plans collection builder. - /// - /// The builder. - public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - } + /// + /// Gets the runtime mode validators collection builder. + /// + /// The builder. + public static RuntimeModeValidatorCollectionBuilder RuntimeModeValidators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 4b30a10159..62bafcd28e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog; +using SixLabors.ImageSharp; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; @@ -50,340 +51,365 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Runtime; +using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; +using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds all core Umbraco services required to run which may be replaced later in the pipeline. + /// + public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) { - /// - /// Adds all core Umbraco services required to run which may be replaced later in the pipeline - /// - public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) + builder + .AddMainDom() + .AddLogging(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); + builder.NPocoMappers()?.Add(); + builder.PackageMigrationPlans()?.Add(() => builder.TypeLoader.GetPackageMigrationPlans()); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + + // Add runtime mode validation + builder.Services.AddSingleton(); + builder.RuntimeModeValidators() + .Add() + .Add() + .Add() + .Add() + .Add(); + + // composers + builder + .AddRepositories() + .AddServices() + .AddCoreMappingProfiles() + .AddFileSystems() + .AddWebAssets(); + + // register persistence mappers - required by database factory so needs to be done here + // means the only place the collection can be modified is in a runtime - afterwards it + // has been frozen and it is too late + builder.Mappers()?.AddCoreMappers(); + + // register the scope provider + builder.Services.AddSingleton(sp => ActivatorUtilities.CreateInstance(sp, sp.GetRequiredService())); // implements IScopeProvider, IScopeAccessor + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // register database builder + // *not* a singleton, don't want to keep it around + builder.Services.AddTransient(); + + // register manifest parser, will be injected in collection builders where needed + builder.Services.AddSingleton(); + + // register the manifest filter collection builder (collection is empty by default) + builder.ManifestFilters(); + + builder.MediaUrlGenerators() + .Add() + .Add(); + + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(factory + => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault( + factory.GetRequiredService>().CurrentValue))); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => new MigrationBuilder(factory)); + + builder.AddPreValueMigrators(); + + builder.Services.AddSingleton(); + + // register the published snapshot accessor - the "current" published snapshot is in the umbraco context + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + // Config manipulator + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be + // discovered when CoreBootManager configures the converters. We will remove the basic one defined + // in core so that the more enhanced version is active. + builder.PropertyValueConverters() + .Remove(); + + // register *all* checks, except those marked [HideFromTypeFinder] of course + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + + // replace + builder.Services.AddSingleton( + services => new EmailSender( + services.GetRequiredService>(), + services.GetRequiredService>(), + services.GetRequiredService(), + services.GetService>(), + services.GetService>())); + + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => + new PublishedContentQueryAccessor(sp.GetRequiredService())); + builder.Services.AddScoped(factory => { - builder - .AddMainDom() - .AddLogging(); + IUmbracoContextAccessor umbCtx = factory.GetRequiredService(); + IUmbracoContext umbracoContext = umbCtx.GetRequiredUmbracoContext(); + return new PublishedContentQuery( + umbracoContext.PublishedSnapshot, + factory.GetRequiredService(), factory.GetRequiredService()); + }); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); - builder.NPocoMappers()?.Add(); - builder.PackageMigrationPlans()?.Add(() => builder.TypeLoader.GetPackageMigrationPlans()); + // register accessors for cultures + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); + builder.Services.AddSingleton(); - // composers - builder - .AddRepositories() - .AddServices() - .AddCoreMappingProfiles() - .AddFileSystems() - .AddWebAssets(); + builder.Services.AddSingleton(); - // register persistence mappers - required by database factory so needs to be done here - // means the only place the collection can be modified is in a runtime - afterwards it - // has been frozen and it is too late - builder.Mappers()?.AddCoreMappers(); + builder.Services.AddSingleton(); - // register the scope provider - builder.Services.AddSingleton(); // implements IScopeProvider, IScopeAccessor - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // Add default ImageSharp configuration and service implementations + builder.Services.AddSingleton(Configuration.Default); + builder.Services.AddSingleton(); - // register database builder - // *not* a singleton, don't want to keep it around - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.AddInstaller(); - // register manifest parser, will be injected in collection builders where needed - builder.Services.AddSingleton(); + // Services required to run background jobs (with out the handler) + builder.Services.AddSingleton(); - // register the manifest filter collection builder (collection is empty by default) - builder.ManifestFilters(); + builder.Services.AddTransient(); + return builder; + } - builder.MediaUrlGenerators() - .Add() - .Add(); + public static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.SetLogViewer(); + builder.Services.AddSingleton(factory => new SerilogJsonLogViewer( + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + Log.Logger)); - builder.Services.AddSingleton(); + return builder; + } - builder.Services.AddSingleton(factory - => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetRequiredService>().CurrentValue))); + /// + /// Adds logging requirements for Umbraco + /// + private static IUmbracoBuilder AddLogging(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + return builder; + } - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => new MigrationBuilder(factory)); + private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => + { + IOptions globalSettings = factory.GetRequiredService>(); + IOptionsMonitor connectionStrings = + factory.GetRequiredService>(); + IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); - builder.AddPreValueMigrators(); + IDbProviderFactoryCreator dbCreator = factory.GetRequiredService(); + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory = + factory.GetRequiredService(); + ILoggerFactory loggerFactory = factory.GetRequiredService(); + NPocoMapperCollection npocoMappers = factory.GetRequiredService(); + IMainDomKeyGenerator mainDomKeyGenerator = factory.GetRequiredService(); - builder.Services.AddSingleton(); - - // register the published snapshot accessor - the "current" published snapshot is in the umbraco context - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - // Config manipulator - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be - // discovered when CoreBootManager configures the converters. We will remove the basic one defined - // in core so that the more enhanced version is active. - builder.PropertyValueConverters() - .Remove(); - - // register *all* checks, except those marked [HideFromTypeFinder] of course - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddScoped(); - - // replace - builder.Services.AddSingleton( - services => new EmailSender( - services.GetRequiredService>(), - services.GetRequiredService>(), - services.GetRequiredService(), - services.GetService>(), - services.GetService>())); - - builder.Services.AddSingleton(); - - builder.Services.AddScoped(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => - new PublishedContentQueryAccessor(sp.GetRequiredService()) - ); - builder.Services.AddScoped(factory => + switch (globalSettings.Value.MainDomLock) { - var umbCtx = factory.GetRequiredService(); - var umbracoContext = umbCtx.GetRequiredUmbracoContext(); - return new PublishedContentQuery(umbracoContext.PublishedSnapshot, factory.GetRequiredService(), factory.GetRequiredService()); - }); + case "SqlMainDomLock": + return new SqlMainDomLock( + loggerFactory, + globalSettings, + connectionStrings, + dbCreator, + mainDomKeyGenerator, + databaseSchemaCreatorFactory, + npocoMappers); - // register accessors for cultures - builder.Services.AddSingleton(); + case "MainDomSemaphoreLock": + return new MainDomSemaphoreLock( + loggerFactory.CreateLogger(), + hostingEnvironment); - builder.Services.AddSingleton(); + case "FileSystemMainDomLock": + default: + return new FileSystemMainDomLock( + loggerFactory.CreateLogger(), + mainDomKeyGenerator, hostingEnvironment, + factory.GetRequiredService>()); + } + }); - builder.Services.AddSingleton(); + return builder; + } - builder.Services.AddSingleton(); + private static IUmbracoBuilder AddPreValueMigrators(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); - builder.Services.AddSingleton(); + return builder; + } - builder.Services.AddSingleton(); + public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder) + { + // add handlers for sending user notifications (i.e. emails) + builder.Services.AddSingleton(); + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - // Add default ImageSharp configuration and service implementations - builder.Services.AddSingleton(SixLabors.ImageSharp.Configuration.Default); - builder.Services.AddSingleton(); + // add handlers for building content relations + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - builder.Services.AddTransient(); - builder.AddInstaller(); + // add notification handlers for property editors + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - // Services required to run background jobs (with out the handler) - builder.Services.AddSingleton(); - return builder; - } + // add notification handlers for redirect tracking + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - /// - /// Adds logging requirements for Umbraco - /// - private static IUmbracoBuilder AddLogging(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - return builder; - } + // Add notification handlers for DistributedCache + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + ; - private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => - { - var globalSettings = factory.GetRequiredService>(); - var connectionStrings = factory.GetRequiredService>(); - var hostingEnvironment = factory.GetRequiredService(); + // add notification handlers for auditing + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - var dbCreator = factory.GetRequiredService(); - var databaseSchemaCreatorFactory = factory.GetRequiredService(); - var loggerFactory = factory.GetRequiredService(); - var npocoMappers = factory.GetRequiredService(); - var mainDomKeyGenerator = factory.GetRequiredService(); - - switch (globalSettings.Value.MainDomLock) - { - case "SqlMainDomLock": - return new SqlMainDomLock( - loggerFactory, - globalSettings, - connectionStrings, - dbCreator, - mainDomKeyGenerator, - databaseSchemaCreatorFactory, - npocoMappers); - - case "MainDomSemaphoreLock": - return new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment); - - case "FileSystemMainDomLock": - default: - return new FileSystemMainDomLock(loggerFactory.CreateLogger(), mainDomKeyGenerator, hostingEnvironment, factory.GetRequiredService>()); - } - }); - - return builder; - } - - - private static IUmbracoBuilder AddPreValueMigrators(this IUmbracoBuilder builder) - { - builder.WithCollectionBuilder() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - - return builder; - } - - public static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.SetLogViewer(); - builder.Services.AddSingleton(factory => new SerilogJsonLogViewer(factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - Log.Logger)); - - return builder; - } - - public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder) - { - // add handlers for sending user notifications (i.e. emails) - builder.Services.AddSingleton(); - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add handlers for building content relations - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add notification handlers for property editors - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add notification handlers for redirect tracking - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // Add notification handlers for DistributedCache - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - ; - // add notification handlers for auditing - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs index 71ea85d80f..21e715b803 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; @@ -7,96 +6,99 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Adds distributed cache support /// - public static partial class UmbracoBuilderExtensions + /// + /// This is still required for websites that are not load balancing because this ensures that sites hosted + /// with managed hosts like IIS/etc... work correctly when AppDomains are running in parallel. + /// + public static IUmbracoBuilder AddDistributedCache(this IUmbracoBuilder builder) { - /// - /// Adds distributed cache support - /// - /// - /// This is still required for websites that are not load balancing because this ensures that sites hosted - /// with managed hosts like IIS/etc... work correctly when AppDomains are running in parallel. - /// - public static IUmbracoBuilder AddDistributedCache(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.SetServerMessenger(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - return builder; - } + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.SetServerMessenger(); +builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The type of the server registrar. - /// The builder. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder) - where T : class, IServerRoleAccessor - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The type of the server registrar. + /// The builder. + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder) + where T : class, IServerRoleAccessor + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The builder. - /// A function creating a server registrar. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The builder. + /// A function creating a server registrar. + public static IUmbracoBuilder SetServerRegistrar( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The builder. - /// A server registrar. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) - { - builder.Services.AddUnique(registrar); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The builder. + /// A server registrar. + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) + { + builder.Services.AddUnique(registrar); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The type of the server registrar. - /// The builder. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder) - where T : class, IServerMessenger - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The type of the server registrar. + /// The builder. + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder) + where T : class, IServerMessenger + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The builder. - /// A function creating a server messenger. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The builder. + /// A function creating a server messenger. + public static IUmbracoBuilder SetServerMessenger( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The builder. - /// A server messenger. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) - { - builder.Services.AddUnique(registrar); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The builder. + /// A server messenger. + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) + { + builder.Services.AddUnique(registrar); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index da31a8df39..aabadc5197 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -1,7 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.PropertyEditors; @@ -12,55 +10,54 @@ using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { - /// - /// Provides extension methods to the class. - /// - public static partial class UmbracoBuilderExtensions + public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder) { - public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder) - { - // populators are not a collection: one cannot remove ours, and can only add more - // the container can inject IEnumerable and get them all - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // populators are not a collection: one cannot remove ours, and can only add more + // the container can inject IEnumerable and get them all + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => - new ContentValueSetBuilder( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - true)); - builder.Services.AddUnique(factory => - new ContentValueSetBuilder( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - false)); - builder.Services.AddUnique, MediaValueSetBuilder>(); - builder.Services.AddUnique, MemberValueSetBuilder>(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => + new ContentValueSetBuilder( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + true)); + builder.Services.AddUnique(factory => + new ContentValueSetBuilder( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + false)); + builder.Services.AddUnique, MediaValueSetBuilder>(); + builder.Services.AddUnique, MemberValueSetBuilder>(); + builder.Services.AddSingleton(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index 310ae0b302..e9564b0263 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -3,58 +3,59 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.IO.MediaPathSchemes; using Umbraco.Extensions; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /* + * HOW TO REPLACE THE MEDIA UNDERLYING FILESYSTEM + * ---------------------------------------------- + * + * Create an implementation of IFileSystem and register it as the underlying filesystem for + * MediaFileSystem with the following extension on composition. + * + * builder.SetMediaFileSystem(factory => FactoryMethodToReturnYourImplementation()) + * + * WHAT IS SHADOWING + * ----------------- + * + * Shadowing is the technology used for Deploy to implement some sort of + * transaction-management on top of filesystems. The plumbing explained above, + * compared to creating your own physical filesystem, ensures that your filesystem + * would participate into such transactions. + * + */ + + internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) { - /* - * HOW TO REPLACE THE MEDIA UNDERLYING FILESYSTEM - * ---------------------------------------------- - * - * Create an implementation of IFileSystem and register it as the underlying filesystem for - * MediaFileSystem with the following extension on composition. - * - * builder.SetMediaFileSystem(factory => FactoryMethodToReturnYourImplementation()) - * - * WHAT IS SHADOWING - * ----------------- - * - * Shadowing is the technology used for Deploy to implement some sort of - * transaction-management on top of filesystems. The plumbing explained above, - * compared to creating your own physical filesystem, ensures that your filesystem - * would participate into such transactions. - * - */ + // register FileSystems, which manages all filesystems + builder.Services.AddSingleton(); - internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) + // register the scheme for media paths + builder.Services.AddUnique(); + + builder.Services.AddUnique(); + builder.Services.AddUnique(); + + builder.SetMediaFileSystem(factory => { - // register FileSystems, which manages all filesystems - builder.Services.AddSingleton(); + IIOHelper ioHelper = factory.GetRequiredService(); + IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); + ILogger logger = factory.GetRequiredService>(); + GlobalSettings globalSettings = factory.GetRequiredService>().Value; - // register the scheme for media paths - builder.Services.AddUnique(); + var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) + ? globalSettings.UmbracoMediaPhysicalRootPath + : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); + var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); + return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); + }); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - - builder.SetMediaFileSystem(factory => - { - IIOHelper ioHelper = factory.GetRequiredService(); - IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); - ILogger logger = factory.GetRequiredService>(); - GlobalSettings globalSettings = factory.GetRequiredService>().Value; - - var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) ? globalSettings.UmbracoMediaPhysicalRootPath : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); - var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); - return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); - }); - - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index d750eb15e0..c3aa291fb7 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -7,39 +7,37 @@ using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Infrastructure.Install.InstallSteps; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds the services for the Umbraco installer + /// + internal static IUmbracoBuilder AddInstaller(this IUmbracoBuilder builder) { - /// - /// Adds the services for the Umbraco installer - /// - internal static IUmbracoBuilder AddInstaller(this IUmbracoBuilder builder) + // register the installer steps + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(provider => { - // register the installer steps - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(provider => - { - return new TelemetryIdentifierStep( - provider.GetRequiredService>(), - provider.GetRequiredService()); - }); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + return new TelemetryIdentifierStep( + provider.GetRequiredService>(), + provider.GetRequiredService()); + }); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddTransient(); - builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs index 42ce7f7932..05fc2f125c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs @@ -5,40 +5,39 @@ using Umbraco.Cms.Core.Models.Mapping; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Registers the core Umbraco mapper definitions + /// + public static IUmbracoBuilder AddCoreMappingProfiles(this IUmbracoBuilder builder) { - /// - /// Registers the core Umbraco mapper definitions - /// - public static IUmbracoBuilder AddCoreMappingProfiles(this IUmbracoBuilder builder) - { - builder.Services.AddUnique(); + builder.Services.AddUnique(); - builder.WithCollectionBuilder() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(); + builder.WithCollectionBuilder() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 28dde10329..3afb9fe64a 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,74 +1,74 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Composes repositories. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Composes repositories. + /// Adds the Umbraco repositories /// - public static partial class UmbracoBuilderExtensions + internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) { - /// - /// Adds the Umbraco repositories - /// - internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) - { - // repositories - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddMultipleUnique(); - builder.Services.AddUnique(); - builder.Services.AddSingleton(); - builder.Services.AddUnique(factory => factory.GetRequiredService()); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + // repositories + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddMultipleUnique(); + builder.Services.AddUnique(); + builder.Services.AddSingleton(); + builder.Services.AddUnique(factory => factory.GetRequiredService()); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 7e0802d558..b7d600ec7c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -8,7 +7,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; @@ -26,93 +25,92 @@ using Umbraco.Cms.Infrastructure.Telemetry.Providers; using Umbraco.Cms.Infrastructure.Templates; using Umbraco.Extensions; using CacheInstructionService = Umbraco.Cms.Core.Services.Implement.CacheInstructionService; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds Umbraco services + /// + internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) { - /// - /// Adds Umbraco services - /// - internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) - { - // register the service context - builder.Services.AddSingleton(); + // register the service context + builder.Services.AddSingleton(); - // register the special idk map - builder.Services.AddUnique(); + // register the special idk map + builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddTransient(CreateLocalizedTextServiceFileSourcesFactory); - builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); - builder.Services.AddUnique(); - builder.Services.AddSingleton(CreatePackageDataInstallation); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddTransient(CreateLocalizedTextServiceFileSourcesFactory); + builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); + builder.Services.AddUnique(); + builder.Services.AddSingleton(CreatePackageDataInstallation); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; + } + private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) + => new( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + packageRepoFileName); - private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) - => new PackagesRepository( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - packageRepoFileName); + // Factory registration is only required because of ambiguous constructor + private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) + => new( + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService()); - // Factory registration is only required because of ambiguous constructor - private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) - => new PackageDataInstallation( - factory.GetRequiredService(), - factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService()); + private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory( + IServiceProvider container) + { + IHostingEnvironment hostingEnvironment = container.GetRequiredService(); + var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); + var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); - private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory(IServiceProvider container) - { - var hostingEnvironment = container.GetRequiredService(); - var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); - var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); - - return new LocalizedTextServiceFileSources( - container.GetRequiredService>(), - container.GetRequiredService(), - mainLangFolder, - container.GetServices(), - new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Lang").GetDirectoryContents(string.Empty)); - } + return new LocalizedTextServiceFileSources( + container.GetRequiredService>(), + container.GetRequiredService(), + mainLangFolder, + container.GetServices(), + new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Lang") + .GetDirectoryContents(string.Empty)); } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs index f0ab1ec344..3c1162bbab 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -1,25 +1,24 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; using Umbraco.Cms.Infrastructure.Telemetry.Providers; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static class UmbracoBuilder_TelemetryProviders { - public static class UmbracoBuilder_TelemetryProviders + public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) { - public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) - { - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - return builder; - } + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs index e3839e152b..f899f311f5 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Dictionary; @@ -8,208 +7,220 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Sets the culture dictionary factory. /// - public static partial class UmbracoBuilderExtensions + /// The type of the factory. + /// The builder. + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder) + where T : class, ICultureDictionaryFactory { - /// - /// Sets the culture dictionary factory. - /// - /// The type of the factory. - /// The builder. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder) - where T : class, ICultureDictionaryFactory - { - builder.Services.AddUnique(); - return builder; - } + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the default view content provider - /// - /// The type of the provider. - /// The builder. - /// - public static IUmbracoBuilder SetDefaultViewContentProvider(this IUmbracoBuilder builder) - where T : class, IDefaultViewContentProvider - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the default view content provider + /// + /// The type of the provider. + /// The builder. + /// + public static IUmbracoBuilder SetDefaultViewContentProvider(this IUmbracoBuilder builder) + where T : class, IDefaultViewContentProvider + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the culture dictionary factory. - /// - /// The builder. - /// A function creating a culture dictionary factory. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the culture dictionary factory. + /// + /// The builder. + /// A function creating a culture dictionary factory. + public static IUmbracoBuilder SetCultureDictionaryFactory( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the culture dictionary factory. - /// - /// The builder. - /// A factory. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, ICultureDictionaryFactory factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the culture dictionary factory. + /// + /// The builder. + /// A factory. + public static IUmbracoBuilder SetCultureDictionaryFactory( + this IUmbracoBuilder builder, + ICultureDictionaryFactory factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The type of the factory. - /// The builder. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder) - where T : class, IPublishedModelFactory - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The type of the factory. + /// The builder. + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder) + where T : class, IPublishedModelFactory + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The builder. - /// A function creating a published content model factory. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The builder. + /// A function creating a published content model factory. + public static IUmbracoBuilder SetPublishedContentModelFactory( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The builder. - /// A published content model factory. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, IPublishedModelFactory factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The builder. + /// A published content model factory. + public static IUmbracoBuilder SetPublishedContentModelFactory( + this IUmbracoBuilder builder, + IPublishedModelFactory factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the short string helper. - /// - /// The type of the short string helper. - /// The builder. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder) - where T : class, IShortStringHelper - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the short string helper. + /// + /// The type of the short string helper. + /// The builder. + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder) + where T : class, IShortStringHelper + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the short string helper. - /// - /// The builder. - /// A function creating a short string helper. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the short string helper. + /// + /// The builder. + /// A function creating a short string helper. + public static IUmbracoBuilder SetShortStringHelper( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the short string helper. - /// - /// A builder. - /// A short string helper. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) - { - builder.Services.AddUnique(helper); - return builder; - } + /// + /// Sets the short string helper. + /// + /// A builder. + /// A short string helper. + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) + { + builder.Services.AddUnique(helper); + return builder; + } - /// - /// Sets the filesystem used by the MediaFileManager - /// - /// A builder. - /// Factory method to create an IFileSystem implementation used in the MediaFileManager - public static IUmbracoBuilder SetMediaFileSystem(this IUmbracoBuilder builder, - Func filesystemFactory) - { - builder.Services.AddUnique( - provider => - { - IFileSystem filesystem = filesystemFactory(provider); - // We need to use the Filesystems to create a shadow wrapper, - // because shadow wrapper requires the IsScoped delegate from the FileSystems. - // This is used by the scope provider when taking control of the filesystems. - FileSystems fileSystems = provider.GetRequiredService(); - IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); - - return provider.CreateInstance(shadow); - }); - return builder; - } - - /// - /// Register FileSystems with a method to configure the . - /// - /// A builder. - /// Method that configures the . - /// Throws exception if is null. - /// Throws exception if full path can't be resolved successfully. - public static IUmbracoBuilder ConfigureFileSystems(this IUmbracoBuilder builder, - Action configure) - { - if (configure == null) + /// + /// Sets the filesystem used by the MediaFileManager + /// + /// A builder. + /// Factory method to create an IFileSystem implementation used in the MediaFileManager + public static IUmbracoBuilder SetMediaFileSystem( + this IUmbracoBuilder builder, + Func filesystemFactory) + { + builder.Services.AddUnique( + provider => { - throw new ArgumentNullException(nameof(configure)); - } + IFileSystem filesystem = filesystemFactory(provider); - builder.Services.AddUnique( - provider => - { - FileSystems fileSystems = provider.CreateInstance(); - configure(provider, fileSystems); - return fileSystems; - }); - return builder; - } + // We need to use the Filesystems to create a shadow wrapper, + // because shadow wrapper requires the IsScoped delegate from the FileSystems. + // This is used by the scope provider when taking control of the filesystems. + FileSystems fileSystems = provider.GetRequiredService(); + IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); - /// - /// Sets the log viewer. - /// - /// The type of the log viewer. - /// The builder. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) - where T : class, ILogViewer + return provider.CreateInstance(shadow); + }); + return builder; + } + + /// + /// Register FileSystems with a method to configure the . + /// + /// A builder. + /// Method that configures the . + /// Throws exception if is null. + /// Throws exception if full path can't be resolved successfully. + public static IUmbracoBuilder ConfigureFileSystems( + this IUmbracoBuilder builder, + Action configure) + { + if (configure == null) { - builder.Services.AddUnique(); - return builder; + throw new ArgumentNullException(nameof(configure)); } - /// - /// Sets the log viewer. - /// - /// The builder. - /// A function creating a log viewer. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + builder.Services.AddUnique( + provider => + { + FileSystems fileSystems = provider.CreateInstance(); + configure(provider, fileSystems); + return fileSystems; + }); + return builder; + } - /// - /// Sets the log viewer. - /// - /// A builder. - /// A log viewer. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) - { - builder.Services.AddUnique(viewer); - return builder; - } + /// + /// Sets the log viewer. + /// + /// The type of the log viewer. + /// The builder. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) + where T : class, ILogViewer + { + builder.Services.AddUnique(); + return builder; + } + + /// + /// Sets the log viewer. + /// + /// The builder. + /// A function creating a log viewer. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } + + /// + /// Sets the log viewer. + /// + /// A builder. + /// A log viewer. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) + { + builder.Services.AddUnique(viewer); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs index 53e638997b..d788f26f88 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs @@ -1,16 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Infrastructure.WebAssets; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + internal static IUmbracoBuilder AddWebAssets(this IUmbracoBuilder builder) { - internal static IUmbracoBuilder AddWebAssets(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - return builder; - } + builder.Services.AddSingleton(); + return builder; } } diff --git a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs index 94dabf7b4f..eb1bc518ac 100644 --- a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs +++ b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs @@ -1,38 +1,44 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Defines methods that can convert a grid cell value to / from an environment-agnostic string. +/// +/// +/// Grid cell values may contain values such as content identifiers, that would be local +/// to one environment, and need to be converted in order to be deployed. +/// +public interface IGridCellValueConnector { /// - /// Defines methods that can convert a grid cell value to / from an environment-agnostic string. + /// Gets a value indicating whether the connector supports a specified grid editor view. /// - /// Grid cell values may contain values such as content identifiers, that would be local - /// to one environment, and need to be converted in order to be deployed. - public interface IGridCellValueConnector - { - /// - /// Gets a value indicating whether the connector supports a specified grid editor view. - /// - /// The grid editor view. It needs to be the view instead of the alias as the view is really what identifies what kind of connector should be used. Alias can be anything and you can have multiple different aliases using the same kind of view. - /// A value indicating whether the connector supports the grid editor view. - /// Note that can be string.Empty to indicate the "default" connector. - bool IsConnector(string view); + /// + /// The grid editor view. It needs to be the view instead of the alias as the view is really what + /// identifies what kind of connector should be used. Alias can be anything and you can have multiple different aliases + /// using the same kind of view. + /// + /// A value indicating whether the connector supports the grid editor view. + /// Note that can be string.Empty to indicate the "default" connector. + bool IsConnector(string view); - /// - /// Gets the value to be deployed from the control value as a string. - /// - /// The control containing the value. - /// The dependencies of the property. - /// The grid cell value to be deployed. - /// Note that - string? GetValue(GridValue.GridControl gridControl, ICollection dependencies); + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The dependencies of the property. + /// The grid cell value to be deployed. + /// Note that + string? GetValue(GridValue.GridControl gridControl, ICollection dependencies); - /// - /// Allows you to modify the value of a control being deployed. - /// - /// The control being deployed. - /// Follows the pattern of the property value connectors (). The SetValue method is used to modify the value of the . - - void SetValue(GridValue.GridControl gridControl); - } + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// + /// Follows the pattern of the property value connectors (). The SetValue method is + /// used to modify the value of the . + /// + void SetValue(GridValue.GridControl gridControl); } diff --git a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs index 7916de3996..b44a7c993b 100644 --- a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs +++ b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking; @@ -10,12 +6,12 @@ namespace Umbraco.Cms.Infrastructure.DistributedLocking; public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMechanismFactory { - private object _lock = new(); - private bool _initialized; - private IDistributedLockingMechanism _distributedLockingMechanism = null!; + private readonly IEnumerable _distributedLockingMechanisms; private readonly IOptionsMonitor _globalSettings; - private readonly IEnumerable _distributedLockingMechanisms; + private IDistributedLockingMechanism _distributedLockingMechanism = null!; + private bool _initialized; + private object _lock = new(); public DefaultDistributedLockingMechanismFactory( IOptionsMonitor globalSettings, @@ -49,7 +45,8 @@ public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMech if (value == null) { - throw new InvalidOperationException($"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); + throw new InvalidOperationException( + $"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); } } @@ -59,6 +56,6 @@ public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMech return defaultMechanism; } - throw new InvalidOperationException($"Couldn't find an appropriate default distributed locking mechanism."); + throw new InvalidOperationException("Couldn't find an appropriate default distributed locking mechanism."); } } diff --git a/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs b/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs index 23bfed0cd9..2bc6aa252a 100644 --- a/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs @@ -1,94 +1,110 @@ -using System; -using System.Collections.Generic; -using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable { - public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable + public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : this(migrationTypes, null, configuredVersion, targetVersion, productName, canCancel) { - public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) - : this(migrationTypes, null, configuredVersion, targetVersion, productName, canCancel) - { } + } - internal MigrationEventArgs(IList migrationTypes, IMigrationContext? migrationContext, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) - : base(migrationTypes, canCancel) + public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName) + : this(migrationTypes, null, configuredVersion, targetVersion, productName, false) + { + } + + internal MigrationEventArgs(IList migrationTypes, IMigrationContext? migrationContext, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : base(migrationTypes, canCancel) + { + MigrationContext = migrationContext; + ConfiguredSemVersion = configuredVersion; + TargetSemVersion = targetVersion; + ProductName = productName; + } + + /// + /// Returns all migrations that were used in the migration runner + /// + public IList? MigrationsTypes => EventObject; + + /// + /// Gets the origin version of the migration, i.e. the one that is currently installed. + /// + public SemVersion ConfiguredSemVersion { get; } + + /// + /// Gets the target version of the migration. + /// + public SemVersion TargetSemVersion { get; } + + /// + /// Gets the product name. + /// + public string ProductName { get; } + + /// + /// Gets the migration context. + /// + /// Is only available after migrations have run, for post-migrations. + internal IMigrationContext? MigrationContext { get; } + + public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) => Equals(left, right); + + public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) => !Equals(left, right); + + public bool Equals(MigrationEventArgs? other) + { + if (ReferenceEquals(null, other)) { - MigrationContext = migrationContext; - ConfiguredSemVersion = configuredVersion; - TargetSemVersion = targetVersion; - ProductName = productName; + return false; } - public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName) - : this(migrationTypes, null, configuredVersion, targetVersion, productName, false) - { } - - /// - /// Returns all migrations that were used in the migration runner - /// - public IList? MigrationsTypes => EventObject; - - /// - /// Gets the origin version of the migration, i.e. the one that is currently installed. - /// - public SemVersion ConfiguredSemVersion { get; } - - /// - /// Gets the target version of the migration. - /// - public SemVersion TargetSemVersion { get; } - - /// - /// Gets the product name. - /// - public string ProductName { get; } - - /// - /// Gets the migration context. - /// - /// Is only available after migrations have run, for post-migrations. - internal IMigrationContext? MigrationContext { get; } - - public bool Equals(MigrationEventArgs? other) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && ConfiguredSemVersion.Equals(other.ConfiguredSemVersion) && (MigrationContext?.Equals(other.MigrationContext) ?? false) && string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + return true; } - public override bool Equals(object? obj) + return base.Equals(other) && ConfiguredSemVersion.Equals(other.ConfiguredSemVersion) && + (MigrationContext?.Equals(other.MigrationContext) ?? false) && + string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MigrationEventArgs) obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - unchecked + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((MigrationEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); + if (MigrationContext is not null) { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); - if (MigrationContext is not null) - { - hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); - } - hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); - hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); - return hashCode; + hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); } - } - public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) - { - return !Equals(left, right); + hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); + hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); + return hashCode; } } } diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs index b06248c79e..768c1bc7aa 100644 --- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs @@ -2,156 +2,162 @@ // See LICENSE for more details. using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +// TODO: lots of duplicate code in this one, refactor +public sealed class RelateOnTrashNotificationHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - // TODO: lots of duplicate code in this one, refactor - public sealed class RelateOnTrashNotificationHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly IAuditService _auditService; + private readonly IEntityService _entityService; + private readonly IRelationService _relationService; + private readonly IScopeProvider _scopeProvider; + private readonly ILocalizedTextService _textService; + + public RelateOnTrashNotificationHandler( + IRelationService relationService, + IEntityService entityService, + ILocalizedTextService textService, + IAuditService auditService, + IScopeProvider scopeProvider) { - private readonly IRelationService _relationService; - private readonly IEntityService _entityService; - private readonly ILocalizedTextService _textService; - private readonly IAuditService _auditService; - private readonly IScopeProvider _scopeProvider; + _relationService = relationService; + _entityService = entityService; + _textService = textService; + _auditService = auditService; + _scopeProvider = scopeProvider; + } - public RelateOnTrashNotificationHandler( - IRelationService relationService, - IEntityService entityService, - ILocalizedTextService textService, - IAuditService auditService, - IScopeProvider scopeProvider) + public void Handle(ContentMovedNotification notification) + { + foreach (MoveEventInfo item in notification.MoveInfoCollection.Where(x => + x.OriginalPath.Contains(Constants.System.RecycleBinContentString))) { - _relationService = relationService; - _entityService = entityService; - _textService = textService; - _auditService = auditService; - _scopeProvider = scopeProvider; - } + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; + IEnumerable relations = _relationService.GetByChildId(item.Entity.Id); - public void Handle(ContentMovedNotification notification) - { - foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinContentString))) + foreach (IRelation relation in + relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relations = _relationService.GetByChildId(item.Entity.Id); - - foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) - { - _relationService.Delete(relation); - } + _relationService.Delete(relation); } } + } - public void Handle(ContentMovedToRecycleBinNotification notification) + public void Handle(ContentMovedToRecycleBinNotification notification) + { + using (IScope scope = _scopeProvider.CreateScope()) { - using (var scope = _scopeProvider.CreateScope()) + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; + IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); + + // check that the relation-type exists, if not, then recreate it + if (relationType == null) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); + Guid documentObjectType = Constants.ObjectTypes.Document; + const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - // check that the relation-type exists, if not, then recreate it - if (relationType == null) - { - var documentObjectType = Constants.ObjectTypes.Document; - const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); - _relationService.Save(relationType); - } - - foreach (var item in notification.MoveInfoCollection) - { - var originalPath = item.OriginalPath.ToDelimitedList(); - var originalParentId = originalPath.Count > 2 - ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) - : Constants.System.Root; - - //before we can create this relation, we need to ensure that the original parent still exists which - //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - - if (_entityService.Exists(originalParentId)) - { - // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later - var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType); - _relationService.Save(relation); - - _auditService.Add(AuditType.Delete, - item.Entity.WriterId, - item.Entity.Id, - ObjectTypes.GetName(UmbracoObjectTypes.Document), - string.Format(_textService.Localize("recycleBin","contentTrashed"), item.Entity.Id, originalParentId) - ); - } - } - - scope.Complete(); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); + _relationService.Save(relationType); } - } - public void Handle(MediaMovedNotification notification) - { - foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinMediaString))) + foreach (MoveEventInfo item in notification.MoveInfoCollection) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relations = _relationService.GetByChildId(item.Entity.Id); - foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) + IList originalPath = item.OriginalPath.ToDelimitedList(); + var originalParentId = originalPath.Count > 2 + ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) + : Constants.System.Root; + + // before we can create this relation, we need to ensure that the original parent still exists which + // may not be the case if the encompassing transaction also deleted it when this item was moved to the bin + if (_entityService.Exists(originalParentId)) { - _relationService.Delete(relation); + // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later + IRelation relation = + _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? + new Relation(originalParentId, item.Entity.Id, relationType); + _relationService.Save(relation); + + _auditService.Add( + AuditType.Delete, + item.Entity.WriterId, + item.Entity.Id, + UmbracoObjectTypes.Document.GetName(), + string.Format(_textService.Localize("recycleBin", "contentTrashed"), item.Entity.Id, originalParentId)); } } + scope.Complete(); } + } - public void Handle(MediaMovedToRecycleBinNotification notification) + public void Handle(MediaMovedNotification notification) + { + foreach (MoveEventInfo item in notification.MoveInfoCollection.Where(x => + x.OriginalPath.Contains(Constants.System.RecycleBinMediaString))) { - using (var scope = _scopeProvider.CreateScope()) + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + IEnumerable relations = _relationService.GetByChildId(item.Entity.Id); + foreach (IRelation relation in + relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); - // check that the relation-type exists, if not, then recreate it - if (relationType == null) - { - var documentObjectType = Constants.ObjectTypes.Document; - const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); - _relationService.Save(relationType); - } + _relationService.Delete(relation); + } + } + } - foreach (var item in notification.MoveInfoCollection) - { - var originalPath = item.OriginalPath.ToDelimitedList(); - var originalParentId = originalPath.Count > 2 - ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) - : Constants.System.Root; - //before we can create this relation, we need to ensure that the original parent still exists which - //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - if (_entityService.Exists(originalParentId)) - { - // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later - var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType); - _relationService.Save(relation); - _auditService.Add(AuditType.Delete, - item.Entity.CreatorId, - item.Entity.Id, - ObjectTypes.GetName(UmbracoObjectTypes.Media), - string.Format(_textService.Localize("recycleBin","mediaTrashed"), item.Entity.Id, originalParentId) - ); - } - } + public void Handle(MediaMovedToRecycleBinNotification notification) + { + using (IScope scope = _scopeProvider.CreateScope()) + { + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); - scope.Complete(); + // check that the relation-type exists, if not, then recreate it + if (relationType == null) + { + Guid documentObjectType = Constants.ObjectTypes.Document; + const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); + _relationService.Save(relationType); } + foreach (MoveEventInfo item in notification.MoveInfoCollection) + { + IList originalPath = item.OriginalPath.ToDelimitedList(); + var originalParentId = originalPath.Count > 2 + ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) + : Constants.System.Root; + + // before we can create this relation, we need to ensure that the original parent still exists which + // may not be the case if the encompassing transaction also deleted it when this item was moved to the bin + if (_entityService.Exists(originalParentId)) + { + // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later + IRelation relation = + _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? + new Relation(originalParentId, item.Entity.Id, relationType); + _relationService.Save(relation); + _auditService.Add( + AuditType.Delete, + item.Entity.CreatorId, + item.Entity.Id, + UmbracoObjectTypes.Media.GetName(), + string.Format(_textService.Localize("recycleBin", "mediaTrashed"), item.Entity.Id, originalParentId)); + } + } + + scope.Complete(); } } } diff --git a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs index ceb203c15c..db6182ee3e 100644 --- a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs @@ -1,70 +1,88 @@ -using System.Collections.Generic; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine -{ - /// - public abstract class BaseValueSetBuilder : IValueSetBuilder - where TContent : IContentBase - { - protected bool PublishedValuesOnly { get; } - private readonly PropertyEditorCollection _propertyEditors; +namespace Umbraco.Cms.Infrastructure.Examine; - protected BaseValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) +/// +public abstract class BaseValueSetBuilder : IValueSetBuilder + where TContent : IContentBase +{ + private readonly PropertyEditorCollection _propertyEditors; + + protected BaseValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + { + PublishedValuesOnly = publishedValuesOnly; + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + } + + protected bool PublishedValuesOnly { get; } + + /// + public abstract IEnumerable GetValueSets(params TContent[] content); + + protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary>? values) + { + IDataEditor? editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; + if (editor == null) { - PublishedValuesOnly = publishedValuesOnly; - _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); + return; } - /// - public abstract IEnumerable GetValueSets(params TContent[] content); - - protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary>? values) + IEnumerable>> indexVals = + editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); + foreach (KeyValuePair> keyVal in indexVals) { - var editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; - if (editor == null) return; - - var indexVals = editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); - foreach (var keyVal in indexVals) + if (keyVal.Key.IsNullOrWhiteSpace()) { - if (keyVal.Key.IsNullOrWhiteSpace()) continue; + continue; + } - var cultureSuffix = culture == null ? string.Empty : "_" + culture; + var cultureSuffix = culture == null ? string.Empty : "_" + culture; - foreach (var val in keyVal.Value) + foreach (var val in keyVal.Value) + { + switch (val) { - switch (val) - { - //only add the value if its not null or empty (we'll check for string explicitly here too) - case null: + // only add the value if its not null or empty (we'll check for string explicitly here too) + case null: + continue; + case string strVal: + { + if (strVal.IsNullOrWhiteSpace()) + { continue; - case string strVal: - { - if (strVal.IsNullOrWhiteSpace()) continue; - var key = $"{keyVal.Key}{cultureSuffix}"; - if (values?.TryGetValue(key, out var v) ?? false) - values[key] = new List(v) { val }.ToArray(); - else - values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); - } - break; - default: - { - var key = $"{keyVal.Key}{cultureSuffix}"; - if (values?.TryGetValue(key, out var v) ?? false) - values[key] = new List(v) { val }.ToArray(); - else - values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); - } + } - break; + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values?.TryGetValue(key, out IEnumerable? v) ?? false) + { + values[key] = new List(v) { val }.ToArray(); + } + else + { + values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } } + + break; + default: + { + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values?.TryGetValue(key, out IEnumerable? v) ?? false) + { + values[key] = new List(v) { val }.ToArray(); + } + else + { + values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } + } + + break; } } } } - } diff --git a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs index c16370aff8..647429ebc4 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs @@ -1,175 +1,165 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Examine; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a content index +/// +public class ContentIndexPopulator : IndexPopulator { + private readonly IContentService _contentService; + private readonly IValueSetBuilder _contentValueSetBuilder; + private readonly ILogger _logger; + private readonly int? _parentId; + + private readonly bool _publishedValuesOnly; + private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + /// - /// Performs the data lookups required to rebuild a content index + /// This is a static query, it's parameters don't change so store statically /// - public class ContentIndexPopulator : IndexPopulator + private IQuery? _publishedQuery; + + /// + /// Default constructor to lookup all content data + /// + public ContentIndexPopulator( + ILogger logger, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IContentValueSetBuilder contentValueSetBuilder) + : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) { - private readonly IContentService _contentService; - private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; - private readonly IValueSetBuilder _contentValueSetBuilder; + } - /// - /// This is a static query, it's parameters don't change so store statically - /// - private IQuery? _publishedQuery; - private IQuery PublishedQuery => _publishedQuery ??= _umbracoDatabaseFactory.SqlContext.Query().Where(x => x.Published); + /// + /// Optional constructor allowing specifying custom query parameters + /// + public ContentIndexPopulator( + ILogger logger, + bool publishedValuesOnly, + int? parentId, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IValueSetBuilder contentValueSetBuilder) + { + _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + _umbracoDatabaseFactory = umbracoDatabaseFactory ?? throw new ArgumentNullException(nameof(umbracoDatabaseFactory)); + _contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _publishedValuesOnly = publishedValuesOnly; + _parentId = parentId; + } - private readonly bool _publishedValuesOnly; - private readonly int? _parentId; - private readonly ILogger _logger; + private IQuery PublishedQuery => _publishedQuery ??= + _umbracoDatabaseFactory.SqlContext.Query().Where(x => x.Published); - /// - /// Default constructor to lookup all content data - /// - /// - /// - /// - public ContentIndexPopulator( - ILogger logger, - IContentService contentService, - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IContentValueSetBuilder contentValueSetBuilder) - : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) + public override bool IsRegistered(IUmbracoContentIndex index) => + + // check if it should populate based on published values + _publishedValuesOnly == index.PublishedValuesOnly; + + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { + _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); + return; } - /// - /// Optional constructor allowing specifying custom query parameters - /// - public ContentIndexPopulator( - ILogger logger, - bool publishedValuesOnly, - int? parentId, - IContentService contentService, - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IValueSetBuilder contentValueSetBuilder) + const int pageSize = 10000; + var pageIndex = 0; + + var contentParentId = -1; + if (_parentId.HasValue && _parentId.Value > 0) { - _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); - _umbracoDatabaseFactory = umbracoDatabaseFactory ?? throw new ArgumentNullException(nameof(umbracoDatabaseFactory)); - _contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _publishedValuesOnly = publishedValuesOnly; - _parentId = parentId; + contentParentId = _parentId.Value; } - public override bool IsRegistered(IUmbracoContentIndex index) + if (_publishedValuesOnly) { - // check if it should populate based on published values - return _publishedValuesOnly == index.PublishedValuesOnly; + IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); } - - protected override void PopulateIndexes(IReadOnlyList indexes) + else { - if (indexes.Count == 0) - { - _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); - return; - } - - const int pageSize = 10000; - var pageIndex = 0; - - var contentParentId = -1; - if (_parentId.HasValue && _parentId.Value > 0) - { - contentParentId = _parentId.Value; - } - - if (_publishedValuesOnly) - { - IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); - } - else - { - IndexAllContent(contentParentId, pageIndex, pageSize, indexes); - } - } - - protected void IndexAllContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) - { - IContent[] content; - - do - { - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); - - if (content.Length > 0) - { - var valueSets = _contentValueSetBuilder.GetValueSets(content).ToList(); - - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - { - index.IndexItems(valueSets); - } - } - - pageIndex++; - } while (content.Length == pageSize); - } - - protected void IndexPublishedContent(int contentParentId, int pageIndex, int pageSize, - IReadOnlyList indexes) - { - IContent[] content; - - var publishedPages = new HashSet(); - - do - { - //add the published filter - //note: We will filter for published variants in the validator - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, PublishedQuery, - Ordering.By("Path", Direction.Ascending)).ToArray(); - - - if (content.Length > 0) - { - var indexableContent = new List(); - - foreach (var item in content) - { - if (item.Level == 1) - { - // first level pages are always published so no need to filter them - indexableContent.Add(item); - publishedPages.Add(item.Id); - } - else - { - if (publishedPages.Contains(item.ParentId)) - { - // only index when parent is published - publishedPages.Add(item.Id); - indexableContent.Add(item); - } - } - } - - var valueSets = _contentValueSetBuilder.GetValueSets(indexableContent.ToArray()).ToList(); - - foreach (IIndex index in indexes) - { - index.IndexItems(valueSets); - } - } - - pageIndex++; - } while (content.Length == pageSize); + IndexAllContent(contentParentId, pageIndex, pageSize, indexes); } } + protected void IndexAllContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) + { + IContent[] content; + do + { + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); + + if (content.Length > 0) + { + var valueSets = _contentValueSetBuilder.GetValueSets(content).ToList(); + + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) + { + index.IndexItems(valueSets); + } + } + + pageIndex++; + } + while (content.Length == pageSize); + } + + protected void IndexPublishedContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) + { + IContent[] content; + + var publishedPages = new HashSet(); + + do + { + // add the published filter + // note: We will filter for published variants in the validator + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, PublishedQuery, Ordering.By("Path")).ToArray(); + + if (content.Length > 0) + { + var indexableContent = new List(); + + foreach (IContent item in content) + { + if (item.Level == 1) + { + // first level pages are always published so no need to filter them + indexableContent.Add(item); + publishedPages.Add(item.Id); + } + else + { + if (publishedPages.Contains(item.ParentId)) + { + // only index when parent is published + publishedPages.Add(item.Id); + indexableContent.Add(item); + } + } + } + + var valueSets = _contentValueSetBuilder.GetValueSets(indexableContent.ToArray()).ToList(); + + foreach (IIndex index in indexes) + { + index.IndexItems(valueSets); + } + } + + pageIndex++; + } + while (content.Length == pageSize); + } } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index f42293a7a1..05274fc28e 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -8,128 +6,146 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Builds s for items +/// +public class ContentValueSetBuilder : BaseValueSetBuilder, IContentValueSetBuilder, + IPublishedContentValueSetBuilder { - /// - /// Builds s for items - /// - public class ContentValueSetBuilder : BaseValueSetBuilder, IContentValueSetBuilder, IPublishedContentValueSetBuilder + private static readonly object[] NoValue = new[] { "n" }; + private static readonly object[] YesValue = new[] { "y" }; + + private readonly IScopeProvider _scopeProvider; + + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IUserService _userService; + + public ContentValueSetBuilder( + PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + IShortStringHelper shortStringHelper, + IScopeProvider scopeProvider, + bool publishedValuesOnly) + : base(propertyEditors, publishedValuesOnly) { - private readonly UrlSegmentProviderCollection _urlSegmentProviders; - private readonly IUserService _userService; - private readonly IScopeProvider _scopeProvider; - - private readonly IShortStringHelper _shortStringHelper; - - public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, - UrlSegmentProviderCollection urlSegmentProviders, - IUserService userService, - IShortStringHelper shortStringHelper, - IScopeProvider scopeProvider, - bool publishedValuesOnly) - : base(propertyEditors, publishedValuesOnly) - { - _urlSegmentProviders = urlSegmentProviders; - _userService = userService; - _shortStringHelper = shortStringHelper; - _scopeProvider = scopeProvider; - } - - /// - public override IEnumerable GetValueSets(params IContent[] content) - { - Dictionary creatorIds; - Dictionary writerIds; - - // We can lookup all of the creator/writer names at once which can save some - // processing below instead of one by one. - using (var scope = _scopeProvider.CreateScope()) - { - creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) - .ToDictionary(x => x.Id, x => x); - writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) - .ToDictionary(x => x.Id, x => x); - scope.Complete(); - } - - return GetValueSetsEnumerable(content, creatorIds, writerIds); - } - - private IEnumerable GetValueSetsEnumerable(IContent[] content, Dictionary creatorIds, Dictionary writerIds) - { - // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways - // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since - // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` - // references and then each array is an array of `FieldValue[]` and values are assigned accordingly. Not sure if it will make a difference or not. - - foreach (var c in content) - { - var isVariant = c.ContentType.VariesByCulture(); - - var urlValue = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); //Always add invariant urlName - var values = new Dictionary> - { - {"icon", c.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {UmbracoExamineFieldNames.PublishedFieldName, new object[] {c.Published ? "y" : "n"}}, //Always add invariant published value - {"id", new object[] {c.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {c.Key}}, - {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, - {"level", new object[] {c.Level}}, - {"creatorID", new object[] {c.CreatorId}}, - {"sortOrder", new object[] {c.SortOrder}}, - {"createDate", new object[] {c.CreateDate}}, //Always add invariant createDate - {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate - {UmbracoExamineFieldNames.NodeNameFieldName, (PublishedValuesOnly //Always add invariant nodeName - ? c.PublishName?.Yield() - : c.Name?.Yield()) ?? Enumerable.Empty()}, - {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName - {"path", c.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name! : "??").Yield() }, - {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name! : "??").Yield() }, - {"writerID", new object[] {c.WriterId}}, - {"templateID", new object[] {c.TemplateId ?? 0}}, - {UmbracoExamineFieldNames.VariesByCultureFieldName, new object[] {"n"}}, - }; - - if (isVariant) - { - values[UmbracoExamineFieldNames.VariesByCultureFieldName] = new object[] { "y" }; - - foreach (var culture in c.AvailableCultures) - { - var variantUrl = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, culture); - var lowerCulture = culture.ToLowerInvariant(); - values[$"urlName_{lowerCulture}"] = variantUrl?.Yield() ?? Enumerable.Empty(); - values[$"nodeName_{lowerCulture}"] = (PublishedValuesOnly - ? c.GetPublishName(culture)?.Yield() - : c.GetCultureName(culture)?.Yield()) ?? Enumerable.Empty(); - values[$"{UmbracoExamineFieldNames.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? "y" : "n").Yield(); - values[$"updateDate_{lowerCulture}"] = (PublishedValuesOnly - ? c.GetPublishDate(culture) - : c.GetUpdateDate(culture))?.Yield() ?? Enumerable.Empty(); - } - } - - foreach (var property in c.Properties) - { - if (!property.PropertyType.VariesByCulture()) - { - AddPropertyValue(property, null, null, values); - } - else - { - foreach (var culture in c.AvailableCultures) - AddPropertyValue(property, culture.ToLowerInvariant(), null, values); - } - } - - var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); - - yield return vs; - } - } + _urlSegmentProviders = urlSegmentProviders; + _userService = userService; + _shortStringHelper = shortStringHelper; + _scopeProvider = scopeProvider; } + /// + public override IEnumerable GetValueSets(params IContent[] content) + { + Dictionary creatorIds; + Dictionary writerIds; + + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (IScope scope = _scopeProvider.CreateScope()) + { + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); + } + + return GetValueSetsEnumerable(content, creatorIds, writerIds); + } + + private IEnumerable GetValueSetsEnumerable(IContent[] content, Dictionary creatorIds, Dictionary writerIds) + { + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways + // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since + // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` + // references and then each array is an array of `FieldValue[]` and values are assigned accordingly. Not sure if it will make a difference or not. + foreach (IContent c in content) + { + var isVariant = c.ContentType.VariesByCulture(); + + var urlValue = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); // Always add invariant urlName + var values = new Dictionary> + { + { "icon", c.ContentType.Icon?.Yield() ?? Enumerable.Empty() }, + { + UmbracoExamineFieldNames.PublishedFieldName, c.Published ? YesValue : NoValue + }, // Always add invariant published value + { "id", new object[] { c.Id } }, + { UmbracoExamineFieldNames.NodeKeyFieldName, new object[] { c.Key } }, + { "parentID", new object[] { c.Level > 1 ? c.ParentId : -1 } }, + { "level", new object[] { c.Level } }, + { "creatorID", new object[] { c.CreatorId } }, + { "sortOrder", new object[] { c.SortOrder } }, + { "createDate", new object[] { c.CreateDate } }, // Always add invariant createDate + { "updateDate", new object[] { c.UpdateDate } }, // Always add invariant updateDate + { + UmbracoExamineFieldNames.NodeNameFieldName, (PublishedValuesOnly // Always add invariant nodeName + ? c.PublishName?.Yield() + : c.Name?.Yield()) ?? Enumerable.Empty() + }, + { "urlName", urlValue?.Yield() ?? Enumerable.Empty() }, // Always add invariant urlName + { "path", c.Path.Yield() }, + { "nodeType", c.ContentType.Id.ToString().Yield() }, + { + "creatorName", + (creatorIds.TryGetValue(c.CreatorId, out IProfile? creatorProfile) ? creatorProfile.Name! : "??") + .Yield() + }, + { + "writerName", + (writerIds.TryGetValue(c.WriterId, out IProfile? writerProfile) ? writerProfile.Name! : "??") + .Yield() + }, + { "writerID", new object[] { c.WriterId } }, + { "templateID", new object[] { c.TemplateId ?? 0 } }, + { UmbracoExamineFieldNames.VariesByCultureFieldName, NoValue }, + }; + + if (isVariant) + { + values[UmbracoExamineFieldNames.VariesByCultureFieldName] = YesValue; + + foreach (var culture in c.AvailableCultures) + { + var variantUrl = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, culture); + var lowerCulture = culture.ToLowerInvariant(); + values[$"urlName_{lowerCulture}"] = variantUrl?.Yield() ?? Enumerable.Empty(); + values[$"nodeName_{lowerCulture}"] = (PublishedValuesOnly + ? c.GetPublishName(culture)?.Yield() + : c.GetCultureName(culture)?.Yield()) ?? Enumerable.Empty(); + values[$"{UmbracoExamineFieldNames.PublishedFieldName}_{lowerCulture}"] = + (c.IsCulturePublished(culture) ? "y" : "n").Yield(); + values[$"updateDate_{lowerCulture}"] = (PublishedValuesOnly + ? c.GetPublishDate(culture) + : c.GetUpdateDate(culture))?.Yield() ?? Enumerable.Empty(); + } + } + + foreach (IProperty property in c.Properties) + { + if (!property.PropertyType.VariesByCulture()) + { + AddPropertyValue(property, null, null, values); + } + else + { + foreach (var culture in c.AvailableCultures) + { + AddPropertyValue(property, culture.ToLowerInvariant(), null, values); + } + } + } + + var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); + + yield return vs; + } + } } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs index ed57184cb8..7120bd37d4 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs @@ -1,161 +1,191 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Examine; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to validate a ValueSet for content/media - based on permissions, parent id, etc.... +/// +public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator { - /// - /// Used to validate a ValueSet for content/media - based on permissions, parent id, etc.... - /// - public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator + private const string PathKey = "path"; + private static readonly IEnumerable ValidCategories = new[] {IndexTypes.Content, IndexTypes.Media}; + private readonly IPublicAccessService? _publicAccessService; + private readonly IScopeProvider? _scopeProvider; + + // used for tests + public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) + : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) { - private readonly IPublicAccessService? _publicAccessService; - private readonly IScopeProvider? _scopeProvider; - private const string PathKey = "path"; - private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Content, IndexTypes.Media }; - protected override IEnumerable ValidIndexCategories => ValidCategories; + } - public bool PublishedValuesOnly { get; } - public bool SupportProtectedContent { get; } - public int? ParentId { get; } + public ContentValueSetValidator( + bool publishedValuesOnly, + bool supportProtectedContent, + IPublicAccessService? publicAccessService, + IScopeProvider? scopeProvider, + int? parentId = null, + IEnumerable? includeItemTypes = null, + IEnumerable? excludeItemTypes = null) + : base(includeItemTypes, excludeItemTypes, null, null) + { + PublishedValuesOnly = publishedValuesOnly; + SupportProtectedContent = supportProtectedContent; + ParentId = parentId; + _publicAccessService = publicAccessService; + _scopeProvider = scopeProvider; + } - public bool ValidatePath(string path, string category) + protected override IEnumerable ValidIndexCategories => ValidCategories; + + public bool PublishedValuesOnly { get; } + public bool SupportProtectedContent { get; } + public int? ParentId { get; } + + public bool ValidatePath(string path, string category) + { + //check if this document is a descendent of the parent + if (ParentId.HasValue && ParentId.Value > 0) { - //check if this document is a descendent of the parent - if (ParentId.HasValue && ParentId.Value > 0) + // we cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by parent Id in the cases that umbraco data is moved to an illegal parent. + if (!path.Contains(string.Concat(",", ParentId.Value.ToString(CultureInfo.InvariantCulture), ","))) { - // we cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there - // because we need to remove anything that doesn't pass by parent Id in the cases that umbraco data is moved to an illegal parent. - if (!path.Contains(string.Concat(",", ParentId.Value.ToString(CultureInfo.InvariantCulture), ","))) - return false; + return false; } - - return true; } - public bool ValidateRecycleBin(string path, string category) - { - var recycleBinId = category == IndexTypes.Content ? Constants.System.RecycleBinContentString : Constants.System.RecycleBinMediaString; + return true; + } - //check for recycle bin - if (PublishedValuesOnly) + public bool ValidateRecycleBin(string path, string category) + { + var recycleBinId = category == IndexTypes.Content + ? Constants.System.RecycleBinContentString + : Constants.System.RecycleBinMediaString; + + //check for recycle bin + if (PublishedValuesOnly) + { + if (path.Contains(string.Concat(",", recycleBinId, ","))) { - if (path.Contains(string.Concat(",", recycleBinId, ","))) - return false; + return false; } - return true; } - public bool ValidateProtectedContent(string path, string category) + return true; + } + + public bool ValidateProtectedContent(string path, string category) + { + if (category == IndexTypes.Content && !SupportProtectedContent) { - if (category == IndexTypes.Content && !SupportProtectedContent) + //if the service is null we can't look this up so we'll return false + if (_publicAccessService == null || _scopeProvider == null) { - //if the service is null we can't look this up so we'll return false - if (_publicAccessService == null || _scopeProvider == null) + return false; + } + + // explicit scope since we may be in a background thread + using (_scopeProvider.CreateScope(autoComplete: true)) + { + if (_publicAccessService.IsProtected(path).Success) { return false; } - - // explicit scope since we may be in a background thread - using (_scopeProvider.CreateScope(autoComplete: true)) - { - if (_publicAccessService.IsProtected(path).Success) - { - return false; - } - } } - - return true; } - // used for tests - public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, - IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) - : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) + return true; + } + + public override ValueSetValidationResult Validate(ValueSet valueSet) + { + ValueSetValidationResult baseValidate = base.Validate(valueSet); + valueSet = baseValidate.ValueSet; + if (baseValidate.Status == ValueSetValidationStatus.Failed) { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); } - public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, - IPublicAccessService? publicAccessService, - IScopeProvider? scopeProvider, - int? parentId = null, - IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) - : base(includeItemTypes, excludeItemTypes, null, null) - { - PublishedValuesOnly = publishedValuesOnly; - SupportProtectedContent = supportProtectedContent; - ParentId = parentId; - _publicAccessService = publicAccessService; - _scopeProvider = scopeProvider; - } + var isFiltered = baseValidate.Status == ValueSetValidationStatus.Filtered; - public override ValueSetValidationResult Validate(ValueSet valueSet) + var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); + //check for published content + if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) { - var baseValidate = base.Validate(valueSet); - valueSet = baseValidate.ValueSet; - if (baseValidate.Status == ValueSetValidationStatus.Failed) + if (!valueSet.Values.TryGetValue(UmbracoExamineFieldNames.PublishedFieldName, out IReadOnlyList? published)) + { return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - var isFiltered = baseValidate.Status == ValueSetValidationStatus.Filtered; - - var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - //check for published content - if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) + if (!published[0].Equals("y")) { - if (!valueSet.Values.TryGetValue(UmbracoExamineFieldNames.PublishedFieldName, out var published)) - { - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - } + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - if (!published[0].Equals("y")) + //deal with variants, if there are unpublished variants than we need to remove them from the value set + if (valueSet.Values.TryGetValue(UmbracoExamineFieldNames.VariesByCultureFieldName, out IReadOnlyList? variesByCulture) + && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) + { + //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values + foreach (KeyValuePair> publishField in valueSet.Values + .Where(x => x.Key.StartsWith($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) { - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - } - - //deal with variants, if there are unpublished variants than we need to remove them from the value set - if (valueSet.Values.TryGetValue(UmbracoExamineFieldNames.VariesByCultureFieldName, out var variesByCulture) - && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) - { - //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values - foreach (var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) + if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) { - if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) + //this culture is not published, so remove all of these culture values + var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); + foreach (KeyValuePair> cultureField in valueSet.Values + .Where(x => x.Key.InvariantEndsWith(cultureSuffix)).ToList()) { - //this culture is not published, so remove all of these culture values - var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); - foreach (var cultureField in valueSet.Values.Where(x => x.Key.InvariantEndsWith(cultureSuffix)).ToList()) - { - filteredValues.Remove(cultureField.Key); - isFiltered = true; - } + filteredValues.Remove(cultureField.Key); + isFiltered = true; } } } } - - //must have a 'path' - if (!valueSet.Values.TryGetValue(PathKey, out var pathValues)) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues.Count == 0) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues[0] == null) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues[0].ToString().IsNullOrWhiteSpace()) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - var path = pathValues[0].ToString(); - - var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x=>x.Key, x=> (IEnumerable)x.Value)); - // We need to validate the path of the content based on ParentId, protected content and recycle bin rules. - // We cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there - // because we need to remove anything that doesn't pass by protected content in the cases that umbraco data is moved to an illegal parent. - if (!ValidatePath(path!, valueSet.Category) - || !ValidateRecycleBin(path!, valueSet.Category) - || !ValidateProtectedContent(path!, valueSet.Category)) - return new ValueSetValidationResult(ValueSetValidationStatus.Filtered, filteredValueSet); - - return new ValueSetValidationResult(isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } + + //must have a 'path' + if (!valueSet.Values.TryGetValue(PathKey, out IReadOnlyList? pathValues)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues.Count == 0) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues[0] == null) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues[0].ToString().IsNullOrWhiteSpace()) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + var path = pathValues[0].ToString(); + + var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); + // We need to validate the path of the content based on ParentId, protected content and recycle bin rules. + // We cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by protected content in the cases that umbraco data is moved to an illegal parent. + if (!ValidatePath(path!, valueSet.Category) + || !ValidateRecycleBin(path!, valueSet.Category) + || !ValidateProtectedContent(path!, valueSet.Category)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Filtered, filteredValueSet); + } + + return new ValueSetValidationResult( + isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs index 076353a990..6ac45b4184 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs @@ -1,94 +1,106 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Examine; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for Examine. +/// +public static class ExamineExtensions { /// - /// Extension methods for Examine. + /// Creates an containing all content from the + /// . /// - public static class ExamineExtensions + /// The search results. + /// The cache to fetch the content from. + /// + /// An containing all content. + /// + /// cache + /// + /// Search results are skipped if it can't be fetched from the by its integer id. + /// + public static IEnumerable ToPublishedSearchResults( + this IEnumerable results, + IPublishedCache? cache) { - /// - /// Creates an containing all content from the . - /// - /// The search results. - /// The cache to fetch the content from. - /// - /// An containing all content. - /// - /// cache - /// - /// Search results are skipped if it can't be fetched from the by its integer id. - /// - public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedCache? cache) + if (cache == null) { - if (cache == null) throw new ArgumentNullException(nameof(cache)); + throw new ArgumentNullException(nameof(cache)); + } - var publishedSearchResults = new List(); + var publishedSearchResults = new List(); - foreach (var result in results) + foreach (ISearchResult result in results) + { + if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId)) { - if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && - cache.GetById(contentId) is IPublishedContent content) + IPublishedContent? content = cache.GetById(contentId); + if (content is not null) { publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); } } - - return publishedSearchResults; } - /// - /// Creates an containing all content, media or members from the . - /// - /// The search results. - /// The snapshot. - /// - /// An containing all content, media or members. - /// - /// snapshot - /// - /// Search results are skipped if it can't be fetched from the respective cache by its integer id. - /// - public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedSnapshot snapshot) + return publishedSearchResults; + } + + /// + /// Creates an containing all content, media or members from the + /// . + /// + /// The search results. + /// The snapshot. + /// + /// An containing all content, media or members. + /// + /// snapshot + /// + /// Search results are skipped if it can't be fetched from the respective cache by its integer id. + /// + public static IEnumerable ToPublishedSearchResults( + this IEnumerable results, + IPublishedSnapshot snapshot) + { + if (snapshot == null) { - if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); + throw new ArgumentNullException(nameof(snapshot)); + } - var publishedSearchResults = new List(); + var publishedSearchResults = new List(); - foreach (var result in results) + foreach (ISearchResult result in results) + { + if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && + result.Values.TryGetValue(ExamineFieldNames.CategoryFieldName, out var indexType)) { - if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && - result.Values.TryGetValue(ExamineFieldNames.CategoryFieldName, out var indexType)) + IPublishedContent? content; + switch (indexType) { - IPublishedContent? content; - switch (indexType) - { - case IndexTypes.Content: - content = snapshot.Content?.GetById(contentId); - break; - case IndexTypes.Media: - content = snapshot.Media?.GetById(contentId); - break; - case IndexTypes.Member: - throw new NotSupportedException("Cannot convert search results to member instances"); - default: - continue; - } + case IndexTypes.Content: + content = snapshot.Content?.GetById(contentId); + break; + case IndexTypes.Media: + content = snapshot.Media?.GetById(contentId); + break; + case IndexTypes.Member: + throw new NotSupportedException("Cannot convert search results to member instances"); + default: + continue; + } - if (content != null) - { - publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); - } + if (content != null) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); } } - - return publishedSearchResults; } + + return publishedSearchResults; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs index bb5a4f5a46..681efa0326 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs @@ -1,25 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +[DataContract(Name = "indexer", Namespace = "")] +public class ExamineIndexModel { - [DataContract(Name = "indexer", Namespace = "")] - public class ExamineIndexModel - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "healthStatus")] - public string? HealthStatus { get; set; } + [DataMember(Name = "healthStatus")] + public string? HealthStatus { get; set; } - [DataMember(Name = "isHealthy")] - public bool IsHealthy => HealthStatus == "Healthy"; + [DataMember(Name = "isHealthy")] + public bool IsHealthy => HealthStatus == "Healthy"; - [DataMember(Name = "providerProperties")] - public IReadOnlyDictionary? ProviderProperties { get; set; } + [DataMember(Name = "providerProperties")] + public IReadOnlyDictionary? ProviderProperties { get; set; } - [DataMember(Name = "canRebuild")] - public bool CanRebuild { get; set; } - - } + [DataMember(Name = "canRebuild")] + public bool CanRebuild { get; set; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index ef6d361970..a1c70d0ec3 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -1,11 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; @@ -13,203 +8,205 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.HostedServices; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class ExamineIndexRebuilder : IIndexRebuilder { - public class ExamineIndexRebuilder : IIndexRebuilder + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IExamineManager _examineManager; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IEnumerable _populators; + private readonly object _rebuildLocker = new(); + private readonly IRuntimeState _runtimeState; + + /// + /// Initializes a new instance of the class. + /// + public ExamineIndexRebuilder( + IMainDom mainDom, + IRuntimeState runtimeState, + ILogger logger, + IExamineManager examineManager, + IEnumerable populators, + IBackgroundTaskQueue backgroundTaskQueue) { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly IMainDom _mainDom; - private readonly IRuntimeState _runtimeState; - private readonly ILogger _logger; - private readonly IExamineManager _examineManager; - private readonly IEnumerable _populators; - private readonly object _rebuildLocker = new(); + _mainDom = mainDom; + _runtimeState = runtimeState; + _logger = logger; + _examineManager = examineManager; + _populators = populators; + _backgroundTaskQueue = backgroundTaskQueue; + } - /// - /// Initializes a new instance of the class. - /// - public ExamineIndexRebuilder( - IMainDom mainDom, - IRuntimeState runtimeState, - ILogger logger, - IExamineManager examineManager, - IEnumerable populators, - IBackgroundTaskQueue backgroundTaskQueue) + public bool CanRebuild(string indexName) + { + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) { - _mainDom = mainDom; - _runtimeState = runtimeState; - _logger = logger; - _examineManager = examineManager; - _populators = populators; - _backgroundTaskQueue = backgroundTaskQueue; + throw new InvalidOperationException("No index found by name " + indexName); } - public bool CanRebuild(string indexName) - { - if (!_examineManager.TryGetIndex(indexName, out IIndex index)) - { - throw new InvalidOperationException("No index found by name " + indexName); - } + return _populators.Any(x => x.IsRegistered(index)); + } - return _populators.Any(x => x.IsRegistered(index)); + public virtual void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) + { + if (delay == null) + { + delay = TimeSpan.Zero; } - public virtual void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) + if (!CanRun()) { - if (delay == null) - { - delay = TimeSpan.Zero; - } + return; + } - if (!CanRun()) - { - return; - } + if (useBackgroundThread) + { + _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName); - if (useBackgroundThread) - { - _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.",indexName); + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken))); + } + else + { + RebuildIndex(indexName, delay.Value, CancellationToken.None); + } + } - _backgroundTaskQueue.QueueBackgroundWorkItem( - cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken))); + public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) + { + if (delay == null) + { + delay = TimeSpan.Zero; + } + + if (!CanRun()) + { + return; + } + + if (useBackgroundThread) + { + _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); + + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => + { + // This is a fire/forget task spawned by the background thread queue (which means we + // don't need to worry about ExecutionContext flowing). + Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); + + // immediately return so the queue isn't waiting. + return Task.CompletedTask; + }); + } + else + { + RebuildIndexes(onlyEmptyIndexes, delay.Value, CancellationToken.None); + } + } + + private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; + + private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) + { + if (delay > TimeSpan.Zero) + { + Thread.Sleep(delay); + } + + try + { + if (!Monitor.TryEnter(_rebuildLocker)) + { + _logger.LogWarning( + "Call was made to RebuildIndexes but the task runner for rebuilding is already running"); } else { - RebuildIndex(indexName, delay.Value, CancellationToken.None); - } - } - - public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) - { - if (delay == null) - { - delay = TimeSpan.Zero; - } - - if (!CanRun()) - { - return; - } - - if (useBackgroundThread) - { - _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); - - _backgroundTaskQueue.QueueBackgroundWorkItem( - cancellationToken => - { - // This is a fire/forget task spawned by the background thread queue (which means we - // don't need to worry about ExecutionContext flowing). - Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); - - // immediately return so the queue isn't waiting. - return Task.CompletedTask; - }); - } - else - { - RebuildIndexes(onlyEmptyIndexes, delay.Value, CancellationToken.None); - } - } - - private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; - - private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) - { - if (delay > TimeSpan.Zero) - { - Thread.Sleep(delay); - } - - try - { - if (!Monitor.TryEnter(_rebuildLocker)) + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) { - _logger.LogWarning("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); + throw new InvalidOperationException($"No index found with name {indexName}"); } - else + + index.CreateIndex(); // clear the index + foreach (IIndexPopulator populator in _populators) { - if (!_examineManager.TryGetIndex(indexName, out IIndex index)) - { - throw new InvalidOperationException($"No index found with name {indexName}"); - } - - index.CreateIndex(); // clear the index - foreach (IIndexPopulator populator in _populators) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - populator.Populate(index); - } - } - } - finally - { - if (Monitor.IsEntered(_rebuildLocker)) - { - Monitor.Exit(_rebuildLocker); - } - } - } - - private void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) - { - if (delay > TimeSpan.Zero) - { - Thread.Sleep(delay); - } - - try - { - if (!Monitor.TryEnter(_rebuildLocker)) - { - _logger.LogWarning($"Call was made to {nameof(RebuildIndexes)} but the task runner for rebuilding is already running"); - } - else - { - // If an index exists but it has zero docs we'll consider it empty and rebuild - IIndex[] indexes = (onlyEmptyIndexes - ? _examineManager.Indexes.Where(x => !x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0)) - : _examineManager.Indexes).ToArray(); - - if (indexes.Length == 0) + if (cancellationToken.IsCancellationRequested) { return; } - foreach (IIndex index in indexes) + populator.Populate(index); + } + } + } + finally + { + if (Monitor.IsEntered(_rebuildLocker)) + { + Monitor.Exit(_rebuildLocker); + } + } + } + + private void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) + { + if (delay > TimeSpan.Zero) + { + Thread.Sleep(delay); + } + + try + { + if (!Monitor.TryEnter(_rebuildLocker)) + { + _logger.LogWarning( + $"Call was made to {nameof(RebuildIndexes)} but the task runner for rebuilding is already running"); + } + else + { + // If an index exists but it has zero docs we'll consider it empty and rebuild + IIndex[] indexes = (onlyEmptyIndexes + ? _examineManager.Indexes.Where(x => + !x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0)) + : _examineManager.Indexes).ToArray(); + + if (indexes.Length == 0) + { + return; + } + + foreach (IIndex index in indexes) + { + index.CreateIndex(); // clear the index + } + + // run each populator over the indexes + foreach (IIndexPopulator populator in _populators) + { + if (cancellationToken.IsCancellationRequested) { - index.CreateIndex(); // clear the index + return; } - // run each populator over the indexes - foreach (IIndexPopulator populator in _populators) + try { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - try - { - populator.Populate(indexes); - } - catch (Exception e) - { - _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); - } + populator.Populate(indexes); + } + catch (Exception e) + { + _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); } } } - finally + } + finally + { + if (Monitor.IsEntered(_rebuildLocker)) { - if (Monitor.IsEntered(_rebuildLocker)) - { - Monitor.Exit(_rebuildLocker); - } + Monitor.Exit(_rebuildLocker); } } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs b/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs index 1fd30de319..aa99ce3fdb 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs @@ -1,17 +1,10 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +[DataContract(Name = "searcher", Namespace = "")] +public class ExamineSearcherModel { - [DataContract(Name = "searcher", Namespace = "")] - public class ExamineSearcherModel - { - public ExamineSearcherModel() - { - } - - [DataMember(Name = "name")] - public string? Name { get; set; } - - } - + [DataMember(Name = "name")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs index c8a07f6193..fb3d7e0720 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Examine; using Examine.Search; using Microsoft.Extensions.Logging; @@ -14,425 +10,451 @@ using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Indexing handler for Examine indexes +/// +internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler { - /// - /// Indexing handler for Examine indexes - /// - internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler + // the default enlist priority is 100 + // enlist with a lower priority to ensure that anything "default" runs after us + // but greater that SafeXmlReaderWriter priority which is 60 + private const int EnlistPriority = 80; + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IContentValueSetBuilder _contentValueSetBuilder; + private readonly Lazy _enabled; + private readonly IExamineManager _examineManager; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IValueSetBuilder _mediaValueSetBuilder; + private readonly IValueSetBuilder _memberValueSetBuilder; + private readonly IProfilingLogger _profilingLogger; + private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; + private readonly ICoreScopeProvider _scopeProvider; + + public ExamineUmbracoIndexingHandler( + IMainDom mainDom, + ILogger logger, + IProfilingLogger profilingLogger, + ICoreScopeProvider scopeProvider, + IExamineManager examineManager, + IBackgroundTaskQueue backgroundTaskQueue, + IContentValueSetBuilder contentValueSetBuilder, + IPublishedContentValueSetBuilder publishedContentValueSetBuilder, + IValueSetBuilder mediaValueSetBuilder, + IValueSetBuilder memberValueSetBuilder) { - // the default enlist priority is 100 - // enlist with a lower priority to ensure that anything "default" runs after us - // but greater that SafeXmlReaderWriter priority which is 60 - private const int EnlistPriority = 80; - private readonly IMainDom _mainDom; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IExamineManager _examineManager; - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly IContentValueSetBuilder _contentValueSetBuilder; - private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; - private readonly IValueSetBuilder _mediaValueSetBuilder; - private readonly IValueSetBuilder _memberValueSetBuilder; - private readonly Lazy _enabled; - - public ExamineUmbracoIndexingHandler( - IMainDom mainDom, - ILogger logger, - IProfilingLogger profilingLogger, - ICoreScopeProvider scopeProvider, - IExamineManager examineManager, - IBackgroundTaskQueue backgroundTaskQueue, - IContentValueSetBuilder contentValueSetBuilder, - IPublishedContentValueSetBuilder publishedContentValueSetBuilder, - IValueSetBuilder mediaValueSetBuilder, - IValueSetBuilder memberValueSetBuilder) - { - _mainDom = mainDom; - _logger = logger; - _profilingLogger = profilingLogger; - _scopeProvider = scopeProvider; - _examineManager = examineManager; - _backgroundTaskQueue = backgroundTaskQueue; - _contentValueSetBuilder = contentValueSetBuilder; - _publishedContentValueSetBuilder = publishedContentValueSetBuilder; - _mediaValueSetBuilder = mediaValueSetBuilder; - _memberValueSetBuilder = memberValueSetBuilder; - _enabled = new Lazy(IsEnabled); - } - - /// - /// Used to lazily check if Examine Index handling is enabled - /// - /// - private bool IsEnabled() - { - //let's deal with shutting down Examine with MainDom - var examineShutdownRegistered = _mainDom.Register(release: () => - { - using (_profilingLogger.TraceDuration("Examine shutting down")) - { - _examineManager.Dispose(); - } - }); - - if (!examineShutdownRegistered) - { - _logger.LogInformation("Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); - - //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! - Suspendable.ExamineEvents.SuspendIndexers(_logger); - return false; //exit, do not continue - } - - _logger.LogDebug("Examine shutdown registered with MainDom"); - - var registeredIndexers = _examineManager.Indexes.OfType().Count(x => x.EnableDefaultEventHandler); - - _logger.LogInformation("Adding examine event handlers for {RegisteredIndexers} index providers.", registeredIndexers); - - // don't bind event handlers if we're not suppose to listen - if (registeredIndexers == 0) - { - return false; - } - - return true; - } - - /// - public bool Enabled => _enabled.Value; - - /// - public void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished)); - } - else - { - DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished); - } - } - - /// - public void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedDeleteIndex(this, entityIds, keepIfUnpublished)); - } - else - { - DeferedDeleteIndex.Execute(this, entityIds, keepIfUnpublished); - } - } - - /// - public void ReIndexForContent(IContent sender, bool isPublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForContent(_backgroundTaskQueue, this, sender, isPublished)); - } - else - { - DeferedReIndexForContent.Execute(_backgroundTaskQueue, this, sender, isPublished); - } - } - - /// - public void ReIndexForMedia(IMedia sender, bool isPublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForMedia(_backgroundTaskQueue, this, sender, isPublished)); - } - else - { - DeferedReIndexForMedia.Execute(_backgroundTaskQueue, this, sender, isPublished); - } - } - - /// - public void ReIndexForMember(IMember member) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForMember(_backgroundTaskQueue, this, member)); - } - else - { - DeferedReIndexForMember.Execute(_backgroundTaskQueue, this, member); - } - } - - /// - public void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes) - { - const int pageSize = 500; - - //Delete all content of this content/media/member type that is in any content indexer by looking up matched examine docs - foreach (var id in removedContentTypes) - { - foreach (var index in _examineManager.Indexes.OfType()) - { - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) - { - //paging with examine, see https://shazwazza.com/post/paging-with-examine/ - var results = index.Searcher - .CreateQuery() - .Field("nodeType", id.ToInvariantString()) - .Execute(QueryOptions.SkipTake(page * pageSize, pageSize)); - total = results.TotalItemCount; - - foreach (ISearchResult item in results) - { - if (int.TryParse(item.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out int contentId)) - { - DeleteIndexForEntity(contentId, false); - } - } - - page++; - } - } - } - } - - #region Deferred Actions - private class DeferedActions - { - private readonly List _actions = new List(); - - public static DeferedActions? Get(ICoreScopeProvider scopeProvider) - { - IScopeContext? scopeContext = scopeProvider.Context; - - return scopeContext?.Enlist("examineEvents", - () => new DeferedActions(), // creator - (completed, actions) => // action - { - if (completed) - { - actions?.Execute(); - } - }, EnlistPriority); - } - - public void Add(DeferedAction action) => _actions.Add(action); - - private void Execute() - { - foreach (DeferedAction action in _actions) - { - action.Execute(); - } - } - } - - /// - /// An action that will execute at the end of the Scope being completed - /// - private abstract class DeferedAction - { - public virtual void Execute() - { } - } - - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForContent : DeferedAction - { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IContent _content; - private readonly bool _isPublished; - - public DeferedReIndexForContent(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) - { - _backgroundTaskQueue = backgroundTaskQueue; - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _content = content; - _isPublished = isPublished; - } - - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _content, _isPublished); - - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) - => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => - { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - - // for content we have a different builder for published vs unpublished - // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published - var builders = new Dictionary>> - { - [true] = new Lazy>(() => examineUmbracoIndexingHandler._publishedContentValueSetBuilder.GetValueSets(content).ToList()), - [false] = new Lazy>(() => examineUmbracoIndexingHandler._contentValueSetBuilder.GetValueSets(content).ToList()) - }; - - // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.CompletedTask; - } - - List valueSet = builders[index.PublishedValuesOnly].Value; - index.IndexItems(valueSet); - } - - return Task.CompletedTask; - }); - } - - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForMedia : DeferedAction - { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IMedia _media; - private readonly bool _isPublished; - - public DeferedReIndexForMedia(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) - { - _backgroundTaskQueue = backgroundTaskQueue; - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _media = media; - _isPublished = isPublished; - } - - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _media, _isPublished); - - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) => - // perform the ValueSet lookup on a background thread - backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => - { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - - var valueSet = examineUmbracoIndexingHandler._mediaValueSetBuilder.GetValueSets(media).ToList(); - - // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) - { - index.IndexItems(valueSet); - } - - return Task.CompletedTask; - }); - } - - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForMember : DeferedAction - { - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IMember _member; - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - - public DeferedReIndexForMember(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) - { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _member = member; - _backgroundTaskQueue = backgroundTaskQueue; - } - - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _member); - - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) => - // perform the ValueSet lookup on a background thread - backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => - { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - - var valueSet = examineUmbracoIndexingHandler._memberValueSetBuilder.GetValueSets(member).ToList(); - - // only process for IUmbracoMemberIndex (not content indexes) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => x.EnableDefaultEventHandler)) - { - index.IndexItems(valueSet); - } - - return Task.CompletedTask; - }); - } - - private class DeferedDeleteIndex : DeferedAction - { - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly int _id; - private readonly IReadOnlyCollection? _ids; - private readonly bool _keepIfUnpublished; - - public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) - { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _id = id; - _keepIfUnpublished = keepIfUnpublished; - } - - public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) - { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _ids = ids; - _keepIfUnpublished = keepIfUnpublished; - } - - public override void Execute() - { - if (_ids is null) - { - Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); - } - else - { - Execute(_examineUmbracoIndexingHandler, _ids, _keepIfUnpublished); - } - } - - public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) - { - foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) - .Where(x => x.EnableDefaultEventHandler)) - { - index.DeleteFromIndex(id.ToString(CultureInfo.InvariantCulture)); - } - } - - public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) - { - foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) - .Where(x => x.EnableDefaultEventHandler)) - { - index.DeleteFromIndex(ids.Select(x => x.ToString(CultureInfo.InvariantCulture))); - } - } - } - #endregion + _mainDom = mainDom; + _logger = logger; + _profilingLogger = profilingLogger; + _scopeProvider = scopeProvider; + _examineManager = examineManager; + _backgroundTaskQueue = backgroundTaskQueue; + _contentValueSetBuilder = contentValueSetBuilder; + _publishedContentValueSetBuilder = publishedContentValueSetBuilder; + _mediaValueSetBuilder = mediaValueSetBuilder; + _memberValueSetBuilder = memberValueSetBuilder; + _enabled = new Lazy(IsEnabled); } + + /// + public bool Enabled => _enabled.Value; + + /// + public void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished)); + } + else + { + DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished); + } + } + + /// + public void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedDeleteIndex(this, entityIds, keepIfUnpublished)); + } + else + { + DeferedDeleteIndex.Execute(this, entityIds, keepIfUnpublished); + } + } + + /// + public void ReIndexForContent(IContent sender, bool isPublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedReIndexForContent(_backgroundTaskQueue, this, sender, isPublished)); + } + else + { + DeferedReIndexForContent.Execute(_backgroundTaskQueue, this, sender, isPublished); + } + } + + /// + public void ReIndexForMedia(IMedia sender, bool isPublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedReIndexForMedia(_backgroundTaskQueue, this, sender, isPublished)); + } + else + { + DeferedReIndexForMedia.Execute(_backgroundTaskQueue, this, sender, isPublished); + } + } + + /// + public void ReIndexForMember(IMember member) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedReIndexForMember(_backgroundTaskQueue, this, member)); + } + else + { + DeferedReIndexForMember.Execute(_backgroundTaskQueue, this, member); + } + } + + /// + public void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes) + { + const int pageSize = 500; + + //Delete all content of this content/media/member type that is in any content indexer by looking up matched examine docs + foreach (var id in removedContentTypes) + { + foreach (IUmbracoIndex index in _examineManager.Indexes.OfType()) + { + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) + { + //paging with examine, see https://shazwazza.com/post/paging-with-examine/ + ISearchResults? results = index.Searcher + .CreateQuery() + .Field("nodeType", id.ToInvariantString()) + .Execute(QueryOptions.SkipTake(page * pageSize, pageSize)); + total = results.TotalItemCount; + + foreach (ISearchResult item in results) + { + if (int.TryParse(item.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var contentId)) + { + DeleteIndexForEntity(contentId, false); + } + } + + page++; + } + } + } + } + + /// + /// Used to lazily check if Examine Index handling is enabled + /// + /// + private bool IsEnabled() + { + //let's deal with shutting down Examine with MainDom + var examineShutdownRegistered = _mainDom.Register(release: () => + { + using (_profilingLogger.TraceDuration("Examine shutting down")) + { + _examineManager.Dispose(); + } + }); + + if (!examineShutdownRegistered) + { + _logger.LogInformation( + "Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); + + //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! + Suspendable.ExamineEvents.SuspendIndexers(_logger); + return false; //exit, do not continue + } + + _logger.LogDebug("Examine shutdown registered with MainDom"); + + var registeredIndexers = + _examineManager.Indexes.OfType().Count(x => x.EnableDefaultEventHandler); + + _logger.LogInformation("Adding examine event handlers for {RegisteredIndexers} index providers.", + registeredIndexers); + + // don't bind event handlers if we're not suppose to listen + if (registeredIndexers == 0) + { + return false; + } + + return true; + } + + #region Deferred Actions + + private class DeferedActions + { + private readonly List _actions = new(); + + public static DeferedActions? Get(ICoreScopeProvider scopeProvider) + { + IScopeContext? scopeContext = scopeProvider.Context; + + return scopeContext?.Enlist("examineEvents", + () => new DeferedActions(), // creator + (completed, actions) => // action + { + if (completed) + { + actions?.Execute(); + } + }, EnlistPriority); + } + + public void Add(DeferedAction action) => _actions.Add(action); + + private void Execute() + { + foreach (DeferedAction action in _actions) + { + action.Execute(); + } + } + } + + /// + /// An action that will execute at the end of the Scope being completed + /// + private abstract class DeferedAction + { + public virtual void Execute() + { + } + } + + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForContent : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IContent _content; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly bool _isPublished; + + public DeferedReIndexForContent(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) + { + _backgroundTaskQueue = backgroundTaskQueue; + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _content = content; + _isPublished = isPublished; + } + + public override void Execute() => + Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _content, _isPublished); + + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) + => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + + // for content we have a different builder for published vs unpublished + // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published + var builders = new Dictionary>> + { + [true] = new(() => examineUmbracoIndexingHandler._publishedContentValueSetBuilder.GetValueSets(content).ToList()), + [false] = new(() => examineUmbracoIndexingHandler._contentValueSetBuilder.GetValueSets(content).ToList()) + }; + + // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + + List valueSet = builders[index.PublishedValuesOnly].Value; + index.IndexItems(valueSet); + } + + return Task.CompletedTask; + }); + } + + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForMedia : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly bool _isPublished; + private readonly IMedia _media; + + public DeferedReIndexForMedia(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) + { + _backgroundTaskQueue = backgroundTaskQueue; + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _media = media; + _isPublished = isPublished; + } + + public override void Execute() => + Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _media, _isPublished); + + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) => + // perform the ValueSet lookup on a background thread + backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + + var valueSet = examineUmbracoIndexingHandler._mediaValueSetBuilder.GetValueSets(media).ToList(); + + // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } + + return Task.CompletedTask; + }); + } + + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForMember : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly IMember _member; + + public DeferedReIndexForMember(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _member = member; + _backgroundTaskQueue = backgroundTaskQueue; + } + + public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _member); + + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) => + // perform the ValueSet lookup on a background thread + backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + + var valueSet = examineUmbracoIndexingHandler._memberValueSetBuilder.GetValueSets(member).ToList(); + + // only process for IUmbracoMemberIndex (not content indexes) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } + + return Task.CompletedTask; + }); + } + + private class DeferedDeleteIndex : DeferedAction + { + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly int _id; + private readonly IReadOnlyCollection? _ids; + private readonly bool _keepIfUnpublished; + + public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, + bool keepIfUnpublished) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _id = id; + _keepIfUnpublished = keepIfUnpublished; + } + + public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, + IReadOnlyCollection ids, bool keepIfUnpublished) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _ids = ids; + _keepIfUnpublished = keepIfUnpublished; + } + + public override void Execute() + { + if (_ids is null) + { + Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); + } + else + { + Execute(_examineUmbracoIndexingHandler, _ids, _keepIfUnpublished); + } + } + + public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, + bool keepIfUnpublished) + { + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) + .Where(x => x.EnableDefaultEventHandler)) + { + index.DeleteFromIndex(id.ToString(CultureInfo.InvariantCulture)); + } + } + + public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, + IReadOnlyCollection ids, bool keepIfUnpublished) + { + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) + .Where(x => x.EnableDefaultEventHandler)) + { + index.DeleteFromIndex(ids.Select(x => x.ToString(CultureInfo.InvariantCulture))); + } + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs index c2bf6b002d..5bbd115cef 100644 --- a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs @@ -1,70 +1,70 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Reflection; using Examine; using Examine.Search; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to return diagnostic data for any index +/// +public class GenericIndexDiagnostics : IIndexDiagnostics { + private static readonly string[] _ignoreProperties = { "Description" }; - /// - /// Used to return diagnostic data for any index - /// - public class GenericIndexDiagnostics : IIndexDiagnostics + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; + private readonly IIndex _index; + + public GenericIndexDiagnostics(IIndex index) => _index = index; + + public int DocumentCount => -1; // unknown + + public int FieldCount => -1; // unknown + + public IReadOnlyDictionary Metadata { - private readonly IIndex _index; - private static readonly string[] s_ignoreProperties = { "Description" }; - - private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; - public GenericIndexDiagnostics(IIndex index) => _index = index; - - public int DocumentCount => -1; //unknown - - public int FieldCount => -1; //unknown - - public Attempt IsHealthy() + get { - if (!_index.IndexExists()) - return Attempt.Fail("Does not exist"); + var result = new Dictionary(); - try + IOrderedEnumerable props = TypeHelper + .CachedDiscoverableProperties(_index.GetType(), mustWrite: false) + .Where(x => _ignoreProperties.InvariantContains(x.Name) == false) + .OrderBy(x => x.Name); + + foreach (PropertyInfo p in props) { - var result = _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet).Execute(new QueryOptions(0, 1)); - return Attempt.Succeed(); //if we can search we'll assume it's healthy + var val = p.GetValue(_index, null) ?? string.Empty; + + result.Add(p.Name, val); } - catch (Exception e) - { - return Attempt.Fail($"Error: {e.Message}"); - } - } - public long GetDocumentCount() => -1L; - - public IEnumerable GetFieldNames() => Enumerable.Empty(); - - public IReadOnlyDictionary Metadata - { - get - { - var result = new Dictionary(); - - var props = TypeHelper.CachedDiscoverableProperties(_index.GetType(), mustWrite: false) - .Where(x => s_ignoreProperties.InvariantContains(x.Name) == false) - .OrderBy(x => x.Name); - - foreach (var p in props) - { - var val = p.GetValue(_index, null) ?? string.Empty; - - result.Add(p.Name, val); - } - - return result; - } + return result; } } + + public Attempt IsHealthy() + { + if (!_index.IndexExists()) + { + return Attempt.Fail("Does not exist"); + } + + try + { + _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet) + .Execute(new QueryOptions(0, 1)); + return Attempt.Succeed(); // if we can search we'll assume it's healthy + } + catch (Exception e) + { + return Attempt.Fail($"Error: {e.Message}"); + } + } + + public long GetDocumentCount() => -1L; + + public IEnumerable GetFieldNames() => Enumerable.Empty(); } diff --git a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs index dc01d7bc94..eb6ce0f01c 100644 --- a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Examine; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to search the back office for Examine indexed entities (Documents, Media and Members) +/// +public interface IBackOfficeExamineSearcher { - /// - /// Used to search the back office for Examine indexed entities (Documents, Media and Members) - /// - public interface IBackOfficeExamineSearcher - { - IEnumerable Search(string query, - UmbracoEntityTypes entityType, - int pageSize, - long pageIndex, out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false); - } + IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string? searchFrom = null, + bool ignoreUserStartNodes = false); } diff --git a/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs index af6b613e24..67c64f603d 100644 --- a/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs @@ -1,12 +1,11 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// +/// Marker interface for a builder for supporting unpublished content +/// +public interface IContentValueSetBuilder : IValueSetBuilder { - /// - /// - /// Marker interface for a builder for supporting unpublished content - /// - public interface IContentValueSetBuilder : IValueSetBuilder - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs index e76153f25e..553732cec8 100644 --- a/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs @@ -1,31 +1,32 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An extended for content indexes +/// +public interface IContentValueSetValidator : IValueSetValidator { /// - /// An extended for content indexes + /// When set to true the index will only retain published values /// - public interface IContentValueSetValidator : IValueSetValidator - { - /// - /// When set to true the index will only retain published values - /// - /// - /// Any non-published values will not be put or kept in the index: - /// * Deleted, Trashed, non-published Content items - /// * non-published Variants - /// - bool PublishedValuesOnly { get; } + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } - /// - /// If true, protected content will be indexed otherwise it will not be put or kept in the index - /// - bool SupportProtectedContent { get; } + /// + /// If true, protected content will be indexed otherwise it will not be put or kept in the index + /// + bool SupportProtectedContent { get; } - int? ParentId { get; } + int? ParentId { get; } - bool ValidatePath(string path, string category); - bool ValidateRecycleBin(string path, string category); - bool ValidateProtectedContent(string path, string category); - } + bool ValidatePath(string path, string category); + + bool ValidateRecycleBin(string path, string category); + + bool ValidateProtectedContent(string path, string category); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs index dd9ee63239..5db9847c1e 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs @@ -1,30 +1,26 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Examine; using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Exposes diagnostic information about an index +/// +public interface IIndexDiagnostics : IIndexStats { + /// + /// A key/value collection of diagnostic properties for the index + /// + /// + /// Used to display in the UI + /// + IReadOnlyDictionary Metadata { get; } /// - /// Exposes diagnostic information about an index + /// If the index can be open/read /// - public interface IIndexDiagnostics : IIndexStats - { - /// - /// If the index can be open/read - /// - /// - /// A successful attempt if it is healthy, else a failed attempt with a message if unhealthy - /// - Attempt IsHealthy(); - - /// - /// A key/value collection of diagnostic properties for the index - /// - /// - /// Used to display in the UI - /// - IReadOnlyDictionary Metadata { get; } - } + /// + /// A successful attempt if it is healthy, else a failed attempt with a message if unhealthy + /// + Attempt IsHealthy(); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs b/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs index b39ef5c3a8..2d484eb03b 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs @@ -1,13 +1,11 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Creates for an index if it doesn't implement +/// +public interface IIndexDiagnosticsFactory { - - /// - /// Creates for an index if it doesn't implement - /// - public interface IIndexDiagnosticsFactory - { - IIndexDiagnostics Create(IIndex index); - } + IIndexDiagnostics Create(IIndex index); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs index 2089bd923a..ca4549f4a2 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs @@ -1,20 +1,19 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public interface IIndexPopulator { - public interface IIndexPopulator - { - /// - /// If this index is registered with this populator - /// - /// - /// - bool IsRegistered(IIndex index); + /// + /// If this index is registered with this populator + /// + /// + /// + bool IsRegistered(IIndex index); - /// - /// Populate indexers - /// - /// - void Populate(params IIndex[] indexes); - } + /// + /// Populate indexers + /// + /// + void Populate(params IIndex[] indexes); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs index 127a20d685..a85c551b8d 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs @@ -1,14 +1,10 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public interface IIndexRebuilder { - public interface IIndexRebuilder - { - bool CanRebuild(string indexName); - void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true); - void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true); - } + bool CanRebuild(string indexName); + + void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true); + + void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true); } diff --git a/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs index 8c5348ed46..3f6982d004 100644 --- a/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs @@ -1,12 +1,11 @@ -using Examine; +using Examine; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Marker interface for a builder for only published content +/// +public interface IPublishedContentValueSetBuilder : IValueSetBuilder { - /// - /// Marker interface for a builder for only published content - /// - public interface IPublishedContentValueSetBuilder : IValueSetBuilder - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs index 0735cc255d..a47c328fc0 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs @@ -1,11 +1,8 @@ -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +/// +/// Marker interface for indexes of Umbraco content +/// +public interface IUmbracoContentIndex : IUmbracoIndex { - /// - /// Marker interface for indexes of Umbraco content - /// - public interface IUmbracoContentIndex : IUmbracoIndex - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs index f2221e5c91..a94201894a 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// A Marker interface for defining an Umbraco indexer +/// +public interface IUmbracoIndex : IIndex, IIndexStats { /// - /// A Marker interface for defining an Umbraco indexer + /// When set to true Umbraco will keep the index in sync with Umbraco data automatically /// - public interface IUmbracoIndex : IIndex, IIndexStats - { - /// - /// When set to true Umbraco will keep the index in sync with Umbraco data automatically - /// - bool EnableDefaultEventHandler { get; } + bool EnableDefaultEventHandler { get; } - /// - /// When set to true the index will only retain published values - /// - /// - /// Any non-published values will not be put or kept in the index: - /// * Deleted, Trashed, non-published Content items - /// * non-published Variants - /// - bool PublishedValuesOnly { get; } - } + /// + /// When set to true the index will only retain published values + /// + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs index 83a3730b97..0aedcc90f5 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs @@ -1,12 +1,12 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine -{ - public interface IUmbracoIndexConfig - { - IContentValueSetValidator GetContentValueSetValidator(); - IContentValueSetValidator GetPublishedContentValueSetValidator(); - IValueSetValidator GetMemberValueSetValidator(); +namespace Umbraco.Cms.Infrastructure.Examine; - } +public interface IUmbracoIndexConfig +{ + IContentValueSetValidator GetContentValueSetValidator(); + + IContentValueSetValidator GetPublishedContentValueSetValidator(); + + IValueSetValidator GetMemberValueSetValidator(); } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs index 7dc07688a9..e914c90d37 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs @@ -1,9 +1,5 @@ -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public interface IUmbracoMemberIndex : IUmbracoIndex { - public interface IUmbracoMemberIndex : IUmbracoIndex - { - - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs index fe135a82b7..ee4fb38760 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs @@ -1,35 +1,35 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +/// +/// Used to propagate hardcoded internal Field lists +/// +public interface IUmbracoTreeSearcherFields { /// - /// Used to propagate hardcoded internal Field lists + /// The default index fields that are searched on in the back office search for umbraco content entities. /// - public interface IUmbracoTreeSearcherFields - { - /// - /// The default index fields that are searched on in the back office search for umbraco content entities. - /// - IEnumerable GetBackOfficeFields(); + IEnumerable GetBackOfficeFields(); - /// - /// The additional index fields that are searched on in the back office for member entities. - /// - IEnumerable GetBackOfficeMembersFields(); + /// + /// The additional index fields that are searched on in the back office for member entities. + /// + IEnumerable GetBackOfficeMembersFields(); - /// - /// The additional index fields that are searched on in the back office for media entities. - /// - IEnumerable GetBackOfficeMediaFields(); + /// + /// The additional index fields that are searched on in the back office for media entities. + /// + IEnumerable GetBackOfficeMediaFields(); - /// - /// The additional index fields that are searched on in the back office for document entities. - /// - IEnumerable GetBackOfficeDocumentFields(); + /// + /// The additional index fields that are searched on in the back office for document entities. + /// + IEnumerable GetBackOfficeDocumentFields(); - ISet GetBackOfficeFieldsToLoad(); - ISet GetBackOfficeMembersFieldsToLoad(); - ISet GetBackOfficeDocumentFieldsToLoad(); - ISet GetBackOfficeMediaFieldsToLoad(); - } + ISet GetBackOfficeFieldsToLoad(); + + ISet GetBackOfficeMembersFieldsToLoad(); + + ISet GetBackOfficeDocumentFieldsToLoad(); + + ISet GetBackOfficeMediaFieldsToLoad(); } diff --git a/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs index 0e1d05440d..d9c5fe9566 100644 --- a/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Creates a collection of to be indexed based on a collection of +/// +/// +public interface IValueSetBuilder { /// - /// Creates a collection of to be indexed based on a collection of + /// Creates a collection of to be indexed based on a collection of /// - /// - public interface IValueSetBuilder - { - /// - /// Creates a collection of to be indexed based on a collection of - /// - /// - /// - IEnumerable GetValueSets(params T[] content); - } - + /// + /// + IEnumerable GetValueSets(params T[] content); } diff --git a/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs b/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs index a60a373e65..acaf42b4b0 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs @@ -1,20 +1,20 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine -{ - /// - /// Default implementation of which returns for indexes that don't have an implementation - /// - public class IndexDiagnosticsFactory : IIndexDiagnosticsFactory - { - public virtual IIndexDiagnostics Create(IIndex index) - { - if (index is not IIndexDiagnostics indexDiag) - { - indexDiag = new GenericIndexDiagnostics(index); - } +namespace Umbraco.Cms.Infrastructure.Examine; - return indexDiag; +/// +/// Default implementation of which returns +/// for indexes that don't have an implementation +/// +public class IndexDiagnosticsFactory : IIndexDiagnosticsFactory +{ + public virtual IIndexDiagnostics Create(IIndex index) + { + if (index is not IIndexDiagnostics indexDiag) + { + indexDiag = new GenericIndexDiagnostics(index); } + + return indexDiag; } } diff --git a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs index d32470d875..db3fe2373f 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs @@ -1,53 +1,46 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An that is automatically associated to any index of type +/// +/// +public abstract class IndexPopulator : IndexPopulator + where TIndex : IIndex { - /// - /// An that is automatically associated to any index of type - /// - /// - public abstract class IndexPopulator : IndexPopulator where TIndex : IIndex + public override bool IsRegistered(IIndex index) { - public override bool IsRegistered(IIndex index) + if (base.IsRegistered(index)) { - if (base.IsRegistered(index)) - return true; - - if (!(index is TIndex casted)) - return false; - - return IsRegistered(casted); + return true; } - public virtual bool IsRegistered(TIndex index) => true; + if (!(index is TIndex casted)) + { + return false; + } + + return IsRegistered(casted); } - public abstract class IndexPopulator : IIndexPopulator - { - private readonly ConcurrentHashSet _registeredIndexes = new ConcurrentHashSet(); - - public virtual bool IsRegistered(IIndex index) - { - return _registeredIndexes.Contains(index.Name); - } - - /// - /// Registers an index for this populator - /// - /// - public void RegisterIndex(string indexName) - { - _registeredIndexes.Add(indexName); - } - - public void Populate(params IIndex[] indexes) - { - PopulateIndexes(indexes.Where(IsRegistered).ToList()); - } - - protected abstract void PopulateIndexes(IReadOnlyList indexes); - } + public virtual bool IsRegistered(TIndex index) => true; +} + +public abstract class IndexPopulator : IIndexPopulator +{ + private readonly ConcurrentHashSet _registeredIndexes = new(); + + public virtual bool IsRegistered(IIndex index) => _registeredIndexes.Contains(index.Name); + + public void Populate(params IIndex[] indexes) => PopulateIndexes(indexes.Where(IsRegistered).ToList()); + + /// + /// Registers an index for this populator + /// + /// + public void RegisterIndex(string indexName) => _registeredIndexes.Add(indexName); + + protected abstract void PopulateIndexes(IReadOnlyList indexes); } diff --git a/src/Umbraco.Infrastructure/Examine/IndexTypes.cs b/src/Umbraco.Infrastructure/Examine/IndexTypes.cs index bb6edaa78b..9b180aa5ea 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexTypes.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexTypes.cs @@ -1,33 +1,31 @@ -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// The index types stored in the Lucene Index +/// +public static class IndexTypes { /// - /// The index types stored in the Lucene Index + /// The content index type /// - public static class IndexTypes - { + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Content = "content"; - /// - /// The content index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Content = "content"; + /// + /// The media index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Media = "media"; - /// - /// The media index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Media = "media"; - - /// - /// The member index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Member = "member"; - } + /// + /// The member index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Member = "member"; } diff --git a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs index 9f6e33f8dd..19a2a96160 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs @@ -1,80 +1,75 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a media index +/// +public class MediaIndexPopulator : IndexPopulator { + private readonly ILogger _logger; + private readonly IMediaService _mediaService; + private readonly IValueSetBuilder _mediaValueSetBuilder; + private readonly int? _parentId; + /// - /// Performs the data lookups required to rebuild a media index + /// Default constructor to lookup all content data /// - public class MediaIndexPopulator : IndexPopulator + public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + : this(logger, null, mediaService, mediaValueSetBuilder) { - private readonly ILogger _logger; - private readonly int? _parentId; - private readonly IMediaService _mediaService; - private readonly IValueSetBuilder _mediaValueSetBuilder; + } - /// - /// Default constructor to lookup all content data - /// - /// - /// - public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) - : this(logger, null, mediaService, mediaValueSetBuilder) + /// + /// Optional constructor allowing specifying custom query parameters + /// + public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + { + _logger = logger; + _parentId = parentId; + _mediaService = mediaService; + _mediaValueSetBuilder = mediaValueSetBuilder; + } + + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { + _logger.LogDebug( + $"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); + return; } - /// - /// Optional constructor allowing specifying custom query parameters - /// - /// - /// - /// - public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + const int pageSize = 10000; + var pageIndex = 0; + + var mediaParentId = -1; + + if (_parentId.HasValue && _parentId.Value > 0) { - _logger = logger; - _parentId = parentId; - _mediaService = mediaService; - _mediaValueSetBuilder = mediaValueSetBuilder; + mediaParentId = _parentId.Value; } - protected override void PopulateIndexes(IReadOnlyList indexes) + IMedia[] media; + + do { - if (indexes.Count == 0) + media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out _).ToArray(); + + if (media.Length > 0) { - _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); - return; - } - - const int pageSize = 10000; - var pageIndex = 0; - - var mediaParentId = -1; - - if (_parentId.HasValue && _parentId.Value > 0) - { - mediaParentId = _parentId.Value; - } - - IMedia[] media; - - do - { - media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out var total).ToArray(); - - if (media.Length > 0) + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) { - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - index.IndexItems(_mediaValueSetBuilder.GetValueSets(media)); + index.IndexItems(_mediaValueSetBuilder.GetValueSets(media)); } + } - pageIndex++; - } while (media.Length == pageSize); + pageIndex++; } - + while (media.Length == pageSize); } } diff --git a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs index ff0c1f142c..344c7d08d2 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs @@ -1,87 +1,76 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Examine; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MediaValueSetBuilder : BaseValueSetBuilder { - public class MediaValueSetBuilder : BaseValueSetBuilder + private readonly ContentSettings _contentSettings; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IUserService _userService; + + public MediaValueSetBuilder( + PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + MediaUrlGeneratorCollection mediaUrlGenerators, + IUserService userService, + IShortStringHelper shortStringHelper, + IOptions contentSettings) + : base(propertyEditors, false) { - private readonly UrlSegmentProviderCollection _urlSegmentProviders; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IUserService _userService; - private readonly IShortStringHelper _shortStringHelper; - private readonly ContentSettings _contentSettings; - - public MediaValueSetBuilder( - PropertyEditorCollection propertyEditors, - UrlSegmentProviderCollection urlSegmentProviders, - MediaUrlGeneratorCollection mediaUrlGenerators, - IUserService userService, - IShortStringHelper shortStringHelper, - IOptions contentSettings) - : base(propertyEditors, false) - { - _urlSegmentProviders = urlSegmentProviders; - _mediaUrlGenerators = mediaUrlGenerators; - _userService = userService; - _shortStringHelper = shortStringHelper; - _contentSettings = contentSettings.Value; - } - - /// - public override IEnumerable GetValueSets(params IMedia[] media) - { - foreach (IMedia m in media) - { - - var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); - - IEnumerable mediaFiles = m.GetUrls(_contentSettings, _mediaUrlGenerators) - .Select(x => Path.GetFileName(x)) - .Distinct(); - - var values = new Dictionary> - { - {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {"id", new object[] {m.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, - {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, - {"path", m.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", m.ContentType.Id.ToString().Yield() }, - {"creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield()}, - {UmbracoExamineFieldNames.UmbracoFileFieldName, mediaFiles} - }; - - foreach (var property in m.Properties) - { - AddPropertyValue(property, null, null, values); - } - - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); - - yield return vs; - } - } + _urlSegmentProviders = urlSegmentProviders; + _mediaUrlGenerators = mediaUrlGenerators; + _userService = userService; + _shortStringHelper = shortStringHelper; + _contentSettings = contentSettings.Value; } + /// + public override IEnumerable GetValueSets(params IMedia[] media) + { + foreach (IMedia m in media) + { + var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); + + IEnumerable mediaFiles = m.GetUrls(_contentSettings, _mediaUrlGenerators) + .Select(x => Path.GetFileName(x)) + .Distinct(); + + var values = new Dictionary> + { + { "icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty() }, + { "id", new object[] { m.Id } }, + { UmbracoExamineFieldNames.NodeKeyFieldName, new object[] { m.Key } }, + { "parentID", new object[] { m.Level > 1 ? m.ParentId : -1 } }, + { "level", new object[] { m.Level } }, + { "creatorID", new object[] { m.CreatorId } }, + { "sortOrder", new object[] { m.SortOrder } }, + { "createDate", new object[] { m.CreateDate } }, + { "updateDate", new object[] { m.UpdateDate } }, + { UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty() }, + { "urlName", urlValue?.Yield() ?? Enumerable.Empty() }, + { "path", m.Path.Yield() }, + { "nodeType", m.ContentType.Id.ToString().Yield() }, + { "creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, + { UmbracoExamineFieldNames.UmbracoFileFieldName, mediaFiles }, + }; + + foreach (IProperty property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } + + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); + + yield return vs; + } + } } diff --git a/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs index 76dee23450..9dc2102d5e 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs @@ -1,44 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MemberIndexPopulator : IndexPopulator { - public class MemberIndexPopulator : IndexPopulator + private readonly IMemberService _memberService; + private readonly IValueSetBuilder _valueSetBuilder; + + public MemberIndexPopulator(IMemberService memberService, IValueSetBuilder valueSetBuilder) { - private readonly IMemberService _memberService; - private readonly IValueSetBuilder _valueSetBuilder; + _memberService = memberService; + _valueSetBuilder = valueSetBuilder; + } - public MemberIndexPopulator(IMemberService memberService, IValueSetBuilder valueSetBuilder) + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { - _memberService = memberService; - _valueSetBuilder = valueSetBuilder; + return; } - protected override void PopulateIndexes(IReadOnlyList indexes) + + const int pageSize = 1000; + var pageIndex = 0; + + IMember[] members; + + // no node types specified, do all members + do { - if (indexes.Count == 0) return; + members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); - const int pageSize = 1000; - var pageIndex = 0; - - IMember[] members; - - //no node types specified, do all members - do + if (members.Length > 0) { - members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); - - if (members.Length > 0) + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) { - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - index.IndexItems(_valueSetBuilder.GetValueSets(members)); + index.IndexItems(_valueSetBuilder.GetValueSets(members)); } + } - pageIndex++; - } while (members.Length == pageSize); + pageIndex++; } + while (members.Length == pageSize); } } diff --git a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs index 33481c96a3..74d3829d6a 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs @@ -1,53 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MemberValueSetBuilder : BaseValueSetBuilder { - - public class MemberValueSetBuilder : BaseValueSetBuilder + public MemberValueSetBuilder(PropertyEditorCollection propertyEditors) + : base(propertyEditors, false) { - public MemberValueSetBuilder(PropertyEditorCollection propertyEditors) - : base(propertyEditors, false) - { - } - - /// - public override IEnumerable GetValueSets(params IMember[] members) - { - foreach (var m in members) - { - var values = new Dictionary> - { - {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {"id", new object[] {m.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, - {"path", m.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", m.ContentType.Id.ToString().Yield() }, - {"loginName", m.Username?.Yield() ?? Enumerable.Empty()}, - {"email", m.Email?.Yield() ?? Enumerable.Empty()}, - }; - - foreach (var property in m.Properties) - { - AddPropertyValue(property, null, null, values); - } - - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); - - yield return vs; - } - } } + /// + public override IEnumerable GetValueSets(params IMember[] members) + { + foreach (IMember m in members) + { + var values = new Dictionary> + { + { "icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty() }, + { "id", new object[] { m.Id } }, + { UmbracoExamineFieldNames.NodeKeyFieldName, new object[] { m.Key } }, + { "parentID", new object[] { m.Level > 1 ? m.ParentId : -1 } }, + { "level", new object[] { m.Level } }, + { "creatorID", new object[] { m.CreatorId } }, + { "sortOrder", new object[] { m.SortOrder } }, + { "createDate", new object[] { m.CreateDate } }, + { "updateDate", new object[] { m.UpdateDate } }, + { UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty() }, + { "path", m.Path.Yield() }, + { "nodeType", m.ContentType.Id.ToString().Yield() }, + { "loginName", m.Username.Yield() }, + { "email", m.Email.Yield() }, + }; + + foreach (IProperty property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } + + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); + + yield return vs; + } + } } diff --git a/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs index f92a9dc620..f1fa96e0fa 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs @@ -1,30 +1,32 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public class MemberValueSetValidator : ValueSetValidator { - public class MemberValueSetValidator : ValueSetValidator + /// + /// By default these are the member fields we index + /// + public static readonly string[] DefaultMemberIndexFields = { - public MemberValueSetValidator() : base(null, null, DefaultMemberIndexFields, null) - { - } + "id", UmbracoExamineFieldNames.NodeNameFieldName, "updateDate", "loginName", "email", + UmbracoExamineFieldNames.NodeKeyFieldName, + }; - public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes) - : base(includeItemTypes, excludeItemTypes, DefaultMemberIndexFields, null) - { - } - - public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes, IEnumerable includeFields, IEnumerable excludeFields) - : base(includeItemTypes, excludeItemTypes, includeFields, excludeFields) - { - } - - /// - /// By default these are the member fields we index - /// - public static readonly string[] DefaultMemberIndexFields = { "id", UmbracoExamineFieldNames.NodeNameFieldName, "updateDate", "loginName", "email", UmbracoExamineFieldNames.NodeKeyFieldName }; - - private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Member }; - protected override IEnumerable ValidIndexCategories => ValidCategories; + private static readonly IEnumerable _validCategories = new[] { IndexTypes.Member }; + public MemberValueSetValidator() + : base(null, null, DefaultMemberIndexFields, null) + { } + + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes) + : base(includeItemTypes, excludeItemTypes, DefaultMemberIndexFields, null) + { + } + + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes, IEnumerable includeFields, IEnumerable excludeFields) + : base(includeItemTypes, excludeItemTypes, includeFields, excludeFields) + { + } + + protected override IEnumerable ValidIndexCategories => _validCategories; } diff --git a/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs index cb01bae57a..625a0fda53 100644 --- a/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs @@ -1,17 +1,20 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class NoopBackOfficeExamineSearcher : IBackOfficeExamineSearcher { - public class NoopBackOfficeExamineSearcher : IBackOfficeExamineSearcher + public IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string? searchFrom = null, + bool ignoreUserStartNodes = false) { - public IEnumerable Search(string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, - string? searchFrom = null, bool ignoreUserStartNodes = false) - { - totalFound = 0; - return Enumerable.Empty(); - } + totalFound = 0; + return Enumerable.Empty(); } } diff --git a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs index f9ccaffdbc..67d59d02d9 100644 --- a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs @@ -2,21 +2,25 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a content index containing only published content +/// +/// +/// The published (external) index will still rebuild just fine using the default +/// which is what is used when rebuilding all indexes, +/// but this will be used when the single index is rebuilt and will go a little bit faster since the data query is more specific. +/// since the data query is more specific. +/// +public class PublishedContentIndexPopulator : ContentIndexPopulator { - /// - /// Performs the data lookups required to rebuild a content index containing only published content - /// - /// - /// The published (external) index will still rebuild just fine using the default which is what - /// is used when rebuilding all indexes, but this will be used when the single index is rebuilt and will go a little bit faster - /// since the data query is more specific. - /// - public class PublishedContentIndexPopulator : ContentIndexPopulator + public PublishedContentIndexPopulator( + ILogger logger, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IPublishedContentValueSetBuilder contentValueSetBuilder) + : base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) { - public PublishedContentIndexPopulator(ILogger logger, IContentService contentService, IUmbracoDatabaseFactory umbracoDatabaseFactory, IPublishedContentValueSetBuilder contentValueSetBuilder) : - base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) - { - } } } diff --git a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs index 9ec145b030..ca7bc49dee 100644 --- a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs @@ -1,72 +1,68 @@ -using System; -using System.Threading; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Handles how the indexes are rebuilt on startup +/// +/// +/// On the first HTTP request this will rebuild the Examine indexes if they are empty. +/// If it is a cold boot, they are all rebuilt. +/// +public sealed class RebuildOnStartupHandler : INotificationHandler { - /// - /// Handles how the indexes are rebuilt on startup - /// - /// - /// On the first HTTP request this will rebuild the Examine indexes if they are empty. - /// If it is a cold boot, they are all rebuilt. - /// - public sealed class RebuildOnStartupHandler : INotificationHandler + // These must be static because notification handlers are transient. + // this does unfortunatley mean that one RebuildOnStartupHandler instance + // will be created for each front-end request even though we only use the first one. + // TODO: Is there a better way to acheive this without allocating? We cannot remove + // a handler from the notification system. It's not a huge deal but would be better + // with less objects. + private static bool _isReady; + private static bool _isReadSet; + private static object? _isReadyLock; + private readonly ExamineIndexRebuilder _backgroundIndexRebuilder; + private readonly IRuntimeState _runtimeState; + private readonly ISyncBootStateAccessor _syncBootStateAccessor; + + public RebuildOnStartupHandler( + ISyncBootStateAccessor syncBootStateAccessor, + ExamineIndexRebuilder backgroundIndexRebuilder, + IRuntimeState runtimeState) { - private readonly ISyncBootStateAccessor _syncBootStateAccessor; - private readonly ExamineIndexRebuilder _backgroundIndexRebuilder; - private readonly IRuntimeState _runtimeState; + _syncBootStateAccessor = syncBootStateAccessor; + _backgroundIndexRebuilder = backgroundIndexRebuilder; + _runtimeState = runtimeState; + } - // These must be static because notification handlers are transient. - // this does unfortunatley mean that one RebuildOnStartupHandler instance - // will be created for each front-end request even though we only use the first one. - // TODO: Is there a better way to acheive this without allocating? We cannot remove - // a handler from the notification system. It's not a huge deal but would be better - // with less objects. - private static bool _isReady; - private static bool _isReadSet; - private static object? _isReadyLock; - - public RebuildOnStartupHandler( - ISyncBootStateAccessor syncBootStateAccessor, - ExamineIndexRebuilder backgroundIndexRebuilder, - IRuntimeState runtimeState) + /// + /// On first http request schedule an index rebuild for any empty indexes (or all if it's a cold boot) + /// + /// + public void Handle(UmbracoRequestBeginNotification notification) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - _syncBootStateAccessor = syncBootStateAccessor; - _backgroundIndexRebuilder = backgroundIndexRebuilder; - _runtimeState = runtimeState; + return; } - /// - /// On first http request schedule an index rebuild for any empty indexes (or all if it's a cold boot) - /// - /// - public void Handle(UmbracoRequestBeginNotification notification) - { - if (_runtimeState.Level != RuntimeLevel.Run) + LazyInitializer.EnsureInitialized( + ref _isReady, + ref _isReadSet, + ref _isReadyLock, + () => { - return; - } + SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); - LazyInitializer.EnsureInitialized( - ref _isReady, - ref _isReadSet, - ref _isReadyLock, - () => - { - SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); + // if it's not a cold boot, only rebuild empty ones + _backgroundIndexRebuilder.RebuildIndexes( + bootState != SyncBootState.ColdBoot, + TimeSpan.FromMinutes(1)); - _backgroundIndexRebuilder.RebuildIndexes( - // if it's not a cold boot, only rebuild empty ones - bootState != SyncBootState.ColdBoot, - TimeSpan.FromMinutes(1)); - - return true; - }); - } + return true; + }); } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs index 7ae567739e..7ba10019c7 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs @@ -1,136 +1,138 @@ -using System.Collections.Generic; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Examine; using Examine.Search; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoExamineExtensions { - public static class UmbracoExamineExtensions + /// + /// Matches a culture iso name suffix + /// + /// + /// myFieldName_en-us will match the "en-us" + /// + internal static readonly Regex _cultureIsoCodeFieldNameMatchExpression = new( + "^(?[_\\w]+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", + RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + // TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression + + /// + /// Returns all index fields that are culture specific (suffixed) + /// + /// + /// + /// + public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) { - /// - /// Matches a culture iso name suffix - /// - /// - /// myFieldName_en-us will match the "en-us" - /// - internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^(?[_\\w]+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + IEnumerable allFields = index.GetFieldNames(); - //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression - - /// - /// Returns all index fields that are culture specific (suffixed) - /// - /// - /// - /// - public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) + var results = new List(); + foreach (var field in allFields) { - IEnumerable allFields = index.GetFieldNames(); - - var results = new List(); - foreach (var field in allFields) + Match match = _cultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) { - var match = CultureIsoCodeFieldNameMatchExpression.Match(field); - if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) - { - results.Add(field); - } - } - - return results; - } - - /// - /// Returns all index fields that are culture specific (suffixed) or invariant - /// - /// - /// - /// - public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) - { - IEnumerable allFields = index.GetFieldNames(); - - foreach (var field in allFields) - { - var match = CultureIsoCodeFieldNameMatchExpression.Match(field); - if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) - { - yield return field; //matches this culture field - } - else if (!match.Success) - { - yield return field; //matches no culture field (invariant) - } + results.Add(field); } } - public static IBooleanOperation Id(this IQuery query, int id) - { - var fieldQuery = query.Id(id.ToInvariantString()); - return fieldQuery; - } + return results; + } - /// - /// Query method to search on parent id - /// - /// - /// - /// - public static IBooleanOperation ParentId(this IQuery query, int id) - { - var fieldQuery = query.Field("parentID", id); - return fieldQuery; - } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + IEnumerable allFields = index.GetFieldNames(); - /// - /// Query method to search on node name - /// - /// - /// - /// - public static IBooleanOperation NodeName(this IQuery query, string nodeName) + foreach (var field in allFields) { - var fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, (IExamineValue)new ExamineValue(Examineness.Explicit, nodeName)); - return fieldQuery; + Match match = _cultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) + { + yield return field; // matches this culture field + } + else if (!match.Success) + { + yield return field; // matches no culture field (invariant) + } } + } - /// - /// Query method to search on node name - /// - /// - /// - /// - public static IBooleanOperation NodeName(this IQuery query, IExamineValue nodeName) - { - var fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, nodeName); - return fieldQuery; - } + public static IBooleanOperation Id(this IQuery query, int id) + { + IBooleanOperation? fieldQuery = query.Id(id.ToInvariantString()); + return fieldQuery; + } - /// - /// Query method to search on node type alias - /// - /// - /// - /// - public static IBooleanOperation NodeTypeAlias(this IQuery query, string nodeTypeAlias) - { - var fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, (IExamineValue)new ExamineValue(Examineness.Explicit, nodeTypeAlias)); - return fieldQuery; - } + /// + /// Query method to search on parent id + /// + /// + /// + /// + public static IBooleanOperation ParentId(this IQuery query, int id) + { + IBooleanOperation? fieldQuery = query.Field("parentID", id); + return fieldQuery; + } - /// - /// Query method to search on node type alias - /// - /// - /// - /// - public static IBooleanOperation NodeTypeAlias(this IQuery query, IExamineValue nodeTypeAlias) - { - var fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, nodeTypeAlias); - return fieldQuery; - } + /// + /// Query method to search on node name + /// + /// + /// + /// + public static IBooleanOperation NodeName(this IQuery query, string nodeName) + { + IBooleanOperation? fieldQuery = query.Field( + UmbracoExamineFieldNames.NodeNameFieldName, + (IExamineValue)new ExamineValue(Examineness.Explicit, nodeName)); + return fieldQuery; + } + /// + /// Query method to search on node name + /// + /// + /// + /// + public static IBooleanOperation NodeName(this IQuery query, IExamineValue nodeName) + { + IBooleanOperation? fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, nodeName); + return fieldQuery; + } + + /// + /// Query method to search on node type alias + /// + /// + /// + /// + public static IBooleanOperation NodeTypeAlias(this IQuery query, string nodeTypeAlias) + { + IBooleanOperation? fieldQuery = query.Field( + ExamineFieldNames.ItemTypeFieldName, + (IExamineValue)new ExamineValue(Examineness.Explicit, nodeTypeAlias)); + return fieldQuery; + } + + /// + /// Query method to search on node type alias + /// + /// + /// + /// + public static IBooleanOperation NodeTypeAlias(this IQuery query, IExamineValue nodeTypeAlias) + { + IBooleanOperation? fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, nodeTypeAlias); + return fieldQuery; } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs index 72e914c584..5e2779e9a3 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs @@ -1,28 +1,28 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public static class UmbracoExamineFieldNames { - public static class UmbracoExamineFieldNames - { - /// - /// Used to store the path of a content object - /// - public const string IndexPathFieldName = ExamineFieldNames.SpecialFieldPrefix + "Path"; - public const string NodeKeyFieldName = ExamineFieldNames.SpecialFieldPrefix + "Key"; - public const string UmbracoFileFieldName = "umbracoFileSrc"; - public const string IconFieldName = ExamineFieldNames.SpecialFieldPrefix + "Icon"; - public const string PublishedFieldName = ExamineFieldNames.SpecialFieldPrefix + "Published"; + /// + /// Used to store the path of a content object + /// + public const string IndexPathFieldName = ExamineFieldNames.SpecialFieldPrefix + "Path"; - /// - /// The prefix added to a field when it is duplicated in order to store the original raw value. - /// - public const string RawFieldPrefix = ExamineFieldNames.SpecialFieldPrefix + "Raw_"; + public const string NodeKeyFieldName = ExamineFieldNames.SpecialFieldPrefix + "Key"; + public const string UmbracoFileFieldName = "umbracoFileSrc"; + public const string IconFieldName = ExamineFieldNames.SpecialFieldPrefix + "Icon"; + public const string PublishedFieldName = ExamineFieldNames.SpecialFieldPrefix + "Published"; - public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture"; + /// + /// The prefix added to a field when it is duplicated in order to store the original raw value. + /// + public const string RawFieldPrefix = ExamineFieldNames.SpecialFieldPrefix + "Raw_"; - public const string NodeNameFieldName = "nodeName"; - public const string ItemIdFieldName ="__NodeId"; - public const string CategoryFieldName = "__IndexType"; - public const string ItemTypeFieldName = "__NodeTypeAlias"; - } + public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture"; + + public const string NodeNameFieldName = "nodeName"; + public const string ItemIdFieldName = "__NodeId"; + public const string CategoryFieldName = "__IndexType"; + public const string ItemTypeFieldName = "__NodeTypeAlias"; } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs index 912ae75460..9f9e214316 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs @@ -1,94 +1,89 @@ -using Examine; +using System.Text.RegularExpressions; +using Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Custom allowing dynamic creation of +/// +public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection { /// - /// Custom allowing dynamic creation of + /// A type that defines the type of index for each Umbraco field (non user defined fields) + /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene + /// for retreival after searching. /// - public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection + public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = { + new("parentID", FieldDefinitionTypes.Integer), new("level", FieldDefinitionTypes.Integer), + new("writerID", FieldDefinitionTypes.Integer), new("creatorID", FieldDefinitionTypes.Integer), + new("sortOrder", FieldDefinitionTypes.Integer), new("template", FieldDefinitionTypes.Integer), + new("createDate", FieldDefinitionTypes.DateTime), new("updateDate", FieldDefinitionTypes.DateTime), + new(UmbracoExamineFieldNames.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("version", FieldDefinitionTypes.Raw), new("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("template", FieldDefinitionTypes.Raw), new("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("path", FieldDefinitionTypes.Raw), new("email", FieldDefinitionTypes.EmailAddress), + new(UmbracoExamineFieldNames.PublishedFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.IconFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw), + }; - public UmbracoFieldDefinitionCollection() - : base(UmbracoIndexFieldDefinitions) + public UmbracoFieldDefinitionCollection() + : base(UmbracoIndexFieldDefinitions) + { + } + + /// + /// Overridden to dynamically add field definitions for culture variations + /// + /// + /// + /// + /// + /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions + /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... + /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like + /// `nodeName_en-us` + /// and we don't want to have a full static list of all of these definitions when we can just define the one definition + /// and then + /// dynamically apply that to culture specific fields. + /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and + /// store a new field + /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more + /// objects. This does mean + /// however that if a language is deleted, the field definitions for that language will still exist in memory. This + /// isn't going to cause any + /// problems and the mem will be cleared on next site restart but it's worth pointing out. + /// + public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) + { + if (base.TryGetValue(fieldName, out fieldDefinition)) { + return true; } - /// - /// A type that defines the type of index for each Umbraco field (non user defined fields) - /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene - /// for retreival after searching. - /// - public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = + // before we use regex to match do some faster simple matching since this is going to execute quite a lot + if (!fieldName.Contains("_")) { - new FieldDefinition("parentID", FieldDefinitionTypes.Integer), - new FieldDefinition("level", FieldDefinitionTypes.Integer), - new FieldDefinition("writerID", FieldDefinitionTypes.Integer), - new FieldDefinition("creatorID", FieldDefinitionTypes.Integer), - new FieldDefinition("sortOrder", FieldDefinitionTypes.Integer), - new FieldDefinition("template", FieldDefinitionTypes.Integer), - - new FieldDefinition("createDate", FieldDefinitionTypes.DateTime), - new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime), - - new FieldDefinition(UmbracoExamineFieldNames.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("version", FieldDefinitionTypes.Raw), - new FieldDefinition("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("template", FieldDefinitionTypes.Raw), - new FieldDefinition("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("path", FieldDefinitionTypes.Raw), - - new FieldDefinition("email", FieldDefinitionTypes.EmailAddress), - - new FieldDefinition(UmbracoExamineFieldNames.PublishedFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.IconFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw), - }; - - - /// - /// Overridden to dynamically add field definitions for culture variations - /// - /// - /// - /// - /// - /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions - /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... - /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like `nodeName_en-us` - /// and we don't want to have a full static list of all of these definitions when we can just define the one definition and then - /// dynamically apply that to culture specific fields. - /// - /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and store a new field - /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more objects. This does mean - /// however that if a language is deleted, the field definitions for that language will still exist in memory. This isn't going to cause any - /// problems and the mem will be cleared on next site restart but it's worth pointing out. - /// - public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) - { - if (base.TryGetValue(fieldName, out fieldDefinition)) - return true; - - //before we use regex to match do some faster simple matching since this is going to execute quite a lot - if (!fieldName.Contains("_")) - return false; - - var match = UmbracoExamineExtensions.CultureIsoCodeFieldNameMatchExpression.Match(fieldName); - if (match.Success) - { - var nonCultureFieldName = match.Groups["FieldName"].Value; - //check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field - if (base.TryGetValue(nonCultureFieldName, out var existingFieldDefinition)) - { - //now add a new field def - fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); - return true; - } - } return false; } + Match match = UmbracoExamineExtensions._cultureIsoCodeFieldNameMatchExpression.Match(fieldName); + if (match.Success) + { + var nonCultureFieldName = match.Groups["FieldName"].Value; + // check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field + if (base.TryGetValue(nonCultureFieldName, out FieldDefinition existingFieldDefinition)) + { + // now add a new field def + fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); + return true; + } + } + + return false; } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs index 49607b5851..2c6377768a 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs @@ -2,36 +2,29 @@ using Examine; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class UmbracoIndexConfig : IUmbracoIndexConfig { - public class UmbracoIndexConfig : IUmbracoIndexConfig + public UmbracoIndexConfig(IPublicAccessService publicAccessService, IScopeProvider scopeProvider) { - - public UmbracoIndexConfig(IPublicAccessService publicAccessService, IScopeProvider scopeProvider) - { - ScopeProvider = scopeProvider; - PublicAccessService = publicAccessService; - } - - protected IPublicAccessService PublicAccessService { get; } - protected IScopeProvider ScopeProvider { get; } - public IContentValueSetValidator GetContentValueSetValidator() - { - return new ContentValueSetValidator(false, true, PublicAccessService, ScopeProvider); - } - - public IContentValueSetValidator GetPublishedContentValueSetValidator() - { - return new ContentValueSetValidator(true, false, PublicAccessService, ScopeProvider); - } - - /// - /// Returns the for the member indexer - /// - /// - public IValueSetValidator GetMemberValueSetValidator() - { - return new MemberValueSetValidator(); - } + ScopeProvider = scopeProvider; + PublicAccessService = publicAccessService; } + + protected IPublicAccessService PublicAccessService { get; } + + protected IScopeProvider ScopeProvider { get; } + + public IContentValueSetValidator GetContentValueSetValidator() => + new ContentValueSetValidator(false, true, PublicAccessService, ScopeProvider); + + public IContentValueSetValidator GetPublishedContentValueSetValidator() => + new ContentValueSetValidator(true, false, PublicAccessService, ScopeProvider); + + /// + /// Returns the for the member indexer + /// + /// + public IValueSetValidator GetMemberValueSetValidator() => new MemberValueSetValidator(); } diff --git a/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs index 3bf4b97bf1..4931bd5220 100644 --- a/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs @@ -1,100 +1,104 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performing basic validation of a value set +/// +public class ValueSetValidator : IValueSetValidator { - /// - /// Performing basic validation of a value set - /// - public class ValueSetValidator : IValueSetValidator + public ValueSetValidator( + IEnumerable? includeItemTypes, + IEnumerable? excludeItemTypes, + IEnumerable? includeFields, + IEnumerable? excludeFields) { - public ValueSetValidator( - IEnumerable? includeItemTypes, - IEnumerable? excludeItemTypes, - IEnumerable? includeFields, - IEnumerable? excludeFields) + IncludeItemTypes = includeItemTypes; + ExcludeItemTypes = excludeItemTypes; + IncludeFields = includeFields; + ExcludeFields = excludeFields; + ValidIndexCategories = null; + } + + /// + /// Optional inclusion list of content types to index + /// + /// + /// All other types will be ignored if they do not match this list + /// + public IEnumerable? IncludeItemTypes { get; } + + /// + /// Optional exclusion list of content types to ignore + /// + /// + /// Any content type alias matched in this will not be included in the index + /// + public IEnumerable? ExcludeItemTypes { get; } + + /// + /// Optional inclusion list of index fields to index + /// + /// + /// If specified, all other fields in a will be filtered + /// + public IEnumerable? IncludeFields { get; } + + /// + /// Optional exclusion list of index fields + /// + /// + /// If specified, all fields matching these field names will be filtered from the + /// + public IEnumerable? ExcludeFields { get; } + + protected virtual IEnumerable? ValidIndexCategories { get; } + + public virtual ValueSetValidationResult Validate(ValueSet valueSet) + { + if (ValidIndexCategories != null && !ValidIndexCategories.InvariantContains(valueSet.Category)) { - IncludeItemTypes = includeItemTypes; - ExcludeItemTypes = excludeItemTypes; - IncludeFields = includeFields; - ExcludeFields = excludeFields; - ValidIndexCategories = null; + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); } - protected virtual IEnumerable? ValidIndexCategories { get; } - - /// - /// Optional inclusion list of content types to index - /// - /// - /// All other types will be ignored if they do not match this list - /// - public IEnumerable? IncludeItemTypes { get; } - - /// - /// Optional exclusion list of content types to ignore - /// - /// - /// Any content type alias matched in this will not be included in the index - /// - public IEnumerable? ExcludeItemTypes { get; } - - /// - /// Optional inclusion list of index fields to index - /// - /// - /// If specified, all other fields in a will be filtered - /// - public IEnumerable? IncludeFields { get; } - - /// - /// Optional exclusion list of index fields - /// - /// - /// If specified, all fields matching these field names will be filtered from the - /// - public IEnumerable? ExcludeFields { get; } - - public virtual ValueSetValidationResult Validate(ValueSet valueSet) + // check if this document is of a correct type of node type alias + if (IncludeItemTypes != null && !IncludeItemTypes.InvariantContains(valueSet.ItemType)) { - if (ValidIndexCategories != null && !ValidIndexCategories.InvariantContains(valueSet.Category)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - //check if this document is of a correct type of node type alias - if (IncludeItemTypes != null && !IncludeItemTypes.InvariantContains(valueSet.ItemType)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + // if this node type is part of our exclusion list + if (ExcludeItemTypes != null && ExcludeItemTypes.InvariantContains(valueSet.ItemType)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - //if this node type is part of our exclusion list - if (ExcludeItemTypes != null && ExcludeItemTypes.InvariantContains(valueSet.ItemType)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + var isFiltered = false; - var isFiltered = false; + var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - //filter based on the fields provided (if any) - if (IncludeFields != null || ExcludeFields != null) + // filter based on the fields provided (if any) + if (IncludeFields != null || ExcludeFields != null) + { + foreach (var key in valueSet.Values.Keys.ToList()) { - foreach (var key in valueSet.Values.Keys.ToList()) + if (IncludeFields != null && !IncludeFields.InvariantContains(key)) { - if (IncludeFields != null && !IncludeFields.InvariantContains(key)) - { - filteredValues.Remove(key); //remove any value with a key that doesn't match the inclusion list - isFiltered = true; - } - - if (ExcludeFields != null && ExcludeFields.InvariantContains(key)) - { - filteredValues.Remove(key); //remove any value with a key that matches the exclusion list - isFiltered = true; - } + filteredValues.Remove(key); // remove any value with a key that doesn't match the inclusion list + isFiltered = true; + } + if (ExcludeFields != null && ExcludeFields.InvariantContains(key)) + { + filteredValues.Remove(key); // remove any value with a key that matches the exclusion list + isFiltered = true; } } - - var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); - return new ValueSetValidationResult(isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } + + var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); + return new ValueSetValidationResult( + isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } } diff --git a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs index 8918b5f951..6eb3350d71 100644 --- a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs @@ -1,132 +1,128 @@ -using System; -using System.Collections.Generic; using MimeKit; using MimeKit.Text; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Infrastructure.Extensions +namespace Umbraco.Cms.Infrastructure.Extensions; + +internal static class EmailMessageExtensions { - internal static class EmailMessageExtensions + public static MimeMessage ToMimeMessage(this EmailMessage mailMessage, string configuredFromAddress) { - public static MimeMessage ToMimeMessage(this EmailMessage mailMessage, string configuredFromAddress) + var fromEmail = string.IsNullOrEmpty(mailMessage.From) ? configuredFromAddress : mailMessage.From; + + if (!InternetAddress.TryParse(fromEmail, out InternetAddress fromAddress)) { - var fromEmail = string.IsNullOrEmpty(mailMessage.From) ? configuredFromAddress : mailMessage.From; + throw new ArgumentException( + $"Email could not be sent. Could not parse from address {fromEmail} as a valid email address."); + } - if (!InternetAddress.TryParse(fromEmail, out InternetAddress fromAddress)) + var messageToSend = new MimeMessage { From = { fromAddress }, Subject = mailMessage.Subject }; + + AddAddresses(messageToSend, mailMessage.To, x => x.To, true); + AddAddresses(messageToSend, mailMessage.Cc, x => x.Cc); + AddAddresses(messageToSend, mailMessage.Bcc, x => x.Bcc); + AddAddresses(messageToSend, mailMessage.ReplyTo, x => x.ReplyTo); + + if (mailMessage.HasAttachments) + { + var builder = new BodyBuilder(); + if (mailMessage.IsBodyHtml) { - throw new ArgumentException($"Email could not be sent. Could not parse from address {fromEmail} as a valid email address."); - } - - var messageToSend = new MimeMessage - { - From = { fromAddress }, - Subject = mailMessage.Subject, - }; - - AddAddresses(messageToSend, mailMessage.To, x => x.To, throwIfNoneValid: true); - AddAddresses(messageToSend, mailMessage.Cc, x => x.Cc); - AddAddresses(messageToSend, mailMessage.Bcc, x => x.Bcc); - AddAddresses(messageToSend, mailMessage.ReplyTo, x => x.ReplyTo); - - if (mailMessage.HasAttachments) - { - var builder = new BodyBuilder(); - if (mailMessage.IsBodyHtml) - { - builder.HtmlBody = mailMessage.Body; - } - else - { - builder.TextBody = mailMessage.Body; - } - - foreach (EmailMessageAttachment attachment in mailMessage.Attachments!) - { - builder.Attachments.Add(attachment.FileName, attachment.Stream); - } - - messageToSend.Body = builder.ToMessageBody(); + builder.HtmlBody = mailMessage.Body; } else { - messageToSend.Body = new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body }; + builder.TextBody = mailMessage.Body; } - return messageToSend; + foreach (EmailMessageAttachment attachment in mailMessage.Attachments!) + { + builder.Attachments.Add(attachment.FileName, attachment.Stream); + } + + messageToSend.Body = builder.ToMessageBody(); + } + else + { + messageToSend.Body = + new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body }; } - private static void AddAddresses(MimeMessage message, string?[]? addresses, Func addressListGetter, bool throwIfNoneValid = false) + return messageToSend; + } + + public static NotificationEmailModel ToNotificationEmail( + this EmailMessage emailMessage, + string? configuredFromAddress) + { + var fromEmail = string.IsNullOrEmpty(emailMessage.From) ? configuredFromAddress : emailMessage.From; + + NotificationEmailAddress? from = ToNotificationAddress(fromEmail); + + return new NotificationEmailModel( + from, + GetNotificationAddresses(emailMessage.To), + GetNotificationAddresses(emailMessage.Cc), + GetNotificationAddresses(emailMessage.Bcc), + GetNotificationAddresses(emailMessage.ReplyTo), + emailMessage.Subject, + emailMessage.Body, + emailMessage.Attachments, + emailMessage.IsBodyHtml); + } + + private static void AddAddresses(MimeMessage message, string?[]? addresses, Func addressListGetter, bool throwIfNoneValid = false) + { + var foundValid = false; + if (addresses != null) { - var foundValid = false; - if (addresses != null) + foreach (var address in addresses) { - foreach (var address in addresses) + if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) { - if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) - { - addressListGetter(message).Add(internetAddress); - foundValid = true; - } + addressListGetter(message).Add(internetAddress); + foundValid = true; } } + } - if (throwIfNoneValid && foundValid == false) + if (throwIfNoneValid && foundValid == false) + { + throw new InvalidOperationException("Email could not be sent. Could not parse a valid recipient address."); + } + } + + private static NotificationEmailAddress? ToNotificationAddress(string? address) + { + if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) + { + if (internetAddress is MailboxAddress mailboxAddress) { - throw new InvalidOperationException($"Email could not be sent. Could not parse a valid recipient address."); + return new NotificationEmailAddress(mailboxAddress.Address, internetAddress.Name); } } - public static NotificationEmailModel ToNotificationEmail(this EmailMessage emailMessage, - string? configuredFromAddress) + return null; + } + + private static IEnumerable? GetNotificationAddresses(IEnumerable? addresses) + { + if (addresses is null) { - var fromEmail = string.IsNullOrEmpty(emailMessage.From) ? configuredFromAddress : emailMessage.From; - - NotificationEmailAddress? from = ToNotificationAddress(fromEmail); - - return new NotificationEmailModel( - from, - GetNotificationAddresses(emailMessage.To), - GetNotificationAddresses(emailMessage.Cc), - GetNotificationAddresses(emailMessage.Bcc), - GetNotificationAddresses(emailMessage.ReplyTo), - emailMessage.Subject, - emailMessage.Body, - emailMessage.Attachments, - emailMessage.IsBodyHtml); - } - - private static NotificationEmailAddress? ToNotificationAddress(string? address) - { - if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) - { - if (internetAddress is MailboxAddress mailboxAddress) - { - return new NotificationEmailAddress(mailboxAddress.Address, internetAddress.Name); - } - } - return null; } - private static IEnumerable? GetNotificationAddresses(IEnumerable? addresses) + var notificationAddresses = new List(); + + foreach (var address in addresses) { - if (addresses is null) + NotificationEmailAddress? notificationAddress = ToNotificationAddress(address); + if (notificationAddress is not null) { - return null; + notificationAddresses.Add(notificationAddress); } - - var notificationAddresses = new List(); - - foreach (var address in addresses) - { - NotificationEmailAddress? notificationAddress = ToNotificationAddress(address); - if (notificationAddress is not null) - { - notificationAddresses.Add(notificationAddress); - } - } - - return notificationAddresses; } + + return notificationAddresses; } } diff --git a/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs b/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs index b420f77253..bb835a5244 100644 --- a/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Packaging; -namespace Umbraco.Extensions -{ - public static class InfrastuctureTypeLoaderExtensions - { - /// - /// Gets all types implementing - /// - /// - /// - public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => mgr.GetTypes(); +namespace Umbraco.Extensions; - } +public static class InfrastuctureTypeLoaderExtensions +{ + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => + mgr.GetTypes(); } diff --git a/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs b/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs index 10f919589a..d9662501e2 100644 --- a/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs @@ -1,20 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Extensions -{ - internal static class InstanceIdentifiableExtensions - { - public static string GetDebugInfo(this IInstanceIdentifiable instance) - { - if (instance == null) - { - return "(NULL)"; - } +namespace Umbraco.Extensions; - return $"(id: {instance.InstanceId.ToString("N").Substring(0, 8)} from thread: {instance.CreatedThreadId})"; +internal static class InstanceIdentifiableExtensions +{ + public static string GetDebugInfo(this IInstanceIdentifiable? instance) + { + if (instance == null) + { + return "(NULL)"; } + + return $"(id: {instance.InstanceId.ToString("N").Substring(0, 8)} from thread: {instance.CreatedThreadId})"; } } diff --git a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs index 62a3f96b22..3cad487bbc 100644 --- a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs @@ -1,43 +1,40 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class MediaPicker3ConfigurationExtensions { - public static class MediaPicker3ConfigurationExtensions + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration? configuration) { - /// - /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. - /// - /// The configuration. - public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration? configuration) + var crops = new List(); + + MediaPicker3Configuration.CropConfiguration[]? configuredCrops = configuration?.Crops; + if (configuredCrops != null) { - var crops = new List(); - - var configuredCrops = configuration?.Crops; - if (configuredCrops != null) + foreach (MediaPicker3Configuration.CropConfiguration configuredCrop in configuredCrops) { - foreach (var configuredCrop in configuredCrops) + ImageCropperValue.ImageCropperCrop? crop = + imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperValue.ImageCropperCrop { - var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); - - crops.Add(new ImageCropperValue.ImageCropperCrop - { - Alias = configuredCrop.Alias, - Width = configuredCrop.Width, - Height = configuredCrop.Height, - Coordinates = crop?.Coordinates - }); - } + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates, + }); } + } - imageCropperValue.Crops = crops; + imageCropperValue.Crops = crops; - if (configuration?.EnableLocalFocalPoint == false) - { - imageCropperValue.FocalPoint = null; - } + if (configuration?.EnableLocalFocalPoint == false) + { + imageCropperValue.FocalPoint = null; } } } diff --git a/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs index 609d8165a3..6a34ae73f0 100644 --- a/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs @@ -1,52 +1,56 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Reflection; using Newtonsoft.Json; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides object extension methods. +/// +public static class ObjectJsonExtensions { + private static readonly ConcurrentDictionary> _toObjectTypes = new(); + /// - /// Provides object extension methods. + /// Converts an object's properties into a dictionary. /// - public static class ObjectJsonExtensions + /// The object to convert. + /// A property namer function. + /// A dictionary containing each properties. + public static Dictionary ToObjectDictionary(T obj, Func? namer = null) { - private static readonly ConcurrentDictionary> ToObjectTypes = new ConcurrentDictionary>(); - - /// - /// Converts an object's properties into a dictionary. - /// - /// The object to convert. - /// A property namer function. - /// A dictionary containing each properties. - public static Dictionary ToObjectDictionary(T obj, Func? namer = null) + if (obj == null) { - if (obj == null) return new Dictionary(); - - string DefaultNamer(PropertyInfo property) - { - var jsonProperty = property.GetCustomAttribute(); - return jsonProperty?.PropertyName ?? property.Name; - } - - var t = obj.GetType(); - - if (namer == null) namer = DefaultNamer; - - if (!ToObjectTypes.TryGetValue(t, out var properties)) - { - properties = new Dictionary(); - - foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)) - properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter(p); - - ToObjectTypes[t] = properties; - } - - return properties.ToDictionary(x => x.Key, x => ((Func) x.Value)(obj)); + return new Dictionary(); } + string DefaultNamer(PropertyInfo property) + { + JsonPropertyAttribute? jsonProperty = property.GetCustomAttribute(); + return jsonProperty?.PropertyName ?? property.Name; + } + + Type t = obj.GetType(); + + if (namer == null) + { + namer = DefaultNamer; + } + + if (!_toObjectTypes.TryGetValue(t, out Dictionary? properties)) + { + properties = new Dictionary(); + + foreach (PropertyInfo p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | + BindingFlags.FlattenHierarchy)) + { + properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter(p); + } + + _toObjectTypes[t] = properties; + } + + return properties.ToDictionary(x => x.Key, x => ((Func)x.Value)(obj)); } } diff --git a/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs index ed2c520070..d49c576924 100644 --- a/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Extensions -{ - public static class ScopeExtensions - { - public static void ReadLock(this IScope scope, ICollection lockIds) - { - foreach(var lockId in lockIds) - { - scope.ReadLock(lockId); - } - } +namespace Umbraco.Extensions; - public static void WriteLock(this IScope scope, ICollection lockIds) +public static class ScopeExtensions +{ + public static void ReadLock(this IScope scope, ICollection lockIds) + { + foreach (var lockId in lockIds) { - foreach (var lockId in lockIds) - { - scope.WriteLock(lockId); - } + scope.ReadLock(lockId); + } + } + + public static void WriteLock(this IScope scope, ICollection lockIds) + { + foreach (var lockId in lockIds) + { + scope.WriteLock(lockId); } } } diff --git a/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs index 64e329be97..4020dc3136 100644 --- a/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs +++ b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs @@ -1,34 +1,31 @@ -using HeyRed.MarkdownSharp; +using HeyRed.MarkdownSharp; using Umbraco.Cms.Core.HealthChecks; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Cms.Infrastructure.HealthChecks +namespace Umbraco.Cms.Infrastructure.HealthChecks; + +public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter { - public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter + public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity) { - public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity) - { - var mark = new Markdown(); - var html = mark.Transform(results.ResultsAsMarkDown(verbosity)); - html = ApplyHtmlHighlighting(html); - return html; - } - - private string ApplyHtmlHighlighting(string html) - { - const string SuccessHexColor = "5cb85c"; - const string WarningHexColor = "f0ad4e"; - const string ErrorHexColor = "d9534f"; - - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor); - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor); - return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor); - } - - private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color) - { - return html - .Replace("Result: '" + status + "'", "Result: " + status + ""); - } + var mark = new Markdown(); + var html = mark.Transform(results.ResultsAsMarkDown(verbosity)); + html = ApplyHtmlHighlighting(html); + return html; } + + private string ApplyHtmlHighlighting(string html) + { + const string successHexColor = "5cb85c"; + const string warningHexColor = "f0ad4e"; + const string errorHexColor = "d9534f"; + + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, successHexColor); + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, warningHexColor); + return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, errorHexColor); + } + + private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color) => + html + .Replace("Result: '" + status + "'", "Result: " + status + ""); } diff --git a/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs index ece1827ed0..522fae5c4d 100644 --- a/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs +++ b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs @@ -1,42 +1,37 @@ -using System; using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// A Background Task Queue, to enqueue tasks for executing in the background. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public class BackgroundTaskQueue : IBackgroundTaskQueue { - /// - /// A Background Task Queue, to enqueue tasks for executing in the background. - /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public class BackgroundTaskQueue : IBackgroundTaskQueue + private readonly SemaphoreSlim _signal = new(0); + + private readonly ConcurrentQueue> _workItems = new(); + + /// + public void QueueBackgroundWorkItem(Func workItem) { - private readonly ConcurrentQueue> _workItems = - new ConcurrentQueue>(); - - private readonly SemaphoreSlim _signal = new SemaphoreSlim(0); - - /// - public void QueueBackgroundWorkItem(Func workItem) + if (workItem == null) { - if (workItem == null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - _workItems.Enqueue(workItem); - _signal.Release(); + throw new ArgumentNullException(nameof(workItem)); } - /// - public async Task?> DequeueAsync(CancellationToken cancellationToken) - { - await _signal.WaitAsync(cancellationToken); - _workItems.TryDequeue(out Func? workItem); + _workItems.Enqueue(workItem); + _signal.Release(); + } - return workItem; - } + /// + public async Task?> DequeueAsync(CancellationToken cancellationToken) + { + await _signal.WaitAsync(cancellationToken); + _workItems.TryDequeue(out Func? workItem); + + return workItem; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs index 5fdd69035d..1b62e8e31d 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -8,89 +6,89 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Recurring hosted service that executes the content history cleanup. +/// +public class ContentVersionCleanup : RecurringHostedServiceBase { + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRoleAccessor; + private readonly IContentVersionService _service; + private readonly IOptionsMonitor _settingsMonitor; + /// - /// Recurring hosted service that executes the content history cleanup. + /// Initializes a new instance of the class. /// - public class ContentVersionCleanup : RecurringHostedServiceBase + public ContentVersionCleanup( + IRuntimeState runtimeState, + ILogger logger, + IOptionsMonitor settingsMonitor, + IContentVersionService service, + IMainDom mainDom, + IServerRoleAccessor serverRoleAccessor) + : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) { - private readonly IRuntimeState _runtimeState; - private readonly ILogger _logger; - private readonly IOptionsMonitor _settingsMonitor; - private readonly IContentVersionService _service; - private readonly IMainDom _mainDom; - private readonly IServerRoleAccessor _serverRoleAccessor; + _runtimeState = runtimeState; + _logger = logger; + _settingsMonitor = settingsMonitor; + _service = service; + _mainDom = mainDom; + _serverRoleAccessor = serverRoleAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public ContentVersionCleanup( - IRuntimeState runtimeState, - ILogger logger, - IOptionsMonitor settingsMonitor, - IContentVersionService service, - IMainDom mainDom, - IServerRoleAccessor serverRoleAccessor) - : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) + /// + public override Task PerformExecuteAsync(object? state) + { + // Globally disabled by feature flag + if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup) { - _runtimeState = runtimeState; - _logger = logger; - _settingsMonitor = settingsMonitor; - _service = service; - _mainDom = mainDom; - _serverRoleAccessor = serverRoleAccessor; + _logger.LogInformation( + "ContentVersionCleanup task will not run as it has been globally disabled via configuration"); + return Task.CompletedTask; } - /// - public override Task PerformExecuteAsync(object? state) + if (_runtimeState.Level != RuntimeLevel.Run) { - // Globally disabled by feature flag - if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup) - { - _logger.LogInformation("ContentVersionCleanup task will not run as it has been globally disabled via configuration"); + return Task.FromResult(true); // repeat... + } + + // Don't run on replicas nor unknown role servers + switch (_serverRoleAccessor.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers"); return Task.CompletedTask; - } - - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.FromResult(true); // repeat... - } - - // Don't run on replicas nor unknown role servers - switch (_serverRoleAccessor.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers"); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role"); - return Task.CompletedTask; - case ServerRole.Single: - case ServerRole.SchedulingPublisher: - default: - break; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (!_mainDom.IsMainDom) - { - _logger.LogDebug("Does not run if not MainDom"); - return Task.FromResult(false); // do NOT repeat, going down - } - - var count = _service.PerformContentVersionCleanup(DateTime.Now).Count; - - if (count > 0) - { - _logger.LogInformation("Deleted {count} ContentVersion(s)", count); - } - else - { - _logger.LogDebug("Task complete, no items were Deleted"); - } - - return Task.FromResult(true); + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role"); + return Task.CompletedTask; + case ServerRole.Single: + case ServerRole.SchedulingPublisher: + default: + break; } + + // Ensure we do not run if not main domain, but do NOT lock it + if (!_mainDom.IsMainDom) + { + _logger.LogDebug("Does not run if not MainDom"); + return Task.FromResult(false); // do NOT repeat, going down + } + + var count = _service.PerformContentVersionCleanup(DateTime.Now).Count; + + if (count > 0) + { + _logger.LogInformation("Deleted {count} ContentVersion(s)", count); + } + else + { + _logger.LogDebug("Task complete, no items were Deleted"); + } + + return Task.FromResult(true); } } diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs index efbb30291e..30d164276a 100644 --- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs +++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -19,123 +15,122 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for recurring health check notifications. +/// +public class HealthCheckNotifier : RecurringHostedServiceBase { + private readonly HealthCheckCollection _healthChecks; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly HealthCheckNotificationMethodCollection _notifications; + private readonly IProfilingLogger _profilingLogger; + private readonly IRuntimeState _runtimeState; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerRoleAccessor _serverRegistrar; + private HealthChecksSettings _healthChecksSettings; + /// - /// Hosted service implementation for recurring health check notifications. + /// Initializes a new instance of the class. /// - public class HealthCheckNotifier : RecurringHostedServiceBase + /// The configuration for health check settings. + /// The collection of healthchecks. + /// The collection of healthcheck notification methods. + /// Representation of the state of the Umbraco runtime. + /// Provider of server registrations to the distributed cache. + /// Representation of the main application domain. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + /// Parser of crontab expressions. + public HealthCheckNotifier( + IOptionsMonitor healthChecksSettings, + HealthCheckCollection healthChecks, + HealthCheckNotificationMethodCollection notifications, + IRuntimeState runtimeState, + IServerRoleAccessor serverRegistrar, + IMainDom mainDom, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger, + ICronTabParser cronTabParser) + : base( + logger, + healthChecksSettings.CurrentValue.Notification.Period, + GetDelay(healthChecksSettings.CurrentValue.Notification.FirstRunTime, cronTabParser, logger, DefaultDelay)) { - private HealthChecksSettings _healthChecksSettings; - private readonly HealthCheckCollection _healthChecks; - private readonly HealthCheckNotificationMethodCollection _notifications; - private readonly IRuntimeState _runtimeState; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IMainDom _mainDom; - private readonly ICoreScopeProvider _scopeProvider; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; + _healthChecksSettings = healthChecksSettings.CurrentValue; + _healthChecks = healthChecks; + _notifications = notifications; + _runtimeState = runtimeState; + _serverRegistrar = serverRegistrar; + _mainDom = mainDom; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; - /// - /// Initializes a new instance of the class. - /// - /// The configuration for health check settings. - /// The collection of healthchecks. - /// The collection of healthcheck notification methods. - /// Representation of the state of the Umbraco runtime. - /// Provider of server registrations to the distributed cache. - /// Representation of the main application domain. - /// Provides scopes for database operations. - /// The typed logger. - /// The profiling logger. - /// Parser of crontab expressions. - public HealthCheckNotifier( - IOptionsMonitor healthChecksSettings, - HealthCheckCollection healthChecks, - HealthCheckNotificationMethodCollection notifications, - IRuntimeState runtimeState, - IServerRoleAccessor serverRegistrar, - IMainDom mainDom, - ICoreScopeProvider scopeProvider, - ILogger logger, - IProfilingLogger profilingLogger, - ICronTabParser cronTabParser) - : base( - logger, - healthChecksSettings.CurrentValue.Notification.Period, - healthChecksSettings.CurrentValue.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay)) + healthChecksSettings.OnChange(x => { - _healthChecksSettings = healthChecksSettings.CurrentValue; - _healthChecks = healthChecks; - _notifications = notifications; - _runtimeState = runtimeState; - _serverRegistrar = serverRegistrar; - _mainDom = mainDom; - _scopeProvider = scopeProvider; - _logger = logger; - _profilingLogger = profilingLogger; + _healthChecksSettings = x; + ChangePeriod(x.Notification.Period); + }); + } - healthChecksSettings.OnChange(x => - { - _healthChecksSettings = x; - ChangePeriod(x.Notification.Period); - }); + public override async Task PerformExecuteAsync(object? state) + { + if (_healthChecksSettings.Notification.Enabled == false) + { + return; } - public override async Task PerformExecuteAsync(object? state) + if (_runtimeState.Level != RuntimeLevel.Run) { - if (_healthChecksSettings.Notification.Enabled == false) - { + return; + } + + switch (_serverRegistrar.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); return; - } - - if (_runtimeState.Level != RuntimeLevel.Run) - { + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); return; - } + } - switch (_serverRegistrar.CurrentServerRole) + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return; + } + + // Ensure we use an explicit scope since we are running on a background thread and plugin health + // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope + // isn't used since that can be problematic. + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete")) + { + // Don't notify for any checks that are disabled, nor for any disabled just for notifications. + Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks + .Select(x => x.Id) + .Union(_healthChecksSettings.DisabledChecks + .Select(x => x.Id)) + .Distinct() + .ToArray(); + + IEnumerable checks = _healthChecks + .Where(x => disabledCheckIds.Contains(x.Id) == false); + + HealthCheckResults results = await HealthCheckResults.Create(checks); + results.LogResults(); + + // Send using registered notification methods that are enabled. + foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled)) { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return; - } - - // Ensure we use an explicit scope since we are running on a background thread and plugin health - // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope - // isn't used since that can be problematic. - using (ICoreScope scope = _scopeProvider.CreateCoreScope()) - using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete")) - { - // Don't notify for any checks that are disabled, nor for any disabled just for notifications. - Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks - .Select(x => x.Id) - .Union(_healthChecksSettings.DisabledChecks - .Select(x => x.Id)) - .Distinct() - .ToArray(); - - IEnumerable checks = _healthChecks - .Where(x => disabledCheckIds.Contains(x.Id) == false); - - var results = await HealthCheckResults.Create(checks); - results.LogResults(); - - // Send using registered notification methods that are enabled. - foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled)) - { - await notificationMethod.SendAsync(results); - } + await notificationMethod.SendAsync(results); } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs index 4e3052dd9e..aa89d59d77 100644 --- a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs +++ b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs @@ -1,25 +1,20 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Infrastructure.HostedServices; -namespace Umbraco.Cms.Infrastructure.HostedServices +/// +/// A Background Task Queue, to enqueue tasks for executing in the background. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public interface IBackgroundTaskQueue { /// - /// A Background Task Queue, to enqueue tasks for executing in the background. + /// Enqueue a work item to be executed on in the background. /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public interface IBackgroundTaskQueue - { - /// - /// Enqueue a work item to be executed on in the background. - /// - void QueueBackgroundWorkItem(Func workItem); + void QueueBackgroundWorkItem(Func workItem); - /// - /// Dequeue the first item on the queue. - /// - Task?> DequeueAsync(CancellationToken cancellationToken); - } + /// + /// Dequeue the first item on the queue. + /// + Task?> DequeueAsync(CancellationToken cancellationToken); } diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index e6b8ce47eb..b10f56cc74 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -16,99 +12,100 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for keep alive feature. +/// +public class KeepAlive : RecurringHostedServiceBase { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _profilingLogger; + private readonly IServerRoleAccessor _serverRegistrar; + private KeepAliveSettings _keepAliveSettings; + /// - /// Hosted service implementation for keep alive feature. + /// Initializes a new instance of the class. /// - public class KeepAlive : RecurringHostedServiceBase + /// The current hosting environment + /// Representation of the main application domain. + /// The configuration for keep alive settings. + /// The typed logger. + /// The profiling logger. + /// Provider of server registrations to the distributed cache. + /// Factory for instances. + public KeepAlive( + IHostingEnvironment hostingEnvironment, + IMainDom mainDom, + IOptionsMonitor keepAliveSettings, + ILogger logger, + IProfilingLogger profilingLogger, + IServerRoleAccessor serverRegistrar, + IHttpClientFactory httpClientFactory) + : base(logger, TimeSpan.FromMinutes(5), DefaultDelay) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IMainDom _mainDom; - private KeepAliveSettings _keepAliveSettings; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IHttpClientFactory _httpClientFactory; + _hostingEnvironment = hostingEnvironment; + _mainDom = mainDom; + _keepAliveSettings = keepAliveSettings.CurrentValue; + _logger = logger; + _profilingLogger = profilingLogger; + _serverRegistrar = serverRegistrar; + _httpClientFactory = httpClientFactory; - /// - /// Initializes a new instance of the class. - /// - /// The current hosting environment - /// Representation of the main application domain. - /// The configuration for keep alive settings. - /// The typed logger. - /// The profiling logger. - /// Provider of server registrations to the distributed cache. - /// Factory for instances. - public KeepAlive( - IHostingEnvironment hostingEnvironment, - IMainDom mainDom, - IOptionsMonitor keepAliveSettings, - ILogger logger, - IProfilingLogger profilingLogger, - IServerRoleAccessor serverRegistrar, - IHttpClientFactory httpClientFactory) - : base(logger, TimeSpan.FromMinutes(5), DefaultDelay) + keepAliveSettings.OnChange(x => _keepAliveSettings = x); + } + + public override async Task PerformExecuteAsync(object? state) + { + if (_keepAliveSettings.DisableKeepAliveTask) { - _hostingEnvironment = hostingEnvironment; - _mainDom = mainDom; - _keepAliveSettings = keepAliveSettings.CurrentValue; - _logger = logger; - _profilingLogger = profilingLogger; - _serverRegistrar = serverRegistrar; - _httpClientFactory = httpClientFactory; - - keepAliveSettings.OnChange(x => _keepAliveSettings = x); + return; } - public override async Task PerformExecuteAsync(object? state) + // Don't run on replicas nor unknown role servers + switch (_serverRegistrar.CurrentServerRole) { - if (_keepAliveSettings.DisableKeepAliveTask) + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return; + } + + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return; + } + + using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) + { + var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString(); + if (umbracoAppUrl.IsNullOrWhiteSpace()) { + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); return; } - // Don't run on replicas nor unknown role servers - switch (_serverRegistrar.CurrentServerRole) + // If the config is an absolute path, just use it + var keepAlivePingUrl = WebPath.Combine( + umbracoAppUrl!, + _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); + + try { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return; + var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); + HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors); + _ = await httpClient.SendAsync(request); } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) + catch (Exception ex) { - _logger.LogDebug("Does not run if not MainDom."); - return; - } - - using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) - { - var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString(); - if (umbracoAppUrl.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return; - } - - // If the config is an absolute path, just use it - string keepAlivePingUrl = WebPath.Combine(umbracoAppUrl!, _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); - - try - { - var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); - HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors); - _ = await httpClient.SendAsync(request); - } - catch (Exception ex) - { - _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); - } + _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs index 4877c4cb25..b69342d25b 100644 --- a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs +++ b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -12,82 +10,81 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices -{ - /// - /// Log scrubbing hosted service. - /// - /// - /// Will only run on non-replica servers. - /// - public class LogScrubber : RecurringHostedServiceBase - { - private readonly IMainDom _mainDom; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IAuditService _auditService; - private LoggingSettings _settings; - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - private readonly ICoreScopeProvider _scopeProvider; +namespace Umbraco.Cms.Infrastructure.HostedServices; - /// - /// Initializes a new instance of the class. - /// - /// Representation of the main application domain. - /// Provider of server registrations to the distributed cache. - /// Service for handling audit operations. - /// The configuration for logging settings. - /// Provides scopes for database operations. - /// The typed logger. - /// The profiling logger. - public LogScrubber( - IMainDom mainDom, - IServerRoleAccessor serverRegistrar, - IAuditService auditService, - IOptionsMonitor settings, - ICoreScopeProvider scopeProvider, - ILogger logger, - IProfilingLogger profilingLogger) - : base(logger, TimeSpan.FromHours(4), DefaultDelay) +/// +/// Log scrubbing hosted service. +/// +/// +/// Will only run on non-replica servers. +/// +public class LogScrubber : RecurringHostedServiceBase +{ + private readonly IAuditService _auditService; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerRoleAccessor _serverRegistrar; + private LoggingSettings _settings; + + /// + /// Initializes a new instance of the class. + /// + /// Representation of the main application domain. + /// Provider of server registrations to the distributed cache. + /// Service for handling audit operations. + /// The configuration for logging settings. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + public LogScrubber( + IMainDom mainDom, + IServerRoleAccessor serverRegistrar, + IAuditService auditService, + IOptionsMonitor settings, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger) + : base(logger, TimeSpan.FromHours(4), DefaultDelay) + { + _mainDom = mainDom; + _serverRegistrar = serverRegistrar; + _auditService = auditService; + _settings = settings.CurrentValue; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; + settings.OnChange(x => _settings = x); + } + + public override Task PerformExecuteAsync(object? state) + { + switch (_serverRegistrar.CurrentServerRole) { - _mainDom = mainDom; - _serverRegistrar = serverRegistrar; - _auditService = auditService; - _settings = settings.CurrentValue; - _scopeProvider = scopeProvider; - _logger = logger; - _profilingLogger = profilingLogger; - settings.OnChange(x => _settings = x); + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return Task.CompletedTask; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return Task.CompletedTask; } - public override Task PerformExecuteAsync(object? state) + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) { - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return Task.CompletedTask; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - // Ensure we use an explicit scope since we are running on a background thread. - using (ICoreScope scope = _scopeProvider.CreateCoreScope()) - using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) - { - _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes); - _ = scope.Complete(); - } - + _logger.LogDebug("Does not run if not MainDom."); return Task.CompletedTask; } + + // Ensure we use an explicit scope since we are running on a background thread. + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) + using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) + { + _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes); + _ = scope.Complete(); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs index e271c98324..79d93a928f 100644 --- a/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs +++ b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs @@ -1,62 +1,57 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// A queue based hosted service used to executing tasks on a background thread. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public class QueuedHostedService : BackgroundService { + private readonly ILogger _logger; - /// - /// A queue based hosted service used to executing tasks on a background thread. - /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public class QueuedHostedService : BackgroundService + public QueuedHostedService( + IBackgroundTaskQueue taskQueue, + ILogger logger) { - private readonly ILogger _logger; + TaskQueue = taskQueue; + _logger = logger; + } - public QueuedHostedService(IBackgroundTaskQueue taskQueue, - ILogger logger) + public IBackgroundTaskQueue TaskQueue { get; } + + public override async Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Queued Hosted Service is stopping."); + + await base.StopAsync(stoppingToken); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) => + await BackgroundProcessing(stoppingToken); + + private async Task BackgroundProcessing(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) { - TaskQueue = taskQueue; - _logger = logger; - } + Func? workItem = await TaskQueue.DequeueAsync(stoppingToken); - public IBackgroundTaskQueue TaskQueue { get; } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await BackgroundProcessing(stoppingToken); - } - - private async Task BackgroundProcessing(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) + try { - Func? workItem = await TaskQueue.DequeueAsync(stoppingToken); - - try + if (workItem is not null) { - if (workItem is not null) - { - await workItem(stoppingToken); - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error occurred executing {WorkItem}.", nameof(workItem)); + await workItem(stoppingToken); } } - } - - public override async Task StopAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Queued Hosted Service is stopping."); - - await base.StopAsync(stoppingToken); + catch (Exception ex) + { + _logger.LogError( + ex, + "Error occurred executing {WorkItem}.", nameof(workItem)); + } } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index f0903ff65f..a08c5f1b59 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -1,122 +1,178 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Provides a base class for recurring background tasks implemented as hosted services. +/// +/// +/// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks +/// +public abstract class RecurringHostedServiceBase : IHostedService, IDisposable { /// - /// Provides a base class for recurring background tasks implemented as hosted services. + /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is + /// configured. /// - /// - /// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks - /// - public abstract class RecurringHostedServiceBase : IHostedService, IDisposable + protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); + + private readonly TimeSpan _delay; + + private readonly ILogger? _logger; + private bool _disposedValue; + private TimeSpan _period; + private Timer? _timer; + + /// + /// Initializes a new instance of the class. + /// + /// Logger. + /// Timespan representing how often the task should recur. + /// + /// Timespan representing the initial delay after application start-up before the first run of the task + /// occurs. + /// + protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay) { - /// - /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is configured. - /// - protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); + _logger = logger; + _period = period; + _delay = delay; + } - private readonly ILogger? _logger; - private TimeSpan _period; - private readonly TimeSpan _delay; - private Timer? _timer; - private bool _disposedValue; + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// Initializes a new instance of the class. - /// - /// Logger. - /// Timespan representing how often the task should recur. - /// Timespan representing the initial delay after application start-up before the first run of the task occurs. - protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay) + /// + /// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal + /// configuration for the first run time is available. + /// + /// The configured time to first run the task in crontab format. + /// An instance of + /// The logger. + /// The default delay to use when a first run time is not configured. + /// The delay before first running the recurring task. + protected static TimeSpan GetDelay( + string firstRunTime, + ICronTabParser cronTabParser, + ILogger logger, + TimeSpan defaultDelay) => GetDelay(firstRunTime, cronTabParser, logger, DateTime.Now, defaultDelay); + + /// + /// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal + /// configuration for the first run time is available. + /// + /// The configured time to first run the task in crontab format. + /// An instance of + /// The logger. + /// The current datetime. + /// The default delay to use when a first run time is not configured. + /// The delay before first running the recurring task. + /// Internal to expose for unit tests. + internal static TimeSpan GetDelay( + string firstRunTime, + ICronTabParser cronTabParser, + ILogger logger, + DateTime now, + TimeSpan defaultDelay) + { + // If first run time not set, start with just small delay after application start. + if (string.IsNullOrEmpty(firstRunTime)) { - _logger = logger; - _period = period; - _delay = delay; + return defaultDelay; } - /// - /// Change the period between operations. - /// - /// The new period between tasks - protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod; - - /// - public Task StartAsync(CancellationToken cancellationToken) + // If first run time not a valid cron tab, log, and revert to small delay after application start. + if (!cronTabParser.IsValidCronTab(firstRunTime)) { - using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) - { - _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); - } - - return Task.CompletedTask; + logger.LogWarning("Could not parse {FirstRunTime} as a crontab expression. Defaulting to default delay for hosted service start.", firstRunTime); + return defaultDelay; } - /// - /// Executes the task. - /// - /// The task state. - public async void ExecuteAsync(object? state) - { - try - { - // First, stop the timer, we do not want tasks to execute in parallel - _timer?.Change(Timeout.Infinite, 0); + // Otherwise start at scheduled time according to cron expression, unless within the default delay period. + DateTime firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now); + TimeSpan delay = firstRunOccurance - now; + return delay < defaultDelay + ? defaultDelay + : delay; + } - // Delegate work to method returning a task, that can be called and asserted in a unit test. - // Without this there can be behaviour where tests pass, but an error within them causes the test - // running process to crash. - // Hat-tip: https://stackoverflow.com/a/14207615/489433 - await PerformExecuteAsync(state); - } - catch (Exception ex) - { - ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType()); - logger.LogError(ex, "Unhandled exception in recurring hosted service."); - } - finally - { - // Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay. - // So first execution is after _delay, and the we wait _period between each - _timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds); - } + /// + public Task StartAsync(CancellationToken cancellationToken) + { + using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) + { + _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); } - public abstract Task PerformExecuteAsync(object? state); + return Task.CompletedTask; + } - /// - public Task StopAsync(CancellationToken cancellationToken) + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _period = Timeout.InfiniteTimeSpan; + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + + /// + /// Executes the task. + /// + /// The task state. + public async void ExecuteAsync(object? state) + { + try { - _period = Timeout.InfiniteTimeSpan; + // First, stop the timer, we do not want tasks to execute in parallel _timer?.Change(Timeout.Infinite, 0); - return Task.CompletedTask; - } - protected virtual void Dispose(bool disposing) + // Delegate work to method returning a task, that can be called and asserted in a unit test. + // Without this there can be behaviour where tests pass, but an error within them causes the test + // running process to crash. + // Hat-tip: https://stackoverflow.com/a/14207615/489433 + await PerformExecuteAsync(state); + } + catch (Exception ex) { - if (!_disposedValue) + ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType()); + logger.LogError(ex, "Unhandled exception in recurring hosted service."); + } + finally + { + // Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay. + // So first execution is after _delay, and the we wait _period between each + _timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds); + } + } + + public abstract Task PerformExecuteAsync(object? state); + + /// + /// Change the period between operations. + /// + /// The new period between tasks + protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod; + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) { - if (disposing) - { - _timer?.Dispose(); - } - - _disposedValue = true; + _timer?.Dispose(); } - } - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 5179d3f2e1..d4a6265052 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -1,83 +1,97 @@ -using System; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.HostedServices -{ - public class ReportSiteTask : RecurringHostedServiceBase - { - private readonly ILogger _logger; - private readonly ITelemetryService _telemetryService; - private static HttpClient s_httpClient = new(); +namespace Umbraco.Cms.Infrastructure.HostedServices; - public ReportSiteTask( - ILogger logger, - ITelemetryService telemetryService) - : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) - { - _logger = logger; - _telemetryService = telemetryService; - s_httpClient = new HttpClient(); - } +public class ReportSiteTask : RecurringHostedServiceBase +{ + private static HttpClient _httpClient = new(); + private readonly ILogger _logger; + private readonly ITelemetryService _telemetryService; + private readonly IRuntimeState _runtimeState; + + public ReportSiteTask( + ILogger logger, + ITelemetryService telemetryService, + IRuntimeState runtimeState) + : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(5)) + { + _logger = logger; + _telemetryService = telemetryService; + _runtimeState = runtimeState; + _httpClient = new HttpClient(); + } + + [Obsolete("Use the constructor that takes IRuntimeState, scheduled for removal in V12")] + public ReportSiteTask( + ILogger logger, + ITelemetryService telemetryService) + : this(logger, telemetryService, StaticServiceProvider.Instance.GetRequiredService()) + { + } /// /// Runs the background task to send the anonymous ID /// to telemetry service /// - public override async Task PerformExecuteAsync(object? state) + public override async Task PerformExecuteAsync(object? state){ + if (_runtimeState.Level is not RuntimeLevel.Run) { - if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) - { - _logger.LogWarning("No telemetry marker found"); + // We probably haven't installed yet, so we can't get telemetry. + return; + } - return; - } + if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) + { + _logger.LogWarning("No telemetry marker found"); - try + return; + } + + try + { + if (_httpClient.BaseAddress is null) { - if (s_httpClient.BaseAddress is null) - { - // Send data to LIVE telemetry - s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); + // Send data to LIVE telemetry + _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); #if DEBUG - // Send data to DEBUG telemetry service - s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); + // Send data to DEBUG telemetry service + _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); #endif - } - - - s_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); - - using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) - { - request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header - - // Make a HTTP Post to telemetry service - // https://telemetry.umbraco.com/installs/ - // Fire & Forget, do not need to know if its a 200, 500 etc - using (HttpResponseMessage response = await s_httpClient.SendAsync(request)) - { - } - } } - catch + + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + + using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) { - // Silently swallow - // The user does not need the logs being polluted if our service has fallen over or is down etc - // Hence only logging this at a more verbose level (which users should not be using in production) - _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); + request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, + "application/json"); + + // Make a HTTP Post to telemetry service + // https://telemetry.umbraco.com/installs/ + // Fire & Forget, do not need to know if its a 200, 500 etc + using (await _httpClient.SendAsync(request)) + { + } } } + catch + { + // Silently swallow + // The user does not need the logs being polluted if our service has fallen over or is down etc + // Hence only logging this at a more verbose level (which users should not be using in production) + _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); + } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index 6d659425e0..d593124ccb 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -1,11 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Runtime; @@ -13,129 +8,127 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for scheduled publishing feature. +/// +/// +/// Runs only on non-replica servers. +/// +public class ScheduledPublishing : RecurringHostedServiceBase { + private readonly IContentService _contentService; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IRuntimeState _runtimeState; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerMessenger _serverMessenger; + private readonly IServerRoleAccessor _serverRegistrar; + private readonly IUmbracoContextFactory _umbracoContextFactory; + /// - /// Hosted service implementation for scheduled publishing feature. + /// Initializes a new instance of the class. /// - /// - /// Runs only on non-replica servers. - public class ScheduledPublishing : RecurringHostedServiceBase + public ScheduledPublishing( + IRuntimeState runtimeState, + IMainDom mainDom, + IServerRoleAccessor serverRegistrar, + IContentService contentService, + IUmbracoContextFactory umbracoContextFactory, + ILogger logger, + IServerMessenger serverMessenger, + ICoreScopeProvider scopeProvider) + : base(logger, TimeSpan.FromMinutes(1), DefaultDelay) { - private readonly IContentService _contentService; - private readonly ILogger _logger; - private readonly IMainDom _mainDom; - private readonly IRuntimeState _runtimeState; - private readonly IServerMessenger _serverMessenger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IUmbracoContextFactory _umbracoContextFactory; + _runtimeState = runtimeState; + _mainDom = mainDom; + _serverRegistrar = serverRegistrar; + _contentService = contentService; + _umbracoContextFactory = umbracoContextFactory; + _logger = logger; + _serverMessenger = serverMessenger; + _scopeProvider = scopeProvider; + } - /// - /// Initializes a new instance of the class. - /// - public ScheduledPublishing( - IRuntimeState runtimeState, - IMainDom mainDom, - IServerRoleAccessor serverRegistrar, - IContentService contentService, - IUmbracoContextFactory umbracoContextFactory, - ILogger logger, - IServerMessenger serverMessenger, - ICoreScopeProvider scopeProvider) - : base(logger, TimeSpan.FromMinutes(1), DefaultDelay) + public override Task PerformExecuteAsync(object? state) + { + if (Suspendable.ScheduledPublishing.CanRun == false) { - _runtimeState = runtimeState; - _mainDom = mainDom; - _serverRegistrar = serverRegistrar; - _contentService = contentService; - _umbracoContextFactory = umbracoContextFactory; - _logger = logger; - _serverMessenger = serverMessenger; - _scopeProvider = scopeProvider; - } - - public override Task PerformExecuteAsync(object? state) - { - if (Suspendable.ScheduledPublishing.CanRun == false) - { - return Task.CompletedTask; - } - - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return Task.CompletedTask; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - // Do NOT run publishing if not properly running - if (_runtimeState.Level != RuntimeLevel.Run) - { - _logger.LogDebug("Does not run if run level is not Run."); - return Task.CompletedTask; - } - - try - { - // Ensure we run with an UmbracoContext, because this will run in a background task, - // and developers may be using the UmbracoContext in the event handlers. - - // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events - // - UmbracoContext 'current' needs to be refactored and cleaned up - // - batched messenger should not depend on a current HttpContext - // but then what should be its "scope"? could we attach it to scopes? - // - and we should definitively *not* have to flush it here (should be auto) - - using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - - /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) - * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. - * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel. - * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's - * only until the old SchedulingPublisher shuts down. */ - scope.EagerWriteLock(Constants.Locks.ScheduledPublishing); - try - { - // Run - IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now); - foreach (IGrouping grouped in result.GroupBy(x => x.Result)) - { - _logger.LogInformation( - "Scheduled publishing result: '{StatusCount}' items with status {Status}", - grouped.Count(), - grouped.Key); - } - } - finally - { - // If running on a temp context, we have to flush the messenger - if (contextReference.IsRoot) - { - _serverMessenger.SendMessages(); - } - } - } - catch (Exception ex) - { - // important to catch *everything* to ensure the task repeats - _logger.LogError(ex, "Failed."); - } - return Task.CompletedTask; } + + switch (_serverRegistrar.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return Task.CompletedTask; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return Task.CompletedTask; + } + + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return Task.CompletedTask; + } + + // Do NOT run publishing if not properly running + if (_runtimeState.Level != RuntimeLevel.Run) + { + _logger.LogDebug("Does not run if run level is not Run."); + return Task.CompletedTask; + } + + try + { + // Ensure we run with an UmbracoContext, because this will run in a background task, + // and developers may be using the UmbracoContext in the event handlers. + + // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events + // - UmbracoContext 'current' needs to be refactored and cleaned up + // - batched messenger should not depend on a current HttpContext + // but then what should be its "scope"? could we attach it to scopes? + // - and we should definitively *not* have to flush it here (should be auto) + using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) + * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. + * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel. + * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's + * only until the old SchedulingPublisher shuts down. */ + scope.EagerWriteLock(Constants.Locks.ScheduledPublishing); + try + { + // Run + IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (IGrouping grouped in result.GroupBy(x => x.Result)) + { + _logger.LogInformation( + "Scheduled publishing result: '{StatusCount}' items with status {Status}", + grouped.Count(), + grouped.Key); + } + } + finally + { + // If running on a temp context, we have to flush the messenger + if (contextReference.IsRoot) + { + _serverMessenger.SendMessages(); + } + } + } + catch (Exception ex) + { + // important to catch *everything* to ensure the task repeats + _logger.LogError(ex, "Failed."); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs index d153366949..e4e5700496 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -10,65 +8,64 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration +namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; + +/// +/// Implements periodic database instruction processing as a hosted service. +/// +public class InstructionProcessTask : RecurringHostedServiceBase { + private readonly ILogger _logger; + private readonly IServerMessenger _messenger; + private readonly IRuntimeState _runtimeState; + private bool _disposedValue; + /// - /// Implements periodic database instruction processing as a hosted service. + /// Initializes a new instance of the class. /// - public class InstructionProcessTask : RecurringHostedServiceBase + /// Representation of the state of the Umbraco runtime. + /// Service broadcasting cache notifications to registered servers. + /// The typed logger. + /// The configuration for global settings. + public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings) + : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) { - private readonly IRuntimeState _runtimeState; - private readonly IServerMessenger _messenger; - private readonly ILogger _logger; - private bool _disposedValue; + _runtimeState = runtimeState; + _messenger = messenger; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Representation of the state of the Umbraco runtime. - /// Service broadcasting cache notifications to registered servers. - /// The typed logger. - /// The configuration for global settings. - public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings) - : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) + public override Task PerformExecuteAsync(object? state) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - _runtimeState = runtimeState; - _messenger = messenger; - _logger = logger; - } - - public override Task PerformExecuteAsync(object? state) - { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.CompletedTask; - } - - try - { - _messenger.Sync(); - } - catch (Exception e) - { - _logger.LogError(e, "Failed (will repeat)."); - } - return Task.CompletedTask; } - protected override void Dispose(bool disposing) + try { - if (!_disposedValue) - { - if (disposing && _messenger is IDisposable disposable) - { - disposable.Dispose(); - } + _messenger.Sync(); + } + catch (Exception e) + { + _logger.LogError(e, "Failed (will repeat)."); + } - _disposedValue = true; + return Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && _messenger is IDisposable disposable) + { + disposable.Dispose(); } - base.Dispose(disposing); + _disposedValue = true; } + + base.Dispose(disposing); } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index d755324878..730282c6b0 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,85 +8,87 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration +namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; + +/// +/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. +/// +public class TouchServerTask : RecurringHostedServiceBase { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IServerRegistrationService _serverRegistrationService; + private readonly IServerRoleAccessor _serverRoleAccessor; + private GlobalSettings _globalSettings; + /// - /// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. + /// Initializes a new instance of the class. /// - public class TouchServerTask : RecurringHostedServiceBase + /// Representation of the state of the Umbraco runtime. + /// Services for server registrations. + /// The typed logger. + /// The configuration for global settings. + /// The hostingEnviroment. + /// The accessor for the server role + public TouchServerTask( + IRuntimeState runtimeState, + IServerRegistrationService serverRegistrationService, + IHostingEnvironment hostingEnvironment, + ILogger logger, + IOptionsMonitor globalSettings, + IServerRoleAccessor serverRoleAccessor) + : base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) { - private readonly IRuntimeState _runtimeState; - private readonly IServerRegistrationService _serverRegistrationService; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly IServerRoleAccessor _serverRoleAccessor; - private GlobalSettings _globalSettings; - - /// - /// Initializes a new instance of the class. - /// - /// Representation of the state of the Umbraco runtime. - /// Services for server registrations. - /// Accessor for the current request. - /// The typed logger. - /// The configuration for global settings. - public TouchServerTask( - IRuntimeState runtimeState, - IServerRegistrationService serverRegistrationService, - IHostingEnvironment hostingEnvironment, - ILogger logger, - IOptionsMonitor globalSettings, - IServerRoleAccessor serverRoleAccessor) - : base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) + _runtimeState = runtimeState; + _serverRegistrationService = serverRegistrationService ?? + throw new ArgumentNullException(nameof(serverRegistrationService)); + _hostingEnvironment = hostingEnvironment; + _logger = logger; + _globalSettings = globalSettings.CurrentValue; + globalSettings.OnChange(x => { - _runtimeState = runtimeState; - _serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService)); - _hostingEnvironment = hostingEnvironment; - _logger = logger; - _globalSettings = globalSettings.CurrentValue; - globalSettings.OnChange(x => - { - _globalSettings = x; - ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls); - }); - _serverRoleAccessor = serverRoleAccessor; - } + _globalSettings = x; + ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls); + }); + _serverRoleAccessor = serverRoleAccessor; + } - public override Task PerformExecuteAsync(object? state) + public override Task PerformExecuteAsync(object? state) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.CompletedTask; - } - - // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, - // since all it's used for is to allow the ElectedServerRoleAccessor - // to figure out what role a given server has, so we just stop this task. - if (_serverRoleAccessor is not ElectedServerRoleAccessor) - { - return StopAsync(CancellationToken.None); - } - - var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); - if (serverAddress.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return Task.CompletedTask; - } - - try - { - _serverRegistrationService.TouchServer(serverAddress!, _globalSettings.DatabaseServerRegistrar.StaleServerTimeout); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update server record in database."); - } - return Task.CompletedTask; } + + // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, + // since all it's used for is to allow the ElectedServerRoleAccessor + // to figure out what role a given server has, so we just stop this task. + if (_serverRoleAccessor is not ElectedServerRoleAccessor) + { + return StopAsync(CancellationToken.None); + } + + var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); + if (serverAddress.IsNullOrWhiteSpace()) + { + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); + return Task.CompletedTask; + } + + try + { + _serverRegistrationService.TouchServer( + serverAddress!, + _globalSettings.DatabaseServerRegistrar.StaleServerTimeout); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update server record in database."); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs index ec8d48bca3..663a89b05a 100644 --- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs @@ -1,102 +1,99 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Used to cleanup temporary file locations. +/// +/// +/// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will +/// ensure that in the case it happens on subscribers that they are cleaned up too. +/// +public class TempFileCleanup : RecurringHostedServiceBase { + private readonly TimeSpan _age = TimeSpan.FromDays(1); + private readonly IIOHelper _ioHelper; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + + private readonly DirectoryInfo[] _tempFolders; + /// - /// Used to cleanup temporary file locations. + /// Initializes a new instance of the class. /// - /// - /// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will - /// ensure that in the case it happens on subscribers that they are cleaned up too. - /// - public class TempFileCleanup : RecurringHostedServiceBase + /// Helper service for IO operations. + /// Representation of the main application domain. + /// The typed logger. + public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) + : base(logger, TimeSpan.FromMinutes(60), DefaultDelay) { - private readonly IIOHelper _ioHelper; - private readonly IMainDom _mainDom; - private readonly ILogger _logger; + _ioHelper = ioHelper; + _mainDom = mainDom; + _logger = logger; - private readonly DirectoryInfo[] _tempFolders; - private readonly TimeSpan _age = TimeSpan.FromDays(1); + _tempFolders = _ioHelper.GetTempFolders(); + } - /// - /// Initializes a new instance of the class. - /// - /// Helper service for IO operations. - /// Representation of the main application domain. - /// The typed logger. - public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) - : base(logger, TimeSpan.FromMinutes(60), DefaultDelay) + public override Task PerformExecuteAsync(object? state) + { + // Ensure we do not run if not main domain + if (_mainDom.IsMainDom == false) { - _ioHelper = ioHelper; - _mainDom = mainDom; - _logger = logger; - - _tempFolders = _ioHelper.GetTempFolders(); - } - - public override Task PerformExecuteAsync(object? state) - { - // Ensure we do not run if not main domain - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - foreach (DirectoryInfo folder in _tempFolders) - { - CleanupFolder(folder); - } - + _logger.LogDebug("Does not run if not MainDom."); return Task.CompletedTask; } - private void CleanupFolder(DirectoryInfo folder) + foreach (DirectoryInfo folder in _tempFolders) { - CleanFolderResult result = _ioHelper.CleanFolder(folder, _age); - switch (result.Status) - { - case CleanFolderResultStatus.FailedAsDoesNotExist: - _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); - break; - case CleanFolderResultStatus.FailedWithException: - foreach (CleanFolderResult.Error error in result.Errors!) - { - _logger.LogError(error.Exception, "Could not delete temp file {FileName}", error.ErroringFile.FullName); - } + CleanupFolder(folder); + } - break; - } + return Task.CompletedTask; + } - folder.Refresh(); // In case it's changed during runtime - if (!folder.Exists) - { + private void CleanupFolder(DirectoryInfo folder) + { + CleanFolderResult result = _ioHelper.CleanFolder(folder, _age); + switch (result.Status) + { + case CleanFolderResultStatus.FailedAsDoesNotExist: _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); - return; - } - - FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); - foreach (FileInfo file in files) - { - if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) + break; + case CleanFolderResultStatus.FailedWithException: + foreach (CleanFolderResult.Error error in result.Errors!) { - try - { - file.IsReadOnly = false; - file.Delete(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); - } + _logger.LogError(error.Exception, "Could not delete temp file {FileName}", + error.ErroringFile.FullName); + } + + break; + } + + folder.Refresh(); // In case it's changed during runtime + if (!folder.Exists) + { + _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); + return; + } + + FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); + foreach (FileInfo file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) + { + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); } } } diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs index ab71edf650..cc034e5768 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs @@ -1,116 +1,131 @@ -using System; -using System.Collections.Generic; using System.Xml.XPath; using Examine.Search; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Query methods used for accessing strongly typed content in templates. +/// +public interface IPublishedContentQuery { + IPublishedContent? Content(int id); + + IPublishedContent? Content(Guid id); + + IPublishedContent? Content(Udi id); + + IPublishedContent? Content(object id); + + IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars); + + IEnumerable Content(IEnumerable ids); + + IEnumerable Content(IEnumerable ids); + + IEnumerable Content(IEnumerable ids); + + IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars); + + IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); + + IEnumerable ContentAtRoot(); + + IPublishedContent? Media(int id); + + IPublishedContent? Media(Guid id); + + IPublishedContent? Media(Udi id); + + IPublishedContent? Media(object id); + + IEnumerable Media(IEnumerable ids); + + IEnumerable Media(IEnumerable ids); + + IEnumerable Media(IEnumerable ids); + + IEnumerable MediaAtRoot(); + /// - /// Query methods used for accessing strongly typed content in templates. + /// Searches content. /// - public interface IPublishedContentQuery - { - IPublishedContent? Content(int id); + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// + /// The name of the index to search (defaults to + /// ). + /// + /// + /// This parameter is no longer used, because the results are loaded from the published snapshot + /// using the single item ID field. + /// + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all + /// invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search( + string term, + int skip, + int take, + out long totalRecords, + string culture = "*", + string indexName = Constants.UmbracoIndexes.ExternalIndexName, + ISet? loadedFields = null); - IPublishedContent? Content(Guid id); + /// + /// Searches content. + /// + /// The term to search. + /// The culture (defaults to a culture insensitive search). + /// + /// The name of the index to search (defaults to + /// ). + /// + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all + /// invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); - IPublishedContent? Content(Udi id); + /// + /// Executes the query and converts the results to . + /// + /// The query. + /// + /// The search results. + /// + IEnumerable Search(IQueryExecutor query); - IPublishedContent? Content(object id); - - IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars); - - IEnumerable Content(IEnumerable ids); - - IEnumerable Content(IEnumerable ids); - - IEnumerable Content(IEnumerable ids); - - IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars); - - IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); - - IEnumerable ContentAtRoot(); - - IPublishedContent? Media(int id); - - IPublishedContent? Media(Guid id); - - IPublishedContent? Media(Udi id); - - IPublishedContent? Media(object id); - - IEnumerable Media(IEnumerable ids); - - IEnumerable Media(IEnumerable ids); - - IEnumerable Media(IEnumerable ids); - - IEnumerable MediaAtRoot(); - - /// - /// Searches content. - /// - /// The term to search. - /// The amount of results to skip. - /// The amount of results to take/return. - /// The total amount of records. - /// The culture (defaults to a culture insensitive search). - /// The name of the index to search (defaults to ). - /// This parameter is no longer used, because the results are loaded from the published snapshot using the single item ID field. - /// - /// The search results. - /// - /// - /// - /// When the is not specified or is *, all cultures are searched. - /// To search for only invariant documents and fields use null. - /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet? loadedFields = null); - - /// - /// Searches content. - /// - /// The term to search. - /// The culture (defaults to a culture insensitive search). - /// The name of the index to search (defaults to ). - /// - /// The search results. - /// - /// - /// - /// When the is not specified or is *, all cultures are searched. - /// To search for only invariant documents and fields use null. - /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// - IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); - - /// - /// Executes the query and converts the results to . - /// - /// The query. - /// - /// The search results. - /// - IEnumerable Search(IQueryExecutor query); - - /// - /// Executes the query and converts the results to . - /// - /// The query. - /// The amount of results to skip. - /// The amount of results to take/return. - /// The total amount of records. - /// - /// The search results. - /// - IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); - } + /// + /// Executes the query and converts the results to . + /// + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// + /// The search results. + /// + IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } diff --git a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs index 01aea4c48f..9e96466377 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs @@ -1,22 +1,23 @@ using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Not intended for use in background threads where you should make use of +/// +/// and instead resolve IPublishedContentQuery from a +/// +/// e.g. using +/// +/// +/// // Background thread example +/// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext(); +/// using IServiceScope serviceScope = _serviceProvider.CreateScope(); +/// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>(); +/// +/// +/// +public interface IPublishedContentQueryAccessor { - /// - /// Not intended for use in background threads where you should make use of - /// and instead resolve IPublishedContentQuery from a - /// e.g. using - /// - /// - /// // Background thread example - /// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext(); - /// using IServiceScope serviceScope = _serviceProvider.CreateScope(); - /// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>(); - /// - /// - /// - public interface IPublishedContentQueryAccessor - { - bool TryGetValue([MaybeNullWhen(false)]out IPublishedContentQuery publishedContentQuery); - } + bool TryGetValue([MaybeNullWhen(false)] out IPublishedContentQuery publishedContentQuery); } diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 0324595133..ccb3e4a0da 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -1,11 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Security.AccessControl; +using System.Security.Principal; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -14,252 +11,258 @@ using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.IO; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +public class FilePermissionHelper : IFilePermissionHelper { - /// - public class FilePermissionHelper : IFilePermissionHelper + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; + + private readonly string[] _packagesPermissionsDirs; + + // ensure that these directories exist and Umbraco can write to them + private readonly string[] _permissionDirs; + + // ensure Umbraco can write to these files (the directories must exist) + private readonly string[] _permissionFiles = Array.Empty(); + private readonly string _basePath; + + /// + /// Initializes a new instance of the class. + /// + public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, + IHostingEnvironment hostingEnvironment) { - // ensure that these directories exist and Umbraco can write to them - private readonly string[] _permissionDirs; - private readonly string[] _packagesPermissionsDirs; - - // ensure Umbraco can write to these files (the directories must exist) - private readonly string[] _permissionFiles = Array.Empty(); - private readonly GlobalSettings _globalSettings; - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private string _basePath; - - /// - /// Initializes a new instance of the class. - /// - public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) + _globalSettings = globalSettings.Value; + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + _basePath = hostingEnvironment.MapPathContentRoot("/"); + _permissionDirs = new[] { - _globalSettings = globalSettings.Value; - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - _basePath = hostingEnvironment.MapPathContentRoot("/"); - _permissionDirs = new[] - { - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) - }; - _packagesPermissionsDirs = new[] - { - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages) - }; - } - - /// - public bool RunFilePermissionTestSuite(out Dictionary> report) + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview), + }; + _packagesPermissionsDirs = new[] { - report = new Dictionary>(); + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), + }; + } - EnsureDirectories(_permissionDirs, out IEnumerable errors); - report[FilePermissionTest.FolderCreation] = errors.ToList(); + /// + public bool RunFilePermissionTestSuite(out Dictionary> report) + { + report = new Dictionary>(); - EnsureDirectories(_packagesPermissionsDirs, out errors); - report[FilePermissionTest.FileWritingForPackages] = errors.ToList(); + EnsureDirectories(_permissionDirs, out IEnumerable errors); + report[FilePermissionTest.FolderCreation] = errors.ToList(); - EnsureFiles(_permissionFiles, out errors); - report[FilePermissionTest.FileWriting] = errors.ToList(); + EnsureDirectories(_packagesPermissionsDirs, out errors); + report[FilePermissionTest.FileWritingForPackages] = errors.ToList(); - EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors); - report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); + EnsureFiles(_permissionFiles, out errors); + report[FilePermissionTest.FileWriting] = errors.ToList(); - return report.Sum(x => x.Value.Count()) == 0; - } + EnsureCanCreateSubDirectory( + _hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), + out errors); + report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); - private bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) + return report.Sum(x => x.Value.Count()) == 0; + } + + private bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) + { + List? temp = null; + var success = true; + foreach (var dir in dirs) { - List? temp = null; - var success = true; - foreach (var dir in dirs) + // we don't want to create/ship unnecessary directories, so + // here we just ensure we can access the directory, not create it + var tryAccess = TryAccessDirectory(dir, !writeCausesRestart); + if (tryAccess) { - // we don't want to create/ship unnecessary directories, so - // here we just ensure we can access the directory, not create it - var tryAccess = TryAccessDirectory(dir, !writeCausesRestart); - if (tryAccess) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(dir.TrimStart(_basePath)); - success = false; + continue; } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; - } - - private bool EnsureFiles(string[] files, out IEnumerable errors) - { - List? temp = null; - var success = true; - foreach (var file in files) + if (temp == null) { - var canWrite = TryWriteFile(file); - if (canWrite) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(file.TrimStart(_basePath)); - success = false; + temp = new List(); } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; + temp.Add(dir.TrimStart(_basePath)); + success = false; } - private bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) - => EnsureCanCreateSubDirectories(new[] { dir }, out errors); + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } - private bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) + private bool EnsureFiles(string[] files, out IEnumerable errors) + { + List? temp = null; + var success = true; + foreach (var file in files) { - List? temp = null; - var success = true; - foreach (var dir in dirs) + var canWrite = TryWriteFile(file); + if (canWrite) { - var canCreate = TryCreateSubDirectory(dir); - if (canCreate) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(dir); - success = false; + continue; } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; + if (temp == null) + { + temp = new List(); + } + + temp.Add(file.TrimStart(_basePath)); + success = false; } - // tries to create a sub-directory - // if successful, the sub-directory is deleted - // creates the directory if needed - does not delete it - private bool TryCreateSubDirectory(string dir) + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } + + private bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) + => EnsureCanCreateSubDirectories(new[] { dir }, out errors); + + private bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) + { + List? temp = null; + var success = true; + foreach (var dir in dirs) { - try + var canCreate = TryCreateSubDirectory(dir); + if (canCreate) + { + continue; + } + + if (temp == null) + { + temp = new List(); + } + + temp.Add(dir); + success = false; + } + + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } + + // tries to create a sub-directory + // if successful, the sub-directory is deleted + // creates the directory if needed - does not delete it + private bool TryCreateSubDirectory(string dir) + { + try + { + var path = Path.Combine(dir, _ioHelper.CreateRandomFileName()); + Directory.CreateDirectory(path); + Directory.Delete(path); + return true; + } + catch + { + return false; + } + } + + // tries to create a file + // if successful, the file is deleted + // + // or + // + // use the ACL APIs to avoid creating files + // + // if the directory does not exist, do nothing & success + private bool TryAccessDirectory(string dirPath, bool canWrite) + { + try + { + if (Directory.Exists(dirPath) == false) { - var path = Path.Combine(dir, _ioHelper.CreateRandomFileName()); - Directory.CreateDirectory(path); - Directory.Delete(path); return true; } - catch + + if (canWrite) { - return false; - } - } - - // tries to create a file - // if successful, the file is deleted - // - // or - // - // use the ACL APIs to avoid creating files - // - // if the directory does not exist, do nothing & success - private bool TryAccessDirectory(string dirPath, bool canWrite) - { - try - { - if (Directory.Exists(dirPath) == false) - { - return true; - } - - if (canWrite) - { - var filePath = dirPath + "/" + _ioHelper.CreateRandomFileName() + ".tmp"; - File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); - File.Delete(filePath); - return true; - } - - return HasWritePermission(dirPath); - } - catch - { - return false; - } - } - - private bool HasWritePermission(string path) - { - var writeAllow = false; - var writeDeny = false; - var accessControlList = new DirectorySecurity(path, AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); - - AuthorizationRuleCollection accessRules; - try - { - accessRules = accessControlList.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier)); - } - catch (Exception) - { - // This is not 100% accurate because it could turn out that the current user doesn't - // have access to read the current permissions but does have write access. - // I think this is an edge case however - return false; - } - - foreach (FileSystemAccessRule rule in accessRules) - { - if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) - { - continue; - } - - if (rule.AccessControlType == AccessControlType.Allow) - { - writeAllow = true; - } - else if (rule.AccessControlType == AccessControlType.Deny) - { - writeDeny = true; - } - } - - return writeAllow && writeDeny == false; - } - - // tries to write into a file - // fails if the directory does not exist - private bool TryWriteFile(string file) - { - try - { - var path = file; - File.AppendText(path).Close(); + var filePath = dirPath + "/" + _ioHelper.CreateRandomFileName() + ".tmp"; + File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); + File.Delete(filePath); return true; } - catch + + return HasWritePermission(dirPath); + } + catch + { + return false; + } + } + + private bool HasWritePermission(string path) + { + var writeAllow = false; + var writeDeny = false; + var accessControlList = new DirectorySecurity( + path, + AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); + + AuthorizationRuleCollection accessRules; + try + { + accessRules = accessControlList.GetAccessRules(true, true, typeof(SecurityIdentifier)); + } + catch (Exception) + { + // This is not 100% accurate because it could turn out that the current user doesn't + // have access to read the current permissions but does have write access. + // I think this is an edge case however + return false; + } + + foreach (FileSystemAccessRule rule in accessRules) + { + if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) { - return false; + continue; } + + if (rule.AccessControlType == AccessControlType.Allow) + { + writeAllow = true; + } + else if (rule.AccessControlType == AccessControlType.Deny) + { + writeDeny = true; + } + } + + return writeAllow && writeDeny == false; + } + + // tries to write into a file + // fails if the directory does not exist + private bool TryWriteFile(string file) + { + try + { + var path = file; + File.AppendText(path).Close(); + return true; + } + catch + { + return false; } } } diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 671dc85c4f..85839e8757 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -11,6 +10,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -26,16 +26,19 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly ICookieManager _cookieManager; private readonly IUserAgentProvider _userAgentProvider; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + private readonly IFireAndForgetRunner _fireAndForgetRunner; private InstallationType? _installationType; - public InstallHelper(DatabaseBuilder databaseBuilder, + public InstallHelper( + DatabaseBuilder databaseBuilder, ILogger logger, IUmbracoVersion umbracoVersion, IOptionsMonitor connectionStrings, IInstallationService installationService, ICookieManager cookieManager, IUserAgentProvider userAgentProvider, - IUmbracoDatabaseFactory umbracoDatabaseFactory) + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IFireAndForgetRunner fireAndForgetRunner) { _logger = logger; _umbracoVersion = umbracoVersion; @@ -45,11 +48,36 @@ namespace Umbraco.Cms.Infrastructure.Install _cookieManager = cookieManager; _userAgentProvider = userAgentProvider; _umbracoDatabaseFactory = umbracoDatabaseFactory; + _fireAndForgetRunner = fireAndForgetRunner; // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. GetInstallationType(); } + [Obsolete("Please use constructor that takes an IFireAndForgetRunner instead, scheduled for removal in Umbraco 12")] + public InstallHelper( + DatabaseBuilder databaseBuilder, + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptionsMonitor connectionStrings, + IInstallationService installationService, + ICookieManager cookieManager, + IUserAgentProvider userAgentProvider, + IUmbracoDatabaseFactory umbracoDatabaseFactory) + : this( + databaseBuilder, + logger, + umbracoVersion, + connectionStrings, + installationService, + cookieManager, + userAgentProvider, + umbracoDatabaseFactory, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + public InstallationType GetInstallationType() => _installationType ??= IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade; public async Task SetInstallStatusAsync(bool isCompleted, string errorMsg) @@ -60,7 +88,7 @@ namespace Umbraco.Cms.Infrastructure.Install // Check for current install ID var installCookie = _cookieManager.GetCookieValue(Constants.Web.InstallerCookieName); - if (!Guid.TryParse(installCookie, out var installId)) + if (!Guid.TryParse(installCookie, out Guid installId)) { installId = Guid.NewGuid(); @@ -75,13 +103,20 @@ namespace Umbraco.Cms.Infrastructure.Install dbProvider = _umbracoDatabaseFactory.SqlContext.SqlSyntax.DbProvider; } - var installLog = new InstallLog(installId: installId, isUpgrade: IsBrandNewInstall == false, - installCompleted: isCompleted, timestamp: DateTime.Now, versionMajor: _umbracoVersion.Version.Major, - versionMinor: _umbracoVersion.Version.Minor, versionPatch: _umbracoVersion.Version.Build, - versionComment: _umbracoVersion.Comment, error: errorMsg, userAgent: userAgent, + var installLog = new InstallLog( + installId: installId, + isUpgrade: IsBrandNewInstall == false, + installCompleted: isCompleted, + timestamp: DateTime.Now, + versionMajor: _umbracoVersion.Version.Major, + versionMinor: _umbracoVersion.Version.Minor, + versionPatch: _umbracoVersion.Version.Build, + versionComment: _umbracoVersion.Comment, + error: errorMsg, + userAgent: userAgent, dbProvider: dbProvider); - await _installationService.LogInstall(installLog); + _fireAndForgetRunner.RunFireAndForget(() => _installationService.LogInstall(installLog)); } catch (Exception ex) { @@ -96,7 +131,7 @@ namespace Umbraco.Cms.Infrastructure.Install /// true if this is a brand new install; otherwise, false. /// private bool IsBrandNewInstall => - _connectionStrings.Get(Constants.System.UmbracoConnectionName).IsConnectionStringConfigured() == false || + _connectionStrings.CurrentValue.IsConnectionStringConfigured() == false || _databaseBuilder.IsDatabaseConfigured == false || _databaseBuilder.CanConnectToDatabase == false || _databaseBuilder.IsUmbracoInstalled() == false; diff --git a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs index 2a9c303349..7b711f8750 100644 --- a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs +++ b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs @@ -1,61 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Install.InstallSteps; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Install.InstallSteps; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +public sealed class InstallStepCollection { - public sealed class InstallStepCollection + private readonly InstallHelper _installHelper; + private readonly IEnumerable _orderedInstallerSteps; + + public InstallStepCollection(InstallHelper installHelper, IEnumerable installerSteps) { - private readonly InstallHelper _installHelper; - private readonly IEnumerable _orderedInstallerSteps; + _installHelper = installHelper; - public InstallStepCollection(InstallHelper installHelper, IEnumerable installerSteps) + // TODO: this is ugly but I have a branch where it's nicely refactored - for now we just want to manage ordering + InstallSetupStep[] a = installerSteps.ToArray(); + _orderedInstallerSteps = new InstallSetupStep[] { - _installHelper = installHelper; + a.OfType().First(), a.OfType().First(), + a.OfType().First(), a.OfType().First(), + a.OfType().First(), a.OfType().First(), + a.OfType().First(), - // TODO: this is ugly but I have a branch where it's nicely refactored - for now we just want to manage ordering - var a = installerSteps.ToArray(); - _orderedInstallerSteps = new InstallSetupStep[] - { - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - - // TODO: Add these back once we have a compatible Starter kit - // a.OfType().First(), - // a.OfType().First(), - // a.OfType().First(), - - a.OfType().First(), - }; - } - - - /// - /// Get the installer steps - /// - /// - /// - /// The step order returned here is how they will appear on the front-end if they have views assigned - /// - public IEnumerable GetAllSteps() - { - return _orderedInstallerSteps; - } - - /// - /// Returns the steps that are used only for the current installation type - /// - /// - public IEnumerable GetStepsForCurrentInstallType() - { - return GetAllSteps().Where(x => x.InstallTypeTarget.HasFlag(_installHelper.GetInstallationType())); - } + // TODO: Add these back once we have a compatible Starter kit + // a.OfType().First(), + // a.OfType().First(), + // a.OfType().First(), + a.OfType().First(), + }; } + + /// + /// Get the installer steps + /// + /// + /// + /// The step order returned here is how they will appear on the front-end if they have views assigned + /// + public IEnumerable GetAllSteps() => _orderedInstallerSteps; + + /// + /// Returns the steps that are used only for the current installation type + /// + /// + public IEnumerable GetStepsForCurrentInstallType() => GetAllSteps() + .Where(x => x.InstallTypeTarget.HasFlag(_installHelper.GetInstallationType())); } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs index 0666a3eee5..d212909a9f 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs @@ -1,31 +1,26 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep( + InstallationType.NewInstall | InstallationType.Upgrade, + "UmbracoVersion", + 50, + "Installation is complete! Get ready to be redirected to your new CMS.", + PerformsAppRestart = true)] +public class CompleteInstallStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.", - PerformsAppRestart = true)] - public class CompleteInstallStep : InstallSetupStep + private readonly InstallHelper _installHelper; + + public CompleteInstallStep(InstallHelper installHelper) => _installHelper = installHelper; + + public override async Task ExecuteAsync(object model) { - private readonly InstallHelper _installHelper; + // reports the ended install + await _installHelper.SetInstallStatusAsync(true, string.Empty); - public CompleteInstallStep(InstallHelper installHelper) - { - _installHelper = installHelper; - } - - public override async Task ExecuteAsync(object model) - { - //reports the ended install - await _installHelper.SetInstallStatusAsync(true, ""); - - return null; - } - - public override bool RequiresExecution(object model) - { - return true; - } + return null; } + + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index f01b65aaa7..87be3c6e8f 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -7,70 +7,65 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)] +public class DatabaseConfigureStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)] - public class DatabaseConfigureStep : InstallSetupStep + private readonly IOptionsMonitor _connectionStrings; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IEnumerable _databaseProviderMetadata; + private readonly ILogger _logger; + + public DatabaseConfigureStep( + DatabaseBuilder databaseBuilder, + IOptionsMonitor connectionStrings, + ILogger logger, + IEnumerable databaseProviderMetadata) { - private readonly DatabaseBuilder _databaseBuilder; - private readonly ILogger _logger; - private readonly IEnumerable _databaseProviderMetadata; - private readonly IOptionsMonitor _connectionStrings; + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _logger = logger; + _databaseProviderMetadata = databaseProviderMetadata; + } - public DatabaseConfigureStep( - DatabaseBuilder databaseBuilder, - IOptionsMonitor connectionStrings, - ILogger logger, - IEnumerable databaseProviderMetadata) + public override object ViewModel => new {databases = _databaseProviderMetadata.GetAvailable().ToList()}; + + public override string View => ShouldDisplayView() ? base.View : string.Empty; + + public override Task ExecuteAsync(DatabaseModel databaseSettings) + { + if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, false)) { - _databaseBuilder = databaseBuilder; - _connectionStrings = connectionStrings; - _logger = logger; - _databaseProviderMetadata = databaseProviderMetadata; + throw new InstallException("Could not connect to the database"); } - public override Task ExecuteAsync(DatabaseModel databaseSettings) + return Task.FromResult(null); + } + + public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); + + private bool ShouldDisplayView() + { + // If the connection string is already present in config we don't need to show the settings page and we jump to installing/upgrading. + if (_connectionStrings.CurrentValue.IsConnectionStringConfigured()) { - if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, isTrialRun: false)) + try { - throw new InstallException("Could not connect to the database"); + // Since a connection string was present we verify the db can connect and query + _databaseBuilder.ValidateSchema(); + + return false; } - - return Task.FromResult(null); - } - - public override object ViewModel => new - { - databases = _databaseProviderMetadata.GetAvailable().ToList() - }; - - public override string View => ShouldDisplayView() ? base.View : string.Empty; - - public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); - - private bool ShouldDisplayView() - { - // If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading. - var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); - if (databaseSettings.IsConnectionStringConfigured()) + catch (Exception ex) { - try - { - // Since a connection string was present we verify the db can connect and query - _databaseBuilder.ValidateSchema(); + // Something went wrong, could not connect so probably need to reconfigure + _logger.LogError(ex, "An error occurred, reconfiguring..."); - return false; - } - catch (Exception ex) - { - // Something went wrong, could not connect so probably need to reconfigure - _logger.LogError(ex, "An error occurred, reconfiguring..."); - - return true; - } + return true; } - - return true; } + + return true; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 21da2f797a..42712f20bd 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -1,55 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] +public class DatabaseInstallStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] - public class DatabaseInstallStep : InstallSetupStep + private readonly DatabaseBuilder _databaseBuilder; + private readonly IRuntimeState _runtime; + + public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) { - private readonly IRuntimeState _runtime; - private readonly DatabaseBuilder _databaseBuilder; - - public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) - { - _runtime = runtime; - _databaseBuilder = databaseBuilder; - } - - public override Task ExecuteAsync(object model) - { - if (_runtime.Level == RuntimeLevel.Run) - throw new Exception("Umbraco is already configured!"); - - if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) - { - _databaseBuilder.CreateDatabase(); - } - - var result = _databaseBuilder.CreateSchemaAndData(); - - if (result?.Success == false) - { - throw new InstallException("The database failed to install. ERROR: " + result.Message); - } - - if (result?.RequiresUpgrade == false) - { - return Task.FromResult(null); - } - - // Upgrade is required, so set the flag for the next step - return Task.FromResult(new InstallSetupResult(new Dictionary - { - { "upgrade", true} - }))!; - } - - public override bool RequiresExecution(object model) => true; + _runtime = runtime; + _databaseBuilder = databaseBuilder; } + + public override Task ExecuteAsync(object model) + { + if (_runtime.Level == RuntimeLevel.Run) + { + throw new Exception("Umbraco is already configured!"); + } + + if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _databaseBuilder.CreateDatabase(); + } + + DatabaseBuilder.Result? result = _databaseBuilder.CreateSchemaAndData(); + + if (result?.Success == false) + { + throw new InstallException("The database failed to install. ERROR: " + result.Message); + } + + if (result?.RequiresUpgrade == false) + { + return Task.FromResult(null); + } + + // Upgrade is required, so set the flag for the next step + return Task.FromResult(new InstallSetupResult(new Dictionary { { "upgrade", true } }))!; + } + + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index 25494ff925..fa35ee5b07 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -16,8 +13,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { - [InstallSetupStep(InstallationType.Upgrade | InstallationType.NewInstall, - "DatabaseUpgrade", 12, "")] + [InstallSetupStep(InstallationType.Upgrade | InstallationType.NewInstall, "DatabaseUpgrade", 12, "")] public class DatabaseUpgradeStep : InstallSetupStep { private readonly DatabaseBuilder _databaseBuilder; @@ -42,8 +38,8 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps public override Task ExecuteAsync(object model) { - var installSteps = InstallStatusTracker.GetStatus().ToArray(); - var previousStep = installSteps.Single(x => x.Name == "DatabaseInstall"); + InstallTrackingItem[] installSteps = InstallStatusTracker.GetStatus().ToArray(); + InstallTrackingItem previousStep = installSteps.Single(x => x.Name == "DatabaseInstall"); var upgrade = previousStep.AdditionalData.ContainsKey("upgrade"); if (upgrade) @@ -53,7 +49,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps var plan = new UmbracoPlan(_umbracoVersion); plan.AddPostMigration(); // needed when running installer (back-office) - var result = _databaseBuilder.UpgradeSchemaAndData(plan); + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); if (result?.Success == false) { @@ -66,28 +62,28 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps public override bool RequiresExecution(object model) { - //if it's properly configured (i.e. the versions match) then no upgrade necessary + // If it's properly configured (i.e. the versions match) then no upgrade necessary if (_runtime.Level == RuntimeLevel.Run) + { return false; + } - var installSteps = InstallStatusTracker.GetStatus().ToArray(); - //this step relies on the previous one completed - because it has stored some information we need + // This step relies on the previous one completed - because it has stored some information we need + InstallTrackingItem[] installSteps = InstallStatusTracker.GetStatus().ToArray(); if (installSteps.Any(x => x.Name == "DatabaseInstall" && x.AdditionalData.ContainsKey("upgrade")) == false) { return false; } - var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); - - if (databaseSettings.IsConnectionStringConfigured()) + if (_connectionStrings.CurrentValue.IsConnectionStringConfigured()) { - // a connection string was present, determine whether this is an install/upgrade - // return true (upgrade) if there is an installed version, else false (install) - var result = _databaseBuilder.ValidateSchema(); + // A connection string was present, determine whether this is an install/upgrade + // Return true (upgrade) if there is an installed version, else false (install) + DatabaseSchemaResult? result = _databaseBuilder.ValidateSchema(); return result?.DetermineHasInstalledVersion() ?? false; } - //no connection string configured, probably a fresh install + // No connection string configured, probably a fresh install return false; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 4f5dbc3cc2..2ebc756dc2 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,16 +1,23 @@ using System.Collections.Specialized; +using System.Data.Common; using System.Text; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; +using HttpResponseMessage = System.Net.Http.HttpResponseMessage; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { @@ -35,6 +42,8 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps private readonly IBackOfficeUserManager _userManager; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IEnumerable _databaseProviderMetadata; + private readonly ILocalizedTextService _localizedTextService; + private readonly IMetricsConsentService _metricsConsentService; public NewInstallStep( IUserService userService, @@ -46,7 +55,9 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps ICookieManager cookieManager, IBackOfficeUserManager userManager, IDbProviderFactoryCreator dbProviderFactoryCreator, - IEnumerable databaseProviderMetadata) + IEnumerable databaseProviderMetadata, + ILocalizedTextService localizedTextService, + IMetricsConsentService metricsConsentService) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); @@ -58,22 +69,53 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); _databaseProviderMetadata = databaseProviderMetadata; + _localizedTextService = localizedTextService; + _metricsConsentService = metricsConsentService; + } + + // Scheduled for removal in V12 + [Obsolete("Please use constructor that takes an IMetricsConsentService and ILocalizedTextService instead")] + public NewInstallStep( + IUserService userService, + DatabaseBuilder databaseBuilder, + IHttpClientFactory httpClientFactory, + IOptions passwordConfiguration, + IOptions securitySettings, + IOptionsMonitor connectionStrings, + ICookieManager cookieManager, + IBackOfficeUserManager userManager, + IDbProviderFactoryCreator dbProviderFactoryCreator, + IEnumerable databaseProviderMetadata) + : this( + userService, + databaseBuilder, + httpClientFactory, + passwordConfiguration, + securitySettings, + connectionStrings, + cookieManager, + userManager, + dbProviderFactoryCreator, + databaseProviderMetadata, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { } public override async Task ExecuteAsync(UserModel user) { - var admin = _userService.GetUserById(Constants.Security.SuperUserId); + IUser? admin = _userService.GetUserById(Constants.Security.SuperUserId); if (admin == null) { throw new InvalidOperationException("Could not find the super user!"); } admin.Email = user.Email.Trim(); - admin.Name = user.Name!.Trim(); + admin.Name = user.Name.Trim(); admin.Username = user.Email.Trim(); _userService.Save(admin); - var membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); + BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); if (membershipUser == null) { throw new InvalidOperationException( @@ -83,11 +125,17 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps //To change the password here we actually need to reset it since we don't have an old one to use to change var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); if (string.IsNullOrWhiteSpace(resetToken)) + { throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + } - var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); if (!resetResult.Succeeded) + { throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + } + + _metricsConsentService.SetConsentLevel(user.TelemetryLevel); if (user.SubscribeToNewsLetter) { @@ -98,7 +146,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps try { - var response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; + HttpResponseMessage response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; } catch { /* fail in silence */ } } @@ -126,61 +174,62 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps minCharLength = _passwordConfiguration.RequiredLength, minNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars(), quickInstallSettings, - customInstallAvailable = !GetInstallState().HasFlag(InstallState.ConnectionStringConfigured) + customInstallAvailable = !GetInstallState().HasFlag(InstallState.ConnectionStringConfigured), + consentLevels = Enum.GetValues(typeof(TelemetryLevel)).Cast().ToList().Select(level => new + { + level, + description = GetTelemetryLevelDescription(level), + }), }; } } - public override string View + public override string View => ShowView() + // the user UI + ? "user" + // continue install UI + : "continueinstall"; + + private string GetTelemetryLevelDescription(TelemetryLevel telemetryLevel) => telemetryLevel switch { - get - { - return ShowView() - // the user UI - ? "user" - // continue install UI - : "continueinstall"; - } - } + TelemetryLevel.Minimal => _localizedTextService.Localize("analytics", "minimalLevelDescription"), + TelemetryLevel.Basic => _localizedTextService.Localize("analytics", "basicLevelDescription"), + TelemetryLevel.Detailed => _localizedTextService.Localize("analytics", "detailedLevelDescription"), + _ => throw new ArgumentOutOfRangeException(nameof(telemetryLevel), $"Did not expect telemetry level of {telemetryLevel}") + }; private InstallState GetInstallState() { - var installState = InstallState.Unknown; + InstallState installState = InstallState.Unknown; - - // TODO: we need to do a null check here since this could be entirely missing and we end up with a null ref - // exception in the installer. - - var databaseSettings = _connectionStrings.Get(Constants.System.UmbracoConnectionName); - - var hasConnString = databaseSettings != null && _databaseBuilder.IsDatabaseConfigured; - if (hasConnString) + if (_databaseBuilder.IsDatabaseConfigured) { installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; } - var connStringConfigured = databaseSettings?.IsConnectionStringConfigured() ?? false; - if (connStringConfigured) + ConnectionStrings? umbracoConnectionString = _connectionStrings.CurrentValue; + + var isConnectionStringConfigured = umbracoConnectionString.IsConnectionStringConfigured(); + if (isConnectionStringConfigured) { installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; } - - var factory = _dbProviderFactoryCreator.CreateFactory(databaseSettings?.ProviderName); - var canConnect = connStringConfigured && DbConnectionExtensions.IsConnectionAvailable(databaseSettings?.ConnectionString, factory); - if (canConnect) + DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(umbracoConnectionString.ProviderName); + var isConnectionAvailable = isConnectionStringConfigured && DbConnectionExtensions.IsConnectionAvailable(umbracoConnectionString.ConnectionString, factory); + if (isConnectionAvailable) { installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; } - var umbracoInstalled = canConnect ? _databaseBuilder.IsUmbracoInstalled() : false; - if (umbracoInstalled) + var isUmbracoInstalled = isConnectionAvailable && _databaseBuilder.IsUmbracoInstalled(); + if (isUmbracoInstalled) { installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; } - var hasNonDefaultUser = umbracoInstalled ? _databaseBuilder.HasSomeNonDefaultUser() : false; - if (hasNonDefaultUser) + var hasSomeNonDefaultUser = isUmbracoInstalled && _databaseBuilder.HasSomeNonDefaultUser(); + if (hasSomeNonDefaultUser) { installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; } @@ -190,16 +239,14 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps private bool ShowView() { - var installState = GetInstallState(); + InstallState installState = GetInstallState(); - return installState.HasFlag(InstallState.Unknown) - || !installState.HasFlag(InstallState.UmbracoInstalled); + return installState.HasFlag(InstallState.Unknown) || !installState.HasFlag(InstallState.UmbracoInstalled); } public override bool RequiresExecution(UserModel model) { - var installState = GetInstallState(); - + InstallState installState = GetInstallState(); if (installState.HasFlag(InstallState.Unknown)) { // In this one case when it's a brand new install and nothing has been configured, make sure the @@ -207,8 +254,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps _cookieManager.ExpireCookie(_securitySettings.AuthCookieName); } - return installState.HasFlag(InstallState.Unknown) - || !installState.HasFlag(InstallState.HasNonDefaultUser); + return installState.HasFlag(InstallState.Unknown) || !installState.HasFlag(InstallState.HasNonDefaultUser); } [Flags] diff --git a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs index 100e3ee26f..db054b2dc5 100644 --- a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs +++ b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs @@ -1,108 +1,106 @@ -using System; -using System.Linq; -using System.Collections.Generic; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Packaging; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade; -using Umbraco.Extensions; using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Notifications; -using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +/// Runs the package migration plans +/// +public class PackageMigrationRunner { - /// - /// Runs the package migration plans - /// - public class PackageMigrationRunner + private readonly IEventAggregator _eventAggregator; + private readonly IKeyValueService _keyValueService; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; + private readonly Dictionary _packageMigrationPlans; + private readonly PendingPackageMigrations _pendingPackageMigrations; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + + public PackageMigrationRunner( + IProfilingLogger profilingLogger, + ICoreScopeProvider scopeProvider, + PendingPackageMigrations pendingPackageMigrations, + PackageMigrationPlanCollection packageMigrationPlans, + IMigrationPlanExecutor migrationPlanExecutor, + IKeyValueService keyValueService, + IEventAggregator eventAggregator) { - private readonly IProfilingLogger _profilingLogger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly PendingPackageMigrations _pendingPackageMigrations; - private readonly IMigrationPlanExecutor _migrationPlanExecutor; - private readonly IKeyValueService _keyValueService; - private readonly IEventAggregator _eventAggregator; - private readonly Dictionary _packageMigrationPlans; + _profilingLogger = profilingLogger; + _scopeProvider = scopeProvider; + _pendingPackageMigrations = pendingPackageMigrations; + _migrationPlanExecutor = migrationPlanExecutor; + _keyValueService = keyValueService; + _eventAggregator = eventAggregator; + _packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name); + } - public PackageMigrationRunner( - IProfilingLogger profilingLogger, - ICoreScopeProvider scopeProvider, - PendingPackageMigrations pendingPackageMigrations, - PackageMigrationPlanCollection packageMigrationPlans, - IMigrationPlanExecutor migrationPlanExecutor, - IKeyValueService keyValueService, - IEventAggregator eventAggregator) + /// + /// Runs all migration plans for a package name if any are pending. + /// + /// + /// + public IEnumerable RunPackageMigrationsIfPending(string packageName) + { + IReadOnlyDictionary? keyValues = + _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + IReadOnlyList pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues); + + IEnumerable packagePlans = _packageMigrationPlans.Values + .Where(x => x.PackageName.InvariantEquals(packageName)) + .Where(x => pendingMigrations.Contains(x.Name)) + .Select(x => x.Name); + + return RunPackagePlans(packagePlans); + } + + /// + /// Runs the all specified package migration plans and publishes a + /// if all are successful. + /// + /// + /// + /// If any plan fails it will throw an exception. + public IEnumerable RunPackagePlans(IEnumerable plansToRun) + { + var results = new List(); + + // Create an explicit scope around all package migrations so they are + // all executed in a single transaction. If one package migration fails, + // none of them will be committed. This is intended behavior so we can + // ensure when we publish the success notification that is is done when they all succeed. + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - _profilingLogger = profilingLogger; - _scopeProvider = scopeProvider; - _pendingPackageMigrations = pendingPackageMigrations; - _migrationPlanExecutor = migrationPlanExecutor; - _keyValueService = keyValueService; - _eventAggregator = eventAggregator; - _packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name); - } - - /// - /// Runs all migration plans for a package name if any are pending. - /// - /// - /// - public IEnumerable RunPackageMigrationsIfPending(string packageName) - { - IReadOnlyDictionary? keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); - IReadOnlyList pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues); - - IEnumerable packagePlans = _packageMigrationPlans.Values - .Where(x => x.PackageName.InvariantEquals(packageName)) - .Where(x => pendingMigrations.Contains(x.Name)) - .Select(x => x.Name); - - return RunPackagePlans(packagePlans); - } - - /// - /// Runs the all specified package migration plans and publishes a - /// if all are successful. - /// - /// - /// - /// If any plan fails it will throw an exception. - public IEnumerable RunPackagePlans(IEnumerable plansToRun) - { - var results = new List(); - - // Create an explicit scope around all package migrations so they are - // all executed in a single transaction. If one package migration fails, - // none of them will be committed. This is intended behavior so we can - // ensure when we publish the success notification that is is done when they all succeed. - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + foreach (var migrationName in plansToRun) { - foreach (var migrationName in plansToRun) + if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan)) { - if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan)) - { - throw new InvalidOperationException("Cannot find package migration plan " + migrationName); - } + throw new InvalidOperationException("Cannot find package migration plan " + migrationName); + } - using (_profilingLogger.TraceDuration( - "Starting unattended package migration for " + migrationName, - "Unattended upgrade completed for " + migrationName)) - { - var upgrader = new Upgrader(plan); - // This may throw, if so the transaction will be rolled back - results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); - } + using (_profilingLogger.TraceDuration( + "Starting unattended package migration for " + migrationName, + "Unattended upgrade completed for " + migrationName)) + { + var upgrader = new Upgrader(plan); + + // This may throw, if so the transaction will be rolled back + results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); } } - - var executedPlansNotification = new MigrationPlansExecutedNotification(results); - _eventAggregator.Publish(executedPlansNotification); - - return results; } + + var executedPlansNotification = new MigrationPlansExecutedNotification(results); + _eventAggregator.Publish(executedPlansNotification); + + return results; } } diff --git a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs index bf38c2b664..bf9817ca94 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,129 +9,131 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +public class UnattendedInstaller : INotificationAsyncHandler { - public class UnattendedInstaller : INotificationAsyncHandler + private readonly IUmbracoDatabaseFactory _databaseFactory; + + private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IOptions _unattendedSettings; + + public UnattendedInstaller( + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEventAggregator eventAggregator, + IOptions unattendedSettings, + IUmbracoDatabaseFactory databaseFactory, + IDbProviderFactoryCreator dbProviderFactoryCreator, + ILogger logger, + IRuntimeState runtimeState) { - private readonly IOptions _unattendedSettings; + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? + throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _unattendedSettings = unattendedSettings; + _databaseFactory = databaseFactory; + _dbProviderFactoryCreator = dbProviderFactoryCreator; + _logger = logger; + _runtimeState = runtimeState; + } - private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly IEventAggregator _eventAggregator; - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly ILogger _logger; - private readonly IRuntimeState _runtimeState; - - public UnattendedInstaller( - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEventAggregator eventAggregator, - IOptions unattendedSettings, - IUmbracoDatabaseFactory databaseFactory, - IDbProviderFactoryCreator dbProviderFactoryCreator, - ILogger logger, - IRuntimeState runtimeState) + public Task HandleAsync(RuntimeUnattendedInstallNotification notification, CancellationToken cancellationToken) + { + // unattended install is not enabled + if (_unattendedSettings.Value.InstallUnattended == false) { - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); - _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); - _unattendedSettings = unattendedSettings; - _databaseFactory = databaseFactory; - _dbProviderFactoryCreator = dbProviderFactoryCreator; - _logger = logger; - _runtimeState = runtimeState; - } - - public Task HandleAsync(RuntimeUnattendedInstallNotification notification, CancellationToken cancellationToken) - { - // unattended install is not enabled - if (_unattendedSettings.Value.InstallUnattended == false) - { - return Task.CompletedTask; - } - - // no connection string set - if (_databaseFactory.Configured == false) - { - return Task.CompletedTask; - } - - _runtimeState.DetermineRuntimeLevel(); - if (_runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) - { - _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, _databaseFactory.ConnectionString!); - } - - bool connect; - try - { - for (var i = 0; ;) - { - connect = _databaseFactory.CanConnect; - if (connect || ++i == 5) - { - break; - } - - _logger.LogDebug("Could not immediately connect to database, trying again."); - - Thread.Sleep(1000); - } - } - catch (Exception ex) - { - _logger.LogInformation(ex, "Error during unattended install."); - - var innerException = new UnattendedInstallException("Unattended installation failed.", ex); - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - return Task.CompletedTask; - } - - // could not connect to the database - if (connect == false) - { - return Task.CompletedTask; - } - - IUmbracoDatabase? database = null; - try - { - using (database = _databaseFactory.CreateDatabase()) - { - var hasUmbracoTables = database?.IsUmbracoInstalled() ?? false; - - // database has umbraco tables, assume Umbraco is already installed - if (hasUmbracoTables) - { - return Task.CompletedTask; - } - - // all conditions fulfilled, do the install - _logger.LogInformation("Starting unattended install."); - - database?.BeginTransaction(); - DatabaseSchemaCreator creator = _databaseSchemaCreatorFactory.Create(database); - creator.InitializeDatabaseSchema(); - database?.CompleteTransaction(); - _logger.LogInformation("Unattended install completed."); - - // Emit an event with EventAggregator that unattended install completed - // Then this event can be listened for and create an unattended user - _eventAggregator.Publish(new UnattendedInstallNotification()); - } - } - catch (Exception ex) - { - _logger.LogInformation(ex, "Error during unattended install."); - database?.AbortTransaction(); - - var innerException = new UnattendedInstallException( - "The database configuration failed." - + "\n Please check log file for additional information (can be found in '/Umbraco/Data/Logs/')", - ex); - - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - } - return Task.CompletedTask; } + + // no connection string set + if (_databaseFactory.Configured == false) + { + return Task.CompletedTask; + } + + _runtimeState.DetermineRuntimeLevel(); + if (_runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _dbProviderFactoryCreator.CreateDatabase( + _databaseFactory.ProviderName!, + _databaseFactory.ConnectionString!); + } + + bool connect; + try + { + for (var i = 0; ;) + { + connect = _databaseFactory.CanConnect; + if (connect || ++i == 5) + { + break; + } + + _logger.LogDebug("Could not immediately connect to database, trying again."); + + Thread.Sleep(1000); + } + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Error during unattended install."); + + var innerException = new UnattendedInstallException("Unattended installation failed.", ex); + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + return Task.CompletedTask; + } + + // could not connect to the database + if (connect == false) + { + return Task.CompletedTask; + } + + IUmbracoDatabase? database = null; + try + { + using (database = _databaseFactory.CreateDatabase()) + { + var hasUmbracoTables = database.IsUmbracoInstalled(); + + // database has umbraco tables, assume Umbraco is already installed + if (hasUmbracoTables) + { + return Task.CompletedTask; + } + + // all conditions fulfilled, do the install + _logger.LogInformation("Starting unattended install."); + + database.BeginTransaction(); + DatabaseSchemaCreator creator = _databaseSchemaCreatorFactory.Create(database); + creator.InitializeDatabaseSchema(); + database.CompleteTransaction(); + _logger.LogInformation("Unattended install completed."); + + // Emit an event with EventAggregator that unattended install completed + // Then this event can be listened for and create an unattended user + _eventAggregator.Publish(new UnattendedInstallNotification()); + } + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Error during unattended install."); + database?.AbortTransaction(); + + var innerException = new UnattendedInstallException( + "The database configuration failed." + + "\n Please check log file for additional information (can be found in '/Umbraco/Data/Logs/')", + ex); + + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs index 3b891b88c4..fb0b389b47 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; @@ -12,98 +9,106 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Extensions; -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +/// Handles to execute the unattended Umbraco upgrader +/// or the unattended Package migrations runner. +/// +public class UnattendedUpgrader : INotificationAsyncHandler { - /// - /// Handles to execute the unattended Umbraco upgrader - /// or the unattended Package migrations runner. - /// - public class UnattendedUpgrader : INotificationAsyncHandler + private readonly DatabaseBuilder _databaseBuilder; + private readonly PackageMigrationRunner _packageMigrationRunner; + private readonly IProfilingLogger _profilingLogger; + private readonly IRuntimeState _runtimeState; + private readonly IUmbracoVersion _umbracoVersion; + + public UnattendedUpgrader( + IProfilingLogger profilingLogger, + IUmbracoVersion umbracoVersion, + DatabaseBuilder databaseBuilder, + IRuntimeState runtimeState, + PackageMigrationRunner packageMigrationRunner) { - private readonly IProfilingLogger _profilingLogger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly DatabaseBuilder _databaseBuilder; - private readonly IRuntimeState _runtimeState; - private readonly PackageMigrationRunner _packageMigrationRunner; - - public UnattendedUpgrader( - IProfilingLogger profilingLogger, - IUmbracoVersion umbracoVersion, - DatabaseBuilder databaseBuilder, - IRuntimeState runtimeState, - PackageMigrationRunner packageMigrationRunner) - { - _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); - _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); - _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); - _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); - _packageMigrationRunner = packageMigrationRunner; - } - - public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken) - { - if (_runtimeState.RunUnattendedBootLogic()) - { - switch (_runtimeState.Reason) - { - case RuntimeLevelReason.UpgradeMigrations: - { - var plan = new UmbracoPlan(_umbracoVersion); - using (_profilingLogger.TraceDuration( - "Starting unattended upgrade.", - "Unattended upgrade completed.")) - { - DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); - if (result?.Success == false) - { - var innerException = new UnattendedInstallException("An error occurred while running the unattended upgrade.\n" + result.Message); - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - } - - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; - } - } - break; - case RuntimeLevelReason.UpgradePackageMigrations: - { - if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPackageMigrationsStateKey, out var pm) - || pm is not IReadOnlyList pendingMigrations) - { - throw new InvalidOperationException($"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state"); - } - - if (pendingMigrations.Count == 0) - { - throw new InvalidOperationException("No pending migrations found but the runtime level reason is " + Core.RuntimeLevelReason.UpgradePackageMigrations); - } - - try - { - IEnumerable result = _packageMigrationRunner.RunPackagePlans(pendingMigrations); - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete; - } - catch (Exception ex ) - { - SetRuntimeError(ex); - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; - } - } - break; - default: - throw new InvalidOperationException("Invalid reason " + _runtimeState.Reason); - } - } - - return Task.CompletedTask; - } - - private void SetRuntimeError(Exception exception) - => _runtimeState.Configure( - RuntimeLevel.BootFailed, - RuntimeLevelReason.BootFailedOnException, - exception); + _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); + _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); + _packageMigrationRunner = packageMigrationRunner; } + + public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken) + { + if (_runtimeState.RunUnattendedBootLogic()) + { + switch (_runtimeState.Reason) + { + case RuntimeLevelReason.UpgradeMigrations: + { + var plan = new UmbracoPlan(_umbracoVersion); + using (_profilingLogger.TraceDuration( + "Starting unattended upgrade.", + "Unattended upgrade completed.")) + { + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); + if (result?.Success == false) + { + var innerException = new UnattendedInstallException( + "An error occurred while running the unattended upgrade.\n" + result.Message); + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + } + + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; + } + } + + break; + case RuntimeLevelReason.UpgradePackageMigrations: + { + if (!_runtimeState.StartupState.TryGetValue( + RuntimeState.PendingPackageMigrationsStateKey, + out var pm) + || pm is not IReadOnlyList pendingMigrations) + { + throw new InvalidOperationException( + $"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state"); + } + + if (pendingMigrations.Count == 0) + { + throw new InvalidOperationException( + "No pending migrations found but the runtime level reason is " + + RuntimeLevelReason.UpgradePackageMigrations); + } + + try + { + _packageMigrationRunner.RunPackagePlans(pendingMigrations); + notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult + .PackageMigrationComplete; + } + catch (Exception ex) + { + SetRuntimeError(ex); + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; + } + } + + break; + default: + throw new InvalidOperationException("Invalid reason " + _runtimeState.Reason); + } + } + + return Task.CompletedTask; + } + + private void SetRuntimeError(Exception exception) + => _runtimeState.Configure( + RuntimeLevel.BootFailed, + RuntimeLevelReason.BootFailedOnException, + exception); } diff --git a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs index 305844d7d6..e740ff1b26 100644 --- a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs +++ b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs @@ -1,52 +1,53 @@ -using System; -using System.IO; -using System.Linq; -using Serilog; +using Serilog; using Serilog.Events; using Serilog.Parsing; -using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +public class MessageTemplates : IMessageTemplates { - public class MessageTemplates : IMessageTemplates + // Umbraco now uses Message Templates (https://messagetemplates.org/) for logging, which means + // we cannot plainly use string.Format() to format them. There is a work-in-progress C# lib, + // derived from Serilog, which should help (https://github.com/messagetemplates/messagetemplates-csharp) + // but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which + // means we cannot get rid of Serilog entirely. We may want to revisit this at some point. + + // TODO: Do we still need this, is there a non-pre release package shipped? + private static readonly Lazy _minimalLogger = new(() => new LoggerConfiguration().CreateLogger()); + + public string Render(string messageTemplate, params object[] args) { - // Umbraco now uses Message Templates (https://messagetemplates.org/) for logging, which means - // we cannot plainly use string.Format() to format them. There is a work-in-progress C# lib, - // derived from Serilog, which should help (https://github.com/messagetemplates/messagetemplates-csharp) - // but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which - // means we cannot get rid of Serilog entirely. We may want to revisit this at some point. + // resolve a minimal logger instance which is used to bind message templates + ILogger logger = _minimalLogger.Value; - // TODO: Do we still need this, is there a non-pre release package shipped? + var bound = logger.BindMessageTemplate(messageTemplate, args, out MessageTemplate? parsedTemplate, out IEnumerable? boundProperties); - private static readonly Lazy MinimalLogger = new Lazy(() => new LoggerConfiguration().CreateLogger()); - - public string Render(string messageTemplate, params object[] args) + if (!bound) { - // resolve a minimal logger instance which is used to bind message templates - var logger = MinimalLogger.Value; - - var bound = logger.BindMessageTemplate(messageTemplate, args, out var parsedTemplate, out var boundProperties); - - if (!bound) - throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); - - var values = boundProperties.ToDictionary(x => x.Name, x => x.Value); - - // this ends up putting every string parameter between quotes - //return parsedTemplate.Render(values); - - // this does not - var tw = new StringWriter(); - foreach (var t in parsedTemplate.Tokens) - { - if (t is PropertyToken pt && - values.TryGetValue(pt.PropertyName, out var propVal) && - (propVal as ScalarValue)?.Value is string s) - tw.Write(s); - else - t.Render(values, tw); - } - return tw.ToString(); + throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); } + + var values = boundProperties!.ToDictionary(x => x.Name, x => x.Value); + + // this ends up putting every string parameter between quotes + // return parsedTemplate.Render(values); + + // this does not + var tw = new StringWriter(); + foreach (MessageTemplateToken? t in parsedTemplate!.Tokens) + { + if (t is PropertyToken pt && + values.TryGetValue(pt.PropertyName, out LogEventPropertyValue? propVal) && + (propVal as ScalarValue)?.Value is string s) + { + tw.Write(s); + } + else + { + t.Render(values, tw); + } + } + + return tw.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs index 3d9182cb96..5abadf4489 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs @@ -1,45 +1,46 @@ -using System; using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Cache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with a HttpRequestId GUID. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpRequestIdEnricher : ILogEventEnricher { /// - /// Enrich log events with a HttpRequestId GUID. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpRequestIdEnricher : ILogEventEnricher + public const string HttpRequestIdPropertyName = "HttpRequestId"; + + private readonly IRequestCache _requestCache; + + public HttpRequestIdEnricher(IRequestCache requestCache) => + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + + /// + /// Enrich the log event with an id assigned to the currently-executing HTTP request, if any. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - private readonly IRequestCache _requestCache; - - public HttpRequestIdEnricher(IRequestCache requestCache) + if (logEvent == null) { - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + throw new ArgumentNullException(nameof(logEvent)); } - /// - /// The property name added to enriched log events. - /// - public const string HttpRequestIdPropertyName = "HttpRequestId"; - - /// - /// Enrich the log event with an id assigned to the currently-executing HTTP request, if any. - /// - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + if (!LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? requestId, _requestCache)) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - Guid? requestId; - if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache)) - return; - - var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId)); - logEvent.AddPropertyIfAbsent(requestIdProperty); + return; } + + var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId)); + logEvent.AddPropertyIfAbsent(requestIdProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs index ee041f7abb..6f22d3e3b1 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs @@ -1,48 +1,48 @@ -using System; -using System.Threading; using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with a HttpRequestNumber unique within the current +/// logging session. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpRequestNumberEnricher : ILogEventEnricher { /// - /// Enrich log events with a HttpRequestNumber unique within the current - /// logging session. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpRequestNumberEnricher : ILogEventEnricher + private const string HttpRequestNumberPropertyName = "HttpRequestNumber"; + private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber"; + + private static int _lastRequestNumber; + private readonly IRequestCache _requestCache; + + public HttpRequestNumberEnricher(IRequestCache requestCache) => + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + + /// + /// Enrich the log event with the number assigned to the currently-executing HTTP request, if any. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - private readonly IRequestCache _requestCache; - private static int _lastRequestNumber; - private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber"; - - /// - /// The property name added to enriched log events. - /// - private const string _httpRequestNumberPropertyName = "HttpRequestNumber"; - - - public HttpRequestNumberEnricher(IRequestCache requestCache) + if (logEvent == null) { - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + throw new ArgumentNullException(nameof(logEvent)); } - /// - /// Enrich the log event with the number assigned to the currently-executing HTTP request, if any. - /// - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + var requestNumber = _requestCache.Get( + _requestNumberItemName, + () => Interlocked.Increment(ref _lastRequestNumber)); - var requestNumber = _requestCache.Get(_requestNumberItemName, - () => Interlocked.Increment(ref _lastRequestNumber)); - - var requestNumberProperty = new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber)); - logEvent.AddPropertyIfAbsent(requestNumberProperty); - } + var requestNumberProperty = + new LogEventProperty(HttpRequestNumberPropertyName, new ScalarValue(requestNumber)); + logEvent.AddPropertyIfAbsent(requestNumberProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs index e2b4f59065..49c9506237 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs @@ -1,43 +1,45 @@ -using System; using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Net; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with the HttpSessionId property. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpSessionIdEnricher : ILogEventEnricher { /// - /// Enrich log events with the HttpSessionId property. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpSessionIdEnricher : ILogEventEnricher + public const string HttpSessionIdPropertyName = "HttpSessionId"; + + private readonly ISessionIdResolver _sessionIdResolver; + + public HttpSessionIdEnricher(ISessionIdResolver sessionIdResolver) => _sessionIdResolver = sessionIdResolver; + + /// + /// Enrich the log event with the current ASP.NET session id, if sessions are enabled. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - private readonly ISessionIdResolver _sessionIdResolver; - - public HttpSessionIdEnricher(ISessionIdResolver sessionIdResolver) + if (logEvent == null) { - _sessionIdResolver = sessionIdResolver; + throw new ArgumentNullException(nameof(logEvent)); } - /// - /// The property name added to enriched log events. - /// - public const string HttpSessionIdPropertyName = "HttpSessionId"; - - /// - /// Enrich the log event with the current ASP.NET session id, if sessions are enabled. - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + var sessionId = _sessionIdResolver.SessionId; + if (sessionId is null) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - var sessionId = _sessionIdResolver.SessionId; - if (sessionId is null) - return; - - var sessionIdProperty = new LogEventProperty(HttpSessionIdPropertyName, new ScalarValue(sessionId)); - logEvent.AddPropertyIfAbsent(sessionIdProperty); + return; } + + var sessionIdProperty = new LogEventProperty(HttpSessionIdPropertyName, new ScalarValue(sessionId)); + logEvent.AddPropertyIfAbsent(sessionIdProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs index a9ae85e2f0..a8a0610d2c 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs @@ -1,49 +1,51 @@ -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// This is used to create a new property in Logs called 'Log4NetLevel' +/// So that we can map Serilog levels to Log4Net levels - so log files stay consistent +/// +internal class Log4NetLevelMapperEnricher : ILogEventEnricher { - /// - /// This is used to create a new property in Logs called 'Log4NetLevel' - /// So that we can map Serilog levels to Log4Net levels - so log files stay consistent - /// - internal class Log4NetLevelMapperEnricher : ILogEventEnricher + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + string log4NetLevel; + + switch (logEvent.Level) { - string log4NetLevel; + case LogEventLevel.Debug: + log4NetLevel = "DEBUG"; + break; - switch (logEvent.Level) - { - case LogEventLevel.Debug: - log4NetLevel = "DEBUG"; - break; + case LogEventLevel.Error: + log4NetLevel = "ERROR"; + break; - case LogEventLevel.Error: - log4NetLevel = "ERROR"; - break; + case LogEventLevel.Fatal: + log4NetLevel = "FATAL"; + break; - case LogEventLevel.Fatal: - log4NetLevel = "FATAL"; - break; + case LogEventLevel.Information: + log4NetLevel = + "INFO "; // Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; - case LogEventLevel.Information: - log4NetLevel = "INFO "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; + case LogEventLevel.Verbose: + log4NetLevel = + "ALL "; // Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; - case LogEventLevel.Verbose: - log4NetLevel = "ALL "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; - - case LogEventLevel.Warning: - log4NetLevel = "WARN "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; - default: - log4NetLevel = string.Empty; - break; - } - - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Log4NetLevel", log4NetLevel)); + case LogEventLevel.Warning: + log4NetLevel = + "WARN "; // Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; + default: + log4NetLevel = string.Empty; + break; } + + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Log4NetLevel", log4NetLevel)); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs index bfe7c4172b..45495de9e8 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs @@ -1,100 +1,117 @@ -using System; using System.Reflection; -using System.Threading; using Microsoft.Extensions.Options; using Serilog.Core; using Serilog.Events; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Diagnostics; using Umbraco.Cms.Core.Hosting; -using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can +/// +public class ThreadAbortExceptionEnricher : ILogEventEnricher { - /// - /// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can - /// - public class ThreadAbortExceptionEnricher : ILogEventEnricher + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IMarchal _marchal; + private CoreDebugSettings _coreDebugSettings; + + public ThreadAbortExceptionEnricher( + IOptionsMonitor coreDebugSettings, + IHostingEnvironment hostingEnvironment, IMarchal marchal) { - private CoreDebugSettings _coreDebugSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IMarchal _marchal; + _coreDebugSettings = coreDebugSettings.CurrentValue; + _hostingEnvironment = hostingEnvironment; + _marchal = marchal; + coreDebugSettings.OnChange(x => _coreDebugSettings = x); + } - public ThreadAbortExceptionEnricher(IOptionsMonitor coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal) + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + switch (logEvent.Level) { - _coreDebugSettings = coreDebugSettings.CurrentValue; - _hostingEnvironment = hostingEnvironment; - _marchal = marchal; - coreDebugSettings.OnChange(x => _coreDebugSettings = x); + case LogEventLevel.Error: + case LogEventLevel.Fatal: + DumpThreadAborts(logEvent, propertyFactory); + break; + } + } + + private static bool IsTimeoutThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) + { + return false; } - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + if (abort.ExceptionState == null) { - switch (logEvent.Level) - { - case LogEventLevel.Error: - case LogEventLevel.Fatal: - DumpThreadAborts(logEvent, propertyFactory); - break; - } + return false; } - private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + Type stateType = abort.ExceptionState.GetType(); + if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") { - if (!IsTimeoutThreadAbortException(logEvent.Exception)) return; + return false; + } - var message = "The thread has been aborted, because the request has timed out."; + FieldInfo? timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); + if (timeoutField == null) + { + return false; + } - // dump if configured, or if stacktrace contains Monitor.ReliableEnter - var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(logEvent.Exception); + return (bool?)timeoutField.GetValue(abort.ExceptionState) ?? false; + } - // dump if it is ok to dump (might have a cap on number of dump...) - dump &= MiniDump.OkToDump(_hostingEnvironment); + private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (!IsTimeoutThreadAbortException(logEvent.Exception)) + { + return; + } - if (!dump) + var message = "The thread has been aborted, because the request has timed out."; + + // dump if configured, or if stacktrace contains Monitor.ReliableEnter + var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || + IsMonitorEnterThreadAbortException(logEvent.Exception); + + // dump if it is ok to dump (might have a cap on number of dump...) + dump &= MiniDump.OkToDump(_hostingEnvironment); + + if (!dump) + { + message += ". No minidump was created."; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); + } + else + { + try { - message += ". No minidump was created."; + var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); + message += dumped + ? ". A minidump was created in App_Data/MiniDump." + : ". Failed to create a minidump."; logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } - else + catch (Exception ex) { - try - { - var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); - message += dumped - ? ". A minidump was created in App_Data/MiniDump." - : ". Failed to create a minidump."; - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); - } - catch (Exception ex) - { - message = "Failed to create a minidump. " + ex; - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); - } + message = "Failed to create a minidump. " + ex; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } } + } - private static bool IsTimeoutThreadAbortException(Exception exception) + private static bool IsMonitorEnterThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) { - if (!(exception is ThreadAbortException abort)) return false; - if (abort.ExceptionState == null) return false; - - var stateType = abort.ExceptionState.GetType(); - if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false; - - var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); - if (timeoutField == null) return false; - - return (bool?)timeoutField.GetValue(abort.ExceptionState) ?? false; + return false; } - private static bool IsMonitorEnterThreadAbortException(Exception exception) - { - if (!(exception is ThreadAbortException abort)) return false; - - var stacktrace = abort.StackTrace; - return stacktrace?.Contains("System.Threading.Monitor.ReliableEnter") ?? false; - } - - + var stacktrace = abort.StackTrace; + return stacktrace?.Contains("System.Threading.Monitor.ReliableEnter") ?? false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 733410cf91..d8c6d1ff8f 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -46,7 +44,7 @@ namespace Umbraco.Extensions IConfiguration configuration, out UmbracoFileConfiguration umbFileConfiguration) { - global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); + Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); //Set this environment variable - so that it can be used in external config file //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> @@ -75,8 +73,7 @@ namespace Umbraco.Extensions rollingInterval: umbracoFileConfiguration.RollingInterval, flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, - retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit - ); + retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit); return logConfig; } @@ -93,7 +90,7 @@ namespace Umbraco.Extensions ILoggingConfiguration loggingConfiguration, UmbracoFileConfiguration umbracoFileConfiguration) { - global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); + Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); //Set this environment variable - so that it can be used in external config file //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> @@ -117,8 +114,7 @@ namespace Umbraco.Extensions rollingInterval: umbracoFileConfiguration.RollingInterval, flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, - retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit - ); + retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit); return logConfig; } @@ -137,7 +133,8 @@ namespace Umbraco.Extensions { //Main .txt logfile - in similar format to older Log4Net output //Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..txt"), + logConfig.WriteTo.File( + Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..txt"), shared: true, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: minimumLevel, @@ -162,8 +159,7 @@ namespace Umbraco.Extensions RollingInterval rollingInterval = RollingInterval.Day, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = 31, - Encoding? encoding = null - ) + Encoding? encoding = null) { formatter ??= new CompactJsonFormatter(); @@ -197,15 +193,19 @@ namespace Umbraco.Extensions /// A Serilog LoggerConfiguration /// The logging configuration /// The log level you wish the JSON file to collect - default is Verbose (highest) + /// /// The number of days to keep log files. Default is set to null which means all logs are kept public static LoggerConfiguration OutputDefaultJsonFile( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) + ILoggingConfiguration loggingConfiguration, + LogEventLevel minimumLevel = LogEventLevel.Verbose, + int? retainedFileCount = null) { // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) // Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(new CompactJsonFormatter(), + logConfig.WriteTo.File( + new CompactJsonFormatter(), Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles) ,$"UmbracoTraceLog.{Environment.MachineName}..json"), shared: true, rollingInterval: RollingInterval.Day, // Create a new JSON file every day diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 2ca43efd0c..4eb054b2a5 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -1,219 +1,155 @@ -using System; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Infrastructure.Logging.Serilog; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Serilog +namespace Umbraco.Cms.Core.Logging.Serilog; + +/// +/// Implements MS ILogger on top of Serilog. +/// +public class SerilogLogger : IDisposable { - /// - /// Implements MS ILogger on top of Serilog. - /// - public class SerilogLogger : IDisposable + public SerilogLogger(LoggerConfiguration logConfig) => + + // Configure Serilog static global logger with config passed in + SerilogLog = logConfig.CreateLogger(); + + public ILogger SerilogLog { get; } + + [Obsolete] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) => + CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); + + public void Dispose() => SerilogLog.DisposeIfDisposable(); + + /// + /// Creates a logger with some pre-defined configuration and remainder from config file + /// + /// Used by UmbracoApplicationBase to get its logger. + [Obsolete] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration, + out UmbracoFileConfiguration umbracoFileConfig) { - public global::Serilog.ILogger SerilogLog { get; } - - public SerilogLogger(LoggerConfiguration logConfig) - { - //Configure Serilog static global logger with config passed in - SerilogLog = logConfig.CreateLogger(); - } - - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) - { - return CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); - } - - /// - /// Creates a logger with some pre-defined configuration and remainder from config file - /// - /// Used by UmbracoApplicationBase to get its logger. - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration, - out UmbracoFileConfiguration umbracoFileConfig) - { - var serilogConfig = new LoggerConfiguration() - .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) - .ReadFrom.Configuration(configuration); - - return new SerilogLogger(serilogConfig); - } - - /// - /// Gets a contextualized logger. - /// - private global::Serilog.ILogger LoggerFor(Type reporting) - => SerilogLog.ForContext(reporting); - - /// - /// Maps Umbraco's log level to Serilog's. - /// - private LogEventLevel MapLevel(LogLevel level) - { - switch (level) - { - case LogLevel.Debug: - return LogEventLevel.Debug; - case LogLevel.Error: - return LogEventLevel.Error; - case LogLevel.Fatal: - return LogEventLevel.Fatal; - case LogLevel.Information: - return LogEventLevel.Information; - case LogLevel.Verbose: - return LogEventLevel.Verbose; - case LogLevel.Warning: - return LogEventLevel.Warning; - } - - throw new NotSupportedException($"LogLevel \"{level}\" is not supported."); - } - - /// - public bool IsEnabled(Type reporting, LogLevel level) - => LoggerFor(reporting).IsEnabled(MapLevel(level)); - - /// - public void Fatal(Type reporting, Exception exception, string message) - { - var logger = LoggerFor(reporting); - logger.Fatal(exception, message); - } - - /// - public void Fatal(Type reporting, Exception exception) - { - var logger = LoggerFor(reporting); - var message = "Exception."; - logger.Fatal(exception, message); - } - - /// - public void Fatal(Type reporting, string message) - { - LoggerFor(reporting).Fatal(message); - } - - /// - public void Fatal(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Fatal(messageTemplate, propertyValues); - } - - /// - public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - var logger = LoggerFor(reporting); - logger.Fatal(exception, messageTemplate, propertyValues); - } - - /// - public void Error(Type reporting, Exception exception, string message) - { - var logger = LoggerFor(reporting); - logger.Error(exception, message); - } - - /// - public void Error(Type reporting, Exception exception) - { - var logger = LoggerFor(reporting); - var message = "Exception"; - logger.Error(exception, message); - } - - /// - public void Error(Type reporting, string message) - { - LoggerFor(reporting).Error(message); - } - - /// - public void Error(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Error(messageTemplate, propertyValues); - } - - /// - public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - var logger = LoggerFor(reporting); - logger.Error(exception, messageTemplate, propertyValues); - } - - /// - public void Warn(Type reporting, string message) - { - LoggerFor(reporting).Warning(message); - } - - /// - public void Warn(Type reporting, string message, params object[] propertyValues) - { - LoggerFor(reporting).Warning(message, propertyValues); - } - - /// - public void Warn(Type reporting, Exception exception, string message) - { - LoggerFor(reporting).Warning(exception, message); - } - - /// - public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Warning(exception, messageTemplate, propertyValues); - } - - /// - public void Info(Type reporting, string message) - { - LoggerFor(reporting).Information(message); - } - - /// - public void Info(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Information(messageTemplate, propertyValues); - } - - /// - public void Debug(Type reporting, string message) - { - LoggerFor(reporting).Debug(message); - } - - /// - public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Debug(messageTemplate, propertyValues); - } - - /// - public void Verbose(Type reporting, string message) - { - LoggerFor(reporting).Verbose(message); - } - - /// - public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Verbose(messageTemplate, propertyValues); - } - - public void Dispose() - { - SerilogLog.DisposeIfDisposable(); - } + LoggerConfiguration? serilogConfig = new LoggerConfiguration() + .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) + .ReadFrom.Configuration(configuration); + return new SerilogLogger(serilogConfig); } + + public bool IsEnabled(Type reporting, LogLevel level) + => LoggerFor(reporting).IsEnabled(MapLevel(level)); + + /// + /// Gets a contextualized logger. + /// + private ILogger LoggerFor(Type reporting) + => SerilogLog.ForContext(reporting); + + /// + /// Maps Umbraco's log level to Serilog's. + /// + private LogEventLevel MapLevel(LogLevel level) + { + switch (level) + { + case LogLevel.Debug: + return LogEventLevel.Debug; + case LogLevel.Error: + return LogEventLevel.Error; + case LogLevel.Fatal: + return LogEventLevel.Fatal; + case LogLevel.Information: + return LogEventLevel.Information; + case LogLevel.Verbose: + return LogEventLevel.Verbose; + case LogLevel.Warning: + return LogEventLevel.Warning; + } + + throw new NotSupportedException($"LogLevel \"{level}\" is not supported."); + } + + public void Fatal(Type reporting, Exception exception, string message) + { + ILogger logger = LoggerFor(reporting); + logger.Fatal(exception, message); + } + + public void Fatal(Type reporting, Exception exception) + { + ILogger logger = LoggerFor(reporting); + var message = "Exception."; + logger.Fatal(exception, message); + } + + public void Fatal(Type reporting, string message) => LoggerFor(reporting).Fatal(message); + + public void Fatal(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Fatal(messageTemplate, propertyValues); + + public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + { + ILogger logger = LoggerFor(reporting); + logger.Fatal(exception, messageTemplate, propertyValues); + } + + public void Error(Type reporting, Exception exception, string message) + { + ILogger logger = LoggerFor(reporting); + logger.Error(exception, message); + } + + public void Error(Type reporting, Exception exception) + { + ILogger logger = LoggerFor(reporting); + var message = "Exception"; + logger.Error(exception, message); + } + + public void Error(Type reporting, string message) => LoggerFor(reporting).Error(message); + + public void Error(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Error(messageTemplate, propertyValues); + + public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + { + ILogger logger = LoggerFor(reporting); + logger.Error(exception, messageTemplate, propertyValues); + } + + public void Warn(Type reporting, string message) => LoggerFor(reporting).Warning(message); + + public void Warn(Type reporting, string message, params object[] propertyValues) => + LoggerFor(reporting).Warning(message, propertyValues); + + public void Warn(Type reporting, Exception exception, string message) => + LoggerFor(reporting).Warning(exception, message); + + public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Warning(exception, messageTemplate, propertyValues); + + public void Info(Type reporting, string message) => LoggerFor(reporting).Information(message); + + public void Info(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Information(messageTemplate, propertyValues); + + public void Debug(Type reporting, string message) => LoggerFor(reporting).Debug(message); + + public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Debug(messageTemplate, propertyValues); + + public void Verbose(Type reporting, string message) => LoggerFor(reporting).Verbose(message); + + public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Verbose(messageTemplate, propertyValues); } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs index f306416cf2..b83c76fbd3 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs @@ -1,47 +1,47 @@ -using System; -using System.IO; -using System.Linq; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Events; -namespace Umbraco.Cms.Infrastructure.Logging.Serilog +namespace Umbraco.Cms.Infrastructure.Logging.Serilog; + +public class UmbracoFileConfiguration { - public class UmbracoFileConfiguration + public UmbracoFileConfiguration(IConfiguration configuration) { - public UmbracoFileConfiguration(IConfiguration configuration) + if (configuration == null) { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - var appSettings = configuration.GetSection("Serilog:WriteTo"); - var umbracoFileAppSettings = appSettings.GetChildren().LastOrDefault(x => x.GetValue("Name") == "UmbracoFile"); - - if (umbracoFileAppSettings is not null) - { - var args = umbracoFileAppSettings.GetSection("Args"); - - RestrictedToMinimumLevel = args.GetValue(nameof(RestrictedToMinimumLevel), RestrictedToMinimumLevel); - FileSizeLimitBytes = args.GetValue(nameof(FileSizeLimitBytes), FileSizeLimitBytes); - RollingInterval = args.GetValue(nameof(RollingInterval), RollingInterval); - FlushToDiskInterval = args.GetValue(nameof(FlushToDiskInterval), FlushToDiskInterval); - RollOnFileSizeLimit = args.GetValue(nameof(RollOnFileSizeLimit), RollOnFileSizeLimit); - RetainedFileCountLimit = args.GetValue(nameof(RetainedFileCountLimit), RetainedFileCountLimit); - } + throw new ArgumentNullException(nameof(configuration)); } - public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose; - public long FileSizeLimitBytes { get; set; } = 1073741824; - public RollingInterval RollingInterval { get; set; } = RollingInterval.Day; - public TimeSpan? FlushToDiskInterval { get; set; } = null; - public bool RollOnFileSizeLimit { get; set; } = false; - public int RetainedFileCountLimit { get; set; } = 31; + IConfigurationSection? appSettings = configuration.GetSection("Serilog:WriteTo"); + IConfigurationSection? umbracoFileAppSettings = + appSettings.GetChildren().LastOrDefault(x => x.GetValue("Name") == "UmbracoFile"); - public string GetPath(string logDirectory) + if (umbracoFileAppSettings is not null) { - return Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json"); + IConfigurationSection? args = umbracoFileAppSettings.GetSection("Args"); + + RestrictedToMinimumLevel = args.GetValue(nameof(RestrictedToMinimumLevel), RestrictedToMinimumLevel); + FileSizeLimitBytes = args.GetValue(nameof(FileSizeLimitBytes), FileSizeLimitBytes); + RollingInterval = args.GetValue(nameof(RollingInterval), RollingInterval); + FlushToDiskInterval = args.GetValue(nameof(FlushToDiskInterval), FlushToDiskInterval); + RollOnFileSizeLimit = args.GetValue(nameof(RollOnFileSizeLimit), RollOnFileSizeLimit); + RetainedFileCountLimit = args.GetValue(nameof(RetainedFileCountLimit), RetainedFileCountLimit); } } + + public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose; + + public long FileSizeLimitBytes { get; set; } = 1073741824; + + public RollingInterval RollingInterval { get; set; } = RollingInterval.Day; + + public TimeSpan? FlushToDiskInterval { get; set; } + + public bool RollOnFileSizeLimit { get; set; } + + public int RetainedFileCountLimit { get; set; } = 31; + + public string GetPath(string logDirectory) => + Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json"); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs index 36d12dee0d..c5692384f6 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs @@ -1,49 +1,43 @@ -using System; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class CountingFilter : ILogFilter { - internal class CountingFilter : ILogFilter + public CountingFilter() => Counts = new LogLevelCounts(); + + public LogLevelCounts Counts { get; } + + public bool TakeLogEvent(LogEvent e) { - public CountingFilter() + switch (e.Level) { - Counts = new LogLevelCounts(); + case LogEventLevel.Debug: + Counts.Debug++; + break; + + case LogEventLevel.Information: + Counts.Information++; + break; + + case LogEventLevel.Warning: + Counts.Warning++; + break; + + case LogEventLevel.Error: + Counts.Error++; + break; + + case LogEventLevel.Fatal: + Counts.Fatal++; + break; + case LogEventLevel.Verbose: + break; + default: + throw new ArgumentOutOfRangeException(); } - public LogLevelCounts Counts { get; } - - public bool TakeLogEvent(LogEvent e) - { - - switch (e.Level) - { - case LogEventLevel.Debug: - Counts.Debug++; - break; - - case LogEventLevel.Information: - Counts.Information++; - break; - - case LogEventLevel.Warning: - Counts.Warning++; - break; - - case LogEventLevel.Error: - Counts.Error++; - break; - - case LogEventLevel.Fatal: - Counts.Fatal++; - break; - case LogEventLevel.Verbose: - break; - default: - throw new ArgumentOutOfRangeException(); - } - - //Don't add it to the list - return false; - } + // Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs index 1a4ececff6..834da2952e 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs @@ -1,18 +1,19 @@ -using Serilog.Events; +using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class ErrorCounterFilter : ILogFilter { - internal class ErrorCounterFilter : ILogFilter + public int Count { get; private set; } + + public bool TakeLogEvent(LogEvent e) { - public int Count { get; private set; } - - public bool TakeLogEvent(LogEvent e) + if (e.Level == LogEventLevel.Fatal || e.Level == LogEventLevel.Error || e.Exception != null) { - if (e.Level == LogEventLevel.Fatal || e.Level == LogEventLevel.Error || e.Exception != null) - Count++; - - //Don't add it to the list - return false; + Count++; } + + // Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs index a1c549add8..a8444f4276 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs @@ -1,79 +1,75 @@ -using System; -using System.Linq; using Serilog.Events; using Serilog.Expressions; using Umbraco.Cms.Infrastructure.Logging.Viewer; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +// Log Expression Filters (pass in filter exp string) +internal class ExpressionFilter : ILogFilter { - //Log Expression Filters (pass in filter exp string) - internal class ExpressionFilter : ILogFilter + private const string ExpressionOperators = "()+=*<>%-"; + private readonly Func? _filter; + + public ExpressionFilter(string? filterExpression) { - private readonly Func? _filter; - private const string s_expressionOperators = "()+=*<>%-"; + Func? filter; - public ExpressionFilter(string? filterExpression) + // Our custom Serilog Functions to extend Serilog.Expressions + // In this case we are plugging the gap for the missing Has() + // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions + // Along with patching support for the more verbose built in property names + var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); + + if (string.IsNullOrEmpty(filterExpression)) { - Func? filter; - - // Our custom Serilog Functions to extend Serilog.Expressions - // In this case we are plugging the gap for the missing Has() - // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions - // Along with patching support for the more verbose built in property names - var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); - - if (string.IsNullOrEmpty(filterExpression)) - { - return; - } - - // If the expression is one word and doesn't contain a serilog operator then we can perform a like search - if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(s_expressionOperators.Select(c => c))) - { - filter = PerformMessageLikeFilter(filterExpression); - } - else // check if it's a valid expression - { - // If the expression evaluates then make it into a filter - if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression? compiled, out var error)) - { - filter = evt => - { - LogEventPropertyValue? result = compiled(evt); - return ExpressionResult.IsTrue(result); - }; - } - else - { - // 'error' describes a syntax error, where it was unable to compile an expression - // Assume the expression was a search string and make a Like filter from that - filter = PerformMessageLikeFilter(filterExpression); - } - } - - _filter = filter; + return; } - public bool TakeLogEvent(LogEvent e) + // If the expression is one word and doesn't contain a serilog operator then we can perform a like search + if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(ExpressionOperators.Select(c => c))) { - return _filter == null || _filter(e); + filter = PerformMessageLikeFilter(filterExpression); } - private Func? PerformMessageLikeFilter(string filterExpression) + // check if it's a valid expression + else { - var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; - if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression? compiled, out var error)) + // If the expression evaluates then make it into a filter + if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression? compiled, out var error)) { - // `compiled` is a function that can be executed against `LogEvent`s: - return evt => + filter = evt => { LogEventPropertyValue? result = compiled(evt); return ExpressionResult.IsTrue(result); }; } - - return null; + else + { + // 'error' describes a syntax error, where it was unable to compile an expression + // Assume the expression was a search string and make a Like filter from that + filter = PerformMessageLikeFilter(filterExpression); + } } + + _filter = filter; + } + + public bool TakeLogEvent(LogEvent e) => _filter == null || _filter(e); + + private Func? PerformMessageLikeFilter(string filterExpression) + { + var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; + if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression? compiled, out var error)) + { + // `compiled` is a function that can be executed against `LogEvent`s: + return evt => + { + LogEventPropertyValue? result = compiled(evt); + return ExpressionResult.IsTrue(result); + }; + } + + return null; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs index 4619df2b13..e276bdfa5a 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs @@ -1,9 +1,8 @@ -using Serilog.Events; +using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public interface ILogFilter { - public interface ILogFilter - { - bool TakeLogEvent(LogEvent e); - } + bool TakeLogEvent(LogEvent e); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs index 1566b96282..25576b88da 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs @@ -1,18 +1,17 @@ using System.Collections.ObjectModel; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer -{ - public interface ILogLevelLoader - { - /// - /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. - /// - ReadOnlyDictionary GetLogLevelsFromSinks(); +namespace Umbraco.Cms.Core.Logging.Viewer; - /// - /// Get the Serilog minimum-level value from the config file. - /// - LogEventLevel? GetGlobalMinLogLevel(); - } +public interface ILogLevelLoader +{ + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + ReadOnlyDictionary GetLogLevelsFromSinks(); + + /// + /// Get the Serilog minimum-level value from the config file. + /// + LogEventLevel? GetGlobalMinLogLevel(); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index 2fd357d5a2..3fc763d92f 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -1,57 +1,52 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Serilog.Events; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public interface ILogViewer { - public interface ILogViewer - { - /// - /// Get all saved searches from your chosen data source - /// - IReadOnlyList? GetSavedSearches(); + bool CanHandleLargeLogs { get; } - /// - /// Adds a new saved search to chosen data source and returns the updated searches - /// - IReadOnlyList? AddSavedSearch(string? name, string? query); + /// + /// Get all saved searches from your chosen data source + /// + IReadOnlyList? GetSavedSearches(); - /// - /// Deletes a saved search to chosen data source and returns the remaining searches - /// - IReadOnlyList? DeleteSavedSearch(string? name, string? query); + /// + /// Adds a new saved search to chosen data source and returns the updated searches + /// + IReadOnlyList? AddSavedSearch(string? name, string? query); - /// - /// A count of number of errors - /// By counting Warnings with Exceptions, Errors & Fatal messages - /// - int GetNumberOfErrors(LogTimePeriod logTimePeriod); + /// + /// Deletes a saved search to chosen data source and returns the remaining searches + /// + IReadOnlyList? DeleteSavedSearch(string? name, string? query); - /// - /// Returns a number of the different log level entries - /// - LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); + /// + /// A count of number of errors + /// By counting Warnings with Exceptions, Errors & Fatal messages + /// + int GetNumberOfErrors(LogTimePeriod logTimePeriod); - /// - /// Returns a list of all unique message templates and their counts - /// - IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); + /// + /// Returns a number of the different log level entries + /// + LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); - bool CanHandleLargeLogs { get; } + /// + /// Returns a list of all unique message templates and their counts + /// + IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); - bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); - /// - /// Returns the collection of logs - /// - PagedResult GetLogs(LogTimePeriod logTimePeriod, - int pageNumber = 1, - int pageSize = 100, - Direction orderDirection = Direction.Descending, - string? filterExpression = null, - string[]? logLevels = null); - - } + /// + /// Returns the collection of logs + /// + PagedResult GetLogs( + LogTimePeriod logTimePeriod, + int pageNumber = 1, + int pageSize = 100, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs index 5be26a1099..bdcbf64a94 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Logging.Viewer; -namespace Umbraco.Cms.Core.Logging.Viewer +public interface ILogViewerConfig { - public interface ILogViewerConfig - { - IReadOnlyList? GetSavedSearches(); - IReadOnlyList? AddSavedSearch(string? name, string? query); - IReadOnlyList? DeleteSavedSearch(string? name, string? query); - } + IReadOnlyList? GetSavedSearches(); + + IReadOnlyList? AddSavedSearch(string? name, string? query); + + IReadOnlyList? DeleteSavedSearch(string? name, string? query); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs index f397c1ab7c..6f03135c1f 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogLevelCounts { - public class LogLevelCounts - { - public int Information { get; set; } + public int Information { get; set; } - public int Debug { get; set; } + public int Debug { get; set; } - public int Warning { get; set; } + public int Warning { get; set; } - public int Error { get; set; } + public int Error { get; set; } - public int Fatal { get; set; } - } + public int Fatal { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs index 37c7923cca..a0f6927ef7 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs @@ -1,41 +1,36 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; using Serilog; using Serilog.Events; using Umbraco.Cms.Infrastructure.Logging.Serilog; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogLevelLoader : ILogLevelLoader { - public class LogLevelLoader : ILogLevelLoader + private readonly UmbracoFileConfiguration _umbracoFileConfig; + + public LogLevelLoader(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; + + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + public ReadOnlyDictionary GetLogLevelsFromSinks() { - private readonly UmbracoFileConfiguration _umbracoFileConfig; - - public LogLevelLoader(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; - - /// - /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. - /// - public ReadOnlyDictionary GetLogLevelsFromSinks() + var configuredLogLevels = new Dictionary { - var configuredLogLevels = new Dictionary - { - { "Global", GetGlobalMinLogLevel() }, - { "UmbracoFile", _umbracoFileConfig.RestrictedToMinimumLevel } - }; + { "Global", GetGlobalMinLogLevel() }, { "UmbracoFile", _umbracoFileConfig.RestrictedToMinimumLevel }, + }; - return new ReadOnlyDictionary(configuredLogLevels); - } + return new ReadOnlyDictionary(configuredLogLevels); + } - /// - /// Get the Serilog minimum-level value from the config file. - /// - public LogEventLevel? GetGlobalMinLogLevel() - { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; - return (LogEventLevel?)logLevel; - } + /// + /// Get the Serilog minimum-level value from the config file. + /// + public LogEventLevel? GetGlobalMinLogLevel() + { + LogEventLevel? logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled) + .DefaultIfEmpty(LogEventLevel.Information).Min(); + return logLevel; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs index 9bdea3f650..3974a8da1e 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs @@ -1,43 +1,40 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Serilog.Events; -using System; -using System.Collections.Generic; // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogMessage { - public class LogMessage - { - /// - /// The time at which the log event occurred. - /// - public DateTimeOffset Timestamp { get; set; } + /// + /// The time at which the log event occurred. + /// + public DateTimeOffset Timestamp { get; set; } - /// - /// The level of the event. - /// - [JsonConverter(typeof(StringEnumConverter))] - public LogEventLevel Level { get; set; } + /// + /// The level of the event. + /// + [JsonConverter(typeof(StringEnumConverter))] + public LogEventLevel Level { get; set; } - /// - /// The message template describing the log event. - /// - public string? MessageTemplateText { get; set; } + /// + /// The message template describing the log event. + /// + public string? MessageTemplateText { get; set; } - /// - /// The message template filled with the log event properties. - /// - public string? RenderedMessage { get; set; } + /// + /// The message template filled with the log event properties. + /// + public string? RenderedMessage { get; set; } - /// - /// Properties associated with the log event, including those presented in Serilog.Events.LogEvent.MessageTemplate. - /// - public IReadOnlyDictionary? Properties { get; set; } + /// + /// Properties associated with the log event, including those presented in Serilog.Events.LogEvent.MessageTemplate. + /// + public IReadOnlyDictionary? Properties { get; set; } - /// - /// An exception associated with the log event, or null. - /// - public string? Exception { get; set; } - } + /// + /// An exception associated with the log event, or null. + /// + public string? Exception { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs index ecded4d35b..821115ff11 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Logging.Viewer -{ - public class LogTemplate - { - public string? MessageTemplate { get; set; } +namespace Umbraco.Cms.Core.Logging.Viewer; - public int Count { get; set; } - } +public class LogTemplate +{ + public string? MessageTemplate { get; set; } + + public int Count { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs index 446f7bf160..67533ef4f1 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs @@ -1,16 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Logging.Viewer; -namespace Umbraco.Cms.Core.Logging.Viewer +public class LogTimePeriod { - public class LogTimePeriod + public LogTimePeriod(DateTime startTime, DateTime endTime) { - public LogTimePeriod(DateTime startTime, DateTime endTime) - { - StartTime = startTime; - EndTime = endTime; - } - - public DateTime StartTime { get; } - public DateTime EndTime { get; } + StartTime = startTime; + EndTime = endTime; } + + public DateTime StartTime { get; } + + public DateTime EndTime { get; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index 8b11639696..e8b9de36d7 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -1,49 +1,47 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogViewerConfig : ILogViewerConfig { - public class LogViewerConfig : ILogViewerConfig + private readonly ILogViewerQueryRepository _logViewerQueryRepository; + private readonly IScopeProvider _scopeProvider; + + public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) { - private readonly ILogViewerQueryRepository _logViewerQueryRepository; - private readonly IScopeProvider _scopeProvider; + _logViewerQueryRepository = logViewerQueryRepository; + _scopeProvider = scopeProvider; + } - public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) + public IReadOnlyList? GetSavedSearches() + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + IEnumerable? logViewerQueries = _logViewerQueryRepository.GetMany(); + SavedLogSearch[]? result = logViewerQueries?.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); + return result; + } + + public IReadOnlyList? AddSavedSearch(string? name, string? query) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); + + return GetSavedSearches(); + } + + public IReadOnlyList? DeleteSavedSearch(string? name, string? query) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + ILogViewerQuery? item = name is null ? null : _logViewerQueryRepository.GetByName(name); + if (item is not null) { - _logViewerQueryRepository = logViewerQueryRepository; - _scopeProvider = scopeProvider; + _logViewerQueryRepository.Delete(item); } - public IReadOnlyList? GetSavedSearches() - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - var logViewerQueries = _logViewerQueryRepository.GetMany(); - var result = logViewerQueries?.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); - return result; - } - - public IReadOnlyList? AddSavedSearch(string? name, string? query) - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); - - return GetSavedSearches(); - } - - public IReadOnlyList? DeleteSavedSearch(string? name, string? query) - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - var item = name is null ? null : _logViewerQueryRepository.GetByName(name); - if (item is not null) - { - _logViewerQueryRepository.Delete(item); - } - - //Return the updated object - so we can instantly reset the entire array from the API response - return GetSavedSearches(); - } + // Return the updated object - so we can instantly reset the entire array from the API response + return GetSavedSearches(); } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs index 1b89716256..62b040f5f1 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class MessageTemplateFilter : ILogFilter { - internal class MessageTemplateFilter : ILogFilter + public readonly Dictionary Counts = new(); + + public bool TakeLogEvent(LogEvent e) { - public readonly Dictionary Counts = new Dictionary(); - - public bool TakeLogEvent(LogEvent e) + var templateText = e.MessageTemplate.Text; + if (Counts.TryGetValue(templateText, out var count)) { - var templateText = e.MessageTemplate.Text; - if (Counts.TryGetValue(templateText, out var count)) - { - count++; - } - else - { - count = 1; - } - - Counts[templateText] = count; - - //Don't add it to the list - return false; + count++; } + else + { + count = 1; + } + + Counts[templateText] = count; + + // Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs index adbd1a6431..320f121890 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs @@ -1,13 +1,12 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class SavedLogSearch { - public class SavedLogSearch - { - [JsonProperty("name")] - public string? Name { get; set; } + [JsonProperty("name")] + public string? Name { get; set; } - [JsonProperty("query")] - public string? Query { get; set; } - } + [JsonProperty("query")] + public string? Query { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs index 92b16b9729..9cf9cc7307 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs @@ -1,14 +1,10 @@ using Serilog.Events; -namespace Umbraco.Cms.Infrastructure.Logging.Viewer +namespace Umbraco.Cms.Infrastructure.Logging.Viewer; + +public class SerilogExpressionsFunctions { - public class SerilogExpressionsFunctions - { - // This Has() code is the same as the renamed IsDefined() function - // Added this to help backport and ensure saved queries continue to work if using Has() - public static LogEventPropertyValue? Has(LogEventPropertyValue? value) - { - return new ScalarValue(value != null); - } - } + // This Has() code is the same as the renamed IsDefined() function + // Added this to help backport and ensure saved queries continue to work if using Has() + public static LogEventPropertyValue? Has(LogEventPropertyValue? value) => new ScalarValue(value != null); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs index ba148a1bda..c573f237b2 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs @@ -1,140 +1,133 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Serilog.Events; using Serilog.Formatting.Compact.Reader; +using ILogger = Serilog.ILogger; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase { - internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase + private const int FileSizeCap = 100; + private readonly ILogger _logger; + private readonly string _logsPath; + + public SerilogJsonLogViewer( + ILogger logger, + ILogViewerConfig logViewerConfig, + ILoggingConfiguration loggingConfiguration, + ILogLevelLoader logLevelLoader, + ILogger serilogLog) + : base(logViewerConfig, logLevelLoader, serilogLog) { - private readonly string _logsPath; - private readonly ILogger _logger; + _logger = logger; + _logsPath = loggingConfiguration.LogDirectory; + } - public SerilogJsonLogViewer( - ILogger logger, - ILogViewerConfig logViewerConfig, - ILoggingConfiguration loggingConfiguration, - ILogLevelLoader logLevelLoader, - global::Serilog.ILogger serilogLog) - : base(logViewerConfig, logLevelLoader, serilogLog) + public override bool CanHandleLargeLogs => false; + + public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) + { + // Log Directory + var logDirectory = _logsPath; + + // Number of entries + long fileSizeCount = 0; + + // foreach full day in the range - see if we can find one or more filenames that end with + // yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing + for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { - _logger = logger; - _logsPath = loggingConfiguration.LogDirectory; + // Filename ending to search for (As could be multiple) + var filesToFind = GetSearchPattern(day); + + var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind); + + fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length); } - private const int FileSizeCap = 100; + // The GetLogSize call on JsonLogViewer returns the total file size in bytes + // Check if the log size is not greater than 100Mb (FileSizeCap) + var logSizeAsMegabytes = fileSizeCount / 1024 / 1024; + return logSizeAsMegabytes <= FileSizeCap; + } - public override bool CanHandleLargeLogs => false; + protected override IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, + int take) + { + var logs = new List(); - public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) + var count = 0; + + // foreach full day in the range - see if we can find one or more filenames that end with + // yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing + for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { - //Log Directory - var logDirectory = _logsPath; + // Filename ending to search for (As could be multiple) + var filesToFind = GetSearchPattern(day); - //Number of entries - long fileSizeCount = 0; + var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind); - //foreach full day in the range - see if we can find one or more filenames that end with - //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) + // Foreach file we find - open it + foreach (var filePath in filesForCurrentDay) { - //Filename ending to search for (As could be multiple) - var filesToFind = GetSearchPattern(day); - - var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind); - - fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length); - } - - //The GetLogSize call on JsonLogViewer returns the total file size in bytes - //Check if the log size is not greater than 100Mb (FileSizeCap) - var logSizeAsMegabytes = fileSizeCount / 1024 / 1024; - return logSizeAsMegabytes <= FileSizeCap; - } - - private string GetSearchPattern(DateTime day) - { - return $"*{day:yyyyMMdd}*.json"; - } - - protected override IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take) - { - var logs = new List(); - - var count = 0; - - //foreach full day in the range - see if we can find one or more filenames that end with - //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) - { - //Filename ending to search for (As could be multiple) - var filesToFind = GetSearchPattern(day); - - var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind); - - //Foreach file we find - open it - foreach (var filePath in filesForCurrentDay) + // Open log file & add contents to the log collection + // Which we then use LINQ to page over + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { - //Open log file & add contents to the log collection - //Which we then use LINQ to page over - using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var stream = new StreamReader(fs)) { - using (var stream = new StreamReader(fs)) + var reader = new LogEventReader(stream); + while (TryRead(reader, out LogEvent? evt)) { - var reader = new LogEventReader(stream); - while (TryRead(reader, out var evt)) + // We may get a null if log line is malformed + if (evt == null) { - //We may get a null if log line is malformed - if (evt == null) - { - continue; - } - - if (count > skip + take) - { - break; - } - - if (count < skip) - { - count++; - continue; - } - - if (filter.TakeLogEvent(evt)) - { - logs.Add(evt); - } - - count++; + continue; } + + if (count > skip + take) + { + break; + } + + if (count < skip) + { + count++; + continue; + } + + if (filter.TakeLogEvent(evt)) + { + logs.Add(evt); + } + + count++; } } } } - - return logs; } - private bool TryRead(LogEventReader reader, out LogEvent? evt) - { - try - { - return reader.TryRead(out evt); - } - catch (JsonReaderException ex) - { - // As we are reading/streaming one line at a time in the JSON file - // Thus we can not report the line number, as it will always be 1 - _logger.LogError(ex, "Unable to parse a line in the JSON log file"); + return logs; + } - evt = null; - return true; - } + private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json"; + + private bool TryRead(LogEventReader reader, out LogEvent? evt) + { + try + { + return reader.TryRead(out evt); + } + catch (JsonReaderException ex) + { + // As we are reading/streaming one line at a time in the JSON file + // Thus we can not report the line number, as it will always be 1 + _logger.LogError(ex, "Unable to parse a line in the JSON log file"); + + evt = null; + return true; } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs index 8e24f40b6c..deac7b3a5a 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs @@ -1,39 +1,38 @@ -using System; using System.Diagnostics.CodeAnalysis; using Serilog.Expressions; -namespace Umbraco.Cms.Infrastructure.Logging.Viewer +namespace Umbraco.Cms.Infrastructure.Logging.Viewer; + +/// +/// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality +/// Of easily allowing any static methods definied in the passed in class/type +/// To extend as functions to use for filtering logs such as Has() and any other custom ones +/// +public class SerilogLegacyNameResolver : StaticMemberNameResolver { - /// - /// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality - /// Of easily allowing any static methods definied in the passed in class/type - /// To extend as functions to use for filtering logs such as Has() and any other custom ones - /// - public class SerilogLegacyNameResolver : StaticMemberNameResolver + public SerilogLegacyNameResolver(Type type) + : base(type) { - public SerilogLegacyNameResolver(Type type) : base(type) - { - } + } - /// - /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions - /// So we can still support the more verbose built in property names such as - /// Exception, Level, MessageTemplate etc - /// - public override bool TryResolveBuiltInPropertyName(string alias, [MaybeNullWhen(false)] out string target) + /// + /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions + /// So we can still support the more verbose built in property names such as + /// Exception, Level, MessageTemplate etc + /// + public override bool TryResolveBuiltInPropertyName(string alias, [MaybeNullWhen(false)] out string target) + { + target = alias switch { - target = alias switch - { - "Exception" => "x", - "Level" => "l", - "Message" => "m", - "MessageTemplate" => "mt", - "Properties" => "p", - "Timestamp" => "t", - _ => null - }; + "Exception" => "x", + "Level" => "l", + "Message" => "m", + "MessageTemplate" => "mt", + "Properties" => "p", + "Timestamp" => "t", + _ => null, + }; - return target != null; - } + return target != null; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index bb362e30b8..56efba4ca9 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -1,136 +1,130 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; +using System.Collections.ObjectModel; using Microsoft.Extensions.DependencyInjection; +using Serilog; using Serilog.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public abstract class SerilogLogViewerSourceBase : ILogViewer { - public abstract class SerilogLogViewerSourceBase : ILogViewer + private readonly ILogLevelLoader _logLevelLoader; + private readonly ILogViewerConfig _logViewerConfig; + private readonly ILogger _serilogLog; + + protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, ILogger serilogLog) { - private readonly ILogViewerConfig _logViewerConfig; - private readonly ILogLevelLoader _logLevelLoader; - private readonly global::Serilog.ILogger _serilogLog; + _logViewerConfig = logViewerConfig; + _logLevelLoader = logLevelLoader; + _serilogLog = serilogLog; + } - protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, global::Serilog.ILogger serilogLog) + public abstract bool CanHandleLargeLogs { get; } + + public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + + public virtual IReadOnlyList? GetSavedSearches() + => _logViewerConfig.GetSavedSearches(); + + public virtual IReadOnlyList? AddSavedSearch(string? name, string? query) + => _logViewerConfig.AddSavedSearch(name, query); + + public virtual IReadOnlyList? DeleteSavedSearch(string? name, string? query) + => _logViewerConfig.DeleteSavedSearch(name, query); + + public int GetNumberOfErrors(LogTimePeriod logTimePeriod) + { + var errorCounter = new ErrorCounterFilter(); + GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue); + return errorCounter.Count; + } + + public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) + { + var counter = new CountingFilter(); + GetLogs(logTimePeriod, counter, 0, int.MaxValue); + return counter.Counts; + } + + public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) + { + var messageTemplates = new MessageTemplateFilter(); + GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue); + + IOrderedEnumerable templates = messageTemplates.Counts + .Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value }) + .OrderByDescending(x => x.Count); + + return templates; + } + + public PagedResult GetLogs( + LogTimePeriod logTimePeriod, + int pageNumber = 1, + int pageSize = 100, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null) + { + var expression = new ExpressionFilter(filterExpression); + IReadOnlyList filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); + + // This is user used the checkbox UI to toggle which log levels they wish to see + // If an empty array or null - its implied all levels to be viewed + if (logLevels?.Length > 0) { - _logViewerConfig = logViewerConfig; - _logLevelLoader = logLevelLoader; - _serilogLog = serilogLog; - } - - public abstract bool CanHandleLargeLogs { get; } - - /// - /// Get all logs from your chosen data source back as Serilog LogEvents - /// - protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); - - public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); - - public virtual IReadOnlyList? GetSavedSearches() - => _logViewerConfig.GetSavedSearches(); - - public virtual IReadOnlyList? AddSavedSearch(string? name, string? query) - => _logViewerConfig.AddSavedSearch(name, query); - - public virtual IReadOnlyList? DeleteSavedSearch(string? name, string? query) - => _logViewerConfig.DeleteSavedSearch(name, query); - - public int GetNumberOfErrors(LogTimePeriod logTimePeriod) - { - var errorCounter = new ErrorCounterFilter(); - GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue); - return errorCounter.Count; - } - - /// - /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. - /// - public ReadOnlyDictionary GetLogLevels() - { - return _logLevelLoader.GetLogLevelsFromSinks(); - } - - public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) - { - var counter = new CountingFilter(); - GetLogs(logTimePeriod, counter, 0, int.MaxValue); - return counter.Counts; - } - - public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) - { - var messageTemplates = new MessageTemplateFilter(); - GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue); - - var templates = messageTemplates.Counts. - Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value }) - .OrderByDescending(x=> x.Count); - - return templates; - } - - public PagedResult GetLogs(LogTimePeriod logTimePeriod, - int pageNumber = 1, int pageSize = 100, - Direction orderDirection = Direction.Descending, - string? filterExpression = null, - string[]? logLevels = null) - { - var expression = new ExpressionFilter(filterExpression); - var filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); - - //This is user used the checkbox UI to toggle which log levels they wish to see - //If an empty array or null - its implied all levels to be viewed - if (logLevels?.Length > 0) + var logsAfterLevelFilters = new List(); + var validLogType = true; + foreach (var level in logLevels) { - var logsAfterLevelFilters = new List(); - var validLogType = true; - foreach (var level in logLevels) + // Check if level string is part of the LogEventLevel enum + if (Enum.IsDefined(typeof(LogEventLevel), level)) { - //Check if level string is part of the LogEventLevel enum - if(Enum.IsDefined(typeof(LogEventLevel), level)) - { - validLogType = true; - logsAfterLevelFilters.AddRange(filteredLogs.Where(x => string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase))); - } - else - { - validLogType = false; - } + validLogType = true; + logsAfterLevelFilters.AddRange(filteredLogs.Where(x => + string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase))); } - - if (validLogType) + else { - filteredLogs = logsAfterLevelFilters; + validLogType = false; } } - long totalRecords = filteredLogs.Count; - - //Order By, Skip, Take & Select - var logMessages = filteredLogs - .OrderBy(l => l.Timestamp, orderDirection) - .Skip(pageSize * (pageNumber - 1)) - .Take(pageSize) - .Select(x => new LogMessage - { - Timestamp = x.Timestamp, - Level = x.Level, - MessageTemplateText = x.MessageTemplate.Text, - Exception = x.Exception?.ToString(), - Properties = x.Properties, - RenderedMessage = x.RenderMessage() - }); - - return new PagedResult(totalRecords, pageNumber, pageSize) + if (validLogType) { - Items = logMessages - }; + filteredLogs = logsAfterLevelFilters; + } } + + long totalRecords = filteredLogs.Count; + + // Order By, Skip, Take & Select + IEnumerable logMessages = filteredLogs + .OrderBy(l => l.Timestamp, orderDirection) + .Skip(pageSize * (pageNumber - 1)) + .Take(pageSize) + .Select(x => new LogMessage + { + Timestamp = x.Timestamp, + Level = x.Level, + MessageTemplateText = x.MessageTemplate.Text, + Exception = x.Exception?.ToString(), + Properties = x.Properties, + RenderedMessage = x.RenderMessage(), + }); + + return new PagedResult(totalRecords, pageNumber, pageSize) { Items = logMessages }; } + + /// + /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. + /// + public ReadOnlyDictionary GetLogLevels() => _logLevelLoader.GetLogLevelsFromSinks(); + + /// + /// Get all logs from your chosen data source back as Serilog LogEvents + /// + protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); } diff --git a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs index 13b0333984..07109729b6 100644 --- a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs +++ b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs @@ -1,208 +1,215 @@ -using System; -using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using HtmlAgilityPack; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Infrastructure.Macros +namespace Umbraco.Cms.Infrastructure.Macros; + +/// +/// Parses the macro syntax in a string and renders out it's contents +/// +public class MacroTagParser { - /// - /// Parses the macro syntax in a string and renders out it's contents - /// - public class MacroTagParser - { - private static readonly Regex MacroRteContent = new Regex(@"()", + private static readonly Regex _macroRteContent = new( + @"()", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); + + private static readonly Regex _macroPersistedFormat = + new( + @"(<\?UMBRACO_MACRO (?:.+?)??macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); - private static readonly Regex MacroPersistedFormat = - new Regex(@"(<\?UMBRACO_MACRO (?:.+?)??macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); - - /// - /// This formats the persisted string to something useful for the rte so that the macro renders properly since we - /// persist all macro formats like {?UMBRACO_MACRO macroAlias=\"myMacro\" /} - /// - /// - /// The HTML attributes to be added to the div - /// - /// - /// This converts the persisted macro format to this: - /// - /// {div class='umb-macro-holder'} - /// - /// {ins}Macro alias: {strong}My Macro{/strong}{/ins} - /// {/div} - /// - /// - public static string FormatRichTextPersistedDataForEditor(string persistedContent, IDictionary htmlAttributes) + /// + /// This formats the persisted string to something useful for the rte so that the macro renders properly since we + /// persist all macro formats like {?UMBRACO_MACRO macroAlias=\"myMacro\" /} + /// + /// + /// The HTML attributes to be added to the div + /// + /// + /// This converts the persisted macro format to this: + /// {div class='umb-macro-holder'} + /// + /// {ins}Macro alias: {strong}My Macro{/strong}{/ins} + /// {/div} + /// + public static string FormatRichTextPersistedDataForEditor( + string persistedContent, + IDictionary htmlAttributes) => + _macroPersistedFormat.Replace(persistedContent, match => { - return MacroPersistedFormat.Replace(persistedContent, match => + if (match.Groups.Count >= 3) { - if (match.Groups.Count >= 3) + //
+ var alias = match.Groups[2].Value; + var sb = new StringBuilder("
htmlAttribute in htmlAttributes) { - //
- var alias = match.Groups[2].Value; - var sb = new StringBuilder("
"); - sb.Append(""); - sb.Append(""); - sb.Append("Macro alias: "); - sb.Append(""); - sb.Append(alias); - sb.Append("
"); - return sb.ToString(); + sb.Append(" "); + sb.Append(htmlAttribute.Key); + sb.Append("=\""); + sb.Append(htmlAttribute.Value); + sb.Append("\""); } - //replace with nothing if we couldn't find the syntax for whatever reason - return ""; - }); + + sb.AppendLine(">"); + sb.Append(""); + sb.Append(""); + sb.Append("Macro alias: "); + sb.Append(""); + sb.Append(alias); + sb.Append("
"); + return sb.ToString(); + } + + // replace with nothing if we couldn't find the syntax for whatever reason + return string.Empty; + }); + + /// + /// This formats the string content posted from a rich text editor that contains macro contents to be persisted. + /// + /// + /// + /// This is required because when editors are using the rte, the HTML that is contained in the editor might actually be + /// displaying + /// the entire macro content, when the data is submitted the editor will clear most of this data out but we'll still + /// need to parse it properly + /// and ensure the correct syntax is persisted to the db. + /// When a macro is inserted into the rte editor, the HTML will be: + /// {div class='umb-macro-holder'} + /// + /// This could be some macro content + /// {/div} + /// What this method will do is remove the {div} and parse out the commented special macro syntax: {?UMBRACO_MACRO + /// macroAlias=\"myMacro\" /} + /// since this is exactly how we need to persist it to the db. + /// + public static string FormatRichTextContentForPersistence(string rteContent) + { + if (string.IsNullOrEmpty(rteContent)) + { + return string.Empty; } - /// - /// This formats the string content posted from a rich text editor that contains macro contents to be persisted. - /// - /// - /// - /// - /// This is required because when editors are using the rte, the HTML that is contained in the editor might actually be displaying - /// the entire macro content, when the data is submitted the editor will clear most of this data out but we'll still need to parse it properly - /// and ensure the correct syntax is persisted to the db. - /// - /// When a macro is inserted into the rte editor, the HTML will be: - /// - /// {div class='umb-macro-holder'} - /// - /// This could be some macro content - /// {/div} - /// - /// What this method will do is remove the {div} and parse out the commented special macro syntax: {?UMBRACO_MACRO macroAlias=\"myMacro\" /} - /// since this is exactly how we need to persist it to the db. - /// - /// - public static string FormatRichTextContentForPersistence(string rteContent) + var html = new HtmlDocument(); + html.LoadHtml(rteContent); + + // get all the comment nodes we want + HtmlNodeCollection? commentNodes = html.DocumentNode.SelectNodes("//comment()[contains(., ' with the comment node itself. - foreach (var c in commentNodes) - { - var div = c.ParentNode; - var divContainer = div.ParentNode; - divContainer.ReplaceChild(c, div); - } - - var parsed = html.DocumentNode.OuterHtml; - - //now replace all the with nothing - return MacroRteContent.Replace(parsed, match => - { - if (match.Groups.Count >= 3) - { - //get the 3rd group which is the macro syntax - return match.Groups[2].Value; - } - //replace with nothing if we couldn't find the syntax for whatever reason - return string.Empty; - }); + // There are no macros found, just return the normal content + return rteContent; } - /// - /// This will accept a text block and search/parse it for macro markup. - /// When either a text block or a a macro is found, it will call the callback method. - /// - /// - /// - /// - /// - /// - /// This method simply parses the macro contents, it does not create a string or result, - /// this is up to the developer calling this method to implement this with the callbacks. - /// - public static void ParseMacros( - string text, - Action textFoundCallback, - Action> macroFoundCallback ) + // replace each containing parent
with the comment node itself. + foreach (HtmlNode? c in commentNodes) { - if (textFoundCallback == null) throw new ArgumentNullException("textFoundCallback"); - if (macroFoundCallback == null) throw new ArgumentNullException("macroFoundCallback"); + HtmlNode? div = c.ParentNode; + HtmlNode? divContainer = div.ParentNode; + divContainer.ReplaceChild(c, div); + } - string elementText = text; + var parsed = html.DocumentNode.OuterHtml; - var fieldResult = new StringBuilder(elementText); - - //NOTE: This is legacy code, this is definitely not the correct way to do a while loop! :) - var stop = false; - while (!stop) + // now replace all the with nothing + return _macroRteContent.Replace(parsed, match => + { + if (match.Groups.Count >= 3) { - var tagIndex = fieldResult.ToString().ToLower().IndexOf(" -1) + // get the 3rd group which is the macro syntax + return match.Groups[2].Value; + } + + // replace with nothing if we couldn't find the syntax for whatever reason + return string.Empty; + }); + } + + /// + /// This will accept a text block and search/parse it for macro markup. + /// When either a text block or a a macro is found, it will call the callback method. + /// + /// + /// + /// + /// + /// + /// This method simply parses the macro contents, it does not create a string or result, + /// this is up to the developer calling this method to implement this with the callbacks. + /// + public static void ParseMacros( + string text, + Action textFoundCallback, + Action> macroFoundCallback) + { + if (textFoundCallback == null) + { + throw new ArgumentNullException("textFoundCallback"); + } + + if (macroFoundCallback == null) + { + throw new ArgumentNullException("macroFoundCallback"); + } + + var elementText = text; + + var fieldResult = new StringBuilder(elementText); + + // NOTE: This is legacy code, this is definitely not the correct way to do a while loop! :) + var stop = false; + while (!stop) + { + var tagIndex = fieldResult.ToString().ToLower().IndexOf(" -1) + { + var tempElementContent = string.Empty; + + // text block found, call the call back method + textFoundCallback(fieldResult.ToString().Substring(0, tagIndex)); + + fieldResult.Remove(0, tagIndex); + + var tag = fieldResult.ToString().Substring(0, fieldResult.ToString().IndexOf(">", StringComparison.InvariantCulture) + 1); + Dictionary attributes = XmlHelper.GetAttributesFromElement(tag); + + // Check whether it's a single tag () or a tag with children (...) + if (tag.Substring(tag.Length - 2, 1) != "/" && tag.IndexOf(" ", StringComparison.InvariantCulture) > -1) { - var tempElementContent = ""; + var closingTag = ""; - //text block found, call the call back method - textFoundCallback(fieldResult.ToString().Substring(0, tagIndex)); - - fieldResult.Remove(0, tagIndex); - - var tag = fieldResult.ToString().Substring(0, fieldResult.ToString().IndexOf(">") + 1); - var attributes = XmlHelper.GetAttributesFromElement(tag); - - // Check whether it's a single tag () or a tag with children (...) - if (tag.Substring(tag.Length - 2, 1) != "/" && tag.IndexOf(" ") > -1) + // Tag with children are only used when a macro is inserted by the umbraco-editor, in the + // following format: "", so we + // need to delete extra information inserted which is the image-tag and the closing + // umbraco_macro tag + if (fieldResult.ToString().IndexOf(closingTag, StringComparison.InvariantCulture) > -1) { - string closingTag = ""; - // Tag with children are only used when a macro is inserted by the umbraco-editor, in the - // following format: "", so we - // need to delete extra information inserted which is the image-tag and the closing - // umbraco_macro tag - if (fieldResult.ToString().IndexOf(closingTag) > -1) - { - fieldResult.Remove(0, fieldResult.ToString().IndexOf(closingTag)); - } + fieldResult.Remove(0, fieldResult.ToString().IndexOf(closingTag, StringComparison.InvariantCulture)); } - - var macroAlias = attributes.ContainsKey("macroalias") ? attributes["macroalias"] : attributes["alias"]; - - //call the callback now that we have the macro parsed - macroFoundCallback(macroAlias, attributes); - - fieldResult.Remove(0, fieldResult.ToString().IndexOf(">") + 1); - fieldResult.Insert(0, tempElementContent); } - else - { - //text block found, call the call back method - textFoundCallback(fieldResult.ToString()); - stop = true; //break; - } + var macroAlias = attributes.ContainsKey("macroalias") ? attributes["macroalias"] : attributes["alias"]; + + // call the callback now that we have the macro parsed + macroFoundCallback(macroAlias, attributes); + + fieldResult.Remove(0, fieldResult.ToString().IndexOf(">", StringComparison.InvariantCulture) + 1); + fieldResult.Insert(0, tempElementContent); + } + else + { + // text block found, call the call back method + textFoundCallback(fieldResult.ToString()); + + stop = true; // break; } } } diff --git a/src/Umbraco.Infrastructure/Mail/EmailSender.cs b/src/Umbraco.Infrastructure/Mail/EmailSender.cs index 6f94942aed..742075656d 100644 --- a/src/Umbraco.Infrastructure/Mail/EmailSender.cs +++ b/src/Umbraco.Infrastructure/Mail/EmailSender.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; using System.Net.Mail; -using System.Threading.Tasks; using MailKit.Net.Smtp; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,147 +13,162 @@ using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.Extensions; +using SecureSocketOptions = MailKit.Security.SecureSocketOptions; using SmtpClient = MailKit.Net.Smtp.SmtpClient; -namespace Umbraco.Cms.Infrastructure.Mail +namespace Umbraco.Cms.Infrastructure.Mail; + +/// +/// A utility class for sending emails +/// +public class EmailSender : IEmailSender { - /// - /// A utility class for sending emails - /// - public class EmailSender : IEmailSender + // TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly bool _notificationHandlerRegistered; + private GlobalSettings _globalSettings; + + public EmailSender( + ILogger logger, + IOptionsMonitor globalSettings, + IEventAggregator eventAggregator) + : this(logger, globalSettings, eventAggregator, null, null) { - // TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! - private readonly IEventAggregator _eventAggregator; - private GlobalSettings _globalSettings; - private readonly bool _notificationHandlerRegistered; - private readonly ILogger _logger; + } - public EmailSender( - ILogger logger, - IOptionsMonitor globalSettings, - IEventAggregator eventAggregator) - : this(logger, globalSettings, eventAggregator, null, null) { } + public EmailSender( + ILogger logger, + IOptionsMonitor globalSettings, + IEventAggregator eventAggregator, + INotificationHandler? handler1, + INotificationAsyncHandler? handler2) + { + _logger = logger; + _eventAggregator = eventAggregator; + _globalSettings = globalSettings.CurrentValue; + _notificationHandlerRegistered = handler1 is not null || handler2 is not null; + globalSettings.OnChange(x => _globalSettings = x); + } - public EmailSender( - ILogger logger, - IOptionsMonitor globalSettings, - IEventAggregator eventAggregator, - INotificationHandler? handler1, - INotificationAsyncHandler? handler2) + /// + /// Sends the message async + /// + /// + public async Task SendAsync(EmailMessage message, string emailType) => + await SendAsyncInternal(message, emailType, false); + + public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => + await SendAsyncInternal(message, emailType, enableNotification); + + /// + /// Returns true if the application should be able to send a required application email + /// + /// + /// We assume this is possible if either an event handler is registered or an smtp server is configured + /// or a pickup directory location is configured + /// + public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured + || _globalSettings.IsPickupDirectoryLocationConfigured + || _notificationHandlerRegistered; + + private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification) + { + if (enableNotification) { - _logger = logger; - _eventAggregator = eventAggregator; - _globalSettings = globalSettings.CurrentValue; - _notificationHandlerRegistered = handler1 is not null || handler2 is not null; - globalSettings.OnChange(x => _globalSettings = x); - } + var notification = + new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From), emailType); + await _eventAggregator.PublishAsync(notification); - /// - /// Sends the message async - /// - /// - /// - public async Task SendAsync(EmailMessage message, string emailType) => await SendAsyncInternal(message, emailType, false); - - public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => - await SendAsyncInternal(message, emailType, enableNotification); - - private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification) - { - if (enableNotification) + // if a handler handled sending the email then don't continue. + if (notification.IsHandled) { - var notification = new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From), emailType); - await _eventAggregator.PublishAsync(notification); - - // if a handler handled sending the email then don't continue. - if (notification.IsHandled) - { - _logger.LogDebug("The email sending for {Subject} was handled by a notification handler", notification.Message.Subject); - return; - } - } - - if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) - { - _logger.LogDebug("Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", message.Subject); + _logger.LogDebug( + "The email sending for {Subject} was handled by a notification handler", + notification.Message.Subject); return; } - - if (_globalSettings.IsPickupDirectoryLocationConfigured && !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) - { - // The following code snippet is the recommended way to handle PickupDirectoryLocation. - // See more https://github.com/jstedfast/MailKit/blob/master/FAQ.md#q-how-can-i-send-email-to-a-specifiedpickupdirectory - do { - var path = Path.Combine(_globalSettings.Smtp.PickupDirectoryLocation!, Guid.NewGuid () + ".eml"); - Stream stream; - - try - { - stream = File.Open(path, FileMode.CreateNew); - } - catch (IOException) - { - if (File.Exists(path)) - { - continue; - } - throw; - } - - try { - using (stream) - { - using var filtered = new FilteredStream(stream); - filtered.Add(new SmtpDataFilter()); - - FormatOptions options = FormatOptions.Default.Clone(); - options.NewLineFormat = NewLineFormat.Dos; - - await message.ToMimeMessage(_globalSettings.Smtp.From).WriteToAsync(options, filtered); - filtered.Flush(); - return; - - } - } catch { - File.Delete(path); - throw; - } - } while (true); - } - - using var client = new SmtpClient(); - - await client.ConnectAsync(_globalSettings.Smtp!.Host, - _globalSettings.Smtp.Port, - (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - - if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) - { - await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); - } - - var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); - if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } - - await client.DisconnectAsync(true); } - /// - /// Returns true if the application should be able to send a required application email - /// - /// - /// We assume this is possible if either an event handler is registered or an smtp server is configured - /// or a pickup directory location is configured - /// - public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured - || _globalSettings.IsPickupDirectoryLocationConfigured - || _notificationHandlerRegistered; + if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) + { + _logger.LogDebug( + "Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", + message.Subject); + return; + } + + if (_globalSettings.IsPickupDirectoryLocationConfigured && + !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) + { + // The following code snippet is the recommended way to handle PickupDirectoryLocation. + // See more https://github.com/jstedfast/MailKit/blob/master/FAQ.md#q-how-can-i-send-email-to-a-specifiedpickupdirectory + do + { + var path = Path.Combine(_globalSettings.Smtp.PickupDirectoryLocation!, Guid.NewGuid() + ".eml"); + Stream stream; + + try + { + stream = File.Open(path, FileMode.CreateNew); + } + catch (IOException) + { + if (File.Exists(path)) + { + continue; + } + + throw; + } + + try + { + using (stream) + { + using var filtered = new FilteredStream(stream); + filtered.Add(new SmtpDataFilter()); + + FormatOptions options = FormatOptions.Default.Clone(); + options.NewLineFormat = NewLineFormat.Dos; + + await message.ToMimeMessage(_globalSettings.Smtp.From).WriteToAsync(options, filtered); + filtered.Flush(); + return; + } + } + catch + { + File.Delete(path); + throw; + } + } + while (true); + } + + using var client = new SmtpClient(); + + await client.ConnectAsync( + _globalSettings.Smtp!.Host, + _globalSettings.Smtp.Port, + (SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); + + if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && + !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) + { + await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); + } + + var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); + if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendAsync(mailMessage); + } + else + { + client.Send(mailMessage); + } + + await client.DisconnectAsync(true); } } diff --git a/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs b/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs index 1a34fe373c..7ef945bda8 100644 --- a/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs @@ -1,46 +1,57 @@ -using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Implements a json read converter for . +/// +internal class DashboardAccessRuleConverter : JsonReadConverter { - /// - /// Implements a json read converter for . - /// - internal class DashboardAccessRuleConverter : JsonReadConverter + /// + protected override IAccessRule Create(Type objectType, string path, JObject jObject) => new AccessRule(); + + /// + protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) { - /// - protected override IAccessRule Create(Type objectType, string path, JObject jObject) + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + if (!(target is AccessRule accessRule)) { - return new AccessRule(); + throw new PanicException("panic."); } - /// - protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) + GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); + GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); + GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); + + if (accessRule.Type == AccessRuleType.Unknown) { - // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) - - if (!(target is AccessRule accessRule)) - throw new PanicException("panic."); - - GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); - GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); - GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); - - if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined."); - } - - private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) - { - var token = jobject[name]; - if (token == null) return; - if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule."); - if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string."); - rule.Type = type; - rule.Value = token.Value(); + throw new InvalidOperationException("Rule is not defined."); } } + + private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) + { + JToken? token = jobject[name]; + if (token == null) + { + return; + } + + if (rule.Type != AccessRuleType.Unknown) + { + throw new InvalidOperationException("Multiple definition of a rule."); + } + + if (token.Type != JTokenType.String) + { + throw new InvalidOperationException("Rule value is not a string."); + } + + rule.Type = type; + rule.Value = token.Value(); + } } diff --git a/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs b/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs index aa10cd6943..6b79e718e7 100644 --- a/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs @@ -1,8 +1,5 @@ -using System; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -11,198 +8,224 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Provides a json read converter for in manifests. +/// +internal class DataEditorConverter : JsonReadConverter { + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private readonly IIOHelper _ioHelper; + private readonly IJsonSerializer _jsonSerializer; + private readonly IShortStringHelper _shortStringHelper; + private readonly ILocalizedTextService _textService; + private const string SupportsReadOnly = "supportsReadOnly"; + /// - /// Provides a json read converter for in manifests. + /// Initializes a new instance of the class. /// - internal class DataEditorConverter : JsonReadConverter + public DataEditorConverter( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + ILocalizedTextService textService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer) { - private readonly IDataValueEditorFactory _dataValueEditorFactory; - private readonly IIOHelper _ioHelper; - private readonly ILocalizedTextService _textService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IJsonSerializer _jsonSerializer; + _dataValueEditorFactory = dataValueEditorFactory; + _ioHelper = ioHelper; + _textService = textService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + } - /// - /// Initializes a new instance of the class. - /// - public DataEditorConverter( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - ILocalizedTextService textService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer) + /// + protected override IDataEditor Create(Type objectType, string path, JObject jobject) + { + // in PackageManifest, property editors are IConfiguredDataEditor[] whereas + // parameter editors are IDataEditor[] - both will end up here because we handle + // IDataEditor and IConfiguredDataEditor implements it, but we can check the + // type to figure out what to create + EditorType type = EditorType.PropertyValue; + + var isPropertyEditor = path.StartsWith("propertyEditors["); + + if (isPropertyEditor) { - _dataValueEditorFactory = dataValueEditorFactory; - _ioHelper = ioHelper; - _textService = textService; - _shortStringHelper = shortStringHelper; - _jsonSerializer = jsonSerializer; - } - - /// - protected override IDataEditor Create(Type objectType, string path, JObject jobject) - { - // in PackageManifest, property editors are IConfiguredDataEditor[] whereas - // parameter editors are IDataEditor[] - both will end up here because we handle - // IDataEditor and IConfiguredDataEditor implements it, but we can check the - // type to figure out what to create - - var type = EditorType.PropertyValue; - - var isPropertyEditor = path.StartsWith("propertyEditors["); - - if (isPropertyEditor) + // property editor + jobject["isPropertyEditor"] = JToken.FromObject(true); + if (jobject["isParameterEditor"] is JToken jToken && jToken.Value()) { - // property editor - jobject["isPropertyEditor"] = JToken.FromObject(true); - if (jobject["isParameterEditor"] is JToken jToken && jToken.Value()) - type |= EditorType.MacroParameter; + type |= EditorType.MacroParameter; } - else - { - // parameter editor - type = EditorType.MacroParameter; - } - - return new DataEditor(_dataValueEditorFactory, type); + } + else + { + // parameter editor + type = EditorType.MacroParameter; } - /// - protected override void Deserialize(JObject jobject, IDataEditor target, JsonSerializer serializer) + return new DataEditor(_dataValueEditorFactory, type); + } + + /// + protected override void Deserialize(JObject jobject, IDataEditor target, JsonSerializer serializer) + { + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + if (!(target is DataEditor dataEditor)) { - // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) - - if (!(target is DataEditor dataEditor)) - throw new Exception("panic."); - - if (jobject["isPropertyEditor"] is JToken jtoken && jtoken.Value()) - PrepareForPropertyEditor(jobject, dataEditor); - else - PrepareForParameterEditor(jobject, dataEditor); - - base.Deserialize(jobject, target, serializer); + throw new Exception("panic."); } - private void PrepareForPropertyEditor(JObject jobject, DataEditor target) + if (jobject["isPropertyEditor"] is JToken jtoken && jtoken.Value()) { - if (jobject["editor"] == null) - throw new InvalidOperationException("Missing 'editor' value."); + PrepareForPropertyEditor(jobject, dataEditor); + } + else + { + PrepareForParameterEditor(jobject, dataEditor); + } - // explicitly assign a value editor of type ValueEditor + base.Deserialize(jobject, target, serializer); + } + + private static JArray RewriteValidators(JObject validation) + { + var jarray = new JArray(); + + foreach (KeyValuePair v in validation) + { + var key = v.Key; + JToken? val = v.Value; + var jo = new JObject { { "type", key }, { "configuration", val } }; + jarray.Add(jo); + } + + return jarray; + } + + private void PrepareForPropertyEditor(JObject jobject, DataEditor target) + { + if (jobject["editor"] == null) + { + throw new InvalidOperationException("Missing 'editor' value."); + } + + if (jobject.Property(SupportsReadOnly) is null) + { + jobject[SupportsReadOnly] = false; + } + + // explicitly assign a value editor of type ValueEditor + // (else the deserializer will try to read it before setting it) + // (and besides it's an interface) + target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + + // in the manifest, validators are a simple dictionary eg + // { + // required: true, + // regex: '\\d*' + // } + // and we need to turn this into a list of IPropertyValidator + // so, rewrite the json structure accordingly + if (jobject["editor"]?["validation"] is JObject validation) + { + jobject["editor"]!["validation"] = RewriteValidators(validation); + } + + if (jobject["editor"]?["view"] is JValue view) + { + jobject["editor"]!["view"] = RewriteVirtualUrl(view); + } + + var prevalues = jobject["prevalues"] as JObject; + var defaultConfig = jobject["defaultConfig"] as JObject; + if (prevalues != null || defaultConfig != null) + { + // explicitly assign a configuration editor of type ConfigurationEditor // (else the deserializer will try to read it before setting it) // (and besides it's an interface) - target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + target.ExplicitConfigurationEditor = new ConfigurationEditor(); - // in the manifest, validators are a simple dictionary eg - // { - // required: true, - // regex: '\\d*' - // } - // and we need to turn this into a list of IPropertyValidator - // so, rewrite the json structure accordingly - if (jobject["editor"]?["validation"] is JObject validation) - jobject["editor"]!["validation"] = RewriteValidators(validation); - - if(jobject["editor"]?["view"] is JValue view) - jobject["editor"]!["view"] = RewriteVirtualUrl(view); - - var prevalues = jobject["prevalues"] as JObject; - var defaultConfig = jobject["defaultConfig"] as JObject; - if (prevalues != null || defaultConfig != null) + var config = new JObject(); + if (prevalues != null) { - // explicitly assign a configuration editor of type ConfigurationEditor - // (else the deserializer will try to read it before setting it) - // (and besides it's an interface) - target.ExplicitConfigurationEditor = new ConfigurationEditor(); + config = prevalues; - var config = new JObject(); - if (prevalues != null) + // see note about validators, above - same applies to field validators + if (config["fields"] is JArray jarray) { - config = prevalues; - // see note about validators, above - same applies to field validators - if (config["fields"] is JArray jarray) + foreach (JToken field in jarray) { - foreach (var field in jarray) + if (field["validation"] is JObject fvalidation) { - if (field["validation"] is JObject fvalidation) - field["validation"] = RewriteValidators(fvalidation); + field["validation"] = RewriteValidators(fvalidation); + } - if(field["view"] is JValue fview) - field["view"] = RewriteVirtualUrl(fview); + if (field["view"] is JValue fview) + { + field["view"] = RewriteVirtualUrl(fview); } } } - - // in the manifest, default configuration is at editor level - // move it down to configuration editor level so it can be deserialized properly - if (defaultConfig != null) - { - config["defaultConfig"] = defaultConfig; - jobject.Remove("defaultConfig"); - } - - // in the manifest, configuration is named 'prevalues', rename - // it is important to do this LAST - jobject["config"] = config; - jobject.Remove("prevalues"); } + + // in the manifest, default configuration is at editor level + // move it down to configuration editor level so it can be deserialized properly + if (defaultConfig != null) + { + config["defaultConfig"] = defaultConfig; + jobject.Remove("defaultConfig"); + } + + // in the manifest, configuration is named 'prevalues', rename + // it is important to do this LAST + jobject["config"] = config; + jobject.Remove("prevalues"); + } + } + + private string? RewriteVirtualUrl(JValue view) => _ioHelper.ResolveRelativeOrVirtualUrl(view.Value as string); + + private void PrepareForParameterEditor(JObject jobject, DataEditor target) + { + // in a manifest, a parameter editor looks like: + // + // { + // "alias": "...", + // "name": "...", + // "view": "...", + // "config": { "key1": "value1", "key2": "value2" ... } + // } + // + // the view is at top level, but should be down one level to be properly + // deserialized as a ParameterValueEditor property -> need to move it + if (jobject.Property("view") != null) + { + // explicitly assign a value editor of type ParameterValueEditor + target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + + // move the 'view' property + jobject["editor"] = new JObject { ["view"] = jobject["view"] }; + jobject.Property("view")?.Remove(); } - private string? RewriteVirtualUrl(JValue view) + if (jobject.Property(SupportsReadOnly) is null) { - return _ioHelper.ResolveRelativeOrVirtualUrl(view.Value as string); + jobject[SupportsReadOnly] = false; } - private void PrepareForParameterEditor(JObject jobject, DataEditor target) + // in the manifest, default configuration is named 'config', rename + if (jobject["config"] is JObject config) { - // in a manifest, a parameter editor looks like: - // - // { - // "alias": "...", - // "name": "...", - // "view": "...", - // "config": { "key1": "value1", "key2": "value2" ... } - // } - // - // the view is at top level, but should be down one level to be properly - // deserialized as a ParameterValueEditor property -> need to move it - - if (jobject.Property("view") != null) - { - // explicitly assign a value editor of type ParameterValueEditor - target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); - - // move the 'view' property - jobject["editor"] = new JObject { ["view"] = jobject["view"] }; - jobject.Property("view")?.Remove(); - } - - // in the manifest, default configuration is named 'config', rename - if (jobject["config"] is JObject config) - { - jobject["defaultConfig"] = config; - jobject.Remove("config"); - } - - if(jobject["editor"]?["view"] is JValue view) // We need to null check, if view do not exists, then editor do not exists - jobject["editor"]!["view"] = RewriteVirtualUrl(view); + jobject["defaultConfig"] = config; + jobject.Remove("config"); } - private static JArray RewriteValidators(JObject validation) + // We need to null check, if view do not exists, then editor do not exists + if (jobject["editor"]?["view"] is JValue view) { - var jarray = new JArray(); - - foreach (var v in validation) - { - var key = v.Key; - var val = v.Value; - var jo = new JObject { { "type", key }, { "configuration", val } }; - jarray.Add(jo); - } - - return jarray; + jobject["editor"]!["view"] = RewriteVirtualUrl(view); } } } diff --git a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs index bdf5e5c620..4dbd6abd40 100644 --- a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs +++ b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -14,248 +10,246 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Parses the Main.js file and replaces all tokens accordingly. +/// +public class ManifestParser : IManifestParser { + private static readonly string _utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); + + private readonly IAppPolicyCache _cache; + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private readonly ManifestFilterCollection _filters; + private readonly IHostingEnvironment _hostingEnvironment; + + private readonly IIOHelper _ioHelper; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILocalizedTextService _localizedTextService; + private readonly ILogger _logger; + private readonly IShortStringHelper _shortStringHelper; + private readonly ManifestValueValidatorCollection _validators; + + private string _path = null!; + /// - /// Parses the Main.js file and replaces all tokens accordingly. + /// Initializes a new instance of the class. /// - public class ManifestParser : IManifestParser + public ManifestParser( + AppCaches appCaches, + ManifestValueValidatorCollection validators, + ManifestFilterCollection filters, + ILogger logger, + IIOHelper ioHelper, + IHostingEnvironment hostingEnvironment, + IJsonSerializer jsonSerializer, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IDataValueEditorFactory dataValueEditorFactory) { - - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IJsonSerializer _jsonSerializer; - private readonly ILocalizedTextService _localizedTextService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IDataValueEditorFactory _dataValueEditorFactory; - private static readonly string s_utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); - - private readonly IAppPolicyCache _cache; - private readonly ILogger _logger; - private readonly ManifestValueValidatorCollection _validators; - private readonly ManifestFilterCollection _filters; - - private string _path = null!; - - /// - /// Initializes a new instance of the class. - /// - public ManifestParser( - AppCaches appCaches, - ManifestValueValidatorCollection validators, - ManifestFilterCollection filters, - ILogger logger, - IIOHelper ioHelper, - IHostingEnvironment hostingEnvironment, - IJsonSerializer jsonSerializer, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IDataValueEditorFactory dataValueEditorFactory) + if (appCaches == null) { - if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); - _cache = appCaches.RuntimeCache; - _validators = validators ?? throw new ArgumentNullException(nameof(validators)); - _filters = filters ?? throw new ArgumentNullException(nameof(filters)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - AppPluginsPath = "~/App_Plugins"; - _jsonSerializer = jsonSerializer; - _localizedTextService = localizedTextService; - _shortStringHelper = shortStringHelper; - _dataValueEditorFactory = dataValueEditorFactory; + throw new ArgumentNullException(nameof(appCaches)); } - public string AppPluginsPath + _cache = appCaches.RuntimeCache; + _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + _filters = filters ?? throw new ArgumentNullException(nameof(filters)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + AppPluginsPath = "~/App_Plugins"; + _jsonSerializer = jsonSerializer; + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _dataValueEditorFactory = dataValueEditorFactory; + } + + public string AppPluginsPath + { + get => _path; + set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; + } + + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + public CompositePackageManifest CombinedManifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => { - get => _path; - set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; + IEnumerable manifests = GetManifests(); + return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0))!; + + /// + /// Gets all manifests. + /// + public IEnumerable GetManifests() + { + var manifests = new List(); + + foreach (var path in GetManifestFiles()) + { + try + { + var text = File.ReadAllText(path); + text = TrimPreamble(text); + if (string.IsNullOrWhiteSpace(text)) + { + continue; + } + + PackageManifest manifest = ParseManifest(text); + manifest.Source = path; + manifests.Add(manifest); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to parse manifest at '{Path}', ignoring.", path); + } } - /// - /// Gets all manifests, merged into a single manifest object. - /// - /// - public CompositePackageManifest CombinedManifest - => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => - { - IEnumerable manifests = GetManifests(); - return MergeManifests(manifests); + _filters.Filter(manifests); - }, new TimeSpan(0, 4, 0))!; + return manifests; + } - /// - /// Gets all manifests. - /// - public IEnumerable GetManifests() + /// + /// Parses a manifest. + /// + public PackageManifest ParseManifest(string text) + { + if (text == null) { - var manifests = new List(); + throw new ArgumentNullException(nameof(text)); + } - foreach (var path in GetManifestFiles()) + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); + } + + PackageManifest? manifest = JsonConvert.DeserializeObject( + text, + new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer), + new ValueValidatorConverter(_validators), + new DashboardAccessRuleConverter()); + + // scripts and stylesheets are raw string, must process here + for (var i = 0; i < manifest!.Scripts.Length; i++) + { + manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; + } + + for (var i = 0; i < manifest.Stylesheets.Length; i++) + { + manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; + } + + foreach (ManifestContentAppDefinition contentApp in manifest.ContentApps) + { + contentApp.View = _ioHelper.ResolveRelativeOrVirtualUrl(contentApp.View); + } + + foreach (ManifestDashboard dashboard in manifest.Dashboards) + { + dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; + } + + foreach (GridEditor gridEditor in manifest.GridEditors) + { + gridEditor.View = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.View); + gridEditor.Render = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.Render); + } + + // add property editors that are also parameter editors, to the parameter editors list + // (the manifest format is kinda legacy) + var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); + if (ppEditors.Count > 0) + { + manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); + } + + return manifest; + } + + /// + /// Merges all manifests into one. + /// + private static CompositePackageManifest MergeManifests(IEnumerable manifests) + { + var scripts = new Dictionary>(); + var stylesheets = new Dictionary>(); + var propertyEditors = new List(); + var parameterEditors = new List(); + var gridEditors = new List(); + var contentApps = new List(); + var dashboards = new List(); + var sections = new List(); + + foreach (PackageManifest manifest in manifests) + { + if (!scripts.TryGetValue(manifest.BundleOptions, out List? scriptsPerBundleOption)) { - try - { - var text = File.ReadAllText(path); - text = TrimPreamble(text); - if (string.IsNullOrWhiteSpace(text)) - { - continue; - } - - PackageManifest manifest = ParseManifest(text); - manifest.Source = path; - manifests.Add(manifest); - } - catch (Exception e) - { - _logger.LogError(e, "Failed to parse manifest at '{Path}', ignoring.", path); - } + scriptsPerBundleOption = new List(); + scripts[manifest.BundleOptions] = scriptsPerBundleOption; } - _filters.Filter(manifests); + scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); - return manifests; + if (!stylesheets.TryGetValue(manifest.BundleOptions, out List? stylesPerBundleOption)) + { + stylesPerBundleOption = new List(); + stylesheets[manifest.BundleOptions] = stylesPerBundleOption; + } + + stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); + + propertyEditors.AddRange(manifest.PropertyEditors); + + parameterEditors.AddRange(manifest.ParameterEditors); + + gridEditors.AddRange(manifest.GridEditors); + + contentApps.AddRange(manifest.ContentApps); + + dashboards.AddRange(manifest.Dashboards); + + sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias, StringComparer.OrdinalIgnoreCase)); } - /// - /// Merges all manifests into one. - /// - private static CompositePackageManifest MergeManifests(IEnumerable manifests) + return new CompositePackageManifest( + propertyEditors, + parameterEditors, + gridEditors, + contentApps, + dashboards, + sections, + scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), + stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); + } + + private static string TrimPreamble(string text) + { + // strangely StartsWith(preamble) would always return true + if (text.Substring(0, 1) == _utf8Preamble) { - var scripts = new Dictionary>(); - var stylesheets = new Dictionary>(); - var propertyEditors = new List(); - var parameterEditors = new List(); - var gridEditors = new List(); - var contentApps = new List(); - var dashboards = new List(); - var sections = new List(); - - foreach (PackageManifest manifest in manifests) - { - if (manifest.Scripts != null) - { - if (!scripts.TryGetValue(manifest.BundleOptions, out List? scriptsPerBundleOption)) - { - scriptsPerBundleOption = new List(); - scripts[manifest.BundleOptions] = scriptsPerBundleOption; - } - - scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); - } - - if (manifest.Stylesheets != null) - { - if (!stylesheets.TryGetValue(manifest.BundleOptions, out List? stylesPerBundleOption)) - { - stylesPerBundleOption = new List(); - stylesheets[manifest.BundleOptions] = stylesPerBundleOption; - } - - stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); - } - - if (manifest.PropertyEditors != null) - { - propertyEditors.AddRange(manifest.PropertyEditors); - } - - if (manifest.ParameterEditors != null) - { - parameterEditors.AddRange(manifest.ParameterEditors); - } - - if (manifest.GridEditors != null) - { - gridEditors.AddRange(manifest.GridEditors); - } - - if (manifest.ContentApps != null) - { - contentApps.AddRange(manifest.ContentApps); - } - - if (manifest.Dashboards != null) - { - dashboards.AddRange(manifest.Dashboards); - } - - if (manifest.Sections != null) - { - sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias, StringComparer.OrdinalIgnoreCase)); - } - } - - return new CompositePackageManifest( - propertyEditors, - parameterEditors, - gridEditors, - contentApps, - dashboards, - sections, - scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), - stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); + text = text.Remove(0, _utf8Preamble.Length); } - // gets all manifest files (recursively) - private IEnumerable GetManifestFiles() + return text; + } + + // gets all manifest files (recursively) + private IEnumerable GetManifestFiles() + { + if (Directory.Exists(_path) == false) { - if (Directory.Exists(_path) == false) - { - return Array.Empty(); - } - - return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); + return Array.Empty(); } - private static string TrimPreamble(string text) - { - // strangely StartsWith(preamble) would always return true - if (text.Substring(0, 1) == s_utf8Preamble) - text = text.Remove(0, s_utf8Preamble.Length); - - return text; - } - - /// - /// Parses a manifest. - /// - public PackageManifest ParseManifest(string text) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); - - var manifest = JsonConvert.DeserializeObject(text, - new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer), - new ValueValidatorConverter(_validators), - new DashboardAccessRuleConverter()); - - // scripts and stylesheets are raw string, must process here - for (var i = 0; i < manifest!.Scripts.Length; i++) - manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; - for (var i = 0; i < manifest.Stylesheets.Length; i++) - manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; - foreach (var contentApp in manifest.ContentApps) - { - contentApp.View = _ioHelper.ResolveRelativeOrVirtualUrl(contentApp.View); - } - foreach (var dashboard in manifest.Dashboards) - { - dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; - } - foreach (var gridEditor in manifest.GridEditors) - { - gridEditor.View = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.View); - gridEditor.Render = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.Render); - } - - // add property editors that are also parameter editors, to the parameter editors list - // (the manifest format is kinda legacy) - var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); - if (ppEditors.Count > 0) - manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); - - return manifest; - } + return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); } } diff --git a/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs b/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs index 6d6483a8bb..64f81e7697 100644 --- a/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs @@ -1,34 +1,31 @@ -using System; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Implements a json read converter for . +/// +internal class ValueValidatorConverter : JsonReadConverter { + private readonly ManifestValueValidatorCollection _validators; + /// - /// Implements a json read converter for . + /// Initializes a new instance of the class. /// - internal class ValueValidatorConverter : JsonReadConverter + public ValueValidatorConverter(ManifestValueValidatorCollection validators) => _validators = validators; + + protected override IValueValidator Create(Type objectType, string path, JObject jObject) { - private readonly ManifestValueValidatorCollection _validators; - - /// - /// Initializes a new instance of the class. - /// - public ValueValidatorConverter(ManifestValueValidatorCollection validators) + var type = jObject["type"]?.Value(); + if (string.IsNullOrWhiteSpace(type)) { - _validators = validators; + throw new InvalidOperationException("Could not get the type of the validator."); } - protected override IValueValidator Create(Type objectType, string path, JObject jObject) - { - var type = jObject["type"]?.Value(); - if (string.IsNullOrWhiteSpace(type)) - throw new InvalidOperationException("Could not get the type of the validator."); + return _validators.GetByName(type); - return _validators.GetByName(type); - - // jObject["configuration"] is going to be deserialized in a Configuration property, if any - } + // jObject["configuration"] is going to be deserialized in a Configuration property, if any } } diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs index 7a73e9303f..09cfcf5aaf 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs @@ -1,492 +1,560 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Scoping; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Mapping +namespace Umbraco.Cms.Core.Mapping; + +// notes: +// AutoMapper maps null to empty arrays, lists, etc + +// TODO: +// when mapping from TSource, and no map is found, consider the actual source.GetType()? +// when mapping to TTarget, and no map is found, consider the actual target.GetType()? +// not sure we want to add magic to this simple mapper class, though + +/// +/// Umbraco Mapper. +/// +/// +/// +/// When a map is defined from TSource to TTarget, the mapper automatically knows how to map +/// from IEnumerable{TSource} to IEnumerable{TTarget} (using a List{TTarget}) and to TTarget[]. +/// +/// +/// When a map is defined from TSource to TTarget, the mapper automatically uses that map +/// for any source type that inherits from, or implements, TSource. +/// +/// +/// When a map is defined from TSource to TTarget, the mapper can map to TTarget exclusively +/// and cannot re-use that map for types that would inherit from, or implement, TTarget. +/// +/// +/// When using the Map{TSource, TTarget}(TSource source, ...) overloads, TSource is explicit. When +/// using the Map{TTarget}(object source, ...) TSource is defined as source.GetType(). +/// +/// In both cases, TTarget is explicit and not typeof(target). +/// +public class UmbracoMapper : IUmbracoMapper { - // notes: - // AutoMapper maps null to empty arrays, lists, etc + // note + // + // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary + // the inner dictionaries are never modified and therefore can be simple Dictionary + private readonly ConcurrentDictionary>> _ctors = + new(); - // TODO: - // when mapping from TSource, and no map is found, consider the actual source.GetType()? - // when mapping to TTarget, and no map is found, consider the actual target.GetType()? - // not sure we want to add magic to this simple mapper class, though + private readonly ConcurrentDictionary>> _maps = + new(); + + private readonly ICoreScopeProvider _scopeProvider; /// - /// Umbraco Mapper. + /// Initializes a new instance of the class. /// - /// - /// When a map is defined from TSource to TTarget, the mapper automatically knows how to map - /// from IEnumerable{TSource} to IEnumerable{TTarget} (using a List{TTarget}) and to TTarget[]. - /// When a map is defined from TSource to TTarget, the mapper automatically uses that map - /// for any source type that inherits from, or implements, TSource. - /// When a map is defined from TSource to TTarget, the mapper can map to TTarget exclusively - /// and cannot re-use that map for types that would inherit from, or implement, TTarget. - /// When using the Map{TSource, TTarget}(TSource source, ...) overloads, TSource is explicit. When - /// using the Map{TTarget}(object source, ...) TSource is defined as source.GetType(). - /// In both cases, TTarget is explicit and not typeof(target). - /// - public class UmbracoMapper : IUmbracoMapper + /// + /// + public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) { - // note - // - // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary - // the inner dictionaries are never modified and therefore can be simple Dictionary + _scopeProvider = scopeProvider; - private readonly ConcurrentDictionary>> _ctors - = new ConcurrentDictionary>>(); - - private readonly ConcurrentDictionary>> _maps - = new ConcurrentDictionary>>(); - - private readonly ICoreScopeProvider _scopeProvider; - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) + foreach (IMapDefinition profile in profiles) { - _scopeProvider = scopeProvider; + profile.DefineMaps(this); + } + } - foreach (var profile in profiles) - profile.DefineMaps(this); + #region Define + + private static TTarget ThrowCtor(TSource source, MapperContext context) + => throw new InvalidOperationException($"Don't know how to create {typeof(TTarget).FullName} instances."); + + private static void Identity(TSource source, TTarget target, MapperContext context) + { + } + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + public void Define() + => Define(ThrowCtor, Identity); + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A mapping method. + public void Define(Action map) + => Define(ThrowCtor, map); + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + public void Define(Func ctor) + => Define(ctor, Identity); + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + /// A mapping method. + public void Define( + Func ctor, + Action map) + { + Type sourceType = typeof(TSource); + Type targetType = typeof(TTarget); + + Dictionary> sourceCtors = DefineCtors(sourceType); + if (ctor != null) + { + sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; } - #region Define + Dictionary> sourceMaps = DefineMaps(sourceType); + sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); + } - private static TTarget ThrowCtor(TSource source, MapperContext context) - => throw new InvalidOperationException($"Don't know how to create {typeof(TTarget).FullName} instances."); + private Dictionary> DefineCtors(Type sourceType) => + _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); - private static void Identity(TSource source, TTarget target, MapperContext context) - { } + private Dictionary> DefineMaps(Type sourceType) => + _maps.GetOrAdd(sourceType, _ => new Dictionary>()); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - public void Define() - => Define(ThrowCtor, Identity); + #endregion - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A mapping method. - public void Define(Action map) - => Define(ThrowCtor, map); + #region Map - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - public void Define(Func ctor) - => Define(ctor, Identity); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(object? source) + => Map(source, new MapperContext(this)); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - /// A mapping method. - public void Define(Func ctor, Action map) + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget? Map(object? source, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, context); + } + + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + public TTarget? Map(object? source, MapperContext context) + => Map(source, source?.GetType(), context); + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(TSource? source) + => Map(source, new MapperContext(this)); + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget? Map(TSource source, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, context); + } + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + public TTarget? Map(TSource? source, MapperContext context) + => Map(source, typeof(TSource), context); + + private TTarget? Map(object? source, Type? sourceType, MapperContext context) + { + if (source == null) { - var sourceType = typeof(TSource); - var targetType = typeof(TTarget); - - var sourceCtors = DefineCtors(sourceType); - if (ctor != null) - sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; - - var sourceMaps = DefineMaps(sourceType); - sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); + return default; } - private Dictionary> DefineCtors(Type sourceType) + Type targetType = typeof(TTarget); + + Func? ctor = GetCtor(sourceType, targetType); + Action? map = GetMap(sourceType, targetType); + + // if there is a direct constructor, map + if (ctor != null && map != null) { - return _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); + var target = ctor(source, context); + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + { + map(source, target, context); + } + + return (TTarget)target; } - private Dictionary> DefineMaps(Type sourceType) + // otherwise, see if we can deal with enumerable + Type? ienumerableOfT = typeof(IEnumerable<>); + + bool IsIEnumerableOfT(Type? type) { - return _maps.GetOrAdd(sourceType, _ => new Dictionary>()); + return type is not null && + type.IsGenericType && + type.GenericTypeArguments.Length == 1 && + type.GetGenericTypeDefinition() == ienumerableOfT; } - #endregion + // try to get source as an IEnumerable + Type? sourceIEnumerable = IsIEnumerableOfT(sourceType) + ? sourceType + : sourceType?.GetInterfaces().FirstOrDefault(IsIEnumerableOfT); - #region Map - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(object? source) - => Map(source, new MapperContext(this)); - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget? Map(object? source, Action f) + // if source is an IEnumerable and target is T[] or IEnumerable, we can create a map + if (sourceIEnumerable != null && IsEnumerableOrArrayOfType(targetType)) { - var context = new MapperContext(this); - f(context); - return Map(source, context); - } + Type sourceGenericArg = sourceIEnumerable.GenericTypeArguments[0]; + Type? targetGenericArg = GetEnumerableOrArrayTypeArgument(targetType); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - public TTarget? Map(object? source, MapperContext context) - => Map(source, source?.GetType(), context); + ctor = GetCtor(sourceGenericArg, targetGenericArg); + map = GetMap(sourceGenericArg, targetGenericArg); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(TSource? source) - => Map(source, new MapperContext(this)); - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget? Map(TSource source, Action f) - { - var context = new MapperContext(this); - f(context); - return Map(source, context); - } - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - public TTarget? Map(TSource? source, MapperContext context) - => Map(source, typeof(TSource), context); - - private TTarget? Map(object? source, Type? sourceType, MapperContext context) - { - if (source == null) - return default; - - var targetType = typeof(TTarget); - - var ctor = GetCtor(sourceType, targetType); - var map = GetMap(sourceType, targetType); - - // if there is a direct constructor, map + // if there is a constructor for the underlying type, create & invoke the map if (ctor != null && map != null) { - var target = ctor(source, context); - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + // register (for next time) and do it now (for this time) + object NCtor(object s, MapperContext c) { - map(source, target, context); - } - return (TTarget)target; - } - - // otherwise, see if we can deal with enumerable - - var ienumerableOfT = typeof(IEnumerable<>); - - bool IsIEnumerableOfT(Type? type) => - type is not null && - type.IsGenericType && - type.GenericTypeArguments.Length == 1 && - type.GetGenericTypeDefinition() == ienumerableOfT; - - // try to get source as an IEnumerable - var sourceIEnumerable = IsIEnumerableOfT(sourceType) ? sourceType : sourceType?.GetInterfaces().FirstOrDefault(IsIEnumerableOfT); - - // if source is an IEnumerable and target is T[] or IEnumerable, we can create a map - if (sourceIEnumerable != null && IsEnumerableOrArrayOfType(targetType)) - { - var sourceGenericArg = sourceIEnumerable.GenericTypeArguments[0]; - var targetGenericArg = GetEnumerableOrArrayTypeArgument(targetType); - - ctor = GetCtor(sourceGenericArg, targetGenericArg); - map = GetMap(sourceGenericArg, targetGenericArg); - - // if there is a constructor for the underlying type, create & invoke the map - if (ctor != null && map != null) - { - // register (for next time) and do it now (for this time) - object NCtor(object s, MapperContext c) => MapEnumerableInternal((IEnumerable)s, targetGenericArg!, ctor, map, c)!; - DefineCtors(sourceType!)[targetType] = NCtor; - DefineMaps(sourceType!)[targetType] = Identity; - return (TTarget)NCtor(source, context); + return MapEnumerableInternal((IEnumerable)s, targetGenericArg!, ctor, map, c)!; } - throw new InvalidOperationException($"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg?.FullName}, so don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + DefineCtors(sourceType!)[targetType] = NCtor; + DefineMaps(sourceType!)[targetType] = Identity; + return (TTarget)NCtor(source, context); } - throw new InvalidOperationException($"Don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + throw new InvalidOperationException( + $"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg?.FullName}, so don't know how to map {sourceType?.FullName} to {targetType.FullName}."); } - private TTarget? MapEnumerableInternal(IEnumerable source, Type targetGenericArg, Func ctor, Action map, MapperContext context) + throw new InvalidOperationException($"Don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + } + + private TTarget? MapEnumerableInternal( + IEnumerable source, + Type targetGenericArg, + Func ctor, + Action map, + MapperContext context) + { + var targetList = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); + + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - var targetList = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); - - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + foreach (var sourceItem in source) { - foreach (var sourceItem in source) - { - var targetItem = ctor(sourceItem, context); - map(sourceItem, targetItem, context); - targetList?.Add(targetItem); - } + var targetItem = ctor(sourceItem, context); + map(sourceItem, targetItem, context); + targetList?.Add(targetItem); } - - object? target = targetList; - - if (typeof(TTarget).IsArray) - { - var elementType = typeof(TTarget).GetElementType(); - if (elementType == null) throw new PanicException("elementType == null which should never occur"); - var targetArray = Array.CreateInstance(elementType, targetList?.Count ?? 0); - targetList?.CopyTo(targetArray, 0); - target = targetArray; - } - - return (TTarget?)target; } - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// The target object. - public TTarget Map(TSource source, TTarget target) - => Map(source, target, new MapperContext(this)); + object? target = targetList; - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(TSource source, TTarget target, Action f) + if (typeof(TTarget).IsArray) { - var context = new MapperContext(this); - f(context); - return Map(source, target, context); + Type? elementType = typeof(TTarget).GetElementType(); + if (elementType == null) + { + throw new PanicException("elementType == null which should never occur"); + } + + var targetArray = Array.CreateInstance(elementType, targetList?.Count ?? 0); + targetList?.CopyTo(targetArray, 0); + target = targetArray; } - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context. - /// The target object. - public TTarget Map(TSource source, TTarget target, MapperContext context) + return (TTarget?)target; + } + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// The target object. + public TTarget Map(TSource source, TTarget target) + => Map(source, target, new MapperContext(this)); + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(TSource source, TTarget target, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, target, context); + } + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context. + /// The target object. + public TTarget Map(TSource source, TTarget target, MapperContext context) + { + Type sourceType = typeof(TSource); + Type targetType = typeof(TTarget); + + Action? map = GetMap(sourceType, targetType); + + // if there is a direct map, map + if (map != null) { - var sourceType = typeof(TSource); - var targetType = typeof(TTarget); - - var map = GetMap(sourceType, targetType); - - // if there is a direct map, map - if (map != null) + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) - { - map(source!, target!, context); - } - return target; + map(source!, target!, context); } - // we cannot really map to an existing enumerable - give up - - throw new InvalidOperationException($"Don't know how to map {typeof(TSource).FullName} to {typeof(TTarget).FullName}."); + return target; } - private Func? GetCtor(Type? sourceType, Type? targetType) + // we cannot really map to an existing enumerable - give up + throw new InvalidOperationException( + $"Don't know how to map {typeof(TSource).FullName} to {typeof(TTarget).FullName}."); + } + + private Func? GetCtor(Type? sourceType, Type? targetType) + { + if (sourceType is null || targetType is null) { - if (sourceType is null || targetType is null) - { - return null; - } - if (_ctors.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor)) - return ctor; - - // we *may* run this more than once but it does not matter - - ctor = null; - foreach (var (stype, sctors) in _ctors) - { - if (!stype.IsAssignableFrom(sourceType)) continue; - if (!sctors.TryGetValue(targetType, out ctor)) continue; - - sourceCtor = sctors; - break; - } - - if (ctor is null || sourceCtor is null) return null; - - _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => - { - // Add missing constructors - foreach (var c in sourceCtor) - { - if (!v.ContainsKey(c.Key)) - { - v.Add(c.Key, c.Value); - } - } - - return v; - }); - + return null; + } + if (_ctors.TryGetValue(sourceType, out Dictionary>? sourceCtor) && + sourceCtor.TryGetValue(targetType, out Func? ctor)) + { return ctor; } - private Action? GetMap(Type? sourceType, Type? targetType) + // we *may* run this more than once but it does not matter + ctor = null; + foreach ((Type stype, Dictionary> sctors) in _ctors) { - if (sourceType is null || targetType is null) + if (!stype.IsAssignableFrom(sourceType)) { - return null; - } - if (_maps.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map)) - return map; - - // we *may* run this more than once but it does not matter - - map = null; - foreach (var (stype, smap) in _maps) - { - if (!stype.IsAssignableFrom(sourceType)) continue; - - // TODO: consider looking for assignable types for target too? - if (!smap.TryGetValue(targetType, out map)) continue; - - sourceMap = smap; - break; + continue; } - if (map is null || sourceMap is null) return null; - - if (_maps.ContainsKey(sourceType)) + if (!sctors.TryGetValue(targetType, out ctor)) { - foreach (var m in sourceMap) + continue; + } + + sourceCtor = sctors; + break; + } + + if (ctor is null || sourceCtor is null) + { + return null; + } + + _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => + { + // Add missing constructors + foreach (KeyValuePair> c in sourceCtor) + { + if (!v.ContainsKey(c.Key)) { - if (!_maps[sourceType].TryGetValue(m.Key, out _)) - _maps[sourceType].Add(m.Key, m.Value); + v.Add(c.Key, c.Value); } } - else - _maps[sourceType] = sourceMap; + return v; + }); + + return ctor; + } + + private Action? GetMap(Type? sourceType, Type? targetType) + { + if (sourceType is null || targetType is null) + { + return null; + } + + if (_maps.TryGetValue(sourceType, out Dictionary>? sourceMap) && + sourceMap.TryGetValue(targetType, out Action? map)) + { return map; } - private static bool IsEnumerableOrArrayOfType(Type type) + // we *may* run this more than once but it does not matter + map = null; + foreach ((Type stype, Dictionary> smap) in _maps) { - if (type.IsArray && type.GetArrayRank() == 1) return true; - if (type.IsGenericType && type.GenericTypeArguments.Length == 1) return true; - return false; + if (!stype.IsAssignableFrom(sourceType)) + { + continue; + } + + // TODO: consider looking for assignable types for target too? + if (!smap.TryGetValue(targetType, out map)) + { + continue; + } + + sourceMap = smap; + break; } - private static Type? GetEnumerableOrArrayTypeArgument(Type type) + if (map is null || sourceMap is null) { - if (type.IsArray) return type.GetElementType(); - if (type.IsGenericType) return type.GenericTypeArguments[0]; - throw new PanicException($"Could not get enumerable or array type from {type}"); + return null; } - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source) + if (_maps.ContainsKey(sourceType)) { - return source - .Select(Map) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); + foreach (KeyValuePair> m in sourceMap) + { + if (!_maps[sourceType].TryGetValue(m.Key, out _)) + { + _maps[sourceType].Add(m.Key, m.Value); + } + } + } + else + { + _maps[sourceType] = sourceMap; } - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context preparation method. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source, Action f) - { - var context = new MapperContext(this); - f(context); - return source - .Select(x => Map(x, context)) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); - } - - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source, MapperContext context) - { - return source - .Select(x => Map(x, context)) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); - } - - #endregion + return map; } + + private static bool IsEnumerableOrArrayOfType(Type type) + { + if (type.IsArray && type.GetArrayRank() == 1) + { + return true; + } + + if (type.IsGenericType && type.GenericTypeArguments.Length == 1) + { + return true; + } + + return false; + } + + private static Type? GetEnumerableOrArrayTypeArgument(Type type) + { + if (type.IsArray) + { + return type.GetElementType(); + } + + if (type.IsGenericType) + { + return type.GenericTypeArguments[0]; + } + + throw new PanicException($"Could not get enumerable or array type from {type}"); + } + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source) => + source + .Select(Map) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context preparation method. + /// A list containing the target objects. + public List MapEnumerable( + IEnumerable source, + Action f) + { + var context = new MapperContext(this); + f(context); + return source + .Select(x => Map(x, context)) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + } + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source, MapperContext context) => + source + .Select(x => Map(x, context)) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs index 7881daa593..fbc2add152 100644 --- a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs +++ b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs @@ -1,71 +1,66 @@ -using System; -using System.IO; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using Umbraco.Cms.Core.Media; using Size = System.Drawing.Size; -namespace Umbraco.Cms.Infrastructure.Media +namespace Umbraco.Cms.Infrastructure.Media; + +internal class ImageSharpDimensionExtractor : IImageDimensionExtractor { - internal class ImageSharpDimensionExtractor : IImageDimensionExtractor + private readonly Configuration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public ImageSharpDimensionExtractor(Configuration configuration) + => _configuration = configuration; + + /// + /// Gets the dimensions of an image. + /// + /// A stream containing the image bytes. + /// + /// The dimension of the image. + /// + public Size? GetDimensions(Stream? stream) { - private readonly Configuration _configuration; + Size? size = null; - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - public ImageSharpDimensionExtractor(Configuration configuration) - => _configuration = configuration; - - /// - /// Gets the dimensions of an image. - /// - /// A stream containing the image bytes. - /// - /// The dimension of the image. - /// - public Size? GetDimensions(Stream? stream) + IImageInfo imageInfo = Image.Identify(_configuration, stream); + if (imageInfo != null) { - Size? size = null; - - IImageInfo imageInfo = Image.Identify(_configuration, stream); - if (imageInfo != null) - { - size = IsExifOrientationRotated(imageInfo) - ? new Size(imageInfo.Height, imageInfo.Width) - : new Size(imageInfo.Width, imageInfo.Height); - } - - return size; + size = IsExifOrientationRotated(imageInfo) + ? new Size(imageInfo.Height, imageInfo.Width) + : new Size(imageInfo.Width, imageInfo.Height); } - private static bool IsExifOrientationRotated(IImageInfo imageInfo) - => GetExifOrientation(imageInfo) switch - { - ExifOrientationMode.LeftTop + return size; + } + + private static bool IsExifOrientationRotated(IImageInfo imageInfo) + => GetExifOrientation(imageInfo) switch + { + ExifOrientationMode.LeftTop or ExifOrientationMode.RightTop or ExifOrientationMode.RightBottom or ExifOrientationMode.LeftBottom => true, - _ => false, - }; + _ => false, + }; - private static ushort GetExifOrientation(IImageInfo imageInfo) + private static ushort GetExifOrientation(IImageInfo imageInfo) + { + IExifValue? orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); + if (orientation is not null) { - IExifValue? orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); - if (orientation is not null) + if (orientation.DataType == ExifDataType.Short) { - if (orientation.DataType == ExifDataType.Short) - { - return orientation.Value; - } - else - { - return Convert.ToUInt16(orientation.Value); - } + return orientation.Value; } - return ExifOrientationMode.Unknown; + return Convert.ToUInt16(orientation.Value); } + + return ExifOrientationMode.Unknown; } } diff --git a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs index 9979da1e40..2e99770874 100644 --- a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs @@ -1,18 +1,17 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +public class ExecutedMigrationPlan { - public class ExecutedMigrationPlan + public ExecutedMigrationPlan(MigrationPlan plan, string initialState, string finalState) { - public ExecutedMigrationPlan(MigrationPlan plan, string initialState, string finalState) - { - Plan = plan; - InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState)); - FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); - } - - public MigrationPlan Plan { get; } - public string InitialState { get; } - public string FinalState { get; } + Plan = plan; + InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState)); + FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); } + + public MigrationPlan Plan { get; } + + public string InitialState { get; } + + public string FinalState { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs index fec6b5d0c1..5340eab203 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs @@ -1,25 +1,21 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter; + +/// +/// Implements . +/// +public class AlterBuilder : IAlterBuilder { - /// - /// Implements . - /// - public class AlterBuilder : IAlterBuilder + private readonly IMigrationContext _context; + + public AlterBuilder(IMigrationContext context) => _context = context; + + /// + public IAlterTableBuilder Table(string tableName) { - private readonly IMigrationContext _context; - - public AlterBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IAlterTableBuilder Table(string tableName) - { - var expression = new AlterTableExpression(_context) { TableName = tableName }; - return new AlterTableBuilder(_context, expression); - } + var expression = new AlterTableExpression(_context) { TableName = tableName }; + return new AlterTableBuilder(_context, expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs index 6d1bfe4561..f24810c62b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs @@ -1,25 +1,22 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterColumnExpression : MigrationExpressionBase { - public class AlterColumnExpression : MigrationExpressionBase - { + public AlterColumnExpression(IMigrationContext context) + : base(context) => + Column = new ColumnDefinition { ModificationType = ModificationType.Alter }; - public AlterColumnExpression(IMigrationContext context) - : base(context) - { - Column = new ColumnDefinition { ModificationType = ModificationType.Alter }; - } + public virtual string? SchemaName { get; set; } - public virtual string? SchemaName { get; set; } - public virtual string? TableName { get; set; } - public virtual ColumnDefinition Column { get; set; } + public virtual string? TableName { get; set; } - protected override string GetSql() - { - return string.Format(SqlSyntax.AlterColumn, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.Format(Column)); - } - } + public virtual ColumnDefinition Column { get; set; } + + protected override string GetSql() => + string.Format( + SqlSyntax.AlterColumn, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.Format(Column)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs index 18298c7378..00bf8bbb3b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterDefaultConstraintExpression : MigrationExpressionBase { - public class AlterDefaultConstraintExpression : MigrationExpressionBase + public AlterDefaultConstraintExpression(IMigrationContext context) + : base(context) { - public AlterDefaultConstraintExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - - public virtual string? ColumnName { get; set; } - - public virtual string? ConstraintName { get; set; } - - public virtual object? DefaultValue { get; set; } - - protected override string GetSql() - { - //NOTE Should probably investigate if Deleting a Default Constraint is different from deleting a 'regular' constraint - - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.GetQuotedName(ConstraintName)); - } } + + public virtual string? TableName { get; set; } + + public virtual string? ColumnName { get; set; } + + public virtual string? ConstraintName { get; set; } + + public virtual object? DefaultValue { get; set; } + + protected override string GetSql() => + + // NOTE Should probably investigate if Deleting a Default Constraint is different from deleting a 'regular' constraint + string.Format( + SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedName(ConstraintName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs index 9be5354590..2787553ab4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs @@ -1,16 +1,13 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterTableExpression : MigrationExpressionBase { - public class AlterTableExpression : MigrationExpressionBase + public AlterTableExpression(IMigrationContext context) + : base(context) { - public AlterTableExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - - protected override string GetSql() - { - return string.Empty; - } } + + public virtual string? TableName { get; set; } + + protected override string GetSql() => string.Empty; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs index 7f3bf080d4..b64c82dec8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter; + +/// +/// Builds an Alter expression. +/// +public interface IAlterBuilder : IFluentBuilder { /// - /// Builds an Alter expression. + /// Specifies the table to alter. /// - public interface IAlterBuilder : IFluentBuilder - { - /// - /// Specifies the table to alter. - /// - IAlterTableBuilder Table(string tableName); - } + IAlterTableBuilder Table(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index 199db34102..3dc5483266 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs @@ -1,279 +1,251 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public class AlterTableBuilder : ExpressionBuilderBase, + IAlterTableColumnTypeBuilder, + IAlterTableColumnOptionForeignKeyCascadeBuilder { - public class AlterTableBuilder : ExpressionBuilderBase, - IAlterTableColumnTypeBuilder, - IAlterTableColumnOptionForeignKeyCascadeBuilder + private readonly IMigrationContext _context; + + public AlterTableBuilder(IMigrationContext context, AlterTableExpression expression) + : base(expression) => + _context = context; + + public ColumnDefinition CurrentColumn { get; set; } = null!; + + public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + + public void Do() => Expression.Execute(); + + public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) { - private readonly IMigrationContext _context; + CurrentColumn.DefaultValue = method; + return this; + } - public AlterTableBuilder(IMigrationContext context, AlterTableExpression expression) - : base(expression) + public IAlterTableColumnOptionBuilder WithDefaultValue(object value) + { + if (CurrentColumn.ModificationType == ModificationType.Alter) { - _context = context; - } - - public void Do() => Expression.Execute(); - - public ColumnDefinition CurrentColumn { get; set; } = null!; - - public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; - - public override ColumnDefinition GetColumnForType() - { - return CurrentColumn; - } - - public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) - { - CurrentColumn.DefaultValue = method; - return this; - } - - public IAlterTableColumnOptionBuilder WithDefaultValue(object value) - { - if (CurrentColumn.ModificationType == ModificationType.Alter) + var dc = new AlterDefaultConstraintExpression(_context) { - var dc = new AlterDefaultConstraintExpression(_context) - { - TableName = Expression.TableName, - ColumnName = CurrentColumn.Name, - DefaultValue = value - }; - - Expression.Expressions.Add(dc); - } - - CurrentColumn.DefaultValue = value; - return this; - } - - public IAlterTableColumnOptionBuilder Identity() - { - CurrentColumn.IsIdentity = true; - return this; - } - - public IAlterTableColumnOptionBuilder Indexed() - { - return Indexed(null); - } - - public IAlterTableColumnOptionBuilder Indexed(string? indexName) - { - CurrentColumn.IsIndexed = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - TableName = Expression.TableName - }); - - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); - - Expression.Expressions.Add(index); - - return this; - } - - public IAlterTableColumnOptionBuilder PrimaryKey() - { - CurrentColumn.IsPrimaryKey = true; - - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - TableName = Expression.TableName, - Columns = new[] { CurrentColumn.Name } - } + TableName = Expression.TableName, + ColumnName = CurrentColumn.Name, + DefaultValue = value, }; - Expression.Expressions.Add(expression); - return this; + Expression.Expressions.Add(dc); } - public IAlterTableColumnOptionBuilder PrimaryKey(string primaryKeyName) - { - CurrentColumn.IsPrimaryKey = true; - CurrentColumn.PrimaryKeyName = primaryKeyName; + CurrentColumn.DefaultValue = value; + return this; + } - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + public IAlterTableColumnOptionBuilder Identity() + { + CurrentColumn.IsIdentity = true; + return this; + } + + public IAlterTableColumnOptionBuilder Indexed() => Indexed(null); + + public IAlterTableColumnOptionBuilder Indexed(string? indexName) + { + CurrentColumn.IsIndexed = true; + + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName }); + + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); + + Expression.Expressions.Add(index); + + return this; + } + + public IAlterTableColumnOptionBuilder PrimaryKey() + { + CurrentColumn.IsPrimaryKey = true; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { TableName = Expression.TableName, Columns = new[] { CurrentColumn.Name } }, + }; + Expression.Expressions.Add(expression); + + return this; + } + + public IAlterTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + CurrentColumn.IsPrimaryKey = true; + CurrentColumn.PrimaryKeyName = primaryKeyName; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { - Constraint = - { - ConstraintName = primaryKeyName, - TableName = Expression.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); + ConstraintName = primaryKeyName, + TableName = Expression.TableName, + Columns = new[] { CurrentColumn.Name } + }, + }; + Expression.Expressions.Add(expression); - return this; - } + return this; + } - public IAlterTableColumnOptionBuilder Nullable() - { - CurrentColumn.IsNullable = true; - return this; - } + public IAlterTableColumnOptionBuilder Nullable() + { + CurrentColumn.IsNullable = true; + return this; + } - public IAlterTableColumnOptionBuilder NotNullable() - { - CurrentColumn.IsNullable = false; - return this; - } + public IAlterTableColumnOptionBuilder NotNullable() + { + CurrentColumn.IsNullable = false; + return this; + } - public IAlterTableColumnOptionBuilder Unique() - { - return Unique(null); - } + public IAlterTableColumnOptionBuilder Unique() => Unique(null); - public IAlterTableColumnOptionBuilder Unique(string? indexName) - { - CurrentColumn.IsUnique = true; + public IAlterTableColumnOptionBuilder Unique(string? indexName) + { + CurrentColumn.IsUnique = true; - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered + IndexType = IndexTypes.UniqueNonClustered, }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder + ForeignKey(string primaryTableName, string primaryColumnName) => + ForeignKey(null, null, primaryTableName, primaryColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - CurrentColumn.IsForeignKey = true; + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey( + string? foreignKeyName, + string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + CurrentColumn.IsForeignKey = true; - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, PrimaryTableSchema = primaryTableSchema, - ForeignTable = Expression.TableName + ForeignTable = Expression.TableName, }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - CurrentColumn.IsForeignKey = true; - return this; - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + CurrentColumn.IsForeignKey = true; + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string foreignTableName, + string foreignColumnName) => ReferencedBy(null, null, foreignTableName, foreignColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string? foreignKeyName, + string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, ForeignTable = foreignTableName, - ForeignTableSchema = foreignTableSchema + ForeignTableSchema = foreignTableSchema, }); - fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } - - public IAlterTableColumnTypeBuilder AddColumn(string name) - { - var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Create }; - var createColumn = new CreateColumnExpression(_context) - { - Column = column, - TableName = Expression.TableName - }; - - CurrentColumn = column; - - Expression.Expressions.Add(createColumn); - return this; - } - - public IAlterTableColumnTypeBuilder AlterColumn(string name) - { - var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Alter }; - var alterColumn = new AlterColumnExpression(_context) - { - Column = column, - TableName = Expression.TableName - }; - - CurrentColumn = column; - - Expression.Expressions.Add(alterColumn); - return this; - } - - public IAlterTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - CurrentForeignKey.OnDelete = rule; - return this; - } - - public IAlterTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - CurrentForeignKey.OnUpdate = rule; - return this; - } - - public IAlterTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; } + + public IAlterTableColumnTypeBuilder AddColumn(string name) + { + var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Create }; + var createColumn = new CreateColumnExpression(_context) { Column = column, TableName = Expression.TableName }; + + CurrentColumn = column; + + Expression.Expressions.Add(createColumn); + return this; + } + + public IAlterTableColumnTypeBuilder AlterColumn(string name) + { + var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Alter }; + var alterColumn = new AlterColumnExpression(_context) { Column = column, TableName = Expression.TableName }; + + CurrentColumn = column; + + Expression.Expressions.Add(alterColumn); + return this; + } + + public IAlterTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + CurrentForeignKey.OnDelete = rule; + return this; + } + + public IAlterTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + CurrentForeignKey.OnUpdate = rule; + return this; + } + + public IAlterTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; + } + + public override ColumnDefinition GetColumnForType() => CurrentColumn; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs index 642e71757a..9d30d28a38 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +/// +/// Builds an Alter Table expression. +/// +public interface IAlterTableBuilder : IFluentBuilder { /// - /// Builds an Alter Table expression. + /// Specifies a column to add. /// - public interface IAlterTableBuilder : IFluentBuilder - { - /// - /// Specifies a column to add. - /// - IAlterTableColumnTypeBuilder AddColumn(string name); + IAlterTableColumnTypeBuilder AddColumn(string name); - /// - /// Specifies a column to alter. - /// - IAlterTableColumnTypeBuilder AlterColumn(string name); - } + /// + /// Specifies a column to alter. + /// + IAlterTableColumnTypeBuilder AlterColumn(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs index 3ace421b7b..8d05199507 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs @@ -1,8 +1,10 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnOptionBuilder : + IColumnOptionBuilder, + IAlterTableBuilder, + IExecutableBuilder { - public interface IAlterTableColumnOptionBuilder : IColumnOptionBuilder, - IAlterTableBuilder, IExecutableBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs index e42fcb266d..e76b4d726e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnOptionForeignKeyCascadeBuilder : + IAlterTableColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface IAlterTableColumnOptionForeignKeyCascadeBuilder : - IAlterTableColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs index 4768b52e7f..90c446468a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnTypeBuilder : IColumnTypeBuilder { - public interface IAlterTableColumnTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs index 5ec8c200e0..a388a971b4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs @@ -1,15 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public class ExecutableBuilder : IExecutableBuilder { - public class ExecutableBuilder : IExecutableBuilder - { - private readonly IMigrationExpression _expression; + private readonly IMigrationExpression _expression; - public ExecutableBuilder(IMigrationExpression expression) - { - _expression = expression; - } + public ExecutableBuilder(IMigrationExpression expression) => _expression = expression; - /// - public void Do() => _expression.Execute(); - } + /// + public void Do() => _expression.Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs index 8e701f845e..f539f9ec7d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs @@ -1,26 +1,27 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateColumnExpression : MigrationExpressionBase { - public class CreateColumnExpression : MigrationExpressionBase + public CreateColumnExpression(IMigrationContext context) + : base(context) => + Column = new ColumnDefinition { ModificationType = ModificationType.Create }; + + public string? TableName { get; set; } + + public ColumnDefinition Column { get; set; } + + protected override string GetSql() { - public CreateColumnExpression(IMigrationContext context) - : base(context) + if (string.IsNullOrEmpty(Column.TableName)) { - Column = new ColumnDefinition { ModificationType = ModificationType.Create }; + Column.TableName = TableName; } - public string? TableName { get; set; } - public ColumnDefinition Column { get; set; } - - protected override string GetSql() - { - if (string.IsNullOrEmpty(Column.TableName)) - Column.TableName = TableName; - - return string.Format(SqlSyntax.AddColumn, - SqlSyntax.GetQuotedTableName(Column.TableName), - SqlSyntax.Format(Column)); - } + return string.Format( + SqlSyntax.AddColumn, + SqlSyntax.GetQuotedTableName(Column.TableName), + SqlSyntax.Format(Column)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs index 511f4ac634..c012455e53 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs @@ -1,26 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateForeignKeyExpression : MigrationExpressionBase { - public class CreateForeignKeyExpression : MigrationExpressionBase - { - public CreateForeignKeyExpression(IMigrationContext context, ForeignKeyDefinition fkDef) - : base(context) - { - ForeignKey = fkDef; - } + public CreateForeignKeyExpression(IMigrationContext context, ForeignKeyDefinition fkDef) + : base(context) => + ForeignKey = fkDef; - public CreateForeignKeyExpression(IMigrationContext context) - : base(context) - { - ForeignKey = new ForeignKeyDefinition(); - } + public CreateForeignKeyExpression(IMigrationContext context) + : base(context) => + ForeignKey = new ForeignKeyDefinition(); - public ForeignKeyDefinition ForeignKey { get; set; } + public ForeignKeyDefinition ForeignKey { get; set; } - protected override string GetSql() - { - return SqlSyntax.Format(ForeignKey); - } - } + protected override string GetSql() => SqlSyntax.Format(ForeignKey); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs index cef5a8387a..837e805d49 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs @@ -1,27 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateIndexExpression : MigrationExpressionBase { - public class CreateIndexExpression : MigrationExpressionBase - { + public CreateIndexExpression(IMigrationContext context, IndexDefinition index) + : base(context) => + Index = index; - public CreateIndexExpression(IMigrationContext context, IndexDefinition index) - : base(context) - { - Index = index; - } + public CreateIndexExpression(IMigrationContext context) + : base(context) => + Index = new IndexDefinition(); - public CreateIndexExpression(IMigrationContext context) - : base(context) - { - Index = new IndexDefinition(); - } + public IndexDefinition Index { get; set; } - public IndexDefinition Index { get; set; } - - protected override string GetSql() - { - return SqlSyntax.Format(Index); - } - } + protected override string GetSql() => SqlSyntax.Format(Index); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs index 10057c0f6f..a1374f735f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs @@ -1,31 +1,46 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IColumnOptionBuilder : IFluentBuilder + where TNext : IFluentBuilder + where TNextFk : IFluentBuilder { - public interface IColumnOptionBuilder : IFluentBuilder - where TNext : IFluentBuilder - where TNextFk : IFluentBuilder - { - TNext WithDefault(SystemMethods method); - TNext WithDefaultValue(object value); - TNext Identity(); - TNext Indexed(); - TNext Indexed(string indexName); + TNext WithDefault(SystemMethods method); - TNext PrimaryKey(); - TNext PrimaryKey(string primaryKeyName); - TNext Nullable(); - TNext NotNullable(); - TNext Unique(); - TNext Unique(string indexName); + TNext WithDefaultValue(object value); - TNextFk ForeignKey(string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(string foreignKeyName, string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(string foreignKeyName, string primaryTableSchema, string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(); + TNext Identity(); - TNextFk ReferencedBy(string foreignTableName, string foreignColumnName); - TNextFk ReferencedBy(string foreignKeyName, string foreignTableName, string foreignColumnName); - TNextFk ReferencedBy(string foreignKeyName, string foreignTableSchema, string foreignTableName, string foreignColumnName); - } + TNext Indexed(); + + TNext Indexed(string indexName); + + TNext PrimaryKey(); + + TNext PrimaryKey(string primaryKeyName); + + TNext Nullable(); + + TNext NotNullable(); + + TNext Unique(); + + TNext Unique(string indexName); + + TNextFk ForeignKey(string primaryTableName, string primaryColumnName); + + TNextFk ForeignKey(string foreignKeyName, string primaryTableName, string primaryColumnName); + + TNextFk ForeignKey(string foreignKeyName, string primaryTableSchema, string primaryTableName, + string primaryColumnName); + + TNextFk ForeignKey(); + + TNextFk ReferencedBy(string foreignTableName, string foreignColumnName); + + TNextFk ReferencedBy(string foreignKeyName, string foreignTableName, string foreignColumnName); + + TNextFk ReferencedBy(string foreignKeyName, string foreignTableSchema, string foreignTableName, + string foreignColumnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs index 75d5512cee..6f910fcc7d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs @@ -1,35 +1,58 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +/// +/// Builds a column type expression. +/// +public interface IColumnTypeBuilder : IFluentBuilder + where TNext : IFluentBuilder { - /// - /// Builds a column type expression. - /// - public interface IColumnTypeBuilder : IFluentBuilder - where TNext : IFluentBuilder - { - TNext AsAnsiString(); - TNext AsAnsiString(int size); - TNext AsBinary(); - TNext AsBinary(int size); - TNext AsBoolean(); - TNext AsByte(); - TNext AsCurrency(); - TNext AsDate(); - TNext AsDateTime(); - TNext AsDecimal(); - TNext AsDecimal(int size, int precision); - TNext AsDouble(); - TNext AsGuid(); - TNext AsFixedLengthString(int size); - TNext AsFixedLengthAnsiString(int size); - TNext AsFloat(); - TNext AsInt16(); - TNext AsInt32(); - TNext AsInt64(); - TNext AsString(); - TNext AsString(int size); - TNext AsTime(); - TNext AsXml(); - TNext AsXml(int size); - TNext AsCustom(string customType); - } + TNext AsAnsiString(); + + TNext AsAnsiString(int size); + + TNext AsBinary(); + + TNext AsBinary(int size); + + TNext AsBoolean(); + + TNext AsByte(); + + TNext AsCurrency(); + + TNext AsDate(); + + TNext AsDateTime(); + + TNext AsDecimal(); + + TNext AsDecimal(int size, int precision); + + TNext AsDouble(); + + TNext AsGuid(); + + TNext AsFixedLengthString(int size); + + TNext AsFixedLengthAnsiString(int size); + + TNext AsFloat(); + + TNext AsInt16(); + + TNext AsInt32(); + + TNext AsInt64(); + + TNext AsString(); + + TNext AsString(int size); + + TNext AsTime(); + + TNext AsXml(); + + TNext AsXml(int size); + + TNext AsCustom(string customType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs index b5a29d801b..bff6789be9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IExecutableBuilder { - public interface IExecutableBuilder - { - /// - /// Executes. - /// - void Do(); - } + /// + /// Executes. + /// + void Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs index f566e5c4bb..07388a9156 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs @@ -1,24 +1,23 @@ -using System.Data; +using System.Data; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IForeignKeyCascadeBuilder : IFluentBuilder + where TNext : IFluentBuilder + where TNextFk : IFluentBuilder { - public interface IForeignKeyCascadeBuilder : IFluentBuilder - where TNext : IFluentBuilder - where TNextFk : IFluentBuilder - { - /// - /// Specifies a rule on deletes. - /// - TNextFk OnDelete(Rule rule); + /// + /// Specifies a rule on deletes. + /// + TNextFk OnDelete(Rule rule); - /// - /// Specifies a rule on updates. - /// - TNextFk OnUpdate(Rule rule); + /// + /// Specifies a rule on updates. + /// + TNextFk OnUpdate(Rule rule); - /// - /// Specifies a rule on deletes and updates. - /// - TNext OnDeleteOrUpdate(Rule rule); - } + /// + /// Specifies a rule on deletes and updates. + /// + TNext OnDeleteOrUpdate(Rule rule); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs index 07c11c57cd..90ae100b3a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs @@ -1,224 +1,200 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public class CreateColumnBuilder : ExpressionBuilderBase, + ICreateColumnOnTableBuilder, + ICreateColumnTypeBuilder, + ICreateColumnOptionForeignKeyCascadeBuilder { - public class CreateColumnBuilder : ExpressionBuilderBase, - ICreateColumnOnTableBuilder, - ICreateColumnTypeBuilder, - ICreateColumnOptionForeignKeyCascadeBuilder + private readonly IMigrationContext _context; + + public CreateColumnBuilder(IMigrationContext context, CreateColumnExpression expression) + : base(expression) => + _context = context; + + public ForeignKeyDefinition? CurrentForeignKey { get; set; } + + public ICreateColumnTypeBuilder OnTable(string name) { - private readonly IMigrationContext _context; + Expression.TableName = name; + return this; + } - public CreateColumnBuilder(IMigrationContext context, CreateColumnExpression expression) - : base(expression) - { - _context = context; - } + public void Do() => Expression.Execute(); - public void Do() => Expression.Execute(); + public ICreateColumnOptionBuilder WithDefault(SystemMethods method) + { + Expression.Column.DefaultValue = method; + return this; + } - public ForeignKeyDefinition? CurrentForeignKey { get; set; } + public ICreateColumnOptionBuilder WithDefaultValue(object value) + { + Expression.Column.DefaultValue = value; + return this; + } - public override ColumnDefinition GetColumnForType() - { - return Expression.Column; - } + public ICreateColumnOptionBuilder Identity() => Indexed(null); - public ICreateColumnTypeBuilder OnTable(string name) - { - Expression.TableName = name; - return this; - } + public ICreateColumnOptionBuilder Indexed() => Indexed(null); - public ICreateColumnOptionBuilder WithDefault(SystemMethods method) - { - Expression.Column.DefaultValue = method; - return this; - } + public ICreateColumnOptionBuilder Indexed(string? indexName) + { + Expression.Column.IsIndexed = true; - public ICreateColumnOptionBuilder WithDefaultValue(object value) - { - Expression.Column.DefaultValue = value; - return this; - } + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName }); - public ICreateColumnOptionBuilder Identity() - { - return Indexed(null); - } + index.Index.Columns.Add(new IndexColumnDefinition { Name = Expression.Column.Name }); - public ICreateColumnOptionBuilder Indexed() - { - return Indexed(null); - } + Expression.Expressions.Add(index); - public ICreateColumnOptionBuilder Indexed(string? indexName) - { - Expression.Column.IsIndexed = true; + return this; + } - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - TableName = Expression.TableName - }); + public ICreateColumnOptionBuilder PrimaryKey() + { + Expression.Column.IsPrimaryKey = true; + return this; + } - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = Expression.Column.Name - }); + public ICreateColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + Expression.Column.IsPrimaryKey = true; + Expression.Column.PrimaryKeyName = primaryKeyName; + return this; + } - Expression.Expressions.Add(index); + public ICreateColumnOptionBuilder Nullable() + { + Expression.Column.IsNullable = true; + return this; + } - return this; - } + public ICreateColumnOptionBuilder NotNullable() + { + Expression.Column.IsNullable = false; + return this; + } - public ICreateColumnOptionBuilder PrimaryKey() - { - Expression.Column.IsPrimaryKey = true; - return this; - } + public ICreateColumnOptionBuilder Unique() => Unique(null); - public ICreateColumnOptionBuilder PrimaryKey(string primaryKeyName) - { - Expression.Column.IsPrimaryKey = true; - Expression.Column.PrimaryKeyName = primaryKeyName; - return this; - } + public ICreateColumnOptionBuilder Unique(string? indexName) + { + Expression.Column.IsUnique = true; - public ICreateColumnOptionBuilder Nullable() - { - Expression.Column.IsNullable = true; - return this; - } - - public ICreateColumnOptionBuilder NotNullable() - { - Expression.Column.IsNullable = false; - return this; - } - - public ICreateColumnOptionBuilder Unique() - { - return Unique(null); - } - - public ICreateColumnOptionBuilder Unique(string? indexName) - { - Expression.Column.IsUnique = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered + IndexType = IndexTypes.UniqueNonClustered, }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = Expression.Column.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition { Name = Expression.Column.Name }); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) => + ForeignKey(null, null, primaryTableName, primaryColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - Expression.Column.IsForeignKey = true; + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + Expression.Column.IsForeignKey = true; - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, PrimaryTableSchema = primaryTableSchema, - ForeignTable = Expression.TableName + ForeignTable = Expression.TableName, }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(Expression.Column.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(Expression.Column.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - Expression.Column.IsForeignKey = true; - return this; - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + Expression.Column.IsForeignKey = true; + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder + ReferencedBy(string foreignTableName, string foreignColumnName) => + ReferencedBy(null, null, foreignTableName, foreignColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, ForeignTable = foreignTableName, - ForeignTableSchema = foreignTableSchema + ForeignTableSchema = foreignTableSchema, }); - fk.ForeignKey.PrimaryColumns.Add(Expression.Column.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(Expression.Column.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } - - public ICreateColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - if (CurrentForeignKey is not null) - { - CurrentForeignKey.OnDelete = rule; - } - - return this; - } - - public ICreateColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - if (CurrentForeignKey is not null) - { - CurrentForeignKey.OnUpdate = rule; - } - - return this; - } - - public ICreateColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; } + + public ICreateColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + if (CurrentForeignKey is not null) + { + CurrentForeignKey.OnDelete = rule; + } + + return this; + } + + public ICreateColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + if (CurrentForeignKey is not null) + { + CurrentForeignKey.OnUpdate = rule; + } + + return this; + } + + public ICreateColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; + } + + public override ColumnDefinition GetColumnForType() => Expression.Column; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs index 982b495ac8..c6b515554b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs @@ -1,12 +1,11 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOnTableBuilder : IColumnTypeBuilder { - public interface ICreateColumnOnTableBuilder : IColumnTypeBuilder - { - /// - /// Specifies the name of the table. - /// - ICreateColumnTypeBuilder OnTable(string name); - } + /// + /// Specifies the name of the table. + /// + ICreateColumnTypeBuilder OnTable(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs index 9a4c2c647e..573fb46c95 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs @@ -1,8 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOptionBuilder : + IColumnOptionBuilder, + IExecutableBuilder { - public interface ICreateColumnOptionBuilder : IColumnOptionBuilder - , IExecutableBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs index 25e0d792c4..3bc5c1a66f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs @@ -1,8 +1,8 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOptionForeignKeyCascadeBuilder : ICreateColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface ICreateColumnOptionForeignKeyCascadeBuilder : ICreateColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs index f1177efad3..42379df6fc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnTypeBuilder : IColumnTypeBuilder { - public interface ICreateColumnTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs index f61d99f237..a7d16bfe11 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs @@ -1,36 +1,39 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public class CreateConstraintBuilder : ExpressionBuilderBase, + ICreateConstraintOnTableBuilder, + ICreateConstraintColumnsBuilder { - public class CreateConstraintBuilder : ExpressionBuilderBase, - ICreateConstraintOnTableBuilder, - ICreateConstraintColumnsBuilder + public CreateConstraintBuilder(CreateConstraintExpression expression) + : base(expression) { - public CreateConstraintBuilder(CreateConstraintExpression expression) - : base(expression) - { } + } - /// - public ICreateConstraintColumnsBuilder OnTable(string tableName) - { - Expression.Constraint.TableName = tableName; - return this; - } + /// + public IExecutableBuilder Column(string columnName) + { + Expression.Constraint.Columns.Add(columnName); + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder Column(string columnName) + /// + public IExecutableBuilder Columns(string[] columnNames) + { + foreach (var columnName in columnNames) { Expression.Constraint.Columns.Add(columnName); - return new ExecutableBuilder(Expression); } - /// - public IExecutableBuilder Columns(string[] columnNames) - { - foreach (var columnName in columnNames) - Expression.Constraint.Columns.Add(columnName); - return new ExecutableBuilder(Expression); - } + return new ExecutableBuilder(Expression); + } + + /// + public ICreateConstraintColumnsBuilder OnTable(string tableName) + { + Expression.Constraint.TableName = tableName; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs index cfc7568686..979d58f98a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs @@ -1,17 +1,16 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public interface ICreateConstraintColumnsBuilder : IFluentBuilder { - public interface ICreateConstraintColumnsBuilder : IFluentBuilder - { - /// - /// Specifies the constraint column. - /// - IExecutableBuilder Column(string columnName); + /// + /// Specifies the constraint column. + /// + IExecutableBuilder Column(string columnName); - /// - /// Specifies the constraint columns. - /// - IExecutableBuilder Columns(string[] columnNames); - } + /// + /// Specifies the constraint columns. + /// + IExecutableBuilder Columns(string[] columnNames); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs index 01d2da0cd1..dc14f5789c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public interface ICreateConstraintOnTableBuilder : IFluentBuilder { - public interface ICreateConstraintOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table name. - /// - ICreateConstraintColumnsBuilder OnTable(string tableName); - } + /// + /// Specifies the table name. + /// + ICreateConstraintColumnsBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs index b672b1e5d4..2ba0c435f1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; @@ -10,121 +9,112 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create; + +public class CreateBuilder : ICreateBuilder { - public class CreateBuilder : ICreateBuilder + private readonly IMigrationContext _context; + + public CreateBuilder(IMigrationContext context) => + _context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + public IExecutableBuilder Table(bool withoutKeysAndIndexes = false) => + new CreateTableOfDtoBuilder(_context) { TypeOfDto = typeof(TDto), WithoutKeysAndIndexes = withoutKeysAndIndexes }; + + /// + public IExecutableBuilder KeysAndIndexes() => + new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeof(TDto) }; + + /// + public IExecutableBuilder KeysAndIndexes(Type typeOfDto) => + new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeOfDto }; + + /// + public ICreateTableWithColumnBuilder Table(string tableName) { - private readonly IMigrationContext _context; + var expression = new CreateTableExpression(_context) { TableName = tableName }; + return new CreateTableBuilder(_context, expression); + } - public CreateBuilder(IMigrationContext context) - { - _context = context ?? throw new ArgumentNullException(nameof(context)); - } + /// + public ICreateColumnOnTableBuilder Column(string columnName) + { + var expression = new CreateColumnExpression(_context) { Column = { Name = columnName } }; + return new CreateColumnBuilder(_context, expression); + } - /// - public IExecutableBuilder Table(bool withoutKeysAndIndexes = false) - { - return new CreateTableOfDtoBuilder(_context) { TypeOfDto = typeof(TDto), WithoutKeysAndIndexes = withoutKeysAndIndexes }; - } + /// + public ICreateForeignKeyFromTableBuilder ForeignKey() + { + var expression = new CreateForeignKeyExpression(_context); + return new CreateForeignKeyBuilder(expression); + } - /// - public IExecutableBuilder KeysAndIndexes() - { - return new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeof(TDto) }; - } + /// + public ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName) + { + var expression = new CreateForeignKeyExpression(_context) { ForeignKey = { Name = foreignKeyName } }; + return new CreateForeignKeyBuilder(expression); + } - /// - public IExecutableBuilder KeysAndIndexes(Type typeOfDto) - { - return new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeOfDto }; - } + /// + public ICreateIndexForTableBuilder Index() + { + var expression = new CreateIndexExpression(_context); + return new CreateIndexBuilder(expression); + } - /// - public ICreateTableWithColumnBuilder Table(string tableName) - { - var expression = new CreateTableExpression(_context) { TableName = tableName }; - return new CreateTableBuilder(_context, expression); - } + /// + public ICreateIndexForTableBuilder Index(string indexName) + { + var expression = new CreateIndexExpression(_context) { Index = { Name = indexName } }; + return new CreateIndexBuilder(expression); + } - /// - public ICreateColumnOnTableBuilder Column(string columnName) - { - var expression = new CreateColumnExpression(_context) { Column = { Name = columnName } }; - return new CreateColumnBuilder(_context, expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey() => PrimaryKey(true); - /// - public ICreateForeignKeyFromTableBuilder ForeignKey() - { - var expression = new CreateForeignKeyExpression(_context); - return new CreateForeignKeyBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey(bool clustered) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); + expression.Constraint.IsPrimaryKeyClustered = clustered; + return new CreateConstraintBuilder(expression); + } - /// - public ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName) - { - var expression = new CreateForeignKeyExpression(_context) { ForeignKey = { Name = foreignKeyName } }; - return new CreateForeignKeyBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName) => PrimaryKey(primaryKeyName, true); - /// - public ICreateIndexForTableBuilder Index() - { - var expression = new CreateIndexExpression(_context); - return new CreateIndexBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); + expression.Constraint.ConstraintName = primaryKeyName; + expression.Constraint.IsPrimaryKeyClustered = clustered; + return new CreateConstraintBuilder(expression); + } - /// - public ICreateIndexForTableBuilder Index(string indexName) - { - var expression = new CreateIndexExpression(_context) { Index = { Name = indexName } }; - return new CreateIndexBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder UniqueConstraint() + { + var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); + return new CreateConstraintBuilder(expression); + } - /// - public ICreateConstraintOnTableBuilder PrimaryKey() => PrimaryKey(true); + /// + public ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); + expression.Constraint.ConstraintName = constraintName; + return new CreateConstraintBuilder(expression); + } - /// - public ICreateConstraintOnTableBuilder PrimaryKey(bool clustered) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); - expression.Constraint.IsPrimaryKeyClustered = clustered; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName) => PrimaryKey(primaryKeyName, true); - - /// - public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); - expression.Constraint.ConstraintName = primaryKeyName; - expression.Constraint.IsPrimaryKeyClustered = clustered; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder UniqueConstraint() - { - var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); - expression.Constraint.ConstraintName = constraintName; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder Constraint(string constraintName) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.NonUnique); - expression.Constraint.ConstraintName = constraintName; - return new CreateConstraintBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder Constraint(string constraintName) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.NonUnique); + expression.Constraint.ConstraintName = constraintName; + return new CreateConstraintBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs index 7440d6c837..f8ea3bde27 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs @@ -1,40 +1,41 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; + +public class CreateConstraintExpression : MigrationExpressionBase { - public class CreateConstraintExpression : MigrationExpressionBase + public CreateConstraintExpression(IMigrationContext context, ConstraintType constraint) + : base(context) => + Constraint = new ConstraintDefinition(constraint); + + public ConstraintDefinition Constraint { get; } + + protected override string GetSql() { - public CreateConstraintExpression(IMigrationContext context, ConstraintType constraint) - : base(context) + var constraintType = Constraint.IsPrimaryKeyConstraint ? "PRIMARY KEY" : "UNIQUE"; + + if (Constraint.IsPrimaryKeyConstraint && SqlSyntax.SupportsClustered()) { - Constraint = new ConstraintDefinition(constraint); + constraintType += Constraint.IsPrimaryKeyClustered ? " CLUSTERED" : " NONCLUSTERED"; } - public ConstraintDefinition Constraint { get; } - - protected override string GetSql() + if (Constraint.IsNonUniqueConstraint) { - var constraintType = (Constraint.IsPrimaryKeyConstraint) ? "PRIMARY KEY" : "UNIQUE"; - - if (Constraint.IsPrimaryKeyConstraint && SqlSyntax.SupportsClustered()) - constraintType += Constraint.IsPrimaryKeyClustered ? " CLUSTERED" : " NONCLUSTERED"; - - if (Constraint.IsNonUniqueConstraint) - constraintType = string.Empty; - - var columns = new string[Constraint.Columns.Count]; - - for (var i = 0; i < Constraint.Columns.Count; i++) - { - columns[i] = SqlSyntax.GetQuotedColumnName(Constraint.Columns.ElementAt(i)); - } - - return string.Format(SqlSyntax.CreateConstraint, - SqlSyntax.GetQuotedTableName(Constraint.TableName), - SqlSyntax.GetQuotedName(Constraint.ConstraintName), - constraintType, - string.Join(", ", columns)); + constraintType = string.Empty; } + + var columns = new string[Constraint.Columns.Count]; + + for (var i = 0; i < Constraint.Columns.Count; i++) + { + columns[i] = SqlSyntax.GetQuotedColumnName(Constraint.Columns.ElementAt(i)); + } + + return string.Format( + SqlSyntax.CreateConstraint, + SqlSyntax.GetQuotedTableName(Constraint.TableName), + SqlSyntax.GetQuotedName(Constraint.ConstraintName), + constraintType, + string.Join(", ", columns)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs index e7ed2faf53..82bc66a058 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs @@ -1,25 +1,30 @@ using System.Collections.Generic; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; + +public class CreateTableExpression : MigrationExpressionBase { - public class CreateTableExpression : MigrationExpressionBase + public CreateTableExpression(IMigrationContext context) + : base(context) => + Columns = new List(); + + public virtual string SchemaName { get; set; } = null!; + + public virtual string TableName { get; set; } = null!; + + public virtual IList Columns { get; set; } + + protected override string GetSql() { - public CreateTableExpression(IMigrationContext context) - : base(context) - { - Columns = new List(); - } + var foreignKeys = Expressions + .OfType() + .Select(x => x.ForeignKey) + .ToList(); - public virtual string SchemaName { get; set; } = null!; - public virtual string TableName { get; set; } = null!; - public virtual IList Columns { get; set; } + var table = new TableDefinition { Name = TableName, SchemaName = SchemaName, Columns = Columns, ForeignKeys = foreignKeys }; - protected override string GetSql() - { - var table = new TableDefinition { Name = TableName, SchemaName = SchemaName, Columns = Columns }; - - return string.Format(SqlSyntax.Format(table)); - } + return string.Format(SqlSyntax.Format(table)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs index 4a30a815a2..0d0f4e82bc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs @@ -1,87 +1,93 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public class CreateForeignKeyBuilder : ExpressionBuilderBase, + ICreateForeignKeyFromTableBuilder, + ICreateForeignKeyForeignColumnBuilder, + ICreateForeignKeyToTableBuilder, + ICreateForeignKeyPrimaryColumnBuilder, + ICreateForeignKeyCascadeBuilder { - public class CreateForeignKeyBuilder : ExpressionBuilderBase, - ICreateForeignKeyFromTableBuilder, - ICreateForeignKeyForeignColumnBuilder, - ICreateForeignKeyToTableBuilder, - ICreateForeignKeyPrimaryColumnBuilder, - ICreateForeignKeyCascadeBuilder + public CreateForeignKeyBuilder(CreateForeignKeyExpression expression) + : base(expression) { - public CreateForeignKeyBuilder(CreateForeignKeyExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public ICreateForeignKeyForeignColumnBuilder FromTable(string table) - { - Expression.ForeignKey.ForeignTable = table; - return this; - } + /// + public ICreateForeignKeyCascadeBuilder OnDelete(Rule rule) + { + Expression.ForeignKey.OnDelete = rule; + return this; + } - /// - public ICreateForeignKeyToTableBuilder ForeignColumn(string column) + /// + public ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + Expression.ForeignKey.OnUpdate = rule; + return this; + } + + /// + public IExecutableBuilder OnDeleteOrUpdate(Rule rule) + { + Expression.ForeignKey.OnDelete = rule; + Expression.ForeignKey.OnUpdate = rule; + return new ExecutableBuilder(Expression); + } + + /// + public ICreateForeignKeyToTableBuilder ForeignColumn(string column) + { + Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } + + /// + public ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.ForeignColumns.Add(column); - return this; } - /// - public ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.ForeignColumns.Add(column); - return this; - } + return this; + } - /// - public ICreateForeignKeyPrimaryColumnBuilder ToTable(string table) - { - Expression.ForeignKey.PrimaryTable = table; - return this; - } + /// + public ICreateForeignKeyForeignColumnBuilder FromTable(string table) + { + Expression.ForeignKey.ForeignTable = table; + return this; + } - /// - public ICreateForeignKeyCascadeBuilder PrimaryColumn(string column) + /// + public ICreateForeignKeyCascadeBuilder PrimaryColumn(string column) + { + Expression.ForeignKey.PrimaryColumns.Add(column); + return this; + } + + /// + public ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.PrimaryColumns.Add(column); - return this; } - /// - public ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.PrimaryColumns.Add(column); - return this; - } + return this; + } - /// - public ICreateForeignKeyCascadeBuilder OnDelete(Rule rule) - { - Expression.ForeignKey.OnDelete = rule; - return this; - } - - /// - public ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - Expression.ForeignKey.OnUpdate = rule; - return this; - } - - /// - public IExecutableBuilder OnDeleteOrUpdate(Rule rule) - { - Expression.ForeignKey.OnDelete = rule; - Expression.ForeignKey.OnUpdate = rule; - return new ExecutableBuilder(Expression); - } + /// + public ICreateForeignKeyPrimaryColumnBuilder ToTable(string table) + { + Expression.ForeignKey.PrimaryTable = table; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs index 3b45404b85..2761cf0b1b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs @@ -1,12 +1,13 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyCascadeBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateForeignKeyCascadeBuilder : IFluentBuilder, IExecutableBuilder - { - ICreateForeignKeyCascadeBuilder OnDelete(Rule rule); - ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule); - IExecutableBuilder OnDeleteOrUpdate(Rule rule); - } + ICreateForeignKeyCascadeBuilder OnDelete(Rule rule); + + ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule); + + IExecutableBuilder OnDeleteOrUpdate(Rule rule); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs index 8f37b40487..321b605693 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyForeignColumnBuilder : IFluentBuilder { - public interface ICreateForeignKeyForeignColumnBuilder : IFluentBuilder - { - ICreateForeignKeyToTableBuilder ForeignColumn(string column); - ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns); - } + ICreateForeignKeyToTableBuilder ForeignColumn(string column); + + ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs index 941647e27d..a5b2cefe4a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyFromTableBuilder : IFluentBuilder { - public interface ICreateForeignKeyFromTableBuilder : IFluentBuilder - { - ICreateForeignKeyForeignColumnBuilder FromTable(string table); - } + ICreateForeignKeyForeignColumnBuilder FromTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs index 95d1346d0f..81a258cfa3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyPrimaryColumnBuilder : IFluentBuilder { - public interface ICreateForeignKeyPrimaryColumnBuilder : IFluentBuilder - { - ICreateForeignKeyCascadeBuilder PrimaryColumn(string column); - ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns); - } + ICreateForeignKeyCascadeBuilder PrimaryColumn(string column); + + ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs index 1ea49b1369..1d78c732f4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyToTableBuilder : IFluentBuilder { - public interface ICreateForeignKeyToTableBuilder : IFluentBuilder - { - ICreateForeignKeyPrimaryColumnBuilder ToTable(string table); - } + ICreateForeignKeyPrimaryColumnBuilder ToTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs index d01326eb0d..80637cb870 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; @@ -6,91 +5,90 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create; + +/// +/// Builds a Create expression. +/// +public interface ICreateBuilder : IFluentBuilder { /// - /// Builds a Create expression. + /// Builds a Create Table expression, and executes. /// - public interface ICreateBuilder : IFluentBuilder - { - /// - /// Builds a Create Table expression, and executes. - /// - IExecutableBuilder Table(bool withoutKeysAndIndexes = false); + IExecutableBuilder Table(bool withoutKeysAndIndexes = false); - /// - /// Builds a Create Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(); + /// + /// Builds a Create Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(); - /// - /// Builds a Create Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(Type typeOfDto); + /// + /// Builds a Create Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(Type typeOfDto); - /// - /// Builds a Create Table expression. - /// - ICreateTableWithColumnBuilder Table(string tableName); + /// + /// Builds a Create Table expression. + /// + ICreateTableWithColumnBuilder Table(string tableName); - /// - /// Builds a Create Column expression. - /// - ICreateColumnOnTableBuilder Column(string columnName); + /// + /// Builds a Create Column expression. + /// + ICreateColumnOnTableBuilder Column(string columnName); - /// - /// Builds a Create Foreign Key expression. - /// - ICreateForeignKeyFromTableBuilder ForeignKey(); + /// + /// Builds a Create Foreign Key expression. + /// + ICreateForeignKeyFromTableBuilder ForeignKey(); - /// - /// Builds a Create Foreign Key expression. - /// - ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName); + /// + /// Builds a Create Foreign Key expression. + /// + ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName); - /// - /// Builds a Create Index expression. - /// - ICreateIndexForTableBuilder Index(); + /// + /// Builds a Create Index expression. + /// + ICreateIndexForTableBuilder Index(); - /// - /// Builds a Create Index expression. - /// - ICreateIndexForTableBuilder Index(string indexName); + /// + /// Builds a Create Index expression. + /// + ICreateIndexForTableBuilder Index(string indexName); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(bool clustered); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(bool clustered); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered); - /// - /// Builds a Create Unique Constraint expression. - /// - ICreateConstraintOnTableBuilder UniqueConstraint(); + /// + /// Builds a Create Unique Constraint expression. + /// + ICreateConstraintOnTableBuilder UniqueConstraint(); - /// - /// Builds a Create Unique Constraint expression. - /// - ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName); + /// + /// Builds a Create Unique Constraint expression. + /// + ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName); - /// - /// Builds a Create Constraint expression. - /// - ICreateConstraintOnTableBuilder Constraint(string constraintName); - } + /// + /// Builds a Create Constraint expression. + /// + ICreateConstraintOnTableBuilder Constraint(string constraintName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs index a6024c19db..ea056afaff 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs @@ -1,94 +1,91 @@ -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public class CreateIndexBuilder : ExpressionBuilderBase, + ICreateIndexForTableBuilder, + ICreateIndexOnColumnBuilder, + ICreateIndexColumnOptionsBuilder, + ICreateIndexOptionsBuilder { - public class CreateIndexBuilder : ExpressionBuilderBase, - ICreateIndexForTableBuilder, - ICreateIndexOnColumnBuilder, - ICreateIndexColumnOptionsBuilder, - ICreateIndexOptionsBuilder + public CreateIndexBuilder(CreateIndexExpression expression) + : base(expression) { - public CreateIndexBuilder(CreateIndexExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + public IndexColumnDefinition? CurrentColumn { get; set; } - public IndexColumnDefinition? CurrentColumn { get; set; } - - /// - public ICreateIndexOnColumnBuilder OnTable(string tableName) + /// + public ICreateIndexOnColumnBuilder Ascending() + { + if (CurrentColumn is not null) { - Expression.Index.TableName = tableName; - return this; + CurrentColumn.Direction = Direction.Ascending; } - /// - public ICreateIndexColumnOptionsBuilder OnColumn(string columnName) + return this; + } + + /// + public ICreateIndexOnColumnBuilder Descending() + { + if (CurrentColumn is not null) { - CurrentColumn = new IndexColumnDefinition { Name = columnName }; - Expression.Index.Columns.Add(CurrentColumn); - return this; + CurrentColumn.Direction = Direction.Descending; } - /// - public ICreateIndexOptionsBuilder WithOptions() - { - return this; - } + return this; + } - /// - public ICreateIndexOnColumnBuilder Ascending() - { - if (CurrentColumn is not null) - { - CurrentColumn.Direction = Direction.Ascending; - } + /// + ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; + return this; + } - return this; - } + /// + public ICreateIndexOnColumnBuilder OnTable(string tableName) + { + Expression.Index.TableName = tableName; + return this; + } - /// - public ICreateIndexOnColumnBuilder Descending() - { - if (CurrentColumn is not null) - { - CurrentColumn.Direction = Direction.Descending; - } + /// + public void Do() => Expression.Execute(); - return this; - } + /// + public ICreateIndexColumnOptionsBuilder OnColumn(string columnName) + { + CurrentColumn = new IndexColumnDefinition { Name = columnName }; + Expression.Index.Columns.Add(CurrentColumn); + return this; + } - /// - ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() - { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - return this; - } + /// + public ICreateIndexOptionsBuilder WithOptions() => this; - /// - public ICreateIndexOnColumnBuilder NonClustered() - { - Expression.Index.IndexType = IndexTypes.NonClustered; - return this; - } + /// + public ICreateIndexOnColumnBuilder NonClustered() + { + Expression.Index.IndexType = IndexTypes.NonClustered; + return this; + } - /// - public ICreateIndexOnColumnBuilder Clustered() - { - Expression.Index.IndexType = IndexTypes.Clustered; - return this; - } + /// + public ICreateIndexOnColumnBuilder Clustered() + { + Expression.Index.IndexType = IndexTypes.Clustered; + return this; + } - /// - ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() - { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - return this; - } + /// + ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs index 037e9e71f5..3ea0b4cb7b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexColumnOptionsBuilder : IFluentBuilder { - public interface ICreateIndexColumnOptionsBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder Ascending(); - ICreateIndexOnColumnBuilder Descending(); - ICreateIndexOnColumnBuilder Unique(); - } + ICreateIndexOnColumnBuilder Ascending(); + + ICreateIndexOnColumnBuilder Descending(); + + ICreateIndexOnColumnBuilder Unique(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs index c74c5b546e..a26d4b41b0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexForTableBuilder : IFluentBuilder { - public interface ICreateIndexForTableBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder OnTable(string tableName); - } + ICreateIndexOnColumnBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs index 4981186fa3..62f0c7c3ea 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs @@ -1,17 +1,16 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexOnColumnBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateIndexOnColumnBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Specifies the index column. - /// - ICreateIndexColumnOptionsBuilder OnColumn(string columnName); + /// + /// Specifies the index column. + /// + ICreateIndexColumnOptionsBuilder OnColumn(string columnName); - /// - /// Specifies options. - /// - ICreateIndexOptionsBuilder WithOptions(); - } + /// + /// Specifies options. + /// + ICreateIndexOptionsBuilder WithOptions(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs index fc2e4f2a53..687f3b0cac 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexOptionsBuilder : IFluentBuilder { - public interface ICreateIndexOptionsBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder Unique(); - ICreateIndexOnColumnBuilder NonClustered(); - ICreateIndexOnColumnBuilder Clustered(); - } + ICreateIndexOnColumnBuilder Unique(); + + ICreateIndexOnColumnBuilder NonClustered(); + + ICreateIndexOnColumnBuilder Clustered(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs index 86c3dc537a..51c70b5dba 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs @@ -1,59 +1,61 @@ -using System; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes; + +public class CreateKeysAndIndexesBuilder : IExecutableBuilder { - public class CreateKeysAndIndexesBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + private readonly DatabaseType[] _supportedDatabaseTypes; + + public CreateKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - private readonly DatabaseType[] _supportedDatabaseTypes; - - public CreateKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) - { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } - - public Type? TypeOfDto { get; set; } - - /// - public void Do() - { - var syntax = _context.SqlContext.SqlSyntax; - if (TypeOfDto is null) - { - return; - } - var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - - // note: of course we are creating the keys and indexes as per the DTO, so - // changing the DTO may break old migrations - or, better, these migrations - // should capture a copy of the DTO class that will not change - - ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); - foreach (var sql in syntax.Format(tableDefinition.Indexes)) - ExecuteSql(sql); - foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) - ExecuteSql(sql); - - // note: we do *not* create the DF_ default constraints - /* - foreach (var column in tableDefinition.Columns) - { - var sql = syntax.FormatDefaultConstraint(column); - if (!sql.IsNullOrWhiteSpace()) - ExecuteSql(sql); - } - */ - } - - private void ExecuteSql(string sql) - { - new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } - .Execute(); - } + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; } + + public Type? TypeOfDto { get; set; } + + /// + public void Do() + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + if (TypeOfDto is null) + { + return; + } + + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); + + // note: of course we are creating the keys and indexes as per the DTO, so + // changing the DTO may break old migrations - or, better, these migrations + // should capture a copy of the DTO class that will not change + ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); + foreach (var sql in syntax.Format(tableDefinition.Indexes)) + { + ExecuteSql(sql); + } + + foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) + { + ExecuteSql(sql); + } + + // note: we do *not* create the DF_ default constraints + /* + foreach (var column in tableDefinition.Columns) + { + var sql = syntax.FormatDefaultConstraint(column); + if (!sql.IsNullOrWhiteSpace()) + ExecuteSql(sql); + } + */ + } + + private void ExecuteSql(string sql) => + new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } + .Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs index 81e3a702ce..0d2970753b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs @@ -1,270 +1,259 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public class CreateTableBuilder : ExpressionBuilderBase, + ICreateTableColumnAsTypeBuilder, + ICreateTableColumnOptionForeignKeyCascadeBuilder { - public class CreateTableBuilder : ExpressionBuilderBase, - ICreateTableColumnAsTypeBuilder, - ICreateTableColumnOptionForeignKeyCascadeBuilder + private readonly IMigrationContext _context; + + public CreateTableBuilder(IMigrationContext context, CreateTableExpression expression) + : base(expression) => + _context = context; + + public ColumnDefinition CurrentColumn { get; set; } = null!; + + public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + + /// + public void Do() => Expression.Execute(); + + /// + public ICreateTableColumnAsTypeBuilder WithColumn(string name) { - private readonly IMigrationContext _context; - - public CreateTableBuilder(IMigrationContext context, CreateTableExpression expression) - : base(expression) + var column = new ColumnDefinition { - _context = context; - } + Name = name, + TableName = Expression.TableName, + ModificationType = ModificationType.Create, + }; + Expression.Columns.Add(column); + CurrentColumn = column; + return this; + } - /// - public void Do() => Expression.Execute(); + /// + public ICreateTableColumnOptionBuilder WithDefault(SystemMethods method) + { + CurrentColumn.DefaultValue = method; + return this; + } - public ColumnDefinition CurrentColumn { get; set; } = null!; + public ICreateTableColumnOptionBuilder WithDefaultValue(object value) + { + CurrentColumn.DefaultValue = value; + return this; + } - public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + /// + public ICreateTableColumnOptionBuilder Identity() + { + CurrentColumn.IsIdentity = true; + return this; + } - public override ColumnDefinition GetColumnForType() - { - return CurrentColumn; - } + /// + public ICreateTableColumnOptionBuilder Indexed() => Indexed(null); - /// - public ICreateTableColumnAsTypeBuilder WithColumn(string name) - { - var column = new ColumnDefinition { Name = name, TableName = Expression.TableName, ModificationType = ModificationType.Create }; - Expression.Columns.Add(column); - CurrentColumn = column; - return this; - } + /// + public ICreateTableColumnOptionBuilder Indexed(string? indexName) + { + CurrentColumn.IsIndexed = true; - /// - public ICreateTableColumnOptionBuilder WithDefault(SystemMethods method) - { - CurrentColumn.DefaultValue = method; - return this; - } - - public ICreateTableColumnOptionBuilder WithDefaultValue(object value) - { - CurrentColumn.DefaultValue = value; - return this; - } - - /// - public ICreateTableColumnOptionBuilder Identity() - { - CurrentColumn.IsIdentity = true; - return this; - } - - /// - public ICreateTableColumnOptionBuilder Indexed() - { - return Indexed(null); - } - - /// - public ICreateTableColumnOptionBuilder Indexed(string? indexName) - { - CurrentColumn.IsIndexed = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - SchemaName = Expression.SchemaName, - TableName = Expression.TableName - }); - - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); - - Expression.Expressions.Add(index); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder PrimaryKey() - { - CurrentColumn.IsPrimaryKey = true; - - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - TableName = CurrentColumn.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder PrimaryKey(string primaryKeyName) - { - CurrentColumn.IsPrimaryKey = true; - CurrentColumn.PrimaryKeyName = primaryKeyName; - - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - ConstraintName = primaryKeyName, - TableName = CurrentColumn.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder Nullable() - { - CurrentColumn.IsNullable = true; - return this; - } - - /// - public ICreateTableColumnOptionBuilder NotNullable() - { - CurrentColumn.IsNullable = false; - return this; - } - - /// - public ICreateTableColumnOptionBuilder Unique() - { - return Unique(null); - } - - /// - public ICreateTableColumnOptionBuilder Unique(string? indexName) - { - CurrentColumn.IsUnique = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, SchemaName = Expression.SchemaName, TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered }); - index.Index.Columns.Add(new IndexColumnDefinition + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); + + Expression.Expressions.Add(index); + + return this; + } + + /// + public ICreateTableColumnOptionBuilder PrimaryKey() + { + CurrentColumn.IsPrimaryKey = true; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { TableName = CurrentColumn.TableName, Columns = new[] { CurrentColumn.Name } }, + }; + Expression.Expressions.Add(expression); + + return this; + } + + /// + public ICreateTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + CurrentColumn.IsPrimaryKey = true; + CurrentColumn.PrimaryKeyName = primaryKeyName; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { - Name = CurrentColumn.Name + ConstraintName = primaryKeyName, + TableName = CurrentColumn.TableName, + Columns = new[] { CurrentColumn.Name } + }, + }; + Expression.Expressions.Add(expression); + + return this; + } + + /// + public ICreateTableColumnOptionBuilder Nullable() + { + CurrentColumn.IsNullable = true; + return this; + } + + /// + public ICreateTableColumnOptionBuilder NotNullable() + { + CurrentColumn.IsNullable = false; + return this; + } + + /// + public ICreateTableColumnOptionBuilder Unique() => Unique(null); + + /// + public ICreateTableColumnOptionBuilder Unique(string? indexName) + { + CurrentColumn.IsUnique = true; + + var index = new CreateIndexExpression( + _context, + new IndexDefinition + { + Name = indexName, + SchemaName = Expression.SchemaName, + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered, }); - Expression.Expressions.Add(index); + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); - return this; - } + Expression.Expressions.Add(index); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey( + string primaryTableName, + string primaryColumnName) => ForeignKey(null, null, primaryTableName, primaryColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - CurrentColumn.IsForeignKey = true; + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey( + string? foreignKeyName, + string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + CurrentColumn.IsForeignKey = true; + + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, PrimaryTableSchema = primaryTableSchema, ForeignTable = Expression.TableName, - ForeignTableSchema = Expression.SchemaName + ForeignTableSchema = Expression.SchemaName, }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - CurrentColumn.IsForeignKey = true; - return this; - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + CurrentColumn.IsForeignKey = true; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string foreignTableName, + string foreignColumnName) => ReferencedBy(null, null, foreignTableName, foreignColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string? foreignKeyName, + string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, PrimaryTableSchema = Expression.SchemaName, ForeignTable = foreignTableName, - ForeignTableSchema = foreignTableSchema + ForeignTableSchema = foreignTableSchema, }); - fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } - - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - CurrentForeignKey.OnDelete = rule; - return this; - } - - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - CurrentForeignKey.OnUpdate = rule; - return this; - } - - /// - public ICreateTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; } + + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + CurrentForeignKey.OnDelete = rule; + return this; + } + + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + CurrentForeignKey.OnUpdate = rule; + return this; + } + + /// + public ICreateTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; + } + + public override ColumnDefinition GetColumnForType() => CurrentColumn; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index 5aac9e90f7..c75d3fb07a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -1,46 +1,44 @@ -using System; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public class CreateTableOfDtoBuilder : IExecutableBuilder { - public class CreateTableOfDtoBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + + // TODO: This doesn't do anything. + private readonly DatabaseType[] _supportedDatabaseTypes; + + public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - - // TODO: This doesn't do anything. - private readonly DatabaseType[] _supportedDatabaseTypes; - - public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) - { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } - - public Type? TypeOfDto { get; set; } - - public bool WithoutKeysAndIndexes { get; set; } - - /// - public void Do() - { - var syntax = _context.SqlContext.SqlSyntax; - if (TypeOfDto is null) - { - return; - } - var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - - syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); - _context.BuildingExpression = false; - } - - private void ExecuteSql(string sql) - { - new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } - .Execute(); - } + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; } + + public Type? TypeOfDto { get; set; } + + public bool WithoutKeysAndIndexes { get; set; } + + /// + public void Do() + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + if (TypeOfDto is null) + { + return; + } + + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); + + syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); + _context.BuildingExpression = false; + } + + private void ExecuteSql(string sql) => + new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } + .Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs index dfbeacde35..fb5d069e3a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnAsTypeBuilder : IColumnTypeBuilder { - public interface ICreateTableColumnAsTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs index 9c3d877277..43c130cb09 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnOptionBuilder : + IColumnOptionBuilder, + ICreateTableWithColumnBuilder { - public interface ICreateTableColumnOptionBuilder : - IColumnOptionBuilder, - ICreateTableWithColumnBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs index 14d9369cfc..bc00242afb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnOptionForeignKeyCascadeBuilder : + ICreateTableColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface ICreateTableColumnOptionForeignKeyCascadeBuilder : - ICreateTableColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs index d913406387..1f16ce80c3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs @@ -1,9 +1,8 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableWithColumnBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateTableWithColumnBuilder : IFluentBuilder, IExecutableBuilder - { - ICreateTableColumnAsTypeBuilder WithColumn(string name); - } + ICreateTableColumnAsTypeBuilder WithColumn(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs index 50101f46a1..bd61efaa48 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs @@ -1,27 +1,27 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; + +public class DeleteColumnBuilder : ExpressionBuilderBase, + IDeleteColumnBuilder { - public class DeleteColumnBuilder : ExpressionBuilderBase, - IDeleteColumnBuilder + public DeleteColumnBuilder(DeleteColumnExpression expression) + : base(expression) { - public DeleteColumnBuilder(DeleteColumnExpression expression) - : base(expression) - { } + } - /// - public IExecutableBuilder FromTable(string tableName) - { - Expression.TableName = tableName; - return new ExecutableBuilder(Expression); - } + /// + public IExecutableBuilder FromTable(string tableName) + { + Expression.TableName = tableName; + return new ExecutableBuilder(Expression); + } - /// - public IDeleteColumnBuilder Column(string columnName) - { - Expression.ColumnNames.Add(columnName); - return this; - } + /// + public IDeleteColumnBuilder Column(string columnName) + { + Expression.ColumnNames.Add(columnName); + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs index 80755635ee..eaed24cfdf 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; + +/// +/// Builds a Delete Column expression. +/// +public interface IDeleteColumnBuilder : IFluentBuilder { /// - /// Builds a Delete Column expression. + /// Specifies the table of the column to delete. /// - public interface IDeleteColumnBuilder : IFluentBuilder - { - /// - /// Specifies the table of the column to delete. - /// - IExecutableBuilder FromTable(string tableName); + IExecutableBuilder FromTable(string tableName); - /// - /// Specifies the column to delete. - /// - IDeleteColumnBuilder Column(string columnName); - } + /// + /// Specifies the column to delete. + /// + IDeleteColumnBuilder Column(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs index 84e5393549..84287c265d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs @@ -1,20 +1,20 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint -{ - public class DeleteConstraintBuilder : ExpressionBuilderBase, - IDeleteConstraintBuilder - { - public DeleteConstraintBuilder(DeleteConstraintExpression expression) - : base(expression) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; - /// - public IExecutableBuilder FromTable(string tableName) - { - Expression.Constraint.TableName = tableName; - return new ExecutableBuilder(Expression); - } +public class DeleteConstraintBuilder : ExpressionBuilderBase, + IDeleteConstraintBuilder +{ + public DeleteConstraintBuilder(DeleteConstraintExpression expression) + : base(expression) + { + } + + /// + public IExecutableBuilder FromTable(string tableName) + { + Expression.Constraint.TableName = tableName; + return new ExecutableBuilder(Expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs index a3304f552d..7030848ceb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; + +/// +/// Builds a Delete Constraint expression. +/// +public interface IDeleteConstraintBuilder : IFluentBuilder { /// - /// Builds a Delete Constraint expression. + /// Specifies the table of the constraint to delete. /// - public interface IDeleteConstraintBuilder : IFluentBuilder - { - /// - /// Specifies the table of the constraint to delete. - /// - IExecutableBuilder FromTable(string tableName); - } + IExecutableBuilder FromTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs index 77d00b29f3..076239f9f9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs @@ -1,53 +1,52 @@ -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; + +public class DeleteDataBuilder : ExpressionBuilderBase, + IDeleteDataBuilder { - public class DeleteDataBuilder : ExpressionBuilderBase, - IDeleteDataBuilder + public DeleteDataBuilder(DeleteDataExpression expression) + : base(expression) { - public DeleteDataBuilder(DeleteDataExpression expression) - : base(expression) - { } + } - /// - public IExecutableBuilder IsNull(string columnName) + /// + public IExecutableBuilder IsNull(string columnName) + { + Expression.Rows.Add(new DeletionDataDefinition { new(columnName, null) }); + return this; + } + + /// + public IDeleteDataBuilder Row(object dataAsAnonymousType) + { + Expression.Rows.Add(GetData(dataAsAnonymousType)); + return this; + } + + /// + public IExecutableBuilder AllRows() + { + Expression.IsAllRows = true; + return this; + } + + /// + public void Do() => Expression.Execute(); + + private static DeletionDataDefinition GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + + var data = new DeletionDataDefinition(); + foreach (PropertyDescriptor property in properties) { - Expression.Rows.Add(new DeletionDataDefinition { new KeyValuePair(columnName, null) }); - return this; + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } - /// - public IDeleteDataBuilder Row(object dataAsAnonymousType) - { - Expression.Rows.Add(GetData(dataAsAnonymousType)); - return this; - } - - /// - public IExecutableBuilder AllRows() - { - Expression.IsAllRows = true; - return this; - } - - /// - public void Do() - { - Expression.Execute(); - } - - private static DeletionDataDefinition GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - - var data = new DeletionDataDefinition(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; - } + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs index 701d526d7d..1450de2c3c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs @@ -1,25 +1,24 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDataBuilder : IFluentBuilder, IExecutableBuilder { /// - /// Builds a Delete expression. + /// Specifies a row to be deleted. /// - public interface IDeleteDataBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Specifies a row to be deleted. - /// - IDeleteDataBuilder Row(object dataAsAnonymousType); + IDeleteDataBuilder Row(object dataAsAnonymousType); - /// - /// Specifies that all rows must be deleted. - /// - IExecutableBuilder AllRows(); + /// + /// Specifies that all rows must be deleted. + /// + IExecutableBuilder AllRows(); - /// - /// Specifies that rows with a specified column being null must be deleted. - /// - IExecutableBuilder IsNull(string columnName); - } + /// + /// Specifies that rows with a specified column being null must be deleted. + /// + IExecutableBuilder IsNull(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs index 7093256c5f..5003d50dbd 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs @@ -1,38 +1,38 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Implements , +/// . +/// +public class DeleteDefaultConstraintBuilder : ExpressionBuilderBase, + IDeleteDefaultConstraintOnTableBuilder, + IDeleteDefaultConstraintOnColumnBuilder { - /// - /// Implements , . - /// - public class DeleteDefaultConstraintBuilder : ExpressionBuilderBase, - IDeleteDefaultConstraintOnTableBuilder, - IDeleteDefaultConstraintOnColumnBuilder + private readonly IMigrationContext _context; + + public DeleteDefaultConstraintBuilder(IMigrationContext context, DeleteDefaultConstraintExpression expression) + : base(expression) => + _context = context; + + /// + public IExecutableBuilder OnColumn(string columnName) { - private readonly IMigrationContext _context; + Expression.ColumnName = columnName; + Expression.HasDefaultConstraint = _context.SqlContext.SqlSyntax.TryGetDefaultConstraint( + _context.Database, + Expression.TableName, columnName, out var constraintName); + Expression.ConstraintName = constraintName ?? string.Empty; - public DeleteDefaultConstraintBuilder(IMigrationContext context, DeleteDefaultConstraintExpression expression) - : base(expression) - { - _context = context; - } + return new ExecutableBuilder(Expression); + } - /// - public IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName) - { - Expression.TableName = tableName; - return this; - } - - /// - public IExecutableBuilder OnColumn(string columnName) - { - Expression.ColumnName = columnName; - Expression.HasDefaultConstraint = _context.SqlContext.SqlSyntax.TryGetDefaultConstraint(_context.Database, Expression.TableName, columnName, out var constraintName); - Expression.ConstraintName = constraintName ?? string.Empty; - - return new ExecutableBuilder(Expression); - } + /// + public IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName) + { + Expression.TableName = tableName; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs index dcc613e0fb..7fda9519d5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDefaultConstraintOnColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the column of the constraint to delete. /// - public interface IDeleteDefaultConstraintOnColumnBuilder : IFluentBuilder - { - /// - /// Specifies the column of the constraint to delete. - /// - IExecutableBuilder OnColumn(string columnName); - } + IExecutableBuilder OnColumn(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs index 4b8be9f3ee..67feb8601e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDefaultConstraintOnTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the constraint to delete. /// - public interface IDeleteDefaultConstraintOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the constraint to delete. - /// - IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName); - } + IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs index c8a3ed5d28..a139af96fa 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; @@ -9,109 +8,120 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete; + +public class DeleteBuilder : IDeleteBuilder { - public class DeleteBuilder : IDeleteBuilder + private readonly IMigrationContext _context; + + public DeleteBuilder(IMigrationContext context) => _context = context; + + /// + public IExecutableBuilder Table(string tableName) { - private readonly IMigrationContext _context; + var expression = new DeleteTableExpression(_context) { TableName = tableName }; + return new ExecutableBuilder(expression); + } - public DeleteBuilder(IMigrationContext context) + /// + public IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true) + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(typeof(TDto), syntax); + return KeysAndIndexes(tableDefinition.Name, local, foreign); + } + + /// + public IExecutableBuilder KeysAndIndexes(string? tableName, bool local = true, bool foreign = true) + { + if (tableName == null) { - _context = context; + throw new ArgumentNullException(nameof(tableName)); } - /// - public IExecutableBuilder Table(string tableName) + if (string.IsNullOrWhiteSpace(tableName)) { - var expression = new DeleteTableExpression(_context) { TableName = tableName }; - return new ExecutableBuilder(expression); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(tableName)); } - /// - public IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true) + return new DeleteKeysAndIndexesBuilder(_context) { - var syntax = _context.SqlContext.SqlSyntax; - var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(TDto), syntax); - return KeysAndIndexes(tableDefinition.Name, local, foreign); - } + TableName = tableName, + DeleteLocal = local, + DeleteForeign = foreign, + }; + } - /// - public IExecutableBuilder KeysAndIndexes(string? tableName, bool local = true, bool foreign = true) + /// + public IDeleteColumnBuilder Column(string columnName) + { + var expression = new DeleteColumnExpression(_context) { ColumnNames = { columnName } }; + return new DeleteColumnBuilder(expression); + } + + /// + public IDeleteForeignKeyFromTableBuilder ForeignKey() + { + var expression = new DeleteForeignKeyExpression(_context); + return new DeleteForeignKeyBuilder(expression); + } + + /// + public IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName) + { + var expression = new DeleteForeignKeyExpression(_context) { ForeignKey = { Name = foreignKeyName } }; + return new DeleteForeignKeyBuilder(expression); + } + + /// + public IDeleteDataBuilder FromTable(string tableName) + { + var expression = new DeleteDataExpression(_context) { TableName = tableName }; + return new DeleteDataBuilder(expression); + } + + /// + public IDeleteIndexForTableBuilder Index() + { + var expression = new DeleteIndexExpression(_context); + return new DeleteIndexBuilder(expression); + } + + /// + public IDeleteIndexForTableBuilder Index(string indexName) + { + var expression = new DeleteIndexExpression(_context) { Index = { Name = indexName } }; + return new DeleteIndexBuilder(expression); + } + + /// + public IDeleteConstraintBuilder PrimaryKey(string primaryKeyName) + { + var expression = new DeleteConstraintExpression(_context, ConstraintType.PrimaryKey) { - if (tableName == null) throw new ArgumentNullException(nameof(tableName)); - if (string.IsNullOrWhiteSpace(tableName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(tableName)); + Constraint = { ConstraintName = primaryKeyName }, + }; + return new DeleteConstraintBuilder(expression); + } - return new DeleteKeysAndIndexesBuilder(_context) { TableName = tableName, DeleteLocal = local, DeleteForeign = foreign }; - } - - /// - public IDeleteColumnBuilder Column(string columnName) + /// + public IDeleteConstraintBuilder UniqueConstraint(string constraintName) + { + var expression = new DeleteConstraintExpression(_context, ConstraintType.Unique) { - var expression = new DeleteColumnExpression(_context) {ColumnNames = {columnName}}; - return new DeleteColumnBuilder(expression); - } + Constraint = { ConstraintName = constraintName }, + }; + return new DeleteConstraintBuilder(expression); + } - /// - public IDeleteForeignKeyFromTableBuilder ForeignKey() - { - var expression = new DeleteForeignKeyExpression(_context); - return new DeleteForeignKeyBuilder(expression); - } - - /// - public IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName) - { - var expression = new DeleteForeignKeyExpression(_context) {ForeignKey = {Name = foreignKeyName}}; - return new DeleteForeignKeyBuilder(expression); - } - - /// - public IDeleteDataBuilder FromTable(string tableName) - { - var expression = new DeleteDataExpression(_context) { TableName = tableName }; - return new DeleteDataBuilder(expression); - } - - /// - public IDeleteIndexForTableBuilder Index() - { - var expression = new DeleteIndexExpression(_context); - return new DeleteIndexBuilder(expression); - } - - /// - public IDeleteIndexForTableBuilder Index(string indexName) - { - var expression = new DeleteIndexExpression(_context) { Index = { Name = indexName } }; - return new DeleteIndexBuilder(expression); - } - - /// - public IDeleteConstraintBuilder PrimaryKey(string primaryKeyName) - { - var expression = new DeleteConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = { ConstraintName = primaryKeyName } - }; - return new DeleteConstraintBuilder(expression); - } - - /// - public IDeleteConstraintBuilder UniqueConstraint(string constraintName) - { - var expression = new DeleteConstraintExpression(_context, ConstraintType.Unique) - { - Constraint = { ConstraintName = constraintName } - }; - return new DeleteConstraintBuilder(expression); - } - - /// - public IDeleteDefaultConstraintOnTableBuilder DefaultConstraint() - { - var expression = new DeleteDefaultConstraintExpression(_context); - return new DeleteDefaultConstraintBuilder(_context, expression); - } + /// + public IDeleteDefaultConstraintOnTableBuilder DefaultConstraint() + { + var expression = new DeleteDefaultConstraintExpression(_context); + return new DeleteDefaultConstraintBuilder(_context, expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs index 7cd93133e7..4c1373ab13 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs @@ -1,28 +1,27 @@ -using System.Collections.Generic; using System.Text; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteColumnExpression : MigrationExpressionBase { - public class DeleteColumnExpression : MigrationExpressionBase + public DeleteColumnExpression(IMigrationContext context) + : base(context) => + ColumnNames = new List(); + + public virtual string? TableName { get; set; } + + public ICollection ColumnNames { get; set; } + + protected override string GetSql() { - public DeleteColumnExpression(IMigrationContext context) - : base(context) + var stmts = new StringBuilder(); + foreach (var columnName in ColumnNames) { - ColumnNames = new List(); + stmts.AppendFormat(SqlSyntax.DropColumn, SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedColumnName(columnName)); + AppendStatementSeparator(stmts); } - public virtual string? TableName { get; set; } - public ICollection ColumnNames { get; set; } - - protected override string GetSql() - { - var stmts = new StringBuilder(); - foreach (var columnName in ColumnNames) - { - stmts.AppendFormat(SqlSyntax.DropColumn, SqlSyntax.GetQuotedTableName(TableName), SqlSyntax.GetQuotedColumnName(columnName)); - AppendStatementSeparator(stmts); - } - return stmts.ToString(); - } + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs index 73e17ba124..f9726b8c39 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs @@ -1,22 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteConstraintExpression : MigrationExpressionBase { - public class DeleteConstraintExpression : MigrationExpressionBase - { - public DeleteConstraintExpression(IMigrationContext context, ConstraintType type) - : base(context) - { - Constraint = new ConstraintDefinition(type); - } + public DeleteConstraintExpression(IMigrationContext context, ConstraintType type) + : base(context) => + Constraint = new ConstraintDefinition(type); - public ConstraintDefinition Constraint { get; } + public ConstraintDefinition Constraint { get; } - protected override string GetSql() - { - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(Constraint.TableName), - SqlSyntax.GetQuotedName(Constraint.ConstraintName)); - } - } + protected override string GetSql() => + string.Format( + SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(Constraint.TableName), + SqlSyntax.GetQuotedName(Constraint.ConstraintName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs index 3d57a77dc0..a45d60f889 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs @@ -1,38 +1,42 @@ -using System.Collections.Generic; -using System.Linq; using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteDataExpression : MigrationExpressionBase { - public class DeleteDataExpression : MigrationExpressionBase + public DeleteDataExpression(IMigrationContext context) + : base(context) { - public DeleteDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } - public virtual bool IsAllRows { get; set; } + public string? TableName { get; set; } - public List Rows { get; } = new List(); + public virtual bool IsAllRows { get; set; } - protected override string GetSql() + public List Rows { get; } = new(); + + protected override string GetSql() + { + if (IsAllRows) { - if (IsAllRows) - return string.Format(SqlSyntax.DeleteData, SqlSyntax.GetQuotedTableName(TableName), "(1=1)"); - - var stmts = new StringBuilder(); - foreach (var row in Rows) - { - var whereClauses = row.Select(kvp => $"{SqlSyntax.GetQuotedColumnName(kvp.Key)} {(kvp.Value == null ? "IS" : "=")} {GetQuotedValue(kvp.Value)}"); - - stmts.Append(string.Format(SqlSyntax.DeleteData, - SqlSyntax.GetQuotedTableName(TableName), - string.Join(" AND ", whereClauses))); - - AppendStatementSeparator(stmts); - } - return stmts.ToString(); + return string.Format(SqlSyntax.DeleteData, SqlSyntax.GetQuotedTableName(TableName), "(1=1)"); } + + var stmts = new StringBuilder(); + foreach (DeletionDataDefinition row in Rows) + { + IEnumerable whereClauses = row.Select(kvp => + $"{SqlSyntax.GetQuotedColumnName(kvp.Key)} {(kvp.Value == null ? "IS" : "=")} {GetQuotedValue(kvp.Value)}"); + + stmts.Append(string.Format( + SqlSyntax.DeleteData, + SqlSyntax.GetQuotedTableName(TableName), + string.Join(" AND ", whereClauses))); + + AppendStatementSeparator(stmts); + } + + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs index e653d0f6bf..aaa73729ef 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs @@ -1,24 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteDefaultConstraintExpression : MigrationExpressionBase { - public class DeleteDefaultConstraintExpression : MigrationExpressionBase + public DeleteDefaultConstraintExpression(IMigrationContext context) + : base(context) { - public DeleteDefaultConstraintExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - public virtual string? ColumnName { get; set; } - public virtual string? ConstraintName { get; set; } - public virtual bool HasDefaultConstraint { get; set; } - - protected override string GetSql() - { - return HasDefaultConstraint - ? string.Format(SqlSyntax.DeleteDefaultConstraint, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.GetQuotedColumnName(ColumnName), - SqlSyntax.GetQuotedName(ConstraintName)) - : string.Empty; - } } + + public virtual string? TableName { get; set; } + + public virtual string? ColumnName { get; set; } + + public virtual string? ConstraintName { get; set; } + + public virtual bool HasDefaultConstraint { get; set; } + + protected override string GetSql() => + HasDefaultConstraint + ? string.Format( + SqlSyntax.DeleteDefaultConstraint, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedColumnName(ColumnName), + SqlSyntax.GetQuotedName(ConstraintName)) + : string.Empty; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs index b7f670006e..2427d905a2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs @@ -1,32 +1,32 @@ -using System; -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteForeignKeyExpression : MigrationExpressionBase { - public class DeleteForeignKeyExpression : MigrationExpressionBase + public DeleteForeignKeyExpression(IMigrationContext context) + : base(context) => + ForeignKey = new ForeignKeyDefinition(); + + public ForeignKeyDefinition ForeignKey { get; set; } + + protected override string GetSql() { - public DeleteForeignKeyExpression(IMigrationContext context) - : base(context) + if (ForeignKey.ForeignTable == null) { - ForeignKey = new ForeignKeyDefinition(); + throw new ArgumentNullException( + "Table name not specified, ensure you have appended the OnTable extension. Format should be Delete.ForeignKey(KeyName).OnTable(TableName)"); } - public ForeignKeyDefinition ForeignKey { get; set; } - - protected override string GetSql() + if (string.IsNullOrEmpty(ForeignKey.Name)) { - if (ForeignKey.ForeignTable == null) - throw new ArgumentNullException("Table name not specified, ensure you have appended the OnTable extension. Format should be Delete.ForeignKey(KeyName).OnTable(TableName)"); - - if (string.IsNullOrEmpty(ForeignKey.Name)) - { - ForeignKey.Name = $"FK_{ForeignKey.ForeignTable}_{ForeignKey.PrimaryTable}_{ForeignKey.PrimaryColumns.First()}"; - } - - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(ForeignKey.ForeignTable), - SqlSyntax.GetQuotedName(ForeignKey.Name)); + ForeignKey.Name = + $"FK_{ForeignKey.ForeignTable}_{ForeignKey.PrimaryTable}_{ForeignKey.PrimaryColumns.First()}"; } + + return string.Format( + SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(ForeignKey.ForeignTable), + SqlSyntax.GetQuotedName(ForeignKey.Name)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs index dd3c41dd2c..c03e74bf67 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs @@ -1,28 +1,22 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteIndexExpression : MigrationExpressionBase { - public class DeleteIndexExpression : MigrationExpressionBase - { - public DeleteIndexExpression(IMigrationContext context) - : base(context) - { - Index = new IndexDefinition(); - } + public DeleteIndexExpression(IMigrationContext context) + : base(context) => + Index = new IndexDefinition(); - public DeleteIndexExpression(IMigrationContext context, IndexDefinition index) - : base(context) - { - Index = index; - } + public DeleteIndexExpression(IMigrationContext context, IndexDefinition index) + : base(context) => + Index = index; - public IndexDefinition Index { get; } + public IndexDefinition Index { get; } - protected override string GetSql() - { - return string.Format(SqlSyntax.DropIndex, - SqlSyntax.GetQuotedName(Index.Name), - SqlSyntax.GetQuotedTableName(Index.TableName)); - } - } + protected override string GetSql() => + string.Format( + SqlSyntax.DropIndex, + SqlSyntax.GetQuotedName(Index.Name), + SqlSyntax.GetQuotedTableName(Index.TableName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs index 75d453eb88..84487ef5f5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteTableExpression : MigrationExpressionBase { - public class DeleteTableExpression : MigrationExpressionBase + public DeleteTableExpression(IMigrationContext context) + : base(context) { - public DeleteTableExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - - protected override string GetSql() - { - return string.Format(SqlSyntax.DropTable, - SqlSyntax.GetQuotedTableName(TableName)); - } } + + public virtual string? TableName { get; set; } + + protected override string GetSql() => + string.Format( + SqlSyntax.DropTable, + SqlSyntax.GetQuotedTableName(TableName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs index 74bee1a440..b98cc3a389 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs @@ -1,72 +1,77 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Implements IDeleteForeignKey... +/// +public class DeleteForeignKeyBuilder : ExpressionBuilderBase, + IDeleteForeignKeyFromTableBuilder, + IDeleteForeignKeyForeignColumnBuilder, + IDeleteForeignKeyToTableBuilder, + IDeleteForeignKeyPrimaryColumnBuilder, + IDeleteForeignKeyOnTableBuilder { - /// - /// Implements IDeleteForeignKey... - /// - public class DeleteForeignKeyBuilder : ExpressionBuilderBase, - IDeleteForeignKeyFromTableBuilder, - IDeleteForeignKeyForeignColumnBuilder, - IDeleteForeignKeyToTableBuilder, - IDeleteForeignKeyPrimaryColumnBuilder, - IDeleteForeignKeyOnTableBuilder + public DeleteForeignKeyBuilder(DeleteForeignKeyExpression expression) + : base(expression) { - public DeleteForeignKeyBuilder(DeleteForeignKeyExpression expression) - : base(expression) - { } + } - /// - public IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName) - { - Expression.ForeignKey.ForeignTable = foreignTableName; - return this; - } + /// + public IDeleteForeignKeyToTableBuilder ForeignColumn(string column) + { + Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } - /// - public IDeleteForeignKeyToTableBuilder ForeignColumn(string column) + /// + public IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.ForeignColumns.Add(column); - return this; } - /// - public IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } - return this; - } + /// + public IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName) + { + Expression.ForeignKey.ForeignTable = foreignTableName; + return this; + } - /// - public IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table) - { - Expression.ForeignKey.PrimaryTable = table; - return this; - } + /// + public IExecutableBuilder OnTable(string foreignTableName) + { + Expression.ForeignKey.ForeignTable = foreignTableName; + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder PrimaryColumn(string column) + /// + public IExecutableBuilder PrimaryColumn(string column) + { + Expression.ForeignKey.PrimaryColumns.Add(column); + return new ExecutableBuilder(Expression); + } + + /// + public IExecutableBuilder PrimaryColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.PrimaryColumns.Add(column); - return new ExecutableBuilder(Expression); } - /// - public IExecutableBuilder PrimaryColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.PrimaryColumns.Add(column); - return new ExecutableBuilder(Expression); - } + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder OnTable(string foreignTableName) - { - Expression.ForeignKey.ForeignTable = foreignTableName; - return new ExecutableBuilder(Expression); - } + /// + public IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table) + { + Expression.ForeignKey.PrimaryTable = table; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs index 3b17700218..1c2c7c1d34 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyForeignColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the foreign column. /// - public interface IDeleteForeignKeyForeignColumnBuilder : IFluentBuilder - { - /// - /// Specifies the foreign column. - /// - IDeleteForeignKeyToTableBuilder ForeignColumn(string column); + IDeleteForeignKeyToTableBuilder ForeignColumn(string column); - /// - /// Specifies the foreign columns. - /// - IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns); - } + /// + /// Specifies the foreign columns. + /// + IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs index 6d422ad535..c10f184835 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyFromTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the source table of the foreign key. /// - public interface IDeleteForeignKeyFromTableBuilder : IFluentBuilder - { - /// - /// Specifies the source table of the foreign key. - /// - IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName); - } + IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs index 19dd14f36e..1a40fb2f02 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyOnTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the foreign key. /// - public interface IDeleteForeignKeyOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the foreign key. - /// - IExecutableBuilder OnTable(string foreignTableName); - } + IExecutableBuilder OnTable(string foreignTableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs index c44696b45d..8dcedb08d9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyPrimaryColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the target primary column. /// - public interface IDeleteForeignKeyPrimaryColumnBuilder : IFluentBuilder - { - /// - /// Specifies the target primary column. - /// - IExecutableBuilder PrimaryColumn(string column); + IExecutableBuilder PrimaryColumn(string column); - /// - /// Specifies the target primary columns. - /// - IExecutableBuilder PrimaryColumns(params string[] columns); - } + /// + /// Specifies the target primary columns. + /// + IExecutableBuilder PrimaryColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs index 6588b7a18a..6a3f3147a3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyToTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the target table of the foreign key. /// - public interface IDeleteForeignKeyToTableBuilder : IFluentBuilder - { - /// - /// Specifies the target table of the foreign key. - /// - IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table); - } + IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs index 0b8da10097..e2d06fd71f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; @@ -6,73 +6,72 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table to delete. /// - public interface IDeleteBuilder : IFluentBuilder - { - /// - /// Specifies the table to delete. - /// - IExecutableBuilder Table(string tableName); + IExecutableBuilder Table(string tableName); - /// - /// Builds a Delete Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true); + /// + /// Builds a Delete Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true); - /// - /// Builds a Delete Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true); + /// + /// Builds a Delete Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true); - /// - /// Specifies the column to delete. - /// - IDeleteColumnBuilder Column(string columnName); + /// + /// Specifies the column to delete. + /// + IDeleteColumnBuilder Column(string columnName); - /// - /// Specifies the foreign key to delete. - /// - IDeleteForeignKeyFromTableBuilder ForeignKey(); + /// + /// Specifies the foreign key to delete. + /// + IDeleteForeignKeyFromTableBuilder ForeignKey(); - /// - /// Specifies the foreign key to delete. - /// - IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName); + /// + /// Specifies the foreign key to delete. + /// + IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName); - /// - /// Specifies the table to delete data from. - /// - /// - /// - IDeleteDataBuilder FromTable(string tableName); + /// + /// Specifies the table to delete data from. + /// + /// + /// + IDeleteDataBuilder FromTable(string tableName); - /// - /// Specifies the index to delete. - /// - IDeleteIndexForTableBuilder Index(); + /// + /// Specifies the index to delete. + /// + IDeleteIndexForTableBuilder Index(); - /// - /// Specifies the index to delete. - /// - IDeleteIndexForTableBuilder Index(string indexName); + /// + /// Specifies the index to delete. + /// + IDeleteIndexForTableBuilder Index(string indexName); - /// - /// Specifies the primary key to delete. - /// - IDeleteConstraintBuilder PrimaryKey(string primaryKeyName); + /// + /// Specifies the primary key to delete. + /// + IDeleteConstraintBuilder PrimaryKey(string primaryKeyName); - /// - /// Specifies the unique constraint to delete. - /// - IDeleteConstraintBuilder UniqueConstraint(string constraintName); + /// + /// Specifies the unique constraint to delete. + /// + IDeleteConstraintBuilder UniqueConstraint(string constraintName); - /// - /// Specifies the default constraint to delete. - /// - IDeleteDefaultConstraintOnTableBuilder DefaultConstraint(); - } + /// + /// Specifies the default constraint to delete. + /// + IDeleteDefaultConstraintOnTableBuilder DefaultConstraint(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs index e55b1e3d8f..0e6c57c32e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs @@ -1,22 +1,22 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; + +public class DeleteIndexBuilder : ExpressionBuilderBase, + IDeleteIndexForTableBuilder, IExecutableBuilder { - public class DeleteIndexBuilder : ExpressionBuilderBase, - IDeleteIndexForTableBuilder, IExecutableBuilder + public DeleteIndexBuilder(DeleteIndexExpression expression) + : base(expression) { - public DeleteIndexBuilder(DeleteIndexExpression expression) - : base(expression) - { } - - /// - public void Do() => Expression.Execute(); - - public IExecutableBuilder OnTable(string tableName) - { - Expression.Index.TableName = tableName; - return this; - } } + + public IExecutableBuilder OnTable(string tableName) + { + Expression.Index.TableName = tableName; + return this; + } + + /// + public void Do() => Expression.Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs index f99e0d1ea0..d649cd502b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteIndexForTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the index to delete. /// - public interface IDeleteIndexForTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the index to delete. - /// - IExecutableBuilder OnTable(string tableName); - } + IExecutableBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 90ade43d6d..ffffb58f9e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -1,102 +1,109 @@ -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes; + +/// +/// +/// Assuming we stick with the current migrations setup this will need to be altered to +/// delegate to SQL syntax provider (we can drop indexes but not PK/FK). +/// +/// +/// 1. For SQLite, rename table.
+/// 2. Create new table with expected keys.
+/// 3. Insert into new from renamed
+/// 4. Drop renamed.
+///
+/// +/// Read more SQL Features That SQLite Does Not Implement +/// +///
+public class DeleteKeysAndIndexesBuilder : IExecutableBuilder { - /// - /// - /// Assuming we stick with the current migrations setup this will need to be altered to - /// delegate to SQL syntax provider (we can drop indexes but not PK/FK). - /// - /// - /// 1. For SQLite, rename table.
- /// 2. Create new table with expected keys.
- /// 3. Insert into new from renamed
- /// 4. Drop renamed.
- ///
- /// - /// Read more SQL Features That SQLite Does Not Implement - /// - ///
- public class DeleteKeysAndIndexesBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + private readonly DatabaseType[] _supportedDatabaseTypes; + + public DeleteKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - private readonly DatabaseType[] _supportedDatabaseTypes; + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; + } - public DeleteKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) + public string? TableName { get; set; } + + public bool DeleteLocal { get; set; } + + public bool DeleteForeign { get; set; } + + private IDeleteBuilder Delete => new DeleteBuilder(_context); + + /// + public void Do() + { + _context.BuildingExpression = false; + + // get a list of all constraints - this will include all PK, FK and unique constraints + var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database) + .DistinctBy(x => x.Item2).ToList(); + + // get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes + var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database) + .DistinctBy(x => x.IndexName).ToList(); + + IEnumerable uniqueConstraintNames = tableConstraints + .Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")) + .Select(x => x.Item2); + var indexNames = indexes.Select(x => x.IndexName).ToList(); + + // drop keys + if (DeleteLocal || DeleteForeign) { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } - - public string? TableName { get; set; } - - public bool DeleteLocal { get; set; } - - public bool DeleteForeign { get; set; } - - /// - public void Do() - { - _context.BuildingExpression = false; - - //get a list of all constraints - this will include all PK, FK and unique constraints - var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); - - //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); - - var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2); - var indexNames = indexes.Select(x => x.IndexName).ToList(); - - // drop keys - if (DeleteLocal || DeleteForeign) + // table, constraint + if (DeleteForeign) { - // table, constraint - - if (DeleteForeign) + // In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: + // * starts with FK OR + // * doesn't start with PK_ and doesn't exist in the list of indexes + foreach (Tuple key in tableConstraints.Where(x => x.Item1 == TableName + && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && + !indexNames.InvariantContains(x.Item2))))) { - //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: - // * starts with FK OR - // * doesn't start with PK_ and doesn't exist in the list of indexes - - foreach (var key in tableConstraints.Where(x => x.Item1 == TableName - && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2))))) - { - Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); - } - - } - if (DeleteLocal) - { - foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) - Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); - - // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways - // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn + Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); } } - // drop indexes if (DeleteLocal) { - foreach (var index in indexes.Where(x => x.TableName == TableName)) + foreach (Tuple key in tableConstraints.Where(x => + x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) { - //if this is a unique constraint we need to drop the constraint, else drop the index - //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints - - if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) - Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); - else - Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); } + // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways + // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn } } - private IDeleteBuilder Delete => new DeleteBuilder(_context); + // drop indexes + if (DeleteLocal) + { + foreach (DbIndexDefinition index in indexes.Where(x => x.TableName == TableName)) + { + // if this is a unique constraint we need to drop the constraint, else drop the index + // to figure this out, the index must be tagged as unique and it must exist in the tableConstraints + if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) + { + Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); + } + else + { + Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + } + } + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs index f483ec6402..e31d4aa497 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs @@ -1,41 +1,44 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute; + +public class ExecuteBuilder : ExpressionBuilderBase, + IExecuteBuilder, IExecutableBuilder { - public class ExecuteBuilder : ExpressionBuilderBase, - IExecuteBuilder, IExecutableBuilder + public ExecuteBuilder(IMigrationContext context) + : base(new ExecuteSqlStatementExpression(context)) { - public ExecuteBuilder(IMigrationContext context) - : base(new ExecuteSqlStatementExpression(context)) - { } + } - /// - public void Do() + /// + public void Do() + { + // slightly awkward, but doing it right would mean a *lot* + // of changes for MigrationExpressionBase + if (Expression.SqlObject == null) { - // slightly awkward, but doing it right would mean a *lot* - // of changes for MigrationExpressionBase - - if (Expression.SqlObject == null) - Expression.Execute(); - else - Expression.ExecuteSqlObject(); + Expression.Execute(); } - - /// - public IExecutableBuilder Sql(string sqlStatement) + else { - Expression.SqlStatement = sqlStatement; - return this; - } - - /// - public IExecutableBuilder Sql(Sql sql) - { - Expression.SqlObject = sql; - return this; + Expression.ExecuteSqlObject(); } } + + /// + public IExecutableBuilder Sql(string sqlStatement) + { + Expression.SqlStatement = sqlStatement; + return this; + } + + /// + public IExecutableBuilder Sql(Sql sql) + { + Expression.SqlObject = sql; + return this; + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs index 4e9186ace9..30b49f9664 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs @@ -1,26 +1,20 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; + +public class ExecuteSqlStatementExpression : MigrationExpressionBase { - public class ExecuteSqlStatementExpression : MigrationExpressionBase + public ExecuteSqlStatementExpression(IMigrationContext context) + : base(context) { - public ExecuteSqlStatementExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? SqlStatement { get; set; } - - public virtual Sql? SqlObject { get; set; } - - public void ExecuteSqlObject() - { - Execute(SqlObject); - } - - protected override string? GetSql() - { - return SqlStatement; - } } + + public virtual string? SqlStatement { get; set; } + + public virtual Sql? SqlObject { get; set; } + + public void ExecuteSqlObject() => Execute(SqlObject); + + protected override string? GetSql() => SqlStatement; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs index 54a1f6a768..36b9460429 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs @@ -1,23 +1,22 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute; + +/// +/// Builds and executes an Sql statement. +/// +/// Deals with multi-statements Sql. +public interface IExecuteBuilder : IFluentBuilder { /// - /// Builds and executes an Sql statement. + /// Specifies the Sql statement to execute. /// - /// Deals with multi-statements Sql. - public interface IExecuteBuilder : IFluentBuilder - { - /// - /// Specifies the Sql statement to execute. - /// - IExecutableBuilder Sql(string sqlStatement); + IExecutableBuilder Sql(string sqlStatement); - /// - /// Specifies the Sql statement to execute. - /// - IExecutableBuilder Sql(Sql sql); - } + /// + /// Specifies the Sql statement to execute. + /// + IExecutableBuilder Sql(Sql sql); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs index a3bed8b5d8..f751861ee7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs @@ -1,22 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +/// +/// Provides a base class for expression builders. +/// +public abstract class ExpressionBuilderBase + where TExpression : IMigrationExpression { /// - /// Provides a base class for expression builders. + /// Initializes a new instance of the class. /// - public abstract class ExpressionBuilderBase - where TExpression : IMigrationExpression - { - /// - /// Initializes a new instance of the class. - /// - protected ExpressionBuilderBase(TExpression expression) - { - Expression = expression; - } + protected ExpressionBuilderBase(TExpression expression) => Expression = expression; - /// - /// Gets the expression. - /// - public TExpression Expression { get; } - } + /// + /// Gets the expression. + /// + public TExpression Expression { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs index 74f1676def..04737ba019 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs @@ -1,284 +1,283 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +/// +/// Provides a base class for expression builders. +/// +public abstract class ExpressionBuilderBase : ExpressionBuilderBase + where TExpression : IMigrationExpression + where TNext : IFluentBuilder { /// - /// Provides a base class for expression builders. + /// Initializes a new instance of the class. /// - public abstract class ExpressionBuilderBase : ExpressionBuilderBase - where TExpression : IMigrationExpression - where TNext : IFluentBuilder + protected ExpressionBuilderBase(TExpression expression) + : base(expression) { - /// - /// Initializes a new instance of the class. - /// - protected ExpressionBuilderBase(TExpression expression) - : base(expression) + } + + private ColumnDefinition? Column => GetColumnForType(); + + public abstract ColumnDefinition? GetColumnForType(); + + public TNext AsAnsiString() + { + if (Column is not null) { + Column.Type = DbType.AnsiString; } - public abstract ColumnDefinition? GetColumnForType(); + return (TNext)(object)this; + } - private ColumnDefinition? Column => GetColumnForType(); - - public TNext AsAnsiString() + public TNext AsAnsiString(int size) + { + if (Column is not null) { - if (Column is not null) - { - Column.Type = DbType.AnsiString; - } - - return (TNext)(object)this; + Column.Type = DbType.AnsiString; + Column.Size = size; } - public TNext AsAnsiString(int size) - { - if (Column is not null) - { - Column.Type = DbType.AnsiString; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBinary() + { + if (Column is not null) + { + Column.Type = DbType.Binary; } - public TNext AsBinary() - { - if (Column is not null) - { - Column.Type = DbType.Binary; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBinary(int size) + { + if (Column is not null) + { + Column.Type = DbType.Binary; + Column.Size = size; } - public TNext AsBinary(int size) - { - if (Column is not null) - { - Column.Type = DbType.Binary; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBoolean() + { + if (Column is not null) + { + Column.Type = DbType.Boolean; } - public TNext AsBoolean() - { - if (Column is not null) - { - Column.Type = DbType.Boolean; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsByte() + { + if (Column is not null) + { + Column.Type = DbType.Byte; } - public TNext AsByte() - { - if (Column is not null) - { - Column.Type = DbType.Byte; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsCurrency() + { + if (Column is not null) + { + Column.Type = DbType.Currency; } - public TNext AsCurrency() - { - if (Column is not null) - { - Column.Type = DbType.Currency; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDate() + { + if (Column is not null) + { + Column.Type = DbType.Date; } - public TNext AsDate() - { - if (Column is not null) - { - Column.Type = DbType.Date; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDateTime() + { + if (Column is not null) + { + Column.Type = DbType.DateTime; } - public TNext AsDateTime() - { - if (Column is not null) - { - Column.Type = DbType.DateTime; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDecimal() + { + if (Column is not null) + { + Column.Type = DbType.Decimal; } - public TNext AsDecimal() - { - if (Column is not null) - { - Column.Type = DbType.Decimal; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDecimal(int size, int precision) + { + if (Column is not null) + { + Column.Type = DbType.Decimal; + Column.Size = size; + Column.Precision = precision; } - public TNext AsDecimal(int size, int precision) - { - if (Column is not null) - { - Column.Type = DbType.Decimal; - Column.Size = size; - Column.Precision = precision; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDouble() + { + if (Column is not null) + { + Column.Type = DbType.Double; } - public TNext AsDouble() - { - if (Column is not null) - { - Column.Type = DbType.Double; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFixedLengthString(int size) + { + if (Column is not null) + { + Column.Type = DbType.StringFixedLength; + Column.Size = size; } - public TNext AsFixedLengthString(int size) - { - if (Column is not null) - { - Column.Type = DbType.StringFixedLength; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFixedLengthAnsiString(int size) + { + if (Column is not null) + { + Column.Type = DbType.AnsiStringFixedLength; + Column.Size = size; } - public TNext AsFixedLengthAnsiString(int size) - { - if (Column is not null) - { - Column.Type = DbType.AnsiStringFixedLength; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFloat() + { + if (Column is not null) + { + Column.Type = DbType.Single; } - public TNext AsFloat() - { - if (Column is not null) - { - Column.Type = DbType.Single; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsGuid() + { + if (Column is not null) + { + Column.Type = DbType.Guid; } - public TNext AsGuid() - { - if (Column is not null) - { - Column.Type = DbType.Guid; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt16() + { + if (Column is not null) + { + Column.Type = DbType.Int16; } - public TNext AsInt16() - { - if (Column is not null) - { - Column.Type = DbType.Int16; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt32() + { + if (Column is not null) + { + Column.Type = DbType.Int32; } - public TNext AsInt32() - { - if (Column is not null) - { - Column.Type = DbType.Int32; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt64() + { + if (Column is not null) + { + Column.Type = DbType.Int64; } - public TNext AsInt64() - { - if (Column is not null) - { - Column.Type = DbType.Int64; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsString() + { + if (Column is not null) + { + Column.Type = DbType.String; } - public TNext AsString() - { - if (Column is not null) - { - Column.Type = DbType.String; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsString(int size) + { + if (Column is not null) + { + Column.Type = DbType.String; + Column.Size = size; } - public TNext AsString(int size) - { - if (Column is not null) - { - Column.Type = DbType.String; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsTime() + { + if (Column is not null) + { + Column.Type = DbType.Time; } - public TNext AsTime() - { - if (Column is not null) - { - Column.Type = DbType.Time; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsXml() + { + if (Column is not null) + { + Column.Type = DbType.Xml; } - public TNext AsXml() - { - if (Column is not null) - { - Column.Type = DbType.Xml; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsXml(int size) + { + if (Column is not null) + { + Column.Type = DbType.Xml; + Column.Size = size; } - public TNext AsXml(int size) - { - if (Column is not null) - { - Column.Type = DbType.Xml; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsCustom(string customType) + { + if (Column is not null) + { + Column.Type = null; + Column.CustomType = customType; } - public TNext AsCustom(string customType) - { - if (Column is not null) - { - Column.Type = null; - Column.CustomType = customType; - } - - return (TNext)(object)this; - } + return (TNext)(object)this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs index 8ad08b5733..b6f3dc07b2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +public interface IFluentBuilder { - public interface IFluentBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs index 75664b701e..abcd9714eb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs @@ -1,68 +1,69 @@ -using System.Collections.Generic; using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; + +public class InsertDataExpression : MigrationExpressionBase { - public class InsertDataExpression : MigrationExpressionBase + public InsertDataExpression(IMigrationContext context) + : base(context) { - public InsertDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } - public bool EnabledIdentityInsert { get; set; } + public string? TableName { get; set; } - public List Rows { get; } = new List(); + public bool EnabledIdentityInsert { get; set; } - protected override string GetSql() + public List Rows { get; } = new(); + + protected override string GetSql() + { + var stmts = new StringBuilder(); + + if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) { - var stmts = new StringBuilder(); + stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} ON"); + AppendStatementSeparator(stmts); + } - if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) + try + { + foreach (InsertionDataDefinition item in Rows) { - stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} ON"); - AppendStatementSeparator(stmts); - } - - try - { - foreach (var item in Rows) + var cols = new StringBuilder(); + var vals = new StringBuilder(); + var first = true; + foreach (KeyValuePair keyVal in item) { - var cols = new StringBuilder(); - var vals = new StringBuilder(); - var first = true; - foreach (var keyVal in item) + if (first) { - if (first) - { - first = false; - } - else - { - cols.Append(","); - vals.Append(","); - } - cols.Append(SqlSyntax.GetQuotedColumnName(keyVal.Key)); - vals.Append(GetQuotedValue(keyVal.Value)); + first = false; + } + else + { + cols.Append(","); + vals.Append(","); } - var sql = string.Format(SqlSyntax.InsertData, SqlSyntax.GetQuotedTableName(TableName), cols, vals); - - stmts.Append(sql); - AppendStatementSeparator(stmts); + cols.Append(SqlSyntax.GetQuotedColumnName(keyVal.Key)); + vals.Append(GetQuotedValue(keyVal.Value)); } - } - finally - { - if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) - { - stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} OFF"); - AppendStatementSeparator(stmts); - } - } - return stmts.ToString(); + var sql = string.Format(SqlSyntax.InsertData, SqlSyntax.GetQuotedTableName(TableName), cols, vals); + + stmts.Append(sql); + AppendStatementSeparator(stmts); + } } + finally + { + if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) + { + stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} OFF"); + AppendStatementSeparator(stmts); + } + } + + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs index 407a7a02f1..9178988b9c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Builds an Insert expression. +/// +public interface IInsertBuilder : IFluentBuilder { /// - /// Builds an Insert expression. + /// Specifies the table to insert into. /// - public interface IInsertBuilder : IFluentBuilder - { - /// - /// Specifies the table to insert into. - /// - IInsertIntoBuilder IntoTable(string tableName); - } + IInsertIntoBuilder IntoTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs index dfe4ba7909..d320231dc1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Builds an Insert Into expression. +/// +public interface IInsertIntoBuilder : IFluentBuilder, IExecutableBuilder { /// - /// Builds an Insert Into expression. + /// Enables identity insert. /// - public interface IInsertIntoBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Enables identity insert. - /// - IInsertIntoBuilder EnableIdentityInsert(); + IInsertIntoBuilder EnableIdentityInsert(); - /// - /// Specifies a row to be inserted. - /// - IInsertIntoBuilder Row(object dataAsAnonymousType); - } + /// + /// Specifies a row to be inserted. + /// + IInsertIntoBuilder Row(object dataAsAnonymousType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs index ddae2d5325..18f4e580b8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs @@ -1,24 +1,20 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Implements . +/// +public class InsertBuilder : IInsertBuilder { - /// - /// Implements . - /// - public class InsertBuilder : IInsertBuilder + private readonly IMigrationContext _context; + + public InsertBuilder(IMigrationContext context) => _context = context; + + /// + public IInsertIntoBuilder IntoTable(string tableName) { - private readonly IMigrationContext _context; - - public InsertBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IInsertIntoBuilder IntoTable(string tableName) - { - var expression = new InsertDataExpression(_context) { TableName = tableName }; - return new InsertIntoBuilder(expression); - } + var expression = new InsertDataExpression(_context) { TableName = tableName }; + return new InsertIntoBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs index 8d27877230..e353d3b782 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs @@ -1,45 +1,47 @@ -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Implements . +/// +public class InsertIntoBuilder : ExpressionBuilderBase, + IInsertIntoBuilder { - /// - /// Implements . - /// - public class InsertIntoBuilder : ExpressionBuilderBase, - IInsertIntoBuilder + public InsertIntoBuilder(InsertDataExpression expression) + : base(expression) { - public InsertIntoBuilder(InsertDataExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IInsertIntoBuilder EnableIdentityInsert() + /// + public IInsertIntoBuilder EnableIdentityInsert() + { + Expression.EnabledIdentityInsert = true; + return this; + } + + /// + public IInsertIntoBuilder Row(object dataAsAnonymousType) + { + Expression.Rows.Add(GetData(dataAsAnonymousType)); + return this; + } + + private static InsertionDataDefinition GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + + var data = new InsertionDataDefinition(); + foreach (PropertyDescriptor property in properties) { - Expression.EnabledIdentityInsert = true; - return this; + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } - /// - public IInsertIntoBuilder Row(object dataAsAnonymousType) - { - Expression.Rows.Add(GetData(dataAsAnonymousType)); - return this; - } - - private static InsertionDataDefinition GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - - var data = new InsertionDataDefinition(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; - } + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs index 76a3c06946..656ace382d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +/// +/// Builds a Rename Column expression. +/// +public interface IRenameColumnBuilder : IFluentBuilder { /// - /// Builds a Rename Column expression. + /// Specifies the table name. /// - public interface IRenameColumnBuilder : IFluentBuilder - { - /// - /// Specifies the table name. - /// - IRenameColumnToBuilder OnTable(string tableName); - } + IRenameColumnToBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs index 5580226c1f..33a02805c8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +/// +/// Builds a Rename Column expression. +/// +public interface IRenameColumnToBuilder : IFluentBuilder { /// - /// Builds a Rename Column expression. + /// Specifies the new name of the column. /// - public interface IRenameColumnToBuilder : IFluentBuilder - { - /// - /// Specifies the new name of the column. - /// - IExecutableBuilder To(string name); - } + IExecutableBuilder To(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs index a3a181c5df..182e997d01 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs @@ -1,30 +1,30 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +public class RenameColumnBuilder : ExpressionBuilderBase, + IRenameColumnToBuilder, IRenameColumnBuilder, IExecutableBuilder { - public class RenameColumnBuilder : ExpressionBuilderBase, - IRenameColumnToBuilder, IRenameColumnBuilder, IExecutableBuilder + public RenameColumnBuilder(RenameColumnExpression expression) + : base(expression) { - public RenameColumnBuilder(RenameColumnExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IExecutableBuilder To(string name) - { - Expression.NewName = name; - return this; - } + /// + public IRenameColumnToBuilder OnTable(string tableName) + { + Expression.TableName = tableName; + return this; + } - /// - public IRenameColumnToBuilder OnTable(string tableName) - { - Expression.TableName = tableName; - return this; - } + /// + public IExecutableBuilder To(string name) + { + Expression.NewName = name; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs index cafbc45108..bdfb4727da 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; + +public class RenameColumnExpression : MigrationExpressionBase { - public class RenameColumnExpression : MigrationExpressionBase + public RenameColumnExpression(IMigrationContext context) + : base(context) { - public RenameColumnExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - public virtual string? OldName { get; set; } - public virtual string? NewName { get; set; } - - /// - protected override string GetSql() - { - return SqlSyntax.FormatColumnRename(TableName, OldName, NewName); - } } + + public virtual string? TableName { get; set; } + + public virtual string? OldName { get; set; } + + public virtual string? NewName { get; set; } + + /// + protected override string GetSql() => SqlSyntax.FormatColumnRename(TableName, OldName, NewName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs index 77f9de03b3..c7b7704eb7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs @@ -1,29 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; + +/// +/// Represents a Rename Table expression. +/// +public class RenameTableExpression : MigrationExpressionBase { - /// - /// Represents a Rename Table expression. - /// - public class RenameTableExpression : MigrationExpressionBase + public RenameTableExpression(IMigrationContext context) + : base(context) { - public RenameTableExpression(IMigrationContext context) - : base(context) - { } - - /// - /// Gets or sets the source name. - /// - public virtual string? OldName { get; set; } - - /// - /// Gets or sets the target name. - /// - public virtual string? NewName { get; set; } - - /// - /// - protected override string GetSql() - { - return SqlSyntax.FormatTableRename(OldName, NewName); - } } + + /// + /// Gets or sets the source name. + /// + public virtual string? OldName { get; set; } + + /// + /// Gets or sets the target name. + /// + public virtual string? NewName { get; set; } + + /// + /// + protected override string GetSql() => SqlSyntax.FormatTableRename(OldName, NewName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs index e93842ae2a..2d4d6661a9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs @@ -1,21 +1,20 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename; + +/// +/// Builds a Rename expression. +/// +public interface IRenameBuilder : IFluentBuilder { /// - /// Builds a Rename expression. + /// Specifies the table to rename. /// - public interface IRenameBuilder : IFluentBuilder - { - /// - /// Specifies the table to rename. - /// - IRenameTableBuilder Table(string oldName); + IRenameTableBuilder Table(string oldName); - /// - /// Specifies the column to rename. - /// - IRenameColumnBuilder Column(string oldName); - } + /// + /// Specifies the column to rename. + /// + IRenameColumnBuilder Column(string oldName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs index c0b80f34bb..e21948a379 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs @@ -1,30 +1,26 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename; + +public class RenameBuilder : IRenameBuilder { - public class RenameBuilder : IRenameBuilder + private readonly IMigrationContext _context; + + public RenameBuilder(IMigrationContext context) => _context = context; + + /// + public IRenameTableBuilder Table(string oldName) { - private readonly IMigrationContext _context; + var expression = new RenameTableExpression(_context) { OldName = oldName }; + return new RenameTableBuilder(expression); + } - public RenameBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IRenameTableBuilder Table(string oldName) - { - var expression = new RenameTableExpression(_context) { OldName = oldName }; - return new RenameTableBuilder(expression); - } - - /// - public IRenameColumnBuilder Column(string oldName) - { - var expression = new RenameColumnExpression(_context) { OldName = oldName }; - return new RenameColumnBuilder(expression); - } + /// + public IRenameColumnBuilder Column(string oldName) + { + var expression = new RenameColumnExpression(_context) { OldName = oldName }; + return new RenameColumnBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs index 53f25a1b41..f2a202e497 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; + +/// +/// Builds a Rename Table expression. +/// +public interface IRenameTableBuilder : IFluentBuilder { /// - /// Builds a Rename Table expression. + /// Specifies the new name of the table. /// - public interface IRenameTableBuilder : IFluentBuilder - { - /// - /// Specifies the new name of the table. - /// - IExecutableBuilder To(string name); - } + IExecutableBuilder To(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs index af849b25d7..4fa9d4ad6d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs @@ -1,23 +1,23 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; + +public class RenameTableBuilder : ExpressionBuilderBase, + IRenameTableBuilder, IExecutableBuilder { - public class RenameTableBuilder : ExpressionBuilderBase, - IRenameTableBuilder, IExecutableBuilder + public RenameTableBuilder(RenameTableExpression expression) + : base(expression) { - public RenameTableBuilder(RenameTableExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IExecutableBuilder To(string name) - { - Expression.NewName = name; - return this; - } + /// + public IExecutableBuilder To(string name) + { + Expression.NewName = name; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs index 62b6f0acfb..fbe7e4d0d4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs @@ -1,36 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions +public class UpdateDataExpression : MigrationExpressionBase { - public class UpdateDataExpression : MigrationExpressionBase + public UpdateDataExpression(IMigrationContext context) + : base(context) { - public UpdateDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } + public string? TableName { get; set; } - public List>? Set { get; set; } - public List>? Where { get; set; } - public bool IsAllRows { get; set; } + public List>? Set { get; set; } - protected override string GetSql() - { - var updateItems = Set?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} = {GetQuotedValue(x.Value)}"); - var whereClauses = IsAllRows - ? null - : Where?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} {(x.Value == null ? "IS" : "=")} {GetQuotedValue(x.Value)}"); + public List>? Where { get; set; } - var whereClause = whereClauses == null - ? "(1=1)" - : string.Join(" AND ", whereClauses.ToArray()); + public bool IsAllRows { get; set; } - return string.Format(SqlSyntax.UpdateData, - SqlSyntax.GetQuotedTableName(TableName), - string.Join(", ", updateItems ?? Array.Empty()), - whereClause); - } + protected override string GetSql() + { + IEnumerable? updateItems = + Set?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} = {GetQuotedValue(x.Value)}"); + IEnumerable? whereClauses = IsAllRows + ? null + : Where?.Select(x => + $"{SqlSyntax.GetQuotedColumnName(x.Key)} {(x.Value == null ? "IS" : "=")} {GetQuotedValue(x.Value)}"); + + var whereClause = whereClauses == null + ? "(1=1)" + : string.Join(" AND ", whereClauses.ToArray()); + + return string.Format( + SqlSyntax.UpdateData, + SqlSyntax.GetQuotedTableName(TableName), + string.Join(", ", updateItems ?? Array.Empty()), + whereClause); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs index 16b1badf48..71849152e5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateBuilder : IFluentBuilder { /// - /// Builds an Update expression. + /// Specifies the table to update. /// - public interface IUpdateBuilder : IFluentBuilder - { - /// - /// Specifies the table to update. - /// - IUpdateTableBuilder Table(string tableName); - } + IUpdateTableBuilder Table(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs index abd5201cc5..7754d0b691 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateTableBuilder { /// - /// Builds an Update expression. + /// Specifies the data. /// - public interface IUpdateTableBuilder - { - /// - /// Specifies the data. - /// - IUpdateWhereBuilder Set(object dataAsAnonymousType); - } + IUpdateWhereBuilder Set(object dataAsAnonymousType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs index 378830cf0f..f81a444302 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateWhereBuilder { /// - /// Builds an Update expression. + /// Specifies rows to update. /// - public interface IUpdateWhereBuilder - { - /// - /// Specifies rows to update. - /// - IExecutableBuilder Where(object dataAsAnonymousType); + IExecutableBuilder Where(object dataAsAnonymousType); - /// - /// Specifies that all rows must be updated. - /// - IExecutableBuilder AllRows(); - } + /// + /// Specifies that all rows must be updated. + /// + IExecutableBuilder AllRows(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs index e47e31168a..75c5778cfe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs @@ -1,21 +1,17 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +public class UpdateBuilder : IUpdateBuilder { - public class UpdateBuilder : IUpdateBuilder + private readonly IMigrationContext _context; + + public UpdateBuilder(IMigrationContext context) => _context = context; + + /// + public IUpdateTableBuilder Table(string tableName) { - private readonly IMigrationContext _context; - - public UpdateBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IUpdateTableBuilder Table(string tableName) - { - var expression = new UpdateDataExpression(_context) { TableName = tableName }; - return new UpdateDataBuilder(expression); - } + var expression = new UpdateDataExpression(_context) { TableName = tableName }; + return new UpdateDataBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs index fc7608f148..60fbea9552 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs @@ -1,49 +1,51 @@ -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +public class UpdateDataBuilder : ExpressionBuilderBase, + IUpdateTableBuilder, IUpdateWhereBuilder, IExecutableBuilder { - public class UpdateDataBuilder : ExpressionBuilderBase, - IUpdateTableBuilder, IUpdateWhereBuilder, IExecutableBuilder + public UpdateDataBuilder(UpdateDataExpression expression) + : base(expression) { - public UpdateDataBuilder(UpdateDataExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IUpdateWhereBuilder Set(object dataAsAnonymousType) + /// + public IUpdateWhereBuilder Set(object dataAsAnonymousType) + { + Expression.Set = GetData(dataAsAnonymousType); + return this; + } + + /// + public IExecutableBuilder Where(object dataAsAnonymousType) + { + Expression.Where = GetData(dataAsAnonymousType); + return this; + } + + /// + public IExecutableBuilder AllRows() + { + Expression.IsAllRows = true; + return this; + } + + private static List> GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + + var data = new List>(); + foreach (PropertyDescriptor property in properties) { - Expression.Set = GetData(dataAsAnonymousType); - return this; + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } - /// - public IExecutableBuilder Where(object dataAsAnonymousType) - { - Expression.Where = GetData(dataAsAnonymousType); - return this; - } - - /// - public IExecutableBuilder AllRows() - { - Expression.IsAllRows = true; - return this; - } - - private static List> GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - - var data = new List>(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; - } + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs index 087e04d41a..078bfcaf38 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs @@ -1,9 +1,6 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +public interface IMigrationBuilder { - public interface IMigrationBuilder - { - MigrationBase Build(Type migrationType, IMigrationContext context); - } + MigrationBase Build(Type migrationType, IMigrationContext context); } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs index 9b164d38a3..6af5af23a3 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs @@ -1,47 +1,46 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides context to migrations. +/// +public interface IMigrationContext { /// - /// Provides context to migrations. + /// Gets the current migration plan /// - public interface IMigrationContext - { - /// - /// Gets the current migration plan - /// - MigrationPlan Plan { get; } + MigrationPlan Plan { get; } - /// - /// Gets the logger. - /// - ILogger Logger { get; } + /// + /// Gets the logger. + /// + ILogger Logger { get; } - /// - /// Gets the database instance. - /// - IUmbracoDatabase Database { get; } + /// + /// Gets the database instance. + /// + IUmbracoDatabase Database { get; } - /// - /// Gets the Sql context. - /// - ISqlContext SqlContext { get; } + /// + /// Gets the Sql context. + /// + ISqlContext SqlContext { get; } - /// - /// Gets or sets the expression index. - /// - int Index { get; set; } + /// + /// Gets or sets the expression index. + /// + int Index { get; set; } - /// - /// Gets or sets a value indicating whether an expression is being built. - /// - bool BuildingExpression { get; set; } + /// + /// Gets or sets a value indicating whether an expression is being built. + /// + bool BuildingExpression { get; set; } - /// - /// Adds a post-migration. - /// - void AddPostMigration() - where TMigration : MigrationBase; - } + /// + /// Adds a post-migration. + /// + void AddPostMigration() + where TMigration : MigrationBase; } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs index 3a5a4649fe..00756a3da2 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Marker interface for migration expressions +/// +public interface IMigrationExpression { - /// - /// Marker interface for migration expressions - /// - public interface IMigrationExpression - { - void Execute(); - } + void Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs index 41a831360a..552ca21b5e 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Migrations +namespace Umbraco.Cms.Core.Migrations; + +public interface IMigrationPlanExecutor { - public interface IMigrationPlanExecutor - { - string Execute(MigrationPlan plan, string fromState); - } + string Execute(MigrationPlan plan, string fromState); } diff --git a/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs b/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs index 67d559c66d..963948d9f6 100644 --- a/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs +++ b/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs @@ -1,49 +1,60 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// The exception that is thrown when a migration expression is not executed. +/// +/// +/// Migration expressions such as Alter.Table(...).Do() must end with Do(), else they are not executed. +/// When a non-executed expression is detected, an IncompleteMigrationExpressionException is thrown. +/// +/// +[Serializable] +public class IncompleteMigrationExpressionException : Exception { /// - /// The exception that is thrown when a migration expression is not executed. + /// Initializes a new instance of the class. /// - /// - /// Migration expressions such as Alter.Table(...).Do() must end with Do(), else they are not executed. - /// When a non-executed expression is detected, an IncompleteMigrationExpressionException is thrown. - /// - /// - [Serializable] - public class IncompleteMigrationExpressionException : Exception + public IncompleteMigrationExpressionException() { - /// - /// Initializes a new instance of the class. - /// - public IncompleteMigrationExpressionException() - { } + } - /// - /// Initializes a new instance of the class with a message. - /// - /// The message that describes the error. - public IncompleteMigrationExpressionException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a message. + /// + /// The message that describes the error. + public IncompleteMigrationExpressionException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public IncompleteMigrationExpressionException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public IncompleteMigrationExpressionException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected IncompleteMigrationExpressionException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected IncompleteMigrationExpressionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index c4a37d4374..b3188720b0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -155,28 +155,44 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install var connectionString = providerMeta.GenerateConnectionString(databaseSettings); var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName; - if (providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName!)) + if (string.IsNullOrEmpty(connectionString) || string.IsNullOrEmpty(providerName) || + (providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName))) { return false; } if (!isTrialRun) { - _configManipulator.SaveConnectionString(connectionString!, providerName); - Configure(connectionString!, providerName, _globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); + // File configuration providers use a delay before reloading and triggering changes, so wait + using var isChanged = new ManualResetEvent(false); + using IDisposable? onChange = _connectionStrings.OnChange((options, name) => + { + // Only watch default named option (CurrentValue) + if (name != Options.DefaultName) + { + return; + } + + // Signal change + isChanged.Set(); + }); + + // Update configuration and wait for change + _configManipulator.SaveConnectionString(connectionString, providerName); + if (!isChanged.WaitOne(10_000)) + { + throw new InstallException("Didn't retrieve updated connection string within 10 seconds, try manual configuration instead."); + } + + Configure(_globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); } return true; } - private void Configure(string connectionString, string? providerName, bool installMissingDatabase) + private void Configure(bool installMissingDatabase) { - // Update existing connection string - var umbracoConnectionString = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); - umbracoConnectionString.ConnectionString = connectionString; - umbracoConnectionString.ProviderName = providerName; - - _databaseFactory.Configure(umbracoConnectionString); + _databaseFactory.Configure(_connectionStrings.CurrentValue); if (installMissingDatabase) { diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index c00a745d8e..1b01a3ba47 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; @@ -12,1046 +10,2277 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Install -{ - /// - /// Creates the initial database data during install. - /// - internal class DatabaseDataCreator - { - private readonly IDatabase _database; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _installDefaultDataSettings; +namespace Umbraco.Cms.Infrastructure.Migrations.Install; - private readonly IDictionary> _entitiesToAlwaysCreate = new Dictionary>() +/// +/// Creates the initial database data during install. +/// +internal class DatabaseDataCreator +{ + private readonly IDatabase _database; + + private readonly IDictionary> _entitiesToAlwaysCreate = new Dictionary> + { + { + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + new List { Constants.DataTypes.Guids.LabelString } + }, + }; + + private readonly IOptionsMonitor _installDefaultDataSettings; + private readonly ILogger _logger; + private readonly IUmbracoVersion _umbracoVersion; + + public DatabaseDataCreator(IDatabase database, ILogger logger, IUmbracoVersion umbracoVersion, + IOptionsMonitor installDefaultDataSettings) + { + _database = database; + _logger = logger; + _umbracoVersion = umbracoVersion; + _installDefaultDataSettings = installDefaultDataSettings; + } + + /// + /// Initialize the base data creation by inserting the data foundation for umbraco + /// specific to a table + /// + /// Name of the table to create base data for + public void InitializeBaseData(string tableName) + { + _logger.LogInformation("Creating data in {TableName}", tableName); + + if (tableName.Equals(Constants.DatabaseSchema.Tables.Node)) + { + CreateNodeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.Lock)) + { + CreateLockData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.ContentType)) + { + CreateContentTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.User)) + { + CreateUserData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.UserGroup)) + { + CreateUserGroupData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.User2UserGroup)) + { + CreateUser2UserGroupData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.UserGroup2App)) + { + CreateUserGroup2AppData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.PropertyTypeGroup)) + { + CreatePropertyTypeGroupData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.PropertyType)) + { + CreatePropertyTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.Language)) + { + CreateLanguageData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.ContentChildType)) + { + CreateContentChildTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.DataType)) + { + CreateDataTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.RelationType)) + { + CreateRelationTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.KeyValue)) + { + CreateKeyValueData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.LogViewerQuery)) + { + CreateLogViewerQueryData(); + } + + _logger.LogInformation("Completed creating data in {TableName}", tableName); + } + + internal static Guid CreateUniqueRelationTypeId(string alias, string name) => (alias + "____" + name).ToGuid(); + + private void CreateNodeData() + { + CreateNodeDataForDataTypes(); + CreateNodeDataForMediaTypes(); + CreateNodeDataForMemberTypes(); + } + + private void CreateNodeDataForDataTypes() + { + void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) + { + var nodeDto = new NodeDto { - { - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - new List - { - Cms.Core.Constants.DataTypes.Guids.LabelString, - } - } + NodeId = id, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1," + id, + SortOrder = sortOrder, + UniqueId = new Guid(uniqueId), + Text = text, + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, }; - public DatabaseDataCreator(IDatabase database, ILogger logger, IUmbracoVersion umbracoVersion, IOptionsMonitor installDefaultDataSettings) - { - _database = database; - _logger = logger; - _umbracoVersion = umbracoVersion; - _installDefaultDataSettings = installDefaultDataSettings; + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + uniqueId, + nodeDto, + Constants.DatabaseSchema.Tables.Node, + "id"); } - /// - /// Initialize the base data creation by inserting the data foundation for umbraco - /// specific to a table - /// - /// Name of the table to create base data for - public void InitializeBaseData(string tableName) + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -1, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1", + SortOrder = 0, + UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), + Text = "SYSTEM DATA: umbraco master root", + NodeObjectType = Constants.ObjectTypes.SystemRoot, + CreateDate = DateTime.Now, + }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -20, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1,-20", + SortOrder = 0, + UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), + Text = "Recycle Bin", + NodeObjectType = Constants.ObjectTypes.ContentRecycleBin, + CreateDate = DateTime.Now, + }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -21, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1,-21", + SortOrder = 0, + UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), + Text = "Recycle Bin", + NodeObjectType = Constants.ObjectTypes.MediaRecycleBin, + CreateDate = DateTime.Now, + }); + + InsertDataTypeNodeDto(Constants.DataTypes.LabelString, 35, Constants.DataTypes.Guids.LabelString, + "Label (string)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelInt, 36, Constants.DataTypes.Guids.LabelInt, "Label (integer)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelBigint, 36, Constants.DataTypes.Guids.LabelBigInt, + "Label (bigint)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, + "Label (datetime)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, + "Label (decimal)"); + + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Upload, + new NodeDto + { + NodeId = Constants.DataTypes.Upload, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Upload}", + SortOrder = 34, + UniqueId = Constants.DataTypes.Guids.UploadGuid, + Text = "Upload File", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadVideo, + new NodeDto + { + NodeId = Constants.DataTypes.UploadVideo, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadVideo}", + SortOrder = 35, + UniqueId = Constants.DataTypes.Guids.UploadVideoGuid, + Text = "Upload Video", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadAudio, + new NodeDto + { + NodeId = Constants.DataTypes.UploadAudio, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadAudio}", + SortOrder = 36, + UniqueId = Constants.DataTypes.Guids.UploadAudioGuid, + Text = "Upload Audio", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadArticle, + new NodeDto + { + NodeId = Constants.DataTypes.UploadArticle, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadArticle}", + SortOrder = 37, + UniqueId = Constants.DataTypes.Guids.UploadArticleGuid, + Text = "Upload Article", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadVectorGraphics, + new NodeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadVectorGraphics}", + SortOrder = 38, + UniqueId = Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Text = "Upload Vector Graphics", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Textarea, + new NodeDto + { + NodeId = Constants.DataTypes.Textarea, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Textarea}", + SortOrder = 33, + UniqueId = Constants.DataTypes.Guids.TextareaGuid, + Text = "Textarea", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Textstring, + new NodeDto + { + NodeId = Constants.DataTypes.Textbox, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Textbox}", + SortOrder = 32, + UniqueId = Constants.DataTypes.Guids.TextstringGuid, + Text = "Textstring", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.RichtextEditor, + new NodeDto + { + NodeId = Constants.DataTypes.RichtextEditor, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.RichtextEditor}", + SortOrder = 4, + UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, + Text = "Richtext editor", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Numeric, + new NodeDto + { + NodeId = -51, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-51", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.NumericGuid, + Text = "Numeric", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Checkbox, + new NodeDto + { + NodeId = Constants.DataTypes.Boolean, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Boolean}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.CheckboxGuid, + Text = "True/false", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.CheckboxList, + new NodeDto + { + NodeId = -43, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-43", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.CheckboxListGuid, + Text = "Checkbox list", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Dropdown, + new NodeDto + { + NodeId = Constants.DataTypes.DropDownSingle, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DropDownSingle}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DropdownGuid, + Text = "Dropdown", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DatePicker, + new NodeDto + { + NodeId = -41, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-41", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DatePickerGuid, + Text = "Date Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Radiobox, + new NodeDto + { + NodeId = -40, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-40", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.RadioboxGuid, + Text = "Radiobox", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DropdownMultiple, + new NodeDto + { + NodeId = Constants.DataTypes.DropDownMultiple, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DropDownMultiple}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DropdownMultipleGuid, + Text = "Dropdown multiple", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ApprovedColor, + new NodeDto + { + NodeId = -37, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-37", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ApprovedColorGuid, + Text = "Approved Color", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DatePickerWithTime, + new NodeDto + { + NodeId = Constants.DataTypes.DateTime, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DateTime}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DatePickerWithTimeGuid, + Text = "Date Picker with time", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewContent, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultContentListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultContentListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewContentGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewMedia, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultMediaListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewMediaGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewMembers, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultMembersListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewMembersGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Tags, + new NodeDto + { + NodeId = Constants.DataTypes.Tags, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Tags}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.TagsGuid, + Text = "Tags", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ImageCropper, + new NodeDto + { + NodeId = Constants.DataTypes.ImageCropper, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.ImageCropper}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ImageCropperGuid, + Text = "Image Cropper", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + // New UDI pickers with newer Ids + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ContentPicker, + new NodeDto + { + NodeId = 1046, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1046", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ContentPickerGuid, + Text = "Content Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MemberPicker, + new NodeDto + { + NodeId = 1047, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1047", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MemberPickerGuid, + Text = "Member Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker, + new NodeDto + { + NodeId = 1048, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1048", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPickerGuid, + Text = "Media Picker (legacy)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MultipleMediaPicker, + new NodeDto + { + NodeId = 1049, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1049", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MultipleMediaPickerGuid, + Text = "Multiple Media Picker (legacy)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.RelatedLinks, + new NodeDto + { + NodeId = 1050, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1050", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.RelatedLinksGuid, + Text = "Multi URL Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3, + new NodeDto + { + NodeId = 1051, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1051", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, + Text = "Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3Multiple, + new NodeDto + { + NodeId = 1052, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1052", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, + Text = "Multiple Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3SingleImage, + new NodeDto + { + NodeId = 1053, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1053", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, + Text = "Image Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3MultipleImages, + new NodeDto + { + NodeId = 1054, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1054", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, + Text = "Multiple Image Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateNodeDataForMediaTypes() + { + var folderUniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + folderUniqueId.ToString(), + new NodeDto + { + NodeId = 1031, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1031", + SortOrder = 2, + UniqueId = folderUniqueId, + Text = Constants.Conventions.MediaTypes.Folder, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var imageUniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + imageUniqueId.ToString(), + new NodeDto + { + NodeId = 1032, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1032", + SortOrder = 2, + UniqueId = imageUniqueId, + Text = Constants.Conventions.MediaTypes.Image, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var fileUniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + fileUniqueId.ToString(), + new NodeDto + { + NodeId = 1033, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1033", + SortOrder = 2, + UniqueId = fileUniqueId, + Text = Constants.Conventions.MediaTypes.File, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var videoUniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + videoUniqueId.ToString(), + new NodeDto + { + NodeId = 1034, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1034", + SortOrder = 2, + UniqueId = videoUniqueId, + Text = Constants.Conventions.MediaTypes.Video, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var audioUniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + audioUniqueId.ToString(), + new NodeDto + { + NodeId = 1035, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1035", + SortOrder = 2, + UniqueId = audioUniqueId, + Text = Constants.Conventions.MediaTypes.Audio, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var articleUniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + articleUniqueId.ToString(), + new NodeDto + { + NodeId = 1036, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1036", + SortOrder = 2, + UniqueId = articleUniqueId, + Text = Constants.Conventions.MediaTypes.Article, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var svgUniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + svgUniqueId.ToString(), + new NodeDto + { + NodeId = 1037, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1037", + SortOrder = 2, + UniqueId = svgUniqueId, + Text = "Vector Graphics (SVG)", + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateNodeDataForMemberTypes() + { + var memberUniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, + memberUniqueId.ToString(), + new NodeDto + { + NodeId = 1044, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1044", + SortOrder = 0, + UniqueId = memberUniqueId, + Text = Constants.Conventions.MemberTypes.DefaultAlias, + NodeObjectType = Constants.ObjectTypes.MemberType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateLockData() + { + // all lock objects + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.Servers, Name = "Servers" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.ContentTypes, Name = "ContentTypes" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.ContentTree, Name = "ContentTree" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MediaTypes, Name = "MediaTypes" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MediaTree, Name = "MediaTree" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MemberTypes, Name = "MemberTypes" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MemberTree, Name = "MemberTree" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.Domains, Name = "Domains" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.Languages, Name = "Languages" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); + + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); + } + + private void CreateContentTypeData() + { + // Insert content types only if the corresponding Node record exists (which may or may not have been created depending on configuration + // of media or member types to create). + + // Media types. + if (_database.Exists(1031)) { - _logger.LogInformation("Creating data in {TableName}", tableName); - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Node)) - { - CreateNodeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Lock)) - { - CreateLockData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.ContentType)) - { - CreateContentTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.User)) - { - CreateUserData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup)) - { - CreateUserGroupData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.User2UserGroup)) - { - CreateUser2UserGroupData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App)) - { - CreateUserGroup2AppData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup)) - { - CreatePropertyTypeGroupData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)) - { - CreatePropertyTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Language)) - { - CreateLanguageData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType)) - { - CreateContentChildTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.DataType)) - { - CreateDataTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.RelationType)) - { - CreateRelationTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue)) - { - CreateKeyValueData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) - { - CreateLogViewerQueryData(); - } - - _logger.LogInformation("Completed creating data in {TableName}", tableName); - } - - private void CreateNodeData() - { - CreateNodeDataForDataTypes(); - CreateNodeDataForMediaTypes(); - CreateNodeDataForMemberTypes(); - } - - private void CreateNodeDataForDataTypes() - { - void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) - { - var nodeDto = new NodeDto + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto { - NodeId = id, - Trashed = false, - ParentId = -1, - UserId = -1, - Level = 1, - Path = "-1," + id, - SortOrder = sortOrder, - UniqueId = new Guid(uniqueId), - Text = text, - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - CreateDate = DateTime.Now, - }; - - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - uniqueId, - nodeDto, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -1, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1", SortOrder = 0, UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), Text = "SYSTEM DATA: umbraco master root", NodeObjectType = Cms.Core.Constants.ObjectTypes.SystemRoot, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -20, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-20", SortOrder = 0, UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), Text = "Recycle Bin", NodeObjectType = Cms.Core.Constants.ObjectTypes.ContentRecycleBin, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -21, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-21", SortOrder = 0, UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), Text = "Recycle Bin", NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaRecycleBin, CreateDate = DateTime.Now }); - - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelString, 35, Cms.Core.Constants.DataTypes.Guids.LabelString, "Label (string)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelInt, 36, Cms.Core.Constants.DataTypes.Guids.LabelInt, "Label (integer)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelBigint, 36, Cms.Core.Constants.DataTypes.Guids.LabelBigInt, "Label (bigint)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelDateTime, 37, Cms.Core.Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelTime, 38, Cms.Core.Constants.DataTypes.Guids.LabelTime, "Label (time)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelDecimal, 39, Cms.Core.Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); - - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Upload, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadGuid, Text = "Upload File", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadVideo, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadVideo, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadVideo}", SortOrder = 35, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadVideoGuid, Text = "Upload Video", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadAudio, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadAudio, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadAudio}", SortOrder = 36, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadAudioGuid, Text = "Upload Audio", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadArticle, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadArticle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadArticle}", SortOrder = 37, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadArticleGuid, Text = "Upload Article", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadVectorGraphics, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadVectorGraphics}", SortOrder = 38, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadVectorGraphicsGuid, Text = "Upload Vector Graphics", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Textarea, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Cms.Core.Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Textstring, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Cms.Core.Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.RichtextEditor, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Cms.Core.Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Numeric, - new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.NumericGuid, Text = "Numeric", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Checkbox, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Boolean, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Boolean}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.CheckboxGuid, Text = "True/false", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.CheckboxList, - new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.CheckboxListGuid, Text = "Checkbox list", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Dropdown, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DropdownGuid, Text = "Dropdown", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DatePicker, - new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DatePickerGuid, Text = "Date Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Radiobox, - new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.RadioboxGuid, Text = "Radiobox", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DropdownMultiple, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DropdownMultipleGuid, Text = "Dropdown multiple", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ApprovedColor, - new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ApprovedColorGuid, Text = "Approved Color", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DatePickerWithTime, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DateTime, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DateTime}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DatePickerWithTimeGuid, Text = "Date Picker with time", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewContent, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewContentGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewMedia, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewMediaGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewMembers, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewMembersGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Tags, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.TagsGuid, Text = "Tags", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ImageCropper, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ImageCropperGuid, Text = "Image Cropper", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - // New UDI pickers with newer Ids - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ContentPicker, - new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ContentPickerGuid, Text = "Content Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MemberPicker, - new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MemberPickerGuid, Text = "Member Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker, - new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPickerGuid, Text = "Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MultipleMediaPicker, - new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MultipleMediaPickerGuid, Text = "Multiple Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.RelatedLinks, - new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.RelatedLinksGuid, Text = "Multi URL Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3, - new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3Multiple, - new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3SingleImage, - new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleImages, - new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - private void CreateNodeDataForMediaTypes() - { - var folderUniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - folderUniqueId.ToString(), - new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = folderUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Folder, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var imageUniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - imageUniqueId.ToString(), - new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = imageUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Image, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var fileUniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - fileUniqueId.ToString(), - new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = fileUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.File, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var videoUniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - videoUniqueId.ToString(), - new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = videoUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Video, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var audioUniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - audioUniqueId.ToString(), - new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = audioUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Audio, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var articleUniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - articleUniqueId.ToString(), - new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = articleUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Article, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var svgUniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - svgUniqueId.ToString(), - new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = svgUniqueId, Text = "Vector Graphics (SVG)", NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - private void CreateNodeDataForMemberTypes() - { - var memberUniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, - memberUniqueId.ToString(), - new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = memberUniqueId, Text = Cms.Core.Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Cms.Core.Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - private void CreateLockData() - { - // all lock objects - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Servers, Name = "Servers" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ContentTypes, Name = "ContentTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ContentTree, Name = "ContentTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MediaTypes, Name = "MediaTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MediaTree, Name = "MediaTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MemberTypes, Name = "MemberTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MemberTree, Name = "MemberTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" }); - } - - private void CreateContentTypeData() - { - // Insert content types only if the corresponding Node record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media types. - if (_database.Exists(1031)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Cms.Core.Constants.Conventions.MediaTypes.Folder, Icon = Cms.Core.Constants.Icons.MediaFolder, Thumbnail = Cms.Core.Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1032)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Cms.Core.Constants.Conventions.MediaTypes.Image, Icon = Cms.Core.Constants.Icons.MediaImage, Thumbnail = Cms.Core.Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1033)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Cms.Core.Constants.Conventions.MediaTypes.File, Icon = Cms.Core.Constants.Icons.MediaFile, Thumbnail = Cms.Core.Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1034)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Cms.Core.Constants.Conventions.MediaTypes.VideoAlias, Icon = Cms.Core.Constants.Icons.MediaVideo, Thumbnail = Cms.Core.Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1035)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Cms.Core.Constants.Conventions.MediaTypes.AudioAlias, Icon = Cms.Core.Constants.Icons.MediaAudio, Thumbnail = Cms.Core.Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1036)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Cms.Core.Constants.Conventions.MediaTypes.ArticleAlias, Icon = Cms.Core.Constants.Icons.MediaArticle, Thumbnail = Cms.Core.Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1037)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Cms.Core.Constants.Conventions.MediaTypes.VectorGraphicsAlias, Icon = Cms.Core.Constants.Icons.MediaVectorGraphics, Thumbnail = Cms.Core.Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - // Member type. - if (_database.Exists(1044)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Cms.Core.Constants.Conventions.MemberTypes.DefaultAlias, Icon = Cms.Core.Constants.Icons.Member, Thumbnail = Cms.Core.Constants.Icons.Member, Variations = (byte)ContentVariation.Nothing }); - } - } - - private void CreateUserData() - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.User, "id", false, new UserDto { Id = Cms.Core.Constants.Security.SuperUserId, Disabled = false, NoConsole = false, UserName = "Administrator", Login = "admin", Password = "default", Email = string.Empty, UserLanguage = "en-US", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); - } - - private void CreateUserGroupData() - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 1, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.WriterGroupAlias, Name = "Writers", DefaultPermissions = "CAH:FN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.EditorGroupAlias, Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5FïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 4, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.TranslatorGroupAlias, Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 5, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = string.Empty, CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" }); - } - - private void CreateUser2UserGroupData() - { - _database.Insert(new User2UserGroupDto { UserGroupId = 1, UserId = Cms.Core.Constants.Security.SuperUserId }); // add super to admins - _database.Insert(new User2UserGroupDto { UserGroupId = 5, UserId = Cms.Core.Constants.Security.SuperUserId }); // add super to sensitive data - } - - private void CreateUserGroup2AppData() - { - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Content }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Packages }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Media }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Members }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Settings }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Users }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Forms }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Translation }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Cms.Core.Constants.Applications.Content }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Content }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Media }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Forms }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 4, AppAlias = Cms.Core.Constants.Applications.Translation }); - } - - private void CreatePropertyTypeGroupData() - { - // Insert property groups only if the corresponding content type node record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media property groups. - if (_database.Exists(1032)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Image), ContentTypeNodeId = 1032, Text = "Image", Alias = "image", SortOrder = 1 }); - } - - if (_database.Exists(1033)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.File), ContentTypeNodeId = 1033, Text = "File", Alias = "file", SortOrder = 1, }); - } - - if (_database.Exists(1034)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Video), ContentTypeNodeId = 1034, Text = "Video", Alias = "video", SortOrder = 1 }); - } - - if (_database.Exists(1035)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Audio), ContentTypeNodeId = 1035, Text = "Audio", Alias = "audio", SortOrder = 1 }); - } - - if (_database.Exists(1036)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Article), ContentTypeNodeId = 1036, Text = "Article", Alias = "article", SortOrder = 1 }); - } - - if (_database.Exists(1037)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.VectorGraphics), ContentTypeNodeId = 1037, Text = "Vector Graphics", Alias = "vectorGraphics", SortOrder = 1 }); - } - - // Membership property group. - if (_database.Exists(1044)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Membership), ContentTypeNodeId = 1044, Text = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName, Alias = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, SortOrder = 1 }); - } - } - - private void CreatePropertyTypeData() - { - // Insert property types only if the corresponding property group record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media property types. - if (_database.Exists(3)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(4)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "File", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(52)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 40, UniqueId = 40.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadVideo, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Video", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 41, UniqueId = 41.ToGuid(), DataTypeId = -92, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 42, UniqueId = 42.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(53)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 43, UniqueId = 43.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadAudio, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Audio", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 44, UniqueId = 44.ToGuid(), DataTypeId = -92, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 45, UniqueId = 45.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(54)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 46, UniqueId = 46.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadArticle, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Article", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 47, UniqueId = 47.ToGuid(), DataTypeId = -92, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 48, UniqueId = 48.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(55)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 49, UniqueId = 49.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Vector Graphics", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 50, UniqueId = 50.ToGuid(), DataTypeId = -92, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 51, UniqueId = 51.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - // Membership property types. - if (_database.Exists(11)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, - new PropertyTypeDto - { - Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Textarea, - ContentTypeId = 1044, PropertyTypeGroupId = 11, - Alias = Cms.Core.Constants.Conventions.Member.Comments, - Name = Cms.Core.Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, - ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing - }); - } - } - - private void CreateLanguageData() => - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.Languages, - "en-us", - new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }, - Cms.Core.Constants.DatabaseSchema.Tables.Language, - "id"); - - private void CreateContentChildTypeData() - { - // Insert data if the corresponding Node records exist (which may or may not have been created depending on configuration - // of media types to create). - if (!_database.Exists(1031)) - { - return; - } - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); - - for (int i = 1032; i <= 1037; i++) - { - if (_database.Exists(i)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = i }); - } - } - } - - private void CreateDataTypeData() - { - void InsertDataTypeDto(int id, string editorAlias, string dbType, string? configuration = null) - { - var dataTypeDto = new DataTypeDto - { - NodeId = id, - EditorAlias = editorAlias, - DbType = dbType - }; - - if (configuration != null) - { - dataTypeDto.Configuration = configuration; - } - - if (_database.Exists(id)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); - } - } - - //layouts for the list view - const string cardLayout = "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; - const string listLayout = "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": 1,\"selected\": true}"; - const string layouts = "[" + cardLayout + "," + listLayout + "]"; - - // Insert data types only if the corresponding Node record exists (which may or may not have been created depending on configuration - // of data types to create). - if (_database.Exists(Cms.Core.Constants.DataTypes.Boolean)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Boolean, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Boolean, DbType = "Integer" }); - } - - if (_database.Exists(-51)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -51, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Integer, DbType = "Integer" }); - } - - if (_database.Exists(-87)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = -87, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TinyMce, - DbType = "Ntext", - Configuration = "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Textbox)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Textbox, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Textarea)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Textarea, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Upload)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Upload, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelString, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"STRING\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelInt, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelBigint, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDateTime, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDecimal, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelTime, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); - - if (_database.Exists(Cms.Core.Constants.DataTypes.DateTime)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.DateTime, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); - } - - if (_database.Exists(-37)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" }); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.DropDownSingle, Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}"); - - if (_database.Exists(-40)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" }); - } - - if (_database.Exists(-41)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.DateTime", DbType = "Date", Configuration = "{\"format\":\"YYYY-MM-DD\"}" }); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.DropDownMultiple, Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}"); - - if (_database.Exists(-43)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Tags)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.Tags, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Tags, - DbType = "Ntext", - Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.ImageCropper)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.ImageCropper, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ImageCropper, DbType = "Ntext" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultContentListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultContentListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultMediaListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultMediaListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultMembersListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultMembersListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":10, \"orderBy\":\"username\", \"orderDirection\":\"asc\", \"includeProperties\":[{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}" - }); - } - - // New UDI pickers with newer Ids - if (_database.Exists(1046)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1046, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker, DbType = "Nvarchar" }); - } - - if (_database.Exists(1047)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1047, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MemberPicker, DbType = "Nvarchar" }); - } - - if (_database.Exists(1048)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1048, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext" }); - } - - if (_database.Exists(1049)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = 1049, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker, - DbType = "Ntext", - Configuration = "{\"multiPicker\":1}" - }); - } - - if (_database.Exists(1050)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadVideo)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadVideo, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}" + PrimaryKey = 532, + NodeId = 1031, + Alias = Constants.Conventions.MediaTypes.Folder, + Icon = Constants.Icons.MediaFolder, + Thumbnail = Constants.Icons.MediaFolder, + IsContainer = false, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadAudio)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadAudio, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadArticle)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadArticle, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadVectorGraphics)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}" - }); - } - - if (_database.Exists(1051)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1051, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" - }); - } - - if (_database.Exists(1052)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1052, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"multiple\": true}" - }); - } - - if (_database.Exists(1053)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1053, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"filter\":\"" + Cms.Core.Constants.Conventions.MediaTypes.Image + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" - }); - } - - if (_database.Exists(1054)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1054, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"filter\":\"" + Cms.Core.Constants.Conventions.MediaTypes.Image + "\", \"multiple\": true}" - }); - } } - private void CreateRelationTypeData() + if (_database.Exists(1032)) { - CreateRelationTypeData(1, Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, Cms.Core.Constants.ObjectTypes.Document, Cms.Core.Constants.ObjectTypes.Document, true, false); - CreateRelationTypeData(2, Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, Cms.Core.Constants.ObjectTypes.Document, Cms.Core.Constants.ObjectTypes.Document, false, false); - CreateRelationTypeData(3, Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, Cms.Core.Constants.ObjectTypes.Media, Cms.Core.Constants.ObjectTypes.Media, false, false); - CreateRelationTypeData(4, Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName, null, null, false, true); - CreateRelationTypeData(5, Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName, null, null, false, true); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 533, + NodeId = 1032, + Alias = Constants.Conventions.MediaTypes.Image, + Icon = Constants.Icons.MediaImage, + Thumbnail = Constants.Icons.MediaImage, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void CreateRelationTypeData(int id, string alias, string name, Guid? parentObjectType, Guid? childObjectType, bool dual, bool isDependency) + if (_database.Exists(1033)) { - var relationType = new RelationTypeDto { Id = id, Alias = alias, ChildObjectType = childObjectType, ParentObjectType = parentObjectType, Dual = dual, Name = name, IsDependency = isDependency }; - relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 534, + NodeId = 1033, + Alias = Constants.Conventions.MediaTypes.File, + Icon = Constants.Icons.MediaFile, + Thumbnail = Constants.Icons.MediaFile, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - internal static Guid CreateUniqueRelationTypeId(string alias, string name) + if (_database.Exists(1034)) { - return (alias + "____" + name).ToGuid(); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 540, + NodeId = 1034, + Alias = Constants.Conventions.MediaTypes.VideoAlias, + Icon = Constants.Icons.MediaVideo, + Thumbnail = Constants.Icons.MediaVideo, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void CreateKeyValueData() + if (_database.Exists(1035)) { - // On install, initialize the umbraco migration plan with the final state. - var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); - var stateValueKey = upgrader.StateValueKey; - var finalState = upgrader.Plan.FinalState; - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 541, + NodeId = 1035, + Alias = Constants.Conventions.MediaTypes.AudioAlias, + Icon = Constants.Icons.MediaAudio, + Thumbnail = Constants.Icons.MediaAudio, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void CreateLogViewerQueryData() + if (_database.Exists(1036)) { - LogViewerQueryDto[] defaultData = MigrateLogViewerQueriesFromFileToDb.DefaultLogQueries.ToArray(); - - for (int i = 0; i < defaultData.Length; i++) - { - LogViewerQueryDto dto = defaultData[i]; - dto.Id = i+1; - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery, "id", false, dto); - } + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 542, + NodeId = 1036, + Alias = Constants.Conventions.MediaTypes.ArticleAlias, + Icon = Constants.Icons.MediaArticle, + Thumbnail = Constants.Icons.MediaArticle, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void ConditionalInsert( - string configKey, - string id, - TDto dto, - string tableName, - string primaryKeyName, - bool autoIncrement = false) + if (_database.Exists(1037)) { - var alwaysInsert = _entitiesToAlwaysCreate.ContainsKey(configKey) && - _entitiesToAlwaysCreate[configKey].InvariantContains(id.ToString()); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 543, + NodeId = 1037, + Alias = Constants.Conventions.MediaTypes.VectorGraphicsAlias, + Icon = Constants.Icons.MediaVectorGraphics, + Thumbnail = Constants.Icons.MediaVectorGraphics, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); + } - InstallDefaultDataSettings installDefaultDataSettings = _installDefaultDataSettings.Get(configKey); - - // If there's no configuration, we assume to create. - if (installDefaultDataSettings == null) - { - alwaysInsert = true; - } - - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.None) - { - return; - } - - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values && !installDefaultDataSettings.Values.InvariantContains(id)) - { - return; - } - - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.ExceptValues && installDefaultDataSettings.Values.InvariantContains(id)) - { - return; - } - - _database.Insert(tableName, primaryKeyName, autoIncrement, dto); + // Member type. + if (_database.Exists(1044)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 531, + NodeId = 1044, + Alias = Constants.Conventions.MemberTypes.DefaultAlias, + Icon = Constants.Icons.Member, + Thumbnail = Constants.Icons.Member, + Variations = (byte)ContentVariation.Nothing, + }); } } + + private void CreateUserData() => _database.Insert(Constants.DatabaseSchema.Tables.User, "id", false, + new UserDto + { + Id = Constants.Security.SuperUserId, + Disabled = false, + NoConsole = false, + UserName = "Administrator", + Login = "admin", + Password = "default", + Email = string.Empty, + UserLanguage = "en-US", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + }); + + private void CreateUserGroupData() + { + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 1, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.AdminGroupAlias, + Name = "Administrators", + DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-medal", + HasAccessToAllLanguages = true, + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 2, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.WriterGroupAlias, + Name = "Writers", + DefaultPermissions = "CAH:FN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-edit", + HasAccessToAllLanguages = true, + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 3, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.EditorGroupAlias, + Name = "Editors", + DefaultPermissions = "CADMOSKTPUZ:5FïN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-tools", + HasAccessToAllLanguages = true, + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 4, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.TranslatorGroupAlias, + Name = "Translators", + DefaultPermissions = "AF", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-globe", + HasAccessToAllLanguages = true, + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 5, + Alias = Constants.Security.SensitiveDataGroupAlias, + Name = "Sensitive data", + DefaultPermissions = string.Empty, + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-lock", + HasAccessToAllLanguages = false, + }); + } + + private void CreateUser2UserGroupData() + { + _database.Insert(new User2UserGroupDto + { + UserGroupId = 1, + UserId = Constants.Security.SuperUserId, + }); // add super to admins + _database.Insert(new User2UserGroupDto + { + UserGroupId = 5, + UserId = Constants.Security.SuperUserId, + }); // add super to sensitive data + } + + private void CreateUserGroup2AppData() + { + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Content }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Packages }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Media }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Members }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Settings }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Users }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Forms }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Translation }); + + _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Constants.Applications.Content }); + + _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Content }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Media }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Forms }); + + _database.Insert(new UserGroup2AppDto { UserGroupId = 4, AppAlias = Constants.Applications.Translation }); + } + + private void CreatePropertyTypeGroupData() + { + // Insert property groups only if the corresponding content type node record exists (which may or may not have been created depending on configuration + // of media or member types to create). + + // Media property groups. + if (_database.Exists(1032)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 3, + UniqueId = new Guid(Constants.PropertyTypeGroups.Image), + ContentTypeNodeId = 1032, + Text = "Image", + Alias = "image", + SortOrder = 1, + }); + } + + if (_database.Exists(1033)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 4, + UniqueId = new Guid(Constants.PropertyTypeGroups.File), + ContentTypeNodeId = 1033, + Text = "File", + Alias = "file", + SortOrder = 1, + }); + } + + if (_database.Exists(1034)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 52, + UniqueId = new Guid(Constants.PropertyTypeGroups.Video), + ContentTypeNodeId = 1034, + Text = "Video", + Alias = "video", + SortOrder = 1, + }); + } + + if (_database.Exists(1035)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 53, + UniqueId = new Guid(Constants.PropertyTypeGroups.Audio), + ContentTypeNodeId = 1035, + Text = "Audio", + Alias = "audio", + SortOrder = 1, + }); + } + + if (_database.Exists(1036)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 54, + UniqueId = new Guid(Constants.PropertyTypeGroups.Article), + ContentTypeNodeId = 1036, + Text = "Article", + Alias = "article", + SortOrder = 1, + }); + } + + if (_database.Exists(1037)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 55, + UniqueId = new Guid(Constants.PropertyTypeGroups.VectorGraphics), + ContentTypeNodeId = 1037, + Text = "Vector Graphics", + Alias = "vectorGraphics", + SortOrder = 1, + }); + } + + // Membership property group. + if (_database.Exists(1044)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 11, + UniqueId = new Guid(Constants.PropertyTypeGroups.Membership), + ContentTypeNodeId = 1044, + Text = Constants.Conventions.Member.StandardPropertiesGroupName, + Alias = Constants.Conventions.Member.StandardPropertiesGroupAlias, + SortOrder = 1, + }); + } + } + + private void CreatePropertyTypeData() + { + // Insert property types only if the corresponding property group record exists (which may or may not have been created depending on configuration + // of media or member types to create). + + // Media property types. + if (_database.Exists(3)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 6, + UniqueId = 6.ToGuid(), + DataTypeId = Constants.DataTypes.ImageCropper, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.File, + Name = "Image", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 7, + UniqueId = 7.ToGuid(), + DataTypeId = Constants.DataTypes.LabelInt, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Width, + Name = "Width", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in pixels", + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 8, + UniqueId = 8.ToGuid(), + DataTypeId = Constants.DataTypes.LabelInt, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Height, + Name = "Height", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in pixels", + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 9, + UniqueId = 9.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 10, + UniqueId = 10.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(4)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 24, + UniqueId = 24.ToGuid(), + DataTypeId = Constants.DataTypes.Upload, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.File, + Name = "File", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 25, + UniqueId = 25.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 26, + UniqueId = 26.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(52)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 40, + UniqueId = 40.ToGuid(), + DataTypeId = Constants.DataTypes.UploadVideo, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.File, + Name = "Video", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 41, + UniqueId = 41.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 42, + UniqueId = 42.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(53)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 43, + UniqueId = 43.ToGuid(), + DataTypeId = Constants.DataTypes.UploadAudio, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.File, + Name = "Audio", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 44, + UniqueId = 44.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 45, + UniqueId = 45.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(54)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 46, + UniqueId = 46.ToGuid(), + DataTypeId = Constants.DataTypes.UploadArticle, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.File, + Name = "Article", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 47, + UniqueId = 47.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 48, + UniqueId = 48.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(55)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 49, + UniqueId = 49.ToGuid(), + DataTypeId = Constants.DataTypes.UploadVectorGraphics, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.File, + Name = "Vector Graphics", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 50, + UniqueId = 50.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 51, + UniqueId = 51.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + // Membership property types. + if (_database.Exists(11)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 28, + UniqueId = 28.ToGuid(), + DataTypeId = Constants.DataTypes.Textarea, + ContentTypeId = 1044, + PropertyTypeGroupId = 11, + Alias = Constants.Conventions.Member.Comments, + Name = Constants.Conventions.Member.CommentsLabel, + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + } + } + + private void CreateLanguageData() => + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.Languages, + "en-us", + new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }, + Constants.DatabaseSchema.Tables.Language, + "id"); + + private void CreateContentChildTypeData() + { + // Insert data if the corresponding Node records exist (which may or may not have been created depending on configuration + // of media types to create). + if (!_database.Exists(1031)) + { + return; + } + + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, + new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); + + for (var i = 1032; i <= 1037; i++) + { + if (_database.Exists(i)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, + new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = i }); + } + } + } + + private void CreateDataTypeData() + { + void InsertDataTypeDto(int id, string editorAlias, string dbType, string? configuration = null) + { + var dataTypeDto = new DataTypeDto { NodeId = id, EditorAlias = editorAlias, DbType = dbType }; + + if (configuration != null) + { + dataTypeDto.Configuration = configuration; + } + + if (_database.Exists(id)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + } + + // layouts for the list view + const string cardLayout = + "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; + const string listLayout = + "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": 1,\"selected\": true}"; + const string layouts = "[" + cardLayout + "," + listLayout + "]"; + + // Insert data types only if the corresponding Node record exists (which may or may not have been created depending on configuration + // of data types to create). + if (_database.Exists(Constants.DataTypes.Boolean)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Boolean, + EditorAlias = Constants.PropertyEditors.Aliases.Boolean, + DbType = "Integer", + }); + } + + if (_database.Exists(-51)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -51, + EditorAlias = Constants.PropertyEditors.Aliases.Integer, + DbType = "Integer", + }); + } + + if (_database.Exists(-87)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = -87, + EditorAlias = Constants.PropertyEditors.Aliases.TinyMce, + DbType = "Ntext", + Configuration = + "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}", + }); + } + + if (_database.Exists(Constants.DataTypes.Textbox)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Textbox, + EditorAlias = Constants.PropertyEditors.Aliases.TextBox, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(Constants.DataTypes.Textarea)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Textarea, + EditorAlias = Constants.PropertyEditors.Aliases.TextArea, + DbType = "Ntext", + }); + } + + if (_database.Exists(Constants.DataTypes.Upload)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Upload, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + }); + } + + InsertDataTypeDto(Constants.DataTypes.LabelString, Constants.PropertyEditors.Aliases.Label, "Nvarchar", + "{\"umbracoDataValueType\":\"STRING\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelInt, Constants.PropertyEditors.Aliases.Label, "Integer", + "{\"umbracoDataValueType\":\"INT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBigint, Constants.PropertyEditors.Aliases.Label, "Nvarchar", + "{\"umbracoDataValueType\":\"BIGINT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDateTime, Constants.PropertyEditors.Aliases.Label, "Date", + "{\"umbracoDataValueType\":\"DATETIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDecimal, Constants.PropertyEditors.Aliases.Label, "Decimal", + "{\"umbracoDataValueType\":\"DECIMAL\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelTime, Constants.PropertyEditors.Aliases.Label, "Date", + "{\"umbracoDataValueType\":\"TIME\"}"); + + if (_database.Exists(Constants.DataTypes.DateTime)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DateTime, + EditorAlias = Constants.PropertyEditors.Aliases.DateTime, + DbType = "Date", + }); + } + + if (_database.Exists(-37)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -37, + EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, + DbType = "Nvarchar", + }); + } + + InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Nvarchar", "{\"multiple\":false}"); + + if (_database.Exists(-40)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -40, + EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(-41)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -41, + EditorAlias = "Umbraco.DateTime", + DbType = "Date", + Configuration = "{\"format\":\"YYYY-MM-DD\"}", + }); + } + + InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Nvarchar", "{\"multiple\":true}"); + + if (_database.Exists(-43)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -43, + EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(Constants.DataTypes.Tags)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Tags, + EditorAlias = Constants.PropertyEditors.Aliases.Tags, + DbType = "Ntext", + Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}", + }); + } + + if (_database.Exists(Constants.DataTypes.ImageCropper)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.ImageCropper, + EditorAlias = Constants.PropertyEditors.Aliases.ImageCropper, + DbType = "Ntext", + }); + } + + if (_database.Exists(Constants.DataTypes.DefaultContentListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultContentListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + + layouts + + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.DefaultMediaListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultMediaListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + + layouts + + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.DefaultMembersListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultMembersListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":10, \"orderBy\":\"username\", \"orderDirection\":\"asc\", \"includeProperties\":[{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}", + }); + } + + // New UDI pickers with newer Ids + if (_database.Exists(1046)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1046, + EditorAlias = Constants.PropertyEditors.Aliases.ContentPicker, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(1047)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1047, + EditorAlias = Constants.PropertyEditors.Aliases.MemberPicker, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(1048)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1048, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, + DbType = "Ntext", + }); + } + + if (_database.Exists(1049)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = 1049, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, + DbType = "Ntext", + Configuration = "{\"multiPicker\":1}", + }); + } + + if (_database.Exists(1050)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1050, + EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, + DbType = "Ntext", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadVideo)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVideo, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadAudio)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadAudio, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadArticle)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadArticle, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadVectorGraphics)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}", + }); + } + + if (_database.Exists(1051)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1051, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}", + }); + } + + if (_database.Exists(1052)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1052, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": true}", + }); + } + + if (_database.Exists(1053)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1053, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}", + }); + } + + if (_database.Exists(1054)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1054, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + + "\", \"multiple\": true}", + }); + } + } + + private void CreateRelationTypeData() + { + CreateRelationTypeData(1, Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, + Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, true, false); + CreateRelationTypeData(2, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, + Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, false, false); + CreateRelationTypeData(3, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, + Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, Constants.ObjectTypes.Media, + Constants.ObjectTypes.Media, false, false); + CreateRelationTypeData(4, Constants.Conventions.RelationTypes.RelatedMediaAlias, + Constants.Conventions.RelationTypes.RelatedMediaName, null, null, false, true); + CreateRelationTypeData(5, Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedDocumentName, null, null, false, true); + } + + private void CreateRelationTypeData(int id, string alias, string name, Guid? parentObjectType, + Guid? childObjectType, bool dual, bool isDependency) + { + var relationType = new RelationTypeDto + { + Id = id, + Alias = alias, + ChildObjectType = childObjectType, + ParentObjectType = parentObjectType, + Dual = dual, + Name = name, + IsDependency = isDependency, + }; + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); + + _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); + } + + private void CreateKeyValueData() + { + // On install, initialize the umbraco migration plan with the final state. + var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); + var stateValueKey = upgrader.StateValueKey; + var finalState = upgrader.Plan.FinalState; + + _database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, + new KeyValueDto { Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now }); + } + + private void CreateLogViewerQueryData() + { + LogViewerQueryDto[] defaultData = MigrateLogViewerQueriesFromFileToDb._defaultLogQueries.ToArray(); + + for (var i = 0; i < defaultData.Length; i++) + { + LogViewerQueryDto dto = defaultData[i]; + dto.Id = i + 1; + _database.Insert(Constants.DatabaseSchema.Tables.LogViewerQuery, "id", false, dto); + } + } + + private void ConditionalInsert( + string configKey, + string id, + TDto dto, + string tableName, + string primaryKeyName, + bool autoIncrement = false) + { + var alwaysInsert = _entitiesToAlwaysCreate.ContainsKey(configKey) && + _entitiesToAlwaysCreate[configKey].InvariantContains(id); + + InstallDefaultDataSettings installDefaultDataSettings = _installDefaultDataSettings.Get(configKey); + + // If there's no configuration, we assume to create. + if (installDefaultDataSettings == null) + { + alwaysInsert = true; + } + + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.None) + { + return; + } + + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values && + !installDefaultDataSettings.Values.InvariantContains(id)) + { + return; + } + + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.ExceptValues && + installDefaultDataSettings.Values.InvariantContains(id)) + { + return; + } + + _database.Insert(tableName, primaryKeyName, autoIncrement, dto); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 0b296b2d77..8c1e0e2a54 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Data.SqlTypes; -using System.Linq; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,85 +12,85 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -namespace Umbraco.Cms.Infrastructure.Migrations.Install -{ - /// - /// Creates the initial database schema during install. - /// - public class DatabaseSchemaCreator - { - // all tables, in order - internal static readonly List OrderedTables = new() - { - typeof(UserDto), - typeof(NodeDto), - typeof(ContentTypeDto), - typeof(TemplateDto), - typeof(ContentDto), - typeof(ContentVersionDto), - typeof(MediaVersionDto), - typeof(DocumentDto), - typeof(ContentTypeTemplateDto), - typeof(DataTypeDto), - typeof(DictionaryDto), - typeof(LanguageDto), - typeof(LanguageTextDto), - typeof(DomainDto), - typeof(LogDto), - typeof(MacroDto), - typeof(MacroPropertyDto), - typeof(MemberPropertyTypeDto), - typeof(MemberDto), - typeof(Member2MemberGroupDto), - typeof(PropertyTypeGroupDto), - typeof(PropertyTypeDto), - typeof(PropertyDataDto), - typeof(RelationTypeDto), - typeof(RelationDto), - typeof(TagDto), - typeof(TagRelationshipDto), - typeof(ContentType2ContentTypeDto), - typeof(ContentTypeAllowedContentTypeDto), - typeof(User2NodeNotifyDto), - typeof(ServerRegistrationDto), - typeof(AccessDto), - typeof(AccessRuleDto), - typeof(CacheInstructionDto), - typeof(ExternalLoginDto), - typeof(ExternalLoginTokenDto), - typeof(TwoFactorLoginDto), - typeof(RedirectUrlDto), - typeof(LockDto), - typeof(UserGroupDto), - typeof(User2UserGroupDto), - typeof(UserGroup2NodePermissionDto), - typeof(UserGroup2AppDto), - typeof(UserStartNodeDto), - typeof(ContentNuDto), - typeof(DocumentVersionDto), - typeof(KeyValueDto), - typeof(UserLoginDto), - typeof(ConsentDto), - typeof(AuditEntryDto), - typeof(ContentVersionCultureVariationDto), - typeof(DocumentCultureVariationDto), - typeof(ContentScheduleDto), - typeof(LogViewerQueryDto), - typeof(ContentVersionCleanupPolicyDto), - typeof(UserGroup2NodeDto), - typeof(CreatedPackageSchemaDto) - }; +namespace Umbraco.Cms.Infrastructure.Migrations.Install; - private readonly IUmbracoDatabase _database; - private readonly IEventAggregator _eventAggregator; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _defaultDataCreationSettings; +/// +/// Creates the initial database schema during install. +/// +public class DatabaseSchemaCreator +{ + // all tables, in order + internal static readonly List _orderedTables = new() + { + typeof(UserDto), + typeof(NodeDto), + typeof(ContentTypeDto), + typeof(TemplateDto), + typeof(ContentDto), + typeof(ContentVersionDto), + typeof(MediaVersionDto), + typeof(DocumentDto), + typeof(ContentTypeTemplateDto), + typeof(DataTypeDto), + typeof(DictionaryDto), + typeof(LanguageDto), + typeof(LanguageTextDto), + typeof(DomainDto), + typeof(LogDto), + typeof(MacroDto), + typeof(MacroPropertyDto), + typeof(MemberPropertyTypeDto), + typeof(MemberDto), + typeof(Member2MemberGroupDto), + typeof(PropertyTypeGroupDto), + typeof(PropertyTypeDto), + typeof(PropertyDataDto), + typeof(RelationTypeDto), + typeof(RelationDto), + typeof(TagDto), + typeof(TagRelationshipDto), + typeof(ContentType2ContentTypeDto), + typeof(ContentTypeAllowedContentTypeDto), + typeof(User2NodeNotifyDto), + typeof(ServerRegistrationDto), + typeof(AccessDto), + typeof(AccessRuleDto), + typeof(CacheInstructionDto), + typeof(ExternalLoginDto), + typeof(ExternalLoginTokenDto), + typeof(TwoFactorLoginDto), + typeof(RedirectUrlDto), + typeof(LockDto), + typeof(UserGroupDto), + typeof(User2UserGroupDto), + typeof(UserGroup2NodePermissionDto), + typeof(UserGroup2AppDto), + typeof(UserStartNodeDto), + typeof(ContentNuDto), + typeof(DocumentVersionDto), + typeof(KeyValueDto), + typeof(UserLoginDto), + typeof(ConsentDto), + typeof(AuditEntryDto), + typeof(ContentVersionCultureVariationDto), + typeof(DocumentCultureVariationDto), + typeof(ContentScheduleDto), + typeof(LogViewerQueryDto), + typeof(ContentVersionCleanupPolicyDto), + typeof(UserGroup2NodeDto), + typeof(CreatedPackageSchemaDto), + typeof(UserGroup2LanguageDto) + }; + + private readonly IUmbracoDatabase _database; + private readonly IOptionsMonitor _defaultDataCreationSettings; + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IUmbracoVersion _umbracoVersion; public DatabaseSchemaCreator( IUmbracoDatabase? database, @@ -111,427 +107,437 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _eventAggregator = eventAggregator; _defaultDataCreationSettings = defaultDataCreationSettings; - if (_database?.SqlContext?.SqlSyntax == null) - { - throw new InvalidOperationException("No SqlContext has been assigned to the database"); - } - } - - private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; - - /// - /// Drops all Umbraco tables in the db. - /// - internal void UninstallDatabaseSchema() + if (_database?.SqlContext?.SqlSyntax == null) { - _logger.LogInformation("Start UninstallDatabaseSchema"); + throw new InvalidOperationException("No SqlContext has been assigned to the database"); + } + } - foreach (Type table in OrderedTables.AsEnumerable().Reverse()) + private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; + + /// + /// Drops all Umbraco tables in the db. + /// + internal void UninstallDatabaseSchema() + { + _logger.LogInformation("Start UninstallDatabaseSchema"); + + foreach (Type table in _orderedTables.AsEnumerable().Reverse()) + { + TableNameAttribute? tableNameAttribute = table.FirstAttribute(); + var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; + + _logger.LogInformation("Uninstall {TableName}", tableName); + + try { - TableNameAttribute? tableNameAttribute = table.FirstAttribute(); - var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; - - _logger.LogInformation("Uninstall {TableName}", tableName); - - try + if (TableExists(tableName)) { - if (TableExists(tableName)) - { - DropTable(tableName); - } - } - catch (Exception ex) - { - //swallow this for now, not sure how best to handle this with diff databases... though this is internal - // and only used for unit tests. If this fails its because the table doesn't exist... generally! - _logger.LogError(ex, "Could not drop table {TableName}", tableName); + DropTable(tableName); } } + catch (Exception ex) + { + //swallow this for now, not sure how best to handle this with diff databases... though this is internal + // and only used for unit tests. If this fails its because the table doesn't exist... generally! + _logger.LogError(ex, "Could not drop table {TableName}", tableName); + } + } + } + + /// + /// Initializes the database by creating the umbraco db schema. + /// + /// This needs to execute as part of a transaction. + public void InitializeDatabaseSchema() + { + if (!_database.InTransaction) + { + throw new InvalidOperationException("Database is not in a transaction."); } - /// - /// Initializes the database by creating the umbraco db schema. - /// - /// This needs to execute as part of a transaction. - public void InitializeDatabaseSchema() + var eventMessages = new EventMessages(); + var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages); + FireBeforeCreation(creatingNotification); + + if (creatingNotification.Cancel == false) { - if (!_database.InTransaction) + var dataCreation = new DatabaseDataCreator( + _database, _loggerFactory.CreateLogger(), + _umbracoVersion, + _defaultDataCreationSettings); + foreach (Type table in _orderedTables) { - throw new InvalidOperationException("Database is not in a transaction."); - } - - var eventMessages = new EventMessages(); - var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages); - FireBeforeCreation(creatingNotification); - - if (creatingNotification.Cancel == false) - { - var dataCreation = new DatabaseDataCreator( - _database, _loggerFactory.CreateLogger(), - _umbracoVersion, - _defaultDataCreationSettings); - foreach (Type table in OrderedTables) - { - CreateTable(false, table, dataCreation); - } - } - - DatabaseSchemaCreatedNotification createdNotification = - new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); - FireAfterCreation(createdNotification); - } - - /// - /// Validates the schema of the current database. - /// - internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(OrderedTables); - - internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) - { - var result = new DatabaseSchemaResult(); - - result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) - .Select(x => new DbIndexDefinition(x))); - - result.TableDefinitions.AddRange(orderedTables - .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); - - ValidateDbTables(result); - ValidateDbColumns(result); - ValidateDbIndexes(result); - ValidateDbConstraints(result); - - return result; - } - - /// - /// This validates the Primary/Foreign keys in the database - /// - /// - /// - /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database - /// with non PK/FK constraints. - /// Any unique "constraints" in the database are done with unique indexes. - /// - private void ValidateDbConstraints(DatabaseSchemaResult result) - { - //Check constraints in configured database against constraints in schema - var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); - var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); - var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); - - var unknownConstraintsInDatabase = constraintsInDatabase.Where( - x => x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && x.Item3.InvariantStartsWith("IX_") == false - ).Select(x => x.Item3).ToList(); - - var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).Where(x => x is not null).ToList(); - var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)).Where(x => x.IsNullOrWhiteSpace() == false).ToList(); - - // Add valid and invalid foreign key differences to the result object - // We'll need to do invariant contains with case insensitivity because foreign key, primary key is not standardized - // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. - foreach (var unknown in unknownConstraintsInDatabase) - { - if (foreignKeysInSchema!.InvariantContains(unknown) || primaryKeysInSchema!.InvariantContains(unknown)) - { - result.ValidConstraints.Add(unknown); - } - else - { - result.Errors.Add(new Tuple("Unknown", unknown)); - } - } - - // Foreign keys: - IEnumerable validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var foreignKey in validForeignKeyDifferences) - { - if (foreignKey is not null) - { - result.ValidConstraints.Add(foreignKey); - } - } - - IEnumerable invalidForeignKeyDifferences = foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var foreignKey in invalidForeignKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", foreignKey ?? "NULL")); - } - - // Primary keys: - // Add valid and invalid primary key differences to the result object - IEnumerable validPrimaryKeyDifferences = primaryKeysInDatabase!.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)!; - foreach (var primaryKey in validPrimaryKeyDifferences) - { - result.ValidConstraints.Add(primaryKey); - } - - IEnumerable invalidPrimaryKeyDifferences = primaryKeysInDatabase!.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)! - .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase))!; - foreach (var primaryKey in invalidPrimaryKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", primaryKey)); + CreateTable(false, table, dataCreation); } } - private void ValidateDbColumns(DatabaseSchemaResult result) + DatabaseSchemaCreatedNotification createdNotification = + new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); + FireAfterCreation(createdNotification); + } + + /// + /// Validates the schema of the current database. + /// + internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(_orderedTables); + + internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) + { + var result = new DatabaseSchemaResult(); + + result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) + .Select(x => new DbIndexDefinition(x))); + + result.TableDefinitions.AddRange(orderedTables + .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); + + ValidateDbTables(result); + ValidateDbColumns(result); + ValidateDbIndexes(result); + ValidateDbConstraints(result); + + return result; + } + + /// + /// This validates the Primary/Foreign keys in the database + /// + /// + /// + /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database + /// with non PK/FK constraints. + /// Any unique "constraints" in the database are done with unique indexes. + /// + private void ValidateDbConstraints(DatabaseSchemaResult result) + { + //Check constraints in configured database against constraints in schema + var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); + var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")) + .Select(x => x.Item3).ToList(); + var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")) + .Select(x => x.Item3).ToList(); + + var unknownConstraintsInDatabase = constraintsInDatabase.Where( + x => x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && + x.Item3.InvariantStartsWith("IX_") == false + ).Select(x => x.Item3).ToList(); + + var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)) + .Where(x => x is not null).ToList(); + var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) + .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); + + // Add valid and invalid foreign key differences to the result object + // We'll need to do invariant contains with case insensitivity because foreign key, primary key is not standardized + // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. + foreach (var unknown in unknownConstraintsInDatabase) { - //Check columns in configured database against columns in schema - IEnumerable columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); - var columnsPerTableInDatabase = - columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); - var columnsPerTableInSchema = result.TableDefinitions - .SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); - //Add valid and invalid column differences to the result object - IEnumerable validColumnDifferences = - columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var column in validColumnDifferences) + if (foreignKeysInSchema!.InvariantContains(unknown) || primaryKeysInSchema!.InvariantContains(unknown)) { - result.ValidColumns.Add(column); - } - - IEnumerable invalidColumnDifferences = - columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, - StringComparer.InvariantCultureIgnoreCase)); - foreach (var column in invalidColumnDifferences) - { - result.Errors.Add(new Tuple("Column", column)); - } - } - - private void ValidateDbTables(DatabaseSchemaResult result) - { - //Check tables in configured database against tables in schema - var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); - var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); - //Add valid and invalid table differences to the result object - IEnumerable validTableDifferences = - tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var tableName in validTableDifferences) - { - if (tableName is not null) - { - result.ValidTables.Add(tableName); - } - } - - IEnumerable invalidTableDifferences = - tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var tableName in invalidTableDifferences) - { - result.Errors.Add(new Tuple("Table", tableName ?? "NULL")); - } - } - - private void ValidateDbIndexes(DatabaseSchemaResult result) - { - //These are just column indexes NOT constraints or Keys - //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); - var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); - var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); - - //Add valid and invalid index differences to the result object - IEnumerable validColIndexDifferences = - colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var index in validColIndexDifferences) - { - if (index is not null) - { - result.ValidIndexes.Add(index); - } - } - - IEnumerable invalidColIndexDifferences = - colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var index in invalidColIndexDifferences) - { - result.Errors.Add(new Tuple("Index", index ?? "NULL")); - } - } - - #region Notifications - - /// - /// Publishes the notification. - /// - /// Cancelable notification marking the creation having begun. - internal virtual void FireBeforeCreation(DatabaseSchemaCreatingNotification notification) => - _eventAggregator.Publish(notification); - - /// - /// Publishes the notification. - /// - /// Notification marking the creation having completed. - internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notification) => - _eventAggregator.Publish(notification); - - #endregion - - #region Utilities - - /// - /// Returns whether a table with the specified exists in the database. - /// - /// The name of the table. - /// true if the table exists; otherwise false. - /// - /// - /// if (schemaHelper.TableExist("MyTable")) - /// { - /// // do something when the table exists - /// } - /// - /// - public bool TableExists(string? tableName) => tableName is not null && SqlSyntax.DoesTableExist(_database, tableName); - - /// - /// Returns whether the table for the specified exists in the database. - /// - /// The type representing the DTO/table. - /// true if the table exists; otherwise false. - /// - /// - /// if (schemaHelper.TableExist<MyDto>) - /// { - /// // do something when the table exists - /// } - /// - /// - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - public bool TableExists() - { - TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); - return table != null && TableExists(table.Name); - } - - /// - /// Creates a new table in the database based on the type of . - /// - /// The type representing the DTO/table. - /// Whether the table should be overwritten if it already exists. - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. - /// - internal void CreateTable(bool overwrite = false) - where T : new() - { - Type tableType = typeof(T); - CreateTable( - overwrite, - tableType, - new DatabaseDataCreator( - _database, - _loggerFactory.CreateLogger(), - _umbracoVersion, - _defaultDataCreationSettings)); - } - - /// - /// Creates a new table in the database for the specified . - /// - /// Whether the table should be overwritten if it already exists. - /// The representing the table. - /// - /// - /// If has been decorated with an , the name from - /// that attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. - /// This need to execute as part of a transaction. - /// - internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) - { - if (!_database.InTransaction) - { - throw new InvalidOperationException("Database is not in a transaction."); - } - - TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); - var tableName = tableDefinition.Name; - var tableExist = TableExists(tableName); - if (string.IsNullOrEmpty(tableName)) - { - throw new SqlNullValueException("Tablename was null"); - } - if (overwrite && tableExist) - { - _logger.LogInformation("Table {TableName} already exists, but will be recreated", tableName); - - DropTable(tableName); - tableExist = false; - } - - if (tableExist) - { - // The table exists and was not recreated/overwritten. - _logger.LogInformation("Table {TableName} already exists - no changes were made", tableName); - return; - } - - //Execute the Create Table sql - SqlSyntax.HandleCreateTable(_database, tableDefinition); - - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - { - // This should probably delegate to whole thing to the syntax provider - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); - } - - //Call the NewTable-event to trigger the insert of base/default data - //OnNewTable(tableName, _db, e, _logger); - - dataCreation.InitializeBaseData(tableName); - - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - { - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); - } - - if (overwrite) - { - _logger.LogInformation("Table {TableName} was recreated", tableName); + result.ValidConstraints.Add(unknown); } else { - _logger.LogInformation("New table {TableName} was created", tableName); + result.Errors.Add(new Tuple("Unknown", unknown)); } } - /// - /// Drops the table for the specified . - /// - /// The type representing the DTO/table. - /// - /// - /// schemaHelper.DropTable<MyDto>); - /// - /// - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - public void DropTable(string? tableName) + // Foreign keys: + IEnumerable validForeignKeyDifferences = + foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var foreignKey in validForeignKeyDifferences) { - var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); - _database.Execute(sql); + if (foreignKey is not null) + { + result.ValidConstraints.Add(foreignKey); + } } - #endregion + IEnumerable invalidForeignKeyDifferences = foreignKeysInDatabase + .Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var foreignKey in invalidForeignKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", foreignKey ?? "NULL")); + } + + // Primary keys: + // Add valid and invalid primary key differences to the result object + IEnumerable validPrimaryKeyDifferences = + primaryKeysInDatabase!.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)!; + foreach (var primaryKey in validPrimaryKeyDifferences) + { + result.ValidConstraints.Add(primaryKey); + } + + IEnumerable invalidPrimaryKeyDifferences = + primaryKeysInDatabase!.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)! + .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase))!; + foreach (var primaryKey in invalidPrimaryKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", primaryKey)); + } } + + private void ValidateDbColumns(DatabaseSchemaResult result) + { + //Check columns in configured database against columns in schema + IEnumerable columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); + var columnsPerTableInDatabase = + columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); + var columnsPerTableInSchema = result.TableDefinitions + .SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); + //Add valid and invalid column differences to the result object + IEnumerable validColumnDifferences = + columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var column in validColumnDifferences) + { + result.ValidColumns.Add(column); + } + + IEnumerable invalidColumnDifferences = + columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, + StringComparer.InvariantCultureIgnoreCase)); + foreach (var column in invalidColumnDifferences) + { + result.Errors.Add(new Tuple("Column", column)); + } + } + + private void ValidateDbTables(DatabaseSchemaResult result) + { + //Check tables in configured database against tables in schema + var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); + var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); + //Add valid and invalid table differences to the result object + IEnumerable validTableDifferences = + tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var tableName in validTableDifferences) + { + if (tableName is not null) + { + result.ValidTables.Add(tableName); + } + } + + IEnumerable invalidTableDifferences = + tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var tableName in invalidTableDifferences) + { + result.Errors.Add(new Tuple("Table", tableName ?? "NULL")); + } + } + + private void ValidateDbIndexes(DatabaseSchemaResult result) + { + //These are just column indexes NOT constraints or Keys + //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); + var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + + //Add valid and invalid index differences to the result object + IEnumerable validColIndexDifferences = + colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var index in validColIndexDifferences) + { + if (index is not null) + { + result.ValidIndexes.Add(index); + } + } + + IEnumerable invalidColIndexDifferences = + colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var index in invalidColIndexDifferences) + { + result.Errors.Add(new Tuple("Index", index ?? "NULL")); + } + } + + #region Notifications + + /// + /// Publishes the notification. + /// + /// Cancelable notification marking the creation having begun. + internal virtual void FireBeforeCreation(DatabaseSchemaCreatingNotification notification) => + _eventAggregator.Publish(notification); + + /// + /// Publishes the notification. + /// + /// Notification marking the creation having completed. + internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notification) => + _eventAggregator.Publish(notification); + + #endregion + + #region Utilities + + /// + /// Returns whether a table with the specified exists in the database. + /// + /// The name of the table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist("MyTable")) + /// { + /// // do something when the table exists + /// } + /// + /// + public bool TableExists(string? tableName) => + tableName is not null && SqlSyntax.DoesTableExist(_database, tableName); + + /// + /// Returns whether the table for the specified exists in the database. + /// + /// The type representing the DTO/table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist<MyDto>) + /// { + /// // do something when the table exists + /// } + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + public bool TableExists() + { + TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + return table != null && TableExists(table.Name); + } + + /// + /// Creates a new table in the database based on the type of . + /// + /// The type representing the DTO/table. + /// Whether the table should be overwritten if it already exists. + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// + internal void CreateTable(bool overwrite = false) + where T : new() + { + Type tableType = typeof(T); + CreateTable( + overwrite, + tableType, + new DatabaseDataCreator( + _database, + _loggerFactory.CreateLogger(), + _umbracoVersion, + _defaultDataCreationSettings)); + } + + /// + /// Creates a new table in the database for the specified . + /// + /// Whether the table should be overwritten if it already exists. + /// The representing the table. + /// + /// + /// If has been decorated with an , the name from + /// that attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// This need to execute as part of a transaction. + /// + internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) + { + if (!_database.InTransaction) + { + throw new InvalidOperationException("Database is not in a transaction."); + } + + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); + var tableName = tableDefinition.Name; + var tableExist = TableExists(tableName); + if (string.IsNullOrEmpty(tableName)) + { + throw new SqlNullValueException("Tablename was null"); + } + + if (overwrite && tableExist) + { + _logger.LogInformation("Table {TableName} already exists, but will be recreated", tableName); + + DropTable(tableName); + tableExist = false; + } + + if (tableExist) + { + // The table exists and was not recreated/overwritten. + _logger.LogInformation("Table {TableName} already exists - no changes were made", tableName); + return; + } + + //Execute the Create Table sql + SqlSyntax.HandleCreateTable(_database, tableDefinition); + + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { + // This should probably delegate to whole thing to the syntax provider + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); + } + + //Call the NewTable-event to trigger the insert of base/default data + //OnNewTable(tableName, _db, e, _logger); + + dataCreation.InitializeBaseData(tableName); + + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); + } + + if (overwrite) + { + _logger.LogInformation("Table {TableName} was recreated", tableName); + } + else + { + _logger.LogInformation("New table {TableName} was created", tableName); + } + } + + /// + /// Drops the table for the specified . + /// + /// The type representing the DTO/table. + /// + /// + /// schemaHelper.DropTable<MyDto>); + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + public void DropTable(string? tableName) + { + var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); + _database.Execute(sql); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs index 7e5fdf11f9..860ed870f7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs @@ -1,25 +1,22 @@ -using System; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Creates the initial database schema during install. +/// +public class DatabaseSchemaCreatorFactory { - /// - /// Creates the initial database schema during install. - /// - public class DatabaseSchemaCreatorFactory - { - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IEventAggregator _eventAggregator; - private readonly IOptionsMonitor _installDefaultDataSettings; + private readonly IEventAggregator _eventAggregator; + private readonly IOptionsMonitor _installDefaultDataSettings; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IUmbracoVersion _umbracoVersion; public DatabaseSchemaCreatorFactory( ILogger logger, @@ -35,9 +32,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _installDefaultDataSettings = installDefaultDataSettings; } - public DatabaseSchemaCreator Create(IUmbracoDatabase? database) - { - return new DatabaseSchemaCreator(database, _logger, _loggerFactory, _umbracoVersion, _eventAggregator, _installDefaultDataSettings); - } - } + public DatabaseSchemaCreator Create(IUmbracoDatabase? database) => new DatabaseSchemaCreator(database, _logger, + _loggerFactory, _umbracoVersion, _eventAggregator, _installDefaultDataSettings); } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs index 83c4fd4cef..5865713cbb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs @@ -1,103 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Represents ... +/// +public class DatabaseSchemaResult { - /// - /// Represents ... - /// - public class DatabaseSchemaResult + public DatabaseSchemaResult() { - public DatabaseSchemaResult() + Errors = new List>(); + TableDefinitions = new List(); + ValidTables = new List(); + ValidColumns = new List(); + ValidConstraints = new List(); + ValidIndexes = new List(); + IndexDefinitions = new List(); + } + + public List> Errors { get; } + + public List TableDefinitions { get; } + + public List ValidTables { get; } + + // TODO: what are these exactly? TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? + internal List IndexDefinitions { get; } + + public List ValidColumns { get; } + + public List ValidConstraints { get; } + + public List ValidIndexes { get; } + + /// + /// Determines whether the database contains an installed version. + /// + /// + /// A database contains an installed version when it contains at least one valid table. + /// + public bool DetermineHasInstalledVersion() => ValidTables.Count > 0; + + /// + /// Gets a summary of the schema validation result + /// + /// A string containing a human readable string with a summary message + public string GetSummary() + { + var sb = new StringBuilder(); + if (Errors.Any() == false) { - Errors = new List>(); - TableDefinitions = new List(); - ValidTables = new List(); - ValidColumns = new List(); - ValidConstraints = new List(); - ValidIndexes = new List(); - IndexDefinitions = new List(); - } - - public List> Errors { get; } - - public List TableDefinitions { get; } - - // TODO: what are these exactly? TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? - internal List IndexDefinitions { get; } - - public List ValidTables { get; } - - public List ValidColumns { get; } - - public List ValidConstraints { get; } - - public List ValidIndexes { get; } - - /// - /// Determines whether the database contains an installed version. - /// - /// - /// A database contains an installed version when it contains at least one valid table. - /// - public bool DetermineHasInstalledVersion() - { - return ValidTables.Count > 0; - } - - /// - /// Gets a summary of the schema validation result - /// - /// A string containing a human readable string with a summary message - public string GetSummary() - { - var sb = new StringBuilder(); - if (Errors.Any() == false) - { - sb.AppendLine("The database schema validation didn't find any errors."); - return sb.ToString(); - } - - //Table error summary - if (Errors.Any(x => x.Item1.Equals("Table"))) - { - sb.AppendLine("The following tables were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Table")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Column error summary - if (Errors.Any(x => x.Item1.Equals("Column"))) - { - sb.AppendLine("The following columns were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Column")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Constraint error summary - if (Errors.Any(x => x.Item1.Equals("Constraint"))) - { - sb.AppendLine("The following constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Constraint")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Index error summary - if (Errors.Any(x => x.Item1.Equals("Index"))) - { - sb.AppendLine("The following indexes were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Index")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Unknown constraint error summary - if (Errors.Any(x => x.Item1.Equals("Unknown"))) - { - sb.AppendLine("The following unknown constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Unknown")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - + sb.AppendLine("The database schema validation didn't find any errors."); return sb.ToString(); } + + // Table error summary + if (Errors.Any(x => x.Item1.Equals("Table"))) + { + sb.AppendLine("The following tables were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Table")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Column error summary + if (Errors.Any(x => x.Item1.Equals("Column"))) + { + sb.AppendLine("The following columns were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Column")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Constraint error summary + if (Errors.Any(x => x.Item1.Equals("Constraint"))) + { + sb.AppendLine( + "The following constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Constraint")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Index error summary + if (Errors.Any(x => x.Item1.Equals("Index"))) + { + sb.AppendLine("The following indexes were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Index")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Unknown constraint error summary + if (Errors.Any(x => x.Item1.Equals("Unknown"))) + { + sb.AppendLine( + "The following unknown constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Unknown")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + return sb.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs index a25c161587..3e80d9ebc5 100644 --- a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs @@ -1,94 +1,91 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +/// +/// Represents a migration plan builder for merges. +/// +public class MergeBuilder { + private readonly List _migrations = new(); + private readonly MigrationPlan _plan; + private bool _with; + private string? _withLast; + /// - /// Represents a migration plan builder for merges. + /// Initializes a new instance of the class. /// - public class MergeBuilder + internal MergeBuilder(MigrationPlan plan) => _plan = plan; + + /// + /// Adds a transition to a target state through an empty migration. + /// + public MergeBuilder To(string targetState) + => To(targetState); + + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState, Type migration) { - private readonly MigrationPlan _plan; - private readonly List _migrations = new List(); - private string? _withLast; - private bool _with; - - /// - /// Initializes a new instance of the class. - /// - internal MergeBuilder(MigrationPlan plan) + if (_with) { - _plan = plan; + _withLast = targetState; + targetState = _plan.CreateRandomState(); + } + else + { + _migrations.Add(migration); } - /// - /// Adds a transition to a target state through an empty migration. - /// - public MergeBuilder To(string targetState) - => To(targetState); + _plan.To(targetState, migration); + return this; + } - /// - /// Adds a transition to a target state through a migration. - /// - public MergeBuilder To(string targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); - - /// - /// Adds a transition to a target state through a migration. - /// - public MergeBuilder To(string targetState, Type migration) + /// + /// Begins the second branch of the merge. + /// + public MergeBuilder With() + { + if (_with) { - if (_with) - { - _withLast = targetState; - targetState = _plan.CreateRandomState(); - } - else - { - _migrations.Add(migration); - } - - _plan.To(targetState, migration); - return this; + throw new InvalidOperationException("Cannot invoke With() twice."); } - /// - /// Begins the second branch of the merge. - /// - public MergeBuilder With() + _with = true; + return this; + } + + /// + /// Completes the merge. + /// + public MigrationPlan As(string targetState) + { + if (!_with) { - if (_with) - throw new InvalidOperationException("Cannot invoke With() twice."); - _with = true; - return this; + throw new InvalidOperationException("Cannot invoke As() without invoking With() first."); } - /// - /// Completes the merge. - /// - public MigrationPlan As(string targetState) + // reach final state + _plan.To(targetState); + + // restart at former end of branch2 + _plan.From(_withLast); + + // and replay all branch1 migrations + foreach (Type migration in _migrations) { - if (!_with) - { - throw new InvalidOperationException("Cannot invoke As() without invoking With() first."); - } - - // reach final state - _plan.To(targetState); - - // restart at former end of branch2 - _plan.From(_withLast); - - // and replay all branch1 migrations - foreach (var migration in _migrations) - { - _plan.To(_plan.CreateRandomState(), migration); - } - // reaching final state - _plan.To(targetState); - - return _plan; + _plan.To(_plan.CreateRandomState(), migration); } + + // reaching final state + _plan.To(targetState); + + return _plan; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs index 4ffc3ab1e4..a4c4c0c99a 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs @@ -11,118 +11,122 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides a base class to all migrations. +/// +public abstract partial class MigrationBase : IDiscoverable { /// - /// Provides a base class to all migrations. + /// Initializes a new instance of the class. /// - public abstract partial class MigrationBase : IDiscoverable + /// A migration context. + protected MigrationBase(IMigrationContext context) + => Context = context; + + /// + /// Builds an Alter expression. + /// + public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); + + /// + /// Gets the migration context. + /// + protected IMigrationContext Context { get; } + + /// + /// Gets the logger. + /// + protected ILogger Logger => Context.Logger; + + /// + /// Gets the Sql syntax. + /// + protected ISqlSyntaxProvider SqlSyntax => Context.SqlContext.SqlSyntax; + + /// + /// Gets the database instance. + /// + protected IUmbracoDatabase Database => Context.Database; + + /// + /// Gets the database type. + /// + protected DatabaseType DatabaseType => Context.Database.DatabaseType; + + /// + /// Builds a Create expression. + /// + public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); + + /// + /// Builds a Delete expression. + /// + public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); + + /// + /// Builds an Execute expression. + /// + public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); + + /// + /// Builds an Insert expression. + /// + public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); + + /// + /// Builds a Rename expression. + /// + public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); + + /// + /// Builds an Update expression. + /// + public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); + + /// + /// Runs the migration. + /// + public void Run() { - /// - /// Initializes a new instance of the class. - /// - /// A migration context. - protected MigrationBase(IMigrationContext context) - => Context = context; + Migrate(); - /// - /// Gets the migration context. - /// - protected IMigrationContext Context { get; } - - /// - /// Gets the logger. - /// - protected ILogger Logger => Context.Logger; - - /// - /// Gets the Sql syntax. - /// - protected ISqlSyntaxProvider SqlSyntax => Context.SqlContext.SqlSyntax; - - /// - /// Gets the database instance. - /// - protected IUmbracoDatabase Database => Context.Database; - - /// - /// Gets the database type. - /// - protected DatabaseType DatabaseType => Context.Database.DatabaseType; - - /// - /// Creates a new Sql statement. - /// - protected Sql Sql() => Context.SqlContext.Sql(); - - /// - /// Creates a new Sql statement with arguments. - /// - protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); - - /// - /// Executes the migration. - /// - protected abstract void Migrate(); - - /// - /// Runs the migration. - /// - public void Run() - { - Migrate(); - - // ensure there is no building expression - // ie we did not forget to .Do() an expression - if (Context.BuildingExpression) - { - throw new IncompleteMigrationExpressionException("The migration has run, but leaves an expression that has not run."); - } - } - - // ensures we are not already building, + // ensure there is no building expression // ie we did not forget to .Do() an expression - private protected T BeginBuild(T builder) + if (Context.BuildingExpression) { - if (Context.BuildingExpression) - throw new IncompleteMigrationExpressionException("Cannot create a new expression: the previous expression has not run."); - Context.BuildingExpression = true; - return builder; + throw new IncompleteMigrationExpressionException( + "The migration has run, but leaves an expression that has not run."); + } + } + + /// + /// Creates a new Sql statement. + /// + protected Sql Sql() => Context.SqlContext.Sql(); + + /// + /// Creates a new Sql statement with arguments. + /// + protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); + + /// + /// Executes the migration. + /// + protected abstract void Migrate(); + + // ensures we are not already building, + // ie we did not forget to .Do() an expression + private protected T BeginBuild(T builder) + { + if (Context.BuildingExpression) + { + throw new IncompleteMigrationExpressionException( + "Cannot create a new expression: the previous expression has not run."); } - /// - /// Builds an Alter expression. - /// - public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); - - /// - /// Builds a Create expression. - /// - public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); - - /// - /// Builds a Delete expression. - /// - public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); - - /// - /// Builds an Execute expression. - /// - public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); - - /// - /// Builds an Insert expression. - /// - public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); - - /// - /// Builds a Rename expression. - /// - public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); - - /// - /// Builds an Update expression. - /// - public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); + Context.BuildingExpression = true; + return builder; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs index 22f7771685..49775bcd0a 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; @@ -13,55 +13,72 @@ namespace Umbraco.Cms.Infrastructure.Migrations public abstract partial class MigrationBase { // provides extra methods for migrations - protected void AddColumn(string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); AddColumn(table, table.Name!, columnName); } protected void AddColumnIfNotExists(IEnumerable columns, string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); if (columns.Any(x => x.TableName.InvariantEquals(table.Name) && !x.ColumnName.InvariantEquals(columnName))) + { AddColumn(table, table.Name!, columnName); + } } protected void AddColumn(string tableName, string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); AddColumn(table, tableName, columnName); } protected void AddColumnIfNotExists(IEnumerable columns, string tableName, string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); if (columns.Any(x => x.TableName.InvariantEquals(tableName) && !x.ColumnName.InvariantEquals(columnName))) + { AddColumn(table, tableName, columnName); + } + } + + protected void AddColumn(string columnName, out IEnumerable sqls) + { + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + AddColumn(table, table.Name!, columnName, out sqls); } private void AddColumn(TableDefinition table, string tableName, string columnName) { - if (ColumnExists(tableName, columnName)) return; + if (ColumnExists(tableName, columnName)) + { + return; + } - var column = table.Columns.First(x => x.Name == columnName); + ColumnDefinition? column = table.Columns.First(x => x.Name == columnName); var createSql = SqlSyntax.Format(column); Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(tableName), createSql)).Do(); } - protected void AddColumn(string columnName, out IEnumerable sqls) - { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); - AddColumn(table, table.Name!, columnName, out sqls); - } - protected void AddColumn(string tableName, string columnName, out IEnumerable sqls) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); AddColumn(table, tableName, columnName, out sqls); } + protected void AlterColumn(string tableName, string columnName) + { + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + ColumnDefinition? column = table.Columns.First(x => x.Name == columnName); + SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out IEnumerable? sqls); + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + } + private void AddColumn(TableDefinition table, string tableName, string columnName, out IEnumerable sqls) { if (ColumnExists(tableName, columnName)) @@ -70,20 +87,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations return; } - var column = table.Columns.First(x => x.Name == columnName); + ColumnDefinition? column = table.Columns.First(x => x.Name == columnName); var createSql = SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out sqls); Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(tableName), createSql)).Do(); } - protected void AlterColumn(string tableName, string columnName) - { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); - var column = table.Columns.First(x => x.Name == columnName); - SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out var sqls); - foreach (var sql in sqls) - Execute.Sql(sql).Do(); - } - protected void ReplaceColumn(string tableName, string currentName, string newName) { Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); @@ -92,26 +100,26 @@ namespace Umbraco.Cms.Infrastructure.Migrations protected bool TableExists(string tableName) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); + IEnumerable? tables = SqlSyntax.GetTablesInSchema(Context.Database); return tables.Any(x => x.InvariantEquals(tableName)); } protected bool IndexExists(string indexName) { - var indexes = SqlSyntax.GetDefinedIndexes(Context.Database); + IEnumerable>? indexes = SqlSyntax.GetDefinedIndexes(Context.Database); return indexes.Any(x => x.Item2.InvariantEquals(indexName)); } protected bool ColumnExists(string tableName, string columnName) { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); + ColumnInfo[]? columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); return columns.Any(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); } protected string? ColumnType(string tableName, string columnName) { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); - var column = columns.FirstOrDefault(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); + ColumnInfo[]? columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); + ColumnInfo? column = columns.FirstOrDefault(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); return column?.DataType; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs index e68dc7a700..40db38e053 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs @@ -1,20 +1,13 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +public class MigrationBuilder : IMigrationBuilder { - public class MigrationBuilder : IMigrationBuilder - { - private readonly IServiceProvider _container; + private readonly IServiceProvider _container; - public MigrationBuilder(IServiceProvider container) - { - _container = container; - } + public MigrationBuilder(IServiceProvider container) => _container = container; - public MigrationBase Build(Type migrationType, IMigrationContext context) - { - return (MigrationBase) _container.CreateInstance(migrationType, context); - } - } + public MigrationBase Build(Type migrationType, IMigrationContext context) => + (MigrationBase)_container.CreateInstance(migrationType, context); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs index 975df9120d..eaf2eb4f4d 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs @@ -1,55 +1,50 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Implements . +/// +internal class MigrationContext : IMigrationContext { + private readonly List _postMigrations = new(); + /// - /// Implements . + /// Initializes a new instance of the class. /// - internal class MigrationContext : IMigrationContext + public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger logger) { - private readonly List _postMigrations = new List(); - - /// - /// Initializes a new instance of the class. - /// - public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger logger) - { - Plan = plan; - Database = database ?? throw new ArgumentNullException(nameof(database)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _postMigrations.AddRange(plan.PostMigrationTypes); - } - - /// - public ILogger Logger { get; } - - public MigrationPlan Plan { get; } - - /// - public IUmbracoDatabase Database { get; } - - /// - public ISqlContext SqlContext => Database.SqlContext; - - /// - public int Index { get; set; } - - /// - public bool BuildingExpression { get; set; } - - // this is only internally exposed - public IReadOnlyList PostMigrations => _postMigrations; - - /// - public void AddPostMigration() - where TMigration : MigrationBase - { - // just adding - will be de-duplicated when executing - _postMigrations.Add(typeof(TMigration)); - } + Plan = plan; + Database = database ?? throw new ArgumentNullException(nameof(database)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _postMigrations.AddRange(plan.PostMigrationTypes); } + + // this is only internally exposed + public IReadOnlyList PostMigrations => _postMigrations; + + /// + public ILogger Logger { get; } + + public MigrationPlan Plan { get; } + + /// + public IUmbracoDatabase Database { get; } + + /// + public ISqlContext SqlContext => Database.SqlContext; + + /// + public int Index { get; set; } + + /// + public bool BuildingExpression { get; set; } + + /// + public void AddPostMigration() + where TMigration : MigrationBase => + + // just adding - will be de-duplicated when executing + _postMigrations.Add(typeof(TMigration)); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs index 4838467197..08b1b2b1ab 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs @@ -1,162 +1,189 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides a base class for migration expressions. +/// +public abstract class MigrationExpressionBase : IMigrationExpression { + private bool _executed; + private List? _expressions; + + protected MigrationExpressionBase(IMigrationContext context) => + Context = context ?? throw new ArgumentNullException(nameof(context)); + + public DatabaseType DatabaseType => Context.Database.DatabaseType; + + protected IMigrationContext Context { get; } + + protected ILogger Logger => Context.Logger; + + protected ISqlSyntaxProvider SqlSyntax => Context.Database.SqlContext.SqlSyntax; + + protected IUmbracoDatabase Database => Context.Database; + + public List Expressions => _expressions ??= new List(); + /// - /// Provides a base class for migration expressions. + /// This might be useful in the future if we add it to the interface, but for now it's used to hack the DeleteAppTables & DeleteForeignKeyExpression + /// to ensure they are not executed twice. /// - public abstract class MigrationExpressionBase : IMigrationExpression + internal string? Name { get; set; } + + public virtual void Execute() { - private bool _executed; - private List? _expressions; - - protected MigrationExpressionBase(IMigrationContext context) + if (_executed) { - Context = context ?? throw new ArgumentNullException(nameof(context)); + throw new InvalidOperationException("This expression has already been executed."); } - protected IMigrationContext Context { get; } + _executed = true; + Context.BuildingExpression = false; - protected ILogger Logger => Context.Logger; + var sql = GetSql(); - protected ISqlSyntaxProvider SqlSyntax => Context.Database.SqlContext.SqlSyntax; - - protected IUmbracoDatabase Database => Context.Database; - - public DatabaseType DatabaseType => Context.Database.DatabaseType; - - public List Expressions => _expressions ?? (_expressions = new List()); - - protected virtual string? GetSql() + if (string.IsNullOrWhiteSpace(sql)) { - return ToString(); + Logger.LogInformation("SQL [{ContextIndex}]: ", Context.Index); } - - public virtual void Execute() + else { - if (_executed) - throw new InvalidOperationException("This expression has already been executed."); - _executed = true; - Context.BuildingExpression = false; - - var sql = GetSql(); - - if (string.IsNullOrWhiteSpace(sql)) + // split multiple statements - required for SQL CE + // http://stackoverflow.com/questions/13665491/sql-ce-inconsistent-with-multiple-statements + var stmtBuilder = new StringBuilder(); + using (var reader = new StringReader(sql)) { - Logger.LogInformation("SQL [{ContextIndex}]: ", Context.Index); - } - else - { - // split multiple statements - required for SQL CE - // http://stackoverflow.com/questions/13665491/sql-ce-inconsistent-with-multiple-statements - var stmtBuilder = new StringBuilder(); - using (var reader = new StringReader(sql)) + string? line; + while ((line = reader.ReadLine()) != null) { - string? line; - while ((line = reader.ReadLine()) != null) + if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) { - if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) - ExecuteStatement(stmtBuilder); - else - stmtBuilder.Append(line); - } - - if (stmtBuilder.Length > 0) ExecuteStatement(stmtBuilder); + } + else + { + stmtBuilder.Append(line); + } + } + + if (stmtBuilder.Length > 0) + { + ExecuteStatement(stmtBuilder); } } - - Context.Index++; - - if (_expressions == null) - return; - - foreach (var expression in _expressions) - expression.Execute(); } - protected void Execute(Sql? sql) - { - if (_executed) - throw new InvalidOperationException("This expression has already been executed."); - _executed = true; - Context.BuildingExpression = false; + Context.Index++; - if (sql == null) + if (_expressions == null) + { + return; + } + + // HACK: We're handling all the constraints higher up the stack for SQLite. + if (Context.Database.DatabaseType.IsSqlite()) { - Logger.LogInformation($"SQL [{Context.Index}]: "); - } - else - { - Logger.LogInformation($"SQL [{Context.Index}]: {sql.ToText()}"); - Database.Execute(sql); + _expressions = _expressions + .Where(x => x is not CreateConstraintExpression) + .Where(x => x is not CreateForeignKeyExpression) + .ToList(); } - Context.Index++; + foreach (IMigrationExpression expression in _expressions) + { + expression.Execute(); + } + } - if (_expressions == null) - return; + protected virtual string? GetSql() => ToString(); - foreach (var expression in _expressions) - expression.Execute(); + protected void Execute(Sql? sql) + { + if (_executed) + { + throw new InvalidOperationException("This expression has already been executed."); } - private void ExecuteStatement(StringBuilder stmtBuilder) + _executed = true; + Context.BuildingExpression = false; + + if (sql == null) { - var stmt = stmtBuilder.ToString(); - Logger.LogInformation("SQL [{ContextIndex}]: {Sql}", Context.Index, stmt); - Database.Execute(stmt); - stmtBuilder.Clear(); + Logger.LogInformation($"SQL [{Context.Index}]: "); + } + else + { + Logger.LogInformation($"SQL [{Context.Index}]: {sql.ToText()}"); + Database.Execute(sql); } - protected void AppendStatementSeparator(StringBuilder stmtBuilder) + Context.Index++; + + if (_expressions == null) { - stmtBuilder.AppendLine(";"); - if (DatabaseType.IsSqlServer()) - stmtBuilder.AppendLine("GO"); + return; } - /// - /// This might be useful in the future if we add it to the interface, but for now it's used to hack the DeleteAppTables & DeleteForeignKeyExpression - /// to ensure they are not executed twice. - /// - internal string? Name { get; set; } - - protected string GetQuotedValue(object? val) + foreach (IMigrationExpression expression in _expressions) { - if (val == null) return "NULL"; + expression.Execute(); + } + } - var type = val.GetType(); + protected void AppendStatementSeparator(StringBuilder stmtBuilder) + { + stmtBuilder.AppendLine(";"); + if (DatabaseType.IsSqlServer()) + { + stmtBuilder.AppendLine("GO"); + } + } - switch (Type.GetTypeCode(type)) - { - case TypeCode.Boolean: - return ((bool)val) ? "1" : "0"; - case TypeCode.Single: - case TypeCode.Double: - case TypeCode.Decimal: - case TypeCode.SByte: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Byte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return val.ToString()!; - case TypeCode.DateTime: - return SqlSyntax.GetQuotedValue(SqlSyntax.FormatDateTime((DateTime) val)); - default: - return SqlSyntax.GetQuotedValue(val.ToString()!); - } + private void ExecuteStatement(StringBuilder stmtBuilder) + { + var stmt = stmtBuilder.ToString(); + Logger.LogInformation("SQL [{ContextIndex}]: {Sql}", Context.Index, stmt); + Database.Execute(stmt); + stmtBuilder.Clear(); + } + + protected string GetQuotedValue(object? val) + { + if (val == null) + { + return "NULL"; + } + + Type type = val.GetType(); + + switch (Type.GetTypeCode(type)) + { + case TypeCode.Boolean: + return (bool)val ? "1" : "0"; + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Byte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + return val.ToString()!; + case TypeCode.DateTime: + return SqlSyntax.GetQuotedValue(SqlSyntax.FormatDateTime((DateTime)val)); + default: + return SqlSyntax.GetQuotedValue(val.ToString()!); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs index 091eebe496..68b870bb7c 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs @@ -1,394 +1,469 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Extensions; -using Type = System.Type; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Represents a migration plan. +/// +public class MigrationPlan { + private readonly List _postMigrationTypes = new(); + private readonly Dictionary _transitions = new(StringComparer.InvariantCultureIgnoreCase); + private string? _finalState; + + private string? _prevState; /// - /// Represents a migration plan. + /// Initializes a new instance of the class. /// - public class MigrationPlan + /// The name of the plan. + public MigrationPlan(string name) { - private readonly Dictionary _transitions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly List _postMigrationTypes = new List(); - - private string? _prevState; - private string? _finalState; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the plan. - public MigrationPlan(string name) + if (name == null) { - if (name == null) + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + Name = name; + } + + /// + /// If set to true the plan executor will ignore any current state persisted and + /// run the plan from its initial state to its end state. + /// + public virtual bool IgnoreCurrentState { get; } = false; + + /// + /// Gets the transitions. + /// + public IReadOnlyDictionary Transitions => _transitions; + + public IReadOnlyList PostMigrationTypes => _postMigrationTypes; + + /// + /// Gets the name of the plan. + /// + public string Name { get; } + + /// + /// Gets the initial state. + /// + /// + /// The initial state is the state when the plan has never + /// run. By default, it is the empty string, but plans may override + /// it if they have other ways of determining where to start from. + /// + public virtual string InitialState => string.Empty; + + /// + /// Gets the final state. + /// + public string FinalState + { + get + { + // modifying the plan clears _finalState + // Validate() either sets _finalState, or throws + if (_finalState == null) { - throw new ArgumentNullException(nameof(name)); + Validate(); } - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - } - - Name = name; - } - - /// - /// If set to true the plan executor will ignore any current state persisted and - /// run the plan from its initial state to its end state. - /// - public virtual bool IgnoreCurrentState { get; } = false; - - /// - /// Gets the transitions. - /// - public IReadOnlyDictionary Transitions => _transitions; - - public IReadOnlyList PostMigrationTypes => _postMigrationTypes; - - /// - /// Gets the name of the plan. - /// - public string Name { get; } - - // adds a transition - private MigrationPlan Add(string? sourceState, string targetState, Type? migration) - { - if (sourceState == null) - throw new ArgumentNullException(nameof(sourceState), $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(MigrationPlan.From)} must not have been called."); - if (targetState == null) - throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (sourceState == targetState) - throw new ArgumentException("Source and target state cannot be identical."); - if (migration == null) - throw new ArgumentNullException(nameof(migration)); - if (!migration.Implements()) - throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); - - sourceState = sourceState.Trim(); - targetState = targetState.Trim(); - - // throw if we already have a transition for that state which is not null, - // null is used to keep track of the last step of the chain - if (_transitions.ContainsKey(sourceState) && _transitions[sourceState] != null) - throw new InvalidOperationException($"A transition from state \"{sourceState}\" has already been defined."); - - // register the transition - _transitions[sourceState] = new Transition(sourceState, targetState, migration); - - // register the target state if we don't know it already - // this is how we keep track of the final state - because - // transitions could be defined in any order, that might - // be overridden afterwards. - if (!_transitions.ContainsKey(targetState)) - _transitions.Add(targetState, null); - - _prevState = targetState; - _finalState = null; // force re-validation - - return this; - } - - /// - /// Adds a transition to a target state through an empty migration. - /// - public MigrationPlan To(string targetState) - => To(targetState); - - public MigrationPlan To(Guid targetState) - => To(targetState.ToString()); - - /// - /// Adds a transition to a target state through a migration. - /// - public MigrationPlan To(string targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); - - public MigrationPlan To(Guid targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); - - /// - /// Adds a transition to a target state through a migration. - /// - public MigrationPlan To(string targetState, Type? migration) - => Add(_prevState, targetState, migration); - - public MigrationPlan To(Guid targetState, Type migration) - => Add(_prevState, targetState.ToString(), migration); - - /// - /// Sets the starting state. - /// - public MigrationPlan From(string? sourceState) - { - _prevState = sourceState ?? throw new ArgumentNullException(nameof(sourceState)); - return this; - } - - /// - /// Adds a transition to a target state through a migration, replacing a previous migration. - /// - /// The new migration. - /// The migration to use to recover from the previous target state. - /// The previous target state, which we need to recover from through . - /// The new target state. - public MigrationPlan ToWithReplace(string recoverState, string targetState) - where TMigrationNew : MigrationBase - where TMigrationRecover : MigrationBase - { - To(targetState); - From(recoverState).To(targetState); - return this; - } - - /// - /// Adds a transition to a target state through a migration, replacing a previous migration. - /// - /// The new migration. - /// The previous target state, which we can recover from directly. - /// The new target state. - public MigrationPlan ToWithReplace(string recoverState, string targetState) - where TMigrationNew : MigrationBase - { - To(targetState); - From(recoverState).To(targetState); - return this; - } - - /// - /// Adds transitions to a target state by cloning transitions from a start state to an end state. - /// - public MigrationPlan ToWithClone(string startState, string endState, string targetState) - { - if (startState == null) - throw new ArgumentNullException(nameof(startState)); - if (string.IsNullOrWhiteSpace(startState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(startState)); - if (endState == null) - throw new ArgumentNullException(nameof(endState)); - if (string.IsNullOrWhiteSpace(endState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(endState)); - if (targetState == null) - throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (startState == endState) - throw new ArgumentException("Start and end states cannot be identical."); - - startState = startState.Trim(); - endState = endState.Trim(); - targetState = targetState.Trim(); - - var state = startState; - var visited = new HashSet(); - - while (state != endState) - { - if (state is null || visited.Contains(state)) - throw new InvalidOperationException("A loop was detected in the copied chain."); - visited.Add(state); - - if (!_transitions.TryGetValue(state, out var transition)) - throw new InvalidOperationException($"There is no transition from state \"{state}\"."); - - var newTargetState = transition?.TargetState == endState - ? targetState - : CreateRandomState(); - To(newTargetState, transition?.MigrationType); - state = transition?.TargetState; - } - - return this; - } - - /// - /// Adds a post-migration to the plan. - /// - public virtual MigrationPlan AddPostMigration() - where TMigration : MigrationBase - { - // TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead. - // The only place we use this is to clear cookies in the installer which could be done - // via notification. Then we can clean up all the code related to post migrations which is - // not insignificant. - - _postMigrationTypes.Add(typeof(TMigration)); - return this; - } - - /// - /// Creates a random, unique state. - /// - public virtual string CreateRandomState() - => Guid.NewGuid().ToString("B").ToUpper(); - - /// - /// Begins a merge. - /// - public MergeBuilder Merge() => new MergeBuilder(this); - - /// - /// Gets the initial state. - /// - /// The initial state is the state when the plan has never - /// run. By default, it is the empty string, but plans may override - /// it if they have other ways of determining where to start from. - public virtual string InitialState => string.Empty; - - /// - /// Gets the final state. - /// - public string FinalState - { - get - { - // modifying the plan clears _finalState - // Validate() either sets _finalState, or throws - if (_finalState == null) - Validate(); - - return _finalState!; - } - } - - /// - /// Validates the plan. - /// - /// The plan's final state. - public void Validate() - { - if (_finalState != null) - return; - - // quick check for dead ends - a dead end is a transition that has a target state - // that is not null and does not match any source state. such a target state has - // been registered as a source state with a null transition. so there should be only - // one. - string? finalState = null; - foreach (var kvp in _transitions.Where(x => x.Value == null)) - { - if (finalState == null) - finalState = kvp.Key; - else - throw new InvalidOperationException($"Multiple final states have been detected in the plan (\"{finalState}\", \"{kvp.Key}\")." - + " Make sure the plan contains only one final state."); - } - - // now check for loops - var verified = new List(); - foreach (var transition in _transitions.Values) - { - if (transition == null || verified.Contains(transition.SourceState)) - continue; - - var visited = new List { transition.SourceState }; - var nextTransition = _transitions[transition.TargetState]; - while (nextTransition != null && !verified.Contains(nextTransition.SourceState)) - { - if (visited.Contains(nextTransition.SourceState)) - throw new InvalidOperationException($"A loop has been detected in the plan around state \"{nextTransition.SourceState}\"." - + " Make sure the plan does not contain circular transition paths."); - visited.Add(nextTransition.SourceState); - nextTransition = _transitions[nextTransition.TargetState]; - } - verified.AddRange(visited); - } - - _finalState = finalState!; - } - - /// - /// Throws an exception when the initial state is unknown. - /// - public virtual void ThrowOnUnknownInitialState(string state) - { - throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); - } - - /// - /// Follows a path (for tests and debugging). - /// - /// Does the same thing Execute does, but does not actually execute migrations. - internal IReadOnlyList FollowPath(string? fromState = null, string? toState = null) - { - toState = toState?.NullOrWhiteSpaceAsNull(); - - Validate(); - - var origState = fromState ?? string.Empty; - var states = new List { origState }; - - if (!_transitions.TryGetValue(origState, out var transition)) - throw new InvalidOperationException($"Unknown state \"{origState}\"."); - - while (transition != null) - { - var nextState = transition.TargetState; - origState = nextState; - states.Add(origState); - - if (nextState == toState) - { - transition = null; - continue; - } - - if (!_transitions.TryGetValue(origState, out transition)) - throw new InvalidOperationException($"Unknown state \"{origState}\"."); - } - - // safety check - if (origState != (toState ?? _finalState)) - throw new InvalidOperationException($"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); - - return states; - } - - /// - /// Represents a plan transition. - /// - public class Transition - { - /// - /// Initializes a new instance of the class. - /// - public Transition(string sourceState, string targetState, Type migrationTtype) - { - SourceState = sourceState; - TargetState = targetState; - MigrationType = migrationTtype; - } - - /// - /// Gets the source state. - /// - public string SourceState { get; } - - /// - /// Gets the target state. - /// - public string TargetState { get; } - - /// - /// Gets the migration type. - /// - public Type MigrationType { get; } - - /// - public override string ToString() - { - return MigrationType == typeof(NoopMigration) - ? $"{(SourceState == string.Empty ? "" : SourceState)} --> {TargetState}" - : $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}"; - } + return _finalState!; } } + + /// + /// Adds a transition to a target state through an empty migration. + /// + public MigrationPlan To(string targetState) + => To(targetState); + + // adds a transition + private MigrationPlan Add(string? sourceState, string targetState, Type? migration) + { + if (sourceState == null) + { + throw new ArgumentNullException( + nameof(sourceState), + $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(From)} must not have been called."); + } + + if (targetState == null) + { + throw new ArgumentNullException(nameof(targetState)); + } + + if (string.IsNullOrWhiteSpace(targetState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(targetState)); + } + + if (sourceState == targetState) + { + throw new ArgumentException("Source and target state cannot be identical."); + } + + if (migration == null) + { + throw new ArgumentNullException(nameof(migration)); + } + + if (!migration.Implements()) + { + throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); + } + + sourceState = sourceState.Trim(); + targetState = targetState.Trim(); + + // throw if we already have a transition for that state which is not null, + // null is used to keep track of the last step of the chain + if (_transitions.ContainsKey(sourceState) && _transitions[sourceState] != null) + { + throw new InvalidOperationException($"A transition from state \"{sourceState}\" has already been defined."); + } + + // register the transition + _transitions[sourceState] = new Transition(sourceState, targetState, migration); + + // register the target state if we don't know it already + // this is how we keep track of the final state - because + // transitions could be defined in any order, that might + // be overridden afterwards. + if (!_transitions.ContainsKey(targetState)) + { + _transitions.Add(targetState, null); + } + + _prevState = targetState; + _finalState = null; // force re-validation + + return this; + } + + public MigrationPlan To(Guid targetState) + => To(targetState.ToString()); + + /// + /// Adds a transition to a target state through a migration. + /// + public MigrationPlan To(string targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + public MigrationPlan To(Guid targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + /// + /// Adds a transition to a target state through a migration. + /// + public MigrationPlan To(string targetState, Type? migration) + => Add(_prevState, targetState, migration); + + public MigrationPlan To(Guid targetState, Type migration) + => Add(_prevState, targetState.ToString(), migration); + + /// + /// Sets the starting state. + /// + public MigrationPlan From(string? sourceState) + { + _prevState = sourceState ?? throw new ArgumentNullException(nameof(sourceState)); + return this; + } + + /// + /// Adds a transition to a target state through a migration, replacing a previous migration. + /// + /// The new migration. + /// The migration to use to recover from the previous target state. + /// + /// The previous target state, which we need to recover from through + /// . + /// + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew : MigrationBase + where TMigrationRecover : MigrationBase + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds a transition to a target state through a migration, replacing a previous migration. + /// + /// The new migration. + /// The previous target state, which we can recover from directly. + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew : MigrationBase + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds transitions to a target state by cloning transitions from a start state to an end state. + /// + public MigrationPlan ToWithClone(string startState, string endState, string targetState) + { + if (startState == null) + { + throw new ArgumentNullException(nameof(startState)); + } + + if (string.IsNullOrWhiteSpace(startState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(startState)); + } + + if (endState == null) + { + throw new ArgumentNullException(nameof(endState)); + } + + if (string.IsNullOrWhiteSpace(endState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(endState)); + } + + if (targetState == null) + { + throw new ArgumentNullException(nameof(targetState)); + } + + if (string.IsNullOrWhiteSpace(targetState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(targetState)); + } + + if (startState == endState) + { + throw new ArgumentException("Start and end states cannot be identical."); + } + + startState = startState.Trim(); + endState = endState.Trim(); + targetState = targetState.Trim(); + + var state = startState; + var visited = new HashSet(); + + while (state != endState) + { + if (state is null || visited.Contains(state)) + { + throw new InvalidOperationException("A loop was detected in the copied chain."); + } + + visited.Add(state); + + if (!_transitions.TryGetValue(state, out Transition? transition)) + { + throw new InvalidOperationException($"There is no transition from state \"{state}\"."); + } + + var newTargetState = transition?.TargetState == endState + ? targetState + : CreateRandomState(); + To(newTargetState, transition?.MigrationType); + state = transition?.TargetState; + } + + return this; + } + + /// + /// Adds a post-migration to the plan. + /// + public virtual MigrationPlan AddPostMigration() + where TMigration : MigrationBase + { + // TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead. + // The only place we use this is to clear cookies in the installer which could be done + // via notification. Then we can clean up all the code related to post migrations which is + // not insignificant. + _postMigrationTypes.Add(typeof(TMigration)); + return this; + } + + /// + /// Creates a random, unique state. + /// + public virtual string CreateRandomState() + => Guid.NewGuid().ToString("B").ToUpper(); + + /// + /// Begins a merge. + /// + public MergeBuilder Merge() => new(this); + + /// + /// Validates the plan. + /// + /// The plan's final state. + public void Validate() + { + if (_finalState != null) + { + return; + } + + // quick check for dead ends - a dead end is a transition that has a target state + // that is not null and does not match any source state. such a target state has + // been registered as a source state with a null transition. so there should be only + // one. + string? finalState = null; + foreach (KeyValuePair kvp in _transitions.Where(x => x.Value == null)) + { + if (finalState == null) + { + finalState = kvp.Key; + } + else + { + throw new InvalidOperationException( + $"Multiple final states have been detected in the plan (\"{finalState}\", \"{kvp.Key}\")." + + " Make sure the plan contains only one final state."); + } + } + + // now check for loops + var verified = new List(); + foreach (Transition? transition in _transitions.Values) + { + if (transition == null || verified.Contains(transition.SourceState)) + { + continue; + } + + var visited = new List { transition.SourceState }; + Transition? nextTransition = _transitions[transition.TargetState]; + while (nextTransition != null && !verified.Contains(nextTransition.SourceState)) + { + if (visited.Contains(nextTransition.SourceState)) + { + throw new InvalidOperationException( + $"A loop has been detected in the plan around state \"{nextTransition.SourceState}\"." + + " Make sure the plan does not contain circular transition paths."); + } + + visited.Add(nextTransition.SourceState); + nextTransition = _transitions[nextTransition.TargetState]; + } + + verified.AddRange(visited); + } + + _finalState = finalState!; + } + + /// + /// Throws an exception when the initial state is unknown. + /// + public virtual void ThrowOnUnknownInitialState(string state) => + throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); + + /// + /// Follows a path (for tests and debugging). + /// + /// Does the same thing Execute does, but does not actually execute migrations. + internal IReadOnlyList FollowPath(string? fromState = null, string? toState = null) + { + toState = toState?.NullOrWhiteSpaceAsNull(); + + Validate(); + + var origState = fromState ?? string.Empty; + var states = new List { origState }; + + if (!_transitions.TryGetValue(origState, out Transition? transition)) + { + throw new InvalidOperationException($"Unknown state \"{origState}\"."); + } + + while (transition != null) + { + var nextState = transition.TargetState; + origState = nextState; + states.Add(origState); + + if (nextState == toState) + { + transition = null; + continue; + } + + if (!_transitions.TryGetValue(origState, out transition)) + { + throw new InvalidOperationException($"Unknown state \"{origState}\"."); + } + } + + // safety check + if (origState != (toState ?? _finalState)) + { + throw new InvalidOperationException( + $"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); + } + + return states; + } + + /// + /// Represents a plan transition. + /// + public class Transition + { + /// + /// Initializes a new instance of the class. + /// + public Transition(string sourceState, string targetState, Type migrationTtype) + { + SourceState = sourceState; + TargetState = targetState; + MigrationType = migrationTtype; + } + + /// + /// Gets the source state. + /// + public string SourceState { get; } + + /// + /// Gets the target state. + /// + public string TargetState { get; } + + /// + /// Gets the migration type. + /// + public Type MigrationType { get; } + + /// + public override string ToString() => + MigrationType == typeof(NoopMigration) + ? $"{(SourceState == string.Empty ? "" : SourceState)} --> {TargetState}" + : $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}"; + } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index a89f89c7bc..caf498132e 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -1,118 +1,118 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; -using Umbraco.Extensions; -using Type = System.Type; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +public class MigrationPlanExecutor : IMigrationPlanExecutor { - public class MigrationPlanExecutor : IMigrationPlanExecutor - { - private readonly ICoreScopeProvider _scopeProvider; - private readonly IScopeAccessor _scopeAccessor; - private readonly ILoggerFactory _loggerFactory; - private readonly IMigrationBuilder _migrationBuilder; - private readonly ILogger _logger; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMigrationBuilder _migrationBuilder; + private readonly IScopeAccessor _scopeAccessor; + private readonly ICoreScopeProvider _scopeProvider; - public MigrationPlanExecutor( - ICoreScopeProvider scopeProvider, - IScopeAccessor scopeAccessor, - ILoggerFactory loggerFactory, - IMigrationBuilder migrationBuilder) + public MigrationPlanExecutor( + ICoreScopeProvider scopeProvider, + IScopeAccessor scopeAccessor, + ILoggerFactory loggerFactory, + IMigrationBuilder migrationBuilder) + { + _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; + _loggerFactory = loggerFactory; + _migrationBuilder = migrationBuilder; + _logger = _loggerFactory.CreateLogger(); + } + + /// + /// Executes the plan. + /// + /// A scope. + /// The state to start execution at. + /// A migration builder. + /// A logger. + /// + /// The final state. + /// The plan executes within the scope, which must then be completed. + public string Execute(MigrationPlan plan, string fromState) + { + plan.Validate(); + + _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); + + fromState ??= string.Empty; + var nextState = fromState; + + _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState); + + if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition)) { - _scopeProvider = scopeProvider; - _scopeAccessor = scopeAccessor; - _loggerFactory = loggerFactory; - _migrationBuilder = migrationBuilder; - _logger = _loggerFactory.CreateLogger(); + plan.ThrowOnUnknownInitialState(nextState); } - /// - /// Executes the plan. - /// - /// A scope. - /// The state to start execution at. - /// A migration builder. - /// A logger. - /// - /// The final state. - /// The plan executes within the scope, which must then be completed. - public string Execute(MigrationPlan plan, string fromState) + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - plan.Validate(); - - _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); - - fromState ??= string.Empty; - var nextState = fromState; - - _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState); - - if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition)) + // We want to suppress scope (service, etc...) notifications during a migration plan + // execution. This is because if a package that doesn't have their migration plan + // executed is listening to service notifications to perform some persistence logic, + // that packages notification handlers may explode because that package isn't fully installed yet. + using (scope.Notifications.Suppress()) { - plan.ThrowOnUnknownInitialState(nextState); - } + var context = new MigrationContext(plan, _scopeAccessor.AmbientScope?.Database, + _loggerFactory.CreateLogger()); - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) - { - // We want to suppress scope (service, etc...) notifications during a migration plan - // execution. This is because if a package that doesn't have their migration plan - // executed is listening to service notifications to perform some persistence logic, - // that packages notification handlers may explode because that package isn't fully installed yet. - using (scope.Notifications.Suppress()) + while (transition != null) { - var context = new MigrationContext(plan, _scopeAccessor.AmbientScope?.Database, _loggerFactory.CreateLogger()); + _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - while (transition != null) + MigrationBase migration = _migrationBuilder.Build(transition.MigrationType, context); + migration.Run(); + + nextState = transition.TargetState; + + _logger.LogInformation("At {OrigState}", nextState); + + // throw a raw exception here: this should never happen as the plan has + // been validated - this is just a paranoid safety test + if (!plan.Transitions.TryGetValue(nextState, out transition)) { - _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - - var migration = _migrationBuilder.Build(transition.MigrationType, context); - migration.Run(); - - nextState = transition.TargetState; - - _logger.LogInformation("At {OrigState}", nextState); - - // throw a raw exception here: this should never happen as the plan has - // been validated - this is just a paranoid safety test - if (!plan.Transitions.TryGetValue(nextState, out transition)) - { - throw new InvalidOperationException($"Unknown state \"{nextState}\"."); - } - } - - // prepare and de-duplicate post-migrations, only keeping the 1st occurence - var temp = new HashSet(); - var postMigrationTypes = context.PostMigrations - .Where(x => !temp.Contains(x)) - .Select(x => { temp.Add(x); return x; }); - - // run post-migrations - foreach (var postMigrationType in postMigrationTypes) - { - _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); - var postMigration = _migrationBuilder.Build(postMigrationType, context); - postMigration.Run(); + throw new InvalidOperationException($"Unknown state \"{nextState}\"."); } } + + // prepare and de-duplicate post-migrations, only keeping the 1st occurence + var temp = new HashSet(); + IEnumerable postMigrationTypes = context.PostMigrations + .Where(x => !temp.Contains(x)) + .Select(x => + { + temp.Add(x); + return x; + }); + + // run post-migrations + foreach (Type postMigrationType in postMigrationTypes) + { + _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); + MigrationBase postMigration = _migrationBuilder.Build(postMigrationType, context); + postMigration.Run(); + } } - - _logger.LogInformation("Done (pending scope completion)."); - - // safety check - again, this should never happen as the plan has been validated, - // and this is just a paranoid safety test - var finalState = plan.FinalState; - if (nextState != finalState) - { - throw new InvalidOperationException($"Internal error, reached state {nextState} which is not final state {finalState}"); - } - - return nextState; } + + _logger.LogInformation("Done (pending scope completion)."); + + // safety check - again, this should never happen as the plan has been validated, + // and this is just a paranoid safety test + var finalState = plan.FinalState; + if (nextState != finalState) + { + throw new InvalidOperationException( + $"Internal error, reached state {nextState} which is not final state {finalState}"); + } + + return nextState; } } diff --git a/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs b/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs index 0cc2fbad25..9ce64977c0 100644 --- a/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs @@ -1,14 +1,14 @@ -namespace Umbraco.Cms.Infrastructure.Migrations -{ - public class NoopMigration : MigrationBase - { - public NoopMigration(IMigrationContext context) : base(context) - { - } +namespace Umbraco.Cms.Infrastructure.Migrations; - protected override void Migrate() - { - // nop - } +public class NoopMigration : MigrationBase +{ + public NoopMigration(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // nop } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs index 2c2888c19d..75875b9384 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs @@ -1,13 +1,11 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +internal class DatabaseSchemaCreatedNotification : StatefulNotification { - internal class DatabaseSchemaCreatedNotification : StatefulNotification - { - public DatabaseSchemaCreatedNotification(EventMessages eventMessages) => EventMessages = eventMessages; + public DatabaseSchemaCreatedNotification(EventMessages eventMessages) => EventMessages = eventMessages; - public EventMessages EventMessages { get; } - - } + public EventMessages EventMessages { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs index 1be96c9a9a..d4dfb35df7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +internal class DatabaseSchemaCreatingNotification : CancelableNotification { - internal class DatabaseSchemaCreatingNotification : CancelableNotification + public DatabaseSchemaCreatingNotification(EventMessages messages) + : base(messages) { - public DatabaseSchemaCreatingNotification(EventMessages messages) : base(messages) - { - } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs index 50ee5c3582..22c7e0710d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs @@ -1,18 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +/// +/// Published when one or more migration plans have been successfully executed. +/// +public class MigrationPlansExecutedNotification : INotification { - /// - /// Published when one or more migration plans have been successfully executed. - /// - public class MigrationPlansExecutedNotification : INotification - { - public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) - => ExecutedPlans = executedPlans; + public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) + => ExecutedPlans = executedPlans; - public IReadOnlyList ExecutedPlans { get; } - - - } + public IReadOnlyList ExecutedPlans { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs index 2a61351d1f..c991d35f01 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs @@ -1,22 +1,21 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Web; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Clears Csrf tokens. +/// +public class ClearCsrfCookies : MigrationBase { - /// - /// Clears Csrf tokens. - /// - public class ClearCsrfCookies : MigrationBase + private readonly ICookieManager _cookieManager; + + public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager) + : base(context) => _cookieManager = cookieManager; + + protected override void Migrate() { - private readonly ICookieManager _cookieManager; - - public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager) - : base(context) => _cookieManager = cookieManager; - - protected override void Migrate() - { - _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); - _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); - } + _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); + _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs index 3531959dbb..a75b01edaf 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs @@ -1,34 +1,30 @@ -using System.IO; using Umbraco.Cms.Core.Hosting; + // using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +/// +/// Deletes the old file that saved log queries +/// +public class DeleteLogViewerQueryFile : MigrationBase { + private readonly IHostingEnvironment _hostingEnvironment; + /// - /// Deletes the old file that saved log queries + /// Initializes a new instance of the class. /// - public class DeleteLogViewerQueryFile : MigrationBase + public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) => + _hostingEnvironment = hostingEnvironment; + + /// + protected override void Migrate() { - private readonly IHostingEnvironment _hostingEnvironment; - - /// - /// Initializes a new instance of the class. - /// - public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) - : base(context) - { - _hostingEnvironment = hostingEnvironment; - } - - /// - protected override void Migrate() - { - // var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); - // - // if(File.Exists(logViewerQueryFile)) - // { - // File.Delete(logViewerQueryFile); - // } - } + // var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); + // + // if(File.Exists(logViewerQueryFile)) + // { + // File.Delete(logViewerQueryFile); + // } } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs index 94a2bc3aad..35e1fb7a30 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs @@ -1,18 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Rebuilds the published snapshot. +/// +/// +/// +/// This interface exists because the entire published snapshot lives in Umbraco.Web +/// but we may want to trigger rebuilds from Umbraco.Core. These two assemblies should +/// be refactored, really. +/// +/// +public interface IPublishedSnapshotRebuilder { /// - /// Rebuilds the published snapshot. + /// Rebuilds. /// - /// - /// This interface exists because the entire published snapshot lives in Umbraco.Web - /// but we may want to trigger rebuilds from Umbraco.Core. These two assemblies should - /// be refactored, really. - /// - public interface IPublishedSnapshotRebuilder - { - /// - /// Rebuilds. - /// - void Rebuild(); - } + void Rebuild(); } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs index b4afea633e..f70fd0ddb3 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs @@ -1,31 +1,32 @@ -using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Implements in Umbraco.Web (rebuilding). +/// +public class PublishedSnapshotRebuilder : IPublishedSnapshotRebuilder { + private readonly DistributedCache _distributedCache; + private readonly IPublishedSnapshotService _publishedSnapshotService; + /// - /// Implements in Umbraco.Web (rebuilding). + /// Initializes a new instance of the class. /// - public class PublishedSnapshotRebuilder : IPublishedSnapshotRebuilder + public PublishedSnapshotRebuilder( + IPublishedSnapshotService publishedSnapshotService, + DistributedCache distributedCache) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly DistributedCache _distributedCache; + _publishedSnapshotService = publishedSnapshotService; + _distributedCache = distributedCache; + } - /// - /// Initializes a new instance of the class. - /// - public PublishedSnapshotRebuilder(IPublishedSnapshotService publishedSnapshotService, DistributedCache distributedCache) - { - _publishedSnapshotService = publishedSnapshotService; - _distributedCache = distributedCache; - } - - /// - public void Rebuild() - { - _publishedSnapshotService.Rebuild(); - _distributedCache.RefreshAllPublishedSnapshot(); - } + /// + public void Rebuild() + { + _publishedSnapshotService.Rebuild(); + _distributedCache.RefreshAllPublishedSnapshot(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs index e2de75b7ec..f8f81acd7b 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Rebuilds the published snapshot. +/// +public class RebuildPublishedSnapshot : MigrationBase { + private readonly IPublishedSnapshotRebuilder _rebuilder; + /// - /// Rebuilds the published snapshot. + /// Initializes a new instance of the class. /// - public class RebuildPublishedSnapshot : MigrationBase - { - private readonly IPublishedSnapshotRebuilder _rebuilder; + public RebuildPublishedSnapshot(IMigrationContext context, IPublishedSnapshotRebuilder rebuilder) + : base(context) + => _rebuilder = rebuilder; - /// - /// Initializes a new instance of the class. - /// - public RebuildPublishedSnapshot(IMigrationContext context, IPublishedSnapshotRebuilder rebuilder) - : base(context) - => _rebuilder = rebuilder; - - /// - protected override void Migrate() => _rebuilder.Rebuild(); - } + /// + protected override void Migrate() => _rebuilder.Rebuild(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs index bacd875f3f..3bd00715d1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs @@ -1,22 +1,25 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; + +public class CreateKeysAndIndexes : MigrationBase { - public class CreateKeysAndIndexes : MigrationBase + public CreateKeysAndIndexes(IMigrationContext context) + : base(context) { - public CreateKeysAndIndexes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // remove those that may already have keys + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.KeyValue).Do(); + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.PropertyData).Do(); + + // re-create *all* keys and indexes + foreach (Type x in DatabaseSchemaCreator._orderedTables) { - // remove those that may already have keys - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue).Do(); - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData).Do(); - - // re-create *all* keys and indexes - foreach (var x in DatabaseSchemaCreator.OrderedTables) - Create.KeysAndIndexes(x).Do(); + Create.KeysAndIndexes(x).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs index 14e4a5236a..23f3928147 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs @@ -1,75 +1,80 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; + +public class DeleteKeysAndIndexes : MigrationBase { - public class DeleteKeysAndIndexes : MigrationBase + public DeleteKeysAndIndexes(IMigrationContext context) + : base(context) { - public DeleteKeysAndIndexes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // all v7.14 tables + var tables = new[] { - // all v7.14 tables - var tables = new[] - { - "cmsContent", - "cmsContentType", - "cmsContentType2ContentType", - "cmsContentTypeAllowedContentType", - "cmsContentVersion", - "cmsContentXml", - "cmsDataType", - "cmsDataTypePreValues", - "cmsDictionary", - "cmsDocument", - "cmsDocumentType", - "cmsLanguageText", - "cmsMacro", - "cmsMacroProperty", - "cmsMedia", - "cmsMember", - "cmsMember2MemberGroup", - "cmsMemberType", - "cmsPreviewXml", - "cmsPropertyData", - "cmsPropertyType", - "cmsPropertyTypeGroup", - "cmsTagRelationship", - "cmsTags", - "cmsTask", - "cmsTaskType", - "cmsTemplate", - "umbracoAccess", - "umbracoAccessRule", - "umbracoAudit", - "umbracoCacheInstruction", - "umbracoConsent", - "umbracoDomains", - "umbracoExternalLogin", - "umbracoLanguage", - "umbracoLock", - "umbracoLog", - "umbracoMigration", - "umbracoNode", - "umbracoRedirectUrl", - "umbracoRelation", - "umbracoRelationType", - "umbracoServer", - "umbracoUser", - "umbracoUser2NodeNotify", - "umbracoUser2UserGroup", - "umbracoUserGroup", - "umbracoUserGroup2App", - "umbracoUserGroup2NodePermission", - "umbracoUserLogin", - "umbracoUserStartNode", - }; + "cmsContent", + "cmsContentType", + "cmsContentType2ContentType", + "cmsContentTypeAllowedContentType", + "cmsContentVersion", + "cmsContentXml", + "cmsDataType", + "cmsDataTypePreValues", + "cmsDictionary", + "cmsDocument", + "cmsDocumentType", + "cmsLanguageText", + "cmsMacro", + "cmsMacroProperty", + "cmsMedia", + "cmsMember", + "cmsMember2MemberGroup", + "cmsMemberType", + "cmsPreviewXml", + "cmsPropertyData", + "cmsPropertyType", + "cmsPropertyTypeGroup", + "cmsTagRelationship", + "cmsTags", + "cmsTask", + "cmsTaskType", + "cmsTemplate", + "umbracoAccess", + "umbracoAccessRule", + "umbracoAudit", + "umbracoCacheInstruction", + "umbracoConsent", + "umbracoDomains", + "umbracoExternalLogin", + "umbracoLanguage", + "umbracoLock", + "umbracoLog", + "umbracoMigration", + "umbracoNode", + "umbracoRedirectUrl", + "umbracoRelation", + "umbracoRelationType", + "umbracoServer", + "umbracoUser", + "umbracoUser2NodeNotify", + "umbracoUser2UserGroup", + "umbracoUserGroup", + "umbracoUserGroup2App", + "umbracoUserGroup2NodePermission", + "umbracoUserLogin", + "umbracoUserStartNode", + }; - // delete *all* keys and indexes - because of FKs - // on known v7 tables only - foreach (var table in tables) - Delete.KeysAndIndexes(table, false, true).Do(); - foreach (var table in tables) - Delete.KeysAndIndexes(table, true, false).Do(); + // delete *all* keys and indexes - because of FKs + // on known v7 tables only + foreach (var table in tables) + { + Delete.KeysAndIndexes(table, false).Do(); + } + + foreach (var table in tables) + { + Delete.KeysAndIndexes(table, true, false).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 37c2ab6c0e..a7d0b708df 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -1,10 +1,10 @@ -using System; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; @@ -21,274 +21,277 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +/// +/// Represents the Umbraco CMS migration plan. +/// +/// +public class UmbracoPlan : MigrationPlan { + private const string InitPrefix = "{init-"; + private const string InitSuffix = "}"; + private readonly IUmbracoVersion _umbracoVersion; + /// - /// Represents the Umbraco CMS migration plan. + /// Initializes a new instance of the class. /// - /// - public class UmbracoPlan : MigrationPlan + /// The Umbraco version. + public UmbracoPlan(IUmbracoVersion umbracoVersion) + : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) { - private const string InitPrefix = "{init-"; - private const string InitSuffix = "}"; - private readonly IUmbracoVersion _umbracoVersion; + _umbracoVersion = umbracoVersion; + DefinePlan(); + } - /// - /// Initializes a new instance of the class. - /// - /// The Umbraco version. - public UmbracoPlan(IUmbracoVersion umbracoVersion) - : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) + /// + /// + /// The default initial state in plans is string.Empty. + /// + /// When upgrading from version 7, we want to use specific initial states + /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper + /// migrations. + /// + /// + /// This is also where we detect the current version, and reject invalid + /// upgrades (from a tool old version, or going back in time, etc). + /// + /// + public override string InitialState + { + get { - _umbracoVersion = umbracoVersion; - DefinePlan(); - } + SemVersion currentVersion = _umbracoVersion.SemanticVersion; - /// - /// - /// The default initial state in plans is string.Empty. - /// - /// When upgrading from version 7, we want to use specific initial states - /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper - /// migrations. - /// - /// - /// This is also where we detect the current version, and reject invalid - /// upgrades (from a tool old version, or going back in time, etc). - /// - /// - public override string InitialState - { - get - { - SemVersion currentVersion = _umbracoVersion.SemanticVersion; - - // only from 8.0.0 and above - var minVersion = new SemVersion(8); - if (currentVersion < minVersion) - { - throw new InvalidOperationException( - $"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." - + $" Please upgrade first to at least {minVersion}."); - } - - // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, - // and we don't want users to workaround my putting in version 7.14.0 them self. - if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) - { - return GetInitState(minVersion); - } - - // initial state is eg "{init-7.14.0}" - return GetInitState(currentVersion); - } - } - - /// - /// Gets the initial state corresponding to a version. - /// - /// The version. - /// - /// The initial state. - /// - private static string GetInitState(SemVersion version) => InitPrefix + version + InitSuffix; - - /// - /// Tries to extract a version from an initial state. - /// - /// The state. - /// The version. - /// - /// true when the state contains a version; otherwise, false.D - /// - private static bool TryGetInitStateVersion(string state, [MaybeNullWhen(false)] out string version) - { - if (state.StartsWith(InitPrefix) && state.EndsWith(InitSuffix)) - { - version = state.TrimStart(InitPrefix).TrimEnd(InitSuffix); - return true; - } - - version = null; - return false; - } - - /// - public override void ThrowOnUnknownInitialState(string state) - { - if (TryGetInitStateVersion(state, out var initVersion)) + // only from 8.0.0 and above + var minVersion = new SemVersion(8); + if (currentVersion < minVersion) { throw new InvalidOperationException( - $"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." - + $" Please verify which versions support migrating from {initVersion}."); + $"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." + + $" Please upgrade first to at least {minVersion}."); } - base.ThrowOnUnknownInitialState(state); - } + // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, + // and we don't want users to workaround my putting in version 7.14.0 them self. + if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) + { + return GetInitState(minVersion); + } - /// - /// Defines the plan. - /// - protected void DefinePlan() - { - // MODIFYING THE PLAN - // - // Please take great care when modifying the plan! - // - // * Creating a migration for version 8: - // Append the migration to the main chain, using a new guid, before the "//FINAL" comment - // - // If the new migration causes a merge conflict, because someone else also added another - // new migration, you NEED to fix the conflict by providing one default path, and paths - // out of the conflict states (see examples below). - // - // * Porting from version 7: - // Append the ported migration to the main chain, using a new guid (same as above). - // Create a new special chain from the {init-...} state to the main chain. - - - // plan starts at 7.14.0 (anything before 7.14.0 is not supported) - From(GetInitState(new SemVersion(7, 14))); - - // begin migrating from v7 - remove all keys and indexes - To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); - - To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); - To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); - To("{5CB66059-45F4-48BA-BCBD-C5035D79206B}"); - To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); - To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); - To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); - To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); - To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); - To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); - To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); - ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", - "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 - To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); - - Merge() - .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") - .With() - .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") - .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); - - To("{1350617A-4930-4D61-852F-E3AA9E692173}"); - To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); - To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); - To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); - To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); - To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); - To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); - To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); - To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); - To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); - To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); - To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); - To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); - To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); - To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); - To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); - To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); - - Merge() - .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") - .With() - .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") - .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); - - To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); - To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); - - // finish migrating from v7 - recreate all keys and indexes - To("{3F9764F5-73D0-4D45-8804-1240A66E43A2}"); - - To("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}"); - To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); - // release-8.0.0 - - // to 8.0.1 - To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); - // release-8.0.1 - - // to 8.1.0 - To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); - To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); - To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); - - // to 8.6.0 - To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); - To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); - To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); - To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); - To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); - - // to 8.7.0 - To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); - - // to 8.9.0 - To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); - - // to 8.10.0 - To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); - - // NOTE: we need to do a merge migration here because as of 'now', - // v9-beta* is already out and 8.15 isn't out yet - // so we need to ensure that migrations from 8.15 are included in the next - // v9*. - - // to 8.15.0 - To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); - To("{4695D0C9-0729-4976-985B-048D503665D8}"); - To("{5C424554-A32D-4852-8ED1-A13508187901}"); - - // to 8.17.0 - To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}"); - - // Hack to support migration from 8.18 - To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}"); - - // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. - // - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} - // - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} - // - 9.0.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} - To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); - To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); - To("{5AAE6276-80DB-4ACF-B845-199BC6C37538}"); - - // to 9.0.0 RC1 - To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); - To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}"); - To("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}"); - To("{1303BDCF-2295-4645-9526-2F32E8B35ABD}"); - To("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); - To( - "{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. - - // TO 9.0.0-rc4 - To( - "5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards - - // TO 9.1.0 - To("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}"); - - // TO 9.2.0 - To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); - To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); - - - - // TO 9.3.0 - To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); - To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); - To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); - - // TO 9.4.0 - To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); - To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); - - // TO 10.0.0 - To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); + // initial state is eg "{init-7.14.0}" + return GetInitState(currentVersion); } } + + /// + public override void ThrowOnUnknownInitialState(string state) + { + if (TryGetInitStateVersion(state, out var initVersion)) + { + throw new InvalidOperationException( + $"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." + + $" Please verify which versions support migrating from {initVersion}."); + } + + base.ThrowOnUnknownInitialState(state); + } + + /// + /// Gets the initial state corresponding to a version. + /// + /// The version. + /// + /// The initial state. + /// + private static string GetInitState(SemVersion version) => InitPrefix + version + InitSuffix; + + /// + /// Tries to extract a version from an initial state. + /// + /// The state. + /// The version. + /// + /// true when the state contains a version; otherwise, false.D + /// + private static bool TryGetInitStateVersion(string state, [MaybeNullWhen(false)] out string version) + { + if (state.StartsWith(InitPrefix) && state.EndsWith(InitSuffix)) + { + version = state.TrimStart(InitPrefix).TrimEnd(InitSuffix); + return true; + } + + version = null; + return false; + } + + /// + /// Defines the plan. + /// + protected void DefinePlan() + { + // MODIFYING THE PLAN + // + // Please take great care when modifying the plan! + // + // * Creating a migration for version 8: + // Append the migration to the main chain, using a new guid, before the "//FINAL" comment + // + // If the new migration causes a merge conflict, because someone else also added another + // new migration, you NEED to fix the conflict by providing one default path, and paths + // out of the conflict states (see examples below). + // + // * Porting from version 7: + // Append the ported migration to the main chain, using a new guid (same as above). + // Create a new special chain from the {init-...} state to the main chain. + + // plan starts at 7.14.0 (anything before 7.14.0 is not supported) + From(GetInitState(new SemVersion(7, 14))); + + // begin migrating from v7 - remove all keys and indexes + To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); + + To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); + To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); + To("{5CB66059-45F4-48BA-BCBD-C5035D79206B}"); + To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); + To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); + To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); + To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); + To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); + To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); + To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + ToWithReplace( + "{941B2ABA-2D06-4E04-81F5-74224F1DB037}", + "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 + To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); + + Merge() + .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") + .With() + .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") + .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + + To("{1350617A-4930-4D61-852F-E3AA9E692173}"); + To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); + To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); + To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); + To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); + To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); + To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); + To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); + To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); + To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); + To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); + To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); + To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); + To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); + To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); + + Merge() + .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") + .With() + .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") + .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); + + To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); + To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); + + // finish migrating from v7 - recreate all keys and indexes + To("{3F9764F5-73D0-4D45-8804-1240A66E43A2}"); + + To("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}"); + To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); + + // release-8.0.0 + + // to 8.0.1 + To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); + + // release-8.0.1 + + // to 8.1.0 + To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); + To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); + To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); + + // to 8.6.0 + To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); + To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); + To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); + To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); + To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); + + // to 8.7.0 + To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); + + // to 8.9.0 + To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); + + // to 8.10.0 + To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); + + // NOTE: we need to do a merge migration here because as of 'now', + // v9-beta* is already out and 8.15 isn't out yet + // so we need to ensure that migrations from 8.15 are included in the next + // v9*. + + // to 8.15.0 + To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); + To("{4695D0C9-0729-4976-985B-048D503665D8}"); + To("{5C424554-A32D-4852-8ED1-A13508187901}"); + + // to 8.17.0 + To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}"); + + // Hack to support migration from 8.18 + To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}"); + + // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. + // - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} + // - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} + // - 9.0.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} + To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); + To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); + To("{5AAE6276-80DB-4ACF-B845-199BC6C37538}"); + + // to 9.0.0 RC1 + To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); + To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}"); + To("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}"); + To("{1303BDCF-2295-4645-9526-2F32E8B35ABD}"); + To("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); + To( + "{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. + + // TO 9.0.0-rc4 + To( + "5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards + + // TO 9.1.0 + To("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}"); + + // TO 9.2.0 + To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); + To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); + + // TO 9.3.0 + To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); + To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); + To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); + + // TO 9.4.0 + To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); + To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); + + // TO 10.0.0 + To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); + + // TO 10.2.0 + To("{D0B3D29D-F4D5-43E3-BA67-9D49256F3266}"); + To("{79D8217B-5920-4C0E-8E9A-3CF8FA021882}"); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index 15225b868a..e67ed8da43 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -1,79 +1,94 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +/// +/// Used to run a +/// +public class Upgrader { /// - /// Used to run a + /// Initializes a new instance of the class. /// - public class Upgrader - { - /// - /// Initializes a new instance of the class. - /// - public Upgrader(MigrationPlan plan) => Plan = plan; + public Upgrader(MigrationPlan plan) => Plan = plan; - /// - /// Gets the name of the migration plan. - /// - public string Name => Plan.Name; + /// + /// Gets the name of the migration plan. + /// + public string Name => Plan.Name; - /// - /// Gets the migration plan. - /// - public MigrationPlan Plan { get; } + /// + /// Gets the migration plan. + /// + public MigrationPlan Plan { get; } - /// - /// Gets the key for the state value. - /// - public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; + /// + /// Gets the key for the state value. + /// + public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; - /// + /// /// Executes. /// /// A scope provider. /// A key-value service. - public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, ICoreScopeProvider scopeProvider, IKeyValueService keyValueService) + [Obsolete("Please use the Execute method that accepts an Umbraco.Cms.Core.Scoping.ICoreScopeProvider instead.")] + public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, IScopeProvider scopeProvider, IKeyValueService keyValueService) + => Execute(migrationPlanExecutor, (ICoreScopeProvider)scopeProvider, keyValueService); + + /// + /// Executes. + /// + /// A scope provider. + /// A key-value service. + public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, ICoreScopeProvider scopeProvider, + IKeyValueService keyValueService) + { + if (scopeProvider == null) { - if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); - if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService)); + throw new ArgumentNullException(nameof(scopeProvider)); + } - using (ICoreScope scope = scopeProvider.CreateCoreScope()) - { - // read current state - var currentState = keyValueService.GetValue(StateValueKey); - var forceState = false; + if (keyValueService == null) + { + throw new ArgumentNullException(nameof(keyValueService)); + } - if (currentState == null || Plan.IgnoreCurrentState) - { - currentState = Plan.InitialState; - forceState = true; - } + using (ICoreScope scope = scopeProvider.CreateCoreScope()) + { + // read current state + var currentState = keyValueService.GetValue(StateValueKey); + var forceState = false; - // execute plan - var state = migrationPlanExecutor.Execute(Plan, currentState); - if (string.IsNullOrWhiteSpace(state)) - { - throw new InvalidOperationException("Plan execution returned an invalid null or empty state."); - } - - // save new state - if (forceState) - { - keyValueService.SetValue(StateValueKey, state); - } - else if (currentState != state) - { - keyValueService.SetValue(StateValueKey, currentState, state); - } - - scope.Complete(); - - return new ExecutedMigrationPlan(Plan, currentState, state); + if (currentState == null || Plan.IgnoreCurrentState) + { + currentState = Plan.InitialState; + forceState = true; } + + // execute plan + var state = migrationPlanExecutor.Execute(Plan, currentState); + if (string.IsNullOrWhiteSpace(state)) + { + throw new InvalidOperationException("Plan execution returned an invalid null or empty state."); + } + + // save new state + if (forceState) + { + keyValueService.SetValue(StateValueKey, state); + } + else if (currentState != state) + { + keyValueService.SetValue(StateValueKey, currentState, state); + } + + scope.Complete(); + + return new ExecutedMigrationPlan(Plan, currentState, state); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs index 5bc58c9b25..1a3cda316d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using System.Text; using NPoco; using Umbraco.Cms.Core; @@ -28,7 +26,9 @@ public class AddMemberPropertiesAsColumns : MigrationBase AddColumnIfNotExists(columns, "lastPasswordChangeDate"); Sql newestContentVersionQuery = Database.SqlContext.Sql() - .Select($"MAX({GetQuotedSelector("cv", "id")}) as {SqlSyntax.GetQuotedColumnName("id")}", GetQuotedSelector("cv", "nodeId")) + .Select( + $"MAX({GetQuotedSelector("cv", "id")}) as {SqlSyntax.GetQuotedColumnName("id")}", + GetQuotedSelector("cv", "nodeId")) .From("cv") .GroupBy(GetQuotedSelector("cv", "nodeId")); @@ -62,15 +62,21 @@ public class AddMemberPropertiesAsColumns : MigrationBase .From("pt") .Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberLastPasswordChangeDate'"); - StringBuilder queryBuilder = new StringBuilder(); + var queryBuilder = new StringBuilder(); queryBuilder.AppendLine($"UPDATE {Constants.DatabaseSchema.Tables.Member}"); queryBuilder.AppendLine("SET"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.FailedPasswordAttempts)} = {GetQuotedSelector("umbracoPropertyData", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsApproved)} = {GetQuotedSelector("pdmp", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsLockedOut)} = {GetQuotedSelector("pdlo", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLockoutDate)} = {GetQuotedSelector("pdlout", "dateValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLoginDate)} = {GetQuotedSelector("pdlin", "dateValue")},"); - queryBuilder.Append($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastPasswordChangeDate)} = {GetQuotedSelector("pdlpc", "dateValue")}"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.FailedPasswordAttempts)} = {GetQuotedSelector("umbracoPropertyData", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsApproved)} = {GetQuotedSelector("pdmp", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsLockedOut)} = {GetQuotedSelector("pdlo", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLockoutDate)} = {GetQuotedSelector("pdlout", "dateValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLoginDate)} = {GetQuotedSelector("pdlin", "dateValue")},"); + queryBuilder.Append( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastPasswordChangeDate)} = {GetQuotedSelector("pdlpc", "dateValue")}"); Sql updateMemberColumnsQuery = Database.SqlContext.Sql(queryBuilder.ToString()) .From() @@ -87,37 +93,43 @@ public class AddMemberPropertiesAsColumns : MigrationBase .LeftJoin() .On((left, right) => left.DataTypeId == right.NodeId) .LeftJoin() - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id) + .On((left, middle, right) => + left.PropertyTypeId == middle.Id && left.VersionId == right.Id) .LeftJoin(memberApprovedQuery, "memberApprovedType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtmp") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtmp") .LeftJoin("pdmp") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdmp") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdmp") .LeftJoin(memberLockedOutQuery, "memberLockedOutType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlo") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlo") .LeftJoin("pdlo") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlo") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlo") .LeftJoin(memberLastLockoutDateQuery, "lastLockOutDateType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlout") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlout") .LeftJoin("pdlout") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlout") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlout") .LeftJoin(memberLastLoginDateQuery, "lastLoginDateType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlin") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlin") .LeftJoin("pdlin") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlin") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlin") .LeftJoin(memberLastPasswordChangeDateQuery, "lastPasswordChangeType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlpc") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlpc") .LeftJoin("pdlpc") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlpc") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlpc") .Where(x => x.NodeObjectType == Constants.ObjectTypes.Member); Database.Execute(updateMemberColumnsQuery); @@ -131,7 +143,7 @@ public class AddMemberPropertiesAsColumns : MigrationBase "umbracoMemberLockedOut", "umbracoMemberLastLockoutDate", "umbracoMemberLastLogin", - "umbracoMemberLastPasswordChangeDate" + "umbracoMemberLastPasswordChangeDate", }; Sql idQuery = Database.SqlContext.Sql().Select(x => x.Id) @@ -157,8 +169,7 @@ public class AddMemberPropertiesAsColumns : MigrationBase private object[] GetSubQueryColumns() => new object[] { - SqlSyntax.GetQuotedColumnName("contentTypeId"), - SqlSyntax.GetQuotedColumnName("dataTypeId"), + SqlSyntax.GetQuotedColumnName("contentTypeId"), SqlSyntax.GetQuotedColumnName("dataTypeId"), SqlSyntax.GetQuotedColumnName("id"), }; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs new file mode 100644 index 0000000000..6078beb815 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; + +public class AddHasAccessToAllLanguagesColumn : MigrationBase +{ + public AddHasAccessToAllLanguagesColumn(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, "hasAccessToAllLanguages") is false) + { + Create.Column("hasAccessToAllLanguages") + .OnTable(Constants.DatabaseSchema.Tables.UserGroup) + .AsBoolean() + .WithDefaultValue(true) + .Do(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs new file mode 100644 index 0000000000..b6a4814e7d --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; + +public class AddUserGroup2LanguageTable : MigrationBase +{ + public AddUserGroup2LanguageTable(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + + if (tables.InvariantContains(UserGroup2LanguageDto.TableName)) + { + return; + } + + Create.Table().Do(); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs index b53fd867b2..a216abf045 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs @@ -1,20 +1,23 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +internal class AddContentNuTable : MigrationBase { - class AddContentNuTable : MigrationBase + public AddContentNuTable(IMigrationContext context) + : base(context) { - public AddContentNuTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains("cmsContentNu")) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains("cmsContentNu")) return; - - Create.Table(true).Do(); + return; } + + Create.Table(true).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs index 28f6e8e6de..36f4dcb5e0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs @@ -1,15 +1,13 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddContentTypeIsElementColumn : MigrationBase { - public class AddContentTypeIsElementColumn : MigrationBase + public AddContentTypeIsElementColumn(IMigrationContext context) + : base(context) { - public AddContentTypeIsElementColumn(IMigrationContext context) : base(context) - { } - - protected override void Migrate() - { - AddColumn("isElement"); - } } + + protected override void Migrate() => AddColumn("isElement"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs index f8332fb0e2..96937d3991 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs @@ -1,43 +1,44 @@ -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddLockObjects : MigrationBase { - public class AddLockObjects : MigrationBase + public AddLockObjects(IMigrationContext context) + : base(context) { - public AddLockObjects(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // some may already exist, just ensure everything we need is here - EnsureLockObject(Cms.Core.Constants.Locks.Servers, "Servers"); - EnsureLockObject(Cms.Core.Constants.Locks.ContentTypes, "ContentTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.ContentTree, "ContentTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MediaTree, "MediaTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MemberTree, "MemberTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MediaTypes, "MediaTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.MemberTypes, "MemberTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.Domains, "Domains"); - } - - private void EnsureLockObject(int id, string name) - { - EnsureLockObject(Database, id, name); - } - - internal static void EnsureLockObject(IUmbracoDatabase db, int id, string name) - { - // not if it already exists - var exists = db.Exists(id); - if (exists) return; - - // be safe: delete old umbracoNode lock objects if any - db.Execute($"DELETE FROM umbracoNode WHERE id={id};"); - - // then create umbracoLock object - db.Execute($"INSERT umbracoLock (id, name, value) VALUES ({id}, '{name}', 1);"); - } } + + internal static void EnsureLockObject(IUmbracoDatabase db, int id, string name) + { + // not if it already exists + var exists = db.Exists(id); + if (exists) + { + return; + } + + // be safe: delete old umbracoNode lock objects if any + db.Execute($"DELETE FROM umbracoNode WHERE id={id};"); + + // then create umbracoLock object + db.Execute($"INSERT umbracoLock (id, name, value) VALUES ({id}, '{name}', 1);"); + } + + protected override void Migrate() + { + // some may already exist, just ensure everything we need is here + EnsureLockObject(Constants.Locks.Servers, "Servers"); + EnsureLockObject(Constants.Locks.ContentTypes, "ContentTypes"); + EnsureLockObject(Constants.Locks.ContentTree, "ContentTree"); + EnsureLockObject(Constants.Locks.MediaTree, "MediaTree"); + EnsureLockObject(Constants.Locks.MemberTree, "MemberTree"); + EnsureLockObject(Constants.Locks.MediaTypes, "MediaTypes"); + EnsureLockObject(Constants.Locks.MemberTypes, "MemberTypes"); + EnsureLockObject(Constants.Locks.Domains, "Domains"); + } + + private void EnsureLockObject(int id, string name) => EnsureLockObject(Database, id, name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs index 4ef9d4ff14..8546566999 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs @@ -1,20 +1,19 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddLogTableColumns : MigrationBase { - public class AddLogTableColumns : MigrationBase + public AddLogTableColumns(IMigrationContext context) + : base(context) { - public AddLogTableColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "entityType"); - AddColumnIfNotExists(columns, "parameters"); - } + AddColumnIfNotExists(columns, "entityType"); + AddColumnIfNotExists(columns, "parameters"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs index fc708b1f4b..e147d185fe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs @@ -1,19 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class AddPackagesSectionAccess : MigrationBase - { - public AddPackagesSectionAccess(IMigrationContext context) - : base(context) - { } +using Umbraco.Cms.Core; - protected override void Migrate() - { - // Any user group which had access to the Developer section should have access to Packages - Database.Execute($@" - insert into {Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App} - select userGroupId, '{Cms.Core.Constants.Applications.Packages}' - from {Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App} - where app='developer'"); - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddPackagesSectionAccess : MigrationBase +{ + public AddPackagesSectionAccess(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() => + + // Any user group which had access to the Developer section should have access to Packages + Database.Execute($@" + insert into {Constants.DatabaseSchema.Tables.UserGroup2App} + select userGroupId, '{Constants.Applications.Packages}' + from {Constants.DatabaseSchema.Tables.UserGroup2App} + where app='developer'"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs index bfbe0434bf..f1369db5c3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs @@ -1,131 +1,158 @@ -using System; using System.Globalization; -using System.Linq; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddTypedLabels : MigrationBase { - public class AddTypedLabels : MigrationBase + public AddTypedLabels(IMigrationContext context) + : base(context) { - public AddTypedLabels(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // insert other label datatypes - - void InsertNodeDto(int id, int sortOrder, string uniqueId, string text) - { - var nodeDto = new NodeDto - { - NodeId = id, - Trashed = false, - ParentId = -1, - UserId = -1, - Level = 1, - Path = "-1,-" + id, - SortOrder = sortOrder, - UniqueId = new Guid(uniqueId), - Text = text, - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - CreateDate = DateTime.Now - }; - - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); - } - - if (SqlSyntax.SupportsIdentityInsert()) - Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} ON ")); - - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", "Label (datetime)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); - - if (SqlSyntax.SupportsIdentityInsert()) - Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} OFF ")); - - void InsertDataTypeDto(int id, string dbType, string? configuration = null) - { - var dataTypeDto = new DataTypeDto - { - NodeId = id, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Label, - DbType = dbType - }; - - if (configuration != null) - dataTypeDto.Configuration = configuration; - - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelInt, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelBigint, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDateTime, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDecimal, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelTime, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); - - // flip known property types - - var labelPropertyTypes = Database.Fetch(Sql() - .Select(x => x.Id, x => x.Alias) - .From() - .Where(x => x.DataTypeId == Cms.Core.Constants.DataTypes.LabelString)); - - // member properties are no longer used in v10, so just added strings here instead of constants - // these migrations should be removed anyways for v11 - var intPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Width, Cms.Core.Constants.Conventions.Media.Height, "umbracoMemberFailedPasswordAttempts" }; - var bigintPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Bytes }; - var dtPropertyAliases = new[] { "umbracoMemberLastLockoutDate", "umbracoMemberLastLogin", "umbracoMemberLastPasswordChangeDate" }; - - var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelBigint)).WhereIn(x => x.Id, bigintPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelDateTime)).WhereIn(x => x.Id, dtPropertyTypes)); - - // update values for known property types - // depending on the size of the site, that *may* take time - // but we want to parse in C# not in the database - var values = Database.Fetch(Sql() - .Select(x => x.Id, x => x.VarcharValue) - .From() - .WhereIn(x => x.PropertyTypeId, intPropertyTypes)); - foreach (var value in values) - Database.Execute(Sql() - .Update(u => u - .Set(x => x.IntegerValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (int?)null : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) - .Set(x => x.TextValue, null) - .Set(x => x.VarcharValue, null)) - .Where(x => x.Id == value.Id)); - - values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue).From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); - foreach (var value in values) - Database.Execute(Sql() - .Update(u => u - .Set(x => x.DateValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (DateTime?)null : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) - .Set(x => x.TextValue, null) - .Set(x => x.VarcharValue, null)) - .Where(x => x.Id == value.Id)); - - // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it - } - - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class PropertyDataValue - { - public int Id { get; set; } - public string? VarcharValue { get;set; } - } - // ReSharper restore UnusedAutoPropertyAccessor.Local } + + protected override void Migrate() + { + // insert other label datatypes + void InsertNodeDto(int id, int sortOrder, string uniqueId, string text) + { + var nodeDto = new NodeDto + { + NodeId = id, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-" + id, + SortOrder = sortOrder, + UniqueId = new Guid(uniqueId), + Text = text, + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }; + + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + } + + if (SqlSyntax.SupportsIdentityInsert()) + { + Database.Execute(new Sql( + $"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} ON ")); + } + + InsertNodeDto(Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); + InsertNodeDto(Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); + InsertNodeDto(Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", + "Label (datetime)"); + InsertNodeDto(Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); + InsertNodeDto(Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); + + if (SqlSyntax.SupportsIdentityInsert()) + { + Database.Execute(new Sql( + $"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} OFF ")); + } + + void InsertDataTypeDto(int id, string dbType, string? configuration = null) + { + var dataTypeDto = new DataTypeDto + { + NodeId = id, + EditorAlias = Constants.PropertyEditors.Aliases.Label, + DbType = dbType, + }; + + if (configuration != null) + { + dataTypeDto.Configuration = configuration; + } + + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + + InsertDataTypeDto(Constants.DataTypes.LabelInt, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBigint, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDateTime, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDecimal, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelTime, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); + + // flip known property types + List? labelPropertyTypes = Database.Fetch(Sql() + .Select(x => x.Id, x => x.Alias) + .From() + .Where(x => x.DataTypeId == Constants.DataTypes.LabelString)); + + var intPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Width, Cms.Core.Constants.Conventions.Media.Height, "umbracoMemberFailedPasswordAttempts" }; + var bigintPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Bytes }; + var dtPropertyAliases = new[] { "umbracoMemberLastLockoutDate", "umbracoMemberLastLogin", "umbracoMemberLastPasswordChangeDate" }; + + var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id) + .ToArray(); + var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)) + .Select(pt => pt.Id).ToArray(); + var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id) + .ToArray(); + + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)) + .WhereIn(x => x.Id, intPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)) + .WhereIn(x => x.Id, intPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelBigint)) + .WhereIn(x => x.Id, bigintPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelDateTime)) + .WhereIn(x => x.Id, dtPropertyTypes)); + + // update values for known property types + // depending on the size of the site, that *may* take time + // but we want to parse in C# not in the database + List? values = Database.Fetch(Sql() + .Select(x => x.Id, x => x.VarcharValue) + .From() + .WhereIn(x => x.PropertyTypeId, intPropertyTypes)); + foreach (PropertyDataValue? value in values) + { + Database.Execute(Sql() + .Update(u => u + .Set( + x => x.IntegerValue, + string.IsNullOrWhiteSpace(value.VarcharValue) + ? null + : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) + .Where(x => x.Id == value.Id)); + } + + values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue) + .From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); + foreach (PropertyDataValue? value in values) + { + Database.Execute(Sql() + .Update(u => u + .Set(x => x.DateValue, + string.IsNullOrWhiteSpace(value.VarcharValue) + ? null + : DateTime.Parse(value.VarcharValue, + CultureInfo.InvariantCulture, + DateTimeStyles.None)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) + .Where(x => x.Id == value.Id)); + } + + // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it + } + + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class PropertyDataValue + { + public int Id { get; set; } + + public string? VarcharValue { get; set; } + } + + // ReSharper restore UnusedAutoPropertyAccessor.Local } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs index 465b17d7fc..118c7f8bb2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs @@ -1,45 +1,53 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class AddVariationTables1A : MigrationBase - { - public AddVariationTables1A(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; +public class AddVariationTables1A : MigrationBase +{ + public AddVariationTables1A(IMigrationContext context) + : base(context) + { + } + + // note - original AddVariationTables1 just did + // Create.Table().Do(); + // + // this is taking care of ppl left in this state + protected override void Migrate() + { // note - original AddVariationTables1 just did // Create.Table().Do(); // - // this is taking care of ppl left in this state + // it's been deprecated, not part of the main upgrade path, + // but we need to take care of ppl caught into the state - protected override void Migrate() + // was not used + Delete.Column("available").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + // was not used + Delete.Column("availableDate").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + // special trick to add the column without constraints and return the sql to add them later + AddColumn("date", out IEnumerable sqls); + + // now we need to update the new column with some values because this column doesn't allow NULL values + Update.Table(ContentVersionCultureVariationDto.TableName).Set(new { date = DateTime.Now }).AllRows().Do(); + + // now apply constraints (NOT NULL) to new table + foreach (var sql in sqls) { - // note - original AddVariationTables1 just did - // Create.Table().Do(); - // - // it's been deprecated, not part of the main upgrade path, - // but we need to take care of ppl caught into the state - - // was not used - Delete.Column("available").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - // was not used - Delete.Column("availableDate").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - //special trick to add the column without constraints and return the sql to add them later - AddColumn("date", out var sqls); - //now we need to update the new column with some values because this column doesn't allow NULL values - Update.Table(ContentVersionCultureVariationDto.TableName).Set(new {date = DateTime.Now}).AllRows().Do(); - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - // name, languageId are now non-nullable - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "name"); - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "languageId"); - - Create.Table().Do(); + Execute.Sql(sql).Do(); } + + // name, languageId are now non-nullable + AlterColumn( + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, + "name"); + AlterColumn( + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, + "languageId"); + + Create.Table().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs index 263dffd2b9..76d20b4667 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs @@ -1,17 +1,17 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddVariationTables2 : MigrationBase { - public class AddVariationTables2 : MigrationBase + public AddVariationTables2(IMigrationContext context) + : base(context) { - public AddVariationTables2(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Create.Table(true).Do(); - Create.Table(true).Do(); - } + protected override void Migrate() + { + Create.Table(true).Do(); + Create.Table(true).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 6171c3df13..edfeb204f8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -1,66 +1,62 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class ContentVariationMigration : MigrationBase { - public class ContentVariationMigration : MigrationBase + public ContentVariationMigration(IMigrationContext context) + : base(context) { - public ContentVariationMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + static byte GetNewValue(byte oldValue) { - byte GetNewValue(byte oldValue) + switch (oldValue) { - switch (oldValue) - { - case 0: // Unknown - case 1: // InvariantNeutral - return 0; // Unknown - case 2: // CultureNeutral - case 3: // CultureNeutral | InvariantNeutral - return 1; // Culture - case 4: // InvariantSegment - case 5: // InvariantSegment | InvariantNeutral - return 2; // Segment - case 6: // InvariantSegment | CultureNeutral - case 7: // InvariantSegment | CultureNeutral | InvariantNeutral - case 8: // CultureSegment - case 9: // CultureSegment | InvariantNeutral - case 10: // CultureSegment | CultureNeutral - case 11: // CultureSegment | CultureNeutral | InvariantNeutral - case 12: // etc - case 13: - case 14: - case 15: - return 3; // Culture | Segment - default: - throw new NotSupportedException($"Invalid value {oldValue}."); - } - } - - var propertyTypes = Database.Fetch(Sql().Select().From()); - foreach (var dto in propertyTypes) - { - dto.Variations = GetNewValue(dto.Variations); - Database.Update(dto); - } - - var contentTypes = Database.Fetch(Sql().Select().From()); - foreach (var dto in contentTypes) - { - dto.Variations = GetNewValue(dto.Variations); - Database.Update(dto); + case 0: // Unknown + case 1: // InvariantNeutral + return 0; // Unknown + case 2: // CultureNeutral + case 3: // CultureNeutral | InvariantNeutral + return 1; // Culture + case 4: // InvariantSegment + case 5: // InvariantSegment | InvariantNeutral + return 2; // Segment + case 6: // InvariantSegment | CultureNeutral + case 7: // InvariantSegment | CultureNeutral | InvariantNeutral + case 8: // CultureSegment + case 9: // CultureSegment | InvariantNeutral + case 10: // CultureSegment | CultureNeutral + case 11: // CultureSegment | CultureNeutral | InvariantNeutral + case 12: // etc + case 13: + case 14: + case 15: + return 3; // Culture | Segment + default: + throw new NotSupportedException($"Invalid value {oldValue}."); } } - // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration - - - - + List? propertyTypes = + Database.Fetch(Sql().Select().From()); + foreach (PropertyTypeDto80? dto in propertyTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } + List? contentTypes = + Database.Fetch(Sql().Select().From()); + foreach (ContentTypeDto80? dto in contentTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } } + + // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index a6ff99f2c7..50ac54436a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -1,146 +1,162 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class ConvertRelatedLinksToMultiUrlPicker : MigrationBase { - public class ConvertRelatedLinksToMultiUrlPicker : MigrationBase + public ConvertRelatedLinksToMultiUrlPicker(IMigrationContext context) + : base(context) { - public ConvertRelatedLinksToMultiUrlPicker(IMigrationContext context) : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + Sql sqlDataTypes = Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks + || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); + + List? dataTypes = Database.Fetch(sqlDataTypes); + var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); + + if (dataTypeIds.Count == 0) { - var sqlDataTypes = Sql() - .Select() - .From() - .Where(x => x.EditorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); + return; + } - var dataTypes = Database.Fetch(sqlDataTypes); - var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); + foreach (DataTypeDto? dataType in dataTypes) + { + dataType.EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker; + Database.Update(dataType); + } - if (dataTypeIds.Count == 0) return; + Sql sqlPropertyTpes = Sql() + .Select() + .From() + .Where(x => dataTypeIds.Contains(x.DataTypeId)); - foreach (var dataType in dataTypes) + var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); + + if (propertyTypeIds.Count == 0) + { + return; + } + + Sql sqlPropertyData = Sql() + .Select() + .From() + .Where(x => propertyTypeIds.Contains(x.PropertyTypeId)); + + List? properties = Database.Fetch(sqlPropertyData); + + // Create a Multi URL Picker datatype for the converted RelatedLinks data + foreach (PropertyDataDto? property in properties) + { + var value = property.Value?.ToString(); + if (string.IsNullOrWhiteSpace(value)) { - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MultiUrlPicker; - Database.Update(dataType); + continue; } - var sqlPropertyTpes = Sql() - .Select() - .From() - .Where(x => dataTypeIds.Contains(x.DataTypeId)); - - var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); - - if (propertyTypeIds.Count == 0) return; - - var sqlPropertyData = Sql() - .Select() - .From() - .Where(x => propertyTypeIds.Contains(x.PropertyTypeId)); - - var properties = Database.Fetch(sqlPropertyData); - - // Create a Multi URL Picker datatype for the converted RelatedLinks data - - foreach (var property in properties) + List? relatedLinks = JsonConvert.DeserializeObject>(value); + var links = new List(); + if (relatedLinks is null) { - var value = property.Value?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - continue; + return; + } - var relatedLinks = JsonConvert.DeserializeObject>(value); - var links = new List(); - if (relatedLinks is null) + foreach (RelatedLink relatedLink in relatedLinks) + { + GuidUdi? udi = null; + if (relatedLink.IsInternal) { - return; - } - - foreach (var relatedLink in relatedLinks) - { - GuidUdi? udi = null; - if (relatedLink.IsInternal) + var linkIsUdi = UdiParser.TryParse(relatedLink.Link, out udi); + if (linkIsUdi == false) { - var linkIsUdi = UdiParser.TryParse(relatedLink.Link, out udi); - if (linkIsUdi == false) + // oh no.. probably an integer, yikes! + if (int.TryParse(relatedLink.Link, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var intId)) { - // oh no.. probably an integer, yikes! - if (int.TryParse(relatedLink.Link, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) - { - var sqlNodeData = Sql() - .Select() - .From() - .Where(x => x.NodeId == intId); + Sql sqlNodeData = Sql() + .Select() + .From() + .Where(x => x.NodeId == intId); - var node = Database.Fetch(sqlNodeData).FirstOrDefault(); - if (node != null) - // Note: RelatedLinks did not allow for picking media items, - // so if there's a value this will be a content item - hence - // the hardcoded "document" here - udi = new GuidUdi("document", node.UniqueId); + NodeDto? node = Database.Fetch(sqlNodeData).FirstOrDefault(); + if (node != null) + + // Note: RelatedLinks did not allow for picking media items, + // so if there's a value this will be a content item - hence + // the hardcoded "document" here + { + udi = new GuidUdi("document", node.UniqueId); } } } - - var link = new LinkDto - { - Name = relatedLink.Caption, - Target = relatedLink.NewWindow ? "_blank" : null, - Udi = udi, - // Should only have a URL if it's an external link otherwise it wil be a UDI - Url = relatedLink.IsInternal == false ? relatedLink.Link : null - }; - - links.Add(link); } - var json = JsonConvert.SerializeObject(links); + var link = new LinkDto + { + Name = relatedLink.Caption, + Target = relatedLink.NewWindow ? "_blank" : null, + Udi = udi, - // Update existing data - property.TextValue = json; - Database.Update(property); + // Should only have a URL if it's an external link otherwise it wil be a UDI + Url = relatedLink.IsInternal == false ? relatedLink.Link : null, + }; + + links.Add(link); } + var json = JsonConvert.SerializeObject(links); + // Update existing data + property.TextValue = json; + Database.Update(property); } } - - internal class RelatedLink - { - public int? Id { get; internal set; } - internal bool IsDeleted { get; set; } - [JsonProperty("caption")] - public string? Caption { get; set; } - [JsonProperty("link")] - public string? Link { get; set; } - [JsonProperty("newWindow")] - public bool NewWindow { get; set; } - [JsonProperty("isInternal")] - public bool IsInternal { get; set; } - } - - [DataContract] - internal class LinkDto - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "target")] - public string? Target { get; set; } - - [DataMember(Name = "udi")] - public GuidUdi? Udi { get; set; } - - [DataMember(Name = "url")] - public string? Url { get; set; } - } +} + +internal class RelatedLink +{ + public int? Id { get; internal set; } + + [JsonProperty("caption")] + public string? Caption { get; set; } + + internal bool IsDeleted { get; set; } + + [JsonProperty("link")] + public string? Link { get; set; } + + [JsonProperty("newWindow")] + public bool NewWindow { get; set; } + + [JsonProperty("isInternal")] + public bool IsInternal { get; set; } +} + +[DataContract] +internal class LinkDto +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "target")] + public string? Target { get; set; } + + [DataMember(Name = "udi")] + public GuidUdi? Udi { get; set; } + + [DataMember(Name = "url")] + public string? Url { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index c254ecc8df..d97b7ebcb5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -1,138 +1,144 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DataTypeMigration : MigrationBase { - - public class DataTypeMigration : MigrationBase + private static readonly ISet _legacyAliases = new HashSet { - private readonly PreValueMigratorCollection _preValueMigrators; - private readonly PropertyEditorCollection _propertyEditors; - private readonly ILogger _logger; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + Constants.PropertyEditors.Legacy.Aliases.Date, + Constants.PropertyEditors.Legacy.Aliases.Textbox, + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + }; - private static readonly ISet LegacyAliases = new HashSet() + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly ILogger _logger; + private readonly PreValueMigratorCollection _preValueMigrators; + private readonly PropertyEditorCollection _propertyEditors; + + public DataTypeMigration( + IMigrationContext context, + PreValueMigratorCollection preValueMigrators, + PropertyEditorCollection propertyEditors, + ILogger logger, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : base(context) + { + _preValueMigrators = preValueMigrators; + _propertyEditors = propertyEditors; + _logger = logger; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + } + + protected override void Migrate() + { + // drop and create columns + Delete.Column("pk").FromTable("cmsDataType").Do(); + + // rename the table + Rename.Table("cmsDataType").To(Constants.DatabaseSchema.Tables.DataType).Do(); + + // create column + AddColumn(Constants.DatabaseSchema.Tables.DataType, "config"); + Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty))).Do(); + + // renames + Execute.Sql(Sql() + .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) + .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias")).Do(); + + // from preValues to configuration... + Sql sql = Sql() + .Select() + .AndSelect(x => x.Id, x => x.Alias, x => x.SortOrder, x => x.Value) + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .OrderBy(x => x.NodeId) + .AndBy(x => x.SortOrder); + + IEnumerable> dtos = Database.Fetch(sql).GroupBy(x => x.NodeId); + + foreach (IGrouping group in dtos) { - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Date, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Textbox, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, - }; - - public DataTypeMigration(IMigrationContext context, - PreValueMigratorCollection preValueMigrators, - PropertyEditorCollection propertyEditors, - ILogger logger, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : base(context) - { - _preValueMigrators = preValueMigrators; - _propertyEditors = propertyEditors; - _logger = logger; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - } - - protected override void Migrate() - { - // drop and create columns - Delete.Column("pk").FromTable("cmsDataType").Do(); - - // rename the table - Rename.Table("cmsDataType").To(Cms.Core.Constants.DatabaseSchema.Tables.DataType).Do(); - - // create column - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "config"); - Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty))).Do(); - - // renames - Execute.Sql(Sql() - .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) - .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias")).Do(); - - // from preValues to configuration... - var sql = Sql() + DataTypeDto? dataType = Database.Fetch(Sql() .Select() - .AndSelect(x => x.Id, x => x.Alias, x => x.SortOrder, x => x.Value) .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .OrderBy(x => x.NodeId) - .AndBy(x => x.SortOrder); + .Where(x => x.NodeId == group.Key)).First(); - var dtos = Database.Fetch(sql).GroupBy(x => x.NodeId); - - foreach (var group in dtos) + // check for duplicate aliases + var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (aliases.Distinct().Count() != aliases.Length) { - var dataType = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.NodeId == group.Key)).First(); - - // check for duplicate aliases - var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); - if (aliases.Distinct().Count() != aliases.Length) - throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); - - // handle null/empty aliases - int index = 0; - var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); - - // migrate the preValues to configuration - var migrator = _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); - var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); - var json = _configurationEditorJsonSerializer.Serialize(config); - - // validate - and kill the migration if it fails - var newAlias = migrator.GetNewAlias(dataType.EditorAlias); - if (newAlias == null) - { - if (!LegacyAliases.Contains(dataType.EditorAlias)) - { - _logger.LogWarning( - "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, dataType.EditorAlias); - } - } - else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor)) - { - if (!LegacyAliases.Contains(newAlias)) - { - _logger.LogWarning("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" - + " because no property editor with that alias was found." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); - } - } - else - { - var configEditor = propertyEditor.GetConfigurationEditor(); - try - { - var _ = configEditor.FromDatabase(json, _configurationEditorJsonSerializer); - } - catch (Exception e) - { - _logger.LogWarning(e, "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); - } - } - - // update - dataType.Configuration = _configurationEditorJsonSerializer.Serialize(config); - Database.Update(dataType); + throw new InvalidOperationException( + $"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); } + + // handle null/empty aliases + var index = 0; + var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); + + // migrate the preValues to configuration + IPreValueMigrator migrator = + _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); + var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); + var json = _configurationEditorJsonSerializer.Serialize(config); + + // validate - and kill the migration if it fails + var newAlias = migrator.GetNewAlias(dataType.EditorAlias); + if (newAlias == null) + { + if (!_legacyAliases.Contains(dataType.EditorAlias)) + { + _logger.LogWarning( + "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, dataType.EditorAlias); + } + } + else if (!_propertyEditors.TryGet(newAlias, out IDataEditor? propertyEditor)) + { + if (!_legacyAliases.Contains(newAlias)) + { + _logger.LogWarning( + "Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + + " because no property editor with that alias was found." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } + } + else + { + IConfigurationEditor configEditor = propertyEditor.GetConfigurationEditor(); + try + { + var _ = configEditor.FromDatabase(json, _configurationEditorJsonSerializer); + } + catch (Exception e) + { + _logger.LogWarning( + e, + "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } + } + + // update + dataType.Configuration = _configurationEditorJsonSerializer.Serialize(config); + Database.Update(dataType); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs index 7e1711604a..13c9645ffe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs @@ -1,20 +1,23 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ContentPickerPreValueMigrator : DefaultPreValueMigrator { - class ContentPickerPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; + + public override string? GetNewAlias(string editorAlias) + => null; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; - - public override string? GetNewAlias(string editorAlias) - => null; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "showOpenButton" || + preValue.Alias == "ignoreUserStartNodes") { - if (preValue.Alias == "showOpenButton" || - preValue.Alias == "ignoreUserStartNodes") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); + return preValue.Value == "1"; } + + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs index 0383e7029e..eb7744cc18 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs @@ -1,21 +1,22 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DecimalPreValueMigrator : DefaultPreValueMigrator { - class DecimalPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Decimal"; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.Decimal"; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "min" || + preValue.Alias == "step" || + preValue.Alias == "max") { - if (preValue.Alias == "min" || - preValue.Alias == "step" || - preValue.Alias == "max") - return decimal.TryParse(preValue.Value, out var d) ? (decimal?) d : null; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return decimal.TryParse(preValue.Value, out var d) ? (decimal?)d : null; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs index 30507ac3ec..2faaca6086 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs @@ -1,43 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DefaultPreValueMigrator : IPreValueMigrator { - class DefaultPreValueMigrator : IPreValueMigrator + public virtual bool CanMigrate(string editorAlias) + => true; + + public virtual string? GetNewAlias(string editorAlias) + => editorAlias; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) { - public virtual bool CanMigrate(string editorAlias) - => true; - - public virtual string? GetNewAlias(string editorAlias) - => editorAlias; - - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + var preValuesA = preValues.Values.ToList(); + var aliases = preValuesA.Select(x => x.Alias).Distinct().ToArray(); + if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) { - var preValuesA = preValues.Values.ToList(); - var aliases = preValuesA.Select(x => x.Alias).Distinct().ToArray(); - if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) + // array-based prevalues + return new Dictionary { - // array-based prevalues - return new Dictionary { ["values"] = preValuesA.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray() }; - } - - // assuming we don't want to fall back to array - if (aliases.Any(string.IsNullOrWhiteSpace)) - throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); - - // dictionary-base prevalues - return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); + ["values"] = preValuesA.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray(), + }; } - protected virtual IEnumerable GetPreValues(IEnumerable preValues) - => preValues; - - protected virtual object? GetPreValueValue(PreValueDto preValue) + // assuming we don't want to fall back to array + if (aliases.Any(string.IsNullOrWhiteSpace)) { - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + throw new InvalidOperationException( + $"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); } + + // dictionary-base prevalues + return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); } + + protected virtual IEnumerable GetPreValues(IEnumerable preValues) + => preValues; + + protected virtual object? GetPreValueValue(PreValueDto preValue) => preValue.Value?.DetectIsJson() ?? false + ? JsonConvert.DeserializeObject(preValue.Value) + : preValue.Value; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs index 6c0f3d4869..6588676283 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs @@ -1,31 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DropDownFlexiblePreValueMigrator : IPreValueMigrator { - class DropDownFlexiblePreValueMigrator : IPreValueMigrator + public bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.DropDown.Flexible"; + + public virtual string? GetNewAlias(string editorAlias) + => null; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) { - public bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.DropDown.Flexible"; - - public virtual string? GetNewAlias(string editorAlias) - => null; - - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + var config = new DropDownFlexibleConfiguration(); + foreach (PreValueDto preValue in preValues.Values) { - var config = new DropDownFlexibleConfiguration(); - foreach (var preValue in preValues.Values) + if (preValue.Alias == "multiple") { - if (preValue.Alias == "multiple") - { - config.Multiple = (preValue.Value == "1"); - } - else - { - config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); - } + config.Multiple = preValue.Value == "1"; + } + else + { + config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); } - return config; } + + return config; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs index 5489fd626e..11a126a60b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs @@ -1,36 +1,35 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +/// +/// Defines a service migrating preValues. +/// +public interface IPreValueMigrator { /// - /// Defines a service migrating preValues. + /// Determines whether this migrator can migrate a data type. /// - public interface IPreValueMigrator - { - /// - /// Determines whether this migrator can migrate a data type. - /// - /// The data type editor alias. - bool CanMigrate(string editorAlias); + /// The data type editor alias. + bool CanMigrate(string editorAlias); - /// - /// Gets the v8 codebase data type editor alias. - /// - /// The original v7 codebase editor alias. - /// - /// This is used to validate that the migrated configuration can be parsed - /// by the new property editor. Return null to bypass this validation, - /// when for instance we know it will fail, and another, later migration will - /// deal with it. - /// - string? GetNewAlias(string editorAlias); + /// + /// Gets the v8 codebase data type editor alias. + /// + /// The original v7 codebase editor alias. + /// + /// + /// This is used to validate that the migrated configuration can be parsed + /// by the new property editor. Return null to bypass this validation, + /// when for instance we know it will fail, and another, later migration will + /// deal with it. + /// + /// + string? GetNewAlias(string editorAlias); - /// - /// Gets the configuration object corresponding to preValue. - /// - /// The data type identifier. - /// The data type editor alias. - /// PreValues. - object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); - } + /// + /// Gets the configuration object corresponding to preValue. + /// + /// The data type identifier. + /// The data type editor alias. + /// PreValues. + object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs index c306e3eef3..7879e9c67d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs @@ -1,29 +1,26 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ListViewPreValueMigrator : DefaultPreValueMigrator { - class ListViewPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.ListView"; + + protected override IEnumerable GetPreValues(IEnumerable preValues) => + preValues.Where(preValue => preValue.Alias != "displayAtTabNumber"); + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.ListView"; - - protected override IEnumerable GetPreValues(IEnumerable preValues) + if (preValue.Alias == "pageSize") { - return preValues.Where(preValue => preValue.Alias != "displayAtTabNumber"); + return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? (int?)i + : null; } - protected override object? GetPreValueValue(PreValueDto preValue) - { - if (preValue.Alias == "pageSize") - { - return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? (int?)i : null; - } - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; - } + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs index 9f8e7da57a..eff4b82477 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs @@ -1,16 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator // PreValueMigratorBase { - class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MarkdownEditor; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "preview") { - if (preValue.Alias == "preview") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); + return preValue.Value == "1"; } + + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs index 364cc3e86b..c630693073 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs @@ -1,37 +1,37 @@ -using System.Linq; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class MediaPickerPreValueMigrator : DefaultPreValueMigrator // PreValueMigratorBase { - class MediaPickerPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + private readonly string[] _editors = { - private readonly string[] _editors = + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker, + }; + + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.MediaPicker; + + // you wish - but MediaPickerConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + return new MediaPickerConfiguration { ... }; + } + */ + + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "multiPicker" || + preValue.Alias == "onlyImages" || + preValue.Alias == "disableFolderSelect") { - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, - Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - }; - - public override bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); - - public override string GetNewAlias(string editorAlias) - => Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker; - - // you wish - but MediaPickerConfiguration lives in Umbraco.Web - /* - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) - { - return new MediaPickerConfiguration { ... }; + return preValue.Value == "1"; } - */ - protected override object? GetPreValueValue(PreValueDto preValue) - { - if (preValue.Alias == "multiPicker" || - preValue.Alias == "onlyImages" || - preValue.Alias == "disableFolderSelect") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); - } + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs index 761f55be4e..72c28dc8ad 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs @@ -1,34 +1,39 @@ -using System.Globalization; +using System.Globalization; using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class NestedContentPreValueMigrator : DefaultPreValueMigrator // PreValueMigratorBase { - class NestedContentPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.NestedContent"; + + // you wish - but NestedContentConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.NestedContent"; + return new NestedContentConfiguration { ... }; + } + */ - // you wish - but NestedContentConfiguration lives in Umbraco.Web - /* - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "confirmDeletes" || + preValue.Alias == "showIcons" || + preValue.Alias == "hideLabel") { - return new NestedContentConfiguration { ... }; + return preValue.Value == "1"; } - */ - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "minItems" || + preValue.Alias == "maxItems") { - if (preValue.Alias == "confirmDeletes" || - preValue.Alias == "showIcons" || - preValue.Alias == "hideLabel") - return preValue.Value == "1"; - - if (preValue.Alias == "minItems" || - preValue.Alias == "maxItems") - return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? (int?)i : null; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? (int?)i + : null; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs index d3f4b06737..d3e639452e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs @@ -1,24 +1,23 @@ -using NPoco; +using NPoco; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +[TableName("cmsDataTypePreValues")] +[ExplicitColumns] +public class PreValueDto { - [TableName("cmsDataTypePreValues")] - [ExplicitColumns] - public class PreValueDto - { - [Column("id")] - public int Id { get; set; } + [Column("id")] + public int Id { get; set; } - [Column("datatypeNodeId")] - public int NodeId { get; set; } + [Column("datatypeNodeId")] + public int NodeId { get; set; } - [Column("alias")] - public string Alias { get; set; } = null!; + [Column("alias")] + public string Alias { get; set; } = null!; - [Column("sortorder")] - public int SortOrder { get; set; } + [Column("sortorder")] + public int SortOrder { get; set; } - [Column("value")] - public string? Value { get; set; } - } + [Column("value")] + public string? Value { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs index d4f5f4c425..df9cb27ec3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +public abstract class PreValueMigratorBase : IPreValueMigrator { - public abstract class PreValueMigratorBase : IPreValueMigrator - { - public abstract bool CanMigrate(string editorAlias); + public abstract bool CanMigrate(string editorAlias); - public virtual string GetNewAlias(string editorAlias) - => editorAlias; + public virtual string GetNewAlias(string editorAlias) + => editorAlias; - public abstract object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); + public abstract object GetConfiguration(int dataTypeId, string editorAlias, + Dictionary preValues); - protected bool GetBoolValue(Dictionary preValues, string alias, bool defaultValue = false) - => preValues.TryGetValue(alias, out var preValue) ? preValue.Value == "1" : defaultValue; + protected bool GetBoolValue(Dictionary preValues, string alias, bool defaultValue = false) + => preValues.TryGetValue(alias, out PreValueDto? preValue) ? preValue.Value == "1" : defaultValue; - protected decimal GetDecimalValue(Dictionary preValues, string alias, decimal defaultValue = 0) - => preValues.TryGetValue(alias, out var preValue) && decimal.TryParse(preValue.Value, out var value) ? value : defaultValue; - } + protected decimal GetDecimalValue(Dictionary preValues, string alias, decimal defaultValue = 0) + => preValues.TryGetValue(alias, out PreValueDto? preValue) && decimal.TryParse(preValue.Value, out var value) + ? value + : defaultValue; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs index b304098188..81a6200991 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs @@ -1,27 +1,23 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +public class PreValueMigratorCollection : BuilderCollectionBase { - public class PreValueMigratorCollection : BuilderCollectionBase + private readonly ILogger _logger; + + public PreValueMigratorCollection( + Func> items, + ILogger logger) + : base(items) => + _logger = logger; + + public IPreValueMigrator? GetMigrator(string editorAlias) { - private readonly ILogger _logger; - - public PreValueMigratorCollection(Func> items, ILogger logger) - : base(items) - { - _logger = logger; - } - - public IPreValueMigrator? GetMigrator(string editorAlias) - { - var migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); - _logger.LogDebug("Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, migrator == null ? "" : migrator.GetType().Name); - return migrator; - } + IPreValueMigrator? migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); + _logger.LogDebug("Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, + migrator == null ? "" : migrator.GetType().Name); + return migrator; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs index 2c90a0d504..ba335eef4b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +public class PreValueMigratorCollectionBuilder : OrderedCollectionBuilderBase { - public class PreValueMigratorCollectionBuilder : OrderedCollectionBuilderBase - { - protected override PreValueMigratorCollectionBuilder This => this; - } + protected override PreValueMigratorCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs index 5d05de56c3..273c8ae51b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs @@ -1,27 +1,23 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Exceptions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class RenamingPreValueMigrator : DefaultPreValueMigrator { - class RenamingPreValueMigrator : DefaultPreValueMigrator + private readonly string[] _editors = { "Umbraco.NoEdit" }; + + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public override string GetNewAlias(string editorAlias) { - private readonly string[] _editors = + switch (editorAlias) { - "Umbraco.NoEdit" - }; - - public override bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); - - public override string GetNewAlias(string editorAlias) - { - switch (editorAlias) - { - case "Umbraco.NoEdit": - return Cms.Core.Constants.PropertyEditors.Aliases.Label; - default: - throw new PanicException($"The alias {editorAlias} is not supported"); - } + case "Umbraco.NoEdit": + return Constants.PropertyEditors.Aliases.Label; + default: + throw new PanicException($"The alias {editorAlias} is not supported"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs index 0abcd86a96..4e7c5b79f1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs @@ -1,22 +1,24 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class RichTextPreValueMigrator : DefaultPreValueMigrator { - class RichTextPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.TinyMCEv3"; + + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.TinyMce; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.TinyMCEv3"; - - public override string GetNewAlias(string editorAlias) - => Cms.Core.Constants.PropertyEditors.Aliases.TinyMce; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "hideLabel") { - if (preValue.Alias == "hideLabel") - return preValue.Value == "1"; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return preValue.Value == "1"; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs index c193f27028..7f8632dd7a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs @@ -1,24 +1,21 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes -{ - class UmbracoSliderPreValueMigrator : PreValueMigratorBase - { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.Slider"; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) +internal class UmbracoSliderPreValueMigrator : PreValueMigratorBase +{ + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Slider"; + + public override object GetConfiguration(int dataTypeId, string editorAlias, + Dictionary preValues) => + new SliderConfiguration { - return new SliderConfiguration - { - EnableRange = GetBoolValue(preValues, "enableRange"), - InitialValue = GetDecimalValue(preValues, "initVal1"), - InitialValue2 = GetDecimalValue(preValues, "initVal2"), - MaximumValue = GetDecimalValue(preValues, "maxVal"), - MinimumValue = GetDecimalValue(preValues, "minVal"), - StepIncrements = GetDecimalValue(preValues, "step") - }; - } - } + EnableRange = GetBoolValue(preValues, "enableRange"), + InitialValue = GetDecimalValue(preValues, "initVal1"), + InitialValue2 = GetDecimalValue(preValues, "initVal2"), + MaximumValue = GetDecimalValue(preValues, "maxVal"), + MinimumValue = GetDecimalValue(preValues, "minVal"), + StepIncrements = GetDecimalValue(preValues, "step"), + }; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs index 44b12addd2..9528cadc8b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -1,33 +1,29 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ValueListPreValueMigrator : IPreValueMigrator { - class ValueListPreValueMigrator : IPreValueMigrator + private readonly string[] _editors = { - private readonly string[] _editors = + "Umbraco.RadioButtonList", "Umbraco.CheckBoxList", "Umbraco.DropDown", "Umbraco.DropdownlistPublishingKeys", + "Umbraco.DropDownMultiple", "Umbraco.DropdownlistMultiplePublishKeys", + }; + + public bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public virtual string? GetNewAlias(string editorAlias) + => null; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var config = new ValueListConfiguration(); + foreach (PreValueDto preValue in preValues.Values) { - "Umbraco.RadioButtonList", - "Umbraco.CheckBoxList", - "Umbraco.DropDown", - "Umbraco.DropdownlistPublishingKeys", - "Umbraco.DropDownMultiple", - "Umbraco.DropdownlistMultiplePublishKeys" - }; - - public bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); - - public virtual string? GetNewAlias(string editorAlias) - => null; - - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) - { - var config = new ValueListConfiguration(); - foreach (var preValue in preValues.Values) - config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); - return config; + config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); } + + return config; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index 0d4b6020a9..e80dd72765 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -13,125 +11,133 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase { - public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + public DropDownPropertyEditorsMigration(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - public DropDownPropertyEditorsMigration(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) + public DropDownPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } + + protected override void Migrate() + { + var refreshCache = Migrate(GetDataTypes(".DropDown", false)); + + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) { + Context.AddPostMigration(); } + } - public DropDownPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, - IEditorConfigurationParser editorConfigurationParser) - : base(context) + private bool Migrate(IEnumerable dataTypes) + { + var refreshCache = false; + ConfigurationEditor? configurationEditor = null; + + foreach (DataTypeDto dataType in dataTypes) { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } + ValueListConfiguration config; - protected override void Migrate() - { - var refreshCache = Migrate(GetDataTypes(".DropDown", false)); - - // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), - // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - Context.AddPostMigration(); - } - - private bool Migrate(IEnumerable dataTypes) - { - var refreshCache = false; - ConfigurationEditor? configurationEditor = null; - - foreach (var dataType in dataTypes) + if (!dataType.Configuration.IsNullOrWhiteSpace()) { - ValueListConfiguration config; - - if (!dataType.Configuration.IsNullOrWhiteSpace()) + // parse configuration, and update everything accordingly + if (configurationEditor == null) { - // parse configuration, and update everything accordingly - if (configurationEditor == null) - configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); - try - { - config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration, _configurationEditorJsonSerializer); - } - catch (Exception ex) - { - Logger.LogError( - ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", - dataType.Configuration); - - // reset - config = new ValueListConfiguration(); - } - - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, true)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) - Database.Update(propertyDataDto); + configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); } - else + + try { - // default configuration + config = (ValueListConfiguration)configurationEditor.FromDatabase( + dataType.Configuration, + _configurationEditorJsonSerializer); + } + catch (Exception ex) + { + Logger.LogError( + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", + dataType.Configuration); + + // reset config = new ValueListConfiguration(); } - switch (dataType.EditorAlias) - { - case string ea when ea.InvariantEquals("Umbraco.DropDown"): - UpdateDataType(dataType, config, false); - break; - case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): - UpdateDataType(dataType, config, false); - break; - case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): - UpdateDataType(dataType, config, true); - break; - case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): - UpdateDataType(dataType, config, true); - break; - } + // get property data dtos + List? propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin() + .On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); - refreshCache = true; + // update dtos + IEnumerable updatedDtos = + propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, true)); + + // persist changes + foreach (PropertyDataDto? propertyDataDto in updatedDtos) + { + Database.Update(propertyDataDto); + } + } + else + { + // default configuration + config = new ValueListConfiguration(); } - return refreshCache; - } - - private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) - { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible; - - var flexConfig = new DropDownFlexibleConfiguration + switch (dataType.EditorAlias) { - Items = config.Items, - Multiple = isMultiple - }; - dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); + case string ea when ea.InvariantEquals("Umbraco.DropDown"): + UpdateDataType(dataType, config, false); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): + UpdateDataType(dataType, config, false); + break; + case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): + UpdateDataType(dataType, config, true); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): + UpdateDataType(dataType, config, true); + break; + } - Database.Update(dataType); + refreshCache = true; } + + return refreshCache; + } + + private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; + + var flexConfig = new DropDownFlexibleConfiguration { Items = config.Items, Multiple = isMultiple }; + dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); + + Database.Update(dataType); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs index 0d1e0506cb..8eebd91772 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropMigrationsTable : MigrationBase - { - public DropMigrationsTable(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropMigrationsTable : MigrationBase +{ + public DropMigrationsTable(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (TableExists("umbracoMigration")) { - if (TableExists("umbracoMigration")) - Delete.Table("umbracoMigration").Do(); + Delete.Table("umbracoMigration").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs index 0195e51e6e..64152d0cb3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs @@ -1,15 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropPreValueTable : MigrationBase - { - public DropPreValueTable(IMigrationContext context) : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropPreValueTable : MigrationBase +{ + public DropPreValueTable(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // drop preValues table + if (TableExists("cmsDataTypePreValues")) { - // drop preValues table - if (TableExists("cmsDataTypePreValues")) - Delete.Table("cmsDataTypePreValues").Do(); + Delete.Table("cmsDataTypePreValues").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs index b4004c1c82..e38c0c3292 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs @@ -1,17 +1,22 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropTaskTables : MigrationBase - { - public DropTaskTables(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropTaskTables : MigrationBase +{ + public DropTaskTables(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (TableExists("cmsTask")) { - if (TableExists("cmsTask")) - Delete.Table("cmsTask").Do(); - if (TableExists("cmsTaskType")) - Delete.Table("cmsTaskType").Do(); + Delete.Table("cmsTask").Do(); + } + + if (TableExists("cmsTaskType")) + { + Delete.Table("cmsTaskType").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs index 9f65689a59..454644b1fb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropTemplateDesignColumn : MigrationBase - { - public DropTemplateDesignColumn(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropTemplateDesignColumn : MigrationBase +{ + public DropTemplateDesignColumn(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (ColumnExists("cmsTemplate", "design")) { - if(ColumnExists("cmsTemplate", "design")) - Delete.Column("design").FromTable("cmsTemplate").Do(); + Delete.Column("design").FromTable("cmsTemplate").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs index 3e86e142aa..1cdb73d410 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs @@ -1,17 +1,22 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropXmlTables : MigrationBase - { - public DropXmlTables(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropXmlTables : MigrationBase +{ + public DropXmlTables(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (TableExists("cmsContentXml")) { - if (TableExists("cmsContentXml")) - Delete.Table("cmsContentXml").Do(); - if (TableExists("cmsPreviewXml")) - Delete.Table("cmsPreviewXml").Do(); + Delete.Table("cmsContentXml").Do(); + } + + if (TableExists("cmsPreviewXml")) + { + Delete.Table("cmsPreviewXml").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs index 48e00df2ff..4f3b685ef5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs @@ -1,25 +1,30 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +/// +/// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for +/// a given language. +/// +public class FallbackLanguage : MigrationBase { - /// - /// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for - /// a given language. - /// - public class FallbackLanguage : MigrationBase + public FallbackLanguage(IMigrationContext context) + : base(context) { - public FallbackLanguage(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + ColumnInfo[] columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => + x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && + x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals(Cms.Core.Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) - AddColumn("fallbackLanguageId"); + AddColumn("fallbackLanguageId"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs index 7a35dc12ed..f1bd804c59 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs @@ -1,21 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class FixLanguageIsoCodeLength : MigrationBase { - public class FixLanguageIsoCodeLength : MigrationBase + public FixLanguageIsoCodeLength(IMigrationContext context) + : base(context) { - public FixLanguageIsoCodeLength(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // there is some confusion here when upgrading from v7 - // it should be 14 already but that's not always the case - - Alter.Table("umbracoLanguage") - .AlterColumn("languageISOCode") - .AsString(14) - .Nullable() - .Do(); - } } + + protected override void Migrate() => + + // there is some confusion here when upgrading from v7 + // it should be 14 already but that's not always the case + Alter.Table("umbracoLanguage") + .AlterColumn("languageISOCode") + .AsString(14) + .Nullable() + .Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs index f6aa86259f..a265195bc9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs @@ -1,17 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class LanguageColumns : MigrationBase { - public class LanguageColumns : MigrationBase + public LanguageColumns(IMigrationContext context) + : base(context) { - public LanguageColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.Language, "isDefaultVariantLang"); - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.Language, "mandatory"); - } + protected override void Migrate() + { + AddColumn(Constants.DatabaseSchema.Tables.Language, "isDefaultVariantLang"); + AddColumn(Constants.DatabaseSchema.Tables.Language, "mandatory"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs index 7958f4fbf8..64c8d4c8b4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs @@ -1,16 +1,13 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MakeRedirectUrlVariant : MigrationBase { - public class MakeRedirectUrlVariant : MigrationBase + public MakeRedirectUrlVariant(IMigrationContext context) + : base(context) { - public MakeRedirectUrlVariant(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - AddColumn("culture"); - } } + + protected override void Migrate() => AddColumn("culture"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs index 74cdd88357..630a853aa2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs @@ -1,16 +1,13 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MakeTagsVariant : MigrationBase { - public class MakeTagsVariant : MigrationBase + public MakeTagsVariant(IMigrationContext context) + : base(context) { - public MakeTagsVariant(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - AddColumn("languageId"); - } } + + protected override void Migrate() => AddColumn("languageId"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index db7766213c..f89a6d1497 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -10,87 +9,91 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MergeDateAndDateTimePropertyEditor : MigrationBase { - public class MergeDateAndDateTimePropertyEditor : MigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) + public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } + + protected override void Migrate() + { + List dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date); + + foreach (DataTypeDto dataType in dataTypes) { - } - - public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, IEditorConfigurationParser editorConfigurationParser) - : base(context) - { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } - - protected override void Migrate() - { - var dataTypes = GetDataTypes(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Date); - - foreach (var dataType in dataTypes) + DateTimeConfiguration config; + try { - DateTimeConfiguration config; - try + config = (DateTimeConfiguration)new CustomDateTimeConfigurationEditor( + _ioHelper, + _editorConfigurationParser).FromDatabase( + dataType.Configuration, _configurationEditorJsonSerializer); + + // If the Umbraco.Date type is the default from V7 and it has never been updated, then the + // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that + // after the migration, so we force the format to the default from V7. + if (string.IsNullOrEmpty(dataType.Configuration)) { - config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor(_ioHelper, _editorConfigurationParser).FromDatabase( - dataType.Configuration, _configurationEditorJsonSerializer); - - // If the Umbraco.Date type is the default from V7 and it has never been updated, then the - // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that - // after the migration, so we force the format to the default from V7. - if (string.IsNullOrEmpty(dataType.Configuration)) - { - config.Format = "YYYY-MM-DD"; - } + config.Format = "YYYY-MM-DD"; } - catch (Exception ex) - { - Logger.LogError( - ex, - "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", - dataType.Configuration); - - continue; - } - - config.OffsetTime = false; - - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DateTime; - dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); - - Database.Update(dataType); } - } - - - - private List GetDataTypes(string editorAlias) - { - //need to convert the old drop down data types to use the new one - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias == editorAlias)); - return dataTypes; - } - - - - private class CustomDateTimeConfigurationEditor : ConfigurationEditor - { - public CustomDateTimeConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) + catch (Exception ex) { + Logger.LogError( + ex, + "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", + dataType.Configuration); + + continue; } + + config.OffsetTime = false; + + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DateTime; + dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); + + Database.Update(dataType); + } + } + + private List GetDataTypes(string editorAlias) + { + // need to convert the old drop down data types to use the new one + List? dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias == editorAlias)); + return dataTypes; + } + + private class CustomDateTimeConfigurationEditor : ConfigurationEditor + { + public CustomDateTimeConfigurationEditor( + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs index bbd1646ad5..856c81af52 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs @@ -1,63 +1,63 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations the schema of this table changed and running SQL against the new table would +/// result in errors +/// +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class ContentTypeDto80 { + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors - /// - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class ContentTypeDto80 - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentType; + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 535)] + public int PrimaryKey { get; set; } - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] - public int PrimaryKey { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get; set; } - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get; set; } + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string? Thumbnail { get; set; } - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string? Thumbnail { get; set; } + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string? Description { get; set; } - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string? Description { get; set; } + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - public NodeDto? NodeDto { get; set; } - } + [ResultColumn] + public NodeDto? NodeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs index 1e9e93aa53..4c537472db 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs @@ -1,143 +1,142 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations the schema of this table changed and running SQL against the new table would +/// result in errors +/// +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyDataDto80 { - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors - /// - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyDataDto80 + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", + ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto80))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string? Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.PropertyData; - public const int VarcharLength = 512; - public const int SegmentLength = 256; + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } - private decimal? _decimalValue; + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } - // pk, not used at the moment (never updating) - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string? VarcharValue { get; set; } - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] - public int VersionId { get; set; } + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TextValue { get; set; } - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto80))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] - public int PropertyTypeId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto80? PropertyTypeDto { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get; set; } - - [Column("segment")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(SegmentLength)] - public string? Segment { get; set; } - - [Column("intValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? IntegerValue { get; set; } - - [Column("decimalValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public decimal? DecimalValue + [Ignore] + public object? Value + { + get { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } - - [Column("dateValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? DateValue { get; set; } - - [Column("varcharValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(VarcharLength)] - public string? VarcharValue { get; set; } - - [Column("textValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TextValue { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] - public PropertyTypeDto80? PropertyTypeDto { get; set; } - - [Ignore] - public object? Value - { - get + if (IntegerValue.HasValue) { - if (IntegerValue.HasValue) - return IntegerValue.Value; - - if (DecimalValue.HasValue) - return DecimalValue.Value; - - if (DateValue.HasValue) - return DateValue.Value; - - if (!string.IsNullOrEmpty(VarcharValue)) - return VarcharValue; - - if (!string.IsNullOrEmpty(TextValue)) - return TextValue; - - return null; + return IntegerValue.Value; } - } - public PropertyDataDto80 Clone(int versionId) - { - return new PropertyDataDto80 + if (DecimalValue.HasValue) { - VersionId = versionId, - PropertyTypeId = PropertyTypeId, - LanguageId = LanguageId, - Segment = Segment, - IntegerValue = IntegerValue, - DecimalValue = DecimalValue, - DateValue = DateValue, - VarcharValue = VarcharValue, - TextValue = TextValue, - PropertyTypeDto = PropertyTypeDto - }; - } + return DecimalValue.Value; + } - protected bool Equals(PropertyDataDto other) - { - return Id == other.Id; - } + if (DateValue.HasValue) + { + return DateValue.Value; + } - public override bool Equals(object? other) - { - return - !ReferenceEquals(null, other) // other is not null - && (ReferenceEquals(this, other) // and either ref-equals, or same id - || other is PropertyDataDto pdata && pdata.Id == Id); - } + if (!string.IsNullOrEmpty(VarcharValue)) + { + return VarcharValue; + } - public override int GetHashCode() - { - // ReSharper disable once NonReadonlyMemberInGetHashCode - return Id; + if (!string.IsNullOrEmpty(TextValue)) + { + return TextValue; + } + + return null; } } + + public PropertyDataDto80 Clone(int versionId) => + new PropertyDataDto80 + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + + protected bool Equals(PropertyDataDto other) => Id == other.Id; + + public override bool Equals(object? other) => + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || (other is PropertyDataDto pdata && pdata.Id == Id)); + + public override int GetHashCode() => + // ReSharper disable once NonReadonlyMemberInGetHashCode + Id; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs index 4d61521d00..a5a7f48d6d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs @@ -1,76 +1,76 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table +/// would result in errors +/// +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeDto80 { - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table would result in errors - /// - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeDto80 - { - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 50)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 50)] + public int Id { get; set; } - [Column("dataTypeId")] - [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } - [Column("propertyTypeGroupId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(PropertyTypeGroupDto))] - public int? PropertyTypeGroupId { get; set; } + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } - [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] - [Column("Alias")] - public string Alias { get; set; } = null!; + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string Alias { get; set; } = null!; - [Column("Name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } - [Column("sortOrder")] - [Constraint(Default = "0")] - public int SortOrder { get; set; } + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } - [Column("mandatory")] - [Constraint(Default = "0")] - public bool Mandatory { get; set; } + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } - [Column("validationRegExp")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? ValidationRegExp { get; set; } - [Column("Description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(2000)] - public string? Description { get; set; } + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string? Description { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] - public DataTypeDto? DataTypeDto { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto? DataTypeDto { get; set; } - [Column("UniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs index 935d51dacd..cef6eb974f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs @@ -1,54 +1,63 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class PropertyEditorsMigration : MigrationBase { - public class PropertyEditorsMigration : MigrationBase + public PropertyEditorsMigration(IMigrationContext context) + : base(context) { - public PropertyEditorsMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Cms.Core.Constants.PropertyEditors.Aliases.MemberPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Cms.Core.Constants.PropertyEditors.Aliases.TextArea, false); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Textbox, Cms.Core.Constants.PropertyEditors.Aliases.TextBox, false); - } + protected override void Migrate() + { + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Aliases.ContentPicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Aliases.MediaPicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Aliases.MemberPicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Aliases.TextArea, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, + false); + } - private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) + private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) + { + if (checkCollision) { - if (checkCollision) + var oldCount = Database.ExecuteScalar(Sql() + .SelectCount() + .From() + .Where(x => x.EditorAlias == toAlias)); + + if (oldCount > 0) { - var oldCount = Database.ExecuteScalar(Sql() - .SelectCount() - .From() - .Where(x => x.EditorAlias == toAlias)); - - if (oldCount > 0) - { - // If we throw it means that the upgrade will exit and cannot continue. - // This will occur if a v7 site has the old "Obsolete" property editors that are already named with the `toAlias` name. - // TODO: We should have an additional upgrade step when going from 7 -> 8 like we did with 6 -> 7 that shows a compatibility report, - // this would include this check and then we can provide users with information on what they should do (i.e. before upgrading to v8 they will - // need to migrate these old obsolete editors to non-obsolete editors) - - throw new InvalidOperationException( - $"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used." + - $"This is generally because when upgrading from a v7 to v8 site, the v7 site contains Data Types that reference old and already Obsolete " + - $"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + - $"to the non-obsolete v7 property editors of the same type."); - } - + // If we throw it means that the upgrade will exit and cannot continue. + // This will occur if a v7 site has the old "Obsolete" property editors that are already named with the `toAlias` name. + // TODO: We should have an additional upgrade step when going from 7 -> 8 like we did with 6 -> 7 that shows a compatibility report, + // this would include this check and then we can provide users with information on what they should do (i.e. before upgrading to v8 they will + // need to migrate these old obsolete editors to non-obsolete editors) + throw new InvalidOperationException( + $"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used." + + "This is generally because when upgrading from a v7 to v8 site, the v7 site contains Data Types that reference old and already Obsolete " + + "Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + + "to the non-obsolete v7 property editors of the same type."); } - - Database.Execute(Sql() - .Update(u => u.Set(x => x.EditorAlias, toAlias)) - .Where(x => x.EditorAlias == fromAlias)); } + + Database.Execute(Sql() + .Update(u => u.Set(x => x.EditorAlias, toAlias)) + .Where(x => x.EditorAlias == fromAlias)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs index 321da13df8..febf872e34 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs @@ -1,105 +1,115 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public abstract class PropertyEditorsMigrationBase : MigrationBase { - public abstract class PropertyEditorsMigrationBase : MigrationBase + protected PropertyEditorsMigrationBase(IMigrationContext context) + : base(context) { - protected PropertyEditorsMigrationBase(IMigrationContext context) - : base(context) - { } + } - internal List GetDataTypes(string editorAlias, bool strict = true) + internal List GetDataTypes(string editorAlias, bool strict = true) + { + Sql sql = Sql() + .Select() + .From(); + + sql = strict + ? sql.Where(x => x.EditorAlias == editorAlias) + : sql.Where(x => x.EditorAlias.Contains(editorAlias)); + + return Database.Fetch(sql); + } + + internal bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool isMultiple) + { + // Get the INT ids stored for this property/drop down + int[]? ids = null; + if (!propData.VarcharValue.IsNullOrWhiteSpace()) { - var sql = Sql() - .Select() - .From(); - - sql = strict - ? sql.Where(x => x.EditorAlias == editorAlias) - : sql.Where(x => x.EditorAlias.Contains(editorAlias)); - - return Database.Fetch(sql); + ids = ConvertStringValues(propData.VarcharValue); + } + else if (!propData.TextValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.TextValue); + } + else if (propData.IntegerValue.HasValue) + { + ids = new[] { propData.IntegerValue.Value }; } - protected int[]? ConvertStringValues(string? val) + // if there are INT ids, convert them to values based on the configuration + if (ids == null || ids.Length <= 0) { - var splitVals = val?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - - var intVals = splitVals? - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : int.MinValue) - .Where(x => x != int.MinValue) - .ToArray(); - - //only return if the number of values are the same (i.e. All INTs) - if (splitVals?.Length == intVals?.Length) - return intVals; - - return null; + return false; } - internal bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool isMultiple) + // map ids to values + var values = new List(); + var canConvert = true; + + foreach (var id in ids) { - //Get the INT ids stored for this property/drop down - int[]? ids = null; - if (!propData.VarcharValue.IsNullOrWhiteSpace()) + ValueListConfiguration.ValueListItem? val = config.Items.FirstOrDefault(x => x.Id == id); + if (val?.Value != null) { - ids = ConvertStringValues(propData.VarcharValue); - } - else if (!propData.TextValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.TextValue); - } - else if (propData.IntegerValue.HasValue) - { - ids = new[] { propData.IntegerValue.Value }; + values.Add(val.Value); + continue; } - // if there are INT ids, convert them to values based on the configuration - if (ids == null || ids.Length <= 0) return false; - - // map ids to values - var values = new List(); - var canConvert = true; - - foreach (var id in ids) - { - var val = config.Items.FirstOrDefault(x => x.Id == id); - if (val?.Value != null) - { - values.Add(val.Value); - continue; - } - - Logger.LogWarning("Could not find PropertyData {PropertyDataId} value '{PropertyValue}' in the datatype configuration: {Values}.", - propData.Id, id, string.Join(", ", config.Items.Select(x => x.Id + ":" + x.Value))); - canConvert = false; - } - - if (!canConvert) return false; - - propData.VarcharValue = isMultiple ? JsonConvert.SerializeObject(values) : values[0]; - propData.TextValue = null; - propData.IntegerValue = null; - return true; + Logger.LogWarning( + "Could not find PropertyData {PropertyDataId} value '{PropertyValue}' in the datatype configuration: {Values}.", + propData.Id, id, string.Join(", ", config.Items.Select(x => x.Id + ":" + x.Value))); + canConvert = false; } - // dummy editor for deserialization - protected class ValueListConfigurationEditor : ConfigurationEditor + if (!canConvert) + { + return false; + } + + propData.VarcharValue = isMultiple ? JsonConvert.SerializeObject(values) : values[0]; + propData.TextValue = null; + propData.IntegerValue = null; + return true; + } + + protected int[]? ConvertStringValues(string? val) + { + var splitVals = val?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + + var intVals = splitVals? + .Select(x => + int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : int.MinValue) + .Where(x => x != int.MinValue) + .ToArray(); + + // only return if the number of values are the same (i.e. All INTs) + if (splitVals?.Length == intVals?.Length) + { + return intVals; + } + + return null; + } + + // dummy editor for deserialization + protected class ValueListConfigurationEditor : ConfigurationEditor + { + public ValueListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public ValueListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs index f7114fb0bd..ab9b01a3b2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -13,101 +11,114 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigrationBase { - public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + public RadioAndCheckboxPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - public RadioAndCheckboxPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) + public RadioAndCheckboxPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } + + protected override void Migrate() + { + var refreshCache = false; + + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.RadioButtonList), false); + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.CheckBoxList), true); + + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) { - } - - public RadioAndCheckboxPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, - IEditorConfigurationParser editorConfigurationParser) - : base(context) - { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } - - protected override void Migrate() - { - var refreshCache = false; - - refreshCache |= Migrate(GetDataTypes(Cms.Core.Constants.PropertyEditors.Aliases.RadioButtonList), false); - refreshCache |= Migrate(GetDataTypes(Cms.Core.Constants.PropertyEditors.Aliases.CheckBoxList), true); - - // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), - // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - Context.AddPostMigration(); - } - - private bool Migrate(IEnumerable dataTypes, bool isMultiple) - { - var refreshCache = false; - ConfigurationEditor? configurationEditor = null; - - foreach (var dataType in dataTypes) - { - ValueListConfiguration config; - - if (dataType.Configuration.IsNullOrWhiteSpace()) - continue; - - // parse configuration, and update everything accordingly - if (configurationEditor == null) - configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); - try - { - config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration, _configurationEditorJsonSerializer); - } - catch (Exception ex) - { - Logger.LogError( - ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", - dataType.Configuration); - - continue; - } - - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, isMultiple)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) - Database.Update(propertyDataDto); - - UpdateDataType(dataType); - refreshCache = true; - } - - return refreshCache; - } - - private void UpdateDataType(DataTypeDto dataType) - { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); - Database.Update(dataType); + Context.AddPostMigration(); } } + + private bool Migrate(IEnumerable dataTypes, bool isMultiple) + { + var refreshCache = false; + ConfigurationEditor? configurationEditor = null; + + foreach (DataTypeDto dataType in dataTypes) + { + ValueListConfiguration config; + + if (dataType.Configuration.IsNullOrWhiteSpace()) + { + continue; + } + + // parse configuration, and update everything accordingly + if (configurationEditor == null) + { + configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); + } + + try + { + config = (ValueListConfiguration)configurationEditor.FromDatabase( + dataType.Configuration, + _configurationEditorJsonSerializer); + } + catch (Exception ex) + { + Logger.LogError( + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", + dataType.Configuration); + + continue; + } + + // get property data dtos + List? propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin() + .On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + IEnumerable updatedDtos = + propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, isMultiple)); + + // persist changes + foreach (PropertyDataDto? propertyDataDto in updatedDtos) + { + Database.Update(propertyDataDto); + } + + UpdateDataType(dataType); + refreshCache = true; + } + + return refreshCache; + } + + private void UpdateDataType(DataTypeDto dataType) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + Database.Update(dataType); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs index 005a2ef464..f8d731e166 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs @@ -1,39 +1,55 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RefactorMacroColumns : MigrationBase { - public class RefactorMacroColumns : MigrationBase + public RefactorMacroColumns(IMigrationContext context) + : base(context) { - public RefactorMacroColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.Macro, "macroXSLT")) { - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.Macro, "macroXSLT")) + // special trick to add the column without constraints and return the sql to add them later + AddColumn("macroType", out IEnumerable sqls1); + AddColumn("macroSource", out IEnumerable sqls2); + + // populate the new columns with legacy data + // when the macro type is PartialView, it corresponds to 7, else it is 4 for Unknown + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = 4").Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = 4 WHERE macroXSLT != '' AND macroXSLT IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = 4 WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = 4 WHERE macroScriptType != '' AND macroScriptType IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = 7 WHERE macroPython != '' AND macroPython IS NOT NULL") + .Do(); + + // now apply constraints (NOT NULL) to new table + foreach (var sql in sqls1) { - //special trick to add the column without constraints and return the sql to add them later - AddColumn("macroType", out var sqls1); - AddColumn("macroSource", out var sqls2); - - //populate the new columns with legacy data - //when the macro type is PartialView, it corresponds to 7, else it is 4 for Unknown - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = 4").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = 4 WHERE macroXSLT != '' AND macroXSLT IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = 4 WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = 4 WHERE macroScriptType != '' AND macroScriptType IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = 7 WHERE macroPython != '' AND macroPython IS NOT NULL").Do(); - - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls1) Execute.Sql(sql).Do(); - foreach (var sql in sqls2) Execute.Sql(sql).Do(); - - //now remove these old columns - Delete.Column("macroXSLT").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroScriptAssembly").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroScriptType").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroPython").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); + Execute.Sql(sql).Do(); } + + foreach (var sql in sqls2) + { + Execute.Sql(sql).Do(); + } + + // now remove these old columns + Delete.Column("macroXSLT").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroScriptAssembly").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroScriptType").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroPython").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs index 1ff19e0698..500db8a4bc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs @@ -1,80 +1,99 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RefactorVariantsModel : MigrationBase { - public class RefactorVariantsModel : MigrationBase + public RefactorVariantsModel(IMigrationContext context) + : base(context) { - public RefactorVariantsModel(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "edited")) - Delete.Column("edited").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - - // add available column - AddColumn("available", out var sqls); - - // so far, only those cultures that were available had records in the table - Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do(); - - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - - // add published column - AddColumn("published", out sqls); - - // make it false by default - Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do(); - - // now figure out whether these available cultures are published, too - var getPublished = Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.LanguageId) - .From() - .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId) - .InnerJoin().On((cv, dv) => cv.Id == dv.Id && dv.Published) - .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); - - foreach (var dto in Database.Fetch(getPublished)) - Database.Execute(Sql() - .Update(u => u.Set(x => x.Published, true)) - .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); - - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - // so far, it was kinda impossible to make a culture unavailable again, - // so we *should* not have anything published but not available - ignore - - - // add name column - AddColumn("name"); - - // so far, every record in the table mapped to an available culture - var getNames = Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.LanguageId, x => x.Name) - .From() - .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId && cv.Current) - .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); - - foreach (var dto in Database.Fetch(getNames)) - Database.Execute(Sql() - .Update(u => u.Set(x => x.Name, dto.Name)) - .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); - } - - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class TempDto - { - public int NodeId { get; set; } - public int LanguageId { get; set; } - public string? Name { get; set; } - } - // ReSharper restore UnusedAutoPropertyAccessor.Local } + + protected override void Migrate() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "edited")) + { + Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + } + + // add available column + AddColumn("available", out IEnumerable sqls); + + // so far, only those cultures that were available had records in the table + Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do(); + + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // add published column + AddColumn("published", out sqls); + + // make it false by default + Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do(); + + // now figure out whether these available cultures are published, too + Sql getPublished = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId) + .From() + .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId) + .InnerJoin() + .On((cv, dv) => cv.Id == dv.Id && dv.Published) + .InnerJoin() + .On((cv, ccv) => cv.Id == ccv.VersionId); + + foreach (TempDto? dto in Database.Fetch(getPublished)) + { + Database.Execute(Sql() + .Update(u => u.Set(x => x.Published, true)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + } + + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // so far, it was kinda impossible to make a culture unavailable again, + // so we *should* not have anything published but not available - ignore + + // add name column + AddColumn("name"); + + // so far, every record in the table mapped to an available culture + Sql getNames = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId, x => x.Name) + .From() + .InnerJoin() + .On((node, cv) => node.NodeId == cv.NodeId && cv.Current) + .InnerJoin() + .On((cv, ccv) => cv.Id == ccv.VersionId); + + foreach (TempDto? dto in Database.Fetch(getNames)) + { + Database.Execute(Sql() + .Update(u => u.Set(x => x.Name, dto.Name)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + } + } + + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TempDto + { + public int NodeId { get; set; } + + public int LanguageId { get; set; } + + public string? Name { get; set; } + } + + // ReSharper restore UnusedAutoPropertyAccessor.Local } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs index c3fdf2d0fc..a638f17dc4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs @@ -1,41 +1,39 @@ -using System.Collections.Generic; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameLabelAndRichTextPropertyEditorAliases : MigrationBase { - public class RenameLabelAndRichTextPropertyEditorAliases : MigrationBase + public RenameLabelAndRichTextPropertyEditorAliases(IMigrationContext context) + : base(context) { - public RenameLabelAndRichTextPropertyEditorAliases(IMigrationContext context) - : base(context) + } + + protected override void Migrate() + { + MigratePropertyEditorAlias("Umbraco.TinyMCEv3", Constants.PropertyEditors.Aliases.TinyMce); + MigratePropertyEditorAlias("Umbraco.NoEdit", Constants.PropertyEditors.Aliases.Label); + } + + private void MigratePropertyEditorAlias(string oldAlias, string newAlias) + { + List dataTypes = GetDataTypes(oldAlias); + + foreach (DataTypeDto dataType in dataTypes) { + dataType.EditorAlias = newAlias; + Database.Update(dataType); } + } - protected override void Migrate() - { - MigratePropertyEditorAlias("Umbraco.TinyMCEv3", Cms.Core.Constants.PropertyEditors.Aliases.TinyMce); - MigratePropertyEditorAlias("Umbraco.NoEdit", Cms.Core.Constants.PropertyEditors.Aliases.Label); - } - - private void MigratePropertyEditorAlias(string oldAlias, string newAlias) - { - var dataTypes = GetDataTypes(oldAlias); - - foreach (var dataType in dataTypes) - { - dataType.EditorAlias = newAlias; - Database.Update(dataType); - } - } - - private List GetDataTypes(string editorAlias) - { - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias == editorAlias)); - return dataTypes; - } - + private List GetDataTypes(string editorAlias) + { + List? dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias == editorAlias)); + return dataTypes; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs index fa88f17422..a6fe5c895b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs @@ -1,45 +1,48 @@ -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameMediaVersionTable : MigrationBase { - public class RenameMediaVersionTable : MigrationBase + public RenameMediaVersionTable(IMigrationContext context) + : base(context) { - public RenameMediaVersionTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Rename.Table("cmsMedia").To(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); + protected override void Migrate() + { + Rename.Table("cmsMedia").To(Constants.DatabaseSchema.Tables.MediaVersion).Do(); - // that is not supported on SqlCE - //Rename.Column("versionId").OnTable(Constants.DatabaseSchema.Tables.MediaVersion).To("id").Do(); + // that is not supported on SqlCE + // Rename.Column("versionId").OnTable(Constants.DatabaseSchema.Tables.MediaVersion).To("id").Do(); + AddColumn("id", out IEnumerable sqls); - AddColumn("id", out var sqls); - - Database.Execute($@"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id -FROM {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} m + Database.Execute($@"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id +FROM {Constants.DatabaseSchema.Tables.MediaVersion} m JOIN cmsContentVersion v on m.versionId = v.versionId JOIN umbracoNode n on v.contentId=n.id -WHERE n.nodeObjectType='{Cms.Core.Constants.ObjectTypes.Media}'"); +WHERE n.nodeObjectType='{Constants.ObjectTypes.Media}'"); - foreach (var sql in sqls) - Execute.Sql(sql).Do(); - - AddColumn("path", out sqls); - - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET path=mediaPath").Do(); - - foreach (var sql in sqls) - Execute.Sql(sql).Do(); - - // we had to run sqls to get the NULL constraints, but we need to get rid of most - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - - Delete.Column("mediaPath").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - Delete.Column("versionId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - Delete.Column("nodeId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); } + + AddColumn("path", out sqls); + + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET path=mediaPath").Do(); + + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // we had to run sqls to get the NULL constraints, but we need to get rid of most + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + + Delete.Column("mediaPath").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("versionId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("nodeId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs index 8bb2a8c14c..8611128458 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class RenameUmbracoDomainsTable : MigrationBase - { - public RenameUmbracoDomainsTable(IMigrationContext context) - : base(context) - { } +using Umbraco.Cms.Core; - protected override void Migrate() - { - Rename.Table("umbracoDomains").To(Cms.Core.Constants.DatabaseSchema.Tables.Domain).Do(); - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameUmbracoDomainsTable : MigrationBase +{ + public RenameUmbracoDomainsTable(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() => Rename.Table("umbracoDomains").To(Constants.DatabaseSchema.Tables.Domain).Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs index 4daab69962..135e562fde 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs @@ -1,20 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class SuperZero : MigrationBase { - public class SuperZero : MigrationBase + public SuperZero(IMigrationContext context) + : base(context) { - public SuperZero(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + var exists = Database.Fetch("select id from umbracoUser where id=-1;").Count > 0; + if (exists) { - var exists = Database.Fetch("select id from umbracoUser where id=-1;").Count > 0; - if (exists) return; + return; + } - Database.Execute("update umbracoUser set userLogin = userLogin + '__' where id=0"); + Database.Execute("update umbracoUser set userLogin = userLogin + '__' where id=0"); - Database.Execute("set identity_insert umbracoUser on;"); - Database.Execute(@" + Database.Execute("set identity_insert umbracoUser on;"); + Database.Execute(@" insert into umbracoUser (id, userDisabled, userNoConsole, userName, userLogin, userPassword, passwordConfig, userEmail, userLanguage, securityStampToken, failedLoginAttempts, lastLockoutDate, @@ -27,14 +33,13 @@ lastPasswordChangeDate, lastLoginDate, emailConfirmedDate, invitedDate, createDate, updateDate, avatar, tourData from umbracoUser where id=0;"); - Database.Execute("set identity_insert umbracoUser off;"); + Database.Execute("set identity_insert umbracoUser off;"); - Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); - Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); - Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); - Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); - Database.Execute($"update {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); - Database.Execute("delete from umbracoUser where id=0;"); - } + Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); + Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); + Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); + Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); + Database.Execute($"update {Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); + Database.Execute("delete from umbracoUser where id=0;"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs index 531e7a06cc..013375352e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -1,58 +1,57 @@ -using System; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TablesForScheduledPublishing : MigrationBase { - public class TablesForScheduledPublishing : MigrationBase + public TablesForScheduledPublishing(IMigrationContext context) + : base(context) { - public TablesForScheduledPublishing(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // Get anything currently scheduled + Sql? releaseSql = new Sql() + .Select("nodeId", "releaseDate") + .From("umbracoDocument") + .Where("releaseDate IS NOT NULL"); + Dictionary? releases = Database.Dictionary(releaseSql); + + Sql? expireSql = new Sql() + .Select("nodeId", "expireDate") + .From("umbracoDocument") + .Where("expireDate IS NOT NULL"); + Dictionary? expires = Database.Dictionary(expireSql); + + // drop old cols + Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); + Delete.Column("expireDate").FromTable("umbracoDocument").Do(); + + // add new table + Create.Table(true).Do(); + + // migrate the schedule + foreach (KeyValuePair s in releases) { - //Get anything currently scheduled - var releaseSql = new Sql() - .Select("nodeId", "releaseDate") - .From("umbracoDocument") - .Where("releaseDate IS NOT NULL"); - var releases = Database.Dictionary (releaseSql); + DateTime date = s.Value; + var action = ContentScheduleAction.Release.ToString(); - var expireSql = new Sql() - .Select("nodeId", "expireDate") - .From("umbracoDocument") - .Where("expireDate IS NOT NULL"); - var expires = Database.Dictionary(expireSql); + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date, action }) + .Do(); + } + foreach (KeyValuePair s in expires) + { + DateTime date = s.Value; + var action = ContentScheduleAction.Expire.ToString(); - //drop old cols - Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); - Delete.Column("expireDate").FromTable("umbracoDocument").Do(); - //add new table - Create.Table(true).Do(); - - //migrate the schedule - foreach(var s in releases) - { - var date = s.Value; - var action = ContentScheduleAction.Release.ToString(); - - Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) - .Do(); - } - - foreach (var s in expires) - { - var date = s.Value; - var action = ContentScheduleAction.Expire.ToString(); - - Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) - .Do(); - } + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date, action }) + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs index 35c32bddb9..2f2ac746ab 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs @@ -1,21 +1,22 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TagsMigration : MigrationBase { - public class TagsMigration : MigrationBase + public TagsMigration(IMigrationContext context) + : base(context) { - public TagsMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // alter columns => non-null - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "group"); - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "tag"); + protected override void Migrate() + { + // alter columns => non-null + AlterColumn(Constants.DatabaseSchema.Tables.Tag, "group"); + AlterColumn(Constants.DatabaseSchema.Tables.Tag, "tag"); - // kill unused parentId column - Delete.Column("ParentId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Tag).Do(); - } + // kill unused parentId column + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs index 63ffd563a9..eaa745a780 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs @@ -1,16 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class TagsMigrationFix : MigrationBase - { - public TagsMigrationFix(IMigrationContext context) - : base(context) - { } +using Umbraco.Cms.Core; - protected override void Migrate() +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TagsMigrationFix : MigrationBase +{ + public TagsMigrationFix(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // kill unused parentId column, if it still exists + if (ColumnExists(Constants.DatabaseSchema.Tables.Tag, "ParentId")) { - // kill unused parentId column, if it still exists - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "ParentId")) - Delete.Column("ParentId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Tag).Do(); + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs index e3251dc6ed..557f658691 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs @@ -1,48 +1,54 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class UpdateDefaultMandatoryLanguage : MigrationBase { - public class UpdateDefaultMandatoryLanguage : MigrationBase + public UpdateDefaultMandatoryLanguage(IMigrationContext context) + : base(context) { - public UpdateDefaultMandatoryLanguage(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // add the new languages lock object + AddLockObjects.EnsureLockObject(Database, Constants.Locks.Languages, "Languages"); + + // get all existing languages + Sql selectDtos = Sql() + .Select() + .From(); + + List? dtos = Database.Fetch(selectDtos); + + // get the id of the language which is already the default one, if any, + // else get the lowest language id, which will become the default language + var defaultId = int.MaxValue; + foreach (LanguageDto? dto in dtos) { - // add the new languages lock object - AddLockObjects.EnsureLockObject(Database, Cms.Core.Constants.Locks.Languages, "Languages"); - - // get all existing languages - var selectDtos = Sql() - .Select() - .From(); - - var dtos = Database.Fetch(selectDtos); - - // get the id of the language which is already the default one, if any, - // else get the lowest language id, which will become the default language - var defaultId = int.MaxValue; - foreach (var dto in dtos) + if (dto.IsDefault) { - if (dto.IsDefault) - { - defaultId = dto.Id; - break; - } - - if (dto.Id < defaultId) defaultId = dto.Id; + defaultId = dto.Id; + break; } - // update, so that language with that id is now default and mandatory - var updateDefault = Sql() - .Update(u => u - .Set(x => x.IsDefault, true) - .Set(x => x.IsMandatory, true)) - .Where(x => x.Id == defaultId); - - Database.Execute(updateDefault); + if (dto.Id < defaultId) + { + defaultId = dto.Id; + } } + + // update, so that language with that id is now default and mandatory + Sql updateDefault = Sql() + .Update(u => u + .Set(x => x.IsDefault, true) + .Set(x => x.IsMandatory, true)) + .Where(x => x.Id == defaultId); + + Database.Execute(updateDefault); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs index 7fe50b2159..18d55aa1e6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs @@ -1,110 +1,120 @@ -using System; using System.Globalization; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco; using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class UpdatePickerIntegerValuesToUdi : MigrationBase { - public class UpdatePickerIntegerValuesToUdi : MigrationBase + public UpdatePickerIntegerValuesToUdi(IMigrationContext context) + : base(context) { - public UpdatePickerIntegerValuesToUdi(IMigrationContext context) : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + Sql sqlDataTypes = Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.ContentPicker + || x.EditorAlias == Constants.PropertyEditors.Aliases.MediaPicker + || x.EditorAlias == Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + + var dataTypes = Database.Fetch(sqlDataTypes).ToList(); + + foreach (DataTypeDto? datatype in dataTypes.Where(x => !x.Configuration.IsNullOrWhiteSpace())) { - var sqlDataTypes = Sql() - .Select() - .From() - .Where(x => x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - - var dataTypes = Database.Fetch(sqlDataTypes).ToList(); - - foreach (var datatype in dataTypes.Where(x => !x.Configuration.IsNullOrWhiteSpace())) + switch (datatype.EditorAlias) { - switch (datatype.EditorAlias) + case Constants.PropertyEditors.Aliases.ContentPicker: + case Constants.PropertyEditors.Aliases.MediaPicker: { - case Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker: - case Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker: + JObject? config = JsonConvert.DeserializeObject(datatype.Configuration!); + var startNodeId = config!.Value("startNodeId"); + if (!startNodeId.IsNullOrWhiteSpace() && int.TryParse(startNodeId, NumberStyles.Integer, + CultureInfo.InvariantCulture, out var intStartNode)) + { + Guid? guid = intStartNode <= 0 + ? null + : Context.Database.ExecuteScalar( + Sql().Select(x => x.UniqueId).From() + .Where(x => x.NodeId == intStartNode)); + if (guid.HasValue) { - var config = JsonConvert.DeserializeObject(datatype.Configuration!); - var startNodeId = config!.Value("startNodeId"); - if (!startNodeId.IsNullOrWhiteSpace() && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intStartNode)) - { - var guid = intStartNode <= 0 - ? null - : Context.Database.ExecuteScalar( - Sql().Select(x => x.UniqueId).From().Where(x => x.NodeId == intStartNode)); - if (guid.HasValue) - { - var udi = new GuidUdi(datatype.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - ? Cms.Core.Constants.UdiEntityType.Media - : Cms.Core.Constants.UdiEntityType.Document, guid.Value); - config!["startNodeId"] = new JValue(udi.ToString()); - } - else - config!.Remove("startNodeId"); + var udi = new GuidUdi( + datatype.EditorAlias == Constants.PropertyEditors.Aliases.MediaPicker + ? Constants.UdiEntityType.Media + : Constants.UdiEntityType.Document, guid.Value); + config!["startNodeId"] = new JValue(udi.ToString()); + } + else + { + config!.Remove("startNodeId"); + } - datatype.Configuration = JsonConvert.SerializeObject(config); - Database.Update(datatype); + datatype.Configuration = JsonConvert.SerializeObject(config); + Database.Update(datatype); + } + + break; + } + + case Constants.PropertyEditors.Aliases.MultiNodeTreePicker: + { + JObject? config = JsonConvert.DeserializeObject(datatype.Configuration!); + JObject? startNodeConfig = config!.Value("startNode"); + if (startNodeConfig != null) + { + var startNodeId = startNodeConfig.Value("id"); + var objectType = startNodeConfig.Value("type"); + if (!objectType.IsNullOrWhiteSpace() + && !startNodeId.IsNullOrWhiteSpace() + && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var intStartNode)) + { + Guid? guid = intStartNode <= 0 + ? null + : Context.Database.ExecuteScalar( + Sql().Select(x => x.UniqueId).From() + .Where(x => x.NodeId == intStartNode)); + + string? entityType = null; + switch (objectType?.ToLowerInvariant()) + { + case "content": + entityType = Constants.UdiEntityType.Document; + break; + case "media": + entityType = Constants.UdiEntityType.Media; + break; + case "member": + entityType = Constants.UdiEntityType.Member; + break; } - break; - } - case Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker: - { - var config = JsonConvert.DeserializeObject(datatype.Configuration!); - var startNodeConfig = config!.Value("startNode"); - if (startNodeConfig != null) + if (entityType != null && guid.HasValue) { - var startNodeId = startNodeConfig.Value("id"); - var objectType = startNodeConfig.Value("type"); - if (!objectType.IsNullOrWhiteSpace() - && !startNodeId.IsNullOrWhiteSpace() - && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intStartNode)) - { - var guid = intStartNode <= 0 - ? null - : Context.Database.ExecuteScalar( - Sql().Select(x => x.UniqueId).From().Where(x => x.NodeId == intStartNode)); - - string? entityType = null; - switch (objectType?.ToLowerInvariant()) - { - case "content": - entityType = Cms.Core.Constants.UdiEntityType.Document; - break; - case "media": - entityType = Cms.Core.Constants.UdiEntityType.Media; - break; - case "member": - entityType = Cms.Core.Constants.UdiEntityType.Member; - break; - } - - if (entityType != null && guid.HasValue) - { - var udi = new GuidUdi(entityType, guid.Value); - startNodeConfig["id"] = new JValue(udi.ToString()); - } - else - startNodeConfig.Remove("id"); - - datatype.Configuration = JsonConvert.SerializeObject(config); - Database.Update(datatype); - } + var udi = new GuidUdi(entityType, guid.Value); + startNodeConfig["id"] = new JValue(udi.ToString()); + } + else + { + startNodeConfig.Remove("id"); } - break; + datatype.Configuration = JsonConvert.SerializeObject(config); + Database.Update(datatype); } + } + + break; } } - } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs index 03c3529f59..fa19ac284a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs @@ -1,31 +1,41 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +/// +/// Creates/Updates non mandatory FK columns to the user table +/// +public class UserForeignKeys : MigrationBase { - /// - /// Creates/Updates non mandatory FK columns to the user table - /// - public class UserForeignKeys : MigrationBase + public UserForeignKeys(IMigrationContext context) + : base(context) { - public UserForeignKeys(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // first allow NULL-able - Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable().Do(); - Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().Do(); - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().Do(); - Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().Do(); + protected override void Migrate() + { + // first allow NULL-able + Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable() + .Do(); + Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().Do(); + Alter.Table(Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().Do(); + Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().Do(); - // then we can update any non existing users to NULL - Execute.Sql($"UPDATE {ContentVersionCultureVariationDto.TableName} SET availableUserId = NULL WHERE availableUserId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {ContentVersionDto.TableName} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Log} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {NodeDto.TableName} SET nodeUser = NULL WHERE nodeUser NOT IN (SELECT id FROM {UserDto.TableName})").Do(); + // then we can update any non existing users to NULL + Execute.Sql( + $"UPDATE {ContentVersionCultureVariationDto.TableName} SET availableUserId = NULL WHERE availableUserId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {ContentVersionDto.TableName} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Log} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {NodeDto.TableName} SET nodeUser = NULL WHERE nodeUser NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); - // FKs will be created after migrations - } + // FKs will be created after migrations } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index f0fbb63729..db41f70711 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -1,215 +1,266 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class VariantsMigration : MigrationBase { - public class VariantsMigration : MigrationBase + public VariantsMigration(IMigrationContext context) + : base(context) { - public VariantsMigration(IMigrationContext context) - : base(context) - { } + } - // notes - // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead + // notes + // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead + protected override void Migrate() + { + MigratePropertyData(); + CreatePropertyDataIndexes(); + MigrateContentAndPropertyTypes(); + MigrateContent(); + MigrateVersions(); - protected override void Migrate() + if (Database.Fetch( + $@"SELECT {Constants.DatabaseSchema.Tables.ContentVersion}.nodeId, COUNT({Constants.DatabaseSchema.Tables.ContentVersion}.id) +FROM {Constants.DatabaseSchema.Tables.ContentVersion} +JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} ON {Constants.DatabaseSchema.Tables.ContentVersion}.id={Constants.DatabaseSchema.Tables.DocumentVersion}.id +WHERE {Constants.DatabaseSchema.Tables.DocumentVersion}.published=1 +GROUP BY {Constants.DatabaseSchema.Tables.ContentVersion}.nodeId +HAVING COUNT({Constants.DatabaseSchema.Tables.ContentVersion}.id) > 1").Any()) { - MigratePropertyData(); - CreatePropertyDataIndexes(); - MigrateContentAndPropertyTypes(); - MigrateContent(); - MigrateVersions(); - - if (Database.Fetch($@"SELECT {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.nodeId, COUNT({Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id) -FROM {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} ON {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id={Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion}.id -WHERE {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion}.published=1 -GROUP BY {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.nodeId -HAVING COUNT({Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id) > 1").Any()) - { - Debugger.Break(); - throw new Exception("Migration failed: duplicate 'published' document versions."); - } - - if (Database.Fetch($@"SELECT v1.nodeId, v1.id, COUNT(v2.id) -FROM {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} v1 -LEFT JOIN {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} v2 ON v1.nodeId=v2.nodeId AND v2.[current]=1 -GROUP BY v1.nodeId, v1.id -HAVING COUNT(v2.id) <> 1").Any()) - { - Debugger.Break(); - throw new Exception("Migration failed: missing or duplicate 'current' content versions."); - } + Debugger.Break(); + throw new Exception("Migration failed: duplicate 'published' document versions."); } - private void MigratePropertyData() + if (Database.Fetch($@"SELECT v1.nodeId, v1.id, COUNT(v2.id) +FROM {Constants.DatabaseSchema.Tables.ContentVersion} v1 +LEFT JOIN {Constants.DatabaseSchema.Tables.ContentVersion} v2 ON v1.nodeId=v2.nodeId AND v2.[current]=1 +GROUP BY v1.nodeId, v1.id +HAVING COUNT(v2.id) <> 1").Any()) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData)) - return; + Debugger.Break(); + throw new Exception("Migration failed: missing or duplicate 'current' content versions."); + } + } - // add columns - if (!ColumnExists(PreTables.PropertyData, "languageId")) - AddColumn(PreTables.PropertyData, "languageId"); - if (!ColumnExists(PreTables.PropertyData, "segment")) - AddColumn(PreTables.PropertyData, "segment"); + private void MigratePropertyData() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.PropertyData)) + { + return; + } - // rename columns - if (ColumnExists(PreTables.PropertyData, "dataNtext")) - ReplaceColumn(PreTables.PropertyData, "dataNtext", "textValue"); - if (ColumnExists(PreTables.PropertyData, "dataNvarchar")) - ReplaceColumn(PreTables.PropertyData, "dataNvarchar", "varcharValue"); - if (ColumnExists(PreTables.PropertyData, "dataDecimal")) - ReplaceColumn(PreTables.PropertyData, "dataDecimal", "decimalValue"); - if (ColumnExists(PreTables.PropertyData, "dataInt")) - ReplaceColumn(PreTables.PropertyData, "dataInt", "intValue"); - if (ColumnExists(PreTables.PropertyData, "dataDate")) - ReplaceColumn(PreTables.PropertyData, "dataDate", "dateValue"); + // add columns + if (!ColumnExists(PreTables.PropertyData, "languageId")) + { + AddColumn(PreTables.PropertyData, "languageId"); + } - // transform column versionId from guid to integer (contentVersion.id) - if (ColumnType(PreTables.PropertyData, "versionId") == "uniqueidentifier") - { - Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); + if (!ColumnExists(PreTables.PropertyData, "segment")) + { + AddColumn(PreTables.PropertyData, "segment"); + } - Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id + // rename columns + if (ColumnExists(PreTables.PropertyData, "dataNtext")) + { + ReplaceColumn(PreTables.PropertyData, "dataNtext", "textValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataNvarchar")) + { + ReplaceColumn(PreTables.PropertyData, "dataNvarchar", "varcharValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataDecimal")) + { + ReplaceColumn(PreTables.PropertyData, "dataDecimal", "decimalValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataInt")) + { + ReplaceColumn(PreTables.PropertyData, "dataInt", "intValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataDate")) + { + ReplaceColumn(PreTables.PropertyData, "dataDate", "dateValue"); + } + + // transform column versionId from guid to integer (contentVersion.id) + if (ColumnType(PreTables.PropertyData, "versionId") == "uniqueidentifier") + { + Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); + + Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id FROM {PreTables.ContentVersion} INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); - Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); - ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); - } - - // drop column - if (ColumnExists(PreTables.PropertyData, "contentNodeId")) - Delete.Column("contentNodeId").FromTable(PreTables.PropertyData).Do(); - - // rename table - Rename.Table(PreTables.PropertyData).To(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData).Do(); + Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); + ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); } - private void CreatePropertyDataIndexes() + // drop column + if (ColumnExists(PreTables.PropertyData, "contentNodeId")) { - // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. - // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created - var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); - Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); - Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) - .WithOptions().Unique() - .WithOptions().NonClustered() - .OnColumn("versionId").Ascending() - .OnColumn("propertyTypeId").Ascending() - .OnColumn("languageId").Ascending() - .OnColumn("segment").Ascending() - .Do(); + Delete.Column("contentNodeId").FromTable(PreTables.PropertyData).Do(); } - private void MigrateContentAndPropertyTypes() + // rename table + Rename.Table(PreTables.PropertyData).To(Constants.DatabaseSchema.Tables.PropertyData).Do(); + } + + private void CreatePropertyDataIndexes() + { + // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. + // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); + Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); + Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) + .WithOptions().Unique() + .WithOptions().NonClustered() + .OnColumn("versionId").Ascending() + .OnColumn("propertyTypeId").Ascending() + .OnColumn("languageId").Ascending() + .OnColumn("segment").Ascending() + .Do(); + } + + private void MigrateContentAndPropertyTypes() + { + if (!ColumnExists(PreTables.ContentType, "variations")) { - if (!ColumnExists(PreTables.ContentType, "variations")) - AddColumn(PreTables.ContentType, "variations"); - if (!ColumnExists(PreTables.PropertyType, "variations")) - AddColumn(PreTables.PropertyType, "variations"); + AddColumn(PreTables.ContentType, "variations"); } - private void MigrateContent() + if (!ColumnExists(PreTables.PropertyType, "variations")) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.Content)) - return; + AddColumn(PreTables.PropertyType, "variations"); + } + } - // rename columns - if (ColumnExists(PreTables.Content, "contentType")) - ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); - - // drop columns - if (ColumnExists(PreTables.Content, "pk")) - Delete.Column("pk").FromTable(PreTables.Content).Do(); - - // rename table - Rename.Table(PreTables.Content).To(Cms.Core.Constants.DatabaseSchema.Tables.Content).Do(); + private void MigrateContent() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.Content)) + { + return; } - private void MigrateVersions() + // rename columns + if (ColumnExists(PreTables.Content, "contentType")) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion)) - return; + ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); + } - // if the table already exists, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)) - return; + // drop columns + if (ColumnExists(PreTables.Content, "pk")) + { + Delete.Column("pk").FromTable(PreTables.Content).Do(); + } - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.Document)) - return; + // rename table + Rename.Table(PreTables.Content).To(Constants.DatabaseSchema.Tables.Content).Do(); + } - // do it all at once + private void MigrateVersions() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.ContentVersion)) + { + return; + } - // add contentVersion columns - if (!ColumnExists(PreTables.ContentVersion, "text")) - AddColumn(PreTables.ContentVersion, "text"); - if (!ColumnExists(PreTables.ContentVersion, "current")) + // if the table already exists, we're done + if (TableExists(Constants.DatabaseSchema.Tables.DocumentVersion)) + { + return; + } + + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.Document)) + { + return; + } + + // do it all at once + + // add contentVersion columns + if (!ColumnExists(PreTables.ContentVersion, "text")) + { + AddColumn(PreTables.ContentVersion, "text"); + } + + if (!ColumnExists(PreTables.ContentVersion, "current")) + { + AddColumn(PreTables.ContentVersion, "current", out IEnumerable sqls); + Database.Execute( + $@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0"); + foreach (var sql in sqls) { - AddColumn(PreTables.ContentVersion, "current", out var sqls); - Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } - if (!ColumnExists(PreTables.ContentVersion, "userId")) + } + + if (!ColumnExists(PreTables.ContentVersion, "userId")) + { + AddColumn(PreTables.ContentVersion, "userId", out IEnumerable sqls); + Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET userId=0"); + foreach (var sql in sqls) { - AddColumn(PreTables.ContentVersion, "userId", out var sqls); - Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET userId=0"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } + } - // rename contentVersion contentId column - if (ColumnExists(PreTables.ContentVersion, "ContentId")) - ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); + // rename contentVersion contentId column + if (ColumnExists(PreTables.ContentVersion, "ContentId")) + { + ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); + } - // populate contentVersion text, current and userId columns for documents - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser + // populate contentVersion text, current and userId columns for documents + Database.Execute( + $@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser FROM {PreTables.ContentVersion} v INNER JOIN {PreTables.Document} d ON d.versionId = v.versionId"); - - // populate contentVersion text and current columns for non-documents, userId is default - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 + // populate contentVersion text and current columns for non-documents, userId is default + Database.Execute( + $@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 FROM {PreTables.ContentVersion} cver -JOIN {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id +JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); + // create table + Create.Table(true).Do(); - // create table - Create.Table(withoutKeysAndIndexes: true).Do(); - - // every document row becomes a document version - Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) + // every document row becomes a document version + Database.Execute( + $@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) SELECT cver.id, doc.templateId, doc.published FROM {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId"); - - // need to add extra rows for where published=newest - // 'cos INSERT above has inserted the 'published' document version - // and v8 always has a 'edited' document version too - Database.Execute($@" + // need to add extra rows for where published=newest + // 'cos INSERT above has inserted the 'published' document version + // and v8 always has a 'edited' document version too + Database.Execute($@" INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) SELECT doc.nodeId, NEWID(), doc.updateDate, doc.documentUser, 1, doc.text FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId WHERE doc.newest=1 AND doc.published=1"); - Database.Execute($@" -INSERT INTO {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) SELECT cverNew.id, doc.templateId, 0 FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); - Database.Execute($@" + Database.Execute($@" INSERT INTO {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} (propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,versionId) SELECT propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,cverNew.id FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc @@ -218,127 +269,137 @@ JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nod JOIN {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} pd ON pd.versionId=cver.id WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); - - // reduce document to 1 row per content - Database.Execute($@"DELETE FROM {PreTables.Document} + // reduce document to 1 row per content + Database.Execute($@"DELETE FROM {PreTables.Document} WHERE versionId NOT IN (SELECT (versionId) FROM {PreTables.ContentVersion} WHERE {SqlSyntax.GetQuotedColumnName("current")} = 1) AND (published<>1 OR newest<>1)"); - // ensure that documents with a published version are marked as published - Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( -SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); + // ensure that documents with a published version are marked as published + Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( +SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); - // drop some document columns - Delete.Column("text").FromTable(PreTables.Document).Do(); - Delete.Column("templateId").FromTable(PreTables.Document).Do(); - Delete.Column("documentUser").FromTable(PreTables.Document).Do(); - Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("updateDate").Do(); - Delete.Column("updateDate").FromTable(PreTables.Document).Do(); - Delete.Column("versionId").FromTable(PreTables.Document).Do(); - Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("newest").Do(); - Delete.Column("newest").FromTable(PreTables.Document).Do(); + // drop some document columns + Delete.Column("text").FromTable(PreTables.Document).Do(); + Delete.Column("templateId").FromTable(PreTables.Document).Do(); + Delete.Column("documentUser").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("updateDate").Do(); + Delete.Column("updateDate").FromTable(PreTables.Document).Do(); + Delete.Column("versionId").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("newest").Do(); + Delete.Column("newest").FromTable(PreTables.Document).Do(); - // add and populate edited column - if (!ColumnExists(PreTables.Document, "edited")) + // add and populate edited column + if (!ColumnExists(PreTables.Document, "edited")) + { + AddColumn(PreTables.Document, "edited", out IEnumerable sqls); + Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); + foreach (var sql in sqls) { - AddColumn(PreTables.Document, "edited", out var sqls); - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } + } - // set 'edited' to true whenever a 'non-published' property data is != a published one - // cannot compare NTEXT values in TSQL - // cannot cast NTEXT to NVARCHAR(MAX) in SQLCE - // ... bah ... - var temp = Database.Fetch($@"SELECT n.id, + // set 'edited' to true whenever a 'non-published' property data is != a published one + // cannot compare NTEXT values in TSQL + // cannot cast NTEXT to NVARCHAR(MAX) in SQLCE + // ... bah ... + List? temp = Database.Fetch($@"SELECT n.id, v1.intValue intValue1, v1.decimalValue decimalValue1, v1.dateValue dateValue1, v1.varcharValue varcharValue1, v1.textValue textValue1, v2.intValue intValue2, v2.decimalValue decimalValue2, v2.dateValue dateValue2, v2.varcharValue varcharValue2, v2.textValue textValue2 -FROM {Cms.Core.Constants.DatabaseSchema.Tables.Node} n +FROM {Constants.DatabaseSchema.Tables.Node} n JOIN {PreTables.ContentVersion} cv1 ON n.id=cv1.nodeId AND cv1.{SqlSyntax.GetQuotedColumnName("current")}=1 -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId JOIN {PreTables.ContentVersion} cv2 ON n.id=cv2.nodeId -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId +JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId WHERE v1.propertyTypeId=v2.propertyTypeId AND (v1.languageId=v2.languageId OR (v1.languageId IS NULL AND v2.languageId IS NULL)) AND (v1.segment=v2.segment OR (v1.segment IS NULL AND v2.segment IS NULL))"); - var updatedIds = new HashSet(); - foreach (var t in temp) - if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) - if (updatedIds.Add((int)t.id)) - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", new { nodeId = t.id }); - - // drop more columns - Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); - - // rename tables - Rename.Table(PreTables.ContentVersion).To(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion).Do(); - Rename.Table(PreTables.Document).To(Cms.Core.Constants.DatabaseSchema.Tables.Document).Do(); - } - - private static class PreTables + var updatedIds = new HashSet(); + foreach (dynamic t in temp) { - // ReSharper disable UnusedMember.Local - public const string Lock = "umbracoLock"; - public const string Log = "umbracoLog"; - - public const string Node = "umbracoNode"; - public const string NodeData = "cmsContentNu"; - public const string NodeXml = "cmsContentXml"; - public const string NodePreviewXml = "cmsPreviewXml"; - - public const string ContentType = "cmsContentType"; - public const string ContentChildType = "cmsContentTypeAllowedContentType"; - public const string DocumentType = "cmsDocumentType"; - public const string ElementTypeTree = "cmsContentType2ContentType"; - public const string DataType = "cmsDataType"; - public const string DataTypePreValue = "cmsDataTypePreValues"; - public const string Template = "cmsTemplate"; - - public const string Content = "cmsContent"; - public const string ContentVersion = "cmsContentVersion"; - public const string Document = "cmsDocument"; - - public const string PropertyType = "cmsPropertyType"; - public const string PropertyTypeGroup = "cmsPropertyTypeGroup"; - public const string PropertyData = "cmsPropertyData"; - - public const string RelationType = "umbracoRelationType"; - public const string Relation = "umbracoRelation"; - - public const string Domain = "umbracoDomains"; - public const string Language = "umbracoLanguage"; - public const string DictionaryEntry = "cmsDictionary"; - public const string DictionaryValue = "cmsLanguageText"; - - public const string User = "umbracoUser"; - public const string UserGroup = "umbracoUserGroup"; - public const string UserStartNode = "umbracoUserStartNode"; - public const string User2UserGroup = "umbracoUser2UserGroup"; - public const string User2NodeNotify = "umbracoUser2NodeNotify"; - public const string UserGroup2App = "umbracoUserGroup2App"; - public const string UserGroup2NodePermission = "umbracoUserGroup2NodePermission"; - public const string ExternalLogin = "umbracoExternalLogin"; - - public const string Macro = "cmsMacro"; - public const string MacroProperty = "cmsMacroProperty"; - - public const string Member = "cmsMember"; - public const string MemberType = "cmsMemberType"; - public const string Member2MemberGroup = "cmsMember2MemberGroup"; - - public const string Access = "umbracoAccess"; - public const string AccessRule = "umbracoAccessRule"; - public const string RedirectUrl = "umbracoRedirectUrl"; - - public const string CacheInstruction = "umbracoCacheInstruction"; - public const string Migration = "umbracoMigration"; - public const string Server = "umbracoServer"; - - public const string Tag = "cmsTags"; - public const string TagRelationship = "cmsTagRelationship"; - - // ReSharper restore UnusedMember.Local + if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || + t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) + { + if (updatedIds.Add((int)t.id)) + { + Database.Execute( + $"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", + new { nodeId = t.id }); + } + } } + + // drop more columns + Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); + + // rename tables + Rename.Table(PreTables.ContentVersion).To(Constants.DatabaseSchema.Tables.ContentVersion).Do(); + Rename.Table(PreTables.Document).To(Constants.DatabaseSchema.Tables.Document).Do(); + } + + private static class PreTables + { + // ReSharper disable UnusedMember.Local + public const string Lock = "umbracoLock"; + public const string Log = "umbracoLog"; + + public const string Node = "umbracoNode"; + public const string NodeData = "cmsContentNu"; + public const string NodeXml = "cmsContentXml"; + public const string NodePreviewXml = "cmsPreviewXml"; + + public const string ContentType = "cmsContentType"; + public const string ContentChildType = "cmsContentTypeAllowedContentType"; + public const string DocumentType = "cmsDocumentType"; + public const string ElementTypeTree = "cmsContentType2ContentType"; + public const string DataType = "cmsDataType"; + public const string DataTypePreValue = "cmsDataTypePreValues"; + public const string Template = "cmsTemplate"; + + public const string Content = "cmsContent"; + public const string ContentVersion = "cmsContentVersion"; + public const string Document = "cmsDocument"; + + public const string PropertyType = "cmsPropertyType"; + public const string PropertyTypeGroup = "cmsPropertyTypeGroup"; + public const string PropertyData = "cmsPropertyData"; + + public const string RelationType = "umbracoRelationType"; + public const string Relation = "umbracoRelation"; + + public const string Domain = "umbracoDomains"; + public const string Language = "umbracoLanguage"; + public const string DictionaryEntry = "cmsDictionary"; + public const string DictionaryValue = "cmsLanguageText"; + + public const string User = "umbracoUser"; + public const string UserGroup = "umbracoUserGroup"; + public const string UserStartNode = "umbracoUserStartNode"; + public const string User2UserGroup = "umbracoUser2UserGroup"; + public const string User2NodeNotify = "umbracoUser2NodeNotify"; + public const string UserGroup2App = "umbracoUserGroup2App"; + public const string UserGroup2NodePermission = "umbracoUserGroup2NodePermission"; + public const string ExternalLogin = "umbracoExternalLogin"; + + public const string Macro = "cmsMacro"; + public const string MacroProperty = "cmsMacroProperty"; + + public const string Member = "cmsMember"; + public const string MemberType = "cmsMemberType"; + public const string Member2MemberGroup = "cmsMember2MemberGroup"; + + public const string Access = "umbracoAccess"; + public const string AccessRule = "umbracoAccessRule"; + public const string RedirectUrl = "umbracoRedirectUrl"; + + public const string CacheInstruction = "umbracoCacheInstruction"; + public const string Migration = "umbracoMigration"; + public const string Server = "umbracoServer"; + + public const string Tag = "cmsTags"; + public const string TagRelationship = "cmsTagRelationship"; + + // ReSharper restore UnusedMember.Local } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs index 74445d268d..60e38eca29 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs @@ -1,16 +1,16 @@ -using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; +using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1; + +public class ChangeNuCacheJsonFormat : MigrationBase { - public class ChangeNuCacheJsonFormat : MigrationBase + public ChangeNuCacheJsonFormat(IMigrationContext context) + : base(context) { - public ChangeNuCacheJsonFormat(IMigrationContext context) : base(context) - { } - - protected override void Migrate() - { - // nothing - just adding the post-migration - Context.AddPostMigration(); - } } + + protected override void Migrate() => + + // nothing - just adding the post-migration + Context.AddPostMigration(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs index 6c6fd6166c..4e57716c4e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs @@ -1,20 +1,18 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0; + +public class AddPropertyTypeLabelOnTopColumn : MigrationBase { - - public class AddPropertyTypeLabelOnTopColumn : MigrationBase + public AddPropertyTypeLabelOnTopColumn(IMigrationContext context) + : base(context) { - public AddPropertyTypeLabelOnTopColumn(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "labelOnTop"); - } + AddColumnIfNotExists(columns, "labelOnTop"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index 23bb979dd9..4a9a494b76 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -1,47 +1,43 @@ using NPoco; -using System.Linq; using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; + +public class AddCmsContentNuByteColumn : MigrationBase { - public class AddCmsContentNuByteColumn : MigrationBase + private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + + public AddCmsContentNuByteColumn(IMigrationContext context) + : base(context) { - public AddCmsContentNuByteColumn(IMigrationContext context) - : base(context) - { + } - } + protected override void Migrate() + { + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); - protected override void Migrate() - { - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + AddColumnIfNotExists(columns, "dataRaw"); + } - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "dataRaw"); - } + [TableName(TempTableName)] + [ExplicitColumns] + private class ContentNuDtoTemp + { + [Column("nodeId")] + public int NodeId { get; set; } - private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + [Column("published")] + public bool Published { get; set; } - [TableName(TempTableName)] - [ExplicitColumns] - private class ContentNuDtoTemp - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Data { get; set; } - [Column("published")] - public bool Published { get; set; } - - [Column("data")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Data { get; set; } - - [Column("rv")] - public long Rv { get; set; } - } + [Column("rv")] + public long Rv { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs index 496e12a1fa..703cfc1474 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs @@ -1,16 +1,14 @@ -using Umbraco.Cms.Infrastructure.Persistence; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +public class UpdateCmsPropertyGroupIdSeed : MigrationBase { - public class UpdateCmsPropertyGroupIdSeed : MigrationBase + public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) + : base(context) { - public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() - { - // NOOP - was sql ce only - } + protected override void Migrate() + { + // NOOP - was sql ce only } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs index 9bdce9bfbf..114bb7becc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs @@ -1,66 +1,73 @@ -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; + +public class UpgradedIncludeIndexes : MigrationBase { - public class UpgradedIncludeIndexes : MigrationBase + public UpgradedIncludeIndexes(IMigrationContext context) + : base(context) { - public UpgradedIncludeIndexes(IMigrationContext context) - : base(context) + } + + protected override void Migrate() + { + // Need to drop the FK for the redirect table before modifying the unique id index + Delete.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); + var nodeDtoIndexes = new[] { + $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType", + $"IX_{NodeDto.TableName}_Level", + }; + DeleteIndexes(nodeDtoIndexes); // delete existing ones + CreateIndexes(nodeDtoIndexes); // update/add - } + // Now re-create the FK for the redirect table + Create.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); - protected override void Migrate() + var contentVersionIndexes = new[] { - // Need to drop the FK for the redirect table before modifying the unique id index - Delete.ForeignKey() - .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) - .ForeignColumn("contentKey") - .ToTable(NodeDto.TableName) - .PrimaryColumn("uniqueID") - .Do(); - var nodeDtoIndexes = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType", $"IX_{NodeDto.TableName}_Level" }; - DeleteIndexes(nodeDtoIndexes); // delete existing ones - CreateIndexes(nodeDtoIndexes); // update/add - // Now re-create the FK for the redirect table - Create.ForeignKey() - .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) - .ForeignColumn("contentKey") - .ToTable(NodeDto.TableName) - .PrimaryColumn("uniqueID") - .Do(); + $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current", + }; + DeleteIndexes(contentVersionIndexes); // delete existing ones + CreateIndexes(contentVersionIndexes); // update/add + } + private void DeleteIndexes(params string[] toDelete) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - var contentVersionIndexes = new[] { $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current" }; - DeleteIndexes(contentVersionIndexes); // delete existing ones - CreateIndexes(contentVersionIndexes); // update/add - } - - private void DeleteIndexes(params string[] toDelete) + foreach (var i in toDelete) { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - - foreach (var i in toDelete) - if (IndexExists(i)) - Delete.Index(i).OnTable(tableDef.Name).Do(); - - } - - private void CreateIndexes(params string[] toCreate) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - - foreach (var c in toCreate) + if (IndexExists(i)) { - // get the definition by name - var index = tableDef.Indexes.First(x => x.Name == c); - new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); + Delete.Index(i).OnTable(tableDef.Name).Do(); } + } + } + private void CreateIndexes(params string[] toCreate) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach (var c in toCreate) + { + // get the definition by name + IndexDefinition index = tableDef.Indexes.First(x => x.Name == c); + new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) } + .Execute(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs index feedc56d9a..cef69d6bd3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs @@ -1,65 +1,70 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0; + +public class AddPropertyTypeGroupColumns : MigrationBase { - public class AddPropertyTypeGroupColumns : MigrationBase + private readonly IShortStringHelper _shortStringHelper; + + public AddPropertyTypeGroupColumns(IMigrationContext context, IShortStringHelper shortStringHelper) + : base(context) => _shortStringHelper = shortStringHelper; + + internal IEnumerable PopulateAliases(IEnumerable dtos) { - private readonly IShortStringHelper _shortStringHelper; - - public AddPropertyTypeGroupColumns(IMigrationContext context, IShortStringHelper shortStringHelper) - : base(context) => _shortStringHelper = shortStringHelper; - - protected override void Migrate() + foreach (IGrouping dtosPerAlias in dtos.GroupBy(x => + x.Text?.ToSafeAlias(_shortStringHelper, true))) { - AddColumn("type"); - - // Add column without constraints - AddColumn("alias", out var sqls); - - // Populate non-null alias column - var dtos = Database.Fetch(); - foreach (var dto in PopulateAliases(dtos)) - Database.Update(dto, x => new { x.Alias }); - - // Finally add the constraints - foreach (var sql in sqls) - Database.Execute(sql); - } - - internal IEnumerable PopulateAliases(IEnumerable dtos) - { - foreach (var dtosPerAlias in dtos.GroupBy(x => x.Text?.ToSafeAlias(_shortStringHelper, true))) + IEnumerable> dtosPerAliasAndText = + dtosPerAlias.GroupBy(x => x.Text); + var numberSuffix = 1; + foreach (IGrouping dtosPerText in dtosPerAliasAndText) { - var dtosPerAliasAndText = dtosPerAlias.GroupBy(x => x.Text); - var numberSuffix = 1; - foreach (var dtosPerText in dtosPerAliasAndText) + foreach (PropertyTypeGroupDto dto in dtosPerText) { - foreach (var dto in dtosPerText) + dto.Alias = dtosPerAlias.Key ?? string.Empty; + + if (numberSuffix > 1) { - dto.Alias = dtosPerAlias.Key ?? string.Empty; - - if (numberSuffix > 1) - { - // More than 1 name found for the alias, so add a suffix - dto.Alias += numberSuffix; - } - - yield return dto; + // More than 1 name found for the alias, so add a suffix + dto.Alias += numberSuffix; } - numberSuffix++; + yield return dto; } - if (numberSuffix > 2) - { - Logger.LogError("Detected the same alias {Alias} for different property group names {Names}, the migration added suffixes, but this might break backwards compatibility.", dtosPerAlias.Key, dtosPerAliasAndText.Select(x => x.Key)); - } + numberSuffix++; + } + + if (numberSuffix > 2) + { + Logger.LogError( + "Detected the same alias {Alias} for different property group names {Names}, the migration added suffixes, but this might break backwards compatibility.", + dtosPerAlias.Key, dtosPerAliasAndText.Select(x => x.Key)); } } } + + protected override void Migrate() + { + AddColumn("type"); + + // Add column without constraints + AddColumn("alias", out IEnumerable sqls); + + // Populate non-null alias column + List? dtos = Database.Fetch(); + foreach (PropertyTypeGroupDto dto in PopulateAliases(dtos)) + { + Database.Update(dto, x => new { x.Alias }); + } + + // Finally add the constraints + foreach (var sql in sqls) + { + Database.Execute(sql); + } + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index 96d60a30e5..0f7fe97663 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -1,127 +1,132 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class ConvertTinyMceAndGridMediaUrlsToLocalLink : MigrationBase { - public class ConvertTinyMceAndGridMediaUrlsToLocalLink : MigrationBase + private readonly IMediaService _mediaService; + + public ConvertTinyMceAndGridMediaUrlsToLocalLink(IMigrationContext context, IMediaService mediaService) + : base(context) => _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); + + protected override void Migrate() { - private readonly IMediaService _mediaService; + var mediaLinkPattern = new Regex( + @"(]*href="")(\/ media[^""\?]*)([^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - public ConvertTinyMceAndGridMediaUrlsToLocalLink(IMigrationContext context, IMediaService mediaService) : base(context) + Sql sqlPropertyData = Sql() + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x!.DataTypeDto))) + .From() + .InnerJoin() + .On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin() + .On((left, right) => left.DataTypeId == right.NodeId) + .Where(x => + x.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce || + x.EditorAlias == Constants.PropertyEditors.Aliases.Grid); + + List? properties = Database.Fetch(sqlPropertyData); + + var exceptions = new List(); + foreach (PropertyDataDto80? property in properties) { - _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - } - - protected override void Migrate() - { - var mediaLinkPattern = new Regex( - @"(]*href="")(\/ media[^""\?]*)([^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - var sqlPropertyData = Sql() - .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x!.DataTypeDto))) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) - .Where(x => - x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.TinyMce || - x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Grid); - - var properties = Database.Fetch(sqlPropertyData); - - var exceptions = new List(); - foreach (var property in properties) + var value = property.TextValue; + if (string.IsNullOrWhiteSpace(value)) { - var value = property.TextValue; - if (string.IsNullOrWhiteSpace(value)) continue; + continue; + } - - bool propertyChanged = false; - if (property.PropertyTypeDto?.DataTypeDto?.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Grid) + var propertyChanged = false; + if (property.PropertyTypeDto?.DataTypeDto?.EditorAlias == Constants.PropertyEditors.Aliases.Grid) + { + try { - try - { - var obj = JsonConvert.DeserializeObject(value); - var allControls = obj?.SelectTokens("$.sections..rows..areas..controls"); + JObject? obj = JsonConvert.DeserializeObject(value); + IEnumerable? allControls = obj?.SelectTokens("$.sections..rows..areas..controls"); - if (allControls is not null) + if (allControls is not null) + { + foreach (JObject control in allControls.SelectMany(c => c).OfType()) { - foreach (var control in allControls.SelectMany(c => c).OfType()) + JToken? controlValue = control["value"]; + if (controlValue?.Type == JTokenType.String) { - var controlValue = control["value"]; - if (controlValue?.Type == JTokenType.String) - { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()!, out var controlChanged); - propertyChanged |= controlChanged; - } + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()!, + out var controlChanged); + propertyChanged |= controlChanged; } } - - property.TextValue = JsonConvert.SerializeObject(obj); - } - catch (JsonException e) - { - exceptions.Add(new InvalidOperationException( - "Cannot deserialize the value as json. This can be because the property editor " + - "type is changed from another type into a grid. Old versions of the value in this " + - "property can have the structure from the old property editor type. This needs to be " + - "changed manually before updating the database.\n" + - $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" - , e)); - continue; } + property.TextValue = JsonConvert.SerializeObject(obj); } - else + catch (JsonException e) { - property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); + exceptions.Add(new InvalidOperationException( + "Cannot deserialize the value as json. This can be because the property editor " + + "type is changed from another type into a grid. Old versions of the value in this " + + "property can have the structure from the old property editor type. This needs to be " + + "changed manually before updating the database.\n" + + $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}", + e)); + continue; } - - if (propertyChanged) - Database.Update(property); } - - - if (exceptions.Any()) + else { - throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions); + property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); } - Context.AddPostMigration(); + if (propertyChanged) + { + Database.Update(property); + } } - private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) + if (exceptions.Any()) { - bool matched = false; - - var result = mediaLinkPattern.Replace(value, match => - { - matched = true; - - // match groups: - // - 1 = from the beginning of the a tag until href attribute value begins - // - 2 = the href attribute value excluding the querystring (if present) - // - 3 = anything after group 2 until the a tag is closed - var href = match.Groups[2].Value; - - var media = _mediaService.GetMediaByPath(href); - return media == null - ? match.Value - : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; - }); - - changed = matched; - - return result; + throw new AggregateException( + "One or more errors related to unexpected data in grid values occurred.", + exceptions); } + + Context.AddPostMigration(); + } + + private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) + { + var matched = false; + + var result = mediaLinkPattern.Replace(value, match => + { + matched = true; + + // match groups: + // - 1 = from the beginning of the a tag until href attribute value begins + // - 2 = the href attribute value excluding the querystring (if present) + // - 3 = anything after group 2 until the a tag is closed + var href = match.Groups[2].Value; + + IMedia? media = _mediaService.GetMediaByPath(href); + return media == null + ? match.Value + : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; + }); + + changed = matched; + + return result; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs index 00389c547e..abb4fbfea8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs @@ -1,17 +1,17 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class FixContentNuCascade : MigrationBase { - public class FixContentNuCascade : MigrationBase + public FixContentNuCascade(IMigrationContext context) + : base(context) { - public FixContentNuCascade(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Delete.KeysAndIndexes().Do(); - Create.KeysAndIndexes().Do(); - } + protected override void Migrate() + { + Delete.KeysAndIndexes().Do(); + Create.KeysAndIndexes().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs index f06477579a..ac2e27b2d6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs @@ -1,36 +1,39 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class RenameUserLoginDtoDateIndex : MigrationBase { - public class RenameUserLoginDtoDateIndex : MigrationBase + public RenameUserLoginDtoDateIndex(IMigrationContext context) + : base(context) { - public RenameUserLoginDtoDateIndex(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // there has been some confusion with an index name, resulting in + // different names depending on which migration path was followed, + // and discrepancies between an upgraded or an installed database. + // better normalize + if (IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) { - // there has been some confusion with an index name, resulting in - // different names depending on which migration path was followed, - // and discrepancies between an upgraded or an installed database. - // better normalize + return; + } - if (IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) - return; - - if (IndexExists("IX_userLoginDto_lastValidatedUtc")) - Delete - .Index("IX_userLoginDto_lastValidatedUtc") - .OnTable(UserLoginDto.TableName) - .Do(); - - Create - .Index("IX_umbracoUserLogin_lastValidatedUtc") + if (IndexExists("IX_userLoginDto_lastValidatedUtc")) + { + Delete + .Index("IX_userLoginDto_lastValidatedUtc") .OnTable(UserLoginDto.TableName) - .OnColumn("lastValidatedUtc") - .Ascending() - .WithOptions().NonClustered() .Do(); } + + Create + .Index("IX_umbracoUserLogin_lastValidatedUtc") + .OnTable(UserLoginDto.TableName) + .OnColumn("lastValidatedUtc") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs index 9fe257fafe..bd76857ab7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs @@ -1,16 +1,15 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class AddMainDomLock : MigrationBase { - public class AddMainDomLock : MigrationBase + public AddMainDomLock(IMigrationContext context) + : base(context) { - public AddMainDomLock(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" }); - } } + + protected override void Migrate() => Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs index 9c770adf15..c2a447e778 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs @@ -1,33 +1,38 @@ -using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +/// +/// Ensures the new relation types are created +/// +public class AddNewRelationTypes : MigrationBase { - /// - /// Ensures the new relation types are created - /// - public class AddNewRelationTypes : MigrationBase + public AddNewRelationTypes(IMigrationContext context) + : base(context) { - public AddNewRelationTypes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - CreateRelation( - Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, - Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName); + protected override void Migrate() + { + CreateRelation( + Constants.Conventions.RelationTypes.RelatedMediaAlias, + Constants.Conventions.RelationTypes.RelatedMediaName); - CreateRelation( - Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, - Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName); - } + CreateRelation( + Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedDocumentName); + } - private void CreateRelation(string alias, string name) - { - var uniqueId = DatabaseDataCreator.CreateUniqueRelationTypeId(alias ,name); //this is the same as how it installs so everything is consistent - Insert.IntoTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias }) - .Do(); - } + private void CreateRelation(string alias, string name) + { + Guid uniqueId = + DatabaseDataCreator + .CreateUniqueRelationTypeId( + alias, + name); // this is the same as how it installs so everything is consistent + Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType) + .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias }) + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs index 9a9e2b5e77..a5aea97fc2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs @@ -1,21 +1,19 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class AddPropertyTypeValidationMessageColumns : MigrationBase { - - public class AddPropertyTypeValidationMessageColumns : MigrationBase + public AddPropertyTypeValidationMessageColumns(IMigrationContext context) + : base(context) { - public AddPropertyTypeValidationMessageColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "mandatoryMessage"); - AddColumnIfNotExists(columns, "validationRegExpMessage"); - } + AddColumnIfNotExists(columns, "mandatoryMessage"); + AddColumnIfNotExists(columns, "validationRegExpMessage"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs index 2d4b227249..7e7b659401 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs @@ -1,33 +1,31 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class MissingContentVersionsIndexes : MigrationBase { - public class MissingContentVersionsIndexes : MigrationBase + private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + + public MissingContentVersionsIndexes(IMigrationContext context) + : base(context) { - private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + } - public MissingContentVersionsIndexes(IMigrationContext context) : base(context) + protected override void Migrate() + { + // We must check before we create an index because if we are upgrading from v7 we force re-create all + // indexes in the whole DB and then this would throw + if (!IndexExists(IndexName)) { - } - - protected override void Migrate() - { - // We must check before we create an index because if we are upgrading from v7 we force re-create all - // indexes in the whole DB and then this would throw - - if (!IndexExists(IndexName)) - { - Create - .Index(IndexName) - .OnTable(ContentVersionDto.TableName) - .OnColumn("nodeId") - .Ascending() - .OnColumn("current") - .Ascending() - .WithOptions().NonClustered() - .Do(); - } - + Create + .Index(IndexName) + .OnTable(ContentVersionDto.TableName) + .OnColumn("nodeId") + .Ascending() + .OnColumn("current") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs index bc3757eaad..032359cfcc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs @@ -1,36 +1,42 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class UpdateRelationTypeTable : MigrationBase { - - public class UpdateRelationTypeTable : MigrationBase + public UpdateRelationTypeTable(IMigrationContext context) + : base(context) { - public UpdateRelationTypeTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable() + .Do(); + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable() + .Do(); + + // TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable + + // drop index before we can alter the column + if (IndexExists("IX_umbracoRelationType_alias")) { - - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable().Do(); - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable().Do(); - - //TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable - - //drop index before we can alter the column - if (IndexExists("IX_umbracoRelationType_alias")) - Delete - .Index("IX_umbracoRelationType_alias") - .OnTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .Do(); - //change the column to non nullable - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do(); - //re-create the index - Create + Delete .Index("IX_umbracoRelationType_alias") - .OnTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .OnColumn("alias") - .Ascending() - .WithOptions().Unique().WithOptions().NonClustered() + .OnTable(Constants.DatabaseSchema.Tables.RelationType) .Do(); } + + // change the column to non nullable + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do(); + + // re-create the index + Create + .Index("IX_umbracoRelationType_alias") + .OnTable(Constants.DatabaseSchema.Tables.RelationType) + .OnColumn("alias") + .Ascending() + .WithOptions().Unique().WithOptions().NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs index 69e4a7423c..1ab43c2eb7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs @@ -1,33 +1,31 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0; + +public class MissingDictionaryIndex : MigrationBase { - public class MissingDictionaryIndex : MigrationBase + public MissingDictionaryIndex(IMigrationContext context) + : base(context) { - public MissingDictionaryIndex(IMigrationContext context) - : base(context) + } + + /// + /// Adds an index to the foreign key column parent on DictionaryDto's table + /// if it doesn't already exist + /// + protected override void Migrate() + { + var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; + + if (!IndexExists(indexName)) { - - } - - /// - /// Adds an index to the foreign key column parent on DictionaryDto's table - /// if it doesn't already exist - /// - protected override void Migrate() - { - var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; - - if (!IndexExists(indexName)) - { - Create - .Index(indexName) - .OnTable(DictionaryDto.TableName) - .OnColumn("parent") - .Ascending() - .WithOptions().NonClustered() - .Do(); - } + Create + .Index(indexName) + .OnTable(DictionaryDto.TableName) + .OnColumn("parent") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs index 7f75bde572..bf21b6b928 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs @@ -1,20 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0; + +public class ExternalLoginTableUserData : MigrationBase { - public class ExternalLoginTableUserData : MigrationBase + public ExternalLoginTableUserData(IMigrationContext context) + : base(context) { - public ExternalLoginTableUserData(IMigrationContext context) - : base(context) - { - } - - /// - /// Adds new column to the External Login table - /// - protected override void Migrate() - { - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin, "userData"); - } } + + /// + /// Adds new column to the External Login table + /// + protected override void Migrate() => + AddColumn(Constants.DatabaseSchema.Tables.ExternalLogin, "userData"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs index 01ea1cf3b3..2bf01c55bb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs @@ -1,23 +1,21 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class AddPasswordConfigToMemberTable : MigrationBase { - public class AddPasswordConfigToMemberTable : MigrationBase + public AddPasswordConfigToMemberTable(IMigrationContext context) + : base(context) { - public AddPasswordConfigToMemberTable(IMigrationContext context) - : base(context) - { - } + } - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + /// + /// Adds new columns to members table + /// + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "passwordConfig"); - } + AddColumnIfNotExists(columns, "passwordConfig"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs index 44034c5e45..3556213cd1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs @@ -1,129 +1,133 @@ -using System.Linq; using Microsoft.Extensions.Logging; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class DictionaryTablesIndexes : MigrationBase { - public class DictionaryTablesIndexes : MigrationBase + private const string IndexedDictionaryColumn = "key"; + private const string IndexedLanguageTextColumn = "languageId"; + + public DictionaryTablesIndexes(IMigrationContext context) + : base(context) { - private const string IndexedDictionaryColumn = "key"; - private const string IndexedLanguageTextColumn = "languageId"; - - public DictionaryTablesIndexes(IMigrationContext context) - : base(context) - { - } - - protected override void Migrate() - { - var indexDictionaryDto = $"IX_{DictionaryDto.TableName}_{IndexedDictionaryColumn}"; - var indexLanguageTextDto = $"IX_{LanguageTextDto.TableName}_{IndexedLanguageTextColumn}"; - var dictionaryColumnsToBeIndexed = new[] { IndexedDictionaryColumn }; - var langTextColumnsToBeIndexed = new[] { IndexedLanguageTextColumn, "UniqueId" }; - - var dictionaryTableHasDuplicates = ContainsDuplicates(dictionaryColumnsToBeIndexed); - var langTextTableHasDuplicates = ContainsDuplicates(langTextColumnsToBeIndexed); - - // Check if there are any duplicates before we delete and re-create the indexes since - // if there are duplicates we won't be able to create the new unique indexes - if (!dictionaryTableHasDuplicates) - { - // Delete existing - DeleteIndex(indexDictionaryDto); - } - - if (!langTextTableHasDuplicates) - { - // Delete existing - DeleteIndex(indexLanguageTextDto); - } - - // Try to re-create/add - TryAddUniqueConstraint(dictionaryColumnsToBeIndexed, indexDictionaryDto, dictionaryTableHasDuplicates); - TryAddUniqueConstraint(langTextColumnsToBeIndexed, indexLanguageTextDto, langTextTableHasDuplicates); - } - - private void DeleteIndex(string indexName) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - if (IndexExists(indexName)) - { - Delete.Index(indexName).OnTable(tableDef.Name).Do(); - } - } - - private void CreateIndex(string indexName) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - // get the definition by name - var index = tableDef.Indexes.First(x => x.Name == indexName); - new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); - } - - private void TryAddUniqueConstraint(string[] columns, string index, bool containsDuplicates) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - // Check the existing data to ensure the constraint can be successfully applied. - // This seems to be better than relying on catching an exception as this leads to - // transaction errors: "This SqlTransaction has completed; it is no longer usable". - var columnsDescription = string.Join("], [", columns); - if (containsDuplicates) - { - var message = $"Could not create unique constraint on [{tableDef.Name}] due to existing " + - $"duplicate records across the column{(columns.Length > 1 ? "s" : string.Empty)}: [{columnsDescription}]."; - - LogIncompleteMigrationStep(message); - return; - } - - CreateIndex(index); - } - - private bool ContainsDuplicates(string[] columns) - { - // Check for duplicates by comparing the total count of all records with the count of records distinct by the - // provided column. If the former is greater than the latter, there's at least one duplicate record. - int recordCount = GetRecordCount(); - int distinctRecordCount = GetDistinctRecordCount(columns); - - return recordCount > distinctRecordCount; - } - - private int GetRecordCount() - { - var countQuery = Database.SqlContext.Sql() - .SelectCount() - .From(); - - return Database.ExecuteScalar(countQuery); - } - - private int GetDistinctRecordCount(string[] columns) - { - string columnSpecification; - - columnSpecification = columns.Length == 1 - ? QuoteColumnName(columns[0]) - : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; - - var distinctCountQuery = Database.SqlContext.Sql() - .Select($"COUNT(DISTINCT({columnSpecification}))") - .From(); - - return Database.ExecuteScalar(distinctCountQuery); - } - - private void LogIncompleteMigrationStep(string message) => Logger.LogError($"Database migration step failed: {message}"); - - private string StringConvertedAndQuotedColumnName(string column) => $"CONVERT(nvarchar(1000),{QuoteColumnName(column)})"; - - private string QuoteColumnName(string column) => $"[{column}]"; } + + protected override void Migrate() + { + var indexDictionaryDto = $"IX_{DictionaryDto.TableName}_{IndexedDictionaryColumn}"; + var indexLanguageTextDto = $"IX_{LanguageTextDto.TableName}_{IndexedLanguageTextColumn}"; + var dictionaryColumnsToBeIndexed = new[] { IndexedDictionaryColumn }; + var langTextColumnsToBeIndexed = new[] { IndexedLanguageTextColumn, "UniqueId" }; + + var dictionaryTableHasDuplicates = ContainsDuplicates(dictionaryColumnsToBeIndexed); + var langTextTableHasDuplicates = ContainsDuplicates(langTextColumnsToBeIndexed); + + // Check if there are any duplicates before we delete and re-create the indexes since + // if there are duplicates we won't be able to create the new unique indexes + if (!dictionaryTableHasDuplicates) + { + // Delete existing + DeleteIndex(indexDictionaryDto); + } + + if (!langTextTableHasDuplicates) + { + // Delete existing + DeleteIndex(indexLanguageTextDto); + } + + // Try to re-create/add + TryAddUniqueConstraint(dictionaryColumnsToBeIndexed, indexDictionaryDto, + dictionaryTableHasDuplicates); + TryAddUniqueConstraint(langTextColumnsToBeIndexed, indexLanguageTextDto, + langTextTableHasDuplicates); + } + + private void DeleteIndex(string indexName) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); + + if (IndexExists(indexName)) + { + Delete.Index(indexName).OnTable(tableDef.Name).Do(); + } + } + + private void CreateIndex(string indexName) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); + + // get the definition by name + IndexDefinition index = tableDef.Indexes.First(x => x.Name == indexName); + new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) } + .Execute(); + } + + private void TryAddUniqueConstraint(string[] columns, string index, bool containsDuplicates) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); + + // Check the existing data to ensure the constraint can be successfully applied. + // This seems to be better than relying on catching an exception as this leads to + // transaction errors: "This SqlTransaction has completed; it is no longer usable". + var columnsDescription = string.Join("], [", columns); + if (containsDuplicates) + { + var message = $"Could not create unique constraint on [{tableDef.Name}] due to existing " + + $"duplicate records across the column{(columns.Length > 1 ? "s" : string.Empty)}: [{columnsDescription}]."; + + LogIncompleteMigrationStep(message); + return; + } + + CreateIndex(index); + } + + private bool ContainsDuplicates(string[] columns) + { + // Check for duplicates by comparing the total count of all records with the count of records distinct by the + // provided column. If the former is greater than the latter, there's at least one duplicate record. + var recordCount = GetRecordCount(); + var distinctRecordCount = GetDistinctRecordCount(columns); + + return recordCount > distinctRecordCount; + } + + private int GetRecordCount() + { + Sql countQuery = Database.SqlContext.Sql() + .SelectCount() + .From(); + + return Database.ExecuteScalar(countQuery); + } + + private int GetDistinctRecordCount(string[] columns) + { + string columnSpecification; + + columnSpecification = columns.Length == 1 + ? QuoteColumnName(columns[0]) + : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; + + Sql distinctCountQuery = Database.SqlContext.Sql() + .Select($"COUNT(DISTINCT({columnSpecification}))") + .From(); + + return Database.ExecuteScalar(distinctCountQuery); + } + + private void LogIncompleteMigrationStep(string message) => + Logger.LogError($"Database migration step failed: {message}"); + + private string StringConvertedAndQuotedColumnName(string column) => + $"CONVERT(nvarchar(1000),{QuoteColumnName(column)})"; + + private string QuoteColumnName(string column) => $"[{column}]"; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs index db7f17eee3..8caa28de03 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs @@ -1,80 +1,70 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NPoco; -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +public class ExternalLoginTableIndexes : MigrationBase { - - public class ExternalLoginTableIndexes : MigrationBase + public ExternalLoginTableIndexes(IMigrationContext context) + : base(context) { - public ExternalLoginTableIndexes(IMigrationContext context) - : base(context) + } + + /// + /// Adds new indexes to the External Login table + /// + protected override void Migrate() + { + // Before adding these indexes we need to remove duplicate data. + // Get all logins by latest + var logins = Database.Fetch() + .OrderByDescending(x => x.CreateDate) + .ToList(); + + var toDelete = new List(); + + // used to track duplicates so they can be removed + var keys = new HashSet<(string, string)>(); + foreach (ExternalLoginTokenTable.LegacyExternalLoginDto login in logins) { - } - - /// - /// Adds new indexes to the External Login table - /// - protected override void Migrate() - { - // Before adding these indexes we need to remove duplicate data. - // Get all logins by latest - var logins = Database.Fetch() - .OrderByDescending(x => x.CreateDate) - .ToList(); - - var toDelete = new List(); - // used to track duplicates so they can be removed - var keys = new HashSet<(string, string)>(); - foreach(ExternalLoginTokenTable.LegacyExternalLoginDto login in logins) + if (!keys.Add((login.ProviderKey, login.LoginProvider))) { - if (!keys.Add((login.ProviderKey, login.LoginProvider))) - { - // if it already exists we need to remove this one - toDelete.Add(login.Id); - } - } - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } - - var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; - - if (!IndexExists(indexName1)) - { - Create - .Index(indexName1) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider") - .Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); - } - - var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; - - if (!IndexExists(indexName2)) - { - Create - .Index(indexName2) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("providerKey").Ascending() - .WithOptions() - .NonClustered() - .Do(); + // if it already exists we need to remove this one + toDelete.Add(login.Id); } } + if (toDelete.Count > 0) + { + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)) + .Execute(); + } + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; + + if (!IndexExists(indexName1)) + { + Create + .Index(indexName1) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider") + .Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + } + + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; + + if (!IndexExists(indexName2)) + { + Create + .Index(indexName2) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs index 2c77b301ce..8c508a3d04 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs @@ -1,59 +1,58 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +/// +/// Fixes up the original for post RC release to ensure that +/// the correct indexes are applied. +/// +public class ExternalLoginTableIndexesFixup : MigrationBase { - /// - /// Fixes up the original for post RC release to ensure that - /// the correct indexes are applied. - /// - public class ExternalLoginTableIndexesFixup : MigrationBase + public ExternalLoginTableIndexesFixup(IMigrationContext context) + : base(context) { - public ExternalLoginTableIndexesFixup(IMigrationContext context) : base(context) + } + + protected override void Migrate() + { + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; + + if (IndexExists(indexName1)) { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); } - protected override void Migrate() + if (IndexExists(indexName2)) { - var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; - var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; - - if (IndexExists(indexName1)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); - } - - if (IndexExists(indexName2)) - { - // drop since it's using a column we're about to modify - Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); - } - - // then fixup the length of the loginProvider column - AlterColumn(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider"); - - // create it with the correct definition - Create - .Index(indexName1) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("userId").Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); - - // re-create the original - Create - .Index(indexName2) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("providerKey").Ascending() - .WithOptions() - .NonClustered() - .Do(); + // drop since it's using a column we're about to modify + Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); } + + // then fixup the length of the loginProvider column + AlterColumn( + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider"); + + // create it with the correct definition + Create + .Index(indexName1) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userId").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + + // re-create the original + Create + .Index(indexName2) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs index ee089ad89c..288dbdf23f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; @@ -7,75 +5,75 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class ExternalLoginTokenTable : MigrationBase { - public class ExternalLoginTokenTable : MigrationBase + public ExternalLoginTokenTable(IMigrationContext context) + : base(context) { - public ExternalLoginTokenTable(IMigrationContext context) - : base(context) + } + + /// + /// Adds new External Login token table + /// + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains(ExternalLoginTokenDto.TableName)) { + return; } + Create.Table().Do(); + } + + [TableName(TableName)] + [ExplicitColumns] + [PrimaryKey("Id")] + internal class LegacyExternalLoginDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Obsolete( + "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [Column("userId")] + public int? UserId { get; set; } + /// - /// Adds new External Login token table + /// Used to store the name of the provider (i.e. Facebook, Google) /// - protected override void Migrate() - { - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains(ExternalLoginTokenDto.TableName)) - { - return; - } + [Column("loginProvider")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", + Name = "IX_" + TableName + "_LoginProvider")] + public string LoginProvider { get; set; } = null!; - Create.Table().Do(); - } + /// + /// Stores the key the provider uses to lookup the login + /// + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", + Name = "IX_" + TableName + "_ProviderKey")] + public string ProviderKey { get; set; } = null!; - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class LegacyExternalLoginDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } - - [Obsolete( - "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] - [Column("userId")] - public int? UserId { get; set; } - - - /// - /// Used to store the name of the provider (i.e. Facebook, Google) - /// - [Column("loginProvider")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", - Name = "IX_" + TableName + "_LoginProvider")] - public string LoginProvider { get; set; } = null!; - - /// - /// Stores the key the provider uses to lookup the login - /// - [Column("providerKey")] - [Length(4000)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", - Name = "IX_" + TableName + "_ProviderKey")] - public string ProviderKey { get; set; } = null!; - - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - - /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider - /// - [Column("userData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? UserData { get; set; } - } + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? UserData { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs index 5dd274ad05..50204e4432 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs @@ -1,24 +1,22 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class MemberTableColumns : MigrationBase { - public class MemberTableColumns : MigrationBase + public MemberTableColumns(IMigrationContext context) + : base(context) { - public MemberTableColumns(IMigrationContext context) - : base(context) - { - } + } - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + /// + /// Adds new columns to members table + /// + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "securityStampToken"); - AddColumnIfNotExists(columns, "emailConfirmedDate"); - } + AddColumnIfNotExists(columns, "securityStampToken"); + AddColumnIfNotExists(columns, "emailConfirmedDate"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs index 365c50b3f8..641433cee9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs @@ -1,105 +1,106 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class MigrateLogViewerQueriesFromFileToDb : MigrationBase { - - public class MigrateLogViewerQueriesFromFileToDb : MigrationBase + internal static readonly IEnumerable _defaultLogQueries = new LogViewerQueryDto[] { - private readonly IHostingEnvironment _hostingEnvironment; - internal static readonly IEnumerable DefaultLogQueries = new LogViewerQueryDto[] + new() { - new (){ - Name = "Find all logs where the Level is NOT Verbose and NOT Debug", - Query = "Not(@Level='Verbose') and Not(@Level='Debug')" - }, - new (){ - Name = "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", - Query = "Has(@Exception)" - }, - new (){ - Name = "Find all logs that have the property 'Duration'", - Query = "Has(Duration)" - }, - new (){ - Name = "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", - Query = "Has(Duration) and Duration > 1000" - }, - new (){ - Name = "Find all logs that are from the namespace 'Umbraco.Core'", - Query = "StartsWith(SourceContext, 'Umbraco.Core')" - }, - new (){ - Name = "Find all logs that use a specific log message template", - Query = "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" - }, - new (){ - Name = "Find logs where one of the items in the SortedComponentTypes property array is equal to", - Query = "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" - }, - new (){ - Name = "Find logs where one of the items in the SortedComponentTypes property array contains", - Query = "Contains(SortedComponentTypes[?], 'DatabaseServer')" - }, - new (){ - Name = "Find all logs that the message has localhost in it with SQL like", - Query = "@Message like '%localhost%'" - }, - new (){ - Name = "Find all logs that the message that starts with 'end' in it with SQL like", - Query = "@Message like 'end%'" - } - }; - - public MigrateLogViewerQueriesFromFileToDb(IMigrationContext context, IHostingEnvironment hostingEnvironment) - : base(context) + Name = "Find all logs where the Level is NOT Verbose and NOT Debug", + Query = "Not(@Level='Verbose') and Not(@Level='Debug')", + }, + new() { - _hostingEnvironment = hostingEnvironment; - } - - protected override void Migrate() + Name = "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", + Query = "Has(@Exception)", + }, + new() { Name = "Find all logs that have the property 'Duration'", Query = "Has(Duration)" }, + new() { - CreateDatabaseTable(); - MigrateFileContentToDB(); - } - private void CreateDatabaseTable() + Name = "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + Query = "Has(Duration) and Duration > 1000", + }, + new() { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) - { - Create.Table().Do(); - } - } - - internal static string GetLogViewerQueryFile(IHostingEnvironment hostingEnvironment) + Name = "Find all logs that are from the namespace 'Umbraco.Core'", + Query = "StartsWith(SourceContext, 'Umbraco.Core')", + }, + new() { - return hostingEnvironment.MapPathContentRoot( - Path.Combine(Cms.Core.Constants.SystemDirectories.Config, "logviewer.searches.config.js")); - } - private void MigrateFileContentToDB() + Name = "Find all logs that use a specific log message template", + Query = "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'", + }, + new() { - var logViewerQueryFile = GetLogViewerQueryFile(_hostingEnvironment); + Name = "Find logs where one of the items in the SortedComponentTypes property array is equal to", + Query = "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'", + }, + new() + { + Name = "Find logs where one of the items in the SortedComponentTypes property array contains", + Query = "Contains(SortedComponentTypes[?], 'DatabaseServer')", + }, + new() + { + Name = "Find all logs that the message has localhost in it with SQL like", + Query = "@Message like '%localhost%'", + }, + new() + { + Name = "Find all logs that the message that starts with 'end' in it with SQL like", + Query = "@Message like 'end%'" + }, + }; - var logQueriesInFile = File.Exists(logViewerQueryFile) ? - JsonConvert.DeserializeObject(File.ReadAllText(logViewerQueryFile)) - : DefaultLogQueries; + private readonly IHostingEnvironment _hostingEnvironment; - var logQueriesInDb = Database.Query().ToArray(); + public MigrateLogViewerQueriesFromFileToDb(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) => + _hostingEnvironment = hostingEnvironment; - if (logQueriesInDb.Any()) - { - return; - } + internal static string GetLogViewerQueryFile(IHostingEnvironment hostingEnvironment) => + hostingEnvironment.MapPathContentRoot( + Path.Combine(Constants.SystemDirectories.Config, "logviewer.searches.config.js")); - Database.InsertBulk(logQueriesInFile!); + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateFileContentToDB(); + } - Context.AddPostMigration(); + private void CreateDatabaseTable() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(Constants.DatabaseSchema.Tables.LogViewerQuery)) + { + Create.Table().Do(); } } + + private void MigrateFileContentToDB() + { + var logViewerQueryFile = GetLogViewerQueryFile(_hostingEnvironment); + + IEnumerable? logQueriesInFile = File.Exists(logViewerQueryFile) + ? JsonConvert.DeserializeObject(File.ReadAllText(logViewerQueryFile)) + : _defaultLogQueries; + + LogViewerQueryDto[]? logQueriesInDb = Database.Query().ToArray(); + + if (logQueriesInDb.Any()) + { + return; + } + + Database.InsertBulk(logQueriesInFile!); + + Context.AddPostMigration(); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs index 601f2bd966..c844606ab2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs @@ -1,20 +1,19 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 -{ - public class UmbracoServerColumn : MigrationBase - { - public UmbracoServerColumn(IMigrationContext context) - : base(context) - { - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - ReplaceColumn(Cms.Core.Constants.DatabaseSchema.Tables.Server, "isMaster", "isSchedulingPublisher"); - } +public class UmbracoServerColumn : MigrationBase +{ + public UmbracoServerColumn(IMigrationContext context) + : base(context) + { } + + /// + /// Adds new columns to members table + /// + protected override void Migrate() => ReplaceColumn( + Constants.DatabaseSchema.Tables.Server, + "isMaster", "isSchedulingPublisher"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs index aa0d4472e8..7a885eca07 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs @@ -1,28 +1,29 @@ -using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0; + +internal class AddContentVersionCleanupFeature : MigrationBase { - class AddContentVersionCleanupFeature : MigrationBase + public AddContentVersionCleanupFeature(IMigrationContext context) + : base(context) { - public AddContentVersionCleanupFeature(IMigrationContext context) - : base(context) { } + } - /// - /// The conditionals are useful to enable the same migration to be used in multiple - /// migration paths x.x -> 8.18 and x.x -> 9.x - /// - protected override void Migrate() + /// + /// The conditionals are useful to enable the same migration to be used in multiple + /// migration paths x.x -> 8.18 and x.x -> 9.x + /// + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(ContentVersionCleanupPolicyDto.TableName)) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(ContentVersionCleanupPolicyDto.TableName)) - { - Create.Table().Do(); - } - - var columns = SqlSyntax.GetColumnsInSchema(Context.Database); - AddColumnIfNotExists(columns, "preventCleanup"); + Create.Table().Do(); } + + IEnumerable columns = SqlSyntax.GetColumnsInSchema(Context.Database); + AddColumnIfNotExists(columns, "preventCleanup"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs index 3bc62ab42e..9b91c0a372 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs @@ -1,17 +1,21 @@ -using Umbraco.Cms.Core; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; + +public class AddDefaultForNotificationsToggle : MigrationBase { - public class AddDefaultForNotificationsToggle : MigrationBase + public AddDefaultForNotificationsToggle(IMigrationContext context) + : base(context) { - public AddDefaultForNotificationsToggle(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var updateSQL = Sql($"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); - Execute.Sql(updateSQL.SQL).Do(); - } + protected override void Migrate() + { + Sql updateSQL = + Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); + Execute.Sql(updateSQL.SQL).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs index 1bb7b71c89..41abd07b23 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs @@ -1,30 +1,31 @@ -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; + +internal class AddUserGroup2NodeTable : MigrationBase { - class AddUserGroup2NodeTable : MigrationBase + public AddUserGroup2NodeTable(IMigrationContext context) + : base(context) { - public AddUserGroup2NodeTable(IMigrationContext context) - : base(context) { } + } - protected override void Migrate() + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(UserGroup2NodeDto.TableName)) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(UserGroup2NodeDto.TableName)) - { - Create.Table().Do(); - } - - // Insert if there exists specific permissions today. Can't do it directly in db in any nice way. - var allData = Database.Fetch(); - var toInsert = allData.Select(x => new UserGroup2NodeDto() { NodeId = x.NodeId, UserGroupId = x.UserGroupId }).Distinct( - new DelegateEqualityComparer( - (x, y) => x?.NodeId == y?.NodeId && x?.UserGroupId == y?.UserGroupId, - x => x.NodeId.GetHashCode() + x.UserGroupId.GetHashCode())).ToArray(); - Database.InsertBulk(toInsert); + Create.Table().Do(); } + + // Insert if there exists specific permissions today. Can't do it directly in db in any nice way. + List? allData = Database.Fetch(); + UserGroup2NodeDto[] toInsert = allData + .Select(x => new UserGroup2NodeDto { NodeId = x.NodeId, UserGroupId = x.UserGroupId }).Distinct( + new DelegateEqualityComparer( + (x, y) => x?.NodeId == y?.NodeId && x?.UserGroupId == y?.UserGroupId, + x => x.NodeId.GetHashCode() + x.UserGroupId.GetHashCode())).ToArray(); + Database.InsertBulk(toInsert); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs index c5e569282a..5e781406ae 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs @@ -1,24 +1,23 @@ -using System.Collections.Generic; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class AddTwoFactorLoginTable : MigrationBase { - public class AddTwoFactorLoginTable : MigrationBase + public AddTwoFactorLoginTable(IMigrationContext context) + : base(context) { - public AddTwoFactorLoginTable(IMigrationContext context) : base(context) + } + + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains(TwoFactorLoginDto.TableName)) { + return; } - protected override void Migrate() - { - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains(TwoFactorLoginDto.TableName)) - { - return; - } - - Create.Table().Do(); - } + Create.Table().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs index 3173e739a9..a2b2b40238 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs @@ -1,72 +1,64 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Linq; using Umbraco.Cms.Core.Packaging; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class MovePackageXMLToDb : MigrationBase { - public class MovePackageXMLToDb : MigrationBase + private readonly PackagesRepository _packagesRepository; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) + : base(context) { - private readonly PackagesRepository _packagesRepository; - private readonly PackageDefinitionXmlParser _xmlParser; + _packagesRepository = packagesRepository; + _xmlParser = new PackageDefinitionXmlParser(); + } - /// - /// Initializes a new instance of the class. - /// - public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) - : base(context) + /// + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateCreatedPackageFilesToDb(); + } + + private void CreateDatabaseTable() + { + // Add CreatedPackage table in database if it doesn't exist + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) { - _packagesRepository = packagesRepository; - _xmlParser = new PackageDefinitionXmlParser(); + Create.Table().Do(); + } + } + + private void MigrateCreatedPackageFilesToDb() + { + // Load data from file + IEnumerable packages = _packagesRepository.GetAll().WhereNotNull(); + var createdPackageDtos = new List(); + foreach (PackageDefinition package in packages) + { + // Create dto from xmlDocument + var dto = new CreatedPackageSchemaDto + { + Name = package.Name, + Value = _xmlParser.ToXml(package).ToString(), + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid(), + }; + createdPackageDtos.Add(dto); } - private void CreateDatabaseTable() + _packagesRepository.DeleteLocalRepositoryFiles(); + if (createdPackageDtos.Any()) { - // Add CreatedPackage table in database if it doesn't exist - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) - { - Create.Table().Do(); - } - } - - private void MigrateCreatedPackageFilesToDb() - { - // Load data from file - IEnumerable packages = _packagesRepository.GetAll().WhereNotNull(); - var createdPackageDtos = new List(); - foreach (PackageDefinition package in packages) - { - // Create dto from xmlDocument - var dto = new CreatedPackageSchemaDto() - { - Name = package.Name, - Value = _xmlParser.ToXml(package).ToString(), - UpdateDate = DateTime.Now, - PackageId = Guid.NewGuid() - }; - createdPackageDtos.Add(dto); - } - - _packagesRepository.DeleteLocalRepositoryFiles(); - if (createdPackageDtos.Any()) - { - // Insert dto into CreatedPackage table - Database.InsertBulk(createdPackageDtos); - } - } - - /// - protected override void Migrate() - { - CreateDatabaseTable(); - MigrateCreatedPackageFilesToDb(); + // Insert dto into CreatedPackage table + Database.InsertBulk(createdPackageDtos); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs index 6b74c49f67..c3f42a90ab 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs @@ -1,63 +1,63 @@ -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase { - public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase + public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) + : base(context) { - public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() + protected override void Migrate() + { + if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey")) { - if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey")) + var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; + var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId"; + + if (IndexExists(indexNameToRecreate)) { - var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; - var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId"; - - if (IndexExists(indexNameToRecreate)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do(); - } - - if (IndexExists(indexNameToDelete)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do(); - } - - //special trick to add the column without constraints and return the sql to add them later - AddColumn("userOrMemberKey", out var sqls); - - //populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. - Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)").Do(); - - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - //now remove these old columns - Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do(); - - // create index with the correct definition - Create - .Index(indexNameToRecreate) - .OnTable(ExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("userOrMemberKey").Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do(); } + + if (IndexExists(indexNameToDelete)) + { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do(); + } + + // special trick to add the column without constraints and return the sql to add them later + AddColumn("userOrMemberKey", out IEnumerable sqls); + + // populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. + Execute.Sql( + $"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)") + .Do(); + + // now apply constraints (NOT NULL) to new table + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // now remove these old columns + Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do(); + + // create index with the correct definition + Create + .Index(indexNameToRecreate) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userOrMemberKey").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); } - - } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs index 01cfb22a3d..550e67879a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs @@ -1,15 +1,15 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 -{ - internal class AddScheduledPublishingLock : MigrationBase - { - public AddScheduledPublishingLock(IMigrationContext context) - : base(context) - { - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; - protected override void Migrate() => - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); +internal class AddScheduledPublishingLock : MigrationBase +{ + public AddScheduledPublishingLock(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() => + Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs index 1c8fe7ed72..44144b93fe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs @@ -1,34 +1,31 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 +internal class UpdateRelationTypesToHandleDependencies : MigrationBase { - internal class UpdateRelationTypesToHandleDependencies : MigrationBase + public UpdateRelationTypesToHandleDependencies(IMigrationContext context) + : base(context) { - public UpdateRelationTypesToHandleDependencies(IMigrationContext context) - : base(context) + } + + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "isDependency"); + + var aliasesWithDependencies = new[] { - } + Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedMediaAlias, + }; - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - - AddColumnIfNotExists(columns, "isDependency"); - - var aliasesWithDependencies = new[] - { - Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, - Core.Constants.Conventions.RelationTypes.RelatedMediaAlias - }; - - Database.Execute( - Sql() - .Update(u => u.Set(x => x.IsDependency, true)) - .WhereIn(x => x.Alias, aliasesWithDependencies)); - - } + Database.Execute( + Sql() + .Update(u => u.Set(x => x.IsDependency, true)) + .WhereIn(x => x.Alias, aliasesWithDependencies)); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs index e2eece8313..04a9a700b1 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs @@ -1,45 +1,48 @@ -using System; -using System.Collections.Generic; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Convertable block data from json +/// +public class BlockEditorData { - /// - /// Convertable block data from json - /// - public class BlockEditorData + private readonly string _propertyEditorAlias; + + public BlockEditorData( + string propertyEditorAlias, + IEnumerable references, + BlockValue blockValue) { - private readonly string _propertyEditorAlias; - - public static BlockEditorData Empty { get; } = new BlockEditorData(); - - private BlockEditorData() + if (string.IsNullOrWhiteSpace(propertyEditorAlias)) { - _propertyEditorAlias = string.Empty; - BlockValue = new BlockValue(); + throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias)); } - public BlockEditorData(string propertyEditorAlias, - IEnumerable references, - BlockValue blockValue) - { - if (string.IsNullOrWhiteSpace(propertyEditorAlias)) - throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias)); - _propertyEditorAlias = propertyEditorAlias; - BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue)); - References = references != null ? new List(references) : throw new ArgumentNullException(nameof(references)); - } - - /// - /// Returns the layout for this specific property editor - /// - public JToken? Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null; - - /// - /// Returns the reference to the original BlockValue - /// - public BlockValue BlockValue { get; } - - public List References { get; } = new List(); + _propertyEditorAlias = propertyEditorAlias; + BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue)); + References = references != null + ? new List(references) + : throw new ArgumentNullException(nameof(references)); } + + private BlockEditorData() + { + _propertyEditorAlias = string.Empty; + BlockValue = new BlockValue(); + } + + public static BlockEditorData Empty { get; } = new(); + + /// + /// Returns the layout for this specific property editor + /// + public JToken? Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out JToken? layout) ? layout : null; + + /// + /// Returns the reference to the original BlockValue + /// + public BlockValue BlockValue { get; } + + public List References { get; } = new(); } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs index 0389603ac2..5b125ce855 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs @@ -1,68 +1,65 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Converts the block json data into objects +/// +public abstract class BlockEditorDataConverter { - /// - /// Converts the block json data into objects - /// - public abstract class BlockEditorDataConverter + private readonly string _propertyEditorAlias; + + protected BlockEditorDataConverter(string propertyEditorAlias) => _propertyEditorAlias = propertyEditorAlias; + + public BlockEditorData ConvertFrom(JToken json) { - private readonly string _propertyEditorAlias; + BlockValue? value = json.ToObject(); + return Convert(value); + } - protected BlockEditorDataConverter(string propertyEditorAlias) + public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) + { + try { - _propertyEditorAlias = propertyEditorAlias; + BlockValue? value = JsonConvert.DeserializeObject(json); + blockEditorData = Convert(value); + return true; + } + catch (Exception) + { + blockEditorData = null; + return false; + } + } + + public BlockEditorData Deserialize(string json) + { + BlockValue? value = JsonConvert.DeserializeObject(json); + return Convert(value); + } + + /// + /// Return the collection of from the block editor's Layout (which could be an array or + /// an object depending on the editor) + /// + /// + /// + protected abstract IEnumerable? GetBlockReferences(JToken jsonLayout); + + private BlockEditorData Convert(BlockValue? value) + { + if (value?.Layout == null) + { + return BlockEditorData.Empty; } - public BlockEditorData ConvertFrom(JToken json) - { - var value = json.ToObject(); - return Convert(value); - } - - public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) - { - try - { - var value = JsonConvert.DeserializeObject(json); - blockEditorData = Convert(value); - return true; - } - catch (System.Exception) - { - blockEditorData = null; - return false; - } - } - - public BlockEditorData Deserialize(string json) - { - var value = JsonConvert.DeserializeObject(json); - return Convert(value); - } - - private BlockEditorData Convert(BlockValue? value) - { - if (value?.Layout == null) - return BlockEditorData.Empty; - - var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout) + IEnumerable? references = + value.Layout.TryGetValue(_propertyEditorAlias, out JToken? layout) ? GetBlockReferences(layout) : Enumerable.Empty(); - return new BlockEditorData(_propertyEditorAlias, references!, value); - } - - /// - /// Return the collection of from the block editor's Layout (which could be an array or an object depending on the editor) - /// - /// - /// - protected abstract IEnumerable? GetBlockReferences(JToken jsonLayout); - + return new BlockEditorData(_propertyEditorAlias, references!, value); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs index a459a055ce..a99a62fea4 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs @@ -1,62 +1,61 @@ -using System; -using System.Collections.Generic; using Newtonsoft.Json; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a single block's data in raw form +/// +public class BlockItemData { + [JsonProperty("contentTypeKey")] + public Guid ContentTypeKey { get; set; } + /// - /// Represents a single block's data in raw form + /// not serialized, manually set and used during internally /// - public class BlockItemData + [JsonIgnore] + public string ContentTypeAlias { get; set; } = string.Empty; + + [JsonProperty("udi")] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? Udi { get; set; } + + [JsonIgnore] + public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); + + /// + /// The remaining properties will be serialized to a dictionary + /// + /// + /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket + /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm + /// NestedContent serializes to string, int, whatever eg + /// "stringValue":"Some String","numericValue":125,"otherNumeric":null + /// + [JsonExtensionData] + public Dictionary RawPropertyValues { get; set; } = new(); + + /// + /// Used during deserialization to convert the raw property data into data with a property type context + /// + [JsonIgnore] + public IDictionary PropertyValues { get; set; } = + new Dictionary(); + + /// + /// Used during deserialization to populate the property value/property type of a block item content property + /// + public class BlockPropertyValue { - [JsonProperty("contentTypeKey")] - public Guid ContentTypeKey { get; set; } - - /// - /// not serialized, manually set and used during internally - /// - [JsonIgnore] - public string ContentTypeAlias { get; set; } = string.Empty; - - [JsonProperty("udi")] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? Udi { get; set; } - - [JsonIgnore] - public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); - - /// - /// The remaining properties will be serialized to a dictionary - /// - /// - /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket - /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm - /// NestedContent serializes to string, int, whatever eg - /// "stringValue":"Some String","numericValue":125,"otherNumeric":null - /// - [JsonExtensionData] - public Dictionary RawPropertyValues { get; set; } = new Dictionary(); - - /// - /// Used during deserialization to convert the raw property data into data with a property type context - /// - [JsonIgnore] - public IDictionary PropertyValues { get; set; } = new Dictionary(); - - /// - /// Used during deserialization to populate the property value/property type of a block item content property - /// - public class BlockPropertyValue + public BlockPropertyValue(object? value, IPropertyType propertyType) { - public BlockPropertyValue(object? value, IPropertyType propertyType) - { - Value = value; - PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - } - - public object? Value { get; } - public IPropertyType PropertyType { get; } + Value = value; + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); } + + public object? Value { get; } + + public IPropertyType PropertyType { get; } } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs index 3d6c49c2e9..289a4245ec 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks -{ - /// - /// Data converter for the block list property editor - /// - public class BlockListEditorDataConverter : BlockEditorDataConverter - { - public BlockListEditorDataConverter() : base(Cms.Core.Constants.PropertyEditors.Aliases.BlockList) - { - } +namespace Umbraco.Cms.Core.Models.Blocks; - protected override IEnumerable? GetBlockReferences(JToken jsonLayout) - { - var blockListLayout = jsonLayout.ToObject>(); - return blockListLayout?.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); - } +/// +/// Data converter for the block list property editor +/// +public class BlockListEditorDataConverter : BlockEditorDataConverter +{ + public BlockListEditorDataConverter() + : base(Constants.PropertyEditors.Aliases.BlockList) + { + } + + protected override IEnumerable? GetBlockReferences(JToken jsonLayout) + { + IEnumerable? blockListLayout = jsonLayout.ToObject>(); + return blockListLayout?.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs index 6df34079f4..f98372be59 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs @@ -1,19 +1,18 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks -{ - /// - /// Used for deserializing the block list layout - /// - public class BlockListLayoutItem - { - [JsonProperty("contentUdi", Required = Required.Always)] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? ContentUdi { get; set; } +namespace Umbraco.Cms.Core.Models.Blocks; - [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? SettingsUdi { get; set; } - } +/// +/// Used for deserializing the block list layout +/// +public class BlockListLayoutItem +{ + [JsonProperty("contentUdi", Required = Required.Always)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? ContentUdi { get; set; } + + [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? SettingsUdi { get; set; } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs index 3b6df71422..aa7498f9f5 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs @@ -1,18 +1,16 @@ -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +public class BlockValue { - public class BlockValue - { - [JsonProperty("layout")] - public IDictionary Layout { get; set; } = null!; + [JsonProperty("layout")] + public IDictionary Layout { get; set; } = null!; - [JsonProperty("contentData")] - public List ContentData { get; set; } = new List(); + [JsonProperty("contentData")] + public List ContentData { get; set; } = new(); - [JsonProperty("settingsData")] - public List SettingsData { get; set; } = new List(); - } + [JsonProperty("settingsData")] + public List SettingsData { get; set; } = new(); } diff --git a/src/Umbraco.Infrastructure/Models/GridValue.cs b/src/Umbraco.Infrastructure/Models/GridValue.cs index a83d235b55..29e88ea564 100644 --- a/src/Umbraco.Infrastructure/Models/GridValue.cs +++ b/src/Umbraco.Infrastructure/Models/GridValue.cs @@ -1,87 +1,84 @@ -using System; -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models -{ - // TODO: Make a property value converter for this! +namespace Umbraco.Cms.Core.Models; - /// - /// A model representing the value saved for the grid - /// - public class GridValue +// TODO: Make a property value converter for this! + +/// +/// A model representing the value saved for the grid +/// +public class GridValue +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("sections")] + public IEnumerable Sections { get; set; } = null!; + + public class GridSection + { + [JsonProperty("grid")] + public string? Grid { get; set; } // TODO: what is this? + + [JsonProperty("rows")] + public IEnumerable Rows { get; set; } = null!; + } + + public class GridRow { [JsonProperty("name")] public string? Name { get; set; } - [JsonProperty("sections")] - public IEnumerable Sections { get; set; } = null!; + [JsonProperty("id")] + public Guid Id { get; set; } - public class GridSection - { - [JsonProperty("grid")] - public string? Grid { get; set; } // TODO: what is this? + [JsonProperty("areas")] + public IEnumerable Areas { get; set; } = null!; - [JsonProperty("rows")] - public IEnumerable Rows { get; set; } = null!; - } + [JsonProperty("styles")] + public JToken? Styles { get; set; } - public class GridRow - { - [JsonProperty("name")] - public string? Name { get; set; } + [JsonProperty("config")] + public JToken? Config { get; set; } + } - [JsonProperty("id")] - public Guid Id { get; set; } + public class GridArea + { + [JsonProperty("grid")] + public string? Grid { get; set; } // TODO: what is this? - [JsonProperty("areas")] - public IEnumerable Areas { get; set; } = null!; + [JsonProperty("controls")] + public IEnumerable Controls { get; set; } = null!; - [JsonProperty("styles")] - public JToken? Styles { get; set; } + [JsonProperty("styles")] + public JToken? Styles { get; set; } - [JsonProperty("config")] - public JToken? Config { get; set; } - } + [JsonProperty("config")] + public JToken? Config { get; set; } + } - public class GridArea - { - [JsonProperty("grid")] - public string? Grid { get; set; } // TODO: what is this? + public class GridControl + { + [JsonProperty("value")] + public JToken? Value { get; set; } - [JsonProperty("controls")] - public IEnumerable Controls { get; set; } = null!; + [JsonProperty("editor")] + public GridEditor Editor { get; set; } = null!; - [JsonProperty("styles")] - public JToken? Styles { get; set; } + [JsonProperty("styles")] + public JToken? Styles { get; set; } - [JsonProperty("config")] - public JToken? Config { get; set; } - } + [JsonProperty("config")] + public JToken? Config { get; set; } + } - public class GridControl - { - [JsonProperty("value")] - public JToken? Value { get; set; } + public class GridEditor + { + [JsonProperty("alias")] + public string Alias { get; set; } = null!; - [JsonProperty("editor")] - public GridEditor Editor { get; set; } = null!; - - [JsonProperty("styles")] - public JToken? Styles { get; set; } - - [JsonProperty("config")] - public JToken? Config { get; set; } - } - - public class GridEditor - { - [JsonProperty("alias")] - public string Alias { get; set; } = null!; - - [JsonProperty("view")] - public string? View { get; set; } - } + [JsonProperty("view")] + public string? View { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs index abb987c119..9e582b6520 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Examine; using Umbraco.Cms.Core.Mapping; @@ -9,283 +7,319 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class EntityMapDefinition : IMapDefinition { - public class EntityMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) { - public void DefineMaps(IUmbracoMapper mapper) + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new ContentTypeSort(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new SearchResultEntity(), Map); + mapper.Define((source, context) => new SearchResultEntity(), Map); + mapper.Define>((source, context) => + context.MapEnumerable(source).WhereNotNull()); + mapper.Define, IEnumerable>((source, context) => + context.MapEnumerable(source).WhereNotNull()); + } + + // Umbraco.Code.MapAll -Alias + private static void Map(IEntitySlim source, EntityBasic target, MapperContext context) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = MapName(source, context); + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Trashed = source.Trashed; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (source is IContentEntitySlim contentSlim) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new ContentTypeSort(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new SearchResultEntity(), Map); - mapper.Define((source, context) => new SearchResultEntity(), Map); - mapper.Define>((source, context) => context.MapEnumerable(source).WhereNotNull()); - mapper.Define, IEnumerable>((source, context) => context.MapEnumerable(source).WhereNotNull()); + source.AdditionalData!["ContentTypeAlias"] = contentSlim.ContentTypeAlias; } - // Umbraco.Code.MapAll -Alias - private static void Map(IEntitySlim source, EntityBasic target, MapperContext context) + if (source is IDocumentEntitySlim documentSlim) { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = MapName(source, context); - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Trashed = source.Trashed; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - - if (source is IContentEntitySlim contentSlim) - { - source.AdditionalData!["ContentTypeAlias"] = contentSlim.ContentTypeAlias; - } - - if (source is IDocumentEntitySlim documentSlim) - { - source.AdditionalData!["IsPublished"] = documentSlim.Published; - } - - if (source is IMediaEntitySlim mediaSlim) - { - if (source.AdditionalData is not null) - { - //pass UpdateDate for MediaPicker ListView ordering - source.AdditionalData["UpdateDate"] = mediaSlim.UpdateDate; - source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; - } - } + source.AdditionalData!["IsPublished"] = documentSlim.Published; + } + if (source is IMediaEntitySlim mediaSlim) + { if (source.AdditionalData is not null) { - // NOTE: we're mapping the objects in AdditionalData by object reference here. - // it works fine for now, but it's something to keep in mind in the future - foreach(var kvp in source.AdditionalData) + // pass UpdateDate for MediaPicker ListView ordering + source.AdditionalData["UpdateDate"] = mediaSlim.UpdateDate; + source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; + } + } + + if (source.AdditionalData is not null) + { + // NOTE: we're mapping the objects in AdditionalData by object reference here. + // it works fine for now, but it's something to keep in mind in the future + foreach (KeyValuePair kvp in source.AdditionalData) + { + if (kvp.Value is not null) { - if (kvp.Value is not null) - { - target.AdditionalData[kvp.Key] = kvp.Value; - } + target.AdditionalData[kvp.Key] = kvp.Value; } } - - target.AdditionalData.Add("IsContainer", source.IsContainer); } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(PropertyType source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = "icon-box"; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + target.AdditionalData.Add("IsContainer", source.IsContainer); + } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(PropertyGroup source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = "icon-tab"; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(PropertyType source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = "icon-box"; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = string.Empty; + } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(IUser source, EntityBasic target, MapperContext context) - { - target.Alias = source.Username; - target.Icon = Constants.Icons.User; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(PropertyGroup source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = "icon-tab"; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = string.Empty; + } - // Umbraco.Code.MapAll -Trashed - private static void Map(ITemplate source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = Constants.Icons.Template; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - target.Udi = Udi.Create(Constants.UdiEntityType.Template, source.Key); - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(IUser source, EntityBasic target, MapperContext context) + { + target.Alias = source.Key.ToString(); + target.Icon = Constants.Icons.User; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = string.Empty; + } - // Umbraco.Code.MapAll -SortOrder - private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = new Lazy(() => Convert.ToInt32(source.Id)); - } + // Umbraco.Code.MapAll -Trashed + private static void Map(ITemplate source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Template; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + target.Udi = Udi.Create(Constants.UdiEntityType.Template, source.Key); + } - // Umbraco.Code.MapAll -Trashed - private static void Map(IContentTypeComposition source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = ContentTypeMapDefinition.MapContentTypeUdi(source); - } + // Umbraco.Code.MapAll -SortOrder + private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = new Lazy(() => Convert.ToInt32(source.Id)); + } - // Umbraco.Code.MapAll -Trashed -Alias -Score - private static void Map(EntitySlim source, SearchResultEntity target, MapperContext context) - { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + // Umbraco.Code.MapAll -Trashed + private static void Map(IContentTypeComposition source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = ContentTypeMapDefinition.MapContentTypeUdi(source); + } - if (target.Icon.IsNullOrWhiteSpace()) + // Umbraco.Code.MapAll -Trashed -Alias -Score + private static void Map(EntitySlim source, SearchResultEntity target, MapperContext context) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (target.Icon.IsNullOrWhiteSpace()) + { + if (source.NodeObjectType == Constants.ObjectTypes.Document) { - if (source.NodeObjectType == Constants.ObjectTypes.Document) - target.Icon = Constants.Icons.Content; - if (source.NodeObjectType == Constants.ObjectTypes.Media) - target.Icon = Constants.Icons.Content; - if (source.NodeObjectType == Constants.ObjectTypes.Member) - target.Icon = Constants.Icons.Member; - else if (source.NodeObjectType == Constants.ObjectTypes.DataType) - target.Icon = Constants.Icons.DataType; - else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) - target.Icon = Constants.Icons.ContentType; - else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) - target.Icon = Constants.Icons.MediaType; - else if (source.NodeObjectType == Constants.ObjectTypes.MemberType) - target.Icon = Constants.Icons.MemberType; - else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) - target.Icon = Constants.Icons.Template; - } - } - - // Umbraco.Code.MapAll -Alias -Trashed - private static void Map(ISearchResult source, SearchResultEntity target, MapperContext context) - { - target.Id = source.Id; - target.Score = source.Score; - - // TODO: Properly map this (not aftermap) - - //get the icon if there is one - target.Icon = source.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) - ? source.Values[UmbracoExamineFieldNames.IconFieldName] - : Constants.Icons.DefaultIcon; - - target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] : "[no name]"; - - var culture = context.GetCulture()?.ToLowerInvariant(); - if(culture.IsNullOrWhiteSpace() == false) - { - target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name; + target.Icon = Constants.Icons.Content; } - if (source.Values.TryGetValue(UmbracoExamineFieldNames.UmbracoFileFieldName, out var umbracoFile) && - umbracoFile.IsNullOrWhiteSpace() == false) + if (source.NodeObjectType == Constants.ObjectTypes.Media) { - if (umbracoFile != null) - { - target.Name = $"{target.Name} ({umbracoFile})"; - } + target.Icon = Constants.Icons.Content; } - if (source.Values.ContainsKey(UmbracoExamineFieldNames.NodeKeyFieldName)) + if (source.NodeObjectType == Constants.ObjectTypes.Member) { - if (Guid.TryParse(source.Values[UmbracoExamineFieldNames.NodeKeyFieldName], out var key)) - { - target.Key = key; - - //need to set the UDI - if (source.Values.ContainsKey(ExamineFieldNames.CategoryFieldName)) - { - switch (source.Values[ExamineFieldNames.CategoryFieldName]) - { - case IndexTypes.Member: - target.Udi = new GuidUdi(Constants.UdiEntityType.Member, target.Key); - break; - case IndexTypes.Content: - target.Udi = new GuidUdi(Constants.UdiEntityType.Document, target.Key); - break; - case IndexTypes.Media: - target.Udi = new GuidUdi(Constants.UdiEntityType.Media, target.Key); - break; - } - } - } + target.Icon = Constants.Icons.Member; } - - if (source.Values.ContainsKey("parentID")) + else if (source.NodeObjectType == Constants.ObjectTypes.DataType) { - if (int.TryParse(source.Values["parentID"], NumberStyles.Integer, CultureInfo.InvariantCulture,out var parentId)) - { - target.ParentId = parentId; - } - else - { - target.ParentId = -1; - } + target.Icon = Constants.Icons.DataType; } - - target.Path = source.Values.ContainsKey(UmbracoExamineFieldNames.IndexPathFieldName) ? source.Values[UmbracoExamineFieldNames.IndexPathFieldName] : ""; - - if (source.Values.ContainsKey(ExamineFieldNames.ItemTypeFieldName)) + else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) { - target.AdditionalData.Add("contentType", source.Values[ExamineFieldNames.ItemTypeFieldName]); + target.Icon = Constants.Icons.ContentType; } - } - - private static string? MapContentTypeIcon(IEntitySlim entity) - { - switch (entity) + else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) { - case IMemberEntitySlim memberEntity: - return memberEntity.ContentTypeIcon; - case IContentEntitySlim contentEntity: - // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; + target.Icon = Constants.Icons.MediaType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.MemberType) + { + target.Icon = Constants.Icons.MemberType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) + { + target.Icon = Constants.Icons.Template; } - - return null; - } - - private static string MapName(IEntitySlim source, MapperContext context) - { - if (!(source is DocumentEntitySlim doc)) - return source.Name!; - - // invariant = only 1 name - if (!doc.Variations.VariesByCulture()) return source.Name!; - - // variant = depends on culture - var culture = context.GetCulture(); - - // if there's no culture here, the issue is somewhere else (UI, whatever) - throw! - if (culture == null) - //throw new InvalidOperationException("Missing culture in mapping options."); - // TODO: we should throw, but this is used in various places that won't set a culture yet - return source.Name!; - - // if we don't have a name for a culture, it means the culture is not available, and - // hey we should probably not be mapping it, but it's too late, return a fallback name - return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() ? name : $"({source.Name})"; } } + + // Umbraco.Code.MapAll -Alias -Trashed + private static void Map(ISearchResult source, SearchResultEntity target, MapperContext context) + { + target.Id = source.Id; + target.Score = source.Score; + + // TODO: Properly map this (not aftermap) + + // get the icon if there is one + target.Icon = source.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) + ? source.Values[UmbracoExamineFieldNames.IconFieldName] + : Constants.Icons.DefaultIcon; + + target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) + ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] + : "[no name]"; + + var culture = context.GetCulture()?.ToLowerInvariant(); + if (culture.IsNullOrWhiteSpace() == false) + { + target.Name = source.Values.ContainsKey($"nodeName_{culture}") + ? source.Values[$"nodeName_{culture}"] + : target.Name; + } + + if (source.Values.TryGetValue(UmbracoExamineFieldNames.UmbracoFileFieldName, out var umbracoFile) && + umbracoFile.IsNullOrWhiteSpace() == false) + { + if (umbracoFile != null) + { + target.Name = $"{target.Name} ({umbracoFile})"; + } + } + + if (source.Values.ContainsKey(UmbracoExamineFieldNames.NodeKeyFieldName)) + { + if (Guid.TryParse(source.Values[UmbracoExamineFieldNames.NodeKeyFieldName], out Guid key)) + { + target.Key = key; + + // need to set the UDI + if (source.Values.ContainsKey(ExamineFieldNames.CategoryFieldName)) + { + switch (source.Values[ExamineFieldNames.CategoryFieldName]) + { + case IndexTypes.Member: + target.Udi = new GuidUdi(Constants.UdiEntityType.Member, target.Key); + break; + case IndexTypes.Content: + target.Udi = new GuidUdi(Constants.UdiEntityType.Document, target.Key); + break; + case IndexTypes.Media: + target.Udi = new GuidUdi(Constants.UdiEntityType.Media, target.Key); + break; + } + } + } + } + + if (source.Values.ContainsKey("parentID")) + { + if (int.TryParse(source.Values["parentID"], NumberStyles.Integer, CultureInfo.InvariantCulture, + out var parentId)) + { + target.ParentId = parentId; + } + else + { + target.ParentId = -1; + } + } + + target.Path = source.Values.ContainsKey(UmbracoExamineFieldNames.IndexPathFieldName) + ? source.Values[UmbracoExamineFieldNames.IndexPathFieldName] + : string.Empty; + + if (source.Values.ContainsKey(ExamineFieldNames.ItemTypeFieldName)) + { + target.AdditionalData.Add("contentType", source.Values[ExamineFieldNames.ItemTypeFieldName]); + } + } + + private static string? MapContentTypeIcon(IEntitySlim entity) + { + switch (entity) + { + case IMemberEntitySlim memberEntity: + return memberEntity.ContentTypeIcon; + case IContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; + } + + return null; + } + + private static string MapName(IEntitySlim source, MapperContext context) + { + if (!(source is DocumentEntitySlim doc)) + { + return source.Name!; + } + + // invariant = only 1 name + if (!doc.Variations.VariesByCulture()) + { + return source.Name!; + } + + // variant = depends on culture + var culture = context.GetCulture(); + + // if there's no culture here, the issue is somewhere else (UI, whatever) - throw! + if (culture == null) + + // throw new InvalidOperationException("Missing culture in mapping options."); + // TODO: we should throw, but this is used in various places that won't set a culture yet + { + return source.Name!; + } + + // if we don't have a name for a culture, it means the culture is not available, and + // hey we should probably not be mapping it, but it's too late, return a fallback name + return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() + ? name + : $"({source.Name})"; + } } diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs index 9e5101550a..04e1a6825d 100644 --- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs +++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs @@ -1,79 +1,73 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a media item with local crops. +/// +/// +public class MediaWithCrops : PublishedContentWrapped { /// - /// Represents a media item with local crops. + /// Initializes a new instance of the class. /// - /// - public class MediaWithCrops : PublishedContentWrapped - { - - /// - /// Gets the content/media item. - /// - /// - /// The content/media item. - /// - public IPublishedContent Content => Unwrap(); - - /// - /// Gets the local crops. - /// - /// - /// The local crops. - /// - public ImageCropperValue LocalCrops { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The published value fallback. - /// The local crops. - public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) - : base(content, publishedValueFallback) - { - LocalCrops = localCrops; - } - } + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) + : base(content, publishedValueFallback) => + LocalCrops = localCrops; /// - /// Represents a media item with local crops. + /// Gets the content/media item. /// - /// The type of the media item. - /// - public class MediaWithCrops : MediaWithCrops - where T : IPublishedContent - { - /// - /// Gets the media item. - /// - /// - /// The media item. - /// - public new T Content { get; } + /// + /// The content/media item. + /// + public IPublishedContent Content => Unwrap(); - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The published value fallback. - /// The local crops. - public MediaWithCrops(T content,IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) - : base(content, publishedValueFallback, localCrops) - { - Content = content; - } - - /// - /// Performs an implicit conversion from to . - /// - /// The media with crops. - /// - /// The result of the conversion. - /// - public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; - } + /// + /// Gets the local crops. + /// + /// + /// The local crops. + /// + public ImageCropperValue LocalCrops { get; } +} + +/// +/// Represents a media item with local crops. +/// +/// The type of the media item. +/// +public class MediaWithCrops : MediaWithCrops + where T : IPublishedContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(T content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) + : base(content, publishedValueFallback, localCrops) => + Content = content; + + /// + /// Gets the media item. + /// + /// + /// The media item. + /// + public new T Content { get; } + + /// + /// Performs an implicit conversion from to . + /// + /// The media with crops. + /// + /// The result of the conversion. + /// + public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; } diff --git a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs index e7286d683f..a14137338d 100644 --- a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs +++ b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs @@ -1,116 +1,134 @@ -using System; -using System.IO; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Provides extension methods for path validation. +/// +internal static class PathValidationExtensions { /// - /// Provides extension methods for path validation. + /// Does a quick check on the entity's set path to ensure that it's valid and consistent /// - internal static class PathValidationExtensions + /// + /// + public static void ValidatePathWithException(this NodeDto entity) { - /// - /// Does a quick check on the entity's set path to ensure that it's valid and consistent - /// - /// - /// - public static void ValidatePathWithException(this NodeDto entity) + // don't validate if it's empty and it has no id + if (entity.NodeId == default && entity.Path.IsNullOrWhiteSpace()) { - //don't validate if it's empty and it has no id - if (entity.NodeId == default(int) && entity.Path.IsNullOrWhiteSpace()) - return; - - if (entity.Path.IsNullOrWhiteSpace()) - throw new InvalidDataException($"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}"); - - var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (pathParts.Length < 2) - { - //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id - throw new InvalidDataException($"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); - } - - if (entity.ParentId != default(int) && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) - { - //the 2nd last id in the path must be it's parent id - throw new InvalidDataException($"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); - } + return; } - /// - /// Does a quick check on the entity's set path to ensure that it's valid and consistent - /// - /// - /// - public static bool ValidatePath(this IUmbracoEntity entity) + if (entity.Path.IsNullOrWhiteSpace()) { - //don't validate if it's empty and it has no id - if (entity.HasIdentity == false && entity.Path.IsNullOrWhiteSpace()) - return true; + throw new InvalidDataException( + $"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}"); + } - if (entity.Path.IsNullOrWhiteSpace()) - return false; + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + { + // a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id + throw new InvalidDataException( + $"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); + } - var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (pathParts.Length < 2) - { - //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id - return false; - } - - if (entity.ParentId != default(int) && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) - { - //the 2nd last id in the path must be it's parent id - return false; - } + if (entity.ParentId != default && pathParts[^2] != entity.ParentId.ToInvariantString()) + { + // the 2nd last id in the path must be it's parent id + throw new InvalidDataException( + $"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); + } + } + /// + /// Does a quick check on the entity's set path to ensure that it's valid and consistent + /// + /// + /// + public static bool ValidatePath(this IUmbracoEntity entity) + { + // don't validate if it's empty and it has no id + if (entity.HasIdentity == false && entity.Path.IsNullOrWhiteSpace()) + { return true; } - /// - /// This will validate the entity's path and if it's invalid it will fix it, if fixing is required it will recursively - /// check and fix all ancestors if required. - /// - /// - /// - /// A callback specified to retrieve the parent entity of the entity - /// A callback specified to update a fixed entity - public static void EnsureValidPath(this T entity, - ILogger logger, - Func getParent, - Action update) - where T: IUmbracoEntity + if (entity.Path.IsNullOrWhiteSpace()) { - if (entity.HasIdentity == false) - throw new InvalidOperationException("Could not ensure the entity path, the entity has not been assigned an identity"); - - if (entity.ValidatePath() == false) - { - logger.LogWarning("The content item {EntityId} has an invalid path: {EntityPath} with parentID: {EntityParentId}", entity.Id, entity.Path, entity.ParentId); - if (entity.ParentId == -1) - { - entity.Path = string.Concat("-1,", entity.Id); - //path changed, update it - update(entity); - } - else - { - var parent = getParent(entity); - if (parent == null) - throw new NullReferenceException("Could not ensure path for entity " + entity.Id + " could not resolve it's parent " + entity.ParentId); - - //the parent must also be valid! - parent.EnsureValidPath(logger, getParent, update); - - entity.Path = string.Concat(parent.Path, ",", entity.Id); - //path changed, update it - update(entity); - } - } + return false; } + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + { + // a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id + return false; + } + + if (entity.ParentId != default && pathParts[^2] != entity.ParentId.ToInvariantString()) + { + // the 2nd last id in the path must be it's parent id + return false; + } + + return true; + } + + /// + /// This will validate the entity's path and if it's invalid it will fix it, if fixing is required it will recursively + /// check and fix all ancestors if required. + /// + /// + /// + /// A callback specified to retrieve the parent entity of the entity + /// A callback specified to update a fixed entity + public static void EnsureValidPath( + this T entity, + ILogger logger, + Func getParent, + Action update) + where T : IUmbracoEntity + { + if (entity.HasIdentity == false) + { + throw new InvalidOperationException( + "Could not ensure the entity path, the entity has not been assigned an identity"); + } + + if (entity.ValidatePath() == false) + { + logger.LogWarning( + "The content item {EntityId} has an invalid path: {EntityPath} with parentID: {EntityParentId}", + entity.Id, entity.Path, entity.ParentId); + if (entity.ParentId == -1) + { + entity.Path = string.Concat("-1,", entity.Id); + + // path changed, update it + update(entity); + } + else + { + T? parent = getParent(entity); + if (parent == null) + { + throw new NullReferenceException("Could not ensure path for entity " + entity.Id + + " could not resolve it's parent " + entity.ParentId); + } + + // the parent must also be valid! + parent.EnsureValidPath(logger, getParent, update); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + + // path changed, update it + update(entity); + } + } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs index fc123d485c..aad8417af9 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs @@ -1,33 +1,33 @@ -using System; using System.Reflection; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Manages API version handshake between client and server. +/// +public class ApiVersion { /// - /// Manages API version handshake between client and server. + /// Initializes a new instance of the class. /// - public class ApiVersion - { - /// - /// Initializes a new instance of the class. - /// - /// The currently executing version. - /// - internal ApiVersion(SemVersion executingVersion) => Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); + /// The currently executing version. + /// + internal ApiVersion(SemVersion executingVersion) => + Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); - private static SemVersion CurrentAssemblyVersion - => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute()!.InformationalVersion); + /// + /// Gets the currently executing API version. + /// + public static ApiVersion Current { get; } + = new(CurrentAssemblyVersion); - /// - /// Gets the currently executing API version. - /// - public static ApiVersion Current { get; } - = new ApiVersion(CurrentAssemblyVersion); + private static SemVersion CurrentAssemblyVersion + => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute()! + .InformationalVersion); - /// - /// Gets the executing version of the API. - /// - public SemVersion Version { get; } - } + /// + /// Gets the executing version of the API. + /// + public SemVersion Version { get; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs index c61de4ada4..5f7d018b67 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs @@ -1,136 +1,131 @@ -using System; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Notification handlers used by . +/// +/// +/// supports mode but not mode. +/// +public sealed class AutoModelsNotificationHandler : INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { + private static int _req; + private readonly ModelsBuilderSettings _config; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly ModelsGenerationError _mbErrors; + private readonly ModelsGenerator _modelGenerator; + /// - /// Notification handlers used by . + /// Initializes a new instance of the class. /// - /// - /// supports mode but not mode. - /// - public sealed class AutoModelsNotificationHandler : INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + public AutoModelsNotificationHandler( + ILogger logger, + IOptionsMonitor config, + ModelsGenerator modelGenerator, + ModelsGenerationError mbErrors, + IMainDom mainDom) { - private static int s_req; - private readonly ILogger _logger; - private readonly ModelsBuilderSettings _config; - private readonly ModelsGenerator _modelGenerator; - private readonly ModelsGenerationError _mbErrors; - private readonly IMainDom _mainDom; + _logger = logger; - /// - /// Initializes a new instance of the class. - /// - - public AutoModelsNotificationHandler( - ILogger logger, - IOptionsMonitor config, - ModelsGenerator modelGenerator, - ModelsGenerationError mbErrors, - IMainDom mainDom) + // We cant use IOptionsSnapshot here, cause this is used in the Core runtime, and that cannot use a scoped service as it has no scope + _config = config.CurrentValue ?? throw new ArgumentNullException(nameof(config)); + _modelGenerator = modelGenerator; + _mbErrors = mbErrors; + _mainDom = mainDom; + } + + // we do not manage InMemory models here + internal bool IsEnabled => _config.ModelsMode.IsAutoNotInMemory(); + + public void Handle(ContentTypeCacheRefresherNotification notification) => RequestModelsGeneration(); + + public void Handle(DataTypeCacheRefresherNotification notification) => RequestModelsGeneration(); + + /// + /// Handles the notification + /// + public void Handle(UmbracoApplicationStartingNotification notification) => Install(); + + public void Handle(UmbracoRequestEndNotification notification) + { + if (IsEnabled && _mainDom.IsMainDom) { - _logger = logger; - //We cant use IOptionsSnapshot here, cause this is used in the Core runtime, and that cannot use a scoped service as it has no scope - _config = config.CurrentValue ?? throw new ArgumentNullException(nameof(config)); - _modelGenerator = modelGenerator; - _mbErrors = mbErrors; - _mainDom = mainDom; + GenerateModelsIfRequested(); + } + } + + private void Install() + { + // don't run if not enabled + if (!IsEnabled) + { + } + } + + // NOTE + // CacheUpdated triggers within some asynchronous backend task where + // we have no HttpContext. So we use a static (non request-bound) + // var to register that models + // need to be generated. Could be by another request. Anyway. We could + // have collisions but... you know the risk. + private void RequestModelsGeneration() + { + if (!_mainDom.IsMainDom) + { + return; } - // we do not manage InMemory models here - internal bool IsEnabled => _config.ModelsMode.IsAutoNotInMemory(); + _logger.LogDebug("Requested to generate models."); - /// - /// Handles the notification - /// - public void Handle(UmbracoApplicationStartingNotification notification) => Install(); + Interlocked.Exchange(ref _req, 1); + } - private void Install() + private void GenerateModelsIfRequested() + { + if (Interlocked.Exchange(ref _req, 0) == 0) { - // don't run if not enabled - if (!IsEnabled) - { - return; - } + return; } - // NOTE - // CacheUpdated triggers within some asynchronous backend task where - // we have no HttpContext. So we use a static (non request-bound) - // var to register that models - // need to be generated. Could be by another request. Anyway. We could - // have collisions but... you know the risk. - - private void RequestModelsGeneration() + // cannot proceed unless we are MainDom + if (_mainDom.IsMainDom) { - if (!_mainDom.IsMainDom) + try { - return; + _logger.LogDebug("Generate models..."); + _logger.LogInformation("Generate models now."); + _modelGenerator.GenerateModels(); + _mbErrors.Clear(); + _logger.LogInformation("Generated."); } - - _logger.LogDebug("Requested to generate models."); - - Interlocked.Exchange(ref s_req, 1); - } - - private void GenerateModelsIfRequested() - { - if (Interlocked.Exchange(ref s_req, 0) == 0) + catch (TimeoutException) { - return; + _logger.LogWarning("Timeout, models were NOT generated."); } - - // cannot proceed unless we are MainDom - if (_mainDom.IsMainDom) + catch (Exception e) { - try - { - _logger.LogDebug("Generate models..."); - _logger.LogInformation("Generate models now."); - _modelGenerator.GenerateModels(); - _mbErrors.Clear(); - _logger.LogInformation("Generated."); - } - catch (TimeoutException) - { - _logger.LogWarning("Timeout, models were NOT generated."); - } - catch (Exception e) - { - _mbErrors.Report("Failed to build Live models.", e); - _logger.LogError("Failed to generate models.", e); - } - } - else - { - // this will only occur if this appdomain was MainDom and it has - // been released while trying to regenerate models. - _logger.LogWarning("Cannot generate models while app is shutting down"); + _mbErrors.Report("Failed to build Live models.", e); + _logger.LogError("Failed to generate models.", e); } } - - public void Handle(UmbracoRequestEndNotification notification) + else { - if (IsEnabled && _mainDom.IsMainDom) - { - GenerateModelsIfRequested(); - } + // this will only occur if this appdomain was MainDom and it has + // been released while trying to regenerate models. + _logger.LogWarning("Cannot generate models while app is shutting down"); } - - public void Handle(ContentTypeCacheRefresherNotification notification) => RequestModelsGeneration(); - - public void Handle(DataTypeCacheRefresherNotification notification) => RequestModelsGeneration(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs index 4bfd6ff348..2f9d4ff4cc 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs @@ -1,219 +1,239 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +// NOTE +// The idea was to have different types of builder, because I wanted to experiment with +// building code with CodeDom. Turns out more complicated than I thought and maybe not +// worth it at the moment, to we're using TextBuilder and its Generate method is specific. +// +// Keeping the code as-is for the time being... + +/// +/// Provides a base class for all builders. +/// +public abstract class Builder { - // NOTE - // The idea was to have different types of builder, because I wanted to experiment with - // building code with CodeDom. Turns out more complicated than I thought and maybe not - // worth it at the moment, to we're using TextBuilder and its Generate method is specific. - // - // Keeping the code as-is for the time being... + /// + /// Initializes a new instance of the class with a list of models to generate, + /// the result of code parsing, and a models namespace. + /// + /// The list of models to generate. + /// Configuration for modelsbuilder settings + protected Builder(ModelsBuilderSettings config, IList typeModels) + { + TypeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); + + Config = config ?? throw new ArgumentNullException(nameof(config)); + + // can be null or empty, we'll manage + ModelsNamespace = Config.ModelsNamespace; + + // but we want it to prepare + Prepare(); + } + + // for unit tests only +#pragma warning disable CS8618 + protected Builder() +#pragma warning restore CS8618 + { + } /// - /// Provides a base class for all builders. + /// Gets or sets a value indicating the namespace to use for the models. /// - public abstract class Builder + /// May be overriden by code attributes. + public string ModelsNamespace { get; set; } + + protected Dictionary ModelsMap { get; } = new(); + + // the list of assemblies that will be 'using' by default + protected IList TypesUsing { get; } = new List { - protected Dictionary ModelsMap { get; } = new Dictionary(); + "System", + "System.Linq.Expressions", + "Umbraco.Cms.Core.Models.PublishedContent", + "Umbraco.Cms.Core.PublishedCache", + "Umbraco.Cms.Infrastructure.ModelsBuilder", + "Umbraco.Cms.Core", + "Umbraco.Extensions", + }; - // the list of assemblies that will be 'using' by default - protected IList TypesUsing { get; } = new List + /// + /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. + /// + public IList Using => TypesUsing; + + /// + /// Gets the list of all models. + /// + /// Includes those that are ignored. + public IList TypeModels { get; } + + public string? ModelsNamespaceForTests { get; set; } + + protected ModelsBuilderSettings Config { get; } + + /// + /// Gets the list of models to generate. + /// + /// The models to generate + public IEnumerable GetModelsToGenerate() => TypeModels; + + public string GetModelsNamespace() + { + if (ModelsNamespaceForTests != null) { - "System", - "System.Linq.Expressions", - "Umbraco.Cms.Core.Models.PublishedContent", - "Umbraco.Cms.Core.PublishedCache", - "Umbraco.Cms.Infrastructure.ModelsBuilder", - "Umbraco.Cms.Core", - "Umbraco.Extensions" - }; - - /// - /// Gets or sets a value indicating the namespace to use for the models. - /// - /// May be overriden by code attributes. - public string ModelsNamespace { get; set; } - - /// - /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. - /// - public IList Using => TypesUsing; - - /// - /// Gets the list of models to generate. - /// - /// The models to generate - public IEnumerable GetModelsToGenerate() => TypeModels; - - /// - /// Gets the list of all models. - /// - /// Includes those that are ignored. - public IList TypeModels { get; } - - /// - /// Initializes a new instance of the class with a list of models to generate, - /// the result of code parsing, and a models namespace. - /// - /// The list of models to generate. - /// The models namespace. - protected Builder(ModelsBuilderSettings config, IList typeModels) - { - TypeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); - - Config = config ?? throw new ArgumentNullException(nameof(config)); - - // can be null or empty, we'll manage - ModelsNamespace = Config.ModelsNamespace; - - // but we want it to prepare - Prepare(); + return ModelsNamespaceForTests; } - // for unit tests only -#pragma warning disable CS8618 - protected Builder() -#pragma warning restore CS8618 - { } - - protected ModelsBuilderSettings Config { get; } - - /// - /// Prepares generation by processing the result of code parsing. - /// - private void Prepare() + // if builder was initialized with a namespace, use that one + if (!string.IsNullOrWhiteSpace(ModelsNamespace)) { - TypeModel.MapModelTypes(TypeModels, ModelsNamespace); + return ModelsNamespace; + } - var isInMemoryMode = Config.ModelsMode == ModelsMode.InMemoryAuto; + // use configured else fallback to default + return string.IsNullOrWhiteSpace(Config.ModelsNamespace) + ? Constants.ModelsBuilder.DefaultModelsNamespace + : Config.ModelsNamespace; + } - // for the first two of these two tests, - // always throw, even in InMemory mode: cannot happen unless ppl start fidling with attributes to rename - // things, and then they should pay attention to the generation error log - there's no magic here - // for the last one, don't throw in InMemory mode, see comment + // looking for a simple symbol eg 'Umbraco' or 'String' + // expecting to match eg 'Umbraco' or 'System.String' + // returns true if either + // - more than 1 symbol is found (explicitely ambiguous) + // - 1 symbol is found BUT not matching (implicitely ambiguous) + protected bool IsAmbiguousSymbol(string symbol, string match) => - // ensure we have no duplicates type names - foreach (var xx in TypeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" - + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting types."); + // cannot figure out is a symbol is ambiguous without Roslyn + // so... let's say everything is ambiguous - code won't be + // pretty but it'll work + // Essentially this means that a `global::` syntax will be output for the generated models + true; - // ensure we have no duplicates property names - foreach (var typeModel in TypeModels) - foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" - + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting properties."); + /// + /// Prepares generation by processing the result of code parsing. + /// + private void Prepare() + { + TypeModel.MapModelTypes(TypeModels, ModelsNamespace); - // ensure content & property type don't have identical name (csharp hates it) - foreach (var typeModel in TypeModels) + var isInMemoryMode = Config.ModelsMode == ModelsMode.InMemoryAuto; + + // for the first two of these two tests, + // always throw, even in InMemory mode: cannot happen unless ppl start fidling with attributes to rename + // things, and then they should pay attention to the generation error log - there's no magic here + // for the last one, don't throw in InMemory mode, see comment + + // ensure we have no duplicates type names + foreach (IGrouping xx in TypeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + { + throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" + + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting types."); + } + + // ensure we have no duplicates property names + foreach (TypeModel typeModel in TypeModels) + { + foreach (IGrouping xx in typeModel.Properties.GroupBy(x => x.ClrName) + .Where(x => x.Count() > 1)) { - foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) + throw new InvalidOperationException( + $"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" + + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting properties."); + } + } + + // ensure content & property type don't have identical name (csharp hates it) + foreach (TypeModel typeModel in TypeModels) + { + foreach (PropertyModel xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) + { + if (!isInMemoryMode) { - if (!isInMemoryMode) - throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." - + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." + throw new InvalidOperationException( + $"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." + + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." + + " Consider using an attribute to assign a different name to the property."); + } + + // in InMemory mode we generate commented out properties with an error message, + // instead of throwing, because then it kills the sites and ppl don't understand why + xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" + + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." + " Consider using an attribute to assign a different name to the property."); - // in InMemory mode we generate commented out properties with an error message, - // instead of throwing, because then it kills the sites and ppl don't understand why - xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" - + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." - + " Consider using an attribute to assign a different name to the property."); - - // will not be implemented on interface nor class - // note: we will still create the static getter, and implement the property on other classes... - } + // will not be implemented on interface nor class + // note: we will still create the static getter, and implement the property on other classes... } + } - // ensure we have no collision between base types - // NO: we may want to define a base class in a partial, on a model that has a parent - // we are NOT checking that the defined base type does maintain the inheritance chain - //foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) - // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", - // xx.Alias)); + // ensure we have no collision between base types + // NO: we may want to define a base class in a partial, on a model that has a parent + // we are NOT checking that the defined base type does maintain the inheritance chain + // foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) + // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", + // xx.Alias)); - // discover interfaces that need to be declared / implemented - foreach (var typeModel in TypeModels) + // discover interfaces that need to be declared / implemented + foreach (TypeModel typeModel in TypeModels) + { + // collect all the (non-removed) types implemented at parent level + // ie the parent content types and the mixins content types, recursively + var parentImplems = new List(); + if (typeModel.BaseType != null) { - // collect all the (non-removed) types implemented at parent level - // ie the parent content types and the mixins content types, recursively - var parentImplems = new List(); - if (typeModel.BaseType != null) - TypeModel.CollectImplems(parentImplems, typeModel.BaseType); - - // interfaces we must declare we implement (initially empty) - // ie this type's mixins, except those that have been removed, - // and except those that are already declared at the parent level - // in other words, DeclaringInterfaces is "local mixins" - var declaring = typeModel.MixinTypes - .Except(parentImplems); - typeModel.DeclaringInterfaces.AddRange(declaring); - - // interfaces we must actually implement (initially empty) - // if we declare we implement a mixin interface, we must actually implement - // its properties, all recursively (ie if the mixin interface implements...) - // so, starting with local mixins, we collect all the (non-removed) types above them - var mixinImplems = new List(); - foreach (var i in typeModel.DeclaringInterfaces) - TypeModel.CollectImplems(mixinImplems, i); - // and then we remove from that list anything that is already declared at the parent level - typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); + TypeModel.CollectImplems(parentImplems, typeModel.BaseType); } - // ensure elements don't inherit from non-elements - foreach (var typeModel in TypeModels.Where(x => x.IsElement)) + // interfaces we must declare we implement (initially empty) + // ie this type's mixins, except those that have been removed, + // and except those that are already declared at the parent level + // in other words, DeclaringInterfaces is "local mixins" + IEnumerable declaring = typeModel.MixinTypes + .Except(parentImplems); + typeModel.DeclaringInterfaces.AddRange(declaring); + + // interfaces we must actually implement (initially empty) + // if we declare we implement a mixin interface, we must actually implement + // its properties, all recursively (ie if the mixin interface implements...) + // so, starting with local mixins, we collect all the (non-removed) types above them + var mixinImplems = new List(); + foreach (TypeModel i in typeModel.DeclaringInterfaces) { - if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) - throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); - - var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); - if (errs.Count > 0) - throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); + TypeModel.CollectImplems(mixinImplems, i); } + + // and then we remove from that list anything that is already declared at the parent level + typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); } - // looking for a simple symbol eg 'Umbraco' or 'String' - // expecting to match eg 'Umbraco' or 'System.String' - // returns true if either - // - more than 1 symbol is found (explicitely ambiguous) - // - 1 symbol is found BUT not matching (implicitely ambiguous) - protected bool IsAmbiguousSymbol(string symbol, string match) + // ensure elements don't inherit from non-elements + foreach (TypeModel typeModel in TypeModels.Where(x => x.IsElement)) { - // cannot figure out is a symbol is ambiguous without Roslyn - // so... let's say everything is ambiguous - code won't be - // pretty but it'll work + if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) + { + throw new InvalidOperationException( + $"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); + } - // Essentially this means that a `global::` syntax will be output for the generated models - return true; - } - - public string? ModelsNamespaceForTests { get; set; } - - public string GetModelsNamespace() - { - if (ModelsNamespaceForTests != null) - return ModelsNamespaceForTests; - - // if builder was initialized with a namespace, use that one - if (!string.IsNullOrWhiteSpace(ModelsNamespace)) - return ModelsNamespace; - - // use configured else fallback to default - return string.IsNullOrWhiteSpace(Config.ModelsNamespace) - ? Constants.ModelsBuilder.DefaultModelsNamespace - : Config.ModelsNamespace; - } - - protected string GetModelsBaseClassName(TypeModel type) - { - // default - return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; + var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); + if (errs.Count > 0) + { + throw new InvalidOperationException( + $"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); + } } } + + protected string GetModelsBaseClassName(TypeModel type) => + + // default + type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs index 930bc163f0..0b2997e994 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs @@ -1,65 +1,64 @@ -using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building -{ - public class ModelsGenerator - { - private readonly UmbracoServices _umbracoService; - private ModelsBuilderSettings _config; - private readonly OutOfDateModelsStatus _outOfDateModels; - private readonly IHostingEnvironment _hostingEnvironment; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; - public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) +public class ModelsGenerator +{ + private readonly IHostingEnvironment _hostingEnvironment; + private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly UmbracoServices _umbracoService; + private ModelsBuilderSettings _config; + + public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, + OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) + { + _umbracoService = umbracoService; + _config = config.CurrentValue; + _outOfDateModels = outOfDateModels; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } + + public void GenerateModels() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) { - _umbracoService = umbracoService; - _config = config.CurrentValue; - _outOfDateModels = outOfDateModels; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); + Directory.CreateDirectory(modelsDirectory); } - public void GenerateModels() + foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - Directory.CreateDirectory(modelsDirectory); - } + File.Delete(file); + } - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - { - File.Delete(file); - } + IList typeModels = _umbracoService.GetAllTypes(); - System.Collections.Generic.IList typeModels = _umbracoService.GetAllTypes(); + var builder = new TextBuilder(_config, typeModels); - var builder = new TextBuilder(_config, typeModels); + foreach (TypeModel typeModel in builder.GetModelsToGenerate()) + { + var sb = new StringBuilder(); + builder.Generate(sb, typeModel); + var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); + File.WriteAllText(filename, sb.ToString()); + } - foreach (TypeModel typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); - File.WriteAllText(filename, sb.ToString()); - } - - // the idea was to calculate the current hash and to add it as an extra file to the compilation, - // in order to be able to detect whether a DLL is consistent with an environment - however the - // environment *might not* contain the local partial files, and thus it could be impossible to - // calculate the hash. So... maybe that's not a good idea after all? - /* - var currentHash = HashHelper.Hash(ourFiles, typeModels); - ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; + // the idea was to calculate the current hash and to add it as an extra file to the compilation, + // in order to be able to detect whether a DLL is consistent with an environment - however the + // environment *might not* contain the local partial files, and thus it could be impossible to + // calculate the hash. So... maybe that's not a good idea after all? + /* + var currentHash = HashHelper.Hash(ourFiles, typeModels); + ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; [assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] "; - */ + */ - _outOfDateModels.Clear(); - } + _outOfDateModels.Clear(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs index 6738308735..2475aebc3f 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs @@ -1,62 +1,67 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Represents a model property. +/// +public class PropertyModel { /// - /// Represents a model property. + /// Gets the alias of the property. /// - public class PropertyModel + public string Alias = string.Empty; + + /// + /// Gets the clr name of the property. + /// + /// This is just the local name eg "Price". + public string ClrName = string.Empty; + + /// + /// Gets the CLR type name of the property values. + /// + public string ClrTypeName = string.Empty; + + /// + /// Gets the description of the property. + /// + public string? Description; + + /// + /// Gets the generation errors for the property. + /// + /// + /// This should be null, unless something prevents the property from being + /// generated, and then the value should explain what. This can be used to generate + /// commented out code eg in mode. + /// + public List? Errors; + + /// + /// Gets the Model Clr type of the property values. + /// + /// + /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter + /// if any, else object. May include some ModelType that will need to be mapped. + /// + public Type ModelClrType = null!; + + /// + /// Gets the name of the property. + /// + public string Name = string.Empty; + + /// + /// Adds an error. + /// + public void AddError(string error) { - /// - /// Gets the alias of the property. - /// - public string Alias = string.Empty; - - /// - /// Gets the name of the property. - /// - public string Name = string.Empty; - - /// - /// Gets the description of the property. - /// - public string? Description; - - /// - /// Gets the clr name of the property. - /// - /// This is just the local name eg "Price". - public string ClrName = string.Empty; - - /// - /// Gets the Model Clr type of the property values. - /// - /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter - /// if any, else object. May include some ModelType that will need to be mapped. - public Type ModelClrType = null!; - - /// - /// Gets the CLR type name of the property values. - /// - public string ClrTypeName = string.Empty; - - /// - /// Gets the generation errors for the property. - /// - /// This should be null, unless something prevents the property from being - /// generated, and then the value should explain what. This can be used to generate - /// commented out code eg in mode. - public List? Errors; - - /// - /// Adds an error. - /// - public void AddError(string error) + if (Errors == null) { - if (Errors == null) Errors = new List(); - Errors.Add(error); + Errors = new List(); } + + Errors.Add(error); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 8bb65eb543..0fa866ec23 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -1,570 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Implements a builder that works by writing text. +/// +public class TextBuilder : Builder { - /// - /// Implements a builder that works by writing text. - /// - public class TextBuilder : Builder - { - /// - /// Initializes a new instance of the class with a list of models to generate - /// and the result of code parsing. - /// - /// The list of models to generate. - public TextBuilder(ModelsBuilderSettings config, IList typeModels) - : base(config, typeModels) - { } - - // internal for unit tests only - public TextBuilder() - { } - - /// - /// Outputs a generated model to a string builder. - /// - /// The string builder. - /// The model to generate. - public void Generate(StringBuilder sb, TypeModel typeModel) - { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); - - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); - - WriteContentType(sb, typeModel); - - sb.Append("}\n"); - } - - /// - /// Outputs generated models to a string builder. - /// - /// The string builder. - /// The models to generate. - public void Generate(StringBuilder sb, IEnumerable typeModels) - { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); - - // assembly attributes marker - sb.Append("\n//ASSATTR\n"); - - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); - - foreach (var typeModel in typeModels) - { - WriteContentType(sb, typeModel); - sb.Append("\n"); - } - - sb.Append("}\n"); - } - - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) - { - TextHeaderWriter.WriteHeader(sb); - } - - // writes an attribute that identifies code generated by a tool - // (helps reduce warnings, tools such as FxCop use it) - // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 - // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute - // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ - // - // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." - // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. - // - private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) - { - sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); - } - - // writes an attribute that specifies that an output may be null. - // (useful for consuming projects with nullable reference types enabled) - private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) - { - sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, isReturn ? "return: " : ""); - } - - private void WriteContentType(StringBuilder sb, TypeModel type) - { - string sep; - - if (type.IsMixin) - { - // write the interface declaration - sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); - if (!string.IsNullOrWhiteSpace(type.Name)) - sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); - var implements = type.BaseType == null - ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) - : type.BaseType.ClrName; - if (implements != null) - sb.AppendFormat(" : I{0}", implements); - - // write the mixins - sep = implements == null ? ":" : ","; - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } - - sb.Append("\n\t{\n"); - - // write the properties - only the local (non-ignored) ones, we're an interface - var more = false; - foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) - { - if (more) sb.Append("\n"); - more = true; - WriteInterfaceProperty(sb, prop); - } - - sb.Append("\t}\n\n"); - } - - // write the class declaration - if (!string.IsNullOrWhiteSpace(type.Name)) - sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - // cannot do it now. see note in ImplementContentTypeAttribute - //if (!type.HasImplement) - // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\tpublic partial class {0}", type.ClrName); - var inherits = type.HasBase - ? null // has its own base already - : (type.BaseType == null - ? GetModelsBaseClassName(type) - : type.BaseType.ClrName); - if (inherits != null) - sb.AppendFormat(" : {0}", inherits); - - sep = inherits == null ? ":" : ","; - if (type.IsMixin) - { - // if it's a mixin it implements its own interface - sb.AppendFormat("{0} I{1}", sep, type.ClrName); - } - else - { - // write the mixins, if any, as interfaces - // only if not a mixin because otherwise the interface already has them already - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } - } - - // begin class body - sb.Append("\n\t{\n"); - - // write the constants & static methods - // as 'new' since parent has its own - or maybe not - disable warning - sb.Append("\t\t// helpers\n"); - sb.Append("#pragma warning disable 0109 // new is redundant\n"); - WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", - type.Alias); - var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme - WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", - itemType); - WriteGeneratedCodeAttribute(sb, "\t\t"); - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); - WriteGeneratedCodeAttribute(sb, "\t\t"); - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", - type.ClrName); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); - sb.Append("#pragma warning restore 0109\n\n"); - sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); - - // write the ctor - sb.AppendFormat("\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\n\t\t}}\n\n", - type.ClrName, type.IsElement ? "Element" : "Content"); - - // write the properties - sb.Append("\t\t// properties\n"); - WriteContentTypeProperties(sb, type); - - // close the class declaration - sb.Append("\t}\n"); - } - - private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) - { - var staticMixinGetters = true; - - // write the properties - foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) - WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); - - // no need to write the parent properties since we inherit from the parent - // and the parent defines its own properties. need to write the mixins properties - // since the mixins are only interfaces and we have to provide an implementation. - - // write the mixins properties - foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) - foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName)) - if (staticMixinGetters) - WriteMixinProperty(sb, prop, mixinType.ClrName); - else - WriteProperty(sb, mixinType, prop); - } - - private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) - { - sb.Append("\n"); - - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) - { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); - else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\t///\n"); - } - - WriteGeneratedCodeAttribute(sb, "\t\t"); - - if (!property.ModelClrType.IsValueType) - { - WriteMaybeNullAttribute(sb, "\t\t", false); - } - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - - sb.AppendFormat(" {0} => ", - property.ClrName); - WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); - sb.AppendFormat(".{0}(this, _publishedValueFallback);\n", - MixinStaticGetterName(property.ClrName)); - } - - private static string MixinStaticGetterName(string clrName) - { - return string.Format("Get{0}", clrName); - } - - private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string? mixinClrName = null) - { - var mixinStatic = mixinClrName != null; - - sb.Append("\n"); - - if (property.Errors != null) - { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) - { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } - } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } - - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) - { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); - else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\t///\n"); - } - - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t"); - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - - if (mixinStatic) - { - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => {1}(this, _publishedValueFallback);\n", - property.ClrName, MixinStaticGetterName(property.ClrName)); - } - else - { - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => this.Value", - property.ClrName); - if (property.ModelClrType != typeof(object)) - { - sb.Append("<"); - WriteClrType(sb, property.ClrTypeName); - sb.Append(">"); - } - sb.AppendFormat("(_publishedValueFallback, \"{0}\");\n", - property.Alias); - } - - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); - } - - if (!mixinStatic) return; - - var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); - - //if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; - - sb.Append("\n"); - - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); - - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.Append("\t\tpublic static "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value", - mixinStaticGetterName, mixinClrName); - if (property.ModelClrType != typeof(object)) - { - sb.Append("<"); - WriteClrType(sb, property.ClrTypeName); - sb.Append(">"); - } - sb.AppendFormat("(publishedValueFallback, \"{0}\");\n", - property.Alias); - } - - private static IEnumerable SplitError(string error) - { - var p = 0; - while (p < error.Length) - { - var n = p + 50; - while (n < error.Length && error[n] != ' ') n++; - if (n >= error.Length) break; - yield return error.Substring(p, n - p); - p = n + 1; - } - if (p < error.Length) - yield return error.Substring(p); - } - - private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) - { - if (property.Errors != null) - { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) - { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } - } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } - - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t"); - - sb.Append("\t\t"); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} {{ get; }}\n", - property.ClrName); - - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); - } - } - - // internal for unit tests - public void WriteClrType(StringBuilder sb, Type type) - { - var s = type.ToString(); - - if (type.IsGenericType) - { - var p = s.IndexOf('`'); - WriteNonGenericClrType(sb, s.Substring(0, p)); - sb.Append("<"); - var args = type.GetGenericArguments(); - for (var i = 0; i < args.Length; i++) - { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); - } - sb.Append(">"); - } - else - { - WriteNonGenericClrType(sb, s); - } - } - - internal void WriteClrType(StringBuilder sb, string type) - { - var p = type.IndexOf('<'); - if (type.Contains('<')) - { - WriteNonGenericClrType(sb, type.Substring(0, p)); - sb.Append("<"); - var args = type.Substring(p + 1).TrimEnd(Constants.CharArrays.GreaterThan).Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types - for (var i = 0; i < args.Length; i++) - { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); - } - sb.Append(">"); - } - else - { - WriteNonGenericClrType(sb, type); - } - } - - private void WriteNonGenericClrType(StringBuilder sb, string s) - { - // map model types - s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); - - // takes care eg of "System.Int32" vs. "int" - if (TypesMap.TryGetValue(s, out string? typeName)) - { - sb.Append(typeName); - return; - } - - // if full type name matches a using clause, strip - // so if we want Umbraco.Core.Models.IPublishedContent - // and using Umbraco.Core.Models, then we just need IPublishedContent - typeName = s; - string? typeUsing = null; - var p = typeName.LastIndexOf('.'); - if (p > 0) - { - var x = typeName.Substring(0, p); - if (Using.Contains(x)) - { - typeName = typeName.Substring(p + 1); - typeUsing = x; - } - else if (x == ModelsNamespace) // that one is used by default - { - typeName = typeName.Substring(p + 1); - typeUsing = ModelsNamespace; - } - } - - // nested types *after* using - typeName = typeName.Replace("+", "."); - - // symbol to test is the first part of the name - // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous - p = typeName.IndexOf('.'); - var symbol = p > 0 ? typeName.Substring(0, p) : typeName; - - // what we should find - WITHOUT any generic thing - just the type - // no 'using' = the exact symbol - // a 'using' = using.symbol - var match = typeUsing == null ? symbol : (typeUsing + "." + symbol); - - // if not ambiguous, be happy - if (!IsAmbiguousSymbol(symbol, match)) - { - sb.Append(typeName); - return; - } - - // symbol is ambiguous - // if no 'using', must prepend global:: - if (typeUsing == null) - { - sb.Append("global::"); - sb.Append(s.Replace("+", ".")); - return; - } - - // could fullname be non-ambiguous? - // note: all-or-nothing, not trying to segment the using clause - typeName = s.Replace("+", "."); - p = typeName.IndexOf('.'); - symbol = typeName.Substring(0, p); - match = symbol; - - // still ambiguous, must prepend global:: - if (IsAmbiguousSymbol(symbol, match)) - sb.Append("global::"); - - sb.Append(typeName); - } - - private static string XmlCommentString(string s) - { - return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); - } - - private static readonly IDictionary TypesMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + private static readonly IDictionary _typesMap = + new Dictionary(StringComparer.OrdinalIgnoreCase) { { "System.Int16", "short" }, { "System.Int32", "int" }, @@ -581,7 +28,659 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { "System.SByte", "sbyte" }, { "System.Single", "float" }, { "System.Double", "double" }, - { "System.Decimal", "decimal" } + { "System.Decimal", "decimal" }, }; + + /// + /// Initializes a new instance of the class with a list of models to generate + /// and the result of code parsing. + /// + /// The list of models to generate. + public TextBuilder(ModelsBuilderSettings config, IList typeModels) + : base(config, typeModels) + { + } + + // internal for unit tests only + public TextBuilder() + { + } + + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) => TextHeaderWriter.WriteHeader(sb); + + /// + /// Outputs a generated model to a string builder. + /// + /// The string builder. + /// The model to generate. + public void Generate(StringBuilder sb, TypeModel typeModel) + { + WriteHeader(sb); + + foreach (var t in TypesUsing) + { + sb.AppendFormat("using {0};\n", t); + } + + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); + + WriteContentType(sb, typeModel); + + sb.Append("}\n"); + } + + /// + /// Outputs generated models to a string builder. + /// + /// The string builder. + /// The models to generate. + public void Generate(StringBuilder sb, IEnumerable typeModels) + { + WriteHeader(sb); + + foreach (var t in TypesUsing) + { + sb.AppendFormat("using {0};\n", t); + } + + // assembly attributes marker + sb.Append("\n//ASSATTR\n"); + + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); + + foreach (TypeModel typeModel in typeModels) + { + WriteContentType(sb, typeModel); + sb.Append("\n"); + } + + sb.Append("}\n"); + } + + // internal for unit tests + public void WriteClrType(StringBuilder sb, Type type) + { + var s = type.ToString(); + + if (type.IsGenericType) + { + var p = s.IndexOf('`'); + WriteNonGenericClrType(sb, s.Substring(0, p)); + sb.Append("<"); + Type[] args = type.GetGenericArguments(); + for (var i = 0; i < args.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + WriteClrType(sb, args[i]); + } + + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, s); + } + } + + // writes an attribute that identifies code generated by a tool + // (helps reduce warnings, tools such as FxCop use it) + // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 + // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute + // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ + // + // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." + // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. + private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat( + "{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", + tabs, ApiVersion.Current.Version); + + // writes an attribute that specifies that an output may be null. + // (useful for consuming projects with nullable reference types enabled) + private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) => + sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, + isReturn ? "return: " : string.Empty); + + private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName); + + private static IEnumerable SplitError(string error) + { + var p = 0; + while (p < error.Length) + { + var n = p + 50; + while (n < error.Length && error[n] != ' ') + { + n++; + } + + if (n >= error.Length) + { + break; + } + + yield return error.Substring(p, n - p); + p = n + 1; + } + + if (p < error.Length) + { + yield return error[p..]; + } + } + + private void WriteContentType(StringBuilder sb, TypeModel type) + { + string sep; + + if (type.IsMixin) + { + // write the interface declaration + sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); + if (!string.IsNullOrWhiteSpace(type.Name)) + { + sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); + } + + sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); + var implements = type.BaseType == null + ? type.HasBase ? null : type.IsElement ? "PublishedElement" : "PublishedContent" + : type.BaseType.ClrName; + if (implements != null) + { + sb.AppendFormat(" : I{0}", implements); + } + + // write the mixins + sep = implements == null ? ":" : ","; + foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) + { + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; + } + + sb.Append("\n\t{\n"); + + // write the properties - only the local (non-ignored) ones, we're an interface + var more = false; + foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName)) + { + if (more) + { + sb.Append("\n"); + } + + more = true; + WriteInterfaceProperty(sb, prop); + } + + sb.Append("\t}\n\n"); + } + + // write the class declaration + if (!string.IsNullOrWhiteSpace(type.Name)) + { + sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); + } + + // cannot do it now. see note in ImplementContentTypeAttribute + // if (!type.HasImplement) + // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\tpublic partial class {0}", type.ClrName); + var inherits = type.HasBase + ? null // has its own base already + : type.BaseType == null + ? GetModelsBaseClassName(type) + : type.BaseType.ClrName; + if (inherits != null) + { + sb.AppendFormat(" : {0}", inherits); + } + + sep = inherits == null ? ":" : ","; + if (type.IsMixin) + { + // if it's a mixin it implements its own interface + sb.AppendFormat("{0} I{1}", sep, type.ClrName); + } + else + { + // write the mixins, if any, as interfaces + // only if not a mixin because otherwise the interface already has them already + foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) + { + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; + } + } + + // begin class body + sb.Append("\n\t{\n"); + + // write the constants & static methods + // as 'new' since parent has its own - or maybe not - disable warning + sb.Append("\t\t// helpers\n"); + sb.Append("#pragma warning disable 0109 // new is redundant\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat( + "\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", + type.Alias); + TypeModel.ItemTypes itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat( + "\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", + itemType); + WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); + sb.Append( + "\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); + sb.Append( + "\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); + sb.AppendFormat( + "\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", + type.ClrName); + sb.Append( + "\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); + sb.Append("#pragma warning restore 0109\n\n"); + sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); + + // write the ctor + sb.AppendFormat( + "\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\n\t\t}}\n\n", + type.ClrName, type.IsElement ? "Element" : "Content"); + + // write the properties + sb.Append("\t\t// properties\n"); + WriteContentTypeProperties(sb, type); + + // close the class declaration + sb.Append("\t}\n"); + } + + private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) + { + var staticMixinGetters = true; + + // write the properties + foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName)) + { + WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); + } + + // no need to write the parent properties since we inherit from the parent + // and the parent defines its own properties. need to write the mixins properties + // since the mixins are only interfaces and we have to provide an implementation. + + // write the mixins properties + foreach (TypeModel mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) + { + foreach (PropertyModel prop in mixinType.Properties.OrderBy(x => x.ClrName)) + { + if (staticMixinGetters) + { + WriteMixinProperty(sb, prop, mixinType.ClrName); + } + else + { + WriteProperty(sb, mixinType, prop); + } + } + } + } + + private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) + { + sb.Append("\n"); + + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) + { + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), + XmlCommentString(property.Description)); + } + else + { + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + } + + sb.Append("\t\t///\n"); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } + + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); + + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + + sb.AppendFormat( + " {0} => ", + property.ClrName); + WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); + sb.AppendFormat( + ".{0}(this, _publishedValueFallback);\n", + MixinStaticGetterName(property.ClrName)); + } + + private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string? mixinClrName = null) + { + var mixinStatic = mixinClrName != null; + + sb.Append("\n"); + + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) + { + if (first) + { + first = false; + } + else + { + sb.Append("\t\t *\n"); + } + + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); + } + } + + sb.Append("\t\t *\n"); + sb.Append("\n"); + } + + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) + { + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), + XmlCommentString(property.Description)); + } + else + { + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + } + + sb.Append("\t\t///\n"); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } + + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); + + if (mixinStatic) + { + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0} => {1}(this, _publishedValueFallback);\n", + property.ClrName, MixinStaticGetterName(property.ClrName)); + } + else + { + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0} => this.Value", + property.ClrName); + if (property.ModelClrType != typeof(object)) + { + sb.Append("<"); + WriteClrType(sb, property.ClrTypeName); + sb.Append(">"); + } + + sb.AppendFormat( + "(_publishedValueFallback, \"{0}\");\n", + property.Alias); + } + + if (property.Errors != null) + { + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); + } + + if (!mixinStatic) + { + return; + } + + var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); + + // if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; + sb.Append("\n"); + + if (!string.IsNullOrWhiteSpace(property.Name)) + { + sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t", true); + } + + sb.Append("\t\tpublic static "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value", + mixinStaticGetterName, mixinClrName); + if (property.ModelClrType != typeof(object)) + { + sb.Append("<"); + WriteClrType(sb, property.ClrTypeName); + sb.Append(">"); + } + + sb.AppendFormat( + "(publishedValueFallback, \"{0}\");\n", + property.Alias); + } + + private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) + { + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) + { + if (first) + { + first = false; + } + else + { + sb.Append("\t\t *\n"); + } + + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); + } + } + + sb.Append("\t\t *\n"); + sb.Append("\n"); + } + + if (!string.IsNullOrWhiteSpace(property.Name)) + { + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } + + sb.Append("\t\t"); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0} {{ get; }}\n", + property.ClrName); + + if (property.Errors != null) + { + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); + } + } + + internal void WriteClrType(StringBuilder sb, string type) + { + var p = type.IndexOf('<'); + if (type.Contains('<')) + { + WriteNonGenericClrType(sb, type[..p]); + sb.Append("<"); + var args = type[(p + 1)..].TrimEnd(Constants.CharArrays.GreaterThan) + .Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types + for (var i = 0; i < args.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + WriteClrType(sb, args[i]); + } + + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, type); + } + } + + private static string XmlCommentString(string s) => + s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); + + private void WriteNonGenericClrType(StringBuilder sb, string s) + { + // map model types + s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); + + // takes care eg of "System.Int32" vs. "int" + if (_typesMap.TryGetValue(s, out var typeName)) + { + sb.Append(typeName); + return; + } + + // if full type name matches a using clause, strip + // so if we want Umbraco.Core.Models.IPublishedContent + // and using Umbraco.Core.Models, then we just need IPublishedContent + typeName = s; + string? typeUsing = null; + var p = typeName.LastIndexOf('.'); + if (p > 0) + { + var x = typeName.Substring(0, p); + if (Using.Contains(x)) + { + typeName = typeName.Substring(p + 1); + typeUsing = x; + } + else if (x == ModelsNamespace) // that one is used by default + { + typeName = typeName.Substring(p + 1); + typeUsing = ModelsNamespace; + } + } + + // nested types *after* using + typeName = typeName.Replace("+", "."); + + // symbol to test is the first part of the name + // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous + p = typeName.IndexOf('.'); + var symbol = p > 0 ? typeName.Substring(0, p) : typeName; + + // what we should find - WITHOUT any generic thing - just the type + // no 'using' = the exact symbol + // a 'using' = using.symbol + var match = typeUsing == null ? symbol : typeUsing + "." + symbol; + + // if not ambiguous, be happy + if (!IsAmbiguousSymbol(symbol, match)) + { + sb.Append(typeName); + return; + } + + // symbol is ambiguous + // if no 'using', must prepend global:: + if (typeUsing == null) + { + sb.Append("global::"); + sb.Append(s.Replace("+", ".")); + return; + } + + // could fullname be non-ambiguous? + // note: all-or-nothing, not trying to segment the using clause + typeName = s.Replace("+", "."); + p = typeName.IndexOf('.'); + symbol = typeName.Substring(0, p); + match = symbol; + + // still ambiguous, must prepend global:: + if (IsAmbiguousSymbol(symbol, match)) + { + sb.Append("global::"); + } + + sb.Append(typeName); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs index a192560f1d..5a532cbdba 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs @@ -1,25 +1,24 @@ using System.Text; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +internal static class TextHeaderWriter { - internal static class TextHeaderWriter + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) { - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) - { - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("// \n"); - sb.Append("// This code was generated by a tool.\n"); - sb.Append("//\n"); - sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version); - sb.Append("//\n"); - sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); - sb.Append("// \n"); - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("\n"); - } + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("// \n"); + sb.Append("// This code was generated by a tool.\n"); + sb.Append("//\n"); + sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version); + sb.Append("//\n"); + sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); + sb.Append("// \n"); + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("\n"); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs index 00da2e06fc..cc6d3be7c8 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs @@ -1,204 +1,218 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Represents a model. +/// +public class TypeModel { /// - /// Represents a model. + /// Gets the list of interfaces that this model needs to declare it implements. /// - public class TypeModel + /// + /// Some of these interfaces may actually be implemented by a base model + /// that this model inherits from. + /// + public readonly List DeclaringInterfaces = new(); + + /// + /// Represents the different model item types. + /// + public enum ItemTypes { /// - /// Gets the unique identifier of the corresponding content type. + /// Element. /// - public int Id; + Element, /// - /// Gets the alias of the model. + /// Content. /// - public string Alias = string.Empty; + Content, /// - /// Gets the name of the content type. + /// Media. /// - public string? Name; + Media, /// - /// Gets the description of the content type. + /// Member. /// - public string? Description; + Member, + } - /// - /// Gets the clr name of the model. - /// - /// This is the complete name eg "Foo.Bar.MyContent". - public string ClrName = string.Empty; + /// + /// Gets the list of interfaces that this model needs to actually implement. + /// + public readonly List ImplementingInterfaces = new(); - /// - /// Gets the unique identifier of the parent. - /// - /// The parent can either be a base content type, or a content types container. If the content - /// type does not have a base content type, then returns -1. - public int ParentId; + /// + /// Gets the mixin models. + /// + /// The current model implements mixins. + public readonly List MixinTypes = new(); - /// - /// Gets the base model. - /// - /// - /// If the content type does not have a base content type, then returns null. - /// The current model inherits from its base model. - /// - public TypeModel? BaseType; // the parent type in Umbraco (type inherits its properties) + /// + /// Gets the list of properties that are defined by this model. + /// + /// + /// These are only those property that are defined locally by this model, + /// and the list does not contain properties inherited from base models or from mixins. + /// + public readonly List Properties = new(); - /// - /// Gets the list of properties that are defined by this model. - /// - /// These are only those property that are defined locally by this model, - /// and the list does not contain properties inherited from base models or from mixins. - public readonly List Properties = new List(); + /// + /// Gets the alias of the model. + /// + public string Alias = string.Empty; - /// - /// Gets the mixin models. - /// - /// The current model implements mixins. - public readonly List MixinTypes = new List(); + private ItemTypes _itemType; - /// - /// Gets the list of interfaces that this model needs to declare it implements. - /// - /// Some of these interfaces may actually be implemented by a base model - /// that this model inherits from. - public readonly List DeclaringInterfaces = new List(); + /// + /// Gets the base model. + /// + /// + /// If the content type does not have a base content type, then returns null. + /// The current model inherits from its base model. + /// + public TypeModel? BaseType; // the parent type in Umbraco (type inherits its properties) - /// - /// Gets the list of interfaces that this model needs to actually implement. - /// - public readonly List ImplementingInterfaces = new List(); + /// + /// Gets the clr name of the model. + /// + /// This is the complete name eg "Foo.Bar.MyContent". + public string ClrName = string.Empty; - ///// - ///// Gets the list of existing static mixin method candidates. - ///// - //public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used + /// + /// Gets the description of the content type. + /// + public string? Description; - /// - /// Gets a value indicating whether this model has a base class. - /// - /// Can be either because the content type has a base content type declared in Umbraco, - /// or because the existing user's code declares a base class for this model. - public bool HasBase; + ///// + ///// Gets the list of existing static mixin method candidates. + ///// + // public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used - /// - /// Gets a value indicating whether this model is used as a mixin by another model. - /// - public bool IsMixin; + /// + /// Gets a value indicating whether this model has a base class. + /// + /// + /// Can be either because the content type has a base content type declared in Umbraco, + /// or because the existing user's code declares a base class for this model. + /// + public bool HasBase; - /// - /// Gets a value indicating whether this model is the base model of another model. - /// - public bool IsParent; + /// + /// Gets the unique identifier of the corresponding content type. + /// + public int Id; - /// - /// Gets a value indicating whether the type is an element. - /// - public bool IsElement => ItemType == ItemTypes.Element; + /// + /// Gets a value indicating whether this model is used as a mixin by another model. + /// + public bool IsMixin; - /// - /// Represents the different model item types. - /// - public enum ItemTypes + /// + /// Gets a value indicating whether this model is the base model of another model. + /// + public bool IsParent; + + /// + /// Gets the name of the content type. + /// + public string? Name; + + /// + /// Gets the unique identifier of the parent. + /// + /// + /// The parent can either be a base content type, or a content types container. If the content + /// type does not have a base content type, then returns -1. + /// + public int ParentId; + + /// + /// Gets a value indicating whether the type is an element. + /// + public bool IsElement => ItemType == ItemTypes.Element; + + /// + /// Gets or sets the model item type. + /// + public ItemTypes ItemType + { + get => _itemType; + set { - /// - /// Element. - /// - Element, - - /// - /// Content. - /// - Content, - - /// - /// Media. - /// - Media, - - /// - /// Member. - /// - Member - } - - private ItemTypes _itemType; - - /// - /// Gets or sets the model item type. - /// - public ItemTypes ItemType - { - get { return _itemType; } - set + switch (value) { - switch (value) - { - case ItemTypes.Element: - case ItemTypes.Content: - case ItemTypes.Media: - case ItemTypes.Member: - _itemType = value; - break; - default: - throw new ArgumentException("value"); - } + case ItemTypes.Element: + case ItemTypes.Content: + case ItemTypes.Media: + case ItemTypes.Member: + _itemType = value; + break; + default: + throw new ArgumentException("value"); } } + } - /// - /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. - /// - /// The collection. - /// The type. - /// Includes the specified type. - internal static void CollectImplems(ICollection types, TypeModel type) + /// + /// Enumerates the base models starting from the current model up. + /// + /// + /// Indicates whether the enumeration should start with the current model + /// or from its base model. + /// + /// The base models. + public IEnumerable EnumerateBaseTypes(bool andSelf = false) + { + TypeModel? typeModel = andSelf ? this : BaseType; + while (typeModel != null) { - if (types.Contains(type) == false) - types.Add(type); - if (type.BaseType != null) - CollectImplems(types, type.BaseType); - foreach (var mixin in type.MixinTypes) - CollectImplems(types, mixin); + yield return typeModel; + typeModel = typeModel.BaseType; + } + } + + /// + /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. + /// + /// The collection. + /// The type. + /// Includes the specified type. + internal static void CollectImplems(ICollection types, TypeModel type) + { + if (types.Contains(type) == false) + { + types.Add(type); } - /// - /// Enumerates the base models starting from the current model up. - /// - /// Indicates whether the enumeration should start with the current model - /// or from its base model. - /// The base models. - public IEnumerable EnumerateBaseTypes(bool andSelf = false) + if (type.BaseType != null) { - var typeModel = andSelf ? this : BaseType; - while (typeModel != null) - { - yield return typeModel; - typeModel = typeModel.BaseType; - } + CollectImplems(types, type.BaseType); } - /// - /// Maps ModelType. - /// - public static void MapModelTypes(IList typeModels, string ns) + foreach (TypeModel mixin in type.MixinTypes) { - var hasNs = !string.IsNullOrWhiteSpace(ns); - var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName); - foreach (var typeModel in typeModels) + CollectImplems(types, mixin); + } + } + + /// + /// Maps ModelType. + /// + public static void MapModelTypes(IList typeModels, string ns) + { + var hasNs = !string.IsNullOrWhiteSpace(ns); + var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? ns + "." + x.ClrName : x.ClrName); + foreach (TypeModel typeModel in typeModels) + { + foreach (PropertyModel propertyModel in typeModel.Properties) { - foreach (var propertyModel in typeModel.Properties) - { - propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); - } + propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs index 46af457299..0e53b9f04b 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs @@ -1,46 +1,42 @@ -using System.Collections.Generic; -using System.Linq; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +public class TypeModelHasher { - public class TypeModelHasher + public static string Hash(IEnumerable typeModels) { - public static string Hash(IEnumerable typeModels) + var builder = new StringBuilder(); + + // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash + // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference + foreach (TypeModel typeModel in typeModels.OrderBy(x => x.Alias)) { - var builder = new StringBuilder(); + builder.AppendLine("--- CONTENT TYPE MODEL ---"); + builder.AppendLine(typeModel.Id.ToString()); + builder.AppendLine(typeModel.Alias); + builder.AppendLine(typeModel.ClrName); + builder.AppendLine(typeModel.ParentId.ToString()); + builder.AppendLine(typeModel.Name); + builder.AppendLine(typeModel.Description); + builder.AppendLine(typeModel.ItemType.ToString()); + builder.AppendLine("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); - // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash - // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference - - foreach (var typeModel in typeModels.OrderBy(x => x.Alias)) + foreach (PropertyModel prop in typeModel.Properties.OrderBy(x => x.Alias)) { - builder.AppendLine("--- CONTENT TYPE MODEL ---"); - builder.AppendLine(typeModel.Id.ToString()); - builder.AppendLine(typeModel.Alias); - builder.AppendLine(typeModel.ClrName); - builder.AppendLine(typeModel.ParentId.ToString()); - builder.AppendLine(typeModel.Name); - builder.AppendLine(typeModel.Description); - builder.AppendLine(typeModel.ItemType.ToString()); - builder.AppendLine("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); - - foreach (var prop in typeModel.Properties.OrderBy(x => x.Alias)) - { - builder.AppendLine("--- PROPERTY ---"); - builder.AppendLine(prop.Alias); - builder.AppendLine(prop.ClrName); - builder.AppendLine(prop.Name); - builder.AppendLine(prop.Description); - builder.AppendLine(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName - } + builder.AppendLine("--- PROPERTY ---"); + builder.AppendLine(prop.Alias); + builder.AppendLine(prop.ClrName); + builder.AppendLine(prop.Name); + builder.AppendLine(prop.Description); + builder.AppendLine(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName } - - // Include the MB version in the hash so that if the MB version changes, models are rebuilt - builder.AppendLine(ApiVersion.Current.Version.ToString()); - - return builder.ToString().GenerateHash(); } + + // Include the MB version in the hash so that if the MB version changes, models are rebuilt + builder.AppendLine(ApiVersion.Current.Version.ToString()); + + return builder.ToString().GenerateHash(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs index 474bea9251..53c70ef8ac 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs @@ -1,16 +1,13 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +/// +/// Indicates that a property implements a given property alias. +/// +/// And therefore it should not be generated. +[AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] +public class ImplementPropertyTypeAttribute : Attribute { - /// - /// Indicates that a property implements a given property alias. - /// - /// And therefore it should not be generated. - [AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] - public class ImplementPropertyTypeAttribute : Attribute - { - public ImplementPropertyTypeAttribute(string alias) => Alias = alias; + public ImplementPropertyTypeAttribute(string alias) => Alias = alias; - public string Alias { get; } - } + public string Alias { get; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs index f016a3ecd2..073f72c6ad 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs @@ -1,23 +1,20 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +/// +/// Indicates that an Assembly is a Models Builder assembly. +/// +[AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] +public sealed class ModelsBuilderAssemblyAttribute : Attribute { /// - /// Indicates that an Assembly is a Models Builder assembly. + /// Gets or sets a value indicating whether the assembly is a InMemory assembly. /// - [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] - public sealed class ModelsBuilderAssemblyAttribute : Attribute - { - /// - /// Gets or sets a value indicating whether the assembly is a InMemory assembly. - /// - /// A Models Builder assembly can be either InMemory or a normal Dll. - public bool IsInMemory { get; set; } + /// A Models Builder assembly can be either InMemory or a normal Dll. + public bool IsInMemory { get; set; } - /// - /// Gets or sets a hash value representing the state of the custom source code files - /// and the Umbraco content types that were used to generate and compile the assembly. - /// - public string? SourceHash { get; set; } - } + /// + /// Gets or sets a hash value representing the state of the custom source code files + /// and the Umbraco content types that were used to generate and compile the assembly. + /// + public string? SourceHash { get; set; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs index b421042928..02db02afda 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs @@ -1,87 +1,84 @@ -using System; -using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public sealed class ModelsGenerationError { - public sealed class ModelsGenerationError + private readonly IHostingEnvironment _hostingEnvironment; + private ModelsBuilderSettings _config; + + /// + /// Initializes a new instance of the class. + /// + public ModelsGenerationError(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) { - private ModelsBuilderSettings _config; - private readonly IHostingEnvironment _hostingEnvironment; + _config = config.CurrentValue; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } - /// - /// Initializes a new instance of the class. - /// - public ModelsGenerationError(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) + public void Clear() + { + var errFile = GetErrFile(); + if (errFile == null) { - _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); + return; } - public void Clear() - { - var errFile = GetErrFile(); - if (errFile == null) - { - return; - } + // "If the file to be deleted does not exist, no exception is thrown." + File.Delete(errFile); + } - // "If the file to be deleted does not exist, no exception is thrown." - File.Delete(errFile); + public void Report(string message, Exception e) + { + var errFile = GetErrFile(); + if (errFile == null) + { + return; } - public void Report(string message, Exception e) + var sb = new StringBuilder(); + sb.Append(message); + sb.Append("\r\n"); + sb.Append(e.Message); + sb.Append("\r\n\r\n"); + sb.Append(e.StackTrace); + sb.Append("\r\n"); + + File.WriteAllText(errFile, sb.ToString()); + } + + public string? GetLastError() + { + var errFile = GetErrFile(); + if (errFile == null) { - var errFile = GetErrFile(); - if (errFile == null) - { - return; - } - - var sb = new StringBuilder(); - sb.Append(message); - sb.Append("\r\n"); - sb.Append(e.Message); - sb.Append("\r\n\r\n"); - sb.Append(e.StackTrace); - sb.Append("\r\n"); - - File.WriteAllText(errFile, sb.ToString()); + return null; } - public string? GetLastError() + try { - var errFile = GetErrFile(); - if (errFile == null) - { - return null; - } - - try - { - return File.ReadAllText(errFile); - } - catch - { - // accepted - return null; - } + return File.ReadAllText(errFile); } - - private string? GetErrFile() + catch { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - return null; - } - - return Path.Combine(modelsDirectory, "models.err"); + // accepted + return null; } } + + private string? GetErrFile() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) + { + return null; + } + + return Path.Combine(modelsDirectory, "models.err"); + } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs index 1d9ea7d499..4336f8ec71 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs @@ -1,102 +1,98 @@ -using System.IO; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Notifications; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Used to track if ModelsBuilder models are out of date/stale +/// +public sealed class OutOfDateModelsStatus : INotificationHandler, + INotificationHandler { + private readonly IHostingEnvironment _hostingEnvironment; + private ModelsBuilderSettings _config; + /// - /// Used to track if ModelsBuilder models are out of date/stale + /// Initializes a new instance of the class. /// - public sealed class OutOfDateModelsStatus : INotificationHandler, - INotificationHandler + public OutOfDateModelsStatus(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) { - private ModelsBuilderSettings _config; - private readonly IHostingEnvironment _hostingEnvironment; + _config = config.CurrentValue; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } - /// - /// Initializes a new instance of the class. - /// - public OutOfDateModelsStatus(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) - { - _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); - } + /// + /// Gets a value indicating whether flagging out of date models is enabled + /// + public bool IsEnabled => _config.FlagOutOfDateModels; - /// - /// Gets a value indicating whether flagging out of date models is enabled - /// - public bool IsEnabled => _config.FlagOutOfDateModels; - - /// - /// Gets a value indicating whether models are out of date - /// - public bool IsOutOfDate - { - get - { - if (_config.FlagOutOfDateModels == false) - { - return false; - } - - var path = GetFlagPath(); - return path != null && File.Exists(path); - } - } - - - private string GetFlagPath() - { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - Directory.CreateDirectory(modelsDirectory); - } - - return Path.Combine(modelsDirectory, "ood.flag"); - } - - private void Write() - { - // don't run if not configured - if (!IsEnabled) - { - return; - } - - var path = GetFlagPath(); - if (path == null || File.Exists(path)) - { - return; - } - - File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); - } - - public void Clear() + /// + /// Gets a value indicating whether models are out of date + /// + public bool IsOutOfDate + { + get { if (_config.FlagOutOfDateModels == false) { - return; + return false; } var path = GetFlagPath(); - if (path == null || !File.Exists(path)) - { - return; - } + return path != null && File.Exists(path); + } + } - File.Delete(path); + public void Handle(ContentTypeCacheRefresherNotification notification) => Write(); + + public void Handle(DataTypeCacheRefresherNotification notification) => Write(); + + public void Clear() + { + if (_config.FlagOutOfDateModels == false) + { + return; } - public void Handle(ContentTypeCacheRefresherNotification notification) => Write(); + var path = GetFlagPath(); + if (!File.Exists(path)) + { + return; + } - public void Handle(DataTypeCacheRefresherNotification notification) => Write(); + File.Delete(path); + } + + private string GetFlagPath() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) + { + Directory.CreateDirectory(modelsDirectory); + } + + return Path.Combine(modelsDirectory, "ood.flag"); + } + + private void Write() + { + // don't run if not configured + if (!IsEnabled) + { + return; + } + + var path = GetFlagPath(); + if (path == null || File.Exists(path)) + { + return; + } + + File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs index 85d953da3a..5da139b147 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Models.PublishedContent; @@ -6,46 +5,60 @@ using Umbraco.Cms.Infrastructure.ModelsBuilder; // same namespace as original Umbraco.Web PublishedElementExtensions // ReSharper disable once CheckNamespace -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to models. +/// +public static class PublishedElementExtensions { /// - /// Provides extension methods to models. + /// Gets the value of a property. /// - public static class PublishedElementExtensions + public static TValue? ValueFor( + this TModel model, + IPublishedValueFallback publishedValueFallback, + Expression> property, + string? culture = null, + string? segment = null, + Fallback fallback = default, + TValue? defaultValue = default) + where TModel : IPublishedElement { - /// - /// Gets the value of a property. - /// - public static TValue? ValueFor(this TModel model, IPublishedValueFallback publishedValueFallback, Expression> property, string? culture = null, string? segment = null, Fallback fallback = default, TValue? defaultValue = default) - where TModel : IPublishedElement + var alias = GetAlias(model, property); + return model.Value(publishedValueFallback, alias, culture, segment, fallback, defaultValue); + } + + // fixme that one should be public so ppl can use it + private static string GetAlias(TModel model, Expression> property) + { + if (property.NodeType != ExpressionType.Lambda) { - var alias = GetAlias(model, property); - return model.Value(publishedValueFallback, alias, culture, segment, fallback, defaultValue); + throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); } - // fixme that one should be public so ppl can use it - private static string GetAlias(TModel model, Expression> property) + var lambda = (LambdaExpression)property; + Expression lambdaBody = lambda.Body; + + if (lambdaBody.NodeType != ExpressionType.MemberAccess) { - if (property.NodeType != ExpressionType.Lambda) - throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); - - var lambda = (LambdaExpression) property; - var lambdaBody = lambda.Body; - - if (lambdaBody.NodeType != ExpressionType.MemberAccess) - throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); - - var memberExpression = (MemberExpression) lambdaBody; - if (memberExpression.Expression?.NodeType != ExpressionType.Parameter) - throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); - - var member = memberExpression.Member; - - var attribute = member.GetCustomAttribute(); - if (attribute == null) - throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - - return attribute.Alias; + throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); } + + var memberExpression = (MemberExpression)lambdaBody; + if (memberExpression.Expression?.NodeType != ExpressionType.Parameter) + { + throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); + } + + MemberInfo member = memberExpression.Member; + + ImplementPropertyTypeAttribute? attribute = member.GetCustomAttribute(); + if (attribute == null) + { + throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); + } + + return attribute.Alias; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs index b782751dd8..cfcbd82229 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs @@ -1,74 +1,77 @@ -using System; -using System.Linq; using System.Linq.Expressions; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// This is called from within the generated model classes +/// +/// +/// DO NOT REMOVE - although there are not code references this is used directly by the generated models. +/// +public static class PublishedModelUtility { - /// - /// This is called from within the generated model classes - /// - /// - /// DO NOT REMOVE - although there are not code references this is used directly by the generated models. - /// - public static class PublishedModelUtility + // looks safer but probably useless... ppl should not call these methods directly + // and if they do... they have to take care about not doing stupid things + + // public static PublishedPropertyType GetModelPropertyType2(Expression> selector) + // where T : PublishedContentModel + // { + // var type = typeof (T); + // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); + // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; + // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); + // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; + + // var contentType = PublishedContentType.Get(itemType, alias); + // // etc... + // } + public static IPublishedContentType? GetModelContentType( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + PublishedItemType itemType, + string alias) { - // looks safer but probably useless... ppl should not call these methods directly - // and if they do... they have to take care about not doing stupid things - - //public static PublishedPropertyType GetModelPropertyType2(Expression> selector) - // where T : PublishedContentModel - //{ - // var type = typeof (T); - // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); - // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; - // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); - // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; - - // var contentType = PublishedContentType.Get(itemType, alias); - // // etc... - //} - - public static IPublishedContentType? GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor, PublishedItemType itemType, string alias) + IPublishedSnapshot publishedSnapshot = publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + switch (itemType) { - var publishedSnapshot = publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - switch (itemType) - { - case PublishedItemType.Content: - return publishedSnapshot.Content?.GetContentType(alias); - case PublishedItemType.Media: - return publishedSnapshot.Media?.GetContentType(alias); - case PublishedItemType.Member: - return publishedSnapshot.Members?.GetContentType(alias); - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - } - - public static IPublishedPropertyType? GetModelPropertyType(IPublishedContentType contentType, Expression> selector) - //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel - { - // fixme therefore, missing a check on TModel here - - var expr = selector.Body as MemberExpression; - - if (expr == null) - throw new ArgumentException("Not a property expression.", nameof(selector)); - - // there _is_ a risk that contentType and T do not match - // see note above : accepted risk... - - var attr = expr.Member - .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) - .OfType() - .SingleOrDefault(); - - if (string.IsNullOrWhiteSpace(attr?.Alias)) - throw new InvalidOperationException($"Could not figure out property alias for property \"{expr.Member.Name}\"."); - - return contentType.GetPropertyType(attr.Alias); + case PublishedItemType.Content: + return publishedSnapshot.Content?.GetContentType(alias); + case PublishedItemType.Media: + return publishedSnapshot.Media?.GetContentType(alias); + case PublishedItemType.Member: + return publishedSnapshot.Members?.GetContentType(alias); + default: + throw new ArgumentOutOfRangeException(nameof(itemType)); } } + + public static IPublishedPropertyType? GetModelPropertyType( + IPublishedContentType contentType, + Expression> selector) + + // where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel + { + // fixme therefore, missing a check on TModel here + if (selector.Body is not MemberExpression expr) + { + throw new ArgumentException("Not a property expression.", nameof(selector)); + } + + // there _is_ a risk that contentType and T do not match + // see note above : accepted risk... + ImplementPropertyTypeAttribute? attr = expr.Member + .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) + .OfType() + .SingleOrDefault(); + + if (string.IsNullOrWhiteSpace(attr?.Alias)) + { + throw new InvalidOperationException( + $"Could not figure out property alias for property \"{expr.Member.Name}\"."); + } + + return contentType.GetPropertyType(attr.Alias); + } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs index fd4b4495d9..4a0fcdb0e7 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs @@ -1,80 +1,78 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyModel; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public class RoslynCompiler { - public class RoslynCompiler + public const string GeneratedAssemblyName = "ModelsGeneratedAssembly"; + + private readonly OutputKind _outputKind; + private readonly CSharpParseOptions _parseOptions; + private readonly IEnumerable _refs; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Roslyn compiler which can be used to compile a c# file to a Dll assembly + /// + public RoslynCompiler() { - public const string GeneratedAssemblyName = "ModelsGeneratedAssembly"; + _outputKind = OutputKind.DynamicallyLinkedLibrary; + _parseOptions = + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion + .Latest); // What languageversion should we default to? - private readonly OutputKind _outputKind; - private readonly CSharpParseOptions _parseOptions; - private readonly IEnumerable _refs; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Roslyn compiler which can be used to compile a c# file to a Dll assembly - /// - public RoslynCompiler() - { - _outputKind = OutputKind.DynamicallyLinkedLibrary; - _parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); // What languageversion should we default to? - - // In order to dynamically compile the assembly, we need to add all refs from our current - // application. This will also add the correct framework dependencies and we won't have to worry - // about the specific framework that is currently being run. - // This was borrowed from: https://github.com/dotnet/core/issues/2082#issuecomment-442713181 - // because we were running into the same error as that thread because we were either: - // - not adding enough of the runtime dependencies OR - // - we were explicitly adding the wrong runtime dependencies - // ... at least that the gist of what I can tell. - MetadataReference[] refs = - DependencyContext.Default.CompileLibraries + // In order to dynamically compile the assembly, we need to add all refs from our current + // application. This will also add the correct framework dependencies and we won't have to worry + // about the specific framework that is currently being run. + // This was borrowed from: https://github.com/dotnet/core/issues/2082#issuecomment-442713181 + // because we were running into the same error as that thread because we were either: + // - not adding enough of the runtime dependencies OR + // - we were explicitly adding the wrong runtime dependencies + // ... at least that the gist of what I can tell. + MetadataReference[] refs = + DependencyContext.Default.CompileLibraries .SelectMany(cl => cl.ResolveReferencePaths()) .Select(asm => MetadataReference.CreateFromFile(asm)) .ToArray(); - _refs = refs.ToList(); - } + _refs = refs.ToList(); + } - /// - /// Compile a source file to a dll - /// - /// Path to the source file containing the code to be compiled. - /// The path where the output assembly will be saved. - public void CompileToFile(string pathToSourceFile, string savePath) + /// + /// Compile a source file to a dll + /// + /// Path to the source file containing the code to be compiled. + /// The path where the output assembly will be saved. + public void CompileToFile(string pathToSourceFile, string savePath) + { + var sourceCode = File.ReadAllText(pathToSourceFile); + + var sourceText = SourceText.From(sourceCode); + + SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); + + // Not entirely certain that assemblyIdentityComparer is nececary? + var compilation = CSharpCompilation.Create( + GeneratedAssemblyName, + new[] { syntaxTree }, + _refs, + new CSharpCompilationOptions( + _outputKind, + optimizationLevel: OptimizationLevel.Release, + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); + + EmitResult emitResult = compilation.Emit(savePath); + + if (!emitResult.Success) { - var sourceCode = File.ReadAllText(pathToSourceFile); - - var sourceText = SourceText.From(sourceCode); - - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); - - var compilation = CSharpCompilation.Create( - GeneratedAssemblyName, - new[] { syntaxTree }, - references: _refs, - options: new CSharpCompilationOptions( - _outputKind, - optimizationLevel: OptimizationLevel.Release, - // Not entirely certain that assemblyIdentityComparer is nececary? - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); - - var emitResult = compilation.Emit(savePath); - - if (!emitResult.Success) - { - throw new InvalidOperationException("Roslyn compiler could not create ModelsBuilder dll:\n" + - string.Join("\n", emitResult.Diagnostics.Select(x=>x.GetMessage()))); - } + throw new InvalidOperationException("Roslyn compiler could not create ModelsBuilder dll:\n" + + string.Join("\n", emitResult.Diagnostics.Select(x => x.GetMessage()))); } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs index 5d3187c707..7f8b029284 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs @@ -1,22 +1,21 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +internal static class TypeExtensions { - internal static class TypeExtensions + /// + /// Creates a generic instance of a generic type with the proper actual type of an object. + /// + /// A generic type such as Something{} + /// An object whose type is used as generic type param. + /// Arguments for the constructor. + /// A generic instance of the generic type with the proper type. + /// + /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return + /// a Something{Type1} if object1.GetType() is Type1. + /// + public static object? CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) { - /// - /// Creates a generic instance of a generic type with the proper actual type of an object. - /// - /// A generic type such as Something{} - /// An object whose type is used as generic type param. - /// Arguments for the constructor. - /// A generic instance of the generic type with the proper type. - /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return - /// a Something{Type1} if object1.GetType() is Type1. - public static object? CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) - { - var type = genericType.MakeGenericType(typeParmObj.GetType()); - return Activator.CreateInstance(type, ctorArgs); - } + Type type = genericType.MakeGenericType(typeParmObj.GetType()); + return Activator.CreateInstance(type, ctorArgs); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs index 8d096ee9e2..3711cd89c2 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -9,200 +6,236 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public sealed class UmbracoServices { + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; + private readonly IShortStringHelper _shortStringHelper; - public sealed class UmbracoServices + /// + /// Initializes a new instance of the class. + /// + public UmbracoServices( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedContentTypeFactory publishedContentTypeFactory, + IShortStringHelper shortStringHelper) { - private readonly IContentTypeService _contentTypeService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IMemberTypeService _memberTypeService; - private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; - private readonly IShortStringHelper _shortStringHelper; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _publishedContentTypeFactory = publishedContentTypeFactory; + _shortStringHelper = shortStringHelper; + } - /// - /// Initializes a new instance of the class. - /// - public UmbracoServices( - IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, - IMemberTypeService memberTypeService, - IPublishedContentTypeFactory publishedContentTypeFactory, - IShortStringHelper shortStringHelper) + public static string GetClrName(IShortStringHelper shortStringHelper, string? name, string alias) => + + // ModelsBuilder's legacy - but not ideal + alias.ToCleanString(shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); + + #region Services + + public IList GetAllTypes() + { + var types = new List(); + + // TODO: this will require 3 rather large SQL queries on startup in ModelsMode.InMemoryAuto mode. I know that these will be cached after lookup but it will slow + // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can + // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup + // in more than one place. + types.AddRange(GetTypes( + PublishedItemType.Content, + _contentTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes( + PublishedItemType.Media, + _mediaTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes( + PublishedItemType.Member, + _memberTypeService.GetAll().Cast().ToArray())); + + return EnsureDistinctAliases(types); + } + + public IList GetContentTypes() + { + IContentTypeComposition[] contentTypes = _contentTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here + } + + public IList GetMediaTypes() + { + IContentTypeComposition[] contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here + } + + public IList GetMemberTypes() + { + IContentTypeComposition[] memberTypes = _memberTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here + } + + internal static IList EnsureDistinctAliases(IList typeModels) + { + IEnumerable> groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); + foreach (IGrouping group in groups.Where(x => x.Count() > 1)) { - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _memberTypeService = memberTypeService; - _publishedContentTypeFactory = publishedContentTypeFactory; - _shortStringHelper = shortStringHelper; + throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" + + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." + + " One of the aliases must be modified in order to use the ModelsBuilder."); } - #region Services + return typeModels; + } - public IList GetAllTypes() + private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) + { + var typeModels = new List(); + var uniqueTypes = new HashSet(); + + // get the types and the properties + foreach (IContentTypeComposition contentType in contentTypes) { - var types = new List(); - - // TODO: this will require 3 rather large SQL queries on startup in ModelsMode.InMemoryAuto mode. I know that these will be cached after lookup but it will slow - // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can - // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup - // in more than one place. - types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); - - return EnsureDistinctAliases(types); - } - - public IList GetContentTypes() - { - var contentTypes = _contentTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here - } - - public IList GetMediaTypes() - { - var contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here - } - - public IList GetMemberTypes() - { - var memberTypes = _memberTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here - } - - public static string GetClrName(IShortStringHelper shortStringHelper, string? name, string alias) - { - // ModelsBuilder's legacy - but not ideal - return alias.ToCleanString(shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); - } - - private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) - { - var typeModels = new List(); - var uniqueTypes = new HashSet(); - - // get the types and the properties - foreach (var contentType in contentTypes) + var typeModel = new TypeModel { - var typeModel = new TypeModel - { - Id = contentType.Id, - Alias = contentType.Alias, - ClrName = GetClrName(_shortStringHelper, contentType.Name, contentType.Alias), - ParentId = contentType.ParentId, + Id = contentType.Id, + Alias = contentType.Alias, + ClrName = GetClrName(_shortStringHelper, contentType.Name, contentType.Alias), + ParentId = contentType.ParentId, + Name = contentType.Name, + Description = contentType.Description, + }; - Name = contentType.Name, - Description = contentType.Description + // of course this should never happen, but when it happens, better detect it + // else we end up with weird nullrefs everywhere + if (uniqueTypes.Contains(typeModel.ClrName)) + { + throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); + } + + uniqueTypes.Add(typeModel.ClrName); + + IPublishedContentType publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); + switch (itemType) + { + case PublishedItemType.Content: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Content; + break; + case PublishedItemType.Media: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Media; + break; + case PublishedItemType.Member: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Member; + break; + default: + throw new InvalidOperationException(string.Format( + "Unsupported PublishedItemType \"{0}\".", + itemType)); + } + + typeModels.Add(typeModel); + + foreach (IPropertyType propertyType in contentType.PropertyTypes) + { + var propertyModel = new PropertyModel + { + Alias = propertyType.Alias, + ClrName = GetClrName(_shortStringHelper, propertyType.Name, propertyType.Alias), + Name = propertyType.Name, + Description = propertyType.Description, }; - // of course this should never happen, but when it happens, better detect it - // else we end up with weird nullrefs everywhere - if (uniqueTypes.Contains(typeModel.ClrName)) - throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); - uniqueTypes.Add(typeModel.ClrName); - - var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); - switch (itemType) + IPublishedPropertyType? publishedPropertyType = + publishedContentType.GetPropertyType(propertyType.Alias); + if (publishedPropertyType == null) { - case PublishedItemType.Content: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Content; - break; - case PublishedItemType.Media: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Media; - break; - case PublishedItemType.Member: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Member; - break; - default: - throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); + throw new PanicException( + $"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); } - typeModels.Add(typeModel); + propertyModel.ModelClrType = publishedPropertyType.ModelClrType; - foreach (var propertyType in contentType.PropertyTypes) - { - var propertyModel = new PropertyModel - { - Alias = propertyType.Alias, - ClrName = GetClrName(_shortStringHelper, propertyType.Name, propertyType.Alias), - - Name = propertyType.Name, - Description = propertyType.Description - }; - - var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); - if (publishedPropertyType == null) - throw new PanicException($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); - - propertyModel.ModelClrType = publishedPropertyType.ModelClrType; - - typeModel.Properties.Add(propertyModel); - } + typeModel.Properties.Add(propertyModel); } - - // wire the base types - foreach (var typeModel in typeModels.Where(x => x.ParentId > 0)) - { - typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); - // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might - // not be a content type - here we assume that BaseType being null while ParentId > 0 means that - // the parent is a container (and we don't check). - typeModel.IsParent = typeModel.BaseType != null; - } - - // discover mixins - foreach (var contentType in contentTypes) - { - var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); - if (typeModel == null) throw new PanicException("Panic: no type model matching content type."); - - IEnumerable compositionTypes; - var contentTypeAsMedia = contentType as IMediaType; - var contentTypeAsContent = contentType as IContentType; - var contentTypeAsMember = contentType as IMemberType; - if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; - else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; - else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; - else throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); - - foreach (var compositionType in compositionTypes) - { - var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); - if (compositionModel == null) throw new PanicException("Panic: composition type does not exist."); - - if (compositionType.Id == contentType.ParentId) continue; - - // add to mixins - typeModel.MixinTypes.Add(compositionModel); - - // mark as mixin - as well as parents - compositionModel.IsMixin = true; - while ((compositionModel = compositionModel.BaseType) != null) - compositionModel.IsMixin = true; - } - } - - return typeModels; } - internal static IList EnsureDistinctAliases(IList typeModels) + // wire the base types + foreach (TypeModel typeModel in typeModels.Where(x => x.ParentId > 0)) { - var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); - foreach (var group in groups.Where(x => x.Count() > 1)) - throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" - + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." - + " One of the aliases must be modified in order to use the ModelsBuilder."); - return typeModels; + typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); + + // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might + // not be a content type - here we assume that BaseType being null while ParentId > 0 means that + // the parent is a container (and we don't check). + typeModel.IsParent = typeModel.BaseType != null; } - #endregion + // discover mixins + foreach (IContentTypeComposition contentType in contentTypes) + { + TypeModel? typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); + if (typeModel == null) + { + throw new PanicException("Panic: no type model matching content type."); + } + + IEnumerable compositionTypes; + if (contentType is IMediaType contentTypeAsMedia) + { + compositionTypes = contentTypeAsMedia.ContentTypeComposition; + } + else if (contentType is IContentType contentTypeAsContent) + { + compositionTypes = contentTypeAsContent.ContentTypeComposition; + } + else if (contentType is IMemberType contentTypeAsMember) + { + compositionTypes = contentTypeAsMember.ContentTypeComposition; + } + else + { + throw new PanicException(string.Format( + "Panic: unsupported type \"{0}\".", + contentType.GetType().FullName)); + } + + foreach (IContentTypeComposition compositionType in compositionTypes) + { + TypeModel? compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); + if (compositionModel == null) + { + throw new PanicException("Panic: composition type does not exist."); + } + + if (compositionType.Id == contentType.ParentId) + { + continue; + } + + // add to mixins + typeModel.MixinTypes.Add(compositionModel); + + // mark as mixin - as well as parents + compositionModel.IsMixin = true; + while ((compositionModel = compositionModel.BaseType) != null) + { + compositionModel.IsMixin = true; + } + } + } + + return typeModels; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs index fef8bfeba6..3e3c2cfae1 100644 --- a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; @@ -9,43 +8,52 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +/// +/// Used to automatically indicate that a package has an embedded package data manifest that needs to be installed +/// +public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan { - /// - /// Used to automatically indicate that a package has an embedded package data manifest that needs to be installed - /// - public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan + protected AutomaticPackageMigrationPlan(string packageName) + : this(packageName, packageName) { - protected AutomaticPackageMigrationPlan(string packageName) - : this(packageName, packageName) - { } + } - protected AutomaticPackageMigrationPlan(string packageName, string planName) - : base(packageName, planName) - { } + protected AutomaticPackageMigrationPlan(string packageName, string planName) + : base(packageName, planName) + { + } - protected sealed override void DefinePlan() + protected sealed override void DefinePlan() + { + // calculate the final state based on the hash value of the embedded resource + Type planType = GetType(); + var hash = PackageMigrationResource.GetEmbeddedPackageDataManifestHash(planType); + + var finalId = hash.ToGuid(); + To(finalId); + } + + private class MigrateToPackageData : PackageMigrationBase + { + public MigrateToPackageData( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, IOptions options) + : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context, options) { - // calculate the final state based on the hash value of the embedded resource - Type planType = GetType(); - var hash = PackageMigrationResource.GetEmbeddedPackageDataManifestHash(planType); - - var finalId = hash.ToGuid(); - To(finalId); } - private class MigrateToPackageData : PackageMigrationBase + protected override void Migrate() { - public MigrateToPackageData(IPackagingService packagingService, IMediaService mediaService, MediaFileManager mediaFileManager, MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IMigrationContext context, IOptions options) : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context, options) - { - } + var plan = (AutomaticPackageMigrationPlan)Context.Plan; - protected override void Migrate() - { - var plan = (AutomaticPackageMigrationPlan)Context.Plan; - - ImportPackage.FromEmbeddedResource(plan.GetType()).Do(); - } + ImportPackage.FromEmbeddedResource(plan.GetType()).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs index 994ac643c6..f826dd9dfe 100644 --- a/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs @@ -1,17 +1,15 @@ -using System; using System.Xml.Linq; using Umbraco.Cms.Infrastructure.Migrations.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public interface IImportPackageBuilder : IFluentBuilder { - public interface IImportPackageBuilder : IFluentBuilder - { - IExecutableBuilder FromEmbeddedResource() - where TPackageMigration : PackageMigrationBase; + IExecutableBuilder FromEmbeddedResource() + where TPackageMigration : PackageMigrationBase; - IExecutableBuilder FromEmbeddedResource(Type packageMigrationType); + IExecutableBuilder FromEmbeddedResource(Type packageMigrationType); - IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest); - } + IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest); } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs index fef61a54c3..8b28628e4c 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Xml.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -10,50 +9,50 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +internal class ImportPackageBuilder : ExpressionBuilderBase, IImportPackageBuilder, + IExecutableBuilder { - internal class ImportPackageBuilder : ExpressionBuilderBase, IImportPackageBuilder, IExecutableBuilder + public ImportPackageBuilder( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions options) + : base(new ImportPackageBuilderExpression( + packagingService, + mediaService, + mediaFileManager, + mediaUrlGenerators, + shortStringHelper, + contentTypeBaseServiceProvider, + context, + options)) { - public ImportPackageBuilder( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions options) - : base(new ImportPackageBuilderExpression( - packagingService, - mediaService, - mediaFileManager, - mediaUrlGenerators, - shortStringHelper, - contentTypeBaseServiceProvider, - context, - options)) - { - } + } - public void Do() => Expression.Execute(); + public void Do() => Expression.Execute(); - public IExecutableBuilder FromEmbeddedResource() - where TPackageMigration : PackageMigrationBase - { - Expression.EmbeddedResourceMigrationType = typeof(TPackageMigration); - return this; - } + public IExecutableBuilder FromEmbeddedResource() + where TPackageMigration : PackageMigrationBase + { + Expression.EmbeddedResourceMigrationType = typeof(TPackageMigration); + return this; + } - public IExecutableBuilder FromEmbeddedResource(Type packageMigrationType) - { - Expression.EmbeddedResourceMigrationType = packageMigrationType; - return this; - } + public IExecutableBuilder FromEmbeddedResource(Type packageMigrationType) + { + Expression.EmbeddedResourceMigrationType = packageMigrationType; + return this; + } - public IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest) - { - Expression.PackageDataManifest = packageDataManifest; - return this; - } + public IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest) + { + Expression.PackageDataManifest = packageDataManifest; + return this; } } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs index 04abcfa8a0..0e4bf757e8 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Microsoft.Extensions.Logging; @@ -17,136 +14,137 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +internal class ImportPackageBuilderExpression : MigrationExpressionBase { - internal class ImportPackageBuilderExpression : MigrationExpressionBase + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly PackageMigrationSettings _packageMigrationSettings; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; + + private bool _executed; + + public ImportPackageBuilderExpression( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions packageMigrationSettings) + : base(context) { - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly MediaFileManager _mediaFileManager; - private readonly IMediaService _mediaService; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IPackagingService _packagingService; - private readonly IShortStringHelper _shortStringHelper; - private readonly PackageMigrationSettings _packageMigrationSettings; + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationSettings = packageMigrationSettings.Value; + } - private bool _executed; + /// + /// The type of the migration which dictates the namespace of the embedded resource + /// + public Type? EmbeddedResourceMigrationType { get; set; } - public ImportPackageBuilderExpression( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions packageMigrationSettings) : base(context) + public XDocument? PackageDataManifest { get; set; } + + public override void Execute() + { + if (_executed) { - _packagingService = packagingService; - _mediaService = mediaService; - _mediaFileManager = mediaFileManager; - _mediaUrlGenerators = mediaUrlGenerators; - _shortStringHelper = shortStringHelper; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _packageMigrationSettings = packageMigrationSettings.Value; + throw new InvalidOperationException("This expression has already been executed."); } - /// - /// The type of the migration which dictates the namespace of the embedded resource - /// - public Type? EmbeddedResourceMigrationType { get; set; } + _executed = true; - public XDocument? PackageDataManifest { get; set; } + Context.BuildingExpression = false; - public override void Execute() + if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) { - if (_executed) - { - throw new InvalidOperationException("This expression has already been executed."); - } + throw new InvalidOperationException( + $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); + } - _executed = true; + if (!_packageMigrationSettings.RunSchemaAndContentMigrations) + { + Logger.LogInformation("Skipping import of embedded schema file, due to configuration"); + return; + } - Context.BuildingExpression = false; - - if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) - { - throw new InvalidOperationException( - $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); - } - - if (!_packageMigrationSettings.RunSchemaAndContentMigrations) - { - Logger.LogInformation("Skipping import of embedded schema file, due to configuration"); - return; - } - - InstallationSummary installationSummary; - if (EmbeddedResourceMigrationType != null) - { - if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( + InstallationSummary installationSummary; + if (EmbeddedResourceMigrationType != null) + { + if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( EmbeddedResourceMigrationType, - out XDocument? xml, out ZipArchive? zipPackage)) + out XDocument? xml, + out ZipArchive? zipPackage)) + { + // first install the package + installationSummary = _packagingService.InstallCompiledPackageData(xml!); + + if (zipPackage is not null) { - // first install the package - installationSummary = _packagingService.InstallCompiledPackageData(xml!); - - if (zipPackage is not null) + // get the embedded resource + using (zipPackage) { - // get the embedded resource - using (zipPackage) + // then we need to save each file to the saved media items + var mediaWithFiles = xml!.XPathSelectElements( + "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") + .ToDictionary( + x => x.AttributeValue("key"), + x => x.AttributeValue("mediaFilePath")); + + // Any existing media by GUID will not be installed by the package service, it will just be skipped + // so you cannot 'update' media (or content) using a package since those are not schema type items. + // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled + // will be empty for any existing media which means that the files will also not be updated. + foreach (IMedia media in installationSummary.MediaInstalled) { - // then we need to save each file to the saved media items - var mediaWithFiles = xml!.XPathSelectElements( - "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") - .ToDictionary( - x => x.AttributeValue("key"), - x => x.AttributeValue("mediaFilePath")); - - // Any existing media by GUID will not be installed by the package service, it will just be skipped - // so you cannot 'update' media (or content) using a package since those are not schema type items. - // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled - // will be empty for any existing media which means that the files will also not be updated. - foreach (IMedia media in installationSummary.MediaInstalled) + if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) { - if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + // this is a media item that has a file, so find that file in the zip + var entryPath = $"media{mediaFilePath!.EnsureStartsWith('/')}"; + ZipArchiveEntry? mediaEntry = zipPackage.GetEntry(entryPath); + if (mediaEntry == null) { - // this is a media item that has a file, so find that file in the zip - var entryPath = $"media{mediaFilePath!.EnsureStartsWith('/')}"; - ZipArchiveEntry? mediaEntry = zipPackage.GetEntry(entryPath); - if (mediaEntry == null) - { - throw new InvalidOperationException( - "No media file found in package zip for path " + - entryPath); - } - - // read the media file and save it to the media item - // using the current file system provider. - using (Stream mediaStream = mediaEntry.Open()) - { - media.SetValue( - _mediaFileManager, - _mediaUrlGenerators, - _shortStringHelper, - _contentTypeBaseServiceProvider, - Constants.Conventions.Media.File, - Path.GetFileName(mediaFilePath)!, - mediaStream); - } - - _mediaService.Save(media); + throw new InvalidOperationException( + "No media file found in package zip for path " + + entryPath); } + + // read the media file and save it to the media item + // using the current file system provider. + using (Stream mediaStream = mediaEntry.Open()) + { + media.SetValue( + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Constants.Conventions.Media.File, + Path.GetFileName(mediaFilePath)!, + mediaStream); + } + + _mediaService.Save(media); } } } } - else - { - installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); - } - - Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); } + else + { + installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); + } + + Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index a45c26a44d..192d1a51c3 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -104,7 +104,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging contentTypeService, contentService, propertyEditors, - scopeProvider, + (Umbraco.Cms.Infrastructure.Scoping.IScopeProvider) scopeProvider, shortStringHelper, serializer, mediaService, @@ -115,7 +115,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId) { - using (var scope = _scopeProvider.CreateScope()) + using (IScope scope = _scopeProvider.CreateScope()) { var installationSummary = new InstallationSummary(compiledPackage.Name) { @@ -200,10 +200,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// /// Imports and saves package xml as /// - /// Xml to import + /// The root contents to import from + /// The content type base service /// Optional parent Id for the content being imported /// A dictionary of already imported document types (basically used as a cache) /// Optional Id of the user performing the import + /// The content service base /// An enumerable list of generated content public IEnumerable ImportContentBase( IEnumerable roots, @@ -267,8 +269,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging importedContentTypes.Add(contentTypeAlias, contentType); } - if (TryCreateContentFromXml(root, importedContentTypes[contentTypeAlias], null, parentId, service, - out var content)) + if (TryCreateContentFromXml(root, importedContentTypes[contentTypeAlias], null, parentId, service, out TContentBase content)) { contents.Add(content); } @@ -296,19 +297,19 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var list = new List(); - foreach (var child in children) + foreach (XElement child in children) { string contentTypeAlias = child.Name.LocalName; if (importedContentTypes.ContainsKey(contentTypeAlias) == false) { - var contentType = FindContentTypeByAlias(contentTypeAlias, typeService); + TContentTypeComposition contentType = FindContentTypeByAlias(contentTypeAlias, typeService); importedContentTypes.Add(contentTypeAlias, contentType); } // Create and add the child to the list if (TryCreateContentFromXml(child, importedContentTypes[contentTypeAlias], parent, default, service, - out var content)) + out TContentBase content)) { list.Add(content); } @@ -338,7 +339,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging Guid key = element.RequiredAttributeValue("key"); // we need to check if the content already exists and if so we ignore the installation for this item - var value = service.GetById(key); + TContentBase? value = service.GetById(key); if (value != null) { output = value; @@ -348,16 +349,15 @@ namespace Umbraco.Cms.Infrastructure.Packaging var level = element.Attribute("level")?.Value ?? string.Empty; var sortOrder = element.Attribute("sortOrder")?.Value ?? string.Empty; var nodeName = element.Attribute("nodeName")?.Value ?? string.Empty; - var path = element.Attribute("path")?.Value; var templateId = element.AttributeValue("template"); - var properties = from property in element.Elements() + IEnumerable? properties = from property in element.Elements() where property.Attribute("isDoc") == null select property; //TODO: This will almost never work, we can't reference a template by an INT Id within a package manifest, we need to change the // packager to package templates by UDI and resolve by the same, in 98% of cases, this isn't going to work, or it will resolve the wrong template. - var template = templateId.HasValue ? _fileService.GetTemplate(templateId.Value) : null; + ITemplate? template = templateId.HasValue ? _fileService.GetTemplate(templateId.Value) : null; //now double check this is correct since its an INT it could very well be pointing to an invalid template :/ if (template != null && contentType is IContentType contentTypex) @@ -390,7 +390,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging // Get the installed culture iso names, we create a localized content node with a culture that does not exist in the project // We have to use Invariant comparisons, because when we get them from ContentBase in EntityXmlSerializer they're all lowercase. var installedLanguages = _localizationService.GetAllLanguages().Select(l => l.IsoCode).ToArray(); - foreach (var localizedNodeName in element.Attributes() + foreach (XAttribute localizedNodeName in element.Attributes() .Where(a => a.Name.LocalName.InvariantStartsWith(nodeNamePrefix))) { var newCulture = localizedNodeName.Name.LocalName.Substring(nodeNamePrefix.Length); @@ -403,12 +403,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging //Here we make sure that we take composition properties in account as well //otherwise we would skip them and end up losing content - var propTypes = contentType.CompositionPropertyTypes.Any() + Dictionary propTypes = contentType.CompositionPropertyTypes.Any() ? contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x) : contentType.PropertyTypes.ToDictionary(x => x.Alias, x => x); var foundLanguages = new HashSet(); - foreach (var property in properties) + foreach (XElement property in properties) { string propertyTypeAlias = property.Name.LocalName; if (content.HasProperty(propertyTypeAlias)) @@ -418,7 +418,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging // Handle properties language attributes var propertyLang = property.Attribute(XName.Get("lang"))?.Value ?? null; foundLanguages.Add(propertyLang); - if (propTypes.TryGetValue(propertyTypeAlias, out var propertyType)) + if (propTypes.TryGetValue(propertyTypeAlias, out _)) { // set property value // Skip unsupported language variation, otherwise we'll get a "not supported error" @@ -522,9 +522,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Xml to import /// Boolean indicating whether or not to import the /// Optional id of the User performing the operation. Default is zero (admin). + /// The content type service. /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, - bool importStructure, int userId, IContentTypeBaseService service) + public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId, IContentTypeBaseService service) where T : class, IContentTypeComposition => ImportDocumentTypes(unsortedDocumentTypes, importStructure, userId, service); @@ -534,10 +534,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Xml to import /// Boolean indicating whether or not to import the /// Optional id of the User performing the operation. Default is zero (admin). + /// The content type service /// Collection of entity containers installed by the package to be populated with those created in installing data types. /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, - bool importStructure, int userId, IContentTypeBaseService service, + public IReadOnlyList ImportDocumentTypes( + IReadOnlyCollection unsortedDocumentTypes, + bool importStructure, int userId, + IContentTypeBaseService service, out IEnumerable entityContainersInstalled) where T : class, IContentTypeComposition { @@ -548,17 +551,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); var isSingleDocTypeImport = unsortedDocumentTypes.Count == 1; - var importedFolders = + Dictionary importedFolders = CreateContentTypeFolderStructure(unsortedDocumentTypes, out entityContainersInstalled); if (isSingleDocTypeImport == false) { //NOTE Here we sort the doctype XElements based on dependencies //before creating the doc types - this should also allow for a better structure/inheritance support. - foreach (var documentType in unsortedDocumentTypes) + foreach (XElement documentType in unsortedDocumentTypes) { - var elementCopy = documentType; - var infoElement = elementCopy.Element("Info"); + XElement elementCopy = documentType; + XElement? infoElement = elementCopy.Element("Info"); var dependencies = new HashSet(); //Add the Master as a dependency @@ -568,13 +571,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging } //Add compositions as dependencies - var compositionsElement = infoElement?.Element("Compositions"); + XElement? compositionsElement = infoElement?.Element("Compositions"); if (compositionsElement != null && compositionsElement.HasElements) { - var compositions = compositionsElement.Elements("Composition"); + IEnumerable? compositions = compositionsElement.Elements("Composition").ToArray(); if (compositions.Any()) { - foreach (var composition in compositions) + foreach (XElement composition in compositions) { dependencies.Add(composition.Value); } @@ -606,9 +609,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging } } - foreach (var contentType in importedContentTypes) + foreach (KeyValuePair contentType in importedContentTypes) { - var ct = contentType.Value; + T ct = contentType.Value; if (importedFolders.ContainsKey(ct.Alias)) { ct.ParentId = importedFolders[ct.Alias]; @@ -625,15 +628,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var updatedContentTypes = new List(); //Update the structure here - we can't do it until all DocTypes have been created - foreach (var documentType in documentTypes) + foreach (XElement documentType in documentTypes) { var alias = documentType.Element("Info")?.Element("Alias")?.Value; - var structureElement = documentType.Element("Structure"); + XElement? structureElement = documentType.Element("Structure"); //Ensure that we only update ContentTypes which has actual structure-elements if (structureElement == null || structureElement.Elements().Any() == false || alias is null) + { continue; + } - var updated = UpdateContentTypesStructure(importedContentTypes[alias], structureElement, + T updated = UpdateContentTypesStructure(importedContentTypes[alias], structureElement, importedContentTypes, service); updatedContentTypes.Add(updated); } @@ -714,7 +719,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging for (var i = 1; i < folders.Length; i++) { var folderName = WebUtility.UrlDecode(folders[i]); - Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; + Guid? folderKey = folderKeys.Length == folders.Length ? folderKeys[i] : null; current = CreateContentTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); trackEntityContainersInstalled.Add(current!); importedFolders[alias!] = current!.Id; @@ -728,7 +733,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private EntityContainer? CreateContentTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) { - var children = _entityService.GetChildren(current.Id).ToArray(); + IEntitySlim[] children = _entityService.GetChildren(current.Id).ToArray(); var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); if (found) { @@ -736,7 +741,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging return _contentTypeService.GetContainer(containerId); } - var tryCreateFolder = _contentTypeService.CreateContainer(current.Id, folderKey, folderName); + Attempt?> tryCreateFolder = _contentTypeService.CreateContainer(current.Id, folderKey, folderName); if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); @@ -769,7 +774,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging } var alias = infoElement?.Element("Alias")?.Value; - var contentType = CreateContentType(key, parent, -1, alias!); + T? contentType = CreateContentType(key, parent, -1, alias!); if (parent != null) { @@ -818,8 +823,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value); - var infoElement = documentType.Element("Info"); - var defaultTemplateElement = infoElement?.Element("DefaultTemplate"); + XElement? infoElement = documentType.Element("Info"); + XElement? defaultTemplateElement = infoElement?.Element("DefaultTemplate"); if (contentType is null) { @@ -828,31 +833,42 @@ namespace Umbraco.Cms.Infrastructure.Packaging contentType.Key = key; contentType.Name = infoElement!.Element("Name")!.Value; if (infoElement.Element("Key") != null) + { contentType.Key = new Guid(infoElement.Element("Key")!.Value); + } + contentType.Icon = infoElement.Element("Icon")?.Value; contentType.Thumbnail = infoElement.Element("Thumbnail")?.Value; contentType.Description = infoElement.Element("Description")?.Value; //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it. - var allowAtRoot = infoElement.Element("AllowAtRoot"); + XElement? allowAtRoot = infoElement.Element("AllowAtRoot"); if (allowAtRoot != null) + { contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true"); + } - var isListView = infoElement.Element("IsListView"); + XElement? isListView = infoElement.Element("IsListView"); if (isListView != null) + { contentType.IsContainer = isListView.Value.InvariantEquals("true"); + } - var isElement = infoElement.Element("IsElement"); + XElement? isElement = infoElement.Element("IsElement"); if (isElement != null) + { contentType.IsElement = isElement.Value.InvariantEquals("true"); + } - var variationsElement = infoElement.Element("Variations"); + XElement? variationsElement = infoElement.Element("Variations"); if (variationsElement != null) + { contentType.Variations = (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value); + } //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set - var masterElement = infoElement.Element("Master"); + XElement? masterElement = infoElement.Element("Master"); if (masterElement != null) { var masterAlias = masterElement.Value; @@ -864,16 +880,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging } //Update Compositions on the ContentType to ensure that they are as is defined in the package xml - var compositionsElement = infoElement.Element("Compositions"); + XElement? compositionsElement = infoElement.Element("Compositions"); if (compositionsElement != null && compositionsElement.HasElements) { - var compositions = compositionsElement.Elements("Composition"); + XElement[] compositions = compositionsElement.Elements("Composition").ToArray(); if (compositions.Any()) { - foreach (var composition in compositions) + foreach (XElement composition in compositions) { var compositionAlias = composition.Value; - var compositionContentType = importedContentTypes.ContainsKey(compositionAlias) + T? compositionContentType = importedContentTypes.ContainsKey(compositionAlias) ? importedContentTypes[compositionAlias] : service.Get(compositionAlias); contentType.AddContentType(compositionContentType); @@ -937,14 +953,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (allowedTemplatesElement != null && allowedTemplatesElement.Elements("Template").Any()) { var allowedTemplates = contentType.AllowedTemplates?.ToList(); - foreach (var templateElement in allowedTemplatesElement.Elements("Template")) + foreach (XElement templateElement in allowedTemplatesElement.Elements("Template")) { var alias = templateElement.Value; - var template = _fileService.GetTemplate(alias.ToSafeAlias(_shortStringHelper)); + ITemplate? template = _fileService.GetTemplate(alias.ToSafeAlias(_shortStringHelper)); if (template != null) { if (allowedTemplates?.Any(x => x.Id == template.Id) ?? true) + { continue; + } + allowedTemplates.Add(template); } else @@ -960,7 +979,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (string.IsNullOrEmpty((string?)defaultTemplateElement) == false) { - var defaultTemplate = + ITemplate? defaultTemplate = _fileService.GetTemplate(defaultTemplateElement.Value.ToSafeAlias(_shortStringHelper)); if (defaultTemplate != null) { @@ -979,10 +998,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging where T : IContentTypeComposition { if (propertyGroupsContainer == null) + { return; + } - var propertyGroupElements = propertyGroupsContainer.Elements("Tab"); - foreach (var propertyGroupElement in propertyGroupElements) + IEnumerable propertyGroupElements = propertyGroupsContainer.Elements("Tab"); + foreach (XElement propertyGroupElement in propertyGroupElements) { var name = propertyGroupElement.Element("Caption")! .Value; // TODO Rename to Name (same in EntityXmlSerializer) @@ -994,14 +1015,14 @@ namespace Umbraco.Cms.Infrastructure.Packaging } contentType.AddPropertyGroup(alias, name); - var propertyGroup = contentType.PropertyGroups[alias]; + PropertyGroup propertyGroup = contentType.PropertyGroups[alias]; - if (Guid.TryParse(propertyGroupElement.Element("Key")?.Value, out var key)) + if (Guid.TryParse(propertyGroupElement.Element("Key")?.Value, out Guid key)) { propertyGroup.Key = key; } - if (Enum.TryParse(propertyGroupElement.Element("Type")?.Value, out var type)) + if (Enum.TryParse(propertyGroupElement.Element("Type")?.Value, out PropertyGroupType type)) { propertyGroup.Type = type; } @@ -1022,13 +1043,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging { return; } - var properties = genericPropertiesElement.Elements("GenericProperty"); - foreach (var property in properties) + IEnumerable properties = genericPropertiesElement.Elements("GenericProperty"); + foreach (XElement property in properties) { var dataTypeDefinitionId = new Guid(property.Element("Definition")!.Value); //Unique Id for a DataTypeDefinition - var dataTypeDefinition = _dataTypeService.GetDataType(dataTypeDefinitionId); + IDataType? dataTypeDefinition = _dataTypeService.GetDataType(dataTypeDefinitionId); //If no DataTypeDefinition with the guid from the xml wasn't found OR the ControlId on the DataTypeDefinition didn't match the DataType Id //We look up a DataTypeDefinition that matches @@ -1042,7 +1063,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (dataTypeDefinition == null) { - var dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); + IDataType[]? dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias).ToArray(); if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) { dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); @@ -1050,7 +1071,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging } else if (dataTypeDefinition.EditorAlias != propertyEditorAlias) { - var dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); + IDataType[]? dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias).ToArray(); if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) { dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); @@ -1071,11 +1092,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging .FirstOrDefault(); //if for some odd reason this isn't there then ignore if (dataTypeDefinition == null) + { continue; + } } var sortOrder = 0; - var sortOrderElement = property.Element("SortOrder"); + XElement? sortOrderElement = property.Element("SortOrder"); if (sortOrderElement != null) { int.TryParse(sortOrderElement.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, @@ -1108,7 +1131,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging propertyType.Key = new Guid(property.Element("Key")!.Value); } - var propertyGroupElement = property.Element("Tab"); + XElement? propertyGroupElement = property.Element("Tab"); if (propertyGroupElement == null || string.IsNullOrEmpty(propertyGroupElement.Value)) { contentType.AddPropertyType(propertyType); @@ -1133,11 +1156,11 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var allowedChildren = contentType.AllowedContentTypes?.ToList(); int sortOrder = allowedChildren?.Any() ?? false ? allowedChildren.Last().SortOrder : 0; - foreach (var element in structureElement.Elements()) + foreach (XElement element in structureElement.Elements()) { var alias = element.Value; - var allowedChild = importedContentTypes.ContainsKey(alias) + T? allowedChild = importedContentTypes.ContainsKey(alias) ? importedContentTypes[alias] : service.Get(alias); if (allowedChild == null) @@ -1149,7 +1172,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging } if (allowedChildren?.Any(x => x.Id.IsValueCreated && x.Id.Value == allowedChild.Id) ?? false) + { continue; + } allowedChildren?.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), sortOrder, allowedChild.Alias)); @@ -1163,15 +1188,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// /// Used during Content import to ensure that the ContentType of a content item exists /// - /// /// private T FindContentTypeByAlias(string contentTypeAlias, IContentTypeBaseService typeService) where T : IContentTypeComposition { - var contentType = typeService.Get(contentTypeAlias); + T? contentType = typeService.Get(contentTypeAlias); if (contentType == null) + { throw new Exception($"ContentType matching the passed in Alias: '{contentTypeAlias}' was null"); + } return contentType; } @@ -1201,25 +1227,27 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var dataTypes = new List(); - var importedFolders = CreateDataTypeFolderStructure(dataTypeElements, out entityContainersInstalled); + Dictionary importedFolders = CreateDataTypeFolderStructure(dataTypeElements, out entityContainersInstalled); - foreach (var dataTypeElement in dataTypeElements) + foreach (XElement dataTypeElement in dataTypeElements) { var dataTypeDefinitionName = dataTypeElement.AttributeValue("Name"); - var dataTypeDefinitionId = dataTypeElement.RequiredAttributeValue("Definition"); - var databaseTypeAttribute = dataTypeElement.Attribute("DatabaseType"); + Guid dataTypeDefinitionId = dataTypeElement.RequiredAttributeValue("Definition"); + XAttribute? databaseTypeAttribute = dataTypeElement.Attribute("DatabaseType"); var parentId = -1; if (dataTypeDefinitionName is not null && importedFolders.ContainsKey(dataTypeDefinitionName)) + { parentId = importedFolders[dataTypeDefinitionName]; + } - var definition = _dataTypeService.GetDataType(dataTypeDefinitionId); + IDataType? definition = _dataTypeService.GetDataType(dataTypeDefinitionId); //If the datatype definition doesn't already exist we create a new according to the one in the package xml if (definition == null) { - var databaseType = databaseTypeAttribute?.Value.EnumParse(true) ?? - ValueStorageType.Ntext; + ValueStorageType databaseType = databaseTypeAttribute?.Value.EnumParse(true) ?? + ValueStorageType.Ntext; // the Id field is actually the string property editor Alias // however, the actual editor with this alias could be installed with the package, and @@ -1227,8 +1255,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging // the actual editor - going with a void editor var editorAlias = dataTypeElement.Attribute("Id")?.Value?.Trim(); - if (!_propertyEditors.TryGet(editorAlias, out var editor)) + if (!_propertyEditors.TryGet(editorAlias, out IDataEditor? editor)) + { editor = new VoidEditor(_dataValueEditorFactory) {Alias = editorAlias ?? string.Empty}; + } var dataType = new DataType(editor, _serializer) { @@ -1240,8 +1270,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging var configurationAttributeValue = dataTypeElement.Attribute("Configuration")?.Value; if (!string.IsNullOrWhiteSpace(configurationAttributeValue)) + { dataType.Configuration = editor.GetConfigurationEditor() .FromDatabase(configurationAttributeValue, _serializer); + } dataTypes.Add(dataType); } @@ -1265,17 +1297,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var importedFolders = new Dictionary(); var trackEntityContainersInstalled = new List(); - foreach (var datatypeElement in datatypeElements) + foreach (XElement datatypeElement in datatypeElements) { - var foldersAttribute = datatypeElement.Attribute("Folders"); + XAttribute? foldersAttribute = datatypeElement.Attribute("Folders"); if (foldersAttribute != null) { var name = datatypeElement.Attribute("Name")?.Value; var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); - var folderKeysAttribute = datatypeElement.Attribute("FolderKeys"); + XAttribute? folderKeysAttribute = datatypeElement.Attribute("FolderKeys"); - var folderKeys = Array.Empty(); + Guid[] folderKeys = Array.Empty(); if (folderKeysAttribute != null) { folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash) @@ -1283,13 +1315,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging } var rootFolder = WebUtility.UrlDecode(folders[0]); - var rootFolderKey = folderKeys.Length > 0 ? folderKeys[0] : Guid.NewGuid(); + Guid rootFolderKey = folderKeys.Length > 0 ? folderKeys[0] : Guid.NewGuid(); //there will only be a single result by name for level 1 (root) containers - var current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); + EntityContainer? current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); if (current == null) { - var tryCreateFolder = _dataTypeService.CreateContainer(-1, rootFolderKey, rootFolder); + Attempt?> tryCreateFolder = _dataTypeService.CreateContainer(-1, rootFolderKey, rootFolder); if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", @@ -1306,8 +1338,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging for (var i = 1; i < folders.Length; i++) { var folderName = WebUtility.UrlDecode(folders[i]); - Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; - current = CreateDataTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current!); + Guid? folderKey = folderKeys.Length == folders.Length ? folderKeys[i] : null; + current = CreateDataTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); trackEntityContainersInstalled.Add(current!); importedFolders[name!] = current!.Id; } @@ -1320,7 +1352,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private EntityContainer? CreateDataTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) { - var children = _entityService.GetChildren(current.Id).ToArray(); + IEntitySlim[] children = _entityService.GetChildren(current.Id).ToArray(); var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); if (found) { @@ -1328,7 +1360,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging return _dataTypeService.GetContainer(containerId); } - var tryCreateFolder = _dataTypeService.CreateContainer(current.Id, folderKey, folderName); + Attempt?> tryCreateFolder = _dataTypeService.CreateContainer(current.Id, folderKey, folderName); if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); @@ -1355,6 +1387,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId); } + public IEnumerable ImportDictionaryItem(XElement dictionaryItemElement, int userId, Guid? parentId) + { + var languages = _localizationService.GetAllLanguages().ToList(); + return ImportDictionaryItem(dictionaryItemElement, languages, parentId, userId); + } + private IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, List languages, Guid? parentId, int userId) { @@ -1398,7 +1436,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging List languages) { var translations = dictionaryItem.Translations.ToList(); - foreach (var valueElement in dictionaryItemElement.Elements("Value") + foreach (XElement valueElement in dictionaryItemElement.Elements("Value") .Where(v => DictionaryValueIsNew(translations, v))) { AddDictionaryTranslation(translations, valueElement, languages); @@ -1438,7 +1476,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging XElement valueElement, IEnumerable languages) { var languageId = valueElement.Attribute("LanguageCultureAlias")?.Value; - var language = languages.SingleOrDefault(l => l.IsoCode == languageId); + ILanguage? language = languages.SingleOrDefault(l => l.IsoCode == languageId); if (language == null) { return; @@ -1461,7 +1499,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging public IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId) { var list = new List(); - foreach (var languageElement in languageElements) + foreach (XElement languageElement in languageElements) { var isoCode = languageElement.AttributeValue("CultureAlias"); if (string.IsNullOrEmpty(isoCode)) @@ -1469,7 +1507,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging continue; } - var existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); + ILanguage? existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); if (existingLanguage != null) { continue; @@ -1560,35 +1598,35 @@ namespace Umbraco.Cms.Infrastructure.Packaging var macroSource = macroElement.Element("macroSource")!.Value; //Following xml elements are treated as nullable properties - var useInEditorElement = macroElement.Element("useInEditor"); + XElement? useInEditorElement = macroElement.Element("useInEditor"); var useInEditor = false; if (useInEditorElement != null && string.IsNullOrEmpty((string)useInEditorElement) == false) { useInEditor = bool.Parse(useInEditorElement.Value); } - var cacheDurationElement = macroElement.Element("refreshRate"); + XElement? cacheDurationElement = macroElement.Element("refreshRate"); var cacheDuration = 0; if (cacheDurationElement != null && string.IsNullOrEmpty((string)cacheDurationElement) == false) { cacheDuration = int.Parse(cacheDurationElement.Value, CultureInfo.InvariantCulture); } - var cacheByMemberElement = macroElement.Element("cacheByMember"); + XElement? cacheByMemberElement = macroElement.Element("cacheByMember"); var cacheByMember = false; if (cacheByMemberElement != null && string.IsNullOrEmpty((string)cacheByMemberElement) == false) { cacheByMember = bool.Parse(cacheByMemberElement.Value); } - var cacheByPageElement = macroElement.Element("cacheByPage"); + XElement? cacheByPageElement = macroElement.Element("cacheByPage"); var cacheByPage = false; if (cacheByPageElement != null && string.IsNullOrEmpty((string)cacheByPageElement) == false) { cacheByPage = bool.Parse(cacheByPageElement.Value); } - var dontRenderElement = macroElement.Element("dontRender"); + XElement? dontRenderElement = macroElement.Element("dontRender"); var dontRender = true; if (dontRenderElement != null && string.IsNullOrEmpty((string)dontRenderElement) == false) { @@ -1596,16 +1634,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging } var existingMacro = _macroService.GetById(macroKey) as Macro; - var macro = existingMacro ?? new Macro(_shortStringHelper, macroAlias, macroName, macroSource, + Macro macro = existingMacro ?? new Macro(_shortStringHelper, macroAlias, macroName, macroSource, cacheByPage, cacheByMember, dontRender, useInEditor, cacheDuration) {Key = macroKey}; - var properties = macroElement.Element("properties"); + XElement? properties = macroElement.Element("properties"); if (properties != null) { int sortOrder = 0; foreach (XElement property in properties.Elements()) { - var propertyKey = property.RequiredAttributeValue("key"); + Guid propertyKey = property.RequiredAttributeValue("key"); var propertyName = property.Attribute("name")?.Value; var propertyAlias = property.Attribute("alias")!.Value; var editorAlias = property.Attribute("propertyType")!.Value; @@ -1723,10 +1761,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging _fileService.SaveStylesheet(s, userId); } - foreach (var prop in n.XPathSelectElements("Properties/Property")) + foreach (XElement prop in n.XPathSelectElements("Properties/Property")) { var alias = prop.Element("Alias")!.Value; - var sp = s.Properties?.SingleOrDefault(p => p != null && p.Alias == alias); + IStylesheetProperty? sp = s.Properties?.SingleOrDefault(p => p != null && p.Alias == alias); var name = prop.Element("Name")!.Value; if (sp == null) { @@ -1776,10 +1814,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); - foreach (var tempElement in templateElements) + foreach (XElement tempElement in templateElements) { var dependencies = new List(); - var elementCopy = tempElement; + XElement elementCopy = tempElement; //Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting. if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && templateElements.Any(x => (string?)x.Element("Alias") == (string?)elementCopy.Element("Master"))) @@ -1800,22 +1838,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging } //Sort templates by dependencies to a potential master template - var sorted = graph.GetSortedItems(); - foreach (var item in sorted) + IEnumerable> sorted = graph.GetSortedItems(); + foreach (TopoGraph.Node? item in sorted) { - var templateElement = item.Item; + XElement templateElement = item.Item; var templateName = templateElement.Element("Name")?.Value; var alias = templateElement.Element("Alias")!.Value; var design = templateElement.Element("Design")?.Value; - var masterElement = templateElement.Element("Master"); + XElement? masterElement = templateElement.Element("Master"); var existingTemplate = _fileService.GetTemplate(alias) as Template; - var template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); + Template? template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); // For new templates, use the serialized key if avaialble. - if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out var key)) + if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out Guid key)) { template.Key = key; } @@ -1825,16 +1863,20 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (masterElement != null && string.IsNullOrEmpty((string)masterElement) == false) { template.MasterTemplateAlias = masterElement.Value; - var masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); + ITemplate? masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); if (masterTemplate != null) + { template.MasterTemplateId = new Lazy(() => masterTemplate.Id); + } } templates.Add(template); } if (templates.Any()) + { _fileService.SaveTemplate(templates, userId); + } return templates; } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs index bb9866e116..ecd17fac4f 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs @@ -1,84 +1,101 @@ -using System; -using System.Linq; using System.Xml.Linq; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public class PackageInstallation : IPackageInstallation { + private readonly PackageDataInstallation _packageDataInstallation; + private readonly CompiledPackageXmlParser _parser; - public class PackageInstallation : IPackageInstallation + /// + /// Initializes a new instance of the class. + /// + public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) { - private readonly PackageDataInstallation _packageDataInstallation; - private readonly CompiledPackageXmlParser _parser; + _packageDataInstallation = + packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + } - - /// - /// Initializes a new instance of the class. - /// - public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) + public CompiledPackage ReadPackage(XDocument? packageXmlFile) + { + if (packageXmlFile == null) { - _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); - _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + throw new ArgumentNullException(nameof(packageXmlFile)); } - public CompiledPackage ReadPackage(XDocument? packageXmlFile) - { - if (packageXmlFile == null) - throw new ArgumentNullException(nameof(packageXmlFile)); + var compiledPackage = _parser.ToCompiledPackage(packageXmlFile); + return compiledPackage; + } - var compiledPackage = _parser.ToCompiledPackage(packageXmlFile); - return compiledPackage; + public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition) + { + packageDefinition = new PackageDefinition { Name = compiledPackage.Name }; + + InstallationSummary installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId); + + // Make sure the definition is up to date with everything (note: macro partial views are embedded in macros) + foreach (IDataType x in installationSummary.DataTypesInstalled) + { + packageDefinition.DataTypes.Add(x.Id.ToInvariantString()); } - public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition) + foreach (ILanguage x in installationSummary.LanguagesInstalled) { - packageDefinition = new PackageDefinition - { - Name = compiledPackage.Name - }; - - InstallationSummary installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId); - - // Make sure the definition is up to date with everything (note: macro partial views are embedded in macros) - foreach (var x in installationSummary.DataTypesInstalled) - packageDefinition.DataTypes.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.LanguagesInstalled) - packageDefinition.Languages.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.DictionaryItemsInstalled) - packageDefinition.DictionaryItems.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.MacrosInstalled) - packageDefinition.Macros.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.TemplatesInstalled) - packageDefinition.Templates.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.DocumentTypesInstalled) - packageDefinition.DocumentTypes.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.MediaTypesInstalled) - packageDefinition.MediaTypes.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.StylesheetsInstalled) - packageDefinition.Stylesheets.Add(x.Path); - - foreach (var x in installationSummary.ScriptsInstalled) - packageDefinition.Scripts.Add(x.Path); - - foreach (var x in installationSummary.PartialViewsInstalled) - packageDefinition.PartialViews.Add(x.Path); - - packageDefinition.ContentNodeId = installationSummary.ContentInstalled.FirstOrDefault()?.Id.ToInvariantString(); - - foreach (var x in installationSummary.MediaInstalled) - packageDefinition.MediaUdis.Add(x.GetUdi()); - - return installationSummary; + packageDefinition.Languages.Add(x.Id.ToInvariantString()); } + foreach (IDictionaryItem x in installationSummary.DictionaryItemsInstalled) + { + packageDefinition.DictionaryItems.Add(x.Id.ToInvariantString()); + } + + foreach (IMacro x in installationSummary.MacrosInstalled) + { + packageDefinition.Macros.Add(x.Id.ToInvariantString()); + } + + foreach (ITemplate x in installationSummary.TemplatesInstalled) + { + packageDefinition.Templates.Add(x.Id.ToInvariantString()); + } + + foreach (IContentType x in installationSummary.DocumentTypesInstalled) + { + packageDefinition.DocumentTypes.Add(x.Id.ToInvariantString()); + } + + foreach (IMediaType x in installationSummary.MediaTypesInstalled) + { + packageDefinition.MediaTypes.Add(x.Id.ToInvariantString()); + } + + foreach (IFile x in installationSummary.StylesheetsInstalled) + { + packageDefinition.Stylesheets.Add(x.Path); + } + + foreach (IScript x in installationSummary.ScriptsInstalled) + { + packageDefinition.Scripts.Add(x.Path); + } + + foreach (IPartialView x in installationSummary.PartialViewsInstalled) + { + packageDefinition.PartialViews.Add(x.Path); + } + + packageDefinition.ContentNodeId = installationSummary.ContentInstalled.FirstOrDefault()?.Id.ToInvariantString(); + + foreach (IMedia x in installationSummary.MediaInstalled) + { + packageDefinition.MediaUdis.Add(x.GetUdi()); + } + + return installationSummary; } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs index f39277975d..6f0355f674 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs @@ -1,6 +1,3 @@ -using System; -using System.ComponentModel; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; @@ -8,39 +5,38 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public abstract class PackageMigrationBase : MigrationBase { - public abstract class PackageMigrationBase : MigrationBase - { - private readonly IPackagingService _packagingService; - private readonly IMediaService _mediaService; - private readonly MediaFileManager _mediaFileManager; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IShortStringHelper _shortStringHelper; - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly IOptions _packageMigrationsSettings; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IOptions _packageMigrationsSettings; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; - public PackageMigrationBase( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions packageMigrationsSettings) - : base(context) - { - _packagingService = packagingService; - _mediaService = mediaService; - _mediaFileManager = mediaFileManager; - _mediaUrlGenerators = mediaUrlGenerators; - _shortStringHelper = shortStringHelper; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _packageMigrationsSettings = packageMigrationsSettings; - } + public PackageMigrationBase( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions packageMigrationsSettings) + : base(context) + { + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationsSettings = packageMigrationsSettings; + } public IImportPackageBuilder ImportPackage => BeginBuild( new ImportPackageBuilder( @@ -53,5 +49,5 @@ namespace Umbraco.Cms.Infrastructure.Packaging Context, _packageMigrationsSettings)); - } + } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs index d25c65cfb8..bdbce82fcb 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs @@ -1,53 +1,53 @@ -using System; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Base class for package migration plans +/// +public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable { + /// + /// Creates a package migration plan + /// + /// The name of the package. If the package has a package.manifest these must match. + protected PackageMigrationPlan(string packageName) + : this(packageName, packageName) + { + } /// - /// Base class for package migration plans + /// Create a plan for a Package Name /// - public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable + /// + /// The package name that the plan is for. If the package has a package.manifest these must + /// match. + /// + /// + /// The plan name for the package. This should be the same name as the + /// package name if there is only one plan in the package. + /// + protected PackageMigrationPlan(string packageName, string planName) + : base(planName) { - /// - /// Creates a package migration plan - /// - /// The name of the package. If the package has a package.manifest these must match. - protected PackageMigrationPlan(string packageName) : this(packageName, packageName) - { - - } - - /// - /// Create a plan for a Package Name - /// - /// The package name that the plan is for. If the package has a package.manifest these must match. - /// - /// The plan name for the package. This should be the same name as the - /// package name if there is only one plan in the package. - /// - protected PackageMigrationPlan(string packageName, string planName) : base(planName) - { - // A call to From must be done first - From(string.Empty); - - DefinePlan(); - PackageName = packageName; - } - - /// - /// Inform the plan executor to ignore all saved package state and - /// run the migration from initial state to it's end state. - /// - public override bool IgnoreCurrentState => true; - - /// - /// Returns the Package Name for this plan - /// - public string PackageName { get; } - - protected abstract void DefinePlan(); + // A call to From must be done first + From(string.Empty); + DefinePlan(); + PackageName = packageName; } + + /// + /// Inform the plan executor to ignore all saved package state and + /// run the migration from initial state to it's end state. + /// + public override bool IgnoreCurrentState => true; + + /// + /// Returns the Package Name for this plan + /// + public string PackageName { get; } + + protected abstract void DefinePlan(); } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs index aa390dcaa4..565f53b57c 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs @@ -1,16 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// A collection of +/// +public class PackageMigrationPlanCollection : BuilderCollectionBase { - /// - /// A collection of - /// - public class PackageMigrationPlanCollection : BuilderCollectionBase + public PackageMigrationPlanCollection(Func> items) + : base(items) { - public PackageMigrationPlanCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs index bf496852c6..91b1364139 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase { - public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase - { - protected override PackageMigrationPlanCollectionBuilder This => this; - } + protected override PackageMigrationPlanCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs b/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs index efefcfcc7a..2931011f38 100644 --- a/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs +++ b/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs @@ -1,65 +1,61 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class PendingPackageMigrations { - public class PendingPackageMigrations + private readonly ILogger _logger; + private readonly PackageMigrationPlanCollection _packageMigrationPlans; + + public PendingPackageMigrations( + ILogger logger, + PackageMigrationPlanCollection packageMigrationPlans) { - private readonly ILogger _logger; - private readonly PackageMigrationPlanCollection _packageMigrationPlans; + _logger = logger; + _packageMigrationPlans = packageMigrationPlans; + } - public PendingPackageMigrations( - ILogger logger, - PackageMigrationPlanCollection packageMigrationPlans) + /// + /// Returns what package migration names are pending + /// + /// + /// These are the key/value pairs from the keyvalue storage of migration names and their final values + /// + /// + public IReadOnlyList GetPendingPackageMigrations(IReadOnlyDictionary? keyValues) + { + var packageMigrationPlans = _packageMigrationPlans.ToList(); + + var pendingMigrations = new List(packageMigrationPlans.Count); + + foreach (PackageMigrationPlan plan in packageMigrationPlans) { - _logger = logger; - _packageMigrationPlans = packageMigrationPlans; - - } - - /// - /// Returns what package migration names are pending - /// - /// - /// These are the key/value pairs from the keyvalue storage of migration names and their final values - /// - /// - public IReadOnlyList GetPendingPackageMigrations(IReadOnlyDictionary? keyValues) - { - var packageMigrationPlans = _packageMigrationPlans.ToList(); - - var pendingMigrations = new List(packageMigrationPlans.Count); - - foreach (PackageMigrationPlan plan in packageMigrationPlans) + string? currentMigrationState = null; + var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; + if (keyValues?.TryGetValue(planKeyValueKey, out var value) ?? false) { - string? currentMigrationState = null; - var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; - if (keyValues?.TryGetValue(planKeyValueKey, out var value) ?? false) - { - currentMigrationState = value; + currentMigrationState = value; - if (!plan.FinalState.InvariantEquals(value)) - { - // Not equal so we need to run - pendingMigrations.Add(plan.Name); - } - } - else + if (!plan.FinalState.InvariantEquals(value)) { - // If there is nothing in the DB then we need to run + // Not equal so we need to run pendingMigrations.Add(plan.Name); } - - _logger.LogDebug("Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", - plan.Name, - plan.FinalState, - currentMigrationState ?? ""); + } + else + { + // If there is nothing in the DB then we need to run + pendingMigrations.Add(plan.Name); } - return pendingMigrations; + _logger.LogDebug( + "Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", + plan.Name, + plan.FinalState, + currentMigrationState ?? ""); } + + return pendingMigrations; } } diff --git a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs index 3797d4a433..f266df71ff 100644 --- a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs @@ -1,11 +1,10 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Install.Models; namespace Umbraco.Cms.Infrastructure.Persistence; /// -/// Provider metadata for custom connection string setup. +/// Provider metadata for custom connection string setup. /// [DataContract] public class CustomConnectionStringDatabaseProviderMetadata : IDatabaseProviderMetadata diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs index 8b8386c93f..be89cb2ef6 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a db constraint +/// +[AttributeUsage(AttributeTargets.Property)] +public class ConstraintAttribute : Attribute { /// - /// Attribute that represents a db constraint + /// Gets or sets the name of the constraint /// - [AttributeUsage(AttributeTargets.Property)] - public class ConstraintAttribute : Attribute - { - /// - /// Gets or sets the name of the constraint - /// - /// - /// Overrides the default naming of a property constraint: - /// DF_tableName_propertyName - /// - public string? Name { get; set; } + /// + /// Overrides the default naming of a property constraint: + /// DF_tableName_propertyName + /// + public string? Name { get; set; } - /// - /// Gets or sets the Default value - /// - public object? Default { get; set; } - } + /// + /// Gets or sets the Default value + /// + public object? Default { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs index a2f053415c..0eca49b8dd 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs @@ -1,40 +1,40 @@ -using System; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Attribute that represents a Foreign Key reference +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] +public class ForeignKeyAttribute : ReferencesAttribute { - /// - /// Attribute that represents a Foreign Key reference - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - public class ForeignKeyAttribute : ReferencesAttribute + public ForeignKeyAttribute(Type type) + : base(type) { - public ForeignKeyAttribute(Type type) : base(type) - { } - - /// - /// Gets or sets the cascade rule for deletions. - /// - public Rule OnDelete { get; set; } = Rule.None; - - /// - /// Gets or sets the cascade rule for updates. - /// - public Rule OnUpdate { get; set; } = Rule.None; - - /// - /// Gets or sets the name of the foreign key reference - /// - /// - /// Overrides the default naming of a foreign key reference: - /// FK_thisTableName_refTableName - /// - public string? Name { get; set; } - - /// - /// Gets or sets the name of the Column that this foreign key should reference. - /// - /// PrimaryKey column is used by default - public string? Column { get; set; } } + + /// + /// Gets or sets the cascade rule for deletions. + /// + public Rule OnDelete { get; set; } = Rule.None; + + /// + /// Gets or sets the cascade rule for updates. + /// + public Rule OnUpdate { get; set; } = Rule.None; + + /// + /// Gets or sets the name of the foreign key reference + /// + /// + /// Overrides the default naming of a foreign key reference: + /// FK_thisTableName_refTableName + /// + public string? Name { get; set; } + + /// + /// Gets or sets the name of the Column that this foreign key should reference. + /// + /// PrimaryKey column is used by default + public string? Column { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs index 053e5b825d..826e56ad89 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs @@ -1,40 +1,34 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents an Index +/// +[AttributeUsage(AttributeTargets.Property)] +public class IndexAttribute : Attribute { + public IndexAttribute(IndexTypes indexType) => IndexType = indexType; + /// - /// Attribute that represents an Index + /// Gets or sets the name of the Index /// - [AttributeUsage(AttributeTargets.Property)] - public class IndexAttribute : Attribute - { - public IndexAttribute(IndexTypes indexType) - { - IndexType = indexType; - } + /// + /// Overrides default naming of indexes: + /// IX_tableName + /// + public string? Name { get; set; } // Overrides default naming of indexes: IX_tableName - /// - /// Gets or sets the name of the Index - /// - /// - /// Overrides default naming of indexes: - /// IX_tableName - /// - public string? Name { get; set; }//Overrides default naming of indexes: IX_tableName + /// + /// Gets or sets the type of index to create + /// + public IndexTypes IndexType { get; } - /// - /// Gets or sets the type of index to create - /// - public IndexTypes IndexType { get; private set; } + /// + /// Gets or sets the column name(s) for the current index + /// + public string? ForColumns { get; set; } - /// - /// Gets or sets the column name(s) for the current index - /// - public string? ForColumns { get; set; } - - /// - /// Gets or sets the column name(s) for the columns to include in the index - /// - public string? IncludeColumns { get; set; } - } + /// + /// Gets or sets the column name(s) for the columns to include in the index + /// + public string? IncludeColumns { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs index 65516bb8c4..46697b9c97 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Enum for the 3 types of indexes that can be created +/// +public enum IndexTypes { - /// - /// Enum for the 3 types of indexes that can be created - /// - public enum IndexTypes - { - Clustered, - NonClustered, - UniqueNonClustered - } + Clustered, + NonClustered, + UniqueNonClustered, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs index 8e77b4bf96..a277e9b028 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs @@ -1,22 +1,16 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the length of a column +/// +/// Used to define the length of fixed sized columns - typically used for nvarchar +[AttributeUsage(AttributeTargets.Property)] +public class LengthAttribute : Attribute { - /// - /// Attribute that represents the length of a column - /// - /// Used to define the length of fixed sized columns - typically used for nvarchar - [AttributeUsage(AttributeTargets.Property)] - public class LengthAttribute : Attribute - { - public LengthAttribute(int length) - { - Length = length; - } + public LengthAttribute(int length) => Length = length; - /// - /// Gets or sets the length of a column - /// - public int Length { get; private set; } - } + /// + /// Gets or sets the length of a column + /// + public int Length { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs index 0db6433e94..f67447099d 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs @@ -1,20 +1,17 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the Null-setting of a column +/// +/// +/// This should only be used for Columns that can be Null. +/// By convention the Columns will be "NOT NULL". +/// +[AttributeUsage(AttributeTargets.Property)] +public class NullSettingAttribute : Attribute { /// - /// Attribute that represents the Null-setting of a column + /// Gets or sets the for a column /// - /// - /// This should only be used for Columns that can be Null. - /// By convention the Columns will be "NOT NULL". - /// - [AttributeUsage(AttributeTargets.Property)] - public class NullSettingAttribute : Attribute - { - /// - /// Gets or sets the for a column - /// - public NullSettings NullSetting { get; set; } - } + public NullSettings NullSetting { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs index 70c901c61e..d9140a0f14 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Enum with the 2 possible Null settings: Null or Not Null +/// +public enum NullSettings { - /// - /// Enum with the 2 possible Null settings: Null or Not Null - /// - public enum NullSettings - { - Null, - NotNull - } + Null, + NotNull, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs index c4c5579028..d6cdf4ec7a 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs @@ -1,59 +1,56 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a Primary Key +/// +/// +/// By default, Clustered and AutoIncrement is set to true. +/// +[AttributeUsage(AttributeTargets.Property)] +public class PrimaryKeyColumnAttribute : Attribute { + public PrimaryKeyColumnAttribute() + { + Clustered = true; + AutoIncrement = true; + } + /// - /// Attribute that represents a Primary Key + /// Gets or sets a boolean indicating whether the primary key is clustered. + /// + /// Defaults to true + public bool Clustered { get; set; } + + /// + /// Gets or sets a boolean indicating whether the primary key is auto incremented. + /// + /// Defaults to true + public bool AutoIncrement { get; set; } + + /// + /// Gets or sets the name of the PrimaryKey. /// /// - /// By default, Clustered and AutoIncrement is set to true. + /// Overrides the default naming of a PrimaryKey constraint: + /// PK_tableName /// - [AttributeUsage(AttributeTargets.Property)] - public class PrimaryKeyColumnAttribute : Attribute - { - public PrimaryKeyColumnAttribute() - { - Clustered = true; - AutoIncrement = true; - } + public string? Name { get; set; } - /// - /// Gets or sets a boolean indicating whether the primary key is clustered. - /// - /// Defaults to true - public bool Clustered { get; set; } + /// + /// Gets or sets the names of the columns for this PrimaryKey. + /// + /// + /// Should only be used if the PrimaryKey spans over multiple columns. + /// Usage: [nodeId], [otherColumn] + /// + public string? OnColumns { get; set; } - /// - /// Gets or sets a boolean indicating whether the primary key is auto incremented. - /// - /// Defaults to true - public bool AutoIncrement { get; set; } - - /// - /// Gets or sets the name of the PrimaryKey. - /// - /// - /// Overrides the default naming of a PrimaryKey constraint: - /// PK_tableName - /// - public string? Name { get; set; } - - /// - /// Gets or sets the names of the columns for this PrimaryKey. - /// - /// - /// Should only be used if the PrimaryKey spans over multiple columns. - /// Usage: [nodeId], [otherColumn] - /// - public string? OnColumns { get; set; } - - /// - /// Gets or sets the Identity Seed, which is used for Sql Ce databases. - /// - /// - /// We'll only look for changes to seeding and apply them if the configured database - /// is an Sql Ce database. - /// - public int IdentitySeed { get; set; } - } + /// + /// Gets or sets the Identity Seed, which is used for Sql Ce databases. + /// + /// + /// We'll only look for changes to seeding and apply them if the configured database + /// is an Sql Ce database. + /// + public int IdentitySeed { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs index f008aa7e22..a324ddb358 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs @@ -1,21 +1,15 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a reference between two tables/DTOs +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] +public class ReferencesAttribute : Attribute { - /// - /// Attribute that represents a reference between two tables/DTOs - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] - public class ReferencesAttribute : Attribute - { - public ReferencesAttribute(Type type) - { - Type = type; - } + public ReferencesAttribute(Type type) => Type = type; - /// - /// Gets or sets the Type of the referenced DTO/table - /// - public Type Type { get; set; } - } + /// + /// Gets or sets the Type of the referenced DTO/table + /// + public Type Type { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs index 41570d7b95..b5e57a3f3f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs @@ -1,43 +1,44 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Allows for specifying custom DB types that are not natively mapped. +/// +public struct SpecialDbType : IEquatable { - /// - /// Allows for specifying custom DB types that are not natively mapped. - /// - public struct SpecialDbType : IEquatable + private readonly string _dbType; + + public SpecialDbType(string dbType) { - private readonly string _dbType; - - public SpecialDbType(string dbType) + if (string.IsNullOrWhiteSpace(dbType)) { - if (string.IsNullOrWhiteSpace(dbType)) - { - throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); - } - - _dbType = dbType; + throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); } - public SpecialDbType(SpecialDbTypes specialDbTypes) - => _dbType = specialDbTypes.ToString(); - - public static SpecialDbType NTEXT { get; } = new SpecialDbType(SpecialDbTypes.NTEXT); - public static SpecialDbType NCHAR { get; } = new SpecialDbType(SpecialDbTypes.NCHAR); - public static SpecialDbType NVARCHARMAX { get; } = new SpecialDbType(SpecialDbTypes.NVARCHARMAX); - - public override bool Equals(object? obj) => obj is SpecialDbType types && Equals(types); - public bool Equals(SpecialDbType other) => _dbType == other._dbType; - public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); - - public override string ToString() => _dbType.ToString(); - - // Make this directly castable to string - public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); - - // direct equality operators with SpecialDbTypes enum - public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; - public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; + _dbType = dbType; } + + public SpecialDbType(SpecialDbTypes specialDbTypes) + => _dbType = specialDbTypes.ToString(); + + public static SpecialDbType NTEXT { get; } = new(SpecialDbTypes.NTEXT); + + public static SpecialDbType NCHAR { get; } = new(SpecialDbTypes.NCHAR); + + public static SpecialDbType NVARCHARMAX { get; } = new(SpecialDbTypes.NVARCHARMAX); + + // Make this directly castable to string + public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); + + public override bool Equals(object? obj) => obj is SpecialDbType types && Equals(types); + + public bool Equals(SpecialDbType other) => _dbType == other._dbType; + + public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); + + public override string ToString() => _dbType; + + // direct equality operators with SpecialDbTypes enum + public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; + + public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs index d7fd2ff34f..cfdd4d80aa 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the usage of a special type +/// +/// +/// Should only be used when the .NET type can't be directly translated to a DbType. +/// +[AttributeUsage(AttributeTargets.Property)] +public class SpecialDbTypeAttribute : Attribute { + public SpecialDbTypeAttribute(SpecialDbTypes databaseType) + => DatabaseType = new SpecialDbType(databaseType); + + public SpecialDbTypeAttribute(string databaseType) + => DatabaseType = new SpecialDbType(databaseType); + /// - /// Attribute that represents the usage of a special type + /// Gets or sets the for this column /// - /// - /// Should only be used when the .NET type can't be directly translated to a DbType. - /// - [AttributeUsage(AttributeTargets.Property)] - public class SpecialDbTypeAttribute : Attribute - { - public SpecialDbTypeAttribute(SpecialDbTypes databaseType) - => DatabaseType = new SpecialDbType(databaseType); - - public SpecialDbTypeAttribute(string databaseType) - => DatabaseType = new SpecialDbType(databaseType); - - /// - /// Gets or sets the for this column - /// - public SpecialDbType DatabaseType { get; private set; } - } + public SpecialDbType DatabaseType { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs index d867d6f682..98b0558a2e 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Known special DB types required for Umbraco. +/// +public enum SpecialDbTypes { - /// - /// Known special DB types required for Umbraco. - /// - public enum SpecialDbTypes - { - NTEXT, - NCHAR, - NVARCHARMAX, - } + NTEXT, + NCHAR, + NVARCHARMAX, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs index f54691994e..e2efea4251 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs @@ -14,7 +14,8 @@ namespace Umbraco.Cms.Core.Persistence internal static class DatabaseDebugHelper { private const int CommandsSize = 100; - private static readonly Queue>> Commands = new Queue>>(); + private static readonly Queue>> Commands = + new Queue>>(); public static void SetCommand(IDbCommand command, string context) { @@ -122,7 +123,8 @@ namespace Umbraco.Cms.Core.Persistence //var rdr = objTarget as DbDataReader; try { - var commandProp = objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic); + var commandProp = + objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic); if (commandProp == null) throw new Exception($"panic: failed to get Command property of {objTarget.GetType().FullName}."); cmd = commandProp.GetValue(objTarget, null) as DbCommand; diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs index a80bbbe3f6..99605476b3 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs @@ -1,38 +1,53 @@ -using System; using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class ColumnDefinition { - public class ColumnDefinition - { - public virtual string Name { get; set; } = null!; - //This type is typically used as part of a migration - public virtual DbType? Type { get; set; } - //When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. - //This type is typically used as part of an initial table creation - public Type PropertyType { get; set; } = null!; + public virtual string Name { get; set; } = null!; - /// - /// Used for column types that cannot be natively mapped. - /// - public SpecialDbType? CustomDbType { get; set; } + // This type is typically used as part of a migration + public virtual DbType? Type { get; set; } - public virtual int Seeding { get; set; } - public virtual int Size { get; set; } - public virtual int Precision { get; set; } - public virtual string? CustomType { get; set; } - public virtual object? DefaultValue { get; set; } - public virtual string? ConstraintName { get; set; } - public virtual bool IsForeignKey { get; set; } - public virtual bool IsIdentity { get; set; } - public virtual bool IsIndexed { get; set; }//Clustered? - public virtual bool IsPrimaryKey { get; set; } - public virtual string? PrimaryKeyName { get; set; } - public virtual string? PrimaryKeyColumns { get; set; }//When the primary key spans multiple columns - public virtual bool IsNullable { get; set; } - public virtual bool IsUnique { get; set; } - public virtual string? TableName { get; set; } - public virtual ModificationType ModificationType { get; set; } - } + // When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. + // This type is typically used as part of an initial table creation + public Type PropertyType { get; set; } = null!; + + /// + /// Used for column types that cannot be natively mapped. + /// + public SpecialDbType? CustomDbType { get; set; } + + public virtual int Seeding { get; set; } + + public virtual int Size { get; set; } + + public virtual int Precision { get; set; } + + public virtual string? CustomType { get; set; } + + public virtual object? DefaultValue { get; set; } + + public virtual string? ConstraintName { get; set; } + + public virtual bool IsForeignKey { get; set; } + + public virtual bool IsIdentity { get; set; } + + public virtual bool IsIndexed { get; set; } // Clustered? + + public virtual bool IsPrimaryKey { get; set; } + + public virtual string? PrimaryKeyName { get; set; } + + public virtual string? PrimaryKeyColumns { get; set; } // When the primary key spans multiple columns + + public virtual bool IsNullable { get; set; } + + public virtual bool IsUnique { get; set; } + + public virtual string? TableName { get; set; } + + public virtual ModificationType ModificationType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs index 87066ce206..da67fd22c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs @@ -1,23 +1,23 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class ConstraintDefinition { - public class ConstraintDefinition - { - public ConstraintDefinition(ConstraintType type) - { - _constraintType = type; - } + public ICollection Columns = new HashSet(); + private readonly ConstraintType _constraintType; - private readonly ConstraintType _constraintType; - public bool IsPrimaryKeyConstraint => ConstraintType.PrimaryKey == _constraintType; - public bool IsUniqueConstraint => ConstraintType.Unique == _constraintType; - public bool IsNonUniqueConstraint => ConstraintType.NonUnique == _constraintType; + public ConstraintDefinition(ConstraintType type) => _constraintType = type; - public string? SchemaName { get; set; } - public string? ConstraintName { get; set; } - public string? TableName { get; set; } - public ICollection Columns = new HashSet(); - public bool IsPrimaryKeyClustered { get; set; } - } + public bool IsPrimaryKeyConstraint => _constraintType == ConstraintType.PrimaryKey; + + public bool IsUniqueConstraint => _constraintType == ConstraintType.Unique; + + public bool IsNonUniqueConstraint => _constraintType == ConstraintType.NonUnique; + + public string? SchemaName { get; set; } + + public string? ConstraintName { get; set; } + + public string? TableName { get; set; } + + public bool IsPrimaryKeyClustered { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs index 4592f1f14f..1d7fbc6408 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum ConstraintType { - public enum ConstraintType - { - PrimaryKey, - Unique, - NonUnique - } + PrimaryKey, + Unique, + NonUnique, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs index df73074a35..4c2cf0a69f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs @@ -1,23 +1,23 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +/// +/// Represents a database index definition retrieved by querying the database +/// +internal class DbIndexDefinition { - /// - /// Represents a database index definition retrieved by querying the database - /// - internal class DbIndexDefinition + public DbIndexDefinition(Tuple data) { - public DbIndexDefinition(Tuple data) - { - TableName = data.Item1; - IndexName = data.Item2; - ColumnName = data.Item3; - IsUnique = data.Item4; - } - - public string IndexName { get; } - public string TableName { get; } - public string ColumnName { get; } - public bool IsUnique { get; } + TableName = data.Item1; + IndexName = data.Item2; + ColumnName = data.Item3; + IsUnique = data.Item4; } + + public string IndexName { get; } + + public string TableName { get; } + + public string ColumnName { get; } + + public bool IsUnique { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 9e26c2722a..32bdd213e6 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using System.Reflection; using NPoco; using Umbraco.Cms.Core; @@ -7,174 +5,189 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public static class DefinitionFactory { - public static class DefinitionFactory + public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) { - public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) + // Looks for NPoco's TableNameAtribute for the name of the table + // If no attribute is set we use the name of the Type as the default convention + TableNameAttribute? tableNameAttribute = modelType.FirstAttribute(); + var tableName = tableNameAttribute == null ? modelType.Name : tableNameAttribute.Value; + + var tableDefinition = new TableDefinition { Name = tableName }; + var objProperties = modelType.GetProperties().ToList(); + foreach (PropertyInfo propertyInfo in objProperties) { - //Looks for NPoco's TableNameAtribute for the name of the table - //If no attribute is set we use the name of the Type as the default convention - var tableNameAttribute = modelType.FirstAttribute(); - string tableName = tableNameAttribute == null ? modelType.Name : tableNameAttribute.Value; - - var tableDefinition = new TableDefinition {Name = tableName}; - var objProperties = modelType.GetProperties().ToList(); - foreach (var propertyInfo in objProperties) + // If current property has an IgnoreAttribute then skip it + IgnoreAttribute? ignoreAttribute = propertyInfo.FirstAttribute(); + if (ignoreAttribute != null) { - //If current property has an IgnoreAttribute then skip it - var ignoreAttribute = propertyInfo.FirstAttribute(); - if (ignoreAttribute != null) continue; + continue; + } - //If current property has a ResultColumnAttribute then skip it - var resultColumnAttribute = propertyInfo.FirstAttribute(); - if (resultColumnAttribute != null) continue; + // If current property has a ResultColumnAttribute then skip it + ResultColumnAttribute? resultColumnAttribute = propertyInfo.FirstAttribute(); + if (resultColumnAttribute != null) + { + continue; + } - //Looks for ColumnAttribute with the name of the column, which would exist with ExplicitColumns - //Otherwise use the name of the property itself as the default convention - var columnAttribute = propertyInfo.FirstAttribute(); - string columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; - var columnDefinition = GetColumnDefinition(modelType, propertyInfo, columnName, tableName, sqlSyntax); - tableDefinition.Columns.Add(columnDefinition); + // Looks for ColumnAttribute with the name of the column, which would exist with ExplicitColumns + // Otherwise use the name of the property itself as the default convention + ColumnAttribute? columnAttribute = propertyInfo.FirstAttribute(); + var columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; + ColumnDefinition columnDefinition = + GetColumnDefinition(modelType, propertyInfo, columnName, tableName, sqlSyntax); + tableDefinition.Columns.Add(columnDefinition); - //Creates a foreignkey definition and adds it to the collection on the table definition - var foreignKeyAttributes = propertyInfo.MultipleAttribute(); - if (foreignKeyAttributes != null) + // Creates a foreignkey definition and adds it to the collection on the table definition + IEnumerable? foreignKeyAttributes = + propertyInfo.MultipleAttribute(); + if (foreignKeyAttributes != null) + { + foreach (ForeignKeyAttribute foreignKeyAttribute in foreignKeyAttributes) { - foreach (var foreignKeyAttribute in foreignKeyAttributes) - { - var foreignKeyDefinition = GetForeignKeyDefinition(modelType, propertyInfo, foreignKeyAttribute, columnName, tableName); - tableDefinition.ForeignKeys.Add(foreignKeyDefinition); - } - } - - //Creates an index definition and adds it to the collection on the table definition - var indexAttribute = propertyInfo.FirstAttribute(); - if (indexAttribute != null) - { - var indexDefinition = GetIndexDefinition(modelType, propertyInfo, indexAttribute, columnName, tableName); - tableDefinition.Indexes.Add(indexDefinition); - } - } - - return tableDefinition; - } - - public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, string tableName, ISqlSyntaxProvider sqlSyntax) - { - var definition = new ColumnDefinition{ Name = columnName, TableName = tableName, ModificationType = ModificationType.Create }; - - //Look for specific Null setting attributed a column - var nullSettingAttribute = propertyInfo.FirstAttribute(); - if (nullSettingAttribute != null) - { - definition.IsNullable = nullSettingAttribute.NullSetting == NullSettings.Null; - } - - //Look for specific DbType attributed a column - var databaseTypeAttribute = propertyInfo.FirstAttribute(); - if (databaseTypeAttribute != null) - { - definition.CustomDbType = databaseTypeAttribute.DatabaseType; - } - else - { - definition.PropertyType = propertyInfo.PropertyType; - } - - //Look for Primary Key for the current column - var primaryKeyColumnAttribute = propertyInfo.FirstAttribute(); - if (primaryKeyColumnAttribute != null) - { - string primaryKeyName = string.IsNullOrEmpty(primaryKeyColumnAttribute.Name) - ? string.Format("PK_{0}", tableName) - : primaryKeyColumnAttribute.Name; - - definition.IsPrimaryKey = true; - definition.IsIdentity = primaryKeyColumnAttribute.AutoIncrement; - definition.IsIndexed = primaryKeyColumnAttribute.Clustered; - definition.PrimaryKeyName = primaryKeyName; - definition.PrimaryKeyColumns = primaryKeyColumnAttribute.OnColumns ?? string.Empty; - definition.Seeding = primaryKeyColumnAttribute.IdentitySeed; - } - - //Look for Size/Length of DbType - var lengthAttribute = propertyInfo.FirstAttribute(); - if (lengthAttribute != null) - { - definition.Size = lengthAttribute.Length; - } - - //Look for Constraint for the current column - var constraintAttribute = propertyInfo.FirstAttribute(); - if (constraintAttribute != null) - { - definition.ConstraintName = constraintAttribute.Name ?? string.Empty; - definition.DefaultValue = constraintAttribute.Default ?? string.Empty; - } - - return definition; - } - - public static ForeignKeyDefinition GetForeignKeyDefinition(Type modelType, PropertyInfo propertyInfo, - ForeignKeyAttribute attribute, string columnName, string tableName) - { - var referencedTable = attribute.Type.FirstAttribute(); - var referencedPrimaryKey = attribute.Type.FirstAttribute(); - - string referencedColumn = string.IsNullOrEmpty(attribute.Column) - ? referencedPrimaryKey!.Value - : attribute.Column; - - string foreignKeyName = string.IsNullOrEmpty(attribute.Name) - ? string.Format("FK_{0}_{1}_{2}", tableName, referencedTable!.Value, referencedColumn) - : attribute.Name; - - var definition = new ForeignKeyDefinition - { - Name = foreignKeyName, - ForeignTable = tableName, - PrimaryTable = referencedTable!.Value, - OnDelete = attribute.OnDelete, - OnUpdate = attribute.OnUpdate - }; - definition.ForeignColumns.Add(columnName); - definition.PrimaryColumns.Add(referencedColumn); - - return definition; - } - - public static IndexDefinition GetIndexDefinition(Type modelType, PropertyInfo propertyInfo, IndexAttribute attribute, string columnName, string tableName) - { - string indexName = string.IsNullOrEmpty(attribute.Name) - ? string.Format("IX_{0}_{1}", tableName, columnName) - : attribute.Name; - - var definition = new IndexDefinition - { - Name = indexName, - IndexType = attribute.IndexType, - ColumnName = columnName, - TableName = tableName, - }; - - if (string.IsNullOrEmpty(attribute.ForColumns) == false) - { - var columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim()); - foreach (var column in columns) - { - definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); + ForeignKeyDefinition foreignKeyDefinition = GetForeignKeyDefinition(modelType, propertyInfo, foreignKeyAttribute, columnName, tableName); + tableDefinition.ForeignKeys.Add(foreignKeyDefinition); } } - if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + + // Creates an index definition and adds it to the collection on the table definition + IndexAttribute? indexAttribute = propertyInfo.FirstAttribute(); + if (indexAttribute != null) { - var columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim()); - foreach (var column in columns) - { - definition.IncludeColumns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); - } + IndexDefinition indexDefinition = + GetIndexDefinition(modelType, propertyInfo, indexAttribute, columnName, tableName); + tableDefinition.Indexes.Add(indexDefinition); } - return definition; } + + return tableDefinition; + } + + public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, string tableName, ISqlSyntaxProvider sqlSyntax) + { + var definition = new ColumnDefinition + { + Name = columnName, + TableName = tableName, + ModificationType = ModificationType.Create, + }; + + // Look for specific Null setting attributed a column + NullSettingAttribute? nullSettingAttribute = propertyInfo.FirstAttribute(); + if (nullSettingAttribute != null) + { + definition.IsNullable = nullSettingAttribute.NullSetting == NullSettings.Null; + } + + // Look for specific DbType attributed a column + SpecialDbTypeAttribute? databaseTypeAttribute = propertyInfo.FirstAttribute(); + if (databaseTypeAttribute != null) + { + definition.CustomDbType = databaseTypeAttribute.DatabaseType; + } + else + { + definition.PropertyType = propertyInfo.PropertyType; + } + + // Look for Primary Key for the current column + PrimaryKeyColumnAttribute? primaryKeyColumnAttribute = propertyInfo.FirstAttribute(); + if (primaryKeyColumnAttribute != null) + { + var primaryKeyName = string.IsNullOrEmpty(primaryKeyColumnAttribute.Name) + ? string.Format("PK_{0}", tableName) + : primaryKeyColumnAttribute.Name; + + definition.IsPrimaryKey = true; + definition.IsIdentity = primaryKeyColumnAttribute.AutoIncrement; + definition.IsIndexed = primaryKeyColumnAttribute.Clustered; + definition.PrimaryKeyName = primaryKeyName; + definition.PrimaryKeyColumns = primaryKeyColumnAttribute.OnColumns ?? string.Empty; + definition.Seeding = primaryKeyColumnAttribute.IdentitySeed; + } + + // Look for Size/Length of DbType + LengthAttribute? lengthAttribute = propertyInfo.FirstAttribute(); + if (lengthAttribute != null) + { + definition.Size = lengthAttribute.Length; + } + + // Look for Constraint for the current column + ConstraintAttribute? constraintAttribute = propertyInfo.FirstAttribute(); + if (constraintAttribute != null) + { + definition.ConstraintName = constraintAttribute.Name ?? string.Empty; + definition.DefaultValue = constraintAttribute.Default ?? string.Empty; + } + + return definition; + } + + public static ForeignKeyDefinition GetForeignKeyDefinition(Type modelType, PropertyInfo propertyInfo, ForeignKeyAttribute attribute, string columnName, string tableName) + { + TableNameAttribute? referencedTable = attribute.Type.FirstAttribute(); + PrimaryKeyAttribute? referencedPrimaryKey = attribute.Type.FirstAttribute(); + + var referencedColumn = string.IsNullOrEmpty(attribute.Column) + ? referencedPrimaryKey!.Value + : attribute.Column; + + var foreignKeyName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("FK_{0}_{1}_{2}", tableName, referencedTable!.Value, referencedColumn) + : attribute.Name; + + var definition = new ForeignKeyDefinition + { + Name = foreignKeyName, + ForeignTable = tableName, + PrimaryTable = referencedTable!.Value, + OnDelete = attribute.OnDelete, + OnUpdate = attribute.OnUpdate, + }; + definition.ForeignColumns.Add(columnName); + definition.PrimaryColumns.Add(referencedColumn); + + return definition; + } + + public static IndexDefinition GetIndexDefinition(Type modelType, PropertyInfo propertyInfo, IndexAttribute attribute, string columnName, string tableName) + { + var indexName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("IX_{0}_{1}", tableName, columnName) + : attribute.Name; + + var definition = new IndexDefinition + { + Name = indexName, + IndexType = attribute.IndexType, + ColumnName = columnName, + TableName = tableName, + }; + + if (string.IsNullOrEmpty(attribute.ForColumns) == false) + { + IEnumerable columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim()); + foreach (var column in columns) + { + definition.Columns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); + } + } + + if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + { + IEnumerable columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim()); + foreach (var column in columns) + { + definition.IncludeColumns.Add( + new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); + } + } + + return definition; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs index c6033f898d..796e52aebf 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class DeletionDataDefinition : List> { - public class DeletionDataDefinition : List> - { - - } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs index e6752159de..b1bc9c2117 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs @@ -1,27 +1,34 @@ -using System.Collections.Generic; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions -{ - public class ForeignKeyDefinition - { - public ForeignKeyDefinition() - { - ForeignColumns = new List(); - PrimaryColumns = new List(); - //Set to None by Default - OnDelete = Rule.None; - OnUpdate = Rule.None; - } +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; - public virtual string? Name { get; set; } - public virtual string? ForeignTable { get; set; } - public virtual string? ForeignTableSchema { get; set; } - public virtual string? PrimaryTable { get; set; } - public virtual string? PrimaryTableSchema { get; set; } - public virtual Rule OnDelete { get; set; } - public virtual Rule OnUpdate { get; set; } - public virtual ICollection ForeignColumns { get; set; } - public virtual ICollection PrimaryColumns { get; set; } +public class ForeignKeyDefinition +{ + public ForeignKeyDefinition() + { + ForeignColumns = new List(); + PrimaryColumns = new List(); + + // Set to None by Default + OnDelete = Rule.None; + OnUpdate = Rule.None; } + + public virtual string? Name { get; set; } + + public virtual string? ForeignTable { get; set; } + + public virtual string? ForeignTableSchema { get; set; } + + public virtual string? PrimaryTable { get; set; } + + public virtual string? PrimaryTableSchema { get; set; } + + public virtual Rule OnDelete { get; set; } + + public virtual Rule OnUpdate { get; set; } + + public virtual ICollection ForeignColumns { get; set; } + + public virtual ICollection PrimaryColumns { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs index 6f3e34e0e4..7805d2ba93 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs @@ -1,10 +1,10 @@ -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class IndexColumnDefinition { - public class IndexColumnDefinition - { - public virtual string? Name { get; set; } - public virtual Direction Direction { get; set; } - } + public virtual string? Name { get; set; } + + public virtual Direction Direction { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index 8761ae2a29..c4deba8b84 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -1,17 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions -{ - public class IndexDefinition - { - public virtual string? Name { get; set; } - public virtual string? SchemaName { get; set; } - public virtual string? TableName { get; set; } - public virtual string? ColumnName { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; - public virtual ICollection Columns { get; set; } = new List(); - public virtual ICollection IncludeColumns { get; set; } = new List(); - public IndexTypes IndexType { get; set; } - } +public class IndexDefinition +{ + public virtual string? Name { get; set; } + + public virtual string? SchemaName { get; set; } + + public virtual string? TableName { get; set; } + + public virtual string? ColumnName { get; set; } + + public virtual ICollection Columns { get; set; } = new List(); + + public virtual ICollection IncludeColumns { get; set; } = new List(); + + public IndexTypes IndexType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs index a09bcaafdf..d3b5341c87 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class InsertionDataDefinition : List> { - public class InsertionDataDefinition : List> - { - - } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs index 490b06e41d..1928e8c994 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum ModificationType { - public enum ModificationType - { - Create, - Alter, - Drop, - Rename, - Insert, - Update, - Delete - } + Create, + Alter, + Drop, + Rename, + Insert, + Update, + Delete, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs index 24daa49f35..6836b96582 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs @@ -1,10 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum SystemMethods { - public enum SystemMethods - { - NewGuid, - CurrentDateTime, - //NewSequentialId, - //CurrentUTCDateTime - } + NewGuid, + + CurrentDateTime, + + // NewSequentialId, + // CurrentUTCDateTime } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs index 4ab479b8fd..32c1cbe364 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs @@ -1,20 +1,21 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class TableDefinition { - public class TableDefinition + public TableDefinition() { - public TableDefinition() - { - Columns = new List(); - ForeignKeys = new List(); - Indexes = new List(); - } - - public virtual string Name { get; set; } = null!; - public virtual string SchemaName { get; set; } = null!; - public virtual ICollection Columns { get; set; } - public virtual ICollection ForeignKeys { get; set; } - public virtual ICollection Indexes { get; set; } + Columns = new List(); + ForeignKeys = new List(); + Indexes = new List(); } + + public virtual string Name { get; set; } = null!; + + public virtual string SchemaName { get; set; } = null!; + + public virtual ICollection Columns { get; set; } + + public virtual ICollection ForeignKeys { get; set; } + + public virtual ICollection Indexes { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs index d0ad59fbb8..1ea941932e 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs @@ -27,7 +27,7 @@ public static class DatabaseProviderMetadataExtensions /// true if a database can be created for the specified provider name; otherwise, false. /// public static bool CanForceCreateDatabase(this IEnumerable databaseProviderMetadata, string? providerName) - => databaseProviderMetadata.FirstOrDefault(x => x.ProviderName == providerName)?.ForceCreateDatabase == true; + => databaseProviderMetadata.FirstOrDefault(x => string.Equals(x.ProviderName, providerName, StringComparison.InvariantCultureIgnoreCase))?.ForceCreateDatabase == true; /// /// Generates the connection string. diff --git a/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs index f70da7c8fb..9201d79c2f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs @@ -1,29 +1,33 @@ -using System.Data; +using System.Data; +using StackExchange.Profiling.Data; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class DbCommandExtensions { - internal static class DbCommandExtensions + /// + /// Unwraps a database command. + /// + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database command. + /// + public static IDbCommand UnwrapUmbraco(this IDbCommand command) { - /// - /// Unwraps a database command. - /// - /// UmbracoDatabase wraps the original database connection in various layers (see - /// OnConnectionOpened); this unwraps and returns the original database command. - public static IDbCommand UnwrapUmbraco(this IDbCommand command) + IDbCommand unwrapped; + + IDbCommand c = command; + do { - IDbCommand unwrapped; + unwrapped = c; - var c = command; - do + if (unwrapped is ProfiledDbCommand profiled) { - unwrapped = c; - - var profiled = unwrapped as StackExchange.Profiling.Data.ProfiledDbCommand; - if (profiled != null) unwrapped = profiled.InternalCommand; - - } while (c != unwrapped); - - return unwrapped; + unwrapped = profiled.InternalCommand; + } } + while (c != unwrapped); + + return unwrapped; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs index 61601fef95..e4bec34987 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Data; using System.Data.Common; using Microsoft.Extensions.Logging; @@ -6,68 +5,71 @@ using StackExchange.Profiling.Data; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class DbConnectionExtensions { - public static class DbConnectionExtensions + public static bool IsConnectionAvailable(string? connectionString, DbProviderFactory? factory) { - public static bool IsConnectionAvailable(string? connectionString, DbProviderFactory? factory) + DbConnection? connection = factory?.CreateConnection(); + + if (connection == null) { - var connection = factory?.CreateConnection(); - - if (connection == null) - throw new InvalidOperationException($"Could not create a connection for provider \"{factory}\"."); - - connection.ConnectionString = connectionString; - using (connection) - { - return connection.IsAvailable(); - } + throw new InvalidOperationException($"Could not create a connection for provider \"{factory}\"."); } - public static bool IsAvailable(this IDbConnection connection) + connection.ConnectionString = connectionString; + using (connection) { - try - { - connection.Open(); - connection.Close(); - } - catch (DbException e) - { - // Don't swallow this error, the exception is super handy for knowing "why" its not available - StaticApplicationLogging.Logger.LogWarning(e, "Configured database is reporting as not being available."); - return false; - } - - return true; - } - - /// - /// Unwraps a database connection. - /// - /// UmbracoDatabase wraps the original database connection in various layers (see - /// OnConnectionOpened); this unwraps and returns the original database connection. - internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) - { - var unwrapped = connection; - - IDbConnection c; - do - { - c = unwrapped; - - if (unwrapped is ProfiledDbConnection profiled) - { - unwrapped = profiled.WrappedConnection; - } - - if (unwrapped is RetryDbConnection retrying) - { - unwrapped = retrying.Inner; - } - } - while (c != unwrapped); - - return unwrapped; + return connection.IsAvailable(); } } + + public static bool IsAvailable(this IDbConnection connection) + { + try + { + connection.Open(); + connection.Close(); + } + catch (DbException e) + { + // Don't swallow this error, the exception is super handy for knowing "why" its not available + StaticApplicationLogging.Logger.LogWarning(e, "Configured database is reporting as not being available."); + return false; + } + + return true; + } + + /// + /// Unwraps a database connection. + /// + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database connection. + /// + internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) + { + IDbConnection? unwrapped = connection; + + IDbConnection c; + do + { + c = unwrapped; + + if (unwrapped is ProfiledDbConnection profiled) + { + unwrapped = profiled.WrappedConnection; + } + + if (unwrapped is RetryDbConnection retrying) + { + unwrapped = retrying.Inner; + } + } + while (c != unwrapped); + + return unwrapped; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 0177475609..acb90da5b2 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; using System.Data.Common; -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence -{ - public class DbProviderFactoryCreator : IDbProviderFactoryCreator - { - private readonly Func _getFactory; - private readonly IEnumerable _providerSpecificInterceptors; - private readonly IDictionary _databaseCreators; - private readonly IDictionary _syntaxProviders; - private readonly IDictionary _bulkSqlInsertProviders; - private readonly IDictionary _providerSpecificMapperFactories; +namespace Umbraco.Cms.Infrastructure.Persistence; - [Obsolete("Please use an alternative constructor.")] - public DbProviderFactoryCreator( - Func getFactory, - IEnumerable syntaxProviders, - IEnumerable bulkSqlInsertProviders, - IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories) +public class DbProviderFactoryCreator : IDbProviderFactoryCreator +{ + private readonly IDictionary _bulkSqlInsertProviders; + private readonly IDictionary _databaseCreators; + private readonly Func _getFactory; + private readonly IEnumerable _providerSpecificInterceptors; + private readonly IDictionary _providerSpecificMapperFactories; + private readonly IDictionary _syntaxProviders; + + [Obsolete("Please use an alternative constructor.")] + public DbProviderFactoryCreator( + Func getFactory, + IEnumerable syntaxProviders, + IEnumerable bulkSqlInsertProviders, + IEnumerable databaseCreators, + IEnumerable providerSpecificMapperFactories) : this( getFactory, syntaxProviders, @@ -30,74 +27,76 @@ namespace Umbraco.Cms.Infrastructure.Persistence databaseCreators, providerSpecificMapperFactories, Enumerable.Empty()) - { - } - - public DbProviderFactoryCreator( - Func getFactory, - IEnumerable syntaxProviders, - IEnumerable bulkSqlInsertProviders, - IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories, - IEnumerable providerSpecificInterceptors) - - { - _getFactory = getFactory; - _providerSpecificInterceptors = providerSpecificInterceptors; - _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); - _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); - _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); - _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); - } - - public DbProviderFactory? CreateFactory(string? providerName) - { - if (string.IsNullOrEmpty(providerName)) - return null; - return _getFactory(providerName); - } - - // gets the sql syntax provider that corresponds, from attribute - public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) - { - - if (!_syntaxProviders.TryGetValue(providerName, out var result)) - { - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - } - - return result; - } - - public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) - { - if (!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) - { - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - } - - return result; - } - - public void CreateDatabase(string providerName, string connectionString) - { - if (_databaseCreators.TryGetValue(providerName, out var creator)) - { - creator.Create(connectionString); - } - } - - public NPocoMapperCollection ProviderSpecificMappers(string providerName) - { - if (_providerSpecificMapperFactories.TryGetValue(providerName, out var mapperFactory)) - { - return mapperFactory.Mappers; - } - - return new NPocoMapperCollection(() => Enumerable.Empty()); - } - - public IEnumerable GetProviderSpecificInterceptors(string providerName) - => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); + { } + + public DbProviderFactoryCreator( + Func getFactory, + IEnumerable syntaxProviders, + IEnumerable bulkSqlInsertProviders, + IEnumerable databaseCreators, + IEnumerable providerSpecificMapperFactories, + IEnumerable providerSpecificInterceptors) + { + _getFactory = getFactory; + _providerSpecificInterceptors = providerSpecificInterceptors; + _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); + _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); + _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); + _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); + } + + public DbProviderFactory? CreateFactory(string? providerName) + { + if (string.IsNullOrEmpty(providerName)) + { + return null; + } + + return _getFactory(providerName); + } + + // gets the sql syntax provider that corresponds, from attribute + public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) + { + if (!_syntaxProviders.TryGetValue(providerName, out ISqlSyntaxProvider? result)) + { + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); + } + + return result; + } + + public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) + { + if (!_bulkSqlInsertProviders.TryGetValue(providerName, out IBulkSqlInsertProvider? result)) + { + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); + } + + return result; + } + + public void CreateDatabase(string providerName, string connectionString) + { + if (_databaseCreators.TryGetValue(providerName, out IDatabaseCreator? creator)) + { + creator.Create(connectionString); + } + } + + public NPocoMapperCollection ProviderSpecificMappers(string providerName) + { + if (_providerSpecificMapperFactories.TryGetValue( + providerName, + out IProviderSpecificMapperFactory? mapperFactory)) + { + return mapperFactory.Mappers; + } + + return new NPocoMapperCollection(() => Enumerable.Empty()); + } + + public IEnumerable GetProviderSpecificInterceptors(string providerName) + => _providerSpecificInterceptors.Where(x => x.ProviderName.Equals(providerName, StringComparison.InvariantCultureIgnoreCase)); } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs index e8842b7cfd..354083dfa8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs @@ -1,43 +1,41 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Access)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class AccessDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Access)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class AccessDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoAccess", AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoAccess", AutoIncrement = false)] + public Guid Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoAccess_nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoAccess_nodeId")] + public int NodeId { get; set; } - [Column("loginNodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id1")] - public int LoginNodeId { get; set; } + [Column("loginNodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id1")] + public int LoginNodeId { get; set; } - [Column("noAccessNodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")] - public int NoAccessNodeId { get; set; } + [Column("noAccessNodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")] + public int NoAccessNodeId { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "AccessId")] - public List Rules { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "AccessId")] + public List Rules { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs index 307f91337b..3aba928bda 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs @@ -1,36 +1,35 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.AccessRule)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class AccessRuleDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.AccessRule)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class AccessRuleDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoAccessRule", AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoAccessRule", AutoIncrement = false)] + public Guid Id { get; set; } - [Column("accessId")] - [ForeignKey(typeof(AccessDto), Name = "FK_umbracoAccessRule_umbracoAccess_id")] - public Guid AccessId { get; set; } + [Column("accessId")] + [ForeignKey(typeof(AccessDto), Name = "FK_umbracoAccessRule_umbracoAccess_id")] + public Guid AccessId { get; set; } - [Column("ruleValue")] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "ruleValue,ruleType,accessId", Name = "IX_umbracoAccessRule")] - public string? RuleValue { get; set; } + [Column("ruleValue")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "ruleValue,ruleType,accessId", Name = "IX_umbracoAccessRule")] + public string? RuleValue { get; set; } - [Column("ruleType")] - public string? RuleType { get; set; } + [Column("ruleType")] + public string? RuleType { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs index 18ffda302e..38e63ffc20 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs @@ -1,57 +1,53 @@ -using System; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.AuditEntry)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class AuditEntryDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.AuditEntry)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class AuditEntryDto - { + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + // there is NO foreign key to the users table here, neither for performing user nor for + // affected user, so we can delete users and NOT delete the associated audit trails, and + // users can still be identified via the details free-form text fields. + [Column("performingUserId")] + public int PerformingUserId { get; set; } - // there is NO foreign key to the users table here, neither for performing user nor for - // affected user, so we can delete users and NOT delete the associated audit trails, and - // users can still be identified via the details free-form text fields. + [Column("performingDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? PerformingDetails { get; set; } - [Column("performingUserId")] - public int PerformingUserId { get; set; } + [Column("performingIp")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.IpLength)] + public string? PerformingIp { get; set; } - [Column("performingDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? PerformingDetails { get; set; } + [Column("eventDateUtc")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime EventDateUtc { get; set; } - [Column("performingIp")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.IpLength)] - public string? PerformingIp { get; set; } + [Column("affectedUserId")] + public int AffectedUserId { get; set; } - [Column("eventDateUtc")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime EventDateUtc { get; set; } + [Column("affectedDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? AffectedDetails { get; set; } - [Column("affectedUserId")] - public int AffectedUserId { get; set; } + [Column("eventType")] + [Length(Constants.Audit.EventTypeLength)] + public string? EventType { get; set; } - [Column("affectedDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? AffectedDetails { get; set; } - - [Column("eventType")] - [Length(Constants.Audit.EventTypeLength)] - public string? EventType { get; set; } - - [Column("eventDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? EventDetails { get; set; } - } + [Column("eventDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? EventDetails { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs index 431502a896..2c1c68c1f0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs @@ -1,16 +1,15 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +internal class AxisDefintionDto { - internal class AxisDefintionDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + public int NodeId { get; set; } - [Column("alias")] - public string? Alias { get; set; } + [Column("alias")] + public string? Alias { get; set; } - [Column("ParentID")] - public int ParentId { get; set; } - } + [Column("ParentID")] + public int ParentId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs index 7d57fca606..0e73112f76 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs @@ -1,36 +1,35 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.CacheInstruction)] +[PrimaryKey("id")] +[ExplicitColumns] +public class CacheInstructionDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.CacheInstruction)] - [PrimaryKey("id")] - [ExplicitColumns] - public class CacheInstructionDto - { - [Column("id")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] - public int Id { get; set; } + [Column("id")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] + public int Id { get; set; } - [Column("utcStamp")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime UtcStamp { get; set; } + [Column("utcStamp")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime UtcStamp { get; set; } - [Column("jsonInstruction")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Instructions { get; set; } = null!; + [Column("jsonInstruction")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Instructions { get; set; } = null!; - [Column("originated")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(500)] - public string OriginIdentity { get; set; } = null!; + [Column("originated")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(500)] + public string OriginIdentity { get; set; } = null!; - [Column("instructionCount")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = 1)] - public int InstructionCount { get; set; } - } + [Column("instructionCount")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = 1)] + public int InstructionCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs index e0c9b73c78..c6f9006b29 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs @@ -1,43 +1,42 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Consent)] +[PrimaryKey("id")] +[ExplicitColumns] +public class ConsentDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Consent)] - [PrimaryKey("id")] - [ExplicitColumns] - public class ConsentDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("current")] - public bool Current { get; set; } + [Column("current")] + public bool Current { get; set; } - [Column("source")] - [Length(512)] - public string? Source { get; set; } + [Column("source")] + [Length(512)] + public string? Source { get; set; } - [Column("context")] - [Length(128)] - public string? Context { get; set; } + [Column("context")] + [Length(128)] + public string? Context { get; set; } - [Column("action")] - [Length(512)] - public string? Action { get; set; } + [Column("action")] + [Length(512)] + public string? Action { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("state")] - public int State { get; set; } + [Column("state")] + public int State { get; set; } - [Column("comment")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Comment { get; set; } - } + [Column("comment")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Comment { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs index 232f055e85..21e94349bd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs @@ -1,33 +1,33 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class ContentDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class ContentDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Content; + public const string TableName = Constants.DatabaseSchema.Tables.Content; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "NodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "NodeId")] + public int ContentTypeId { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; - // although a content has many content versions, - // they can only be loaded one by one (as several content), - // so this here is a OneToOne reference - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + // although a content has many content versions, + // they can only be loaded one by one (as several content), + // so this here is a OneToOne reference + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs index eb3077cb3b..7f64054d14 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs @@ -1,40 +1,38 @@ using System.Data; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.NodeData)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class ContentNuDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.NodeData)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class ContentNuDto - { - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")] - [ForeignKey(typeof(ContentDto), Column = "nodeId", OnDelete = Rule.Cascade)] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")] + [ForeignKey(typeof(ContentDto), Column = "nodeId", OnDelete = Rule.Cascade)] + public int NodeId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] + public bool Published { get; set; } - /// - /// Stores serialized JSON representing the content item's property and culture name values - /// - /// - /// Pretty much anything that would require a 1:M lookup is serialized here - /// - [Column("data")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Data { get; set; } + /// + /// Stores serialized JSON representing the content item's property and culture name values + /// + /// + /// Pretty much anything that would require a 1:M lookup is serialized here + /// + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Data { get; set; } - [Column("rv")] - public long Rv { get; set; } + [Column("rv")] + public long Rv { get; set; } - [Column("dataRaw")] - [NullSetting(NullSetting = NullSettings.Null)] - public byte[]? RawData { get; set; } - - - } + [Column("dataRaw")] + [NullSetting(NullSetting = NullSettings.Null)] + public byte[]? RawData { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs index d50da8a124..ad4c03ac53 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs @@ -1,33 +1,32 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentScheduleDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentScheduleDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentSchedule; + public const string TableName = Constants.DatabaseSchema.Tables.ContentSchedule; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [NullSetting(NullSetting = NullSettings.Null)] // can be invariant - public int? LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [NullSetting(NullSetting = NullSettings.Null)] // can be invariant + public int? LanguageId { get; set; } - [Column("date")] - public DateTime Date { get; set; } + [Column("date")] + public DateTime Date { get; set; } - [Column("action")] - public string? Action { get; set; } - } + [Column("action")] + public string? Action { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs index 2bda31c1fc..3931f2c5e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs @@ -1,19 +1,19 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ElementTypeTree)] - [ExplicitColumns] - internal class ContentType2ContentTypeDto - { - [Column("parentContentTypeId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentType2ContentType", OnColumns = "parentContentTypeId, childContentTypeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_parent")] - public int ParentId { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("childContentTypeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_child")] - public int ChildId { get; set; } - } +[TableName(Constants.DatabaseSchema.Tables.ElementTypeTree)] +[ExplicitColumns] +internal class ContentType2ContentTypeDto +{ + [Column("parentContentTypeId")] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentType2ContentType", OnColumns = "parentContentTypeId, childContentTypeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_parent")] + public int ParentId { get; set; } + + [Column("childContentTypeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_child")] + public int ChildId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs index c9a4b274b7..fec7983d1f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs @@ -1,28 +1,28 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.ContentChildType)] +[PrimaryKey("Id", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentTypeAllowedContentTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType)] - [PrimaryKey("Id", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentTypeAllowedContentTypeDto - { - [Column("Id")] - [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType", Column = "nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentTypeAllowedContentType", OnColumns = "Id, AllowedId")] - public int Id { get; set; } + [Column("Id")] + [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType", Column = "nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentTypeAllowedContentType", OnColumns = "Id, AllowedId")] + public int Id { get; set; } - [Column("AllowedId")] - [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType1", Column = "nodeId")] - public int AllowedId { get; set; } + [Column("AllowedId")] + [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType1", Column = "nodeId")] + public int AllowedId { get; set; } - [Column("SortOrder")] - [Constraint(Name = "df_cmsContentTypeAllowedContentType_sortOrder", Default = "0")] - public int SortOrder { get; set; } + [Column("SortOrder")] + [Constraint(Name = "df_cmsContentTypeAllowedContentType_sortOrder", Default = "0")] + public int SortOrder { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentTypeDto? ContentTypeDto { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentTypeDto? ContentTypeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs index 8d4019de09..75df880478 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs @@ -1,61 +1,61 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class ContentTypeDto { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class ContentTypeDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentType; - private string? _alias; + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + private string? _alias; - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 700)] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 700)] + public int PrimaryKey { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string? Thumbnail { get; set; } + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string? Thumbnail { get; set; } - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string? Description { get; set; } + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string? Description { get; set; } - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } - [Column("isElement")] - [Constraint(Default = "0")] - public bool IsElement { get; set; } + [Column("isElement")] + [Constraint(Default = "0")] + public bool IsElement { get; set; } - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs index 0b79aeb7aa..ad653f2759 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs @@ -1,29 +1,29 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.DocumentType)] +[PrimaryKey("contentTypeNodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentTypeTemplateDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentType)] - [PrimaryKey("contentTypeNodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentTypeTemplateDto - { - [Column("contentTypeNodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", OnColumns = "contentTypeNodeId, templateNodeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - [ForeignKey(typeof(NodeDto))] - public int ContentTypeNodeId { get; set; } + [Column("contentTypeNodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", OnColumns = "contentTypeNodeId, templateNodeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + [ForeignKey(typeof(NodeDto))] + public int ContentTypeNodeId { get; set; } - [Column("templateNodeId")] - [ForeignKey(typeof(TemplateDto), Column = "nodeId")] - public int TemplateNodeId { get; set; } + [Column("templateNodeId")] + [ForeignKey(typeof(TemplateDto), Column = "nodeId")] + public int TemplateNodeId { get; set; } - [Column("IsDefault")] - [Constraint(Default = "0")] - public bool IsDefault { get; set; } + [Column("IsDefault")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentTypeDto? ContentTypeDto { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentTypeDto? ContentTypeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs index 4b2faa166f..da0771df01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs @@ -1,34 +1,32 @@ -using System; using System.Data; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("contentTypeId", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentVersionCleanupPolicyDto { - [TableName(TableName)] - [PrimaryKey("contentTypeId", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentVersionCleanupPolicyDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy; + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy; - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId", OnDelete = Rule.Cascade)] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId", OnDelete = Rule.Cascade)] + public int ContentTypeId { get; set; } - [Column("preventCleanup")] - public bool PreventCleanup { get; set; } + [Column("preventCleanup")] + public bool PreventCleanup { get; set; } - [Column("keepAllVersionsNewerThanDays")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? KeepAllVersionsNewerThanDays { get; set; } + [Column("keepAllVersionsNewerThanDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepAllVersionsNewerThanDays { get; set; } - [Column("keepLatestVersionPerDayForDays")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? KeepLatestVersionPerDayForDays { get; set; } + [Column("keepLatestVersionPerDayForDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepLatestVersionPerDayForDays { get; set; } - [Column("updated")] - public DateTime Updated { get; set; } - } + [Column("updated")] + public DateTime Updated { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs index 32307efb2b..48c6ee97ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -1,44 +1,47 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class ContentVersionCultureVariationDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class ContentVersionCultureVariationDto + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; + private int? _updateUserId; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,languageId")] + public int VersionId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } + + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string? Culture { get; set; } + + [Column("name")] + public string? Name { get; set; } + + [Column("date")] // TODO: db rename to 'updateDate' + public DateTime UpdateDate { get; set; } + + [Column("availableUserId")] // TODO: db rename to 'updateDate' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UpdateUserId { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; - private int? _updateUserId; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,languageId")] - public int VersionId { get; set; } - - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - public int LanguageId { get; set; } - - // this is convenient to carry the culture around, but has no db counterpart - [Ignore] - public string? Culture { get; set; } - - [Column("name")] - public string? Name { get; set; } - - [Column("date")] // TODO: db rename to 'updateDate' - public DateTime UpdateDate { get; set; } - - [Column("availableUserId")] // TODO: db rename to 'updateDate' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UpdateUserId { get => _updateUserId == 0 ? null : _updateUserId; set => _updateUserId = value; } //return null if zero - } + get => _updateUserId == 0 ? null : _updateUserId; + set => _updateUserId = value; + } // return null if zero } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs index a000811c55..3a6aae2aff 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs @@ -1,57 +1,55 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +public class ContentVersionDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - public class ContentVersionDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion; - private int? _userId; + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersion; + private int? _userId; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(ContentDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId")] + public int NodeId { get; set; } - [Column("versionDate")] // TODO: db rename to 'updateDate' - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime VersionDate { get; set; } + [Column("versionDate")] // TODO: db rename to 'updateDate' + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime VersionDate { get; set; } - [Column("userId")] // TODO: db rename to 'updateUserId' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + [Column("userId")] // TODO: db rename to 'updateUserId' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } // return null if zero - [Column("current")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")] - public bool Current { get; set; } + [Column("current")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")] + public bool Current { get; set; } - // about current: - // there is nothing in the DB that guarantees that there will be one, and exactly one, current version per content item. - // that would require circular FKs that are impossible (well, it is possible to create them, but not to insert). - // we could use a content.currentVersionId FK that would need to be nullable, or (better?) an additional table - // linking a content itemt to its current version (nodeId, versionId) - that would guarantee uniqueness BUT it would - // not guarantee existence - so, really... we are trusting our code to manage 'current' correctly. + // about current: + // there is nothing in the DB that guarantees that there will be one, and exactly one, current version per content item. + // that would require circular FKs that are impossible (well, it is possible to create them, but not to insert). + // we could use a content.currentVersionId FK that would need to be nullable, or (better?) an additional table + // linking a content itemt to its current version (nodeId, versionId) - that would guarantee uniqueness BUT it would + // not guarantee existence - so, really... we are trusting our code to manage 'current' correctly. + [Column("text")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Text { get; set; } - [Column("text")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Text { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId", ReferenceMemberName = "NodeId")] + public ContentDto? ContentDto { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId", ReferenceMemberName = "NodeId")] - public ContentDto? ContentDto { get; set; } - - [Column("preventCleanup")] - [Constraint(Default = "0")] - public bool PreventCleanup { get; set; } - } + [Column("preventCleanup")] + [Constraint(Default = "0")] + public bool PreventCleanup { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs index ae6b922657..060c9d5a23 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs @@ -1,38 +1,37 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("id")] +public class CreatedPackageSchemaDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("id")] - public class CreatedPackageSchemaDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.CreatedPackageSchema; + public const string TableName = Constants.DatabaseSchema.Tables.CreatedPackageSchema; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("name")] - [Length(255)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] - public string Name { get; set; } = null!; + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } = null!; - [Column("value")] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Value { get; set; } = null!; + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } = null!; - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } - [Column("packageId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public Guid PackageId { get; set; } - } + [Column("packageId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid PackageId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs index c51ce4947c..f3d376b078 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs @@ -1,32 +1,32 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.DataType)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class DataTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.DataType)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class DataTypeDto - { - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("propertyEditorAlias")] - public string EditorAlias { get; set; } = null!; // TODO: should this have a length + [Column("propertyEditorAlias")] + public string EditorAlias { get; set; } = null!; // TODO: should this have a length - [Column("dbType")] - [Length(50)] - public string DbType { get; set; } = null!; + [Column("dbType")] + [Length(50)] + public string DbType { get; set; } = null!; - [Column("config")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Configuration { get; set; } + [Column("config")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Configuration { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs index ad14f20c6b..8d93616ac8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs @@ -1,38 +1,36 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +public class DictionaryDto // public as required to be accessible from Deploy for the RepairDictionaryIdsWorkItem. { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - public class DictionaryDto // public as required to be accessible from Deploy for the RepairDictionaryIdsWorkItem. - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DictionaryEntry; + public const string TableName = Constants.DatabaseSchema.Tables.DictionaryEntry; - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("id")] - [Index(IndexTypes.UniqueNonClustered)] - public Guid UniqueId { get; set; } + [Column("id")] + [Index(IndexTypes.UniqueNonClustered)] + public Guid UniqueId { get; set; } - [Column("parent")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(DictionaryDto), Column = "id")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Parent")] - public Guid? Parent { get; set; } + [Column("parent")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(DictionaryDto), Column = "id")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Parent")] + public Guid? Parent { get; set; } - [Column("key")] - [Length(450)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_key")] - public string Key { get; set; } = null!; + [Column("key")] + [Length(450)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_key")] + public string Key { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.Many, ColumnName = "UniqueId", ReferenceMemberName = "UniqueId")] - public List? LanguageTextDtos { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.Many, ColumnName = "UniqueId", ReferenceMemberName = "UniqueId")] + public List? LanguageTextDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs index e13d19ae34..2bd9f559ec 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs @@ -1,52 +1,52 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class DocumentCultureVariationDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class DocumentCultureVariationDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation; + public const string TableName = Constants.DatabaseSchema.Tables.DocumentCultureVariation; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] + public int NodeId { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - public int LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } - // this is convenient to carry the culture around, but has no db counterpart - [Ignore] - public string? Culture { get; set; } + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string? Culture { get; set; } - // authority on whether a culture has been edited - [Column("edited")] - public bool Edited { get; set; } + // authority on whether a culture has been edited + [Column("edited")] + public bool Edited { get; set; } - // de-normalized for perfs - // (means there is a current content version culture variation for the language) - [Column("available")] - public bool Available { get; set; } + // de-normalized for perfs + // (means there is a current content version culture variation for the language) + [Column("available")] + public bool Available { get; set; } - // de-normalized for perfs - // (means there is a published content version culture variation for the language) - [Column("published")] - public bool Published { get; set; } + // de-normalized for perfs + // (means there is a published content version culture variation for the language) + [Column("published")] + public bool Published { get; set; } - // de-normalized for perfs - // (when available, copies name from current content version culture variation for the language) - // (otherwise, it's the published one, 'cos we need to have one) - [Column("name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } - } + // de-normalized for perfs + // (when available, copies name from current content version culture variation for the language) + // (otherwise, it's the published one, 'cos we need to have one) + [Column("name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs index 39e4e933b2..715d588ff4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs @@ -1,58 +1,56 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class DocumentDto { + private const string TableName = Constants.DatabaseSchema.Tables.Document; - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class DocumentDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Document; + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } + [Column("published")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")] + public bool Published { get; set; } - [Column("published")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")] - public bool Published { get; set; } + [Column("edited")] + public bool Edited { get; set; } - [Column("edited")] - public bool Edited { get; set; } + // [Column("publishDate")] + // [NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version + // public DateTime? PublishDate { get; set; } - //[Column("publishDate")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version - //public DateTime? PublishDate { get; set; } + // [Column("publishUserId")] + // [NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.UserId for the published version + // public int? PublishUserId { get; set; } - //[Column("publishUserId")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.UserId for the published version - //public int? PublishUserId { get; set; } + // [Column("publishName")] + // [NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.Text for the published version + // public string PublishName { get; set; } - //[Column("publishName")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.Text for the published version - //public string PublishName { get; set; } + // [Column("publishTemplateId")] + // [NullSetting(NullSetting = NullSettings.Null)] // is documentVersionDto.TemplateId for the published version + // public int? PublishTemplateId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; - //[Column("publishTemplateId")] - //[NullSetting(NullSetting = NullSettings.Null)] // is documentVersionDto.TemplateId for the published version - //public int? PublishTemplateId { get; set; } + // although a content has many content versions, + // they can only be loaded one by one (as several content), + // so this here is a OneToOne reference + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public DocumentVersionDto DocumentVersionDto { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; - - // although a content has many content versions, - // they can only be loaded one by one (as several content), - // so this here is a OneToOne reference - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public DocumentVersionDto DocumentVersionDto { get; set; } = null!; - - // same - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public DocumentVersionDto PublishedVersionDto { get; set; } = null!; - } + // same + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public DocumentVersionDto? PublishedVersionDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs index a6fcd6b319..2f0b2ed5f5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs @@ -1,26 +1,25 @@ -using System; using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Document)] +[PrimaryKey("versionId", AutoIncrement = false)] +[ExplicitColumns] +internal class DocumentPublishedReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Document)] - [PrimaryKey("versionId", AutoIncrement = false)] - [ExplicitColumns] - internal class DocumentPublishedReadOnlyDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + public int NodeId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] + public bool Published { get; set; } - [Column("versionId")] - public Guid VersionId { get; set; } + [Column("versionId")] + public Guid VersionId { get; set; } - [Column("newest")] - public bool Newest { get; set; } + [Column("newest")] + public bool Newest { get; set; } - [Column("updateDate")] - public DateTime VersionDate { get; set; } - } + [Column("updateDate")] + public DateTime VersionDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs index 2d06129ba6..75dea080a2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs @@ -1,30 +1,30 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +public class DocumentVersionDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - public class DocumentVersionDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion; + private const string TableName = Constants.DatabaseSchema.Tables.DocumentVersion; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentVersionDto))] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentVersionDto))] + public int Id { get; set; } - [Column("templateId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(TemplateDto), Column = "nodeId")] - public int? TemplateId { get; set; } + [Column("templateId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(TemplateDto), Column = "nodeId")] + public int? TemplateId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] + public bool Published { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs index 60f0635035..31a04fd664 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs @@ -1,33 +1,33 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Domain)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class DomainDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Domain)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class DomainDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("domainDefaultLanguage")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? DefaultLanguage { get; set; } + [Column("domainDefaultLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? DefaultLanguage { get; set; } - [Column("domainRootStructureID")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto))] - public int? RootStructureId { get; set; } + [Column("domainRootStructureID")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto))] + public int? RootStructureId { get; set; } - [Column("domainName")] - public string DomainName { get; set; } = null!; + [Column("domainName")] + public string DomainName { get; set; } = null!; - /// - /// Used for a result on the query to get the associated language for a domain if there is one - /// - [ResultColumn("languageISOCode")] - public string IsoCode { get; set; } = null!; - } + /// + /// Used for a result on the query to get the associated language for a domain if there is one + /// + [ResultColumn("languageISOCode")] + public string IsoCode { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index b6eae7f234..017ab3c6e4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -1,57 +1,56 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class ExternalLoginDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class ExternalLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin; + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Obsolete("This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] - [ResultColumn("userId")] - public int? UserId { get; set; } + [Obsolete("This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [ResultColumn("userId")] + public int? UserId { get; set; } - [Column("userOrMemberKey")] - [Index(IndexTypes.NonClustered)] - public Guid UserOrMemberKey { get; set; } + [Column("userOrMemberKey")] + [Index(IndexTypes.NonClustered)] + public Guid UserOrMemberKey { get; set; } - /// - /// Used to store the name of the provider (i.e. Facebook, Google) - /// - [Column("loginProvider")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", Name = "IX_" + TableName + "_LoginProvider")] - public string LoginProvider { get; set; } = null!; + /// + /// Used to store the name of the provider (i.e. Facebook, Google) + /// + [Column("loginProvider")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", Name = "IX_" + TableName + "_LoginProvider")] + public string LoginProvider { get; set; } = null!; - /// - /// Stores the key the provider uses to lookup the login - /// - [Column("providerKey")] - [Length(4000)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")] - public string ProviderKey { get; set; } = null!; + /// + /// Stores the key the provider uses to lookup the login + /// + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")] + public string ProviderKey { get; set; } = null!; - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider - /// - [Column("userData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? UserData { get; set; } - } + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? UserData { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs index cd16703bdc..b9ae050960 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs @@ -1,42 +1,41 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class ExternalLoginTokenDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class ExternalLoginTokenDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLoginToken; + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLoginToken; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("externalLoginId")] - [ForeignKey(typeof(ExternalLoginDto), Column = "id")] - public int ExternalLoginId { get; set; } + [Column("externalLoginId")] + [ForeignKey(typeof(ExternalLoginDto), Column = "id")] + public int ExternalLoginId { get; set; } - [Column("name")] - [Length(255)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "externalLoginId,name", Name = "IX_" + TableName + "_Name")] - public string Name { get; set; } = null!; + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "externalLoginId,name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } = null!; - [Column("value")] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Value { get; set; } = null!; + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } = null!; - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "ExternalLoginId")] - public ExternalLoginDto ExternalLoginDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "ExternalLoginId")] + public ExternalLoginDto ExternalLoginDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs index 654d3071b0..c5829873fe 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs @@ -1,26 +1,25 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.KeyValue)] +[PrimaryKey("key", AutoIncrement = false)] +[ExplicitColumns] +internal class KeyValueDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue)] - [PrimaryKey("key", AutoIncrement = false)] - [ExplicitColumns] - internal class KeyValueDto - { - [Column("key")] - [Length(256)] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true)] - public string Key { get; set; } = null!; + [Column("key")] + [Length(256)] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true)] + public string Key { get; set; } = null!; - [Column("value")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Value { get; set; } + [Column("value")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Value { get; set; } - [Column("updated")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - } + [Column("updated")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs index e5b25fa166..bcf8403b73 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs @@ -1,60 +1,60 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LanguageDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LanguageDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Language; + public const string TableName = Constants.DatabaseSchema.Tables.Language; - /// - /// Gets or sets the identifier of the language. - /// - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 2)] - public short Id { get; set; } + /// + /// Gets or sets the identifier of the language. + /// + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 2)] + public short Id { get; set; } - /// - /// Gets or sets the ISO code of the language. - /// - [Column("languageISOCode")] - [Index(IndexTypes.UniqueNonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(14)] - public string? IsoCode { get; set; } + /// + /// Gets or sets the ISO code of the language. + /// + [Column("languageISOCode")] + [Index(IndexTypes.UniqueNonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(14)] + public string? IsoCode { get; set; } - /// - /// Gets or sets the culture name of the language. - /// - [Column("languageCultureName")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(100)] - public string? CultureName { get; set; } + /// + /// Gets or sets the culture name of the language. + /// + [Column("languageCultureName")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(100)] + public string? CultureName { get; set; } - /// - /// Gets or sets a value indicating whether the language is the default language. - /// - [Column("isDefaultVariantLang")] - [Constraint(Default = "0")] - public bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether the language is the default language. + /// + [Column("isDefaultVariantLang")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } - /// - /// Gets or sets a value indicating whether the language is mandatory. - /// - [Column("mandatory")] - [Constraint(Default = "0")] - public bool IsMandatory { get; set; } + /// + /// Gets or sets a value indicating whether the language is mandatory. + /// + [Column("mandatory")] + [Constraint(Default = "0")] + public bool IsMandatory { get; set; } - /// - /// Gets or sets the identifier of a fallback language. - /// - [Column("fallbackLanguageId")] - [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FallbackLanguageId { get; set; } - } + /// + /// Gets or sets the identifier of a fallback language. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs index 3d08c9de98..1cbc3a9a15 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs @@ -1,31 +1,30 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +public class LanguageTextDto { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - public class LanguageTextDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DictionaryValue; + public const string TableName = Constants.DatabaseSchema.Tables.DictionaryValue; - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_languageId", ForColumns = "languageId,UniqueId")] - public int LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_languageId", ForColumns = "languageId,UniqueId")] + public int LanguageId { get; set; } - [Column("UniqueId")] - [ForeignKey(typeof(DictionaryDto), Column = "id")] - public Guid UniqueId { get; set; } + [Column("UniqueId")] + [ForeignKey(typeof(DictionaryDto), Column = "id")] + public Guid UniqueId { get; set; } - [Column("value")] - [Length(1000)] - public string Value { get; set; } = null!; - } + [Column("value")] + [Length(1000)] + public string Value { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs index 21fe45c22b..5b1fce4623 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs @@ -1,24 +1,24 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Lock)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class LockDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Lock)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class LockDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoLock", AutoIncrement = false)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoLock", AutoIncrement = false)] + public int Id { get; set; } - [Column("value")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int Value { get; set; } = 1; + [Column("value")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int Value { get; set; } = 1; - [Column("name")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(64)] - public string Name { get; set; } = null!; - } + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(64)] + public string Name { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs index 3b2009f3da..b464d6628d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs @@ -1,61 +1,60 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LogDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LogDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Log; + public const string TableName = Constants.DatabaseSchema.Tables.Log; - private int? _userId; + private int? _userId; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("userId")] - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + [Column("userId")] + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } // return null if zero - [Column("NodeId")] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoLog")] - public int NodeId { get; set; } + [Column("NodeId")] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoLog")] + public int NodeId { get; set; } - /// - /// This is the entity type associated with the log - /// - [Column("entityType")] - [Length(50)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? EntityType { get; set; } + /// + /// This is the entity type associated with the log + /// + [Column("entityType")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? EntityType { get; set; } - // TODO: Should we have an index on this since we allow searching on it? - [Column("Datestamp")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime Datestamp { get; set; } + // TODO: Should we have an index on this since we allow searching on it? + [Column("Datestamp")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime Datestamp { get; set; } - // TODO: Should we have an index on this since we allow searching on it? - [Column("logHeader")] - [Length(50)] - public string Header { get; set; } = null!; + // TODO: Should we have an index on this since we allow searching on it? + [Column("logHeader")] + [Length(50)] + public string Header { get; set; } = null!; - [Column("logComment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(4000)] - public string? Comment { get; set; } + [Column("logComment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(4000)] + public string? Comment { get; set; } - /// - /// Used to store additional data parameters for the log - /// - [Column("parameters")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? Parameters { get; set; } - } + /// + /// Used to store additional data parameters for the log + /// + [Column("parameters")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Parameters { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs index 66b4a1902c..a39c4ef756 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs @@ -1,22 +1,22 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.LogViewerQuery)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LogViewerQueryDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LogViewerQueryDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("name")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_LogViewerQuery_name")] - public string? Name { get; set; } + [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_LogViewerQuery_name")] + public string? Name { get; set; } - [Column("query")] - public string? Query { get; set; } - } + [Column("query")] + public string? Query { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs index 3f9dae2744..eb46f8f5b9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs @@ -1,61 +1,59 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Macro)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class MacroDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Macro)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class MacroDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("uniqueId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacro_UniqueId")] - public Guid UniqueId { get; set; } + [Column("uniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacro_UniqueId")] + public Guid UniqueId { get; set; } - [Column("macroUseInEditor")] - [Constraint(Default = "0")] - public bool UseInEditor { get; set; } + [Column("macroUseInEditor")] + [Constraint(Default = "0")] + public bool UseInEditor { get; set; } - [Column("macroRefreshRate")] - [Constraint(Default = "0")] - public int RefreshRate { get; set; } + [Column("macroRefreshRate")] + [Constraint(Default = "0")] + public int RefreshRate { get; set; } - [Column("macroAlias")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroPropertyAlias")] - public string Alias { get; set; } = string.Empty; + [Column("macroAlias")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroPropertyAlias")] + public string Alias { get; set; } = string.Empty; - [Column("macroName")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } + [Column("macroName")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } - [Column("macroCacheByPage")] - [Constraint(Default = "1")] - public bool CacheByPage { get; set; } + [Column("macroCacheByPage")] + [Constraint(Default = "1")] + public bool CacheByPage { get; set; } - [Column("macroCachePersonalized")] - [Constraint(Default = "0")] - public bool CachePersonalized { get; set; } + [Column("macroCachePersonalized")] + [Constraint(Default = "0")] + public bool CachePersonalized { get; set; } - [Column("macroDontRender")] - [Constraint(Default = "0")] - public bool DontRender { get; set; } + [Column("macroDontRender")] + [Constraint(Default = "0")] + public bool DontRender { get; set; } - [Column("macroSource")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string MacroSource { get; set; } = null!; + [Column("macroSource")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string MacroSource { get; set; } = null!; - [Column("macroType")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int MacroType { get; set; } + [Column("macroType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int MacroType { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "Macro")] - public List? MacroPropertyDtos { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "Macro")] + public List? MacroPropertyDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs index 62e64e77a9..98eb9de0b6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs @@ -1,40 +1,39 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.MacroProperty)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class MacroPropertyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.MacroProperty)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class MacroPropertyDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - // important to use column name != cmsMacro.uniqueId (fix in v8) - [Column("uniquePropertyId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_UniquePropertyId")] - public Guid UniqueId { get; set; } + // important to use column name != cmsMacro.uniqueId (fix in v8) + [Column("uniquePropertyId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_UniquePropertyId")] + public Guid UniqueId { get; set; } - [Column("editorAlias")] - public string EditorAlias { get; set; } = null!; + [Column("editorAlias")] + public string EditorAlias { get; set; } = null!; - [Column("macro")] - [ForeignKey(typeof(MacroDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_Alias", ForColumns = "macro, macroPropertyAlias")] - public int Macro { get; set; } + [Column("macro")] + [ForeignKey(typeof(MacroDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_Alias", ForColumns = "macro, macroPropertyAlias")] + public int Macro { get; set; } - [Column("macroPropertySortOrder")] - [Constraint(Default = "0")] - public byte SortOrder { get; set; } + [Column("macroPropertySortOrder")] + [Constraint(Default = "0")] + public byte SortOrder { get; set; } - [Column("macroPropertyAlias")] - [Length(50)] - public string Alias { get; set; } = null!; + [Column("macroPropertyAlias")] + [Length(50)] + public string Alias { get; set; } = null!; - [Column("macroPropertyName")] - public string? Name { get; set; } - } + [Column("macroPropertyName")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs index 374f2437ff..bb6c672f32 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs @@ -1,21 +1,19 @@ -using NPoco; +using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +// this is a special Dto that does not have a corresponding table +// and is only used in our code to represent a media item, similar +// to document items. +internal class MediaDto { - // this is a special Dto that does not have a corresponding table - // and is only used in our code to represent a media item, similar - // to document items. + public int NodeId { get; set; } - internal class MediaDto - { - public int NodeId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; - - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public MediaVersionDto MediaVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public MediaVersionDto MediaVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs index dabdb14ca7..223414a976 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs @@ -1,27 +1,27 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class MediaVersionDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class MediaVersionDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion; + public const string TableName = Constants.DatabaseSchema.Tables.MediaVersion; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName, ForColumns = "id, path")] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName, ForColumns = "id, path")] + public int Id { get; set; } - [Column("path")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Path { get; set; } + [Column("path")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Path { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs index a32257a087..3fa9d3980a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs @@ -1,20 +1,20 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Member2MemberGroup)] - [PrimaryKey("Member", AutoIncrement = false)] - [ExplicitColumns] - internal class Member2MemberGroupDto - { - [Column("Member")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsMember2MemberGroup", OnColumns = "Member, MemberGroup")] - [ForeignKey(typeof(MemberDto))] - public int Member { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("MemberGroup")] - [ForeignKey(typeof(NodeDto))] - public int MemberGroup { get; set; } - } +[TableName(Constants.DatabaseSchema.Tables.Member2MemberGroup)] +[PrimaryKey("Member", AutoIncrement = false)] +[ExplicitColumns] +internal class Member2MemberGroupDto +{ + [Column("Member")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsMember2MemberGroup", OnColumns = "Member, MemberGroup")] + [ForeignKey(typeof(MemberDto))] + public int Member { get; set; } + + [Column("MemberGroup")] + [ForeignKey(typeof(NodeDto))] + public int MemberGroup { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs index 6c24bad7c4..77b500eef5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs @@ -1,85 +1,84 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class MemberDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class MemberDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Member; + private const string TableName = Constants.DatabaseSchema.Tables.Member; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } - [Column("Email")] - [Length(1000)] - [Constraint(Default = "''")] - public string Email { get; set; } = null!; + [Column("Email")] + [Length(1000)] + [Constraint(Default = "''")] + public string Email { get; set; } = null!; - [Column("LoginName")] - [Length(1000)] - [Constraint(Default = "''")] - [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] - public string LoginName { get; set; } = null!; + [Column("LoginName")] + [Length(1000)] + [Constraint(Default = "''")] + [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] + public string LoginName { get; set; } = null!; - [Column("Password")] - [Length(1000)] - [Constraint(Default = "''")] - public string? Password { get; set; } + [Column("Password")] + [Length(1000)] + [Constraint(Default = "''")] + public string? Password { get; set; } - /// - /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) - /// - [Column("passwordConfig")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? PasswordConfig { get; set; } + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } - [Column("securityStampToken")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(255)] - public string? SecurityStampToken { get; set; } + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } - [Column("emailConfirmedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? EmailConfirmedDate { get; set; } + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } - [Column("failedPasswordAttempts")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FailedPasswordAttempts { get; set; } + [Column("failedPasswordAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedPasswordAttempts { get; set; } - [Column("isLockedOut")] - [Constraint(Default = 0)] - [NullSetting(NullSetting = NullSettings.Null)] - public bool IsLockedOut { get; set; } + [Column("isLockedOut")] + [Constraint(Default = 0)] + [NullSetting(NullSetting = NullSettings.Null)] + public bool IsLockedOut { get; set; } - [Column("isApproved")] - [Constraint(Default = 1)] - public bool IsApproved { get; set; } + [Column("isApproved")] + [Constraint(Default = 1)] + public bool IsApproved { get; set; } - [Column("lastLoginDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLoginDate { get; set; } + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } - [Column("lastLockoutDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLockoutDate { get; set; } + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } - [Column("lastPasswordChangeDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastPasswordChangeDate { get; set; } + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs index 9e9b97daf3..5b27863c8a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs @@ -1,35 +1,35 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.MemberPropertyType)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class MemberPropertyTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.MemberPropertyType)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class MemberPropertyTypeDto - { - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("NodeId")] - [ForeignKey(typeof(NodeDto))] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int NodeId { get; set; } + [Column("NodeId")] + [ForeignKey(typeof(NodeDto))] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int NodeId { get; set; } - [Column("propertytypeId")] - public int PropertyTypeId { get; set; } + [Column("propertytypeId")] + public int PropertyTypeId { get; set; } - [Column("memberCanEdit")] - [Constraint(Default = "0")] - public bool CanEdit { get; set; } + [Column("memberCanEdit")] + [Constraint(Default = "0")] + public bool CanEdit { get; set; } - [Column("viewOnProfile")] - [Constraint(Default = "0")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] + [Constraint(Default = "0")] + public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - [Constraint(Default = "0")] - public bool IsSensitive { get; set; } - } + [Column("isSensitive")] + [Constraint(Default = "0")] + public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index 621aba121a..d11ebc96ce 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -1,68 +1,67 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +public class NodeDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - public class NodeDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Node; - public const int NodeIdSeed = 1060; - private int? _userId; + public const string TableName = Constants.DatabaseSchema.Tables.Node; + public const int NodeIdSeed = 1060; + private int? _userId; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] - public int NodeId { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] + public int NodeId { get; set; } - [Column("uniqueId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] - [Constraint(Default = SystemMethods.NewGuid)] - public Guid UniqueId { get; set; } + [Column("uniqueId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] + [Constraint(Default = SystemMethods.NewGuid)] + public Guid UniqueId { get; set; } - [Column("parentId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] - public int ParentId { get; set; } + [Column("parentId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] + public int ParentId { get; set; } - // NOTE: This index is primarily for the nucache data lookup, see https://github.com/umbraco/Umbraco-CMS/pull/8365#issuecomment-673404177 - [Column("level")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Level", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")] - public short Level { get; set; } + // NOTE: This index is primarily for the nucache data lookup, see https://github.com/umbraco/Umbraco-CMS/pull/8365#issuecomment-673404177 + [Column("level")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Level", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")] + public short Level { get; set; } - [Column("path")] - [Length(150)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")] - public string Path { get; set; } = null!; + [Column("path")] + [Length(150)] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")] + public string Path { get; set; } = null!; - [Column("sortOrder")] - public int SortOrder { get; set; } + [Column("sortOrder")] + public int SortOrder { get; set; } - [Column("trashed")] - [Constraint(Default = "0")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] - public bool Trashed { get; set; } + [Column("trashed")] + [Constraint(Default = "0")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] + public bool Trashed { get; set; } - [Column("nodeUser")] // TODO: db rename to 'createUserId' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + [Column("nodeUser")] // TODO: db rename to 'createUserId' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } // return null if zero - [Column("text")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Text { get; set; } + [Column("text")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Text { get; set; } - [Column("nodeObjectType")] // TODO: db rename to 'objectType' - [NullSetting(NullSetting = NullSettings.Null)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")] - public Guid? NodeObjectType { get; set; } + [Column("nodeObjectType")] // TODO: db rename to 'objectType' + [NullSetting(NullSetting = NullSettings.Null)] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")] + public Guid? NodeObjectType { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs index bd0c63a412..f0c57e0d18 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs @@ -1,136 +1,136 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyDataDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyDataDto + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string? Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.PropertyData; - public const int VarcharLength = 512; - public const int SegmentLength = 256; + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } - private decimal? _decimalValue; + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } - // pk, not used at the moment (never updating) - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string? VarcharValue { get; set; } - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] - public int VersionId { get; set; } + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TextValue { get; set; } - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] - public int PropertyTypeId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto? PropertyTypeDto { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get; set; } - - [Column("segment")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(SegmentLength)] - public string? Segment { get; set; } - - [Column("intValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? IntegerValue { get; set; } - - [Column("decimalValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public decimal? DecimalValue + [Ignore] + public object? Value + { + get { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } - - [Column("dateValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? DateValue { get; set; } - - [Column("varcharValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(VarcharLength)] - public string? VarcharValue { get; set; } - - [Column("textValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TextValue { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] - public PropertyTypeDto? PropertyTypeDto { get; set; } - - [Ignore] - public object? Value - { - get + if (IntegerValue.HasValue) { - if (IntegerValue.HasValue) - return IntegerValue.Value; - - if (DecimalValue.HasValue) - return DecimalValue.Value; - - if (DateValue.HasValue) - return DateValue.Value; - - if (!string.IsNullOrEmpty(VarcharValue)) - return VarcharValue; - - if (!string.IsNullOrEmpty(TextValue)) - return TextValue; - - return null; + return IntegerValue.Value; } - } - public PropertyDataDto Clone(int versionId) - { - return new PropertyDataDto + if (DecimalValue.HasValue) { - VersionId = versionId, - PropertyTypeId = PropertyTypeId, - LanguageId = LanguageId, - Segment = Segment, - IntegerValue = IntegerValue, - DecimalValue = DecimalValue, - DateValue = DateValue, - VarcharValue = VarcharValue, - TextValue = TextValue, - PropertyTypeDto = PropertyTypeDto - }; - } + return DecimalValue.Value; + } - protected bool Equals(PropertyDataDto other) - { - return Id == other.Id; - } + if (DateValue.HasValue) + { + return DateValue.Value; + } - public override bool Equals(object? other) - { - return - !ReferenceEquals(null, other) // other is not null - && (ReferenceEquals(this, other) // and either ref-equals, or same id - || other is PropertyDataDto pdata && pdata.Id == Id); - } + if (!string.IsNullOrEmpty(VarcharValue)) + { + return VarcharValue; + } - public override int GetHashCode() - { - // ReSharper disable once NonReadonlyMemberInGetHashCode - return Id; + if (!string.IsNullOrEmpty(TextValue)) + { + return TextValue; + } + + return null; } } + + public PropertyDataDto Clone(int versionId) => + new PropertyDataDto + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto, + }; + + protected bool Equals(PropertyDataDto other) => Id == other.Id; + + public override bool Equals(object? other) => + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || (other is PropertyDataDto pdata && pdata.Id == Id)); + + public override int GetHashCode() => + + // ReSharper disable once NonReadonlyMemberInGetHashCode + Id; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs index 8e321fa962..2645735b82 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs @@ -1,18 +1,17 @@ -using NPoco; +using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +// this is PropertyTypeDto + the special property type fields for members +// it is used for querying everything needed for a property type, at once +internal class PropertyTypeCommonDto : PropertyTypeDto { - // this is PropertyTypeDto + the special property type fields for members - // it is used for querying everything needed for a property type, at once - internal class PropertyTypeCommonDto : PropertyTypeDto - { - [Column("memberCanEdit")] - public bool CanEdit { get; set; } + [Column("memberCanEdit")] + public bool CanEdit { get; set; } - [Column("viewOnProfile")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] + public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - public bool IsSensitive { get; set; } - } + [Column("isSensitive")] + public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs index dd4652f366..721ea6c507 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs @@ -1,85 +1,84 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeDto - { - private string? _alias; + private string? _alias; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 100)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 100)] + public int Id { get; set; } - [Column("dataTypeId")] - [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } - [Column("propertyTypeGroupId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(PropertyTypeGroupDto))] - public int? PropertyTypeGroupId { get; set; } + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } - [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] - [Column("Alias")] - public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } - [Column("Name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } - [Column("sortOrder")] - [Constraint(Default = "0")] - public int SortOrder { get; set; } + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } - [Column("mandatory")] - [Constraint(Default = "0")] - public bool Mandatory { get; set; } + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } - [Column("mandatoryMessage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? MandatoryMessage { get; set; } + [Column("mandatoryMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? MandatoryMessage { get; set; } - [Column("validationRegExp")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? ValidationRegExp { get; set; } - [Column("validationRegExpMessage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? ValidationRegExpMessage { get; set; } + [Column("validationRegExpMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? ValidationRegExpMessage { get; set; } - [Column("Description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(2000)] - public string? Description { get; set; } + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string? Description { get; set; } - [Column("labelOnTop")] - [Constraint(Default = "0")] - public bool LabelOnTop { get; set; } + [Column("labelOnTop")] + [Constraint(Default = "0")] + public bool LabelOnTop { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] - public DataTypeDto DataTypeDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto DataTypeDto { get; set; } = null!; - [Column("UniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs index 489cb7fcb5..9910caaa7b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs @@ -1,47 +1,45 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +internal class PropertyTypeGroupDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - internal class PropertyTypeGroupDto - { - public const string TableName = Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup; + public const string TableName = Constants.DatabaseSchema.Tables.PropertyTypeGroup; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 56)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 56)] + public int Id { get; set; } - [Column("uniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")] - public Guid UniqueId { get; set; } + [Column("uniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")] + public Guid UniqueId { get; set; } - [Column("contenttypeNodeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeNodeId { get; set; } + [Column("contenttypeNodeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeNodeId { get; set; } - [Column("type")] - [Constraint(Default = 0)] - public short Type { get; set; } + [Column("type")] + [Constraint(Default = 0)] + public short Type { get; set; } - [Column("text")] - public string? Text { get; set; } + [Column("text")] + public string? Text { get; set; } - [Column("alias")] - public string Alias { get; set; } = null!; + [Column("alias")] + public string Alias { get; set; } = null!; - [Column("sortorder")] - public int SortOrder { get; set; } + [Column("sortorder")] + public int SortOrder { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "PropertyTypeGroupId")] - public List? PropertyTypeDtos { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "PropertyTypeGroupId")] + public List? PropertyTypeDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs index f93b9b602a..9829604193 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs @@ -1,26 +1,25 @@ -using System; using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyTypeGroup)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +internal class PropertyTypeGroupReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - internal class PropertyTypeGroupReadOnlyDto - { - [Column("PropertyTypeGroupId")] - public int? Id { get; set; } + [Column("PropertyTypeGroupId")] + public int? Id { get; set; } - [Column("PropertyGroupName")] - public string? Text { get; set; } + [Column("PropertyGroupName")] + public string? Text { get; set; } - [Column("PropertyGroupSortOrder")] - public int SortOrder { get; set; } + [Column("PropertyGroupSortOrder")] + public int SortOrder { get; set; } - [Column("contenttypeNodeId")] - public int ContentTypeNodeId { get; set; } + [Column("contenttypeNodeId")] + public int ContentTypeNodeId { get; set; } - [Column("PropertyGroupUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("PropertyGroupUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs index ae1358b5cd..94d8436401 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs @@ -1,70 +1,69 @@ -using System; using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeReadOnlyDto - { - [Column("PropertyTypeId")] - public int? Id { get; set; } + [Column("PropertyTypeId")] + public int? Id { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] + public int DataTypeId { get; set; } - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + public int ContentTypeId { get; set; } - [Column("PropertyTypesGroupId")] - public int? PropertyTypeGroupId { get; set; } + [Column("PropertyTypesGroupId")] + public int? PropertyTypeGroupId { get; set; } - [Column("Alias")] - public string? Alias { get; set; } + [Column("Alias")] + public string? Alias { get; set; } - [Column("Name")] - public string? Name { get; set; } + [Column("Name")] + public string? Name { get; set; } - [Column("PropertyTypeSortOrder")] - public int SortOrder { get; set; } + [Column("PropertyTypeSortOrder")] + public int SortOrder { get; set; } - [Column("mandatory")] - public bool Mandatory { get; set; } + [Column("mandatory")] + public bool Mandatory { get; set; } - [Column("mandatoryMessage")] - public string? MandatoryMessage { get; set; } + [Column("mandatoryMessage")] + public string? MandatoryMessage { get; set; } - [Column("validationRegExp")] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] + public string? ValidationRegExp { get; set; } - [Column("validationRegExpMessage")] - public string? ValidationRegExpMessage { get; set; } + [Column("validationRegExpMessage")] + public string? ValidationRegExpMessage { get; set; } - [Column("Description")] - public string? Description { get; set; } + [Column("Description")] + public string? Description { get; set; } - [Column("labelOnTop")] - public bool LabelOnTop { get; set; } + [Column("labelOnTop")] + public bool LabelOnTop { get; set; } - /* cmsMemberType */ - [Column("memberCanEdit")] - public bool CanEdit { get; set; } + /* cmsMemberType */ + [Column("memberCanEdit")] + public bool CanEdit { get; set; } - [Column("viewOnProfile")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] + public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - public bool IsSensitive { get; set; } + [Column("isSensitive")] + public bool IsSensitive { get; set; } - /* DataType */ - [Column("propertyEditorAlias")] - public string? PropertyEditorAlias { get; set; } + /* DataType */ + [Column("propertyEditorAlias")] + public string? PropertyEditorAlias { get; set; } - [Column("dbType")] - public string? DbType { get; set; } + [Column("dbType")] + public string? DbType { get; set; } - [Column("UniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs index 435e072307..b377b49177 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs @@ -1,54 +1,49 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.RedirectUrl)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class RedirectUrlDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.RedirectUrl)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - class RedirectUrlDto - { - public RedirectUrlDto() - { - CreateDateUtc = DateTime.UtcNow; - } + public RedirectUrlDto() => CreateDateUtc = DateTime.UtcNow; - // notes - // - // we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the - // problem is that the index key must be 900 bytes max. should we run without an index? done - // some perfs comparisons, and running with an index on a hash is only slightly slower on - // inserts, and much faster on reads, so... we have an index on a hash. + // notes + // + // we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the + // problem is that the index key must be 900 bytes max. should we run without an index? done + // some perfs comparisons, and running with an index on a hash is only slightly slower on + // inserts, and much faster on reads, so... we have an index on a hash. + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoRedirectUrl", AutoIncrement = false)] + public Guid Id { get; set; } - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoRedirectUrl", AutoIncrement = false)] - public Guid Id { get; set; } + [ResultColumn] + public int ContentId { get; set; } - [ResultColumn] - public int ContentId { get; set; } + [Column("contentKey")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto), Column = "uniqueID")] + public Guid ContentKey { get; set; } - [Column("contentKey")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(NodeDto), Column = "uniqueID")] - public Guid ContentKey { get; set; } + [Column("createDateUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime CreateDateUtc { get; set; } - [Column("createDateUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime CreateDateUtc { get; set; } + [Column("url")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Url { get; set; } = null!; - [Column("url")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Url { get; set; } = null!; + [Column("culture")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Culture { get; set; } - [Column("culture")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Culture { get; set; } - - [Column("urlHash")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, culture, createDateUtc")] - [Length(40)] - public string UrlHash { get; set; } = null!; - } + [Column("urlHash")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, culture, createDateUtc")] + [Length(40)] + public string UrlHash { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs index b197f12692..59484734dc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs @@ -1,46 +1,45 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Relation)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class RelationDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Relation)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class RelationDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("parentId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] - public int ParentId { get; set; } + [Column("parentId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] + public int ParentId { get; set; } - [Column("childId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode1")] - public int ChildId { get; set; } + [Column("childId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode1")] + public int ChildId { get; set; } - [Column("relType")] - [ForeignKey(typeof(RelationTypeDto))] - public int RelationType { get; set; } + [Column("relType")] + [ForeignKey(typeof(RelationTypeDto))] + public int RelationType { get; set; } - [Column("datetime")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime Datetime { get; set; } + [Column("datetime")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime Datetime { get; set; } - [Column("comment")] - [Length(1000)] - public string? Comment { get; set; } + [Column("comment")] + [Length(1000)] + public string? Comment { get; set; } - [ResultColumn] - [Column("parentObjectType")] - public Guid ParentObjectType { get; set; } + [ResultColumn] + [Column("parentObjectType")] + public Guid ParentObjectType { get; set; } - [ResultColumn] - [Column("childObjectType")] - public Guid ChildObjectType { get; set; } - } + [ResultColumn] + [Column("childObjectType")] + public Guid ChildObjectType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs index d1cb5cc278..adefd7ae38 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs @@ -1,48 +1,47 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.RelationType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class RelationTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.RelationType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class RelationTypeDto - { - public const int NodeIdSeed = 10; + public const int NodeIdSeed = 10; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] + public int Id { get; set; } - [Column("typeUniqueId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] - public Guid UniqueId { get; set; } + [Column("typeUniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] + public Guid UniqueId { get; set; } - [Column("dual")] - public bool Dual { get; set; } + [Column("dual")] + public bool Dual { get; set; } - [Column("parentObjectType")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid? ParentObjectType { get; set; } + [Column("parentObjectType")] + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ParentObjectType { get; set; } - [Column("childObjectType")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid? ChildObjectType { get; set; } + [Column("childObjectType")] + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ChildObjectType { get; set; } - [Column("name")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] - public string Name { get; set; } = null!; + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] + public string Name { get; set; } = null!; - [Column("alias")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(100)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] - public string Alias { get; set; } = null!; + [Column("alias")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] + public string Alias { get; set; } = null!; - [Constraint(Default = "0")] - [Column("isDependency")] - public bool IsDependency { get; set; } - } + [Constraint(Default = "0")] + [Column("isDependency")] + public bool IsDependency { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs index 89ef0039ab..66a8c2bd07 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs @@ -1,40 +1,39 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Server)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class ServerRegistrationDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Server)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class ServerRegistrationDto - { - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = true)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } - [Column("address")] - [Length(500)] - public string? ServerAddress { get; set; } + [Column("address")] + [Length(500)] + public string? ServerAddress { get; set; } - [Column("computerName")] - [Length(255)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique - public string? ServerIdentity { get; set; } + [Column("computerName")] + [Length(255)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique + public string? ServerIdentity { get; set; } - [Column("registeredDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime DateRegistered { get; set; } + [Column("registeredDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime DateRegistered { get; set; } - [Column("lastNotifiedDate")] - public DateTime DateAccessed { get; set; } + [Column("lastNotifiedDate")] + public DateTime DateAccessed { get; set; } - [Column("isActive")] - [Index(IndexTypes.NonClustered)] - public bool IsActive { get; set; } + [Column("isActive")] + [Index(IndexTypes.NonClustered)] + public bool IsActive { get; set; } - [Column("isSchedulingPublisher")] - public bool IsSchedulingPublisher { get; set; } - } + [Column("isSchedulingPublisher")] + public bool IsSchedulingPublisher { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs index 8c032660df..cc8b80c777 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs @@ -1,40 +1,40 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class TagDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class TagDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Tag; + public const string TableName = Constants.DatabaseSchema.Tables.Tag; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("group")] - [Length(100)] - public string Group { get; set; } = null!; + [Column("group")] + [Length(100)] + public string Group { get; set; } = null!; - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get;set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } - [Column("tag")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] - public string Text { get; set; } = null!; + [Column("tag")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] + public string Text { get; set; } = null!; - //[Column("key")] - //[Length(301)] // de-normalized "{group}/{tag}" - //public string Key { get; set; } + // [Column("key")] + // [Length(301)] // de-normalized "{group}/{tag}" + // public string Key { get; set; } - // queries result column - [ResultColumn("NodeCount")] - public int NodeCount { get; set; } - } + // queries result column + [ResultColumn("NodeCount")] + public int NodeCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs index 2cc287ac92..3799679e4d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs @@ -1,26 +1,26 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class TagRelationshipDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class TagRelationshipDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship; + public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] - [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] + [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] + public int NodeId { get; set; } - [Column("tagId")] - [ForeignKey(typeof(TagDto))] - public int TagId { get; set; } + [Column("tagId")] + [ForeignKey(typeof(TagDto))] + public int TagId { get; set; } - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto), Name = "FK_cmsTagRelationship_cmsPropertyType")] - public int PropertyTypeId { get; set; } - } + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto), Name = "FK_cmsTagRelationship_cmsPropertyType")] + public int PropertyTypeId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs index 9a80cdd8e2..4355a3c983 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs @@ -1,29 +1,29 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Template)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class TemplateDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Template)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class TemplateDto - { - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("nodeId")] - [Index(IndexTypes.UniqueNonClustered)] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsTemplate_umbracoNode")] - public int NodeId { get; set; } + [Column("nodeId")] + [Index(IndexTypes.UniqueNonClustered)] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsTemplate_umbracoNode")] + public int NodeId { get; set; } - [Column("alias")] - [Length(100)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get; set; } + [Column("alias")] + [Length(100)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs index 09f6647bfe..760419a307 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs @@ -1,34 +1,32 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class TwoFactorLoginDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class TwoFactorLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TwoFactorLogin; + public const string TableName = Constants.DatabaseSchema.Tables.TwoFactorLogin; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("userOrMemberKey")] - [Index(IndexTypes.NonClustered)] - public Guid UserOrMemberKey { get; set; } + [Column("userOrMemberKey")] + [Index(IndexTypes.NonClustered)] + public Guid UserOrMemberKey { get; set; } - [Column("providerName")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", - Name = "IX_" + TableName + "_ProviderName")] - public string ProviderName { get; set; } = null!; + [Column("providerName")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", Name = "IX_" + TableName + "_ProviderName")] + public string ProviderName { get; set; } = null!; - [Column("secret")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Secret { get; set; } = null!; - } + [Column("secret")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Secret { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs index fd8806124e..aca7d3682e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs @@ -1,25 +1,25 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.User2NodeNotify)] +[PrimaryKey("userId", AutoIncrement = false)] +[ExplicitColumns] +internal class User2NodeNotifyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify)] - [PrimaryKey("userId", AutoIncrement = false)] - [ExplicitColumns] - internal class User2NodeNotifyDto - { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodeNotify", OnColumns = "userId, nodeId, action")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodeNotify", OnColumns = "userId, nodeId, action")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("action")] - [SpecialDbType(SpecialDbTypes.NCHAR)] - [Length(1)] - public string? Action { get; set; } - } + [Column("action")] + [SpecialDbType(SpecialDbTypes.NCHAR)] + [Length(1)] + public string? Action { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs index db3d5b4e74..3bc059ff21 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs @@ -1,19 +1,19 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.User2UserGroup)] - [ExplicitColumns] - public class User2UserGroupDto - { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("userGroupId")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } - } +[TableName(Constants.DatabaseSchema.Tables.User2UserGroup)] +[ExplicitColumns] +public class User2UserGroupDto +{ + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("userGroupId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index 20768eed65..16db4a10ad 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -1,127 +1,124 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +public class UserDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - public class UserDto + public const string TableName = Constants.DatabaseSchema.Tables.User; + + public UserDto() { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.User; - - public UserDto() - { - UserGroupDtos = new List(); - UserStartNodeDtos = new HashSet(); - } - - // TODO: We need to add a GUID for users and track external logins with that instead of the INT - - [Column("id")] - [PrimaryKeyColumn(Name = "PK_user")] - public int Id { get; set; } - - [Column("userDisabled")] - [Constraint(Default = "0")] - public bool Disabled { get; set; } - - [Column("userNoConsole")] - [Constraint(Default = "0")] - public bool NoConsole { get; set; } - - [Column("userName")] - public string UserName { get; set; } = null!; - - [Column("userLogin")] - [Length(125)] - [Index(IndexTypes.NonClustered)] - public string? Login { get; set; } - - [Column("userPassword")] - [Length(500)] - public string? Password { get; set; } - - /// - /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) - /// - [Column("passwordConfig")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? PasswordConfig { get; set; } - - [Column("userEmail")] - public string Email { get; set; } = null!; - - [Column("userLanguage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(10)] - public string? UserLanguage { get; set; } - - [Column("securityStampToken")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(255)] - public string? SecurityStampToken { get; set; } - - [Column("failedLoginAttempts")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FailedLoginAttempts { get; set; } - - [Column("lastLockoutDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLockoutDate { get; set; } - - [Column("lastPasswordChangeDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastPasswordChangeDate { get; set; } - - [Column("lastLoginDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLoginDate { get; set; } - - [Column("emailConfirmedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? EmailConfirmedDate { get; set; } - - [Column("invitedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? InvitedDate { get; set; } - - [Column("createDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } = DateTime.Now; - - [Column("updateDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } = DateTime.Now; - - /// - /// Will hold the media file system relative path of the users custom avatar if they uploaded one - /// - [Column("avatar")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? Avatar { get; set; } - - /// - /// A Json blob stored for recording tour data for a user - /// - [Column("tourData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TourData { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] - public List UserGroupDtos { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] - public HashSet UserStartNodeDtos { get; set; } + UserGroupDtos = new List(); + UserStartNodeDtos = new HashSet(); } + + // TODO: We need to add a GUID for users and track external logins with that instead of the INT + [Column("id")] + [PrimaryKeyColumn(Name = "PK_user")] + public int Id { get; set; } + + [Column("userDisabled")] + [Constraint(Default = "0")] + public bool Disabled { get; set; } + + [Column("userNoConsole")] + [Constraint(Default = "0")] + public bool NoConsole { get; set; } + + [Column("userName")] + public string UserName { get; set; } = null!; + + [Column("userLogin")] + [Length(125)] + [Index(IndexTypes.NonClustered)] + public string? Login { get; set; } + + [Column("userPassword")] + [Length(500)] + public string? Password { get; set; } + + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } + + [Column("userEmail")] + public string Email { get; set; } = null!; + + [Column("userLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(10)] + public string? UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } + + [Column("failedLoginAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedLoginAttempts { get; set; } + + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } + + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } + + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } + + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("invitedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? InvitedDate { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } = DateTime.Now; + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } = DateTime.Now; + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + [Column("tourData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TourData { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public List UserGroupDtos { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public HashSet UserStartNodeDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs index b5719c1c63..7e2099ae44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs @@ -1,19 +1,19 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App)] - [ExplicitColumns] - public class UserGroup2AppDto - { - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("app")] - [Length(50)] - public string AppAlias { get; set; } = null!; - } +[TableName(Constants.DatabaseSchema.Tables.UserGroup2App)] +[ExplicitColumns] +public class UserGroup2AppDto +{ + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } + + [Column("app")] + [Length(50)] + public string AppAlias { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2LanguageDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2LanguageDto.cs new file mode 100644 index 0000000000..1ecf84fafc --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2LanguageDto.cs @@ -0,0 +1,21 @@ +using System.Data; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Language)] +[ExplicitColumns] +public class UserGroup2LanguageDto +{ + public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Language; + + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2language", OnColumns = "userGroupId, languageId")] + [ForeignKey(typeof(UserGroupDto), OnDelete = Rule.Cascade)] + public int UserGroupId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto), OnDelete = Rule.Cascade)] + public int LanguageId { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs index ad172c846c..54b66b8e22 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs @@ -2,22 +2,21 @@ using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +internal class UserGroup2NodeDto { - [TableName(TableName)] - [ExplicitColumns] - internal class UserGroup2NodeDto - { - public const string TableName = Constants.DatabaseSchema.Tables.UserGroup2Node; + public const string TableName = Constants.DatabaseSchema.Tables.UserGroup2Node; - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_" + TableName, OnColumns = "userGroupId, nodeId")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_" + TableName, OnColumns = "userGroupId, nodeId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_nodeId")] - public int NodeId { get; set; } - } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_nodeId")] + public int NodeId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs index 4461089f96..94a8fd4361 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs @@ -1,23 +1,23 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup2NodePermission)] +[ExplicitColumns] +internal class UserGroup2NodePermissionDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission)] - [ExplicitColumns] - internal class UserGroup2NodePermissionDto - { - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", OnColumns = "userGroupId, nodeId, permission")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", OnColumns = "userGroupId, nodeId, permission")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] + public int NodeId { get; set; } - [Column("permission")] - public string? Permission { get; set; } - } + [Column("permission")] + public string? Permission { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs index afbda3cc9a..62cdad9e26 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs @@ -1,72 +1,79 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup)] +[PrimaryKey("id")] +[ExplicitColumns] +public class UserGroupDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup)] - [PrimaryKey("id")] - [ExplicitColumns] - public class UserGroupDto + public UserGroupDto() { - public UserGroupDto() - { - UserGroup2AppDtos = new List(); - } - - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 6)] - public int Id { get; set; } - - [Column("userGroupAlias")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")] - public string? Alias { get; set; } - - [Column("userGroupName")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] - public string? Name { get; set; } - - [Column("userGroupDefaultPermissions")] - [Length(50)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? DefaultPermissions { get; set; } - - [Column("createDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - - [Column("updateDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - - [Column("icon")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } - - [Column("startContentId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] - public int? StartContentId { get; set; } - - [Column("startMediaId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] - public int? StartMediaId { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] - public List UserGroup2AppDtos { get; set; } - - /// - /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) - /// - [ResultColumn] - public int UserCount { get; set; } + UserGroup2AppDtos = new List(); + UserGroup2LanguageDtos = new List(); } + + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 6)] + public int Id { get; set; } + + [Column("userGroupAlias")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")] + public string? Alias { get; set; } + + [Column("userGroupName")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] + public string? Name { get; set; } + + [Column("userGroupDefaultPermissions")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? DefaultPermissions { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + + [Column("icon")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } + + [Column("hasAccessToAllLanguages")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public bool HasAccessToAllLanguages { get; set; } + + [Column("startContentId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] + public int? StartContentId { get; set; } + + [Column("startMediaId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] + public int? StartMediaId { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] + public List UserGroup2AppDtos { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] + public List UserGroup2LanguageDtos { get; set; } + + /// + /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) + /// + [ResultColumn] + public int UserCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs index 4d18a39557..bf52e3fd9c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs @@ -1,57 +1,60 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("sessionId", AutoIncrement = false)] +[ExplicitColumns] +internal class UserLoginDto { - [TableName(TableName)] - [PrimaryKey("sessionId", AutoIncrement = false)] - [ExplicitColumns] - internal class UserLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.UserLogin; + public const string TableName = Constants.DatabaseSchema.Tables.UserLogin; - [Column("sessionId")] - [PrimaryKeyColumn(AutoIncrement = false)] - public Guid SessionId { get; set; } + [Column("sessionId")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid SessionId { get; set; } - [Column("userId")] - [ForeignKey(typeof(UserDto), Name = "FK_" + TableName + "_umbracoUser_id")] - public int? UserId { get; set; } + [Column("userId")] + [ForeignKey(typeof(UserDto), Name = "FK_" + TableName + "_umbracoUser_id")] + public int? UserId { get; set; } - /// - /// Tracks when the session is created - /// - [Column("loggedInUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime LoggedInUtc { get; set; } + /// + /// Tracks when the session is created + /// + [Column("loggedInUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime LoggedInUtc { get; set; } - /// - /// Updated every time a user's session is validated - /// - /// - /// This allows us to guess if a session is timed out if a user doesn't actively - /// log out and also allows us to trim the data in the table. - /// The index is IMPORTANT as it prevents deadlocks during deletion of - /// old sessions (DELETE ... WHERE lastValidatedUtc < date). - /// - [Column("lastValidatedUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserLogin_lastValidatedUtc")] - public DateTime LastValidatedUtc { get; set; } + /// + /// Updated every time a user's session is validated + /// + /// + /// + /// This allows us to guess if a session is timed out if a user doesn't actively + /// log out and also allows us to trim the data in the table. + /// + /// + /// The index is IMPORTANT as it prevents deadlocks during deletion of + /// old sessions (DELETE ... WHERE lastValidatedUtc < date). + /// + /// + [Column("lastValidatedUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserLogin_lastValidatedUtc")] + public DateTime LastValidatedUtc { get; set; } - /// - /// Tracks when the session is removed when the user's account is logged out - /// - [Column("loggedOutUtc")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LoggedOutUtc { get; set; } + /// + /// Tracks when the session is removed when the user's account is logged out + /// + [Column("loggedOutUtc")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LoggedOutUtc { get; set; } - /// - /// Logs the IP address of the session if available - /// - [Column("ipAddress")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? IpAddress { get; set; } - } + /// + /// Logs the IP address of the session if available + /// + [Column("ipAddress")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? IpAddress { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs index c6116648c7..327bb69b63 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs @@ -1,20 +1,18 @@ -using System; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +internal class UserNotificationDto { - internal class UserNotificationDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + public int NodeId { get; set; } - [Column("userId")] - public int UserId { get; set; } + [Column("userId")] + public int UserId { get; set; } - [Column("nodeObjectType")] - public Guid NodeObjectType { get; set; } + [Column("nodeObjectType")] + public Guid NodeObjectType { get; set; } - [Column("action")] - public string Action { get; set; } = null!; - } + [Column("action")] + public string Action { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs index 44e6379007..4d54752e75 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs @@ -1,67 +1,77 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserStartNode)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +public class UserStartNodeDto : IEquatable { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - public class UserStartNodeDto : IEquatable + public enum StartNodeTypeValue { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_userStartNode")] - public int Id { get; set; } - - [Column("userId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } - - [Column("startNode")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(NodeDto))] - public int StartNode { get; set; } - - [Column("startNodeType")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", Name = "IX_umbracoUserStartNode_startNodeType")] - public int StartNodeType { get; set; } - - public enum StartNodeTypeValue - { - Content = 1, - Media = 2 - } - - public bool Equals(UserStartNodeDto? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id == other.Id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((UserStartNodeDto) obj); - } - - public override int GetHashCode() - { - return Id; - } - - public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) - { - return Equals(left, right); - } - - public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) - { - return !Equals(left, right); - } + Content = 1, + Media = 2, } + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_userStartNode")] + public int Id { get; set; } + + [Column("userId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("startNode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto))] + public int StartNode { get; set; } + + [Column("startNodeType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", Name = "IX_umbracoUserStartNode_startNodeType")] + public int StartNodeType { get; set; } + + public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) => Equals(left, right); + + public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) => !Equals(left, right); + + public bool Equals(UserStartNodeDto? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((UserStartNodeDto)obj); + } + + public override int GetHashCode() => Id; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs index 67c60ffb4d..297cea9025 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs @@ -1,52 +1,45 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class AuditEntryFactory { - internal static class AuditEntryFactory + public static IEnumerable BuildEntities(IEnumerable dtos) => + dtos.Select(BuildEntity).ToList(); + + public static IAuditEntry BuildEntity(AuditEntryDto dto) { - public static IEnumerable BuildEntities(IEnumerable dtos) + var entity = new AuditEntry { - return dtos.Select(BuildEntity).ToList(); - } + Id = dto.Id, + PerformingUserId = dto.PerformingUserId, + PerformingDetails = dto.PerformingDetails, + PerformingIp = dto.PerformingIp, + EventDateUtc = dto.EventDateUtc, + AffectedUserId = dto.AffectedUserId, + AffectedDetails = dto.AffectedDetails, + EventType = dto.EventType, + EventDetails = dto.EventDetails, + }; - public static IAuditEntry BuildEntity(AuditEntryDto dto) - { - var entity = new AuditEntry - { - Id = dto.Id, - PerformingUserId = dto.PerformingUserId, - PerformingDetails = dto.PerformingDetails, - PerformingIp = dto.PerformingIp, - EventDateUtc = dto.EventDateUtc, - AffectedUserId = dto.AffectedUserId, - AffectedDetails = dto.AffectedDetails, - EventType = dto.EventType, - EventDetails = dto.EventDetails - }; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - entity.ResetDirtyProperties(false); - return entity; - } - - public static AuditEntryDto BuildDto(IAuditEntry entity) - { - return new AuditEntryDto - { - Id = entity.Id, - PerformingUserId = entity.PerformingUserId, - PerformingDetails = entity.PerformingDetails, - PerformingIp = entity.PerformingIp, - EventDateUtc = entity.EventDateUtc, - AffectedUserId = entity.AffectedUserId, - AffectedDetails = entity.AffectedDetails, - EventType = entity.EventType, - EventDetails = entity.EventDetails - }; - } + // on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + entity.ResetDirtyProperties(false); + return entity; } + + public static AuditEntryDto BuildDto(IAuditEntry entity) => + new AuditEntryDto + { + Id = entity.Id, + PerformingUserId = entity.PerformingUserId, + PerformingDetails = entity.PerformingDetails, + PerformingIp = entity.PerformingIp, + EventDateUtc = entity.EventDateUtc, + AffectedUserId = entity.AffectedUserId, + AffectedDetails = entity.AffectedDetails, + EventType = entity.EventType, + EventDetails = entity.EventDetails, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs index 1a38348acf..bc33b15fd5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class CacheInstructionFactory { - internal static class CacheInstructionFactory - { - public static IEnumerable BuildEntities(IEnumerable dtos) => dtos.Select(BuildEntity).ToList(); + public static IEnumerable BuildEntities(IEnumerable dtos) => + dtos.Select(BuildEntity).ToList(); - public static CacheInstruction BuildEntity(CacheInstructionDto dto) => - new CacheInstruction(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount); + public static CacheInstruction BuildEntity(CacheInstructionDto dto) => + new(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount); - public static CacheInstructionDto BuildDto(CacheInstruction entity) => - new CacheInstructionDto - { - Id = entity.Id, - UtcStamp = entity.UtcStamp, - Instructions = entity.Instructions, - OriginIdentity = entity.OriginIdentity, - InstructionCount = entity.InstructionCount, - }; - } + public static CacheInstructionDto BuildDto(CacheInstruction entity) => + new() + { + Id = entity.Id, + UtcStamp = entity.UtcStamp, + Instructions = entity.Instructions, + OriginIdentity = entity.OriginIdentity, + InstructionCount = entity.InstructionCount, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs index 33f348a644..5e4035b0b8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs @@ -1,65 +1,64 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ConsentFactory { - internal static class ConsentFactory + public static IEnumerable BuildEntities(IEnumerable dtos) { - public static IEnumerable BuildEntities(IEnumerable dtos) + var ix = new Dictionary(); + var output = new List(); + + foreach (ConsentDto dto in dtos) { - var ix = new Dictionary(); - var output = new List(); + var k = dto.Source + "::" + dto.Context + "::" + dto.Action; - foreach (var dto in dtos) + var consent = new Consent { - var k = dto.Source + "::" + dto.Context + "::" + dto.Action; - - var consent = new Consent - { - Id = dto.Id, - Current = dto.Current, - CreateDate = dto.CreateDate, - Source = dto.Source, - Context = dto.Context, - Action = dto.Action, - State = (ConsentState) dto.State, // assume value is valid - Comment = dto.Comment - }; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - consent.ResetDirtyProperties(false); - - if (ix.TryGetValue(k, out var current)) - { - if (current.HistoryInternal == null) - current.HistoryInternal = new List(); - current.HistoryInternal.Add(consent); - } - else - { - ix[k] = consent; - output.Add(consent); - } - } - - return output; - } - - public static ConsentDto BuildDto(IConsent entity) - { - return new ConsentDto - { - Id = entity.Id, - Current = entity.Current, - CreateDate = entity.CreateDate, - Source = entity.Source, - Context = entity.Context, - Action = entity.Action, - State = (int) entity.State, - Comment = entity.Comment + Id = dto.Id, + Current = dto.Current, + CreateDate = dto.CreateDate, + Source = dto.Source, + Context = dto.Context, + Action = dto.Action, + State = (ConsentState)dto.State, // assume value is valid + Comment = dto.Comment, }; + + // on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + consent.ResetDirtyProperties(false); + + if (ix.TryGetValue(k, out Consent? current)) + { + if (current.HistoryInternal == null) + { + current.HistoryInternal = new List(); + } + + current.HistoryInternal.Add(consent); + } + else + { + ix[k] = consent; + output.Add(consent); + } } + + return output; } + + public static ConsentDto BuildDto(IConsent entity) => + new ConsentDto + { + Id = entity.Id, + Current = entity.Current, + CreateDate = entity.CreateDate, + Source = entity.Source, + Context = entity.Context, + Action = entity.Action, + State = (int)entity.State, + Comment = entity.Comment, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs index 5048474ee7..ad295505c4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs @@ -1,331 +1,320 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal class ContentBaseFactory { - internal class ContentBaseFactory + /// + /// Builds an IContent item from a dto and content type. + /// + public static Content BuildEntity(DocumentDto dto, IContentType? contentType) { - /// - /// Builds an IContent item from a dto and content type. - /// - public static Content BuildEntity(DocumentDto dto, IContentType? contentType) + ContentDto contentDto = dto.ContentDto; + NodeDto nodeDto = contentDto.NodeDto; + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; + ContentVersionDto contentVersionDto = documentVersionDto.ContentVersionDto; + DocumentVersionDto? publishedVersionDto = dto.PublishedVersionDto; + + var content = new Content(nodeDto.Text, nodeDto.ParentId, contentType); + + try { - var contentDto = dto.ContentDto; - var nodeDto = contentDto.NodeDto; - var documentVersionDto = dto.DocumentVersionDto; - var contentVersionDto = documentVersionDto.ContentVersionDto; - var publishedVersionDto = dto.PublishedVersionDto; + content.DisableChangeTracking(); - var content = new Content(nodeDto.Text, nodeDto.ParentId, contentType); + content.Id = dto.NodeId; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; - try + content.Name = contentVersionDto.Text; + + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; + + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; + + content.Published = dto.Published; + content.Edited = dto.Edited; + + // TODO: shall we get published infos or not? + // if (dto.Published) + if (publishedVersionDto != null) { - content.DisableChangeTracking(); - - content.Id = dto.NodeId; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; - - content.Name = contentVersionDto.Text; - - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; - - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; - - content.Published = dto.Published; - content.Edited = dto.Edited; - - // TODO: shall we get published infos or not? - //if (dto.Published) - if (publishedVersionDto != null) - { - content.PublishedVersionId = publishedVersionDto.Id; - content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; - content.PublishName = publishedVersionDto.ContentVersionDto.Text; - content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; - } - - // templates = ignored, managed by the repository - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); + content.PublishedVersionId = publishedVersionDto.Id; + content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; + content.PublishName = publishedVersionDto.ContentVersionDto.Text; + content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; } + + // templates = ignored, managed by the repository + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; } - - /// - /// Builds an IMedia item from a dto and content type. - /// - public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) + finally { - var nodeDto = dto.NodeDto; - var contentVersionDto = dto.ContentVersionDto; - - var content = new Core.Models.Media(nodeDto.Text, nodeDto.ParentId, contentType); - - try - { - content.DisableChangeTracking(); - - content.Id = dto.NodeId; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; - - // TODO: missing names? - - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; - - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); - } + content.EnableChangeTracking(); } + } - /// - /// Builds an IMedia item from a dto and content type. - /// - public static Member BuildEntity(MemberDto dto, IMemberType? contentType) + /// + /// Builds an IMedia item from a dto and content type. + /// + public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) + { + NodeDto nodeDto = dto.NodeDto; + ContentVersionDto contentVersionDto = dto.ContentVersionDto; + + var content = new Core.Models.Media(nodeDto.Text, nodeDto.ParentId, contentType); + + try { - var nodeDto = dto.ContentDto.NodeDto; - var contentVersionDto = dto.ContentVersionDto; + content.DisableChangeTracking(); - var content = new Member(nodeDto.Text, dto.Email, dto.LoginName, dto.Password, contentType); + content.Id = dto.NodeId; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; - try - { - content.DisableChangeTracking(); + // TODO: missing names? + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; - content.Id = dto.NodeId; - content.SecurityStamp = dto.SecurityStampToken; - content.EmailConfirmedDate = dto.EmailConfirmedDate; - content.PasswordConfiguration = dto.PasswordConfig; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; - // TODO: missing names? - - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; - - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; - content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; - content.IsLockedOut = dto.IsLockedOut; - content.IsApproved = dto.IsApproved; - content.LastLoginDate = dto.LastLoginDate; - content.LastLockoutDate = dto.LastLockoutDate; - content.LastPasswordChangeDate = dto.LastPasswordChangeDate; - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; } - - /// - /// Builds a dto from an IContent item. - /// - public static DocumentDto BuildDto(IContent entity, Guid objectType) + finally { - var contentDto = BuildContentDto(entity, objectType); - - var dto = new DocumentDto - { - NodeId = entity.Id, - Published = entity.Published, - ContentDto = contentDto, - DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) - }; - - return dto; + content.EnableChangeTracking(); } + } - public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ContentScheduleCollection contentSchedule, ILanguageRepository languageRepository) + /// + /// Builds an IMedia item from a dto and content type. + /// + public static Member BuildEntity(MemberDto dto, IMemberType? contentType) + { + NodeDto nodeDto = dto.ContentDto.NodeDto; + ContentVersionDto contentVersionDto = dto.ContentVersionDto; + + var content = new Member(nodeDto.Text, dto.Email, dto.LoginName, dto.Password, contentType); + + try { - return contentSchedule.FullSchedule.Select(x => - (x, new ContentScheduleDto + content.DisableChangeTracking(); + + content.Id = dto.NodeId; + content.SecurityStamp = dto.SecurityStampToken; + content.EmailConfirmedDate = dto.EmailConfirmedDate; + content.PasswordConfiguration = dto.PasswordConfig; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; + + // TODO: missing names? + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; + + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; + content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; + content.IsLockedOut = dto.IsLockedOut; + content.IsApproved = dto.IsApproved; + content.LastLoginDate = dto.LastLoginDate; + content.LastLockoutDate = dto.LastLockoutDate; + content.LastPasswordChangeDate = dto.LastPasswordChangeDate; + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally + { + content.EnableChangeTracking(); + } + } + + /// + /// Builds a dto from an IContent item. + /// + public static DocumentDto BuildDto(IContent entity, Guid objectType) + { + ContentDto contentDto = BuildContentDto(entity, objectType); + + var dto = new DocumentDto + { + NodeId = entity.Id, + Published = entity.Published, + ContentDto = contentDto, + DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto), + }; + + return dto; + } + + public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto( + IContent entity, + ContentScheduleCollection contentSchedule, + ILanguageRepository languageRepository) => + contentSchedule.FullSchedule.Select(x => + (x, + new ContentScheduleDto { Action = x.Action.ToString(), Date = x.Date, NodeId = entity.Id, LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), - Id = x.Id + Id = x.Id, })); - } - /// - /// Builds a dto from an IMedia item. - /// - public static MediaDto BuildDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity) + /// + /// Builds a dto from an IMedia item. + /// + public static MediaDto BuildDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity) + { + ContentDto contentDto = BuildContentDto(entity, Constants.ObjectTypes.Media); + + var dto = new MediaDto { - var contentDto = BuildContentDto(entity, Cms.Core.Constants.ObjectTypes.Media); + NodeId = entity.Id, + ContentDto = contentDto, + MediaVersionDto = BuildMediaVersionDto(mediaUrlGenerators, entity, contentDto), + }; - var dto = new MediaDto - { - NodeId = entity.Id, - ContentDto = contentDto, - MediaVersionDto = BuildMediaVersionDto(mediaUrlGenerators, entity, contentDto) - }; + return dto; + } - return dto; - } + /// + /// Builds a dto from an IMember item. + /// + public static MemberDto BuildDto(IMember entity) + { + ContentDto contentDto = BuildContentDto(entity, Constants.ObjectTypes.Member); - /// - /// Builds a dto from an IMember item. - /// - public static MemberDto BuildDto(IMember entity) + var dto = new MemberDto { - var contentDto = BuildContentDto(entity, Cms.Core.Constants.ObjectTypes.Member); + Email = entity.Email, + LoginName = entity.Username, + NodeId = entity.Id, + Password = entity.RawPasswordValue, + SecurityStampToken = entity.SecurityStamp, + EmailConfirmedDate = entity.EmailConfirmedDate, + ContentDto = contentDto, + ContentVersionDto = BuildContentVersionDto(entity, contentDto), + PasswordConfig = entity.PasswordConfiguration, + FailedPasswordAttempts = entity.FailedPasswordAttempts, + IsApproved = entity.IsApproved, + IsLockedOut = entity.IsLockedOut, + LastLockoutDate = entity.LastLockoutDate, + LastLoginDate = entity.LastLoginDate, + LastPasswordChangeDate = entity.LastPasswordChangeDate, + }; + return dto; + } - var dto = new MemberDto - { - Email = entity.Email, - LoginName = entity.Username, - NodeId = entity.Id, - Password = entity.RawPasswordValue, - SecurityStampToken = entity.SecurityStamp, - EmailConfirmedDate = entity.EmailConfirmedDate, - ContentDto = contentDto, - ContentVersionDto = BuildContentVersionDto(entity, contentDto), - PasswordConfig = entity.PasswordConfiguration, - FailedPasswordAttempts = entity.FailedPasswordAttempts, - IsApproved = entity.IsApproved, - IsLockedOut = entity.IsLockedOut, - LastLockoutDate = entity.LastLockoutDate, - LastLoginDate = entity.LastLoginDate, - LastPasswordChangeDate = entity.LastPasswordChangeDate, - }; - return dto; - } - - private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) + private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) + { + var dto = new ContentDto { - var dto = new ContentDto - { - NodeId = entity.Id, - ContentTypeId = entity.ContentTypeId, + NodeId = entity.Id, ContentTypeId = entity.ContentTypeId, NodeDto = BuildNodeDto(entity, objectType), + }; - NodeDto = BuildNodeDto(entity, objectType) - }; + return dto; + } - return dto; - } - - private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) + private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) + { + var dto = new NodeDto { - var dto = new NodeDto - { - NodeId = entity.Id, - UniqueId = entity.Key, - ParentId = entity.ParentId, - Level = Convert.ToInt16(entity.Level), - Path = entity.Path, - SortOrder = entity.SortOrder, - Trashed = entity.Trashed, - UserId = entity.CreatorId, - Text = entity.Name, - NodeObjectType = objectType, - CreateDate = entity.CreateDate - }; + NodeId = entity.Id, + UniqueId = entity.Key, + ParentId = entity.ParentId, + Level = Convert.ToInt16(entity.Level), + Path = entity.Path, + SortOrder = entity.SortOrder, + Trashed = entity.Trashed, + UserId = entity.CreatorId, + Text = entity.Name, + NodeObjectType = objectType, + CreateDate = entity.CreateDate, + }; - return dto; - } + return dto; + } - // always build the current / VersionPk dto - // we're never going to build / save old versions (which are immutable) - private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) + // always build the current / VersionPk dto + // we're never going to build / save old versions (which are immutable) + private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) + { + var dto = new ContentVersionDto { - var dto = new ContentVersionDto - { - Id = entity.VersionId, - NodeId = entity.Id, - VersionDate = entity.UpdateDate, - UserId = entity.WriterId, - Current = true, // always building the current one - Text = entity.Name, + Id = entity.VersionId, + NodeId = entity.Id, + VersionDate = entity.UpdateDate, + UserId = entity.WriterId, + Current = true, // always building the current one + Text = entity.Name, + ContentDto = contentDto, + }; - ContentDto = contentDto - }; + return dto; + } - return dto; - } - - // always build the current / VersionPk dto - // we're never going to build / save old versions (which are immutable) - private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) + // always build the current / VersionPk dto + // we're never going to build / save old versions (which are immutable) + private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) + { + var dto = new DocumentVersionDto { - var dto = new DocumentVersionDto - { - Id = entity.VersionId, - TemplateId = entity.TemplateId, - Published = false, // always building the current, unpublished one + Id = entity.VersionId, + TemplateId = entity.TemplateId, + Published = false, // always building the current, unpublished one - ContentVersionDto = BuildContentVersionDto(entity, contentDto) - }; + ContentVersionDto = BuildContentVersionDto(entity, contentDto), + }; - return dto; - } + return dto; + } - private static MediaVersionDto BuildMediaVersionDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity, ContentDto contentDto) + private static MediaVersionDto BuildMediaVersionDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity, ContentDto contentDto) + { + // try to get a path from the string being stored for media + // TODO: only considering umbracoFile + string? path = null; + + if (entity.Properties.TryGetValue(Constants.Conventions.Media.File, out IProperty? property) + && mediaUrlGenerators.TryGetMediaPath(property.PropertyType.PropertyEditorAlias, property.GetValue(), out var mediaPath)) { - // try to get a path from the string being stored for media - // TODO: only considering umbracoFile - - string? path = null; - - if (entity.Properties.TryGetValue(Cms.Core.Constants.Conventions.Media.File, out var property) - && mediaUrlGenerators.TryGetMediaPath(property.PropertyType.PropertyEditorAlias, property.GetValue(), out var mediaPath)) - { - path = mediaPath; - } - - var dto = new MediaVersionDto - { - Id = entity.VersionId, - Path = path, - - ContentVersionDto = BuildContentVersionDto(entity, contentDto) - }; - - return dto; + path = mediaPath; } + + var dto = new MediaVersionDto + { + Id = entity.VersionId, Path = path, ContentVersionDto = BuildContentVersionDto(entity, contentDto), + }; + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs index a3a1deb62d..095f338399 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs @@ -1,180 +1,188 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +// factory for +// IContentType (document types) +// IMediaType (media types) +// IMemberType (member types) +// +internal static class ContentTypeFactory { - // factory for - // IContentType (document types) - // IMediaType (media types) - // IMemberType (member types) - // - internal static class ContentTypeFactory + #region IContentType + + public static IContentType BuildContentTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) { - #region IContentType + var contentType = new ContentType(shortStringHelper, dto.NodeDto.ParentId); - public static IContentType BuildContentTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + try { - var contentType = new ContentType(shortStringHelper, dto.NodeDto.ParentId); + contentType.DisableChangeTracking(); - try - { - contentType.DisableChangeTracking(); - - BuildCommonEntity(contentType, dto); - - // reset dirty initial properties (U4-1946) - contentType.ResetDirtyProperties(false); - return contentType; - } - finally - { - contentType.EnableChangeTracking(); - } - } - - #endregion - - #region IMediaType - - public static IMediaType BuildMediaTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) - { - var contentType = new MediaType(shortStringHelper, dto.NodeDto.ParentId); - try - { - contentType.DisableChangeTracking(); - - BuildCommonEntity(contentType, dto); - - // reset dirty initial properties (U4-1946) - contentType.ResetDirtyProperties(false); - } - finally - { - contentType.EnableChangeTracking(); - } + BuildCommonEntity(contentType, dto); + // reset dirty initial properties (U4-1946) + contentType.ResetDirtyProperties(false); return contentType; } - - #endregion - - #region IMemberType - - public static IMemberType BuildMemberTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + finally { - var contentType = new MemberType(shortStringHelper, dto.NodeDto.ParentId); - try - { - contentType.DisableChangeTracking(); - BuildCommonEntity(contentType, dto, false); - contentType.ResetDirtyProperties(false); - } - finally - { - contentType.EnableChangeTracking(); - } - - return contentType; + contentType.EnableChangeTracking(); } - - public static IEnumerable BuildMemberPropertyTypeDtos(IMemberType entity) - { - var memberType = entity as MemberType; - if (memberType == null || memberType.PropertyTypes.Any() == false) - return Enumerable.Empty(); - - var dtos = memberType.PropertyTypes.Select(x => new MemberPropertyTypeDto - { - NodeId = entity.Id, - PropertyTypeId = x.Id, - CanEdit = memberType.MemberCanEditProperty(x.Alias), - ViewOnProfile = memberType.MemberCanViewProperty(x.Alias), - IsSensitive = memberType.IsSensitiveProperty(x.Alias) - }).ToList(); - return dtos; - } - - #endregion - - #region Common - - private static void BuildCommonEntity(ContentTypeBase entity, ContentTypeDto dto, bool setVariations = true) - { - entity.Id = dto.NodeDto.NodeId; - entity.Key = dto.NodeDto.UniqueId; - entity.Alias = dto.Alias ?? string.Empty; - entity.Name = dto.NodeDto.Text; - entity.Icon = dto.Icon; - entity.Thumbnail = dto.Thumbnail; - entity.SortOrder = dto.NodeDto.SortOrder; - entity.Description = dto.Description; - entity.CreateDate = dto.NodeDto.CreateDate; - entity.UpdateDate = dto.NodeDto.CreateDate; - entity.Path = dto.NodeDto.Path; - entity.Level = dto.NodeDto.Level; - entity.CreatorId = dto.NodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - entity.AllowedAsRoot = dto.AllowAtRoot; - entity.IsContainer = dto.IsContainer; - entity.IsElement = dto.IsElement; - entity.Trashed = dto.NodeDto.Trashed; - - if (setVariations) - entity.Variations = (ContentVariation) dto.Variations; - } - - public static ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) - { - Guid nodeObjectType; - if (entity is IContentType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.DocumentType; - else if (entity is IMediaType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType; - else if (entity is IMemberType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.MemberType; - else - throw new Exception("Invalid entity."); - - var contentTypeDto = new ContentTypeDto - { - Alias = entity.Alias, - Description = entity.Description, - Icon = entity.Icon, - Thumbnail = entity.Thumbnail, - NodeId = entity.Id, - AllowAtRoot = entity.AllowedAsRoot, - IsContainer = entity.IsContainer, - IsElement = entity.IsElement, - Variations = (byte) entity.Variations, - NodeDto = BuildNodeDto(entity, nodeObjectType) - }; - return contentTypeDto; - } - - private static NodeDto BuildNodeDto(IUmbracoEntity entity, Guid nodeObjectType) - { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeObjectType = nodeObjectType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - return nodeDto; - } - - #endregion } + + #endregion + + #region IMediaType + + public static IMediaType BuildMediaTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + { + var contentType = new MediaType(shortStringHelper, dto.NodeDto.ParentId); + try + { + contentType.DisableChangeTracking(); + + BuildCommonEntity(contentType, dto); + + // reset dirty initial properties (U4-1946) + contentType.ResetDirtyProperties(false); + } + finally + { + contentType.EnableChangeTracking(); + } + + return contentType; + } + + #endregion + + #region IMemberType + + public static IMemberType BuildMemberTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + { + var contentType = new MemberType(shortStringHelper, dto.NodeDto.ParentId); + try + { + contentType.DisableChangeTracking(); + BuildCommonEntity(contentType, dto, false); + contentType.ResetDirtyProperties(false); + } + finally + { + contentType.EnableChangeTracking(); + } + + return contentType; + } + + public static IEnumerable BuildMemberPropertyTypeDtos(IMemberType entity) + { + if (entity is not MemberType memberType || memberType.PropertyTypes.Any() == false) + { + return Enumerable.Empty(); + } + + var dtos = memberType.PropertyTypes.Select(x => new MemberPropertyTypeDto + { + NodeId = entity.Id, + PropertyTypeId = x.Id, + CanEdit = memberType.MemberCanEditProperty(x.Alias), + ViewOnProfile = memberType.MemberCanViewProperty(x.Alias), + IsSensitive = memberType.IsSensitiveProperty(x.Alias), + }).ToList(); + return dtos; + } + + public static ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) + { + Guid nodeObjectType; + if (entity is IContentType) + { + nodeObjectType = Constants.ObjectTypes.DocumentType; + } + else if (entity is IMediaType) + { + nodeObjectType = Constants.ObjectTypes.MediaType; + } + else if (entity is IMemberType) + { + nodeObjectType = Constants.ObjectTypes.MemberType; + } + else + { + throw new Exception("Invalid entity."); + } + + var contentTypeDto = new ContentTypeDto + { + Alias = entity.Alias, + Description = entity.Description, + Icon = entity.Icon, + Thumbnail = entity.Thumbnail, + NodeId = entity.Id, + AllowAtRoot = entity.AllowedAsRoot, + IsContainer = entity.IsContainer, + IsElement = entity.IsElement, + Variations = (byte)entity.Variations, + NodeDto = BuildNodeDto(entity, nodeObjectType), + }; + return contentTypeDto; + } + + #endregion + + #region Common + + private static void BuildCommonEntity(ContentTypeBase entity, ContentTypeDto dto, bool setVariations = true) + { + entity.Id = dto.NodeDto.NodeId; + entity.Key = dto.NodeDto.UniqueId; + entity.Alias = dto.Alias ?? string.Empty; + entity.Name = dto.NodeDto.Text; + entity.Icon = dto.Icon; + entity.Thumbnail = dto.Thumbnail; + entity.SortOrder = dto.NodeDto.SortOrder; + entity.Description = dto.Description; + entity.CreateDate = dto.NodeDto.CreateDate; + entity.UpdateDate = dto.NodeDto.CreateDate; + entity.Path = dto.NodeDto.Path; + entity.Level = dto.NodeDto.Level; + entity.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; + entity.AllowedAsRoot = dto.AllowAtRoot; + entity.IsContainer = dto.IsContainer; + entity.IsElement = dto.IsElement; + entity.Trashed = dto.NodeDto.Trashed; + + if (setVariations) + { + entity.Variations = (ContentVariation)dto.Variations; + } + } + + private static NodeDto BuildNodeDto(IUmbracoEntity entity, Guid nodeObjectType) + { + var nodeDto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), + NodeObjectType = nodeObjectType, + ParentId = entity.ParentId, + Path = entity.Path, + SortOrder = entity.SortOrder, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key, + UserId = entity.CreatorId, + }; + return nodeDto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs index df655d3ade..69862364de 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs @@ -1,91 +1,90 @@ -using System; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DataTypeFactory { - internal static class DataTypeFactory + public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, IConfigurationEditorJsonSerializer serializer) { - public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, IConfigurationEditorJsonSerializer serializer) + // Check we have an editor for the data type. + if (!editors.TryGet(dto.EditorAlias, out IDataEditor? editor)) { - // Check we have an editor for the data type. - if (!editors.TryGet(dto.EditorAlias, out var editor)) - { - logger.LogWarning("Could not find an editor with alias {EditorAlias}, treating as Label. " + - "The site may fail to boot and/or load data types and run.", dto.EditorAlias); + logger.LogWarning( + "Could not find an editor with alias {EditorAlias}, treating as Label. " + "The site may fail to boot and/or load data types and run.", dto.EditorAlias); - // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear - // the situation to the user. - editor = new MissingPropertyEditor(); - } - - var dataType = new DataType(editor, serializer); - - try - { - dataType.DisableChangeTracking(); - - dataType.CreateDate = dto.NodeDto.CreateDate; - dataType.DatabaseType = dto.DbType.EnumParse(true); - dataType.Id = dto.NodeId; - dataType.Key = dto.NodeDto.UniqueId; - dataType.Level = dto.NodeDto.Level; - dataType.UpdateDate = dto.NodeDto.CreateDate; - dataType.Name = dto.NodeDto.Text; - dataType.ParentId = dto.NodeDto.ParentId; - dataType.Path = dto.NodeDto.Path; - dataType.SortOrder = dto.NodeDto.SortOrder; - dataType.Trashed = dto.NodeDto.Trashed; - dataType.CreatorId = dto.NodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - - dataType.SetLazyConfiguration(dto.Configuration); - - // reset dirty initial properties (U4-1946) - dataType.ResetDirtyProperties(false); - return dataType; - } - finally - { - dataType.EnableChangeTracking(); - } + // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear + // the situation to the user. + editor = new MissingPropertyEditor(); } - public static DataTypeDto BuildDto(IDataType entity, IConfigurationEditorJsonSerializer serializer) - { - var dataTypeDto = new DataTypeDto - { - EditorAlias = entity.EditorAlias, - NodeId = entity.Id, - DbType = entity.DatabaseType.ToString(), - Configuration = ConfigurationEditor.ToDatabase(entity.Configuration, serializer), - NodeDto = BuildNodeDto(entity) - }; + var dataType = new DataType(editor, serializer); - return dataTypeDto; + try + { + dataType.DisableChangeTracking(); + + dataType.CreateDate = dto.NodeDto.CreateDate; + dataType.DatabaseType = dto.DbType.EnumParse(true); + dataType.Id = dto.NodeId; + dataType.Key = dto.NodeDto.UniqueId; + dataType.Level = dto.NodeDto.Level; + dataType.UpdateDate = dto.NodeDto.CreateDate; + dataType.Name = dto.NodeDto.Text; + dataType.ParentId = dto.NodeDto.ParentId; + dataType.Path = dto.NodeDto.Path; + dataType.SortOrder = dto.NodeDto.SortOrder; + dataType.Trashed = dto.NodeDto.Trashed; + dataType.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; + + dataType.SetLazyConfiguration(dto.Configuration); + + // reset dirty initial properties (U4-1946) + dataType.ResetDirtyProperties(false); + return dataType; } - - private static NodeDto BuildNodeDto(IDataType entity) + finally { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = Convert.ToInt16(entity.Level), - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = entity.Trashed, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - - return nodeDto; + dataType.EnableChangeTracking(); } } + + public static DataTypeDto BuildDto(IDataType entity, IConfigurationEditorJsonSerializer serializer) + { + var dataTypeDto = new DataTypeDto + { + EditorAlias = entity.EditorAlias, + NodeId = entity.Id, + DbType = entity.DatabaseType.ToString(), + Configuration = ConfigurationEditor.ToDatabase(entity.Configuration, serializer), + NodeDto = BuildNodeDto(entity), + }; + + return dataTypeDto; + } + + private static NodeDto BuildNodeDto(IDataType entity) + { + var nodeDto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = Convert.ToInt16(entity.Level), + NodeObjectType = Constants.ObjectTypes.DataType, + ParentId = entity.ParentId, + Path = entity.Path, + SortOrder = entity.SortOrder, + Text = entity.Name, + Trashed = entity.Trashed, + UniqueId = entity.Key, + UserId = entity.CreatorId, + }; + + return nodeDto; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs index 31dc7ef2ec..5a82c3be01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs @@ -1,70 +1,68 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DictionaryItemFactory { - internal static class DictionaryItemFactory + #region Implementation of IEntityFactory + + public static IDictionaryItem BuildEntity(DictionaryDto dto) { - #region Implementation of IEntityFactory + var item = new DictionaryItem(dto.Parent, dto.Key); - public static IDictionaryItem BuildEntity(DictionaryDto dto) + try { - var item = new DictionaryItem(dto.Parent, dto.Key); + item.DisableChangeTracking(); - try - { - item.DisableChangeTracking(); + item.Id = dto.PrimaryKey; + item.Key = dto.UniqueId; - item.Id = dto.PrimaryKey; - item.Key = dto.UniqueId; - - // reset dirty initial properties (U4-1946) - item.ResetDirtyProperties(false); - return item; - } - finally - { - item.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + item.ResetDirtyProperties(false); + return item; } - - public static DictionaryDto BuildDto(IDictionaryItem entity) + finally { - return new DictionaryDto - { - UniqueId = entity.Key, - Key = entity.ItemKey, - Parent = entity.ParentId, - PrimaryKey = entity.Id, - LanguageTextDtos = BuildLanguageTextDtos(entity) - }; - } - - #endregion - - private static List BuildLanguageTextDtos(IDictionaryItem entity) - { - var list = new List(); - if (entity.Translations is not null) - { - foreach (var translation in entity.Translations) - { - var text = new LanguageTextDto - { - LanguageId = translation.LanguageId, - UniqueId = translation.Key, - Value = translation.Value!, - }; - - if (translation.HasIdentity) - text.PrimaryKey = translation.Id; - - list.Add(text); - } - } - - return list; + item.EnableChangeTracking(); } } + + private static List BuildLanguageTextDtos(IDictionaryItem entity) + { + var list = new List(); + if (entity.Translations is not null) + { + foreach (IDictionaryTranslation translation in entity.Translations) + { + var text = new LanguageTextDto + { + LanguageId = translation.LanguageId, + UniqueId = translation.Key, + Value = translation.Value, + }; + + if (translation.HasIdentity) + { + text.PrimaryKey = translation.Id; + } + + list.Add(text); + } + } + + return list; + } + + public static DictionaryDto BuildDto(IDictionaryItem entity) => + new DictionaryDto + { + UniqueId = entity.Key, + Key = entity.ItemKey, + Parent = entity.ParentId, + PrimaryKey = entity.Id, + LanguageTextDtos = BuildLanguageTextDtos(entity), + }; + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs index a53222ad5e..a06adb5c34 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs @@ -1,48 +1,43 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DictionaryTranslationFactory { - internal static class DictionaryTranslationFactory + #region Implementation of IEntityFactory + + public static IDictionaryTranslation BuildEntity(LanguageTextDto dto, Guid uniqueId) { - #region Implementation of IEntityFactory + var item = new DictionaryTranslation(dto.LanguageId, dto.Value, uniqueId); - public static IDictionaryTranslation BuildEntity(LanguageTextDto dto, Guid uniqueId) + try { - var item = new DictionaryTranslation(dto.LanguageId, dto.Value, uniqueId); + item.DisableChangeTracking(); - try - { - item.DisableChangeTracking(); + item.Id = dto.PrimaryKey; - item.Id = dto.PrimaryKey; - - // reset dirty initial properties (U4-1946) - item.ResetDirtyProperties(false); - return item; - } - finally - { - item.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + item.ResetDirtyProperties(false); + return item; } - - public static LanguageTextDto BuildDto(IDictionaryTranslation entity, Guid uniqueId) + finally { - var text = new LanguageTextDto - { - LanguageId = entity.LanguageId, - UniqueId = uniqueId, - Value = entity.Value - }; - - if (entity.HasIdentity) - text.PrimaryKey = entity.Id; - - return text; + item.EnableChangeTracking(); } - - #endregion } + + public static LanguageTextDto BuildDto(IDictionaryTranslation entity, Guid uniqueId) + { + var text = new LanguageTextDto { LanguageId = entity.LanguageId, UniqueId = uniqueId, Value = entity.Value }; + + if (entity.HasIdentity) + { + text.PrimaryKey = entity.Id; + } + + return text; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 1c74dcb8bd..77ab4ed404 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -1,81 +1,78 @@ -using System; -using System.Globalization; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ExternalLoginFactory { - internal static class ExternalLoginFactory + public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto) { - public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto) - { - var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate); + var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate); - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) - { + public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) + { + // If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state. + // At this point we have to manually set the key, to ensure external logins can be used to upgrade + var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString(); - //If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state. - //At this point we have to manually set the key, to ensure external logins can be used to upgrade - var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString(); - - var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate) + var entity = + new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate) { - UserData = dto.UserData + UserData = dto.UserData, }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) + public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) + { + var dto = new ExternalLoginDto { - var dto = new ExternalLoginDto - { - Id = entity.Id, - CreateDate = entity.CreateDate, - LoginProvider = entity.LoginProvider, - ProviderKey = entity.ProviderKey, - UserOrMemberKey = entity.Key, - UserData = entity.UserData - }; + Id = entity.Id, + CreateDate = entity.CreateDate, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserOrMemberKey = entity.Key, + UserData = entity.UserData, + }; - return dto; - } + return dto; + } - public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null) + public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null) + { + var dto = new ExternalLoginDto { - var dto = new ExternalLoginDto - { - Id = id ?? default, - UserOrMemberKey = userOrMemberKey, - LoginProvider = entity.LoginProvider, - ProviderKey = entity.ProviderKey, - UserData = entity.UserData, - CreateDate = DateTime.Now - }; + Id = id ?? default, + UserOrMemberKey = userOrMemberKey, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserData = entity.UserData, + CreateDate = DateTime.Now, + }; - return dto; - } + return dto; + } - public static ExternalLoginTokenDto BuildDto(int externalLoginId, IExternalLoginToken token, int? id = null) + public static ExternalLoginTokenDto BuildDto(int externalLoginId, IExternalLoginToken token, int? id = null) + { + var dto = new ExternalLoginTokenDto { - var dto = new ExternalLoginTokenDto - { - Id = id ?? default, - ExternalLoginId = externalLoginId, - Name = token.Name, - Value = token.Value, - CreateDate = DateTime.Now - }; + Id = id ?? default, + ExternalLoginId = externalLoginId, + Name = token.Name, + Value = token.Value, + CreateDate = DateTime.Now, + }; - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs index 2c7c6c081e..9ab958c306 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs @@ -1,51 +1,53 @@ +using System.Globalization; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class LanguageFactory { - internal static class LanguageFactory + public static ILanguage BuildEntity(LanguageDto dto) { - public static ILanguage BuildEntity(LanguageDto dto) + ArgumentNullException.ThrowIfNull(dto); + if (dto.IsoCode is null) { - ArgumentNullException.ThrowIfNull(dto); - if (dto.IsoCode == null || dto.CultureName == null) - { - throw new InvalidOperationException("Language ISO code and/or culture name can't be null."); - } - - var lang = new Language(dto.IsoCode, dto.CultureName) - { - Id = dto.Id, - IsDefault = dto.IsDefault, - IsMandatory = dto.IsMandatory, - FallbackLanguageId = dto.FallbackLanguageId - }; - - // Reset dirty initial properties - lang.ResetDirtyProperties(false); - - return lang; + throw new InvalidOperationException("Language ISO code can't be null."); } - public static LanguageDto BuildDto(ILanguage entity) + dto.CultureName ??= CultureInfo.GetCultureInfo(dto.IsoCode).EnglishName; + + var lang = new Language(dto.IsoCode, dto.CultureName) { - ArgumentNullException.ThrowIfNull(entity); + Id = dto.Id, + IsDefault = dto.IsDefault, + IsMandatory = dto.IsMandatory, + FallbackLanguageId = dto.FallbackLanguageId, + }; - var dto = new LanguageDto - { - IsoCode = entity.IsoCode, - CultureName = entity.CultureName, - IsDefault = entity.IsDefault, - IsMandatory = entity.IsMandatory, - FallbackLanguageId = entity.FallbackLanguageId - }; + // Reset dirty initial properties + lang.ResetDirtyProperties(false); - if (entity.HasIdentity) - { - dto.Id = (short)entity.Id; - } + return lang; + } - return dto; + public static LanguageDto BuildDto(ILanguage entity) + { + ArgumentNullException.ThrowIfNull(entity); + + var dto = new LanguageDto + { + IsoCode = entity.IsoCode, + CultureName = entity.CultureName, + IsDefault = entity.IsDefault, + IsMandatory = entity.IsMandatory, + FallbackLanguageId = entity.FallbackLanguageId, + }; + + if (entity.HasIdentity) + { + dto.Id = (short)entity.Id; } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs index 7f73abacaa..3725a2be6a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs @@ -1,79 +1,80 @@ -using System.Collections.Generic; -using System.Globalization; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class MacroFactory { - internal static class MacroFactory + public static IMacro BuildEntity(IShortStringHelper shortStringHelper, MacroDto dto) { - public static IMacro BuildEntity(IShortStringHelper shortStringHelper, MacroDto dto) + var model = new Macro(shortStringHelper, dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, + dto.Name, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroSource); + + try { - var model = new Macro(shortStringHelper, dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, dto.Name, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroSource); + model.DisableChangeTracking(); - try + foreach (MacroPropertyDto p in dto.MacroPropertyDtos.EmptyNull()) { - model.DisableChangeTracking(); - - foreach (var p in dto.MacroPropertyDtos.EmptyNull()) - { - model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); - } - - // reset dirty initial properties (U4-1946) - model.ResetDirtyProperties(false); - return model; - } - finally - { - model.EnableChangeTracking(); + model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); } + + // reset dirty initial properties (U4-1946) + model.ResetDirtyProperties(false); + return model; } - - public static MacroDto BuildDto(IMacro entity) + finally { - var dto = new MacroDto - { - UniqueId = entity.Key, - Alias = entity.Alias, - CacheByPage = entity.CacheByPage, - CachePersonalized = entity.CacheByMember, - DontRender = entity.DontRender, - Name = entity.Name, - MacroSource = entity.MacroSource, - RefreshRate = entity.CacheDuration, - UseInEditor = entity.UseInEditor, - MacroPropertyDtos = BuildPropertyDtos(entity), - MacroType = 7 //PartialView - }; - - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; - } - - private static List BuildPropertyDtos(IMacro entity) - { - var list = new List(); - foreach (var p in entity.Properties) - { - var text = new MacroPropertyDto - { - UniqueId = p.Key, - Alias = p.Alias, - Name = p.Name, - Macro = entity.Id, - SortOrder = (byte)p.SortOrder, - EditorAlias = p.EditorAlias, - Id = p.Id - }; - - list.Add(text); - } - return list; + model.EnableChangeTracking(); } } + + public static MacroDto BuildDto(IMacro entity) + { + var dto = new MacroDto + { + UniqueId = entity.Key, + Alias = entity.Alias, + CacheByPage = entity.CacheByPage, + CachePersonalized = entity.CacheByMember, + DontRender = entity.DontRender, + Name = entity.Name, + MacroSource = entity.MacroSource, + RefreshRate = entity.CacheDuration, + UseInEditor = entity.UseInEditor, + MacroPropertyDtos = BuildPropertyDtos(entity), + MacroType = 7, // PartialView + }; + + if (entity.HasIdentity) + { + dto.Id = entity.Id; + } + + return dto; + } + + private static List BuildPropertyDtos(IMacro entity) + { + var list = new List(); + foreach (IMacroProperty p in entity.Properties) + { + var text = new MacroPropertyDto + { + UniqueId = p.Key, + Alias = p.Alias, + Name = p.Name, + Macro = entity.Id, + SortOrder = (byte)p.SortOrder, + EditorAlias = p.EditorAlias, + Id = p.Id, + }; + + list.Add(text); + } + + return list; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs index d3ddf40ce3..b6ecbe3b6f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs @@ -1,71 +1,65 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class MemberGroupFactory { - internal static class MemberGroupFactory + private static readonly Guid _nodeObjectTypeId; + + static MemberGroupFactory() => _nodeObjectTypeId = Constants.ObjectTypes.MemberGroup; + + #region Implementation of IEntityFactory + + public static IMemberGroup BuildEntity(NodeDto dto) { + var group = new MemberGroup(); - private static readonly Guid _nodeObjectTypeId; - - static MemberGroupFactory() + try { - _nodeObjectTypeId = Cms.Core.Constants.ObjectTypes.MemberGroup; + group.DisableChangeTracking(); + + group.CreateDate = dto.CreateDate; + group.Id = dto.NodeId; + group.Key = dto.UniqueId; + group.Name = dto.Text; + + // reset dirty initial properties (U4-1946) + group.ResetDirtyProperties(false); + return group; } - - #region Implementation of IEntityFactory - - public static IMemberGroup BuildEntity(NodeDto dto) + finally { - var group = new MemberGroup(); - - try - { - group.DisableChangeTracking(); - - group.CreateDate = dto.CreateDate; - group.Id = dto.NodeId; - group.Key = dto.UniqueId; - group.Name = dto.Text; - - // reset dirty initial properties (U4-1946) - group.ResetDirtyProperties(false); - return group; - } - finally - { - group.EnableChangeTracking(); - } + group.EnableChangeTracking(); } - - public static NodeDto BuildDto(IMemberGroup entity) - { - var dto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = 0, - NodeObjectType = _nodeObjectTypeId, - ParentId = -1, - Path = "", - SortOrder = 0, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - - if (entity.HasIdentity) - { - dto.NodeId = entity.Id; - dto.Path = "-1," + entity.Id; - } - - return dto; - } - - #endregion - } + + public static NodeDto BuildDto(IMemberGroup entity) + { + var dto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = 0, + NodeObjectType = _nodeObjectTypeId, + ParentId = -1, + Path = string.Empty, + SortOrder = 0, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key, + UserId = entity.CreatorId, + }; + + if (entity.HasIdentity) + { + dto.NodeId = entity.Id; + dto.Path = "-1," + entity.Id; + } + + return dto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs index 1b8708c640..73e6de29a6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs @@ -1,193 +1,220 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PropertyFactory { - internal static class PropertyFactory + public static IEnumerable BuildEntities(IPropertyType[]? propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) { - public static IEnumerable BuildEntities(IPropertyType[]? propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) + var properties = new List(); + var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); + + if (propertyTypes is null) { - var properties = new List(); - var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); - - if (propertyTypes is null) - { - return properties; - } - foreach (var propertyType in propertyTypes) - { - var values = new List(); - int propertyId = default; - - // see notes in BuildDtos - we always have edit+published dtos - if (xdtos.TryGetValue(propertyType.Id, out var propDtos)) - { - foreach (var propDto in propDtos) - { - propertyId = propDto.Id; - values.Add(new Property.InitialPropertyValue(languageRepository.GetIsoCodeById(propDto.LanguageId), propDto.Segment, propDto.VersionId == publishedVersionId, propDto.Value)); - } - } - - var property = Property.CreateWithValues(propertyId, propertyType, values.ToArray()); - properties.Add(property); - } - return properties; } - private static PropertyDataDto BuildDto(int versionId, IProperty property, int? languageId, string? segment, object? value) + foreach (IPropertyType propertyType in propertyTypes) { - var dto = new PropertyDataDto { VersionId = versionId, PropertyTypeId = property.PropertyTypeId }; + var values = new List(); + int propertyId = default; - if (languageId.HasValue) - dto.LanguageId = languageId; - - if (segment != null) - dto.Segment = segment; - - if (property.ValueStorageType == ValueStorageType.Integer) + // see notes in BuildDtos - we always have edit+published dtos + if (xdtos.TryGetValue(propertyType.Id, out IEnumerable? propDtos)) { - if (value is bool || property.PropertyType.PropertyEditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Boolean) + foreach (PropertyDataDto propDto in propDtos) { - dto.IntegerValue = value != null && string.IsNullOrEmpty(value.ToString()) ? 0 : Convert.ToInt32(value); - } - else if (value != null && string.IsNullOrWhiteSpace(value.ToString()) == false && int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - dto.IntegerValue = val; + propertyId = propDto.Id; + values.Add(new Property.InitialPropertyValue( + languageRepository.GetIsoCodeById(propDto.LanguageId), + propDto.Segment, + propDto.VersionId == publishedVersionId, + propDto.Value)); } } - else if (property.ValueStorageType == ValueStorageType.Decimal && value != null) - { - if (decimal.TryParse(value.ToString(), out var val)) - { - dto.DecimalValue = val; // property value should be normalized already - } - } - else if (property.ValueStorageType == ValueStorageType.Date && value != null && string.IsNullOrWhiteSpace(value.ToString()) == false) - { - if (DateTime.TryParse(value.ToString(), out var date)) - { - dto.DateValue = date; - } - } - else if (property.ValueStorageType == ValueStorageType.Ntext && value != null) - { - dto.TextValue = value.ToString(); - } - else if (property.ValueStorageType == ValueStorageType.Nvarchar && value != null) - { - dto.VarcharValue = value.ToString(); - } - return dto; + var property = Property.CreateWithValues(propertyId, propertyType, values.ToArray()); + properties.Add(property); } - /// - /// Creates a collection of from a collection of - /// - /// - /// The of the entity containing the collection of - /// - /// - /// - /// The properties to map - /// - /// out parameter indicating that one or more properties have been edited - /// - /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. - /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. - /// - /// - public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, - ILanguageRepository languageRepository, out bool edited, - out HashSet? editedCultures) + return properties; + } + + /// + /// Creates a collection of from a collection of + /// + /// + /// The of the entity containing the collection of + /// + /// + /// + /// The properties to map + /// + /// out parameter indicating that one or more properties have been edited + /// + /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. + /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. + /// + /// + public static IEnumerable BuildDtos( + ContentVariation contentVariation, + int currentVersionId, + int publishedVersionId, + IEnumerable properties, + ILanguageRepository languageRepository, + out bool edited, + out HashSet? editedCultures) + { + var propertyDataDtos = new List(); + edited = false; + editedCultures = null; // don't allocate unless necessary + string? defaultCulture = null; // don't allocate unless necessary + + var entityVariesByCulture = contentVariation.VariesByCulture(); + + // create dtos for each property values, but only for values that do actually exist + // ie have a non-null value, everything else is just ignored and won't have a db row + foreach (IProperty property in properties) { - var propertyDataDtos = new List(); - edited = false; - editedCultures = null; // don't allocate unless necessary - string? defaultCulture = null; //don't allocate unless necessary - - var entityVariesByCulture = contentVariation.VariesByCulture(); - - // create dtos for each property values, but only for values that do actually exist - // ie have a non-null value, everything else is just ignored and won't have a db row - - foreach (var property in properties) + if (property.PropertyType.SupportsPublishing) { - if (property.PropertyType.SupportsPublishing) + // create the resulting hashset if it's not created and the entity varies by culture + if (entityVariesByCulture && editedCultures == null) { - //create the resulting hashset if it's not created and the entity varies by culture - if (entityVariesByCulture && editedCultures == null) - editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + } - // publishing = deal with edit and published values - foreach (var propertyValue in property.Values) + // publishing = deal with edit and published values + foreach (IPropertyValue propertyValue in property.Values) + { + var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; + var isCultureValue = propertyValue.Culture != null; + var isSegmentValue = propertyValue.Segment != null; + + // deal with published value + if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) { - var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; - var isCultureValue = propertyValue.Culture != null; - var isSegmentValue = propertyValue.Segment != null; + propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); + } - // deal with published value - if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) - propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue?.Segment, propertyValue?.PublishedValue)); + // deal with edit value + if (propertyValue.EditedValue != null || isSegmentValue) + { + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + } - // deal with edit value - if (propertyValue?.EditedValue != null || isSegmentValue) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue?.Culture), propertyValue?.Segment, propertyValue?.EditedValue)); + // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the + // administrator has previously changed the property type to be variant vs invariant. + // We need to check for this scenario here because otherwise the editedCultures and edited flags + // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to + // only process edited cultures based on the current value type and how the property varies. + // The above logic will still persist the currently saved property value for each culture in case the admin + // decides to swap the property's variance again, in which case the edited flag will be recalculated. + if ((property.PropertyType.VariesByCulture() && isInvariantValue) || + (!property.PropertyType.VariesByCulture() && isCultureValue)) + { + continue; + } - // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the - // administrator has previously changed the property type to be variant vs invariant. - // We need to check for this scenario here because otherwise the editedCultures and edited flags - // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to - // only process edited cultures based on the current value type and how the property varies. - // The above logic will still persist the currently saved property value for each culture in case the admin - // decides to swap the property's variance again, in which case the edited flag will be recalculated. + // use explicit equals here, else object comparison fails at comparing eg strings + var sameValues = propertyValue?.PublishedValue == null + ? propertyValue?.EditedValue == null + : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); - if (property.PropertyType.VariesByCulture() && isInvariantValue || !property.PropertyType.VariesByCulture() && isCultureValue) - continue; + edited |= !sameValues; - // use explicit equals here, else object comparison fails at comparing eg strings - var sameValues = propertyValue?.PublishedValue == null ? propertyValue?.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); - - edited |= !sameValues; - - if (entityVariesByCulture && !sameValues) + if (entityVariesByCulture && !sameValues) + { + if (isCultureValue && propertyValue?.Culture is not null) { - if (isCultureValue && propertyValue?.Culture is not null) + editedCultures?.Add(propertyValue.Culture); // report culture as edited + } + else if (isInvariantValue) + { + // flag culture as edited if it contains an edited invariant property + if (defaultCulture == null) { - editedCultures?.Add(propertyValue.Culture); // report culture as edited + defaultCulture = languageRepository.GetDefaultIsoCode(); } - else if (isInvariantValue) - { - // flag culture as edited if it contains an edited invariant property - if (defaultCulture == null) - defaultCulture = languageRepository.GetDefaultIsoCode(); - editedCultures?.Add(defaultCulture); - } + editedCultures?.Add(defaultCulture); } } } - else - { - foreach (var propertyValue in property.Values) - { - // not publishing = only deal with edit values - if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); - } - edited = true; - } } + else + { + foreach (IPropertyValue propertyValue in property.Values) + { + // not publishing = only deal with edit values + if (propertyValue.EditedValue != null) + { + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + } + } - return propertyDataDtos; + edited = true; + } } + + return propertyDataDtos; + } + + private static PropertyDataDto BuildDto(int versionId, IProperty property, int? languageId, string? segment, object? value) + { + var dto = new PropertyDataDto { VersionId = versionId, PropertyTypeId = property.PropertyTypeId }; + + if (languageId.HasValue) + { + dto.LanguageId = languageId; + } + + if (segment != null) + { + dto.Segment = segment; + } + + if (property.ValueStorageType == ValueStorageType.Integer) + { + if (value is bool || property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.Boolean) + { + dto.IntegerValue = value != null && string.IsNullOrEmpty(value.ToString()) ? 0 : Convert.ToInt32(value); + } + else if (value != null && string.IsNullOrWhiteSpace(value.ToString()) == false && + int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + dto.IntegerValue = val; + } + } + else if (property.ValueStorageType == ValueStorageType.Decimal && value != null) + { + if (decimal.TryParse(value.ToString(), out var val)) + { + dto.DecimalValue = val; // property value should be normalized already + } + } + else if (property.ValueStorageType == ValueStorageType.Date && value != null && + string.IsNullOrWhiteSpace(value.ToString()) == false) + { + if (DateTime.TryParse(value.ToString(), out DateTime date)) + { + dto.DateValue = date; + } + } + else if (property.ValueStorageType == ValueStorageType.Ntext && value != null) + { + dto.TextValue = value.ToString(); + } + else if (property.ValueStorageType == ValueStorageType.Nvarchar && value != null) + { + dto.VarcharValue = value.ToString(); + } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs index 333c0176c8..65dc528c17 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs @@ -1,160 +1,163 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PropertyGroupFactory { - internal static class PropertyGroupFactory + internal static PropertyTypeGroupDto BuildGroupDto(PropertyGroup propertyGroup, int contentTypeId) { - - #region Implementation of IEntityFactory,IEnumerable> - - public static IEnumerable BuildEntity(IEnumerable groupDtos, - bool isPublishing, - int contentTypeId, - DateTime createDate, - DateTime updateDate, - Func propertyTypeCtor) + var dto = new PropertyTypeGroupDto { - // groupDtos contains all the groups, those that are defined on the current - // content type, and those that are inherited from composition content types - var propertyGroups = new PropertyGroupCollection(); - foreach (var groupDto in groupDtos) - { - var group = new PropertyGroup(isPublishing); + UniqueId = propertyGroup.Key, + Type = (short)propertyGroup.Type, + ContentTypeNodeId = contentTypeId, + Text = propertyGroup.Name, + Alias = propertyGroup.Alias, + SortOrder = propertyGroup.SortOrder, + }; - try - { - group.DisableChangeTracking(); - - // if the group is defined on the current content type, - // assign its identifier, else it will be zero - if (groupDto.ContentTypeNodeId == contentTypeId) - group.Id = groupDto.Id; - - group.Key = groupDto.UniqueId; - group.Type = (PropertyGroupType)groupDto.Type; - group.Name = groupDto.Text; - group.Alias = groupDto.Alias; - group.SortOrder = groupDto.SortOrder; - - group.PropertyTypes = new PropertyTypeCollection(isPublishing); - - //Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded - var typeDtos = groupDto.PropertyTypeDtos?.Where(x => x.Id > 0) ?? Enumerable.Empty(); - foreach (var typeDto in typeDtos) - { - var tempGroupDto = groupDto; - var propertyType = propertyTypeCtor(typeDto.DataTypeDto.EditorAlias, - typeDto.DataTypeDto.DbType.EnumParse(true), - typeDto.Alias); - - try - { - propertyType.DisableChangeTracking(); - - propertyType.Alias = typeDto.Alias ?? string.Empty; - propertyType.DataTypeId = typeDto.DataTypeId; - propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; - propertyType.Description = typeDto.Description; - propertyType.Id = typeDto.Id; - propertyType.Key = typeDto.UniqueId; - propertyType.Name = typeDto.Name ?? string.Empty; - propertyType.Mandatory = typeDto.Mandatory; - propertyType.MandatoryMessage = typeDto.MandatoryMessage; - propertyType.SortOrder = typeDto.SortOrder; - propertyType.ValidationRegExp = typeDto.ValidationRegExp; - propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage; - propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id); - propertyType.CreateDate = createDate; - propertyType.UpdateDate = updateDate; - propertyType.Variations = (ContentVariation)typeDto.Variations; - - // reset dirty initial properties (U4-1946) - propertyType.ResetDirtyProperties(false); - group.PropertyTypes.Add(propertyType); - } - finally - { - propertyType.EnableChangeTracking(); - } - } - - // reset dirty initial properties (U4-1946) - group.ResetDirtyProperties(false); - propertyGroups.Add(group); - } - finally - { - group.EnableChangeTracking(); - } - } - - return propertyGroups; + if (propertyGroup.HasIdentity) + { + dto.Id = propertyGroup.Id; } - public static IEnumerable BuildDto(IEnumerable entity) - { - return entity.Select(BuildGroupDto).ToList(); - } + dto.PropertyTypeDtos = propertyGroup.PropertyTypes + ?.Select(propertyType => BuildPropertyTypeDto(propertyGroup.Id, propertyType, contentTypeId)).ToList(); - #endregion - - internal static PropertyTypeGroupDto BuildGroupDto(PropertyGroup propertyGroup, int contentTypeId) - { - var dto = new PropertyTypeGroupDto - { - UniqueId = propertyGroup.Key, - Type = (short)propertyGroup.Type, - ContentTypeNodeId = contentTypeId, - Text = propertyGroup.Name, - Alias = propertyGroup.Alias, - SortOrder = propertyGroup.SortOrder - }; - - if (propertyGroup.HasIdentity) - dto.Id = propertyGroup.Id; - - dto.PropertyTypeDtos = propertyGroup.PropertyTypes?.Select(propertyType => BuildPropertyTypeDto(propertyGroup.Id, propertyType, contentTypeId)).ToList(); - - return dto; - } - - internal static PropertyTypeDto BuildPropertyTypeDto(int groupId, IPropertyType propertyType, int contentTypeId) - { - var propertyTypeDto = new PropertyTypeDto - { - Alias = propertyType.Alias, - ContentTypeId = contentTypeId, - DataTypeId = propertyType.DataTypeId, - Description = propertyType.Description, - Mandatory = propertyType.Mandatory, - MandatoryMessage = propertyType.MandatoryMessage, - Name = propertyType.Name, - SortOrder = propertyType.SortOrder, - ValidationRegExp = propertyType.ValidationRegExp, - ValidationRegExpMessage = propertyType.ValidationRegExpMessage, - UniqueId = propertyType.Key, - Variations = (byte)propertyType.Variations, - LabelOnTop = propertyType.LabelOnTop - }; - - if (groupId != default) - { - propertyTypeDto.PropertyTypeGroupId = groupId; - } - else - { - propertyTypeDto.PropertyTypeGroupId = null; - } - - if (propertyType.HasIdentity) - propertyTypeDto.Id = propertyType.Id; - - return propertyTypeDto; - } + return dto; } + + internal static PropertyTypeDto BuildPropertyTypeDto(int groupId, IPropertyType propertyType, int contentTypeId) + { + var propertyTypeDto = new PropertyTypeDto + { + Alias = propertyType.Alias, + ContentTypeId = contentTypeId, + DataTypeId = propertyType.DataTypeId, + Description = propertyType.Description, + Mandatory = propertyType.Mandatory, + MandatoryMessage = propertyType.MandatoryMessage, + Name = propertyType.Name, + SortOrder = propertyType.SortOrder, + ValidationRegExp = propertyType.ValidationRegExp, + ValidationRegExpMessage = propertyType.ValidationRegExpMessage, + UniqueId = propertyType.Key, + Variations = (byte)propertyType.Variations, + LabelOnTop = propertyType.LabelOnTop, + }; + + if (groupId != default) + { + propertyTypeDto.PropertyTypeGroupId = groupId; + } + else + { + propertyTypeDto.PropertyTypeGroupId = null; + } + + if (propertyType.HasIdentity) + { + propertyTypeDto.Id = propertyType.Id; + } + + return propertyTypeDto; + } + + #region Implementation of IEntityFactory,IEnumerable> + + public static IEnumerable BuildEntity( + IEnumerable groupDtos, + bool isPublishing, + int contentTypeId, + DateTime createDate, + DateTime updateDate, + Func propertyTypeCtor) + { + // groupDtos contains all the groups, those that are defined on the current + // content type, and those that are inherited from composition content types + var propertyGroups = new PropertyGroupCollection(); + foreach (PropertyTypeGroupDto groupDto in groupDtos) + { + var group = new PropertyGroup(isPublishing); + + try + { + group.DisableChangeTracking(); + + // if the group is defined on the current content type, + // assign its identifier, else it will be zero + if (groupDto.ContentTypeNodeId == contentTypeId) + { + group.Id = groupDto.Id; + } + + group.Key = groupDto.UniqueId; + group.Type = (PropertyGroupType)groupDto.Type; + group.Name = groupDto.Text; + group.Alias = groupDto.Alias; + group.SortOrder = groupDto.SortOrder; + + group.PropertyTypes = new PropertyTypeCollection(isPublishing); + + // Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded + IEnumerable typeDtos = groupDto.PropertyTypeDtos?.Where(x => x.Id > 0) ?? + Enumerable.Empty(); + foreach (PropertyTypeDto typeDto in typeDtos) + { + PropertyTypeGroupDto tempGroupDto = groupDto; + PropertyType propertyType = propertyTypeCtor( + typeDto.DataTypeDto.EditorAlias, + typeDto.DataTypeDto.DbType.EnumParse(true), + typeDto.Alias); + + try + { + propertyType.DisableChangeTracking(); + + propertyType.Alias = typeDto.Alias ?? string.Empty; + propertyType.DataTypeId = typeDto.DataTypeId; + propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; + propertyType.Description = typeDto.Description; + propertyType.Id = typeDto.Id; + propertyType.Key = typeDto.UniqueId; + propertyType.Name = typeDto.Name ?? string.Empty; + propertyType.Mandatory = typeDto.Mandatory; + propertyType.MandatoryMessage = typeDto.MandatoryMessage; + propertyType.SortOrder = typeDto.SortOrder; + propertyType.ValidationRegExp = typeDto.ValidationRegExp; + propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage; + propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id); + propertyType.CreateDate = createDate; + propertyType.UpdateDate = updateDate; + propertyType.Variations = (ContentVariation)typeDto.Variations; + + // reset dirty initial properties (U4-1946) + propertyType.ResetDirtyProperties(false); + group.PropertyTypes.Add(propertyType); + } + finally + { + propertyType.EnableChangeTracking(); + } + } + + // reset dirty initial properties (U4-1946) + group.ResetDirtyProperties(false); + propertyGroups.Add(group); + } + finally + { + group.EnableChangeTracking(); + } + } + + return propertyGroups; + } + + public static IEnumerable BuildDto(IEnumerable entity) => + entity.Select(BuildGroupDto).ToList(); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs index 0ed16d80da..25232b4f9f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs @@ -1,53 +1,52 @@ -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PublicAccessEntryFactory { - internal static class PublicAccessEntryFactory + public static PublicAccessEntry BuildEntity(AccessDto dto) { - public static PublicAccessEntry BuildEntity(AccessDto dto) - { - var entity = new PublicAccessEntry(dto.Id, dto.NodeId, dto.LoginNodeId, dto.NoAccessNodeId, + var entity = new PublicAccessEntry( + dto.Id, + dto.NodeId, + dto.LoginNodeId, + dto.NoAccessNodeId, dto.Rules.Select(x => new PublicAccessRule(x.Id, x.AccessId) - { - RuleValue = x.RuleValue, - RuleType = x.RuleType, - CreateDate = x.CreateDate, - UpdateDate = x.UpdateDate - })) { - CreateDate = dto.CreateDate, - UpdateDate = dto.UpdateDate - }; + RuleValue = x.RuleValue, + RuleType = x.RuleType, + CreateDate = x.CreateDate, + UpdateDate = x.UpdateDate, + })) + { CreateDate = dto.CreateDate, UpdateDate = dto.UpdateDate }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static AccessDto BuildDto(PublicAccessEntry entity) + public static AccessDto BuildDto(PublicAccessEntry entity) + { + var dto = new AccessDto { - var dto = new AccessDto + Id = entity.Key, + NoAccessNodeId = entity.NoAccessNodeId, + LoginNodeId = entity.LoginNodeId, + NodeId = entity.ProtectedNodeId, + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Rules = entity.Rules.Select(x => new AccessRuleDto { - Id = entity.Key, - NoAccessNodeId = entity.NoAccessNodeId, - LoginNodeId = entity.LoginNodeId, - NodeId = entity.ProtectedNodeId, - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Rules = entity.Rules.Select(x => new AccessRuleDto - { - AccessId = x.AccessEntryId, - Id = x.Key, - RuleValue = x.RuleValue, - RuleType = x.RuleType, - CreateDate = x.CreateDate, - UpdateDate = x.UpdateDate - }).ToList() - }; + AccessId = x.AccessEntryId, + Id = x.Key, + RuleValue = x.RuleValue, + RuleType = x.RuleType, + CreateDate = x.CreateDate, + UpdateDate = x.UpdateDate, + }).ToList(), + }; - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs index 63d3292160..872810ddd6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs @@ -1,66 +1,68 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class RelationFactory { - internal static class RelationFactory + public static IRelation BuildEntity(RelationDto dto, IRelationType relationType) { - public static IRelation BuildEntity(RelationDto dto, IRelationType relationType) + var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType); + + try { - var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType); + entity.DisableChangeTracking(); - try - { - entity.DisableChangeTracking(); + entity.Comment = dto.Comment; + entity.CreateDate = dto.Datetime; + entity.Id = dto.Id; + entity.UpdateDate = dto.Datetime; - entity.Comment = dto.Comment; - entity.CreateDate = dto.Datetime; - entity.Id = dto.Id; - entity.UpdateDate = dto.Datetime; + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } + finally + { + entity.EnableChangeTracking(); + } + } - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } - finally - { - entity.EnableChangeTracking(); - } + public static RelationDto BuildDto(IRelation entity) + { + var dto = new RelationDto + { + ChildId = entity.ChildId, + Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, + Datetime = entity.CreateDate, + ParentId = entity.ParentId, + RelationType = entity.RelationType.Id, + }; + + if (entity.HasIdentity) + { + dto.Id = entity.Id; } - public static RelationDto BuildDto(IRelation entity) + return dto; + } + + public static RelationDto BuildDto(ReadOnlyRelation entity) + { + var dto = new RelationDto { - var dto = new RelationDto - { - ChildId = entity.ChildId, - Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, - Datetime = entity.CreateDate, - ParentId = entity.ParentId, - RelationType = entity.RelationType.Id - }; + ChildId = entity.ChildId, + Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, + Datetime = entity.CreateDate, + ParentId = entity.ParentId, + RelationType = entity.RelationTypeId, + }; - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; - } - - public static RelationDto BuildDto(ReadOnlyRelation entity) - { - var dto = new RelationDto - { - ChildId = entity.ChildId, - Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, - Datetime = entity.CreateDate, - ParentId = entity.ParentId, - RelationType = entity.RelationTypeId - }; - - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; + if (entity.HasIdentity) + { + dto.Id = entity.Id; } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs index 57b1831c9d..3fbc91f51e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs @@ -1,60 +1,58 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class RelationTypeFactory { - internal static class RelationTypeFactory + #region Implementation of IEntityFactory + + public static IRelationType BuildEntity(RelationTypeDto dto) { - #region Implementation of IEntityFactory + var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency); - public static IRelationType BuildEntity(RelationTypeDto dto) + try { - var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency); + entity.DisableChangeTracking(); - try - { - entity.DisableChangeTracking(); + entity.Id = dto.Id; + entity.Key = dto.UniqueId; - entity.Id = dto.Id; - entity.Key = dto.UniqueId; - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } - finally - { - entity.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; } - - public static RelationTypeDto BuildDto(IRelationType entity) + finally { - var isDependency = false; - if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) - { - isDependency = relationTypeWithIsDependency.IsDependency; - } - var dto = new RelationTypeDto - { - Alias = entity.Alias, - ChildObjectType = entity.ChildObjectType, - Dual = entity.IsBidirectional, - IsDependency = isDependency, - Name = entity.Name ?? string.Empty, - ParentObjectType = entity.ParentObjectType, - UniqueId = entity.Key - }; - if (entity.HasIdentity) - { - dto.Id = entity.Id; - } - - return dto; + entity.EnableChangeTracking(); } - - - - #endregion } + + public static RelationTypeDto BuildDto(IRelationType entity) + { + var isDependency = false; + if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) + { + isDependency = relationTypeWithIsDependency.IsDependency; + } + + var dto = new RelationTypeDto + { + Alias = entity.Alias, + ChildObjectType = entity.ChildObjectType, + Dual = entity.IsBidirectional, + IsDependency = isDependency, + Name = entity.Name ?? string.Empty, + ParentObjectType = entity.ParentObjectType, + UniqueId = entity.Key, + }; + if (entity.HasIdentity) + { + dto.Id = entity.Id; + } + + return dto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs index f662faf561..cfbb27bd44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs @@ -1,34 +1,36 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ServerRegistrationFactory { - internal static class ServerRegistrationFactory + public static ServerRegistration BuildEntity(ServerRegistrationDto dto) { - public static ServerRegistration BuildEntity(ServerRegistrationDto dto) + var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive, dto.IsSchedulingPublisher); + + // reset dirty initial properties (U4-1946) + model.ResetDirtyProperties(false); + return model; + } + + public static ServerRegistrationDto BuildDto(IServerRegistration entity) + { + var dto = new ServerRegistrationDto { - var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive, dto.IsSchedulingPublisher); - // reset dirty initial properties (U4-1946) - model.ResetDirtyProperties(false); - return model; + ServerAddress = entity.ServerAddress, + DateRegistered = entity.CreateDate, + IsActive = entity.IsActive, + IsSchedulingPublisher = ((ServerRegistration)entity).IsSchedulingPublisher, + DateAccessed = entity.UpdateDate, + ServerIdentity = entity.ServerIdentity, + }; + + if (entity.HasIdentity) + { + dto.Id = entity.Id; } - public static ServerRegistrationDto BuildDto(IServerRegistration entity) - { - var dto = new ServerRegistrationDto - { - ServerAddress = entity.ServerAddress, - DateRegistered = entity.CreateDate, - IsActive = entity.IsActive, - IsSchedulingPublisher = ((ServerRegistration) entity).IsSchedulingPublisher, - DateAccessed = entity.UpdateDate, - ServerIdentity = entity.ServerIdentity - }; - - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs index e666e53658..1774bb854c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs @@ -1,28 +1,27 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories -{ - internal static class TagFactory - { - public static ITag BuildEntity(TagDto dto) - { - var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; - public static TagDto BuildDto(ITag entity) - { - return new TagDto - { - Id = entity.Id, - Group = entity.Group, - Text = entity.Text, - LanguageId = entity.LanguageId - //Key = entity.Group + "/" + entity.Text // de-normalize - }; - } +internal static class TagFactory +{ + public static ITag BuildEntity(TagDto dto) + { + var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; } + + public static TagDto BuildDto(ITag entity) => + new TagDto + { + Id = entity.Id, + Group = entity.Group, + Text = entity.Text, + LanguageId = entity.LanguageId, + + // Key = entity.Group + "/" + entity.Text // de-normalize + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs index eb84d46f68..3028d1a509 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs @@ -1,87 +1,81 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using File = Umbraco.Cms.Core.Models.File; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class TemplateFactory { - internal static class TemplateFactory + private static NodeDto BuildNodeDto(Template entity, Guid? nodeObjectTypeId) { - - #region Implementation of IEntityFactory - - public static Template BuildEntity(IShortStringHelper shortStringHelper, TemplateDto dto, IEnumerable childDefinitions, Func getFileContent) + var nodeDto = new NodeDto { - var template = new Template(shortStringHelper, dto.NodeDto.Text, dto.Alias, getFileContent); + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = 1, + NodeObjectType = nodeObjectTypeId, + ParentId = entity.MasterTemplateId?.Value ?? 0, + Path = entity.Path, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key + }; - try + return nodeDto; + } + + #region Implementation of IEntityFactory + + public static Template BuildEntity(IShortStringHelper shortStringHelper, TemplateDto dto, + IEnumerable childDefinitions, Func getFileContent) + { + var template = new Template(shortStringHelper, dto.NodeDto.Text, dto.Alias, getFileContent); + + try + { + template.DisableChangeTracking(); + + template.CreateDate = dto.NodeDto.CreateDate; + template.Id = dto.NodeId; + template.Key = dto.NodeDto.UniqueId; + template.Path = dto.NodeDto.Path; + + template.IsMasterTemplate = childDefinitions.Any(x => x.ParentId == dto.NodeId); + + if (dto.NodeDto.ParentId > 0) { - template.DisableChangeTracking(); - - template.CreateDate = dto.NodeDto.CreateDate; - template.Id = dto.NodeId; - template.Key = dto.NodeDto.UniqueId; - template.Path = dto.NodeDto.Path; - - template.IsMasterTemplate = childDefinitions.Any(x => x.ParentId == dto.NodeId); - - if (dto.NodeDto.ParentId > 0) - template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); - - // reset dirty initial properties (U4-1946) - template.ResetDirtyProperties(false); - return template; - } - finally - { - template.EnableChangeTracking(); + template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); } + + // reset dirty initial properties (U4-1946) + template.ResetDirtyProperties(false); + return template; } - - public static TemplateDto BuildDto(Template entity, Guid? nodeObjectTypeId,int primaryKey) + finally { - var dto = new TemplateDto - { - Alias = entity.Alias, - NodeDto = BuildNodeDto(entity, nodeObjectTypeId) - }; - - if (entity.MasterTemplateId != null && entity.MasterTemplateId.Value > 0) - { - dto.NodeDto.ParentId = entity.MasterTemplateId.Value; - } - - if (entity.HasIdentity) - { - dto.NodeId = entity.Id; - dto.PrimaryKey = primaryKey; - } - - return dto; - } - - #endregion - - private static NodeDto BuildNodeDto(Template entity,Guid? nodeObjectTypeId) - { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = 1, - NodeObjectType = nodeObjectTypeId, - ParentId = entity.MasterTemplateId?.Value ?? 0, - Path = entity.Path, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key - }; - - return nodeDto; + template.EnableChangeTracking(); } } + + public static TemplateDto BuildDto(Template entity, Guid? nodeObjectTypeId, int primaryKey) + { + var dto = new TemplateDto {Alias = entity.Alias, NodeDto = BuildNodeDto(entity, nodeObjectTypeId)}; + + if (entity.MasterTemplateId != null && entity.MasterTemplateId.Value > 0) + { + dto.NodeDto.ParentId = entity.MasterTemplateId.Value; + } + + if (entity.HasIdentity) + { + dto.NodeId = entity.Id; + dto.PrimaryKey = primaryKey; + } + + return dto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 8ee2bcfec0..11e2479691 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -1,119 +1,121 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class UserFactory { - internal static class UserFactory + public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) { - public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) + var guidId = dto.Id.ToGuid(); + + var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, + dto.PasswordConfig, + dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content) + .Select(x => x.StartNode).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media) + .Select(x => x.StartNode).ToArray()); + + try { - var guidId = dto.Id.ToGuid(); + user.DisableChangeTracking(); - var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, dto.PasswordConfig, - dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(), - dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.StartNode).ToArray(), - dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.StartNode).ToArray()); + user.Key = guidId; + user.IsLockedOut = dto.NoConsole; + user.IsApproved = dto.Disabled == false; + user.Language = dto.UserLanguage; + user.SecurityStamp = dto.SecurityStampToken; + user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; + user.LastLockoutDate = dto.LastLockoutDate; + user.LastLoginDate = dto.LastLoginDate; + user.LastPasswordChangeDate = dto.LastPasswordChangeDate; + user.CreateDate = dto.CreateDate; + user.UpdateDate = dto.UpdateDate; + user.Avatar = dto.Avatar; + user.EmailConfirmedDate = dto.EmailConfirmedDate; + user.InvitedDate = dto.InvitedDate; + user.TourData = dto.TourData; - try - { - user.DisableChangeTracking(); + // reset dirty initial properties (U4-1946) + user.ResetDirtyProperties(false); - user.Key = guidId; - user.IsLockedOut = dto.NoConsole; - user.IsApproved = dto.Disabled == false; - user.Language = dto.UserLanguage; - user.SecurityStamp = dto.SecurityStampToken; - user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; - user.LastLockoutDate = dto.LastLockoutDate; - user.LastLoginDate = dto.LastLoginDate; - user.LastPasswordChangeDate = dto.LastPasswordChangeDate; - user.CreateDate = dto.CreateDate; - user.UpdateDate = dto.UpdateDate; - user.Avatar = dto.Avatar; - user.EmailConfirmedDate = dto.EmailConfirmedDate; - user.InvitedDate = dto.InvitedDate; - user.TourData = dto.TourData; - - // reset dirty initial properties (U4-1946) - user.ResetDirtyProperties(false); - - return user; - } - finally - { - user.EnableChangeTracking(); - } + return user; } - - public static UserDto BuildDto(IUser entity) + finally { - var dto = new UserDto - { - Disabled = entity.IsApproved == false, - Email = entity.Email, - Login = entity.Username, - NoConsole = entity.IsLockedOut, - Password = entity.RawPasswordValue, - PasswordConfig = entity.PasswordConfiguration, - UserLanguage = entity.Language, - UserName = entity.Name!, - SecurityStampToken = entity.SecurityStamp, - FailedLoginAttempts = entity.FailedPasswordAttempts, - LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? (DateTime?)null : entity.LastLockoutDate, - LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, - LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Avatar = entity.Avatar, - EmailConfirmedDate = entity.EmailConfirmedDate, - InvitedDate = entity.InvitedDate, - TourData = entity.TourData - }; - - if (entity.StartContentIds is not null) - { - foreach (var startNodeId in entity.StartContentIds) - { - dto.UserStartNodeDtos.Add(new UserStartNodeDto - { - StartNode = startNodeId, - StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Content, - UserId = entity.Id - }); - } - } - - if (entity.StartMediaIds is not null) - { - foreach (var startNodeId in entity.StartMediaIds) - { - dto.UserStartNodeDtos.Add(new UserStartNodeDto - { - StartNode = startNodeId, - StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Media, - UserId = entity.Id - }); - } - } - - if (entity.HasIdentity) - { - dto.Id = entity.Id.SafeCast(); - } - - return dto; - } - - private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) - { - return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, - group.StartContentId, group.StartMediaId, group.Alias, - group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), - group.DefaultPermissions == null ? Enumerable.Empty() : group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); + user.EnableChangeTracking(); } } + + public static UserDto BuildDto(IUser entity) + { + var dto = new UserDto + { + Disabled = entity.IsApproved == false, + Email = entity.Email, + Login = entity.Username, + NoConsole = entity.IsLockedOut, + Password = entity.RawPasswordValue, + PasswordConfig = entity.PasswordConfiguration, + UserLanguage = entity.Language, + UserName = entity.Name!, + SecurityStampToken = entity.SecurityStamp, + FailedLoginAttempts = entity.FailedPasswordAttempts, + LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? null : entity.LastLockoutDate, + LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? null : entity.LastLoginDate, + LastPasswordChangeDate = + entity.LastPasswordChangeDate == DateTime.MinValue ? null : entity.LastPasswordChangeDate, + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Avatar = entity.Avatar, + EmailConfirmedDate = entity.EmailConfirmedDate, + InvitedDate = entity.InvitedDate, + TourData = entity.TourData, + }; + + if (entity.StartContentIds is not null) + { + foreach (var startNodeId in entity.StartContentIds) + { + dto.UserStartNodeDtos.Add(new UserStartNodeDto + { + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Content, + UserId = entity.Id, + }); + } + } + + if (entity.StartMediaIds is not null) + { + foreach (var startNodeId in entity.StartMediaIds) + { + dto.UserStartNodeDtos.Add(new UserStartNodeDto + { + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Media, + UserId = entity.Id, + }); + } + } + + if (entity.HasIdentity) + { + dto.Id = entity.Id.SafeCast(); + } + + return dto; + } + + private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) => + new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, + group.StartContentId, group.StartMediaId, group.Alias, group.UserGroup2LanguageDtos.Select(x => x.LanguageId), + group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), + group.DefaultPermissions == null + ? Enumerable.Empty() + : group.DefaultPermissions.ToCharArray().Select(x => x.ToString()), + group.HasAccessToAllLanguages); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs index 9672e0e3a9..3c4546da04 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs @@ -1,82 +1,86 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class UserGroupFactory { - internal static class UserGroupFactory + public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto) { - public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto) + var userGroup = new UserGroup( + shortStringHelper, + dto.UserCount, + dto.Alias, + dto.Name, + dto.DefaultPermissions.IsNullOrWhiteSpace() ? Enumerable.Empty() : dto.DefaultPermissions!.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), + dto.Icon); + + try { - var userGroup = new UserGroup(shortStringHelper, dto.UserCount, dto.Alias, dto.Name, - dto.DefaultPermissions.IsNullOrWhiteSpace() - ? Enumerable.Empty() - : dto.DefaultPermissions!.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), - dto.Icon); - - try + userGroup.DisableChangeTracking(); + userGroup.Id = dto.Id; + userGroup.CreateDate = dto.CreateDate; + userGroup.UpdateDate = dto.UpdateDate; + userGroup.StartContentId = dto.StartContentId; + userGroup.StartMediaId = dto.StartMediaId; + userGroup.HasAccessToAllLanguages = dto.HasAccessToAllLanguages; + if (dto.UserGroup2AppDtos != null) { - userGroup.DisableChangeTracking(); - userGroup.Id = dto.Id; - userGroup.CreateDate = dto.CreateDate; - userGroup.UpdateDate = dto.UpdateDate; - userGroup.StartContentId = dto.StartContentId; - userGroup.StartMediaId = dto.StartMediaId; - if (dto.UserGroup2AppDtos != null) + foreach (UserGroup2AppDto app in dto.UserGroup2AppDtos) { - foreach (var app in dto.UserGroup2AppDtos) - { - userGroup.AddAllowedSection(app.AppAlias); - } + userGroup.AddAllowedSection(app.AppAlias); } + } - userGroup.ResetDirtyProperties(false); - return userGroup; - } - finally + foreach (UserGroup2LanguageDto language in dto.UserGroup2LanguageDtos) { - userGroup.EnableChangeTracking(); + userGroup.AddAllowedLanguage(language.LanguageId); } + + userGroup.ResetDirtyProperties(false); + return userGroup; } - - public static UserGroupDto BuildDto(IUserGroup entity) + finally { - var dto = new UserGroupDto - { - Alias = entity.Alias, - DefaultPermissions = entity.Permissions == null ? "" : string.Join("", entity.Permissions), - Name = entity.Name, - UserGroup2AppDtos = new List(), - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Icon = entity.Icon, - StartMediaId = entity.StartMediaId, - StartContentId = entity.StartContentId - }; + userGroup.EnableChangeTracking(); + } + } - foreach (var app in entity.AllowedSections) - { - var appDto = new UserGroup2AppDto - { - AppAlias = app - }; - if (entity.HasIdentity) - { - appDto.UserGroupId = entity.Id; - } - - dto.UserGroup2AppDtos.Add(appDto); - } + public static UserGroupDto BuildDto(IUserGroup entity) + { + var dto = new UserGroupDto + { + Alias = entity.Alias, + DefaultPermissions = entity.Permissions == null ? string.Empty : string.Join(string.Empty, entity.Permissions), + Name = entity.Name, + UserGroup2AppDtos = new List(), + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Icon = entity.Icon, + StartMediaId = entity.StartMediaId, + StartContentId = entity.StartContentId, + HasAccessToAllLanguages = entity.HasAccessToAllLanguages, + }; + foreach (var app in entity.AllowedSections) + { + var appDto = new UserGroup2AppDto { AppAlias = app }; if (entity.HasIdentity) - dto.Id = short.Parse(entity.Id.ToString()); + { + appDto.UserGroupId = entity.Id; + } - return dto; + dto.UserGroup2AppDtos.Add(appDto); } + if (entity.HasIdentity) + { + dto.Id = short.Parse(entity.Id.ToString()); + } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs index 59d4f1c0b7..1b0b3e8f82 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs @@ -1,17 +1,15 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +/// +/// Defines an interface which must be implemented by custom components responsible for detecting specific transient +/// conditions. +/// +public interface ITransientErrorDetectionStrategy { /// - /// Defines an interface which must be implemented by custom components responsible for detecting specific transient conditions. + /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// - public interface ITransientErrorDetectionStrategy - { - /// - /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. - /// - /// The exception object to be verified. - /// True if the specified exception is considered as transient, otherwise false. - bool IsTransient(Exception ex); - } + /// The exception object to be verified. + /// True if the specified exception is considered as transient, otherwise false. + bool IsTransient(Exception ex); } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs index 57b14bf0a9..c6a3976637 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs @@ -1,239 +1,195 @@ -using System; using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; -using Transaction = System.Transactions.Transaction; +using System.Transactions; +using IsolationLevel = System.Data.IsolationLevel; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +public class RetryDbConnection : DbConnection { - public class RetryDbConnection : DbConnection + private readonly RetryPolicy? _cmdRetryPolicy; + private readonly RetryPolicy _conRetryPolicy; + + public RetryDbConnection(DbConnection connection, RetryPolicy? conRetryPolicy, RetryPolicy? cmdRetryPolicy) { - private DbConnection _inner; - private readonly RetryPolicy _conRetryPolicy; - private readonly RetryPolicy? _cmdRetryPolicy; + Inner = connection; + Inner.StateChange += StateChangeHandler; - public RetryDbConnection(DbConnection connection, RetryPolicy? conRetryPolicy, RetryPolicy? cmdRetryPolicy) + _conRetryPolicy = conRetryPolicy ?? RetryPolicy.NoRetry; + _cmdRetryPolicy = cmdRetryPolicy; + } + + public DbConnection Inner { get; } + + [AllowNull] + public override string ConnectionString + { + get => Inner.ConnectionString; + set => Inner.ConnectionString = value; + } + + public override int ConnectionTimeout => Inner.ConnectionTimeout; + + protected override bool CanRaiseEvents => true; + + public override string DataSource => Inner.DataSource; + + public override string Database => Inner.Database; + + public override string ServerVersion => Inner.ServerVersion; + + public override ConnectionState State => Inner.State; + + public override void ChangeDatabase(string databaseName) => Inner.ChangeDatabase(databaseName); + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => + Inner.BeginTransaction(isolationLevel); + + public override void Close() => Inner.Close(); + + public override void EnlistTransaction(Transaction? transaction) => Inner.EnlistTransaction(transaction); + + protected override DbCommand CreateDbCommand() => + new FaultHandlingDbCommand(this, Inner.CreateCommand(), _cmdRetryPolicy); + + protected override void Dispose(bool disposing) + { + if (disposing && Inner != null) { - _inner = connection; - _inner.StateChange += StateChangeHandler; - - _conRetryPolicy = conRetryPolicy ?? RetryPolicy.NoRetry; - _cmdRetryPolicy = cmdRetryPolicy; + Inner.StateChange -= StateChangeHandler; + Inner.Dispose(); } - public DbConnection Inner { get { return _inner; } } + base.Dispose(disposing); + } - [AllowNull] - public override string ConnectionString { get { return _inner.ConnectionString; } set { _inner.ConnectionString = value; } } + public override DataTable GetSchema() => Inner.GetSchema(); - protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + public override DataTable GetSchema(string collectionName) => Inner.GetSchema(collectionName); + + public override DataTable GetSchema(string collectionName, string?[] restrictionValues) => + Inner.GetSchema(collectionName, restrictionValues); + + public override void Open() => _conRetryPolicy.ExecuteAction(Inner.Open); + + public void Ensure() + { + // verify whether or not the connection is valid and is open. This code may be retried therefore + // it is important to ensure that a connection is re-established should it have previously failed + if (State != ConnectionState.Open) { - return _inner.BeginTransaction(isolationLevel); - } - - protected override bool CanRaiseEvents - { - get { return true; } - } - - public override void ChangeDatabase(string databaseName) - { - _inner.ChangeDatabase(databaseName); - } - - public override void Close() - { - _inner.Close(); - } - - public override int ConnectionTimeout - { - get { return _inner.ConnectionTimeout; } - } - - protected override DbCommand CreateDbCommand() - { - return new FaultHandlingDbCommand(this, _inner.CreateCommand(), _cmdRetryPolicy); - } - - public override string DataSource - { - get { return _inner.DataSource; } - } - - public override string Database - { - get { return _inner.Database; } - } - - protected override void Dispose(bool disposing) - { - if (disposing && _inner != null) - { - _inner.StateChange -= StateChangeHandler; - _inner.Dispose(); - } - base.Dispose(disposing); - } - - public override void EnlistTransaction(Transaction? transaction) - { - _inner.EnlistTransaction(transaction); - } - - public override DataTable GetSchema() - { - return _inner.GetSchema(); - } - - public override DataTable GetSchema(string collectionName) - { - return _inner.GetSchema(collectionName); - } - - public override DataTable GetSchema(string collectionName, string?[] restrictionValues) - { - return _inner.GetSchema(collectionName, restrictionValues); - } - - public override void Open() - { - _conRetryPolicy.ExecuteAction(_inner.Open); - } - - public override string ServerVersion - { - get { return _inner.ServerVersion; } - } - - public override ConnectionState State - { - get { return _inner.State; } - } - - private void StateChangeHandler(object sender, StateChangeEventArgs stateChangeEventArguments) - { - OnStateChange(stateChangeEventArguments); - } - - public void Ensure() - { - // verify whether or not the connection is valid and is open. This code may be retried therefore - // it is important to ensure that a connection is re-established should it have previously failed - if (State != ConnectionState.Open) - Open(); + Open(); } } - class FaultHandlingDbCommand : DbCommand - { - private RetryDbConnection _connection; - private DbCommand _inner; - private readonly RetryPolicy _cmdRetryPolicy; - - public FaultHandlingDbCommand(RetryDbConnection connection, DbCommand command, RetryPolicy? cmdRetryPolicy) - { - _connection = connection; - _inner = command; - _cmdRetryPolicy = cmdRetryPolicy ?? RetryPolicy.NoRetry; - } - - public DbCommand Inner => _inner; - - protected override void Dispose(bool disposing) - { - if (disposing) - _inner.Dispose(); - _inner = null!; - base.Dispose(disposing); - } - - public override void Cancel() - { - _inner.Cancel(); - } - - [AllowNull] - public override string CommandText - { - get => _inner.CommandText; - set => _inner.CommandText = value; - } - - public override int CommandTimeout - { - get => _inner.CommandTimeout; - set => _inner.CommandTimeout = value; - } - - public override CommandType CommandType - { - get => _inner.CommandType; - set => _inner.CommandType = value; - } - - [AllowNull] - protected override DbConnection DbConnection - { - get => _connection; - set - { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (!(value is RetryDbConnection connection)) throw new ArgumentException("Value is not a FaultHandlingDbConnection instance."); - if (_connection != null && _connection != connection) throw new Exception("Value is another FaultHandlingDbConnection instance."); - _connection = connection; - _inner.Connection = connection.Inner; - } - } - - protected override DbParameter CreateDbParameter() - { - return _inner.CreateParameter(); - } - - protected override DbParameterCollection DbParameterCollection => _inner.Parameters; - - protected override DbTransaction? DbTransaction - { - get => _inner.Transaction; - set => _inner.Transaction = value; - } - - public override bool DesignTimeVisible { get; set; } - - protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) - { - return Execute(() => _inner.ExecuteReader(behavior)); - } - - public override int ExecuteNonQuery() - { - return Execute(() => _inner.ExecuteNonQuery()); - } - - public override object? ExecuteScalar() - { - return Execute(() => _inner.ExecuteScalar()); - } - - private T Execute(Func f) - { - return _cmdRetryPolicy.ExecuteAction(() => - { - _connection.Ensure(); - return f(); - })!; - } - - public override void Prepare() - { - _inner.Prepare(); - } - - public override UpdateRowSource UpdatedRowSource - { - get => _inner.UpdatedRowSource; - set => _inner.UpdatedRowSource = value; - } - } + private void StateChangeHandler(object sender, StateChangeEventArgs stateChangeEventArguments) => + OnStateChange(stateChangeEventArguments); +} + +internal class FaultHandlingDbCommand : DbCommand +{ + private readonly RetryPolicy _cmdRetryPolicy; + private RetryDbConnection _connection; + + public FaultHandlingDbCommand(RetryDbConnection connection, DbCommand command, RetryPolicy? cmdRetryPolicy) + { + _connection = connection; + Inner = command; + _cmdRetryPolicy = cmdRetryPolicy ?? RetryPolicy.NoRetry; + } + + public DbCommand Inner { get; private set; } + + [AllowNull] + public override string CommandText + { + get => Inner.CommandText; + set => Inner.CommandText = value; + } + + public override int CommandTimeout + { + get => Inner.CommandTimeout; + set => Inner.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => Inner.CommandType; + set => Inner.CommandType = value; + } + + public override bool DesignTimeVisible { get; set; } + + [AllowNull] + protected override DbConnection DbConnection + { + get => _connection; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (!(value is RetryDbConnection connection)) + { + throw new ArgumentException("Value is not a FaultHandlingDbConnection instance."); + } + + if (_connection != null && _connection != connection) + { + throw new Exception("Value is another FaultHandlingDbConnection instance."); + } + + _connection = connection; + Inner.Connection = connection.Inner; + } + } + + protected override DbParameterCollection DbParameterCollection => Inner.Parameters; + + protected override DbTransaction? DbTransaction + { + get => Inner.Transaction; + set => Inner.Transaction = value; + } + + public override UpdateRowSource UpdatedRowSource + { + get => Inner.UpdatedRowSource; + set => Inner.UpdatedRowSource = value; + } + + public override void Cancel() => Inner.Cancel(); + + public override int ExecuteNonQuery() => Execute(() => Inner.ExecuteNonQuery()); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Inner.Dispose(); + } + + Inner = null!; + base.Dispose(disposing); + } + + protected override DbParameter CreateDbParameter() => Inner.CreateParameter(); + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => + Execute(() => Inner.ExecuteReader(behavior)); + + public override object? ExecuteScalar() => Execute(() => Inner.ExecuteScalar()); + + public override void Prepare() => Inner.Prepare(); + + private T Execute(Func f) => + _cmdRetryPolicy.ExecuteAction(() => + { + _connection.Ensure(); + return f(); + })!; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs index 78c8ab9c25..3d8021a660 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs @@ -1,54 +1,64 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// The special type of exception that provides managed exit from a retry loop. The user code can use this exception to +/// notify the retry policy that no further retry attempts are required. +/// +/// +[Serializable] +public sealed class RetryLimitExceededException : Exception { /// - /// The special type of exception that provides managed exit from a retry loop. The user code can use this exception to notify the retry policy that no further retry attempts are required. + /// Initializes a new instance of the class with a default error message. /// - /// - [Serializable] - public sealed class RetryLimitExceededException : Exception + public RetryLimitExceededException() { - /// - /// Initializes a new instance of the class with a default error message. - /// - public RetryLimitExceededException() - : base() - { } + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public RetryLimitExceededException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public RetryLimitExceededException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a reference to the inner exception that is the cause of this exception. - /// - /// The exception that is the cause of the current exception. - public RetryLimitExceededException(Exception innerException) - : base(null, innerException) - { } + /// + /// Initializes a new instance of the class with a reference to the inner + /// exception that is the cause of this exception. + /// + /// The exception that is the cause of the current exception. + public RetryLimitExceededException(Exception innerException) + : base(null, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - /// The exception that is the cause of the current exception. - public RetryLimitExceededException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public RetryLimitExceededException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - private RetryLimitExceededException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + private RetryLimitExceededException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs index 82e4f20c50..716906f5b8 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs @@ -1,239 +1,262 @@ -using System; -using System.Threading; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// Provides the base implementation of the retry mechanism for unreliable actions and transient conditions. +/// +public class RetryPolicy { /// - /// Provides the base implementation of the retry mechanism for unreliable actions and transient conditions. + /// Returns a default policy that does no retries, it just invokes action exactly once. /// - public class RetryPolicy + public static readonly RetryPolicy NoRetry = new(new TransientErrorIgnoreStrategy(), 0); + + /// + /// Returns a default policy that implements a fixed retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy DefaultFixed = new(new TransientErrorCatchAllStrategy(), new FixedInterval()); + + /// + /// Returns a default policy that implements a progressive retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy + DefaultProgressive = new(new TransientErrorCatchAllStrategy(), new Incremental()); + + /// + /// Returns a default policy that implements a random exponential retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy DefaultExponential = + new(new TransientErrorCatchAllStrategy(), new ExponentialBackoff()); + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters + /// defining the progressive delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The retry strategy to use for this retry policy. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, RetryStrategy retryStrategy) + { + // Guard.ArgumentNotNull(errorDetectionStrategy, "errorDetectionStrategy"); + // Guard.ArgumentNotNull(retryStrategy, "retryPolicy"); + ErrorDetectionStrategy = errorDetectionStrategy; + + if (errorDetectionStrategy == null) + { + throw new InvalidOperationException( + "The error detection strategy type must implement the ITransientErrorDetectionStrategy interface."); + } + + RetryStrategy = retryStrategy; + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and default fixed + /// time interval between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount) + : this(errorDetectionStrategy, new FixedInterval(retryCount)) + { + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and fixed time + /// interval between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The interval between retries. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan retryInterval) + : this(errorDetectionStrategy, new FixedInterval(retryCount, retryInterval)) + { + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and back-off + /// parameters for calculating the exponential delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The minimum back-off time. + /// The maximum back-off time. + /// + /// The time value that will be used for calculating a random delta in the exponential delay + /// between retries. + /// + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(errorDetectionStrategy, new ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff)) + { + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters + /// defining the progressive delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(errorDetectionStrategy, new Incremental(retryCount, initialInterval, increment)) + { + } + + /// + /// An instance of a callback delegate that will be invoked whenever a retry condition is encountered. + /// + public event EventHandler? Retrying; + + /// + /// Gets the retry strategy. + /// + public RetryStrategy RetryStrategy { get; } + + /// + /// Gets the instance of the error detection strategy. + /// + public ITransientErrorDetectionStrategy ErrorDetectionStrategy { get; } + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// A delegate representing the executable action which doesn't return any results. + public virtual void ExecuteAction(Action action) => + + // Guard.ArgumentNotNull(action, "action"); + ExecuteAction(() => + { + action(); + return default(object); + }); + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// The type of result expected from the executable action. + /// A delegate representing the executable action which returns the result of type R. + /// The result from the action. + public virtual TResult? ExecuteAction(Func func) + { + // Guard.ArgumentNotNull(func, "func"); + var retryCount = 0; + TimeSpan delay = TimeSpan.Zero; + Exception? lastError; + + ShouldRetry shouldRetry = RetryStrategy.GetShouldRetry(); + + for (; ;) + { + lastError = null; + + try + { + return func(); + } + catch (RetryLimitExceededException limitExceededEx) + { + // The user code can throw a RetryLimitExceededException to force the exit from the retry loop. + // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception + // which we will have to throw up the stack so that callers can handle it. + if (limitExceededEx.InnerException != null) + { + throw limitExceededEx.InnerException; + } + + return default; + } + catch (Exception ex) + { + lastError = ex; + + if (!(ErrorDetectionStrategy.IsTransient(lastError) && shouldRetry(retryCount++, lastError, out delay))) + { + throw; + } + } + + // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 that will block a thread indefinitely. + // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault that will be thrown by Thread.Sleep. + if (delay.TotalMilliseconds < 0) + { + delay = TimeSpan.Zero; + } + + OnRetrying(retryCount, lastError, delay); + + if (retryCount > 1 || !RetryStrategy.FastFirstRetry) + { + Thread.Sleep(delay); + } + } + } + + /// + /// Notifies the subscribers whenever a retry condition is encountered. + /// + /// The current retry attempt count. + /// The exception which caused the retry conditions to occur. + /// + /// The delay indicating how long the current thread will be suspended for before the next iteration + /// will be invoked. + /// + protected virtual void OnRetrying(int retryCount, Exception lastError, TimeSpan delay) + { + Retrying?.Invoke(this, new RetryingEventArgs(retryCount, delay, lastError)); + } + + #region Private classes + + /// + /// Implements a strategy that ignores any transient errors. + /// + private sealed class TransientErrorIgnoreStrategy : ITransientErrorDetectionStrategy { /// - /// Returns a default policy that does no retries, it just invokes action exactly once. + /// Always return false. /// - public static readonly RetryPolicy NoRetry = new RetryPolicy(new TransientErrorIgnoreStrategy(), 0); - - /// - /// Returns a default policy that implements a fixed retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultFixed = new RetryPolicy(new TransientErrorCatchAllStrategy(), new FixedInterval()); - - /// - /// Returns a default policy that implements a progressive retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultProgressive = new RetryPolicy(new TransientErrorCatchAllStrategy(), new Incremental()); - - /// - /// Returns a default policy that implements a random exponential retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultExponential = new RetryPolicy(new TransientErrorCatchAllStrategy(), new ExponentialBackoff()); - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The retry strategy to use for this retry policy. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, RetryStrategy retryStrategy) - { - //Guard.ArgumentNotNull(errorDetectionStrategy, "errorDetectionStrategy"); - //Guard.ArgumentNotNull(retryStrategy, "retryPolicy"); - - this.ErrorDetectionStrategy = errorDetectionStrategy; - - if (errorDetectionStrategy == null) - { - throw new InvalidOperationException("The error detection strategy type must implement the ITransientErrorDetectionStrategy interface."); - } - - this.RetryStrategy = retryStrategy; - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and default fixed time interval between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount) - : this(errorDetectionStrategy, new FixedInterval(retryCount)) - { - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and fixed time interval between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The interval between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan retryInterval) - : this(errorDetectionStrategy, new FixedInterval(retryCount, retryInterval)) - { - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and back-off parameters for calculating the exponential delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The minimum back-off time. - /// The maximum back-off time. - /// The time value that will be used for calculating a random delta in the exponential delay between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(errorDetectionStrategy, new ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff)) - { - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(errorDetectionStrategy, new Incremental(retryCount, initialInterval, increment)) - { - } - - /// - /// An instance of a callback delegate that will be invoked whenever a retry condition is encountered. - /// - public event EventHandler? Retrying; - - /// - /// Gets the retry strategy. - /// - public RetryStrategy RetryStrategy { get; private set; } - - /// - /// Gets the instance of the error detection strategy. - /// - public ITransientErrorDetectionStrategy ErrorDetectionStrategy { get; private set; } - - /// - /// Repetitively executes the specified action while it satisfies the current retry policy. - /// - /// A delegate representing the executable action which doesn't return any results. - public virtual void ExecuteAction(Action action) - { - //Guard.ArgumentNotNull(action, "action"); - - this.ExecuteAction(() => { action(); return default(object); }); - } - - /// - /// Repetitively executes the specified action while it satisfies the current retry policy. - /// - /// The type of result expected from the executable action. - /// A delegate representing the executable action which returns the result of type R. - /// The result from the action. - public virtual TResult? ExecuteAction(Func func) - { - //Guard.ArgumentNotNull(func, "func"); - - int retryCount = 0; - TimeSpan delay = TimeSpan.Zero; - Exception? lastError; - - var shouldRetry = this.RetryStrategy.GetShouldRetry(); - - for (; ; ) - { - lastError = null; - - try - { - return func(); - } - catch (RetryLimitExceededException limitExceededEx) - { - // The user code can throw a RetryLimitExceededException to force the exit from the retry loop. - // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception - // which we will have to throw up the stack so that callers can handle it. - if (limitExceededEx.InnerException != null) - { - throw limitExceededEx.InnerException; - } - else - { - return default(TResult); - } - } - catch (Exception ex) - { - lastError = ex; - - if (!(this.ErrorDetectionStrategy.IsTransient(lastError) && shouldRetry(retryCount++, lastError, out delay))) - { - throw; - } - } - - // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 that will block a thread indefinitely. - // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault that will be thrown by Thread.Sleep. - if (delay.TotalMilliseconds < 0) - { - delay = TimeSpan.Zero; - } - - this.OnRetrying(retryCount, lastError, delay); - - if (retryCount > 1 || !this.RetryStrategy.FastFirstRetry) - { - Thread.Sleep(delay); - } - } - } - - /// - /// Notifies the subscribers whenever a retry condition is encountered. - /// - /// The current retry attempt count. - /// The exception which caused the retry conditions to occur. - /// The delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - protected virtual void OnRetrying(int retryCount, Exception lastError, TimeSpan delay) - { - if (this.Retrying != null) - { - this.Retrying(this, new RetryingEventArgs(retryCount, delay, lastError)); - } - } - - #region Private classes - /// - /// Implements a strategy that ignores any transient errors. - /// - private sealed class TransientErrorIgnoreStrategy : ITransientErrorDetectionStrategy - { - /// - /// Always return false. - /// - /// The exception. - /// Returns false. - public bool IsTransient(Exception ex) - { - return false; - } - } - - /// - /// Implements a strategy that treats all exceptions as transient errors. - /// - private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy - { - /// - /// Always return true. - /// - /// The exception. - /// Returns true. - public bool IsTransient(Exception ex) - { - return true; - } - } - #endregion + /// The exception. + /// Returns false. + public bool IsTransient(Exception ex) => false; } + + /// + /// Implements a strategy that treats all exceptions as transient errors. + /// + private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy + { + /// + /// Always return true. + /// + /// The exception. + /// Returns true. + public bool IsTransient(Exception ex) => true; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs index 41f2337644..785c6cebe5 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs @@ -1,59 +1,56 @@ -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +// TODO: These should move to Persistence.SqlServer + +/// +/// Provides a factory class for instantiating application-specific retry policies. +/// +public static class RetryPolicyFactory { - // TODO: These should move to Persistence.SqlServer + public static RetryPolicy GetDefaultSqlConnectionRetryPolicyByConnectionString(string? connectionString) => - /// - /// Provides a factory class for instantiating application-specific retry policies. - /// - public static class RetryPolicyFactory + // Is this really the best way to determine if the database is an Azure database? + connectionString?.Contains("database.windows.net") ?? false + ? GetDefaultSqlAzureConnectionRetryPolicy() + : GetDefaultSqlConnectionRetryPolicy(); + + public static RetryPolicy GetDefaultSqlConnectionRetryPolicy() { - public static RetryPolicy GetDefaultSqlConnectionRetryPolicyByConnectionString(string? connectionString) - { - //Is this really the best way to determine if the database is an Azure database? - return connectionString?.Contains("database.windows.net") ?? false - ? GetDefaultSqlAzureConnectionRetryPolicy() - : GetDefaultSqlConnectionRetryPolicy(); - } + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); - public static RetryPolicy GetDefaultSqlConnectionRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultExponential; - var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); + return retryPolicy; + } - return retryPolicy; - } + public static RetryPolicy GetDefaultSqlAzureConnectionRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); + return retryPolicy; + } - public static RetryPolicy GetDefaultSqlAzureConnectionRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultExponential; - var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); - return retryPolicy; - } + public static RetryPolicy GetDefaultSqlCommandRetryPolicyByConnectionString(string? connectionString) => - public static RetryPolicy GetDefaultSqlCommandRetryPolicyByConnectionString(string? connectionString) - { - //Is this really the best way to determine if the database is an Azure database? - return connectionString?.Contains("database.windows.net") ?? false - ? GetDefaultSqlAzureCommandRetryPolicy() - : GetDefaultSqlCommandRetryPolicy(); - } + // Is this really the best way to determine if the database is an Azure database? + connectionString?.Contains("database.windows.net") ?? false + ? GetDefaultSqlAzureCommandRetryPolicy() + : GetDefaultSqlCommandRetryPolicy(); - public static RetryPolicy GetDefaultSqlCommandRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultFixed; - var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); + public static RetryPolicy GetDefaultSqlCommandRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultFixed; + var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); - return retryPolicy; - } + return retryPolicy; + } - public static RetryPolicy GetDefaultSqlAzureCommandRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultFixed; - var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); + public static RetryPolicy GetDefaultSqlAzureCommandRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultFixed; + var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); - return retryPolicy; - } + return retryPolicy; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs index 3f120261d7..81ad045641 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs @@ -1,5 +1,4 @@ -using System; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling { @@ -17,7 +16,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling /// public abstract class RetryStrategy { - #region Public members /// /// The default number of retry attempts. /// @@ -54,8 +52,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling /// public static readonly bool DefaultFirstFastRetry = true; - #endregion - /// /// Returns a default policy that does no retries, it just invokes action exactly once. /// diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs index 456dc87391..4dd473d97d 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs @@ -1,40 +1,40 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +/// +/// Contains information required for the event. +/// +public class RetryingEventArgs : EventArgs { /// - /// Contains information required for the event. + /// Initializes a new instance of the class. /// - public class RetryingEventArgs : EventArgs + /// The current retry attempt count. + /// + /// The delay indicating how long the current thread will be suspended for before the next iteration + /// will be invoked. + /// + /// The exception which caused the retry conditions to occur. + public RetryingEventArgs(int currentRetryCount, TimeSpan delay, Exception lastException) { - /// - /// Initializes a new instance of the class. - /// - /// The current retry attempt count. - /// The delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - /// The exception which caused the retry conditions to occur. - public RetryingEventArgs(int currentRetryCount, TimeSpan delay, Exception lastException) - { - //Guard.ArgumentNotNull(lastException, "lastException"); - - this.CurrentRetryCount = currentRetryCount; - this.Delay = delay; - this.LastException = lastException; - } - - /// - /// Gets the current retry count. - /// - public int CurrentRetryCount { get; private set; } - - /// - /// Gets the delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - /// - public TimeSpan Delay { get; private set; } - - /// - /// Gets the exception which caused the retry conditions to occur. - /// - public Exception LastException { get; private set; } + // Guard.ArgumentNotNull(lastException, "lastException"); + CurrentRetryCount = currentRetryCount; + Delay = delay; + LastException = lastException; } + + /// + /// Gets the current retry count. + /// + public int CurrentRetryCount { get; } + + /// + /// Gets the delay indicating how long the current thread will be suspended for before the next iteration will be + /// invoked. + /// + public TimeSpan Delay { get; } + + /// + /// Gets the exception which caused the retry conditions to occur. + /// + public Exception LastException { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs index 91dcaf9feb..ba19fee617 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs @@ -1,100 +1,104 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with back-off parameters for calculating the exponential delay between retries. +/// +public class ExponentialBackoff : RetryStrategy { + private readonly TimeSpan _deltaBackoff; + private readonly TimeSpan _maxBackoff; + private readonly TimeSpan _minBackoff; + private readonly int _retryCount; + /// - /// A retry strategy with back-off parameters for calculating the exponential delay between retries. + /// Initializes a new instance of the class. /// - public class ExponentialBackoff : RetryStrategy + public ExponentialBackoff() + : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) { - private readonly int retryCount; - private readonly TimeSpan minBackoff; - private readonly TimeSpan maxBackoff; - private readonly TimeSpan deltaBackoff; - - /// - /// Initializes a new instance of the class. - /// - public ExponentialBackoff() - : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - /// - /// Indicates whether or not the very first retry attempt will be made immediately - /// whereas the subsequent retries will remain subject to retry interval. - /// - public ExponentialBackoff(string? name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff"); - //Guard.ArgumentNotNegativeValue(maxBackoff.Ticks, "maxBackoff"); - //Guard.ArgumentNotNegativeValue(deltaBackoff.Ticks, "deltaBackoff"); - //Guard.ArgumentNotGreaterThan(minBackoff.TotalMilliseconds, maxBackoff.TotalMilliseconds, "minBackoff"); - - this.retryCount = retryCount; - this.minBackoff = minBackoff; - this.maxBackoff = maxBackoff; - this.deltaBackoff = deltaBackoff; - } - - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() - { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) - { - if (currentRetryCount < this.retryCount) - { - var random = new Random(); - - var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next((int)(this.deltaBackoff.TotalMilliseconds * 0.8), (int)(this.deltaBackoff.TotalMilliseconds * 1.2))); - var interval = (int)Math.Min(checked(this.minBackoff.TotalMilliseconds + delta), this.maxBackoff.TotalMilliseconds); - - retryInterval = TimeSpan.FromMilliseconds(interval); - - return true; - } - - retryInterval = TimeSpan.Zero; - return false; - }; - } } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + /// + /// Indicates whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public ExponentialBackoff(string? name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry) + : base(name, firstFastRetry) + { + // Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + // Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff"); + // Guard.ArgumentNotNegativeValue(maxBackoff.Ticks, "maxBackoff"); + // Guard.ArgumentNotNegativeValue(deltaBackoff.Ticks, "deltaBackoff"); + // Guard.ArgumentNotGreaterThan(minBackoff.TotalMilliseconds, maxBackoff.TotalMilliseconds, "minBackoff"); + this._retryCount = retryCount; + this._minBackoff = minBackoff; + this._maxBackoff = maxBackoff; + this._deltaBackoff = deltaBackoff; + } + + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() => + delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) + { + if (currentRetryCount < _retryCount) + { + var random = new Random(); + + var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next( + (int)(_deltaBackoff.TotalMilliseconds * 0.8), (int)(_deltaBackoff.TotalMilliseconds * 1.2))); + var interval = (int)Math.Min(_minBackoff.TotalMilliseconds + delta, _maxBackoff.TotalMilliseconds); + + retryInterval = TimeSpan.FromMilliseconds(interval); + + return true; + } + + retryInterval = TimeSpan.Zero; + return false; + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs index 546b10b55a..a798a5866a 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs @@ -1,96 +1,95 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with a specified number of retry attempts and a default fixed time interval between retries. +/// +public class FixedInterval : RetryStrategy { + private readonly int _retryCount; + private readonly TimeSpan _retryInterval; + /// - /// A retry strategy with a specified number of retry attempts and a default fixed time interval between retries. + /// Initializes a new instance of the class. /// - public class FixedInterval : RetryStrategy + public FixedInterval() + : this(DefaultClientRetryCount) { - private readonly int retryCount; - private readonly TimeSpan retryInterval; + } - /// - /// Initializes a new instance of the class. - /// - public FixedInterval() - : this(DefaultClientRetryCount) + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + public FixedInterval(int retryCount) + : this(retryCount, DefaultRetryInterval) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + /// The time interval between retries. + public FixedInterval(int retryCount, TimeSpan retryInterval) + : this(null, retryCount, retryInterval, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The time interval between retries. + public FixedInterval(string name, int retryCount, TimeSpan retryInterval) + : this(name, retryCount, retryInterval, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The time interval between retries. + /// + /// a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public FixedInterval(string? name, int retryCount, TimeSpan retryInterval, bool firstFastRetry) + : base(name, firstFastRetry) + { + // Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + // Guard.ArgumentNotNegativeValue(retryInterval.Ticks, "retryInterval"); + this._retryCount = retryCount; + this._retryInterval = retryInterval; + } + + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() + { + if (_retryCount == 0) { - } - - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - public FixedInterval(int retryCount) - : this(retryCount, DefaultRetryInterval) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - /// The time interval between retries. - public FixedInterval(int retryCount, TimeSpan retryInterval) - : this(null, retryCount, retryInterval, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The time interval between retries. - public FixedInterval(string name, int retryCount, TimeSpan retryInterval) - : this(name, retryCount, retryInterval, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The time interval between retries. - /// a value indicating whether or not the very first retry attempt will be made immediately whereas the subsequent retries will remain subject to retry interval. - public FixedInterval(string? name, int retryCount, TimeSpan retryInterval, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(retryInterval.Ticks, "retryInterval"); - - this.retryCount = retryCount; - this.retryInterval = retryInterval; - } - - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() - { - if (this.retryCount == 0) - { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) - { - interval = TimeSpan.Zero; - return false; - }; - } - return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) { - if (currentRetryCount < this.retryCount) - { - interval = this.retryInterval; - return true; - } - interval = TimeSpan.Zero; return false; }; } + + return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) + { + if (currentRetryCount < _retryCount) + { + interval = _retryInterval; + return true; + } + + interval = TimeSpan.Zero; + return false; + }; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs index 1848436ae1..91fda41d00 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs @@ -1,86 +1,93 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with a specified number of retry attempts and an incremental time interval between retries. +/// +public class Incremental : RetryStrategy { + private readonly TimeSpan _increment; + private readonly TimeSpan _initialInterval; + private readonly int _retryCount; + /// - /// A retry strategy with a specified number of retry attempts and an incremental time interval between retries. + /// Initializes a new instance of the class. /// - public class Incremental : RetryStrategy + public Incremental() + : this(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement) { - private readonly int retryCount; - private readonly TimeSpan initialInterval; - private readonly TimeSpan increment; - - /// - /// Initializes a new instance of the class. - /// - public Incremental() - : this(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public Incremental(int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(null, retryCount, initialInterval, increment) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(name, retryCount, initialInterval, increment, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - /// a value indicating whether or not the very first retry attempt will be made immediately whereas the subsequent retries will remain subject to retry interval. - public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(initialInterval.Ticks, "initialInterval"); - //Guard.ArgumentNotNegativeValue(increment.Ticks, "increment"); - - this.retryCount = retryCount; - this.initialInterval = initialInterval; - this.increment = increment; - } - - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() - { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) - { - if (currentRetryCount < this.retryCount) - { - retryInterval = TimeSpan.FromMilliseconds(this.initialInterval.TotalMilliseconds + (this.increment.TotalMilliseconds * currentRetryCount)); - - return true; - } - - retryInterval = TimeSpan.Zero; - - return false; - }; - } } + + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public Incremental(int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(null, retryCount, initialInterval, increment) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(name, retryCount, initialInterval, increment, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + /// + /// a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment, bool firstFastRetry) + : base(name, firstFastRetry) + { + // Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + // Guard.ArgumentNotNegativeValue(initialInterval.Ticks, "initialInterval"); + // Guard.ArgumentNotNegativeValue(increment.Ticks, "increment"); + this._retryCount = retryCount; + this._initialInterval = initialInterval; + this._increment = increment; + } + + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() => + delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) + { + if (currentRetryCount < _retryCount) + { + retryInterval = TimeSpan.FromMilliseconds(_initialInterval.TotalMilliseconds + + (_increment.TotalMilliseconds * currentRetryCount)); + + return true; + } + + retryInterval = TimeSpan.Zero; + + return false; + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs index fc7bb72b6b..e046f8592d 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs @@ -1,31 +1,27 @@ -using System; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; + +/// +/// Implements a strategy that detects network connectivity errors such as host not found. +/// +public class NetworkConnectivityErrorDetectionStrategy : ITransientErrorDetectionStrategy { - /// - /// Implements a strategy that detects network connectivity errors such as host not found. - /// - public class NetworkConnectivityErrorDetectionStrategy : ITransientErrorDetectionStrategy + public bool IsTransient(Exception? ex) { - public bool IsTransient(Exception ex) + if (ex != null && ex is SqlException sqlException) { - SqlException? sqlException; - - if (ex != null && (sqlException = ex as SqlException) != null) + switch (sqlException.Number) { - switch (sqlException.Number) - { - // SQL Error Code: 11001 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL - // Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) - case 11001: - return true; - } + // SQL Error Code: 11001 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL + // Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) + case 11001: + return true; } - - return false; } + + return false; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs index 2711ce4714..b4ae18d55a 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs @@ -1,174 +1,165 @@ -using System; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; + +// See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues +// Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? +// but i guess that's not netcore so we'll just leave it. + +/// +/// Provides the transient error detection logic for transient faults that are specific to SQL Azure. +/// +public class SqlAzureTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy { - // See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues - // Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? - // but i guess that's not netcore so we'll just leave it. - /// - /// Provides the transient error detection logic for transient faults that are specific to SQL Azure. + /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// - public class SqlAzureTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy + /// The exception object to be verified. + /// true if the specified exception is considered as transient; otherwise, false. + public bool IsTransient(Exception? ex) { - #region ProcessNetLibErrorCode enumeration - - /// - /// Error codes reported by the DBNETLIB module. - /// - private enum ProcessNetLibErrorCode + if (ex != null) { - ZeroBytes = -3, - - Timeout = -2, /* Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. */ - - Unknown = -1, - - InsufficientMemory = 1, - - AccessDenied = 2, - - ConnectionBusy = 3, - - ConnectionBroken = 4, - - ConnectionLimit = 5, - - ServerNotFound = 6, - - NetworkNotFound = 7, - - InsufficientResources = 8, - - NetworkBusy = 9, - - NetworkAccessDenied = 10, - - GeneralError = 11, - - IncorrectMode = 12, - - NameNotFound = 13, - - InvalidConnection = 14, - - ReadWriteError = 15, - - TooManyHandles = 16, - - ServerError = 17, - - SSLError = 18, - - EncryptionError = 19, - - EncryptionNotSupported = 20 - } - - #endregion - - #region ITransientErrorDetectionStrategy implementation - - /// - /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. - /// - /// The exception object to be verified. - /// true if the specified exception is considered as transient; otherwise, false. - public bool IsTransient(Exception ex) - { - if (ex != null) + SqlException? sqlException; + if ((sqlException = ex as SqlException) != null) { - SqlException? sqlException; - if ((sqlException = ex as SqlException) != null) + // Enumerate through all errors found in the exception. + foreach (SqlError err in sqlException.Errors) { - // Enumerate through all errors found in the exception. - foreach (SqlError err in sqlException.Errors) + switch (err.Number) { - switch (err.Number) - { - // SQL Error Code: 40501 - // The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). - case ThrottlingCondition.ThrottlingErrorNumber: - // Decode the reason code from the error message to determine the grounds for throttling. - var condition = ThrottlingCondition.FromError(err); + // SQL Error Code: 40501 + // The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). + case ThrottlingCondition.ThrottlingErrorNumber: + // Decode the reason code from the error message to determine the grounds for throttling. + var condition = ThrottlingCondition.FromError(err); - // Attach the decoded values as additional attributes to the original SQL exception. - sqlException.Data[condition.ThrottlingMode.GetType().Name] = - condition.ThrottlingMode.ToString(); - sqlException.Data[condition.GetType().Name] = condition; + // Attach the decoded values as additional attributes to the original SQL exception. + sqlException.Data[condition.ThrottlingMode.GetType().Name] = + condition.ThrottlingMode.ToString(); + sqlException.Data[condition.GetType().Name] = condition; - return true; + return true; - // SQL Error Code: 10928 - // Resource ID: %d. The %s limit for the database is %d and has been reached. - case 10928: - // SQL Error Code: 10929 - // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. - // However, the server is currently too busy to support requests greater than %d for this database. - case 10929: - // SQL Error Code: 10053 - // A transport-level error has occurred when receiving results from the server. - // An established connection was aborted by the software in your host machine. - case 10053: - // SQL Error Code: 10054 - // A transport-level error has occurred when sending the request to the server. - // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) - case 10054: - // SQL Error Code: 10060 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server - // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed - // because the connected party did not properly respond after a period of time, or established connection failed - // because connected host has failed to respond.)"} - case 10060: - // SQL Error Code: 40197 - // The service has encountered an error processing your request. Please try again. - case 40197: - // SQL Error Code: 40540 - // The service has encountered an error processing your request. Please try again. - case 40540: - // SQL Error Code: 40613 - // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer - // support, and provide them the session tracing ID of ZZZZZ. - case 40613: - // SQL Error Code: 40143 - // The service has encountered an error processing your request. Please try again. - case 40143: - // SQL Error Code: 233 - // The client was unable to establish a connection because of an error during connection initialization process before login. - // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy - // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. - // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) - case 233: - // SQL Error Code: 64 - // A connection was successfully established with the server, but then an error occurred during the login process. - // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) - case 64: - // DBNETLIB Error Code: 20 - // The instance of SQL Server you attempted to connect to does not support encryption. - case (int)ProcessNetLibErrorCode.EncryptionNotSupported: - return true; - } + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + case 10928: + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // However, the server is currently too busy to support requests greater than %d for this database. + case 10929: + // SQL Error Code: 10053 + // A transport-level error has occurred when receiving results from the server. + // An established connection was aborted by the software in your host machine. + case 10053: + // SQL Error Code: 10054 + // A transport-level error has occurred when sending the request to the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 10054: + // SQL Error Code: 10060 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed + // because connected host has failed to respond.)"} + case 10060: + // SQL Error Code: 40197 + // The service has encountered an error processing your request. Please try again. + case 40197: + // SQL Error Code: 40540 + // The service has encountered an error processing your request. Please try again. + case 40540: + // SQL Error Code: 40613 + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // support, and provide them the session tracing ID of ZZZZZ. + case 40613: + // SQL Error Code: 40143 + // The service has encountered an error processing your request. Please try again. + case 40143: + // SQL Error Code: 233 + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 233: + // SQL Error Code: 64 + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + case 64: + // DBNETLIB Error Code: 20 + // The instance of SQL Server you attempted to connect to does not support encryption. + case (int)ProcessNetLibErrorCode.EncryptionNotSupported: + return true; } } - else if (ex is TimeoutException) - { - return true; - } - // else - // { - // EntityException entityException; - // if ((entityException = ex as EntityException) != null) - // { - // return this.IsTransient(entityException.InnerException); - // } - // } + } + else if (ex is TimeoutException) + { + return true; } - return false; + // else + // { + // EntityException entityException; + // if ((entityException = ex as EntityException) != null) + // { + // return this.IsTransient(entityException.InnerException); + // } + // } } - #endregion + return false; + } + + /// + /// Error codes reported by the DBNETLIB module. + /// + private enum ProcessNetLibErrorCode + { + ZeroBytes = -3, + + Timeout = -2, /* Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. */ + + Unknown = -1, + + InsufficientMemory = 1, + + AccessDenied = 2, + + ConnectionBusy = 3, + + ConnectionBroken = 4, + + ConnectionLimit = 5, + + ServerNotFound = 6, + + NetworkNotFound = 7, + + InsufficientResources = 8, + + NetworkBusy = 9, + + NetworkAccessDenied = 10, + + GeneralError = 11, + + IncorrectMode = 12, + + NameNotFound = 13, + + InvalidConnection = 14, + + ReadWriteError = 15, + + TooManyHandles = 16, + + ServerError = 17, + + SSLError = 18, + + EncryptionError = 19, + + EncryptionNotSupported = 20, } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs index 96d42a9481..24ba741c06 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs @@ -1,330 +1,331 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// Defines the possible throttling modes in SQL Azure. +/// +public enum ThrottlingMode { /// - /// Defines the possible throttling modes in SQL Azure. + /// Corresponds to "No Throttling" throttling mode whereby all SQL statements can be processed. /// - public enum ThrottlingMode - { - /// - /// Corresponds to "No Throttling" throttling mode whereby all SQL statements can be processed. - /// - NoThrottling = 0, - - /// - /// Corresponds to "Reject Update / Insert" throttling mode whereby SQL statements such as INSERT, UPDATE, CREATE TABLE and CREATE INDEX are rejected. - /// - RejectUpdateInsert = 1, - - /// - /// Corresponds to "Reject All Writes" throttling mode whereby SQL statements such as INSERT, UPDATE, DELETE, CREATE, DROP are rejected. - /// - RejectAllWrites = 2, - - /// - /// Corresponds to "Reject All" throttling mode whereby all SQL statements are rejected. - /// - RejectAll = 3, - - /// - /// Corresponds to an unknown throttling mode whereby throttling mode cannot be determined with certainty. - /// - Unknown = -1 - } + NoThrottling = 0, /// - /// Defines the possible throttling types in SQL Azure. + /// Corresponds to "Reject Update / Insert" throttling mode whereby SQL statements such as INSERT, UPDATE, CREATE TABLE + /// and CREATE INDEX are rejected. /// - public enum ThrottlingType - { - /// - /// Indicates that no throttling was applied to a given resource. - /// - None = 0, - - /// - /// Corresponds to a Soft throttling type. Soft throttling is applied when machine resources such as, CPU, IO, storage, and worker threads exceed - /// predefined safety thresholds despite the load balancer’s best efforts. - /// - Soft = 1, - - /// - /// Corresponds to a Hard throttling type. Hard throttling is applied when the machine is out of resources, for example storage space. - /// With hard throttling, no new connections are allowed to the databases hosted on the machine until resources are freed up. - /// - Hard = 2, - - /// - /// Corresponds to an unknown throttling type in the event when the throttling type cannot be determined with certainty. - /// - Unknown = 3 - } + RejectUpdateInsert = 1, /// - /// Defines the types of resources in SQL Azure which may be subject to throttling conditions. + /// Corresponds to "Reject All Writes" throttling mode whereby SQL statements such as INSERT, UPDATE, DELETE, CREATE, + /// DROP are rejected. /// - public enum ThrottledResourceType - { - /// - /// Corresponds to "Physical Database Space" resource which may be subject to throttling. - /// - PhysicalDatabaseSpace = 0, - - /// - /// Corresponds to "Physical Log File Space" resource which may be subject to throttling. - /// - PhysicalLogSpace = 1, - - /// - /// Corresponds to "Transaction Log Write IO Delay" resource which may be subject to throttling. - /// - LogWriteIoDelay = 2, - - /// - /// Corresponds to "Database Read IO Delay" resource which may be subject to throttling. - /// - DataReadIoDelay = 3, - - /// - /// Corresponds to "CPU" resource which may be subject to throttling. - /// - Cpu = 4, - - /// - /// Corresponds to "Database Size" resource which may be subject to throttling. - /// - DatabaseSize = 5, - - /// - /// Corresponds to "SQL Worker Thread Pool" resource which may be subject to throttling. - /// - WorkerThreads = 7, - - /// - /// Corresponds to an internal resource which may be subject to throttling. - /// - Internal = 6, - - /// - /// Corresponds to an unknown resource type in the event when the actual resource cannot be determined with certainty. - /// - Unknown = -1 - } + RejectAllWrites = 2, /// - /// Implements an object holding the decoded reason code returned from SQL Azure when encountering throttling conditions. + /// Corresponds to "Reject All" throttling mode whereby all SQL statements are rejected. /// - [Serializable] - public class ThrottlingCondition + RejectAll = 3, + + /// + /// Corresponds to an unknown throttling mode whereby throttling mode cannot be determined with certainty. + /// + Unknown = -1, +} + +/// +/// Defines the possible throttling types in SQL Azure. +/// +public enum ThrottlingType +{ + /// + /// Indicates that no throttling was applied to a given resource. + /// + None = 0, + + /// + /// Corresponds to a Soft throttling type. Soft throttling is applied when machine resources such as, CPU, IO, storage, + /// and worker threads exceed + /// predefined safety thresholds despite the load balancer’s best efforts. + /// + Soft = 1, + + /// + /// Corresponds to a Hard throttling type. Hard throttling is applied when the machine is out of resources, for example + /// storage space. + /// With hard throttling, no new connections are allowed to the databases hosted on the machine until resources are + /// freed up. + /// + Hard = 2, + + /// + /// Corresponds to an unknown throttling type in the event when the throttling type cannot be determined with + /// certainty. + /// + Unknown = 3, +} + +/// +/// Defines the types of resources in SQL Azure which may be subject to throttling conditions. +/// +public enum ThrottledResourceType +{ + /// + /// Corresponds to "Physical Database Space" resource which may be subject to throttling. + /// + PhysicalDatabaseSpace = 0, + + /// + /// Corresponds to "Physical Log File Space" resource which may be subject to throttling. + /// + PhysicalLogSpace = 1, + + /// + /// Corresponds to "Transaction Log Write IO Delay" resource which may be subject to throttling. + /// + LogWriteIoDelay = 2, + + /// + /// Corresponds to "Database Read IO Delay" resource which may be subject to throttling. + /// + DataReadIoDelay = 3, + + /// + /// Corresponds to "CPU" resource which may be subject to throttling. + /// + Cpu = 4, + + /// + /// Corresponds to "Database Size" resource which may be subject to throttling. + /// + DatabaseSize = 5, + + /// + /// Corresponds to "SQL Worker Thread Pool" resource which may be subject to throttling. + /// + WorkerThreads = 7, + + /// + /// Corresponds to an internal resource which may be subject to throttling. + /// + Internal = 6, + + /// + /// Corresponds to an unknown resource type in the event when the actual resource cannot be determined with certainty. + /// + Unknown = -1, +} + +/// +/// Implements an object holding the decoded reason code returned from SQL Azure when encountering throttling +/// conditions. +/// +[Serializable] +public class ThrottlingCondition +{ + /// + /// Gets the error number that corresponds to throttling conditions reported by SQL Azure. + /// + public const int ThrottlingErrorNumber = 40501; + + /// + /// Provides a compiled regular expression used for extracting the reason code from the error message. + /// + private static readonly Regex _sqlErrorCodeRegEx = + new(@"Code:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Maintains a collection of key-value pairs where a key is resource type and a value is the type of throttling + /// applied to the given resource type. + /// + private readonly IList> _throttledResources = + new List>(9); + + /// + /// Gets an unknown throttling condition in the event the actual throttling condition cannot be determined. + /// + public static ThrottlingCondition Unknown { - /// - /// Gets the error number that corresponds to throttling conditions reported by SQL Azure. - /// - public const int ThrottlingErrorNumber = 40501; - - /// - /// Maintains a collection of key-value pairs where a key is resource type and a value is the type of throttling applied to the given resource type. - /// - private readonly IList> throttledResources = new List>(9); - - /// - /// Provides a compiled regular expression used for extracting the reason code from the error message. - /// - private static readonly Regex sqlErrorCodeRegEx = new Regex(@"Code:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - /// - /// Gets an unknown throttling condition in the event the actual throttling condition cannot be determined. - /// - public static ThrottlingCondition Unknown + get { - get + var unknownCondition = new ThrottlingCondition { ThrottlingMode = ThrottlingMode.Unknown }; + unknownCondition._throttledResources.Add(Tuple.Create( + ThrottledResourceType.Unknown, + ThrottlingType.Unknown)); + + return unknownCondition; + } + } + + /// + /// Gets the value that reflects the throttling mode in SQL Azure. + /// + public ThrottlingMode ThrottlingMode { get; private set; } + + /// + /// Gets a list of resources in SQL Azure that were subject to throttling conditions. + /// + public IEnumerable> ThrottledResources => _throttledResources; + + /// + /// Gets a value indicating whether physical data file space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataSpace => + _throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalDatabaseSpace); + + /// + /// Gets a value indicating whether physical log space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogSpace => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalLogSpace); + + /// + /// Gets a value indicating whether transaction activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogWrite => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.LogWriteIoDelay); + + /// + /// Gets a value indicating whether data read activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataRead => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.DataReadIoDelay); + + /// + /// Gets a value indicating whether CPU throttling was reported by SQL Azure. + /// + public bool IsThrottledOnCpu => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.Cpu); + + /// + /// Gets a value indicating whether database size throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDatabaseSize => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.DatabaseSize); + + /// + /// Gets a value indicating whether concurrent requests throttling was reported by SQL Azure. + /// + public bool IsThrottledOnWorkerThreads => + _throttledResources.Any(x => x.Item1 == ThrottledResourceType.WorkerThreads); + + /// + /// Gets a value indicating whether throttling conditions were not determined with certainty. + /// + public bool IsUnknown => ThrottlingMode == ThrottlingMode.Unknown; + + /// + /// Determines throttling conditions from the specified SQL exception. + /// + /// + /// The object containing information relevant to an error returned by SQL + /// Server when encountering throttling conditions. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure upon encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromException(SqlException? ex) + { + if (ex != null) + { + foreach (SqlError error in ex.Errors) { - var unknownCondition = new ThrottlingCondition { ThrottlingMode = ThrottlingMode.Unknown }; - unknownCondition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Unknown, ThrottlingType.Unknown)); - - return unknownCondition; - } - } - - /// - /// Gets the value that reflects the throttling mode in SQL Azure. - /// - public ThrottlingMode ThrottlingMode { get; private set; } - - /// - /// Gets a list of resources in SQL Azure that were subject to throttling conditions. - /// - public IEnumerable> ThrottledResources - { - get { return this.throttledResources; } - } - - /// - /// Gets a value indicating whether physical data file space throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDataSpace - { - get { return throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalDatabaseSpace); } - } - - /// - /// Gets a value indicating whether physical log space throttling was reported by SQL Azure. - /// - public bool IsThrottledOnLogSpace - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalLogSpace); } - } - - /// - /// Gets a value indicating whether transaction activity throttling was reported by SQL Azure. - /// - public bool IsThrottledOnLogWrite - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.LogWriteIoDelay); } - } - - /// - /// Gets a value indicating whether data read activity throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDataRead - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.DataReadIoDelay); } - } - - /// - /// Gets a value indicating whether CPU throttling was reported by SQL Azure. - /// - public bool IsThrottledOnCpu - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.Cpu); } - } - - /// - /// Gets a value indicating whether database size throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDatabaseSize - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.DatabaseSize); } - } - - /// - /// Gets a value indicating whether concurrent requests throttling was reported by SQL Azure. - /// - public bool IsThrottledOnWorkerThreads - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.WorkerThreads); } - } - - /// - /// Gets a value indicating whether throttling conditions were not determined with certainty. - /// - public bool IsUnknown - { - get { return ThrottlingMode == ThrottlingMode.Unknown; } - } - - /// - /// Determines throttling conditions from the specified SQL exception. - /// - /// The object containing information relevant to an error returned by SQL Server when encountering throttling conditions. - /// An instance of the object holding the decoded reason codes returned from SQL Azure upon encountering throttling conditions. - public static ThrottlingCondition FromException(SqlException ex) - { - if (ex != null) - { - foreach (SqlError error in ex.Errors) + if (error.Number == ThrottlingErrorNumber) { - if (error.Number == ThrottlingErrorNumber) - { - return FromError(error); - } + return FromError(error); } } - - return Unknown; } - /// - /// Determines the throttling conditions from the specified SQL error. - /// - /// The object containing information relevant to a warning or error returned by SQL Server. - /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. - public static ThrottlingCondition FromError(SqlError error) + return Unknown; + } + + /// + /// Determines the throttling conditions from the specified SQL error. + /// + /// + /// The object containing information relevant to a warning or error returned + /// by SQL Server. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromError(SqlError? error) + { + if (error != null) { - if (error != null) + Match match = _sqlErrorCodeRegEx.Match(error.Message); + + if (match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int reasonCode)) { - var match = sqlErrorCodeRegEx.Match(error.Message); - int reasonCode; - - if (match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out reasonCode)) - { - return FromReasonCode(reasonCode); - } + return FromReasonCode(reasonCode); } - - return Unknown; } - /// - /// Determines the throttling conditions from the specified reason code. - /// - /// The reason code returned by SQL Azure which contains the throttling mode and the exceeded resource types. - /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. - public static ThrottlingCondition FromReasonCode(int reasonCode) + return Unknown; + } + + /// + /// Determines the throttling conditions from the specified reason code. + /// + /// + /// The reason code returned by SQL Azure which contains the throttling mode and the exceeded + /// resource types. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromReasonCode(int reasonCode) + { + if (reasonCode > 0) { - if (reasonCode > 0) - { - // Decode throttling mode from the last 2 bits. - var throttlingMode = (ThrottlingMode)(reasonCode & 3); + // Decode throttling mode from the last 2 bits. + var throttlingMode = (ThrottlingMode)(reasonCode & 3); - var condition = new ThrottlingCondition { ThrottlingMode = throttlingMode }; + var condition = new ThrottlingCondition { ThrottlingMode = throttlingMode }; - // Shift 8 bits to truncate throttling mode. - var groupCode = reasonCode >> 8; + // Shift 8 bits to truncate throttling mode. + var groupCode = reasonCode >> 8; - // Determine throttling type for all well-known resources that may be subject to throttling conditions. - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalDatabaseSpace, (ThrottlingType)(groupCode & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalLogSpace, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.LogWriteIoDelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DataReadIoDelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Cpu, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DatabaseSize, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.WorkerThreads, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode >> 2) & 3))); + // Determine throttling type for all well-known resources that may be subject to throttling conditions. + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalDatabaseSpace, (ThrottlingType)(groupCode & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalLogSpace, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.LogWriteIoDelay, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.DataReadIoDelay, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.Cpu, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.DatabaseSize, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.WorkerThreads, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode >> 2) & 3))); - return condition; - } - - return Unknown; + return condition; } - /// - /// Returns a textual representation the current ThrottlingCondition object including the information held with respect to throttled resources. - /// - /// A string that represents the current ThrottlingCondition object. - public override string ToString() - { - var result = new StringBuilder(); + return Unknown; + } - result.AppendFormat(CultureInfo.CurrentCulture, "Mode: {0} | ", ThrottlingMode); + /// + /// Returns a textual representation the current ThrottlingCondition object including the information held with respect + /// to throttled resources. + /// + /// A string that represents the current ThrottlingCondition object. + public override string ToString() + { + var result = new StringBuilder(); - var resources = - this.throttledResources - .Where(x => x.Item1 != ThrottledResourceType.Internal) - .Select(x => string.Format(CultureInfo.CurrentCulture, "{0}: {1}", x.Item1, x.Item2)) - .OrderBy(x => x).ToArray(); + result.AppendFormat(CultureInfo.CurrentCulture, "Mode: {0} | ", ThrottlingMode); - result.Append(string.Join(", ", resources)); + var resources = + _throttledResources + .Where(x => x.Item1 != ThrottledResourceType.Internal) + .Select(x => string.Format(CultureInfo.CurrentCulture, "{0}: {1}", x.Item1, x.Item2)) + .OrderBy(x => x).ToArray(); - return result.ToString(); - } + result.Append(string.Join(", ", resources)); + + return result.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs index 6a928b6859..fb2a15a01a 100644 --- a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Persistence +public interface IBulkSqlInsertProvider { - public interface IBulkSqlInsertProvider - { - string ProviderName { get; } - int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records); - } + string ProviderName { get; } + + int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs index 2d97cfbcd3..bf01728075 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence -{ - public interface IDatabaseCreator - { - string ProviderName { get; } +namespace Umbraco.Cms.Infrastructure.Persistence; - void Create(string connectionString); - } +public interface IDatabaseCreator +{ + string ProviderName { get; } + + void Create(string connectionString); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs index c766c50d69..1c06dd089f 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Install.Models; @@ -7,84 +6,84 @@ namespace Umbraco.Cms.Infrastructure.Persistence; public interface IDatabaseProviderMetadata { /// - /// Gets a unique identifier for this set of metadata used for filtering. + /// Gets a unique identifier for this set of metadata used for filtering. /// [DataMember(Name = "id")] Guid Id { get; } /// - /// Gets a value to determine display order and quick install priority. + /// Gets a value to determine display order and quick install priority. /// [DataMember(Name = "sortOrder")] int SortOrder { get; } /// - /// Gets a friendly name to describe the provider. + /// Gets a friendly name to describe the provider. /// [DataMember(Name = "displayName")] string DisplayName { get; } /// - /// Gets the default database name for the provider. + /// Gets the default database name for the provider. /// [DataMember(Name = "defaultDatabaseName")] string DefaultDatabaseName { get; } /// - /// Gets the database factory provider name. + /// Gets the database factory provider name. /// [DataMember(Name = "providerName")] string? ProviderName { get; } /// - /// Gets a value indicating whether can be used for one click install. + /// Gets a value indicating whether can be used for one click install. /// [DataMember(Name = "supportsQuickInstall")] bool SupportsQuickInstall { get; } /// - /// Gets a value indicating whether should be available for selection. + /// Gets a value indicating whether should be available for selection. /// [DataMember(Name = "isAvailable")] bool IsAvailable { get; } /// - /// Gets a value indicating whether the server/hostname field must be populated. + /// Gets a value indicating whether the server/hostname field must be populated. /// [DataMember(Name = "requiresServer")] bool RequiresServer { get; } /// - /// Gets a value used as input placeholder for server/hostnmae field. + /// Gets a value used as input placeholder for server/hostnmae field. /// [DataMember(Name = "serverPlaceholder")] string? ServerPlaceholder { get; } /// - /// Gets a value indicating whether a username and password are required (in general) to connect to the database + /// Gets a value indicating whether a username and password are required (in general) to connect to the database /// [DataMember(Name = "requiresCredentials")] bool RequiresCredentials { get; } /// - /// Gets a value indicating whether integrated authentication is supported (e.g. SQL Server & Oracle). + /// Gets a value indicating whether integrated authentication is supported (e.g. SQL Server & Oracle). /// [DataMember(Name = "supportsIntegratedAuthentication")] bool SupportsIntegratedAuthentication { get; } /// - /// Gets a value indicating whether the connection should be tested before continuing install process. + /// Gets a value indicating whether the connection should be tested before continuing install process. /// [DataMember(Name = "requiresConnectionTest")] bool RequiresConnectionTest { get; } /// - /// Gets a value indicating to ignore the value of GlobalSettings.InstallMissingDatabase + /// Gets a value indicating to ignore the value of GlobalSettings.InstallMissingDatabase /// public bool ForceCreateDatabase { get; } /// - /// Creates a connection string for this provider. + /// Creates a connection string for this provider. /// string? GenerateConnectionString(DatabaseModel databaseModel); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs index 4ee6ce7d59..4357a11063 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; using System.Data.Common; -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IDbProviderFactoryCreator { + DbProviderFactory? CreateFactory(string? providerName); - public interface IDbProviderFactoryCreator - { - DbProviderFactory? CreateFactory(string? providerName); - ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); - IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); - void CreateDatabase(string providerName, string connectionString); - NPocoMapperCollection ProviderSpecificMappers(string providerName); + ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); - IEnumerable GetProviderSpecificInterceptors(string providerName) => - Enumerable.Empty(); - } + IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); + + void CreateDatabase(string providerName, string connectionString); + + NPocoMapperCollection ProviderSpecificMappers(string providerName); + + IEnumerable GetProviderSpecificInterceptors(string providerName) => + Enumerable.Empty(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs index 736ba80854..41af78253a 100644 --- a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs @@ -23,6 +23,6 @@ public interface IProviderSpecificDataInterceptor : IProviderSpecificInterceptor { } -public interface IProviderSpecificTransactionInterceptor: IProviderSpecificInterceptor, ITransactionInterceptor +public interface IProviderSpecificTransactionInterceptor : IProviderSpecificInterceptor, ITransactionInterceptor { } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs index 3a73d647e8..ed463a1bb1 100644 --- a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IProviderSpecificMapperFactory { - public interface IProviderSpecificMapperFactory - { - string ProviderName { get; } - NPocoMapperCollection Mappers { get; } - } + string ProviderName { get; } + + NPocoMapperCollection Mappers { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs index 29f1128a44..c937880a22 100644 --- a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs @@ -1,13 +1,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence; /// -/// Provides a mapping function for +/// Provides a mapping function for /// public interface IScalarMapper { /// - /// Performs a mapping operation for a scalar value. + /// Performs a mapping operation for a scalar value. /// object Map(object value); } - diff --git a/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs b/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs index 9178ba8ae7..87bc1dd81f 100644 --- a/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs +++ b/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs @@ -1,53 +1,52 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Specifies the Sql context. +/// +public interface ISqlContext { /// - /// Specifies the Sql context. + /// Gets the Sql syntax provider. /// - public interface ISqlContext - { - /// - /// Gets the Sql syntax provider. - /// - ISqlSyntaxProvider SqlSyntax { get; } + ISqlSyntaxProvider SqlSyntax { get; } - /// - /// Gets the database type. - /// - DatabaseType DatabaseType { get; } + /// + /// Gets the database type. + /// + DatabaseType DatabaseType { get; } - /// - /// Creates a new Sql expression. - /// - Sql Sql(); + /// + /// Gets the Sql templates. + /// + SqlTemplates Templates { get; } - /// - /// Creates a new Sql expression. - /// - Sql Sql(string sql, params object[] args); + /// + /// Gets the Poco data factory. + /// + IPocoDataFactory PocoDataFactory { get; } - /// - /// Creates a new query expression. - /// - IQuery Query(); + /// + /// Gets the mappers. + /// + IMapperCollection? Mappers { get; } - /// - /// Gets the Sql templates. - /// - SqlTemplates Templates { get; } + /// + /// Creates a new Sql expression. + /// + Sql Sql(); - /// - /// Gets the Poco data factory. - /// - IPocoDataFactory PocoDataFactory { get; } + /// + /// Creates a new Sql expression. + /// + Sql Sql(string sql, params object[] args); - /// - /// Gets the mappers. - /// - IMapperCollection? Mappers { get; } - } + /// + /// Creates a new query expression. + /// + IQuery Query(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs index c28b0d984d..431ddeb5e8 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs @@ -1,32 +1,36 @@ -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IUmbracoDatabase : IDatabase { - public interface IUmbracoDatabase : IDatabase - { - /// - /// Gets the Sql context. - /// - ISqlContext SqlContext { get; } + /// + /// Gets the Sql context. + /// + ISqlContext SqlContext { get; } - /// - /// Gets the database instance unique identifier as a string. - /// - /// UmbracoDatabase returns the first eight digits of its unique Guid and, in some - /// debug mode, the underlying database connection identifier (if any). - string InstanceId { get; } + /// + /// Gets the database instance unique identifier as a string. + /// + /// + /// UmbracoDatabase returns the first eight digits of its unique Guid and, in some + /// debug mode, the underlying database connection identifier (if any). + /// + string InstanceId { get; } - /// - /// Gets a value indicating whether the database is currently in a transaction. - /// - bool InTransaction { get; } + /// + /// Gets a value indicating whether the database is currently in a transaction. + /// + bool InTransaction { get; } - bool EnableSqlCount { get; set; } - int SqlCount { get; } - int BulkInsertRecords(IEnumerable records); - bool IsUmbracoInstalled(); - DatabaseSchemaResult ValidateSchema(); - } + bool EnableSqlCount { get; set; } + + int SqlCount { get; } + + int BulkInsertRecords(IEnumerable records); + + bool IsUmbracoInstalled(); + + DatabaseSchemaResult ValidateSchema(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs index a4d4259cbe..8bb717e577 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs @@ -1,82 +1,83 @@ -using System; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Creates and manages the "ambient" database. +/// +public interface IUmbracoDatabaseFactory : IDisposable { /// - /// Creates and manages the "ambient" database. + /// Gets a value indicating whether the database factory is configured, i.e. whether + /// its connection string and provider name have been set. The factory may however not + /// be initialized (see ). /// - public interface IUmbracoDatabaseFactory : IDisposable - { - /// - /// Creates a new database. - /// - /// - /// The new database must be disposed after being used. - /// Creating a database causes the factory to initialize if it is not already initialized. - /// - IUmbracoDatabase CreateDatabase(); + bool Configured { get; } - /// - /// Gets a value indicating whether the database factory is configured, i.e. whether - /// its connection string and provider name have been set. The factory may however not - /// be initialized (see ). - /// - bool Configured { get; } + /// + /// Gets a value indicating whether the database factory is initialized, i.e. whether + /// its internal state is ready and it has been possible to connect to the database. + /// + bool Initialized { get; } - /// - /// Gets a value indicating whether the database factory is initialized, i.e. whether - /// its internal state is ready and it has been possible to connect to the database. - /// - bool Initialized { get; } + /// + /// Gets the connection string. + /// + /// May return null if the database factory is not configured. + string? ConnectionString { get; } - /// - /// Gets the connection string. - /// - /// May return null if the database factory is not configured. - string? ConnectionString { get; } + /// + /// Gets the provider name. + /// + /// May return null if the database factory is not configured. + string? ProviderName { get; } - /// - /// Gets the provider name. - /// - /// May return null if the database factory is not configured. - string? ProviderName { get; } + /// + /// Gets a value indicating whether the database factory is configured (see ), + /// and it is possible to connect to the database. The factory may however not be initialized (see + /// ). + /// + bool CanConnect { get; } - /// - /// Gets a value indicating whether the database factory is configured (see ), - /// and it is possible to connect to the database. The factory may however not be initialized (see - /// ). - /// - bool CanConnect { get; } + /// + /// Gets the . + /// + /// + /// Getting the causes the factory to initialize if it is not already initialized. + /// + ISqlContext SqlContext { get; } - /// - /// Configures the database factory. - /// - void Configure(ConnectionStrings umbracoConnectionString); + /// + /// Gets the . + /// + /// + /// + /// Getting the causes the factory to initialize if it is not already + /// initialized. + /// + /// + IBulkSqlInsertProvider? BulkSqlInsertProvider { get; } - [Obsolete("Please use alternative Configure method.")] - void Configure(string connectionString, string providerName) => - Configure(new ConnectionStrings { ConnectionString = connectionString, ProviderName = providerName }); + /// + /// Creates a new database. + /// + /// + /// The new database must be disposed after being used. + /// Creating a database causes the factory to initialize if it is not already initialized. + /// + IUmbracoDatabase CreateDatabase(); - /// - /// Gets the . - /// - /// - /// Getting the causes the factory to initialize if it is not already initialized. - /// - ISqlContext SqlContext { get; } + /// + /// Configures the database factory. + /// + void Configure(ConnectionStrings umbracoConnectionString); - /// - /// Gets the . - /// - /// - /// Getting the causes the factory to initialize if it is not already initialized. - /// - IBulkSqlInsertProvider? BulkSqlInsertProvider { get; } + [Obsolete("Please use alternative Configure method.")] + void Configure(string connectionString, string providerName) => + Configure(new ConnectionStrings { ConnectionString = connectionString, ProviderName = providerName }); - /// - /// Configures the database factory for upgrades. - /// - void ConfigureForUpgrade(); - } + /// + /// Configures the database factory for upgrades. + /// + void ConfigureForUpgrade(); } diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 709940f3cc..d6c23abba8 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -1,961 +1,1047 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Diagnostics; -using System.IO; -using System.Linq; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Manages LocalDB databases. +/// +/// +/// +/// Latest version is SQL Server 2016 Express LocalDB, +/// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb +/// which can be installed by downloading the Express installer from +/// https://www.microsoft.com/en-us/sql-server/sql-server-downloads +/// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This +/// installs +/// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server +/// Management +/// Studio which can be used to connect to LocalDB databases. +/// +/// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. +/// +public class LocalDb { + private string? _exe; + private bool _hasVersion; + private int _version; + + #region Availability & Version + /// - /// Manages LocalDB databases. + /// Gets the LocalDb installed version. /// /// - /// Latest version is SQL Server 2016 Express LocalDB, - /// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb - /// which can be installed by downloading the Express installer from https://www.microsoft.com/en-us/sql-server/sql-server-downloads - /// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This installs - /// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server Management - /// Studio which can be used to connect to LocalDB databases. - /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. + /// If more than one version is installed, returns the highest available. Returns + /// the major version as an integer e.g. 11, 12... /// - public class LocalDb + /// Thrown when LocalDb is not available. + public int Version { - private int _version; - private bool _hasVersion; - private string? _exe; - - #region Availability & Version - - /// - /// Gets the LocalDb installed version. - /// - /// If more than one version is installed, returns the highest available. Returns - /// the major version as an integer e.g. 11, 12... - /// Thrown when LocalDb is not available. - public int Version + get { - get + EnsureVersion(); + if (_version <= 0) { - EnsureVersion(); - if (_version <= 0) - throw new InvalidOperationException("LocalDb is not available."); - return _version; - } - } - - /// - /// Ensures that the LocalDb version is detected. - /// - private void EnsureVersion() - { - if (_hasVersion) return; - DetectVersion(); - _hasVersion = true; - } - - /// - /// Gets a value indicating whether LocalDb is available. - /// - public bool IsAvailable - { - get - { - EnsureVersion(); - return _version > 0; - } - } - - /// - /// Ensures that LocalDb is available. - /// - /// Thrown when LocalDb is not available. - private void EnsureAvailable() - { - if (IsAvailable == false) throw new InvalidOperationException("LocalDb is not available."); - } - - /// - /// Detects LocalDb installed version. - /// - /// If more than one version is installed, the highest available is detected. - private void DetectVersion() - { - _hasVersion = true; - _version = -1; - _exe = null; - - var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); - - // MS SQL Server installs in e.g. "C:\Program Files\Microsoft SQL Server", so - // we want to detect it in "%ProgramFiles%\Microsoft SQL Server" - however, if - // Umbraco runs as a 32bits process (e.g. IISExpress configured as 32bits) - // on a 64bits system, %ProgramFiles% will point to "C:\Program Files (x86)" - // and SQL Server cannot be found. But then, %ProgramW6432% will point to - // the original "C:\Program Files". Using it to fix the path. - // see also: MSDN doc for WOW64 implementation - // - var programW6432 = Environment.GetEnvironmentVariable("ProgramW6432"); - if (string.IsNullOrWhiteSpace(programW6432) == false && programW6432 != programFiles) - programFiles = programW6432; - - if (string.IsNullOrWhiteSpace(programFiles)) return; - - // detect 15, 14, 13, 12, 11 - for (var i = 15; i > 10; i--) - { - var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe"); - if (File.Exists(exe) == false) continue; - _version = i; - _exe = exe; - break; } + + return _version; } + } - #endregion - - #region Instances - - /// - /// Gets the name of existing LocalDb instances. - /// - /// The name of existing LocalDb instances. - /// Thrown when LocalDb is not available. - public string[]? GetInstances() + /// + /// Ensures that the LocalDb version is detected. + /// + private void EnsureVersion() + { + if (_hasVersion) { - EnsureAvailable(); - var rc = ExecuteSqlLocalDb("i", out var output, out var error); // info - if (rc != 0 || error != string.Empty) return null; - return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + return; } - /// - /// Gets a value indicating whether a LocalDb instance exists. - /// - /// The name of the instance. - /// A value indicating whether a LocalDb instance with the specified name exists. - /// Thrown when LocalDb is not available. - public bool InstanceExists(string instanceName) + DetectVersion(); + _hasVersion = true; + } + + /// + /// Gets a value indicating whether LocalDb is available. + /// + public bool IsAvailable + { + get { - EnsureAvailable(); - var instances = GetInstances(); - return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); + EnsureVersion(); + return _version > 0; } + } - /// - /// Creates a LocalDb instance. - /// - /// The name of the instance. - /// A value indicating whether the instance was created without errors. - /// Thrown when LocalDb is not available. - public bool CreateInstance(string instanceName) + /// + /// Ensures that LocalDb is available. + /// + /// Thrown when LocalDb is not available. + private void EnsureAvailable() + { + if (IsAvailable == false) { - EnsureAvailable(); - return ExecuteSqlLocalDb($"c \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + throw new InvalidOperationException("LocalDb is not available."); + } + } + + /// + /// Detects LocalDb installed version. + /// + /// If more than one version is installed, the highest available is detected. + private void DetectVersion() + { + _hasVersion = true; + _version = -1; + _exe = null; + + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + + // MS SQL Server installs in e.g. "C:\Program Files\Microsoft SQL Server", so + // we want to detect it in "%ProgramFiles%\Microsoft SQL Server" - however, if + // Umbraco runs as a 32bits process (e.g. IISExpress configured as 32bits) + // on a 64bits system, %ProgramFiles% will point to "C:\Program Files (x86)" + // and SQL Server cannot be found. But then, %ProgramW6432% will point to + // the original "C:\Program Files". Using it to fix the path. + // see also: MSDN doc for WOW64 implementation + // + var programW6432 = Environment.GetEnvironmentVariable("ProgramW6432"); + if (string.IsNullOrWhiteSpace(programW6432) == false && programW6432 != programFiles) + { + programFiles = programW6432; + } + + if (string.IsNullOrWhiteSpace(programFiles)) + { + return; + } + + // detect 15, 14, 13, 12, 11 + for (var i = 15; i > 10; i--) + { + var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe"); + if (File.Exists(exe) == false) + { + continue; + } + + _version = i; + _exe = exe; + break; + } + } + + #endregion + + #region Instances + + /// + /// Gets the name of existing LocalDb instances. + /// + /// The name of existing LocalDb instances. + /// Thrown when LocalDb is not available. + public string[]? GetInstances() + { + EnsureAvailable(); + var rc = ExecuteSqlLocalDb("i", out var output, out var error); // info + if (rc != 0 || error != string.Empty) + { + return null; + } + + return output.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Gets a value indicating whether a LocalDb instance exists. + /// + /// The name of the instance. + /// A value indicating whether a LocalDb instance with the specified name exists. + /// Thrown when LocalDb is not available. + public bool InstanceExists(string instanceName) + { + EnsureAvailable(); + var instances = GetInstances(); + return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Creates a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was created without errors. + /// Thrown when LocalDb is not available. + public bool CreateInstance(string instanceName) + { + EnsureAvailable(); + return ExecuteSqlLocalDb($"c \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + } + + /// + /// Drops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was dropped without errors. + /// Thrown when LocalDb is not available. + /// + /// When an instance is dropped all the attached database files are deleted. + /// Successful if the instance does not exist. + /// + public bool DropInstance(string instanceName) + { + EnsureAvailable(); + Instance? instance = GetInstance(instanceName); + if (instance == null) + { + return true; + } + + instance.DropDatabases(); // else the files remain + + // -i force NOWAIT, -k kills + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty + && ExecuteSqlLocalDb($"d \"{instanceName}\"", out _, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was stopped without errors. + /// Thrown when LocalDb is not available. + /// + /// Successful if the instance does not exist. + /// + public bool StopInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) + { + return true; + } + + // -i force NOWAIT, -k kills + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was started without errors. + /// Thrown when LocalDb is not available. + /// + /// Failed if the instance does not exist. + /// + public bool StartInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) + { + return false; + } + + return ExecuteSqlLocalDb($"s \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + } + + /// + /// Gets a LocalDb instance. + /// + /// The name of the instance. + /// The instance with the specified name if it exists, otherwise null. + /// Thrown when LocalDb is not available. + public Instance? GetInstance(string instanceName) + { + EnsureAvailable(); + return InstanceExists(instanceName) ? new Instance(instanceName) : null; + } + + #endregion + + #region Databases + + /// + /// Represents a LocalDb instance. + /// + /// + /// LocalDb is assumed to be available, and the instance is assumed to exist. + /// + public class Instance + { + private readonly string _masterCstr; + + /// + /// Initializes a new instance of the class. + /// + /// + public Instance(string instanceName) + { + InstanceName = instanceName; + _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; } /// - /// Drops a LocalDb instance. + /// Gets the name of the instance. /// - /// The name of the instance. - /// A value indicating whether the instance was dropped without errors. - /// Thrown when LocalDb is not available. + public string InstanceName { get; } + + public static string GetConnectionString(string instanceName, string databaseName) => + $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; + + /// + /// Gets a LocalDb connection string. + /// + /// The name of the database. + /// The connection string for the specified database. /// - /// When an instance is dropped all the attached database files are deleted. - /// Successful if the instance does not exist. + /// The database should exist in the LocalDb instance. /// - public bool DropInstance(string instanceName) - { - EnsureAvailable(); - var instance = GetInstance(instanceName); - if (instance == null) return true; - instance.DropDatabases(); // else the files remain - - // -i force NOWAIT, -k kills - return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty - && ExecuteSqlLocalDb($"d \"{instanceName}\"", out _, out error) == 0 && error == string.Empty; - } + public string GetConnectionString(string databaseName) => _masterCstr + $@"Database={databaseName};"; /// - /// Stops a LocalDb instance. + /// Gets a LocalDb connection string for an attached database. /// - /// The name of the instance. - /// A value indicating whether the instance was stopped without errors. - /// Thrown when LocalDb is not available. + /// The name of the database. + /// The directory containing database files. + /// The connection string for the specified database. /// - /// Successful if the instance does not exist. + /// The database should not exist in the LocalDb instance. + /// It will be attached with its name being its MDF filename (full path), uppercased, when + /// the first connection is opened, and remain attached until explicitly detached. /// - public bool StopInstance(string instanceName) + public string GetAttachedConnectionString(string databaseName, string filesPath) { - EnsureAvailable(); - if (InstanceExists(instanceName) == false) return true; + GetDatabaseFiles(databaseName, filesPath, out _, out _, out _, out var mdfFilename, out _); - // -i force NOWAIT, -k kills - return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty; + return _masterCstr + $@"AttachDbFileName='{mdfFilename}';"; } /// - /// Stops a LocalDb instance. + /// Gets the name of existing databases. /// - /// The name of the instance. - /// A value indicating whether the instance was started without errors. - /// Thrown when LocalDb is not available. - /// - /// Failed if the instance does not exist. - /// - public bool StartInstance(string instanceName) + /// The name of existing databases. + public string[] GetDatabases() { - EnsureAvailable(); - if (InstanceExists(instanceName) == false) return false; - return ExecuteSqlLocalDb($"s \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; - } + var userDatabases = new List(); - /// - /// Gets a LocalDb instance. - /// - /// The name of the instance. - /// The instance with the specified name if it exists, otherwise null. - /// Thrown when LocalDb is not available. - public Instance? GetInstance(string instanceName) - { - EnsureAvailable(); - return InstanceExists(instanceName) ? new Instance(instanceName) : null; - } - - #endregion - - #region Databases - - /// - /// Represents a LocalDb instance. - /// - /// - /// LocalDb is assumed to be available, and the instance is assumed to exist. - /// - public class Instance - { - private readonly string _masterCstr; - - /// - /// Gets the name of the instance. - /// - public string InstanceName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// - public Instance(string instanceName) + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - InstanceName = instanceName; - _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; - } + conn.Open(); - public static string GetConnectionString(string instanceName, string databaseName) - { - return $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; - } + var databases = new Dictionary(); - /// - /// Gets a LocalDb connection string. - /// - /// The name of the database. - /// The connection string for the specified database. - /// - /// The database should exist in the LocalDb instance. - /// - public string GetConnectionString(string databaseName) - { - return _masterCstr + $@"Database={databaseName};"; - } - - /// - /// Gets a LocalDb connection string for an attached database. - /// - /// The name of the database. - /// The directory containing database files. - /// The connection string for the specified database. - /// - /// The database should not exist in the LocalDb instance. - /// It will be attached with its name being its MDF filename (full path), uppercased, when - /// the first connection is opened, and remain attached until explicitly detached. - /// - public string GetAttachedConnectionString(string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, out _, out _, out _, out var mdfFilename, out _); - - return _masterCstr + $@"AttachDbFileName='{mdfFilename}';"; - } - - /// - /// Gets the name of existing databases. - /// - /// The name of existing databases. - public string[] GetDatabases() - { - var userDatabases = new List(); - - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var databases = new Dictionary(); - - SetCommand(cmd, @" - SELECT name, filename FROM sys.sysdatabases"); - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - databases[reader.GetString(0)] = reader.GetString(1); - } - } - - foreach (var database in databases) - { - var dbname = database.Key; - - if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") - continue; - - // TODO: shall we deal with stale databases? - // TODO: is it always ok to assume file names? - //var mdf = database.Value; - //var ldf = mdf.Replace(".mdf", "_log.ldf"); - //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) - // continue; - - //ExecuteDropDatabase(cmd, dbname, mdf, ldf); - //count++; - - userDatabases.Add(dbname); - } - } - - return userDatabases.ToArray(); - } - - /// - /// Gets a value indicating whether a database exists. - /// - /// The name of the database. - /// A value indicating whether a database with the specified name exists. - /// - /// A database exists if it is registered in the instance, and its files exist. If the database - /// is registered but some of its files are missing, the database is dropped. - /// - public bool DatabaseExists(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) return false; - - // it can exist, even though its files have been deleted - // if files exist assume all is ok (should we try to connect?) - var ldf = GetLogFilename(mdf); - if (File.Exists(mdf) && File.Exists(ldf)) - return true; - - ExecuteDropDatabase(cmd, databaseName, mdf, ldf); - } - - return false; - } - - /// - /// Creates a new database. - /// - /// The name of the database. - /// The directory containing database files. - /// A value indicating whether the database was created without errors. - /// - /// Failed if a database with the specified name already exists in the instance, - /// or if the database files already exist in the specified directory. - /// - public bool CreateDatabase(string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename, out var ldfFilename); - - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf != null) return false; - - // cannot use parameters on CREATE DATABASE - // ie "CREATE DATABASE @0 ..." does not work - SetCommand(cmd, $@" - CREATE DATABASE {QuotedName(databaseName)} - ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) - LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')})"); - - var unused = cmd.ExecuteNonQuery(); - } - return true; - } - - /// - /// Drops a database. - /// - /// The name of the database. - /// A value indicating whether the database was dropped without errors. - /// - /// Successful if the database does not exist. - /// Deletes the database files. - /// - public bool DropDatabase(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - SetCommand(cmd, @" - SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", - databaseName); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) return true; - - ExecuteDropDatabase(cmd, databaseName, mdf); - } - - return true; - } - - /// - /// Drops stale databases. - /// - /// The number of databases that were dropped. - /// - /// A database is considered stale when its files cannot be found. - /// - public int DropStaleDatabases() - { - return DropDatabases(true); - } - - /// - /// Drops databases. - /// - /// A value indicating whether to delete only stale database. - /// The number of databases that were dropped. - /// - /// A database is considered stale when its files cannot be found. - /// - public int DropDatabases(bool staleOnly = false) - { - var count = 0; - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var databases = new Dictionary(); - - SetCommand(cmd, @" - SELECT name, filename FROM sys.sysdatabases"); - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - databases[reader.GetString(0)] = reader.GetString(1); - } - } - - foreach (var database in databases) - { - var dbname = database.Key; - - if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") - continue; - - var mdf = database.Value; - var ldf = mdf.Replace(".mdf", "_log.ldf"); - if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) - continue; - - ExecuteDropDatabase(cmd, dbname, mdf, ldf); - count++; - } - } - - return count; - } - - /// - /// Detaches a database. - /// - /// The name of the database. - /// The directory containing the database files. - /// Thrown when a database with the specified name does not exist. - public string? DetachDatabase(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) - throw new InvalidOperationException("Database does not exist."); - - DetachDatabase(cmd, databaseName); - - return Path.GetDirectoryName(mdf); - } - } - - /// - /// Attaches a database. - /// - /// The name of the database. - /// The directory containing database files. - /// Thrown when a database with the specified name already exists. - public void AttachDatabase(string databaseName, string filesPath) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf != null) - throw new InvalidOperationException("Database already exists."); - - AttachDatabase(cmd, databaseName, filesPath); - } - } - - /// - /// Gets the file names of a database. - /// - /// The name of the database. - /// The MDF logical name. - /// The LDF logical name. - /// The MDF filename. - /// The LDF filename. - public void GetFilenames(string databaseName, - out string? mdfName, out string? ldfName, - out string? mdfFilename, out string? ldfFilename) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); - } - } - - /// - /// Kills all existing connections. - /// - /// The name of the database. - public void KillConnections(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - SetCommand(cmd, @" - DECLARE @sql VARCHAR(MAX); - SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' - FROM master.sys.sysprocesses - WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; - EXEC(@sql);", - databaseName); - cmd.ExecuteNonQuery(); - } - } - - /// - /// Gets a database. - /// - /// The Sql Command. - /// The name of the database. - /// The full filename of the MDF file, if the database exists, otherwise null. - private static string? GetDatabase(SqlCommand cmd, string databaseName) - { SetCommand(cmd, @" - SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", - databaseName); + SELECT name, filename FROM sys.sysdatabases"); - string? mdf = null; - using (var reader = cmd.ExecuteReader()) + using (SqlDataReader? reader = cmd.ExecuteReader()) { - if (reader.Read()) - mdf = reader.GetString(1) ?? string.Empty; while (reader.Read()) { + databases[reader.GetString(0)] = reader.GetString(1); } } - return mdf; - } - - /// - /// Drops a database and its files. - /// - /// The Sql command. - /// The name of the database. - /// The name of the database (MDF) file. - /// The name of the log (LDF) file. - private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string? ldf = null) - { - try + foreach (KeyValuePair database in databases) { - // cannot use parameters on ALTER DATABASE - // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, $@" - ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); + var dbname = database.Key; - var unused1 = cmd.ExecuteNonQuery(); + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + { + continue; + } + + // TODO: shall we deal with stale databases? + // TODO: is it always ok to assume file names? + //var mdf = database.Value; + //var ldf = mdf.Replace(".mdf", "_log.ldf"); + //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + // continue; + + //ExecuteDropDatabase(cmd, dbname, mdf, ldf); + //count++; + + userDatabases.Add(dbname); } - catch (SqlException e) + } + + return userDatabases.ToArray(); + } + + /// + /// Gets a value indicating whether a database exists. + /// + /// The name of the database. + /// A value indicating whether a database with the specified name exists. + /// + /// A database exists if it is registered in the instance, and its files exist. If the database + /// is registered but some of its files are missing, the database is dropped. + /// + public bool DatabaseExists(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) { - if (e.Message.Contains("Unable to open the physical file") && e.Message.Contains("Operating system error 2:")) - { - // quite probably, the files were missing - // yet, it should be possible to drop the database anyways - // but we'll have to deal with the files - } - else - { - // no idea, throw - throw; - } + return false; } - // cannot use parameters on DROP DATABASE - // ie "DROP DATABASE @0 ..." does not work - SetCommand(cmd, $@" - DROP DATABASE {QuotedName(databaseName)}"); + // it can exist, even though its files have been deleted + // if files exist assume all is ok (should we try to connect?) + var ldf = GetLogFilename(mdf); + if (File.Exists(mdf) && File.Exists(ldf)) + { + return true; + } - var unused2 = cmd.ExecuteNonQuery(); - - // be absolutely sure - if (File.Exists(mdf)) File.Delete(mdf); - ldf = ldf ?? GetLogFilename(mdf); - if (File.Exists(ldf)) File.Delete(ldf); + ExecuteDropDatabase(cmd, databaseName, mdf, ldf); } - /// - /// Gets the log (LDF) filename corresponding to a database (MDF) filename. - /// - /// The MDF filename. - /// - private static string GetLogFilename(string mdfFilename) + return false; + } + + /// + /// Creates a new database. + /// + /// The name of the database. + /// The directory containing database files. + /// A value indicating whether the database was created without errors. + /// + /// Failed if a database with the specified name already exists in the instance, + /// or if the database files already exist in the specified directory. + /// + public bool CreateDatabase(string databaseName, string filesPath) + { + GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename, + out var ldfFilename); + + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - if (mdfFilename.EndsWith(".mdf") == false) - throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename)); - return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; - } + conn.Open(); - /// - /// Detaches a database. - /// - /// The Sql command. - /// The name of the database. - private static void DetachDatabase(SqlCommand cmd, string databaseName) - { - // cannot use parameters on ALTER DATABASE - // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, $@" - ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); - - var unused1 = cmd.ExecuteNonQuery(); - - SetCommand(cmd, @" - EXEC sp_detach_db @dbname=@0", - databaseName); - - var unused2 = cmd.ExecuteNonQuery(); - } - - /// - /// Attaches a database. - /// - /// The Sql command. - /// The name of the database. - /// The directory containing database files. - private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, - out var logName, out _, out _, out var mdfFilename, out var ldfFilename); + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + { + return false; + } // cannot use parameters on CREATE DATABASE // ie "CREATE DATABASE @0 ..." does not work SetCommand(cmd, $@" CREATE DATABASE {QuotedName(databaseName)} ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) - LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')}) - FOR ATTACH"); + LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')})"); var unused = cmd.ExecuteNonQuery(); } - /// - /// Sets a database command. - /// - /// The command. - /// The command text. - /// The command arguments. - /// - /// The command text must refer to arguments as @0, @1... each referring - /// to the corresponding position in . - /// - private static void SetCommand(SqlCommand cmd, string sql, params object[] args) - { - cmd.CommandType = CommandType.Text; - cmd.CommandText = sql; - cmd.Parameters.Clear(); - for (var i = 0; i < args.Length; i++) - cmd.Parameters.AddWithValue("@" + i, args[i]); - } + return true; + } - /// - /// Gets the file names of a database. - /// - /// The Sql command. - /// The name of the database. - /// The MDF logical name. - /// The LDF logical name. - /// The MDF filename. - /// The LDF filename. - private void GetFilenames(SqlCommand cmd, string databaseName, - out string? mdfName, out string? ldfName, - out string? mdfFilename, out string? ldfFilename) + /// + /// Drops a database. + /// + /// The name of the database. + /// A value indicating whether the database was dropped without errors. + /// + /// Successful if the database does not exist. + /// Deletes the database files. + /// + public bool DropDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - mdfName = ldfName = mdfFilename = ldfFilename = null; + conn.Open(); SetCommand(cmd, @" - SELECT DB_NAME(database_id), type_desc, name, physical_name - FROM master.sys.master_files - WHERE database_id=DB_ID(@0)", + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", databaseName); - using (var reader = cmd.ExecuteReader()) + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + { + return true; + } + + ExecuteDropDatabase(cmd, databaseName, mdf); + } + + return true; + } + + /// + /// Drops stale databases. + /// + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropStaleDatabases() => DropDatabases(true); + + /// + /// Drops databases. + /// + /// A value indicating whether to delete only stale database. + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropDatabases(bool staleOnly = false) + { + var count = 0; + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (SqlDataReader? reader = cmd.ExecuteReader()) { while (reader.Read()) { - var type = reader.GetString(1); - if (type == "ROWS") - { - mdfName = reader.GetString(2); - ldfName = reader.GetString(3); - } - else if (type == "LOG") - { - ldfName = reader.GetString(2); - ldfFilename = reader.GetString(3); - } + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (KeyValuePair database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + { + continue; + } + + var mdf = database.Value; + var ldf = mdf.Replace(".mdf", "_log.ldf"); + if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + { + continue; + } + + ExecuteDropDatabase(cmd, dbname, mdf, ldf); + count++; + } + } + + return count; + } + + /// + /// Detaches a database. + /// + /// The name of the database. + /// The directory containing the database files. + /// Thrown when a database with the specified name does not exist. + public string? DetachDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + { + throw new InvalidOperationException("Database does not exist."); + } + + DetachDatabase(cmd, databaseName); + + return Path.GetDirectoryName(mdf); + } + } + + /// + /// Attaches a database. + /// + /// The name of the database. + /// The directory containing database files. + /// Thrown when a database with the specified name already exists. + public void AttachDatabase(string databaseName, string filesPath) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + { + throw new InvalidOperationException("Database already exists."); + } + + AttachDatabase(cmd, databaseName, filesPath); + } + } + + /// + /// Gets the file names of a database. + /// + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + public void GetFilenames(string databaseName, + out string? mdfName, out string? ldfName, + out string? mdfFilename, out string? ldfFilename) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); + } + } + + /// + /// Kills all existing connections. + /// + /// The name of the database. + public void KillConnections(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + DECLARE @sql VARCHAR(MAX); + SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' + FROM master.sys.sysprocesses + WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; + EXEC(@sql);", + databaseName); + cmd.ExecuteNonQuery(); + } + } + + /// + /// Gets a database. + /// + /// The Sql Command. + /// The name of the database. + /// The full filename of the MDF file, if the database exists, otherwise null. + private static string? GetDatabase(SqlCommand cmd, string databaseName) + { + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + string? mdf = null; + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + mdf = reader.GetString(1) ?? string.Empty; + } + + while (reader.Read()) + { + } + } + + return mdf; + } + + /// + /// Drops a database and its files. + /// + /// The Sql command. + /// The name of the database. + /// The name of the database (MDF) file. + /// The name of the log (LDF) file. + private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string? ldf = null) + { + try + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, $@" + ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); + + var unused1 = cmd.ExecuteNonQuery(); + } + catch (SqlException e) + { + if (e.Message.Contains("Unable to open the physical file") && + e.Message.Contains("Operating system error 2:")) + { + // quite probably, the files were missing + // yet, it should be possible to drop the database anyways + // but we'll have to deal with the files + } + else + { + // no idea, throw + throw; + } + } + + // cannot use parameters on DROP DATABASE + // ie "DROP DATABASE @0 ..." does not work + SetCommand(cmd, $@" + DROP DATABASE {QuotedName(databaseName)}"); + + var unused2 = cmd.ExecuteNonQuery(); + + // be absolutely sure + if (File.Exists(mdf)) + { + File.Delete(mdf); + } + + ldf = ldf ?? GetLogFilename(mdf); + if (File.Exists(ldf)) + { + File.Delete(ldf); + } + } + + /// + /// Gets the log (LDF) filename corresponding to a database (MDF) filename. + /// + /// The MDF filename. + /// + private static string GetLogFilename(string mdfFilename) + { + if (mdfFilename.EndsWith(".mdf") == false) + { + throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename)); + } + + return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; + } + + /// + /// Detaches a database. + /// + /// The Sql command. + /// The name of the database. + private static void DetachDatabase(SqlCommand cmd, string databaseName) + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, $@" + ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); + + var unused1 = cmd.ExecuteNonQuery(); + + SetCommand(cmd, @" + EXEC sp_detach_db @dbname=@0", + databaseName); + + var unused2 = cmd.ExecuteNonQuery(); + } + + /// + /// Attaches a database. + /// + /// The Sql command. + /// The name of the database. + /// The directory containing database files. + private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) + { + GetDatabaseFiles(databaseName, filesPath, + out var logName, out _, out _, out var mdfFilename, out var ldfFilename); + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, $@" + CREATE DATABASE {QuotedName(databaseName)} + ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) + LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')}) + FOR ATTACH"); + + var unused = cmd.ExecuteNonQuery(); + } + + /// + /// Sets a database command. + /// + /// The command. + /// The command text. + /// The command arguments. + /// + /// The command text must refer to arguments as @0, @1... each referring + /// to the corresponding position in . + /// + private static void SetCommand(SqlCommand cmd, string sql, params object[] args) + { + cmd.CommandType = CommandType.Text; + cmd.CommandText = sql; + cmd.Parameters.Clear(); + for (var i = 0; i < args.Length; i++) + { + cmd.Parameters.AddWithValue("@" + i, args[i]); + } + } + + /// + /// Gets the file names of a database. + /// + /// The Sql command. + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + private void GetFilenames(SqlCommand cmd, string databaseName, + out string? mdfName, out string? ldfName, + out string? mdfFilename, out string? ldfFilename) + { + mdfName = ldfName = mdfFilename = ldfFilename = null; + + SetCommand(cmd, @" + SELECT DB_NAME(database_id), type_desc, name, physical_name + FROM master.sys.master_files + WHERE database_id=DB_ID(@0)", + databaseName); + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var type = reader.GetString(1); + if (type == "ROWS") + { + mdfName = reader.GetString(2); + ldfName = reader.GetString(3); + } + else if (type == "LOG") + { + ldfName = reader.GetString(2); + ldfFilename = reader.GetString(3); } } } } + } - /// - /// Copy database files. - /// - /// The name of the source database. - /// The directory containing source database files. - /// The name of the target database. - /// The directory containing target database files. - /// The source database files extension. - /// The target database files extension. - /// A value indicating whether to overwrite the target files. - /// A value indicating whether to delete the source files. - /// - /// The , , - /// and parameters are optional. If they result in target being identical - /// to source, no copy is performed. If is false, nothing happens, otherwise the source - /// files are deleted. - /// If target is not identical to source, files are copied or moved, depending on the value of . - /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. - /// - public void CopyDatabaseFiles(string databaseName, string filesPath, - string? targetDatabaseName = null, string? targetFilesPath = null, - string? sourceExtension = null, string? targetExtension = null, - bool overwrite = false, bool delete = false) + /// + /// Copy database files. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The name of the target database. + /// The directory containing target database files. + /// The source database files extension. + /// The target database files extension. + /// A value indicating whether to overwrite the target files. + /// A value indicating whether to delete the source files. + /// + /// The , , + /// + /// and parameters are optional. If they result in target being identical + /// to source, no copy is performed. If is false, nothing happens, otherwise the source + /// files are deleted. + /// If target is not identical to source, files are copied or moved, depending on the value of + /// . + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public void CopyDatabaseFiles(string databaseName, string filesPath, + string? targetDatabaseName = null, string? targetFilesPath = null, + string? sourceExtension = null, string? targetExtension = null, + bool overwrite = false, bool delete = false) + { + var nop = (targetFilesPath == null || targetFilesPath == filesPath) + && (targetDatabaseName == null || targetDatabaseName == databaseName) + && ((sourceExtension == null && targetExtension == null) || sourceExtension == targetExtension); + if (nop && delete == false) { - var nop = (targetFilesPath == null || targetFilesPath == filesPath) - && (targetDatabaseName == null || targetDatabaseName == databaseName) - && (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension); - if (nop && delete == false) return; + return; + } - GetDatabaseFiles(databaseName, filesPath, - out _, out _, out _, out var mdfFilename, out var ldfFilename); + GetDatabaseFiles(databaseName, filesPath, + out _, out _, out _, out var mdfFilename, out var ldfFilename); - if (sourceExtension != null) + if (sourceExtension != null) + { + mdfFilename += "." + sourceExtension; + ldfFilename += "." + sourceExtension; + } + + if (nop) + { + // delete + if (File.Exists(mdfFilename)) { - mdfFilename += "." + sourceExtension; - ldfFilename += "." + sourceExtension; + File.Delete(mdfFilename); } - if (nop) + if (File.Exists(ldfFilename)) { - // delete - if (File.Exists(mdfFilename)) File.Delete(mdfFilename); - if (File.Exists(ldfFilename)) File.Delete(ldfFilename); + File.Delete(ldfFilename); + } + } + else + { + // copy or copy+delete ie move + GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, + out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename); + + if (targetExtension != null) + { + targetMdfFilename += "." + targetExtension; + targetLdfFilename += "." + targetExtension; + } + + if (delete) + { + if (overwrite && File.Exists(targetMdfFilename)) + { + File.Delete(targetMdfFilename); + } + + if (overwrite && File.Exists(targetLdfFilename)) + { + File.Delete(targetLdfFilename); + } + + File.Move(mdfFilename, targetMdfFilename); + File.Move(ldfFilename, targetLdfFilename); } else { - // copy or copy+delete ie move - GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, - out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename); - - if (targetExtension != null) - { - targetMdfFilename += "." + targetExtension; - targetLdfFilename += "." + targetExtension; - } - - if (delete) - { - if (overwrite && File.Exists(targetMdfFilename)) File.Delete(targetMdfFilename); - if (overwrite && File.Exists(targetLdfFilename)) File.Delete(targetLdfFilename); - File.Move(mdfFilename, targetMdfFilename); - File.Move(ldfFilename, targetLdfFilename); - } - else - { - File.Copy(mdfFilename, targetMdfFilename, overwrite); - File.Copy(ldfFilename, targetLdfFilename, overwrite); - } + File.Copy(mdfFilename, targetMdfFilename, overwrite); + File.Copy(ldfFilename, targetLdfFilename, overwrite); } } - - /// - /// Gets a value indicating whether database files exist. - /// - /// The name of the source database. - /// The directory containing source database files. - /// The database files extension. - /// A value indicating whether the database files exist. - /// - /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. - /// - public bool DatabaseFilesExist(string databaseName, string filesPath, string? extension = null) - { - GetDatabaseFiles(databaseName, filesPath, - out _, out _, out _, out var mdfFilename, out var ldfFilename); - - if (extension != null) - { - mdfFilename += "." + extension; - ldfFilename += "." + extension; - } - - return File.Exists(mdfFilename) && File.Exists(ldfFilename); - } - - /// - /// Gets the name of the database files. - /// - /// The name of the database. - /// The directory containing database files. - /// The name of the log. - /// The base filename (the MDF filename without the .mdf extension). - /// The base log filename (the LDF filename without the .ldf extension). - /// The MDF filename. - /// The LDF filename. - private static void GetDatabaseFiles(string databaseName, string filesPath, - out string logName, - out string baseFilename, out string baseLogFilename, - out string mdfFilename, out string ldfFilename) - { - logName = databaseName + "_log"; - baseFilename = Path.Combine(filesPath, databaseName); - baseLogFilename = Path.Combine(filesPath, logName); - mdfFilename = baseFilename + ".mdf"; - ldfFilename = baseFilename + "_log.ldf"; - } - - #endregion - - #region SqlLocalDB - - /// - /// Executes the SqlLocalDB command. - /// - /// The arguments. - /// The command standard output. - /// The command error output. - /// The process exit code. - /// - /// Execution is successful if the exit code is zero, and error is empty. - /// - private int ExecuteSqlLocalDb(string args, out string output, out string error) - { - if (_exe == null) // should never happen - we should not execute if not available - { - output = string.Empty; - error = "SqlLocalDB.exe not found"; - return -1; - } - - using (var p = new Process - { - StartInfo = - { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - FileName = _exe, - Arguments = args, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden - } - }) - { - p.Start(); - output = p.StandardOutput.ReadToEnd(); - error = p.StandardError.ReadToEnd(); - p.WaitForExit(); - - return p.ExitCode; - } - - } - - /// - /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited identifier. - /// - /// The name to quote. - /// A quote character. - /// - /// - /// This is a C# implementation of T-SQL QUOTEDNAME. - /// is optional, it can be '[' (default), ']', '\'' or '"'. - /// - internal static string QuotedName(string name, char quote = '[') - { - switch (quote) - { - case '[': - case ']': - return "[" + name.Replace("]", "]]") + "]"; - case '\'': - return "'" + name.Replace("'", "''") + "'"; - case '"': - return "\"" + name.Replace("\"", "\"\"") + "\""; - default: - throw new NotSupportedException("Not a valid quote character."); - } - } - - #endregion } + + /// + /// Gets a value indicating whether database files exist. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The database files extension. + /// A value indicating whether the database files exist. + /// + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public bool DatabaseFilesExist(string databaseName, string filesPath, string? extension = null) + { + GetDatabaseFiles(databaseName, filesPath, + out _, out _, out _, out var mdfFilename, out var ldfFilename); + + if (extension != null) + { + mdfFilename += "." + extension; + ldfFilename += "." + extension; + } + + return File.Exists(mdfFilename) && File.Exists(ldfFilename); + } + + /// + /// Gets the name of the database files. + /// + /// The name of the database. + /// The directory containing database files. + /// The name of the log. + /// The base filename (the MDF filename without the .mdf extension). + /// The base log filename (the LDF filename without the .ldf extension). + /// The MDF filename. + /// The LDF filename. + private static void GetDatabaseFiles(string databaseName, string filesPath, + out string logName, + out string baseFilename, out string baseLogFilename, + out string mdfFilename, out string ldfFilename) + { + logName = databaseName + "_log"; + baseFilename = Path.Combine(filesPath, databaseName); + baseLogFilename = Path.Combine(filesPath, logName); + mdfFilename = baseFilename + ".mdf"; + ldfFilename = baseFilename + "_log.ldf"; + } + + #endregion + + #region SqlLocalDB + + /// + /// Executes the SqlLocalDB command. + /// + /// The arguments. + /// The command standard output. + /// The command error output. + /// The process exit code. + /// + /// Execution is successful if the exit code is zero, and error is empty. + /// + private int ExecuteSqlLocalDb(string args, out string output, out string error) + { + if (_exe == null) // should never happen - we should not execute if not available + { + output = string.Empty; + error = "SqlLocalDB.exe not found"; + return -1; + } + + using (var p = new Process + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = _exe, + Arguments = args, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + } + }) + { + p.Start(); + output = p.StandardOutput.ReadToEnd(); + error = p.StandardError.ReadToEnd(); + p.WaitForExit(); + + return p.ExitCode; + } + } + + /// + /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited + /// identifier. + /// + /// The name to quote. + /// A quote character. + /// + /// + /// This is a C# implementation of T-SQL QUOTEDNAME. + /// is optional, it can be '[' (default), ']', '\'' or '"'. + /// + internal static string QuotedName(string name, char quote = '[') + { + switch (quote) + { + case '[': + case ']': + return "[" + name.Replace("]", "]]") + "]"; + case '\'': + return "'" + name.Replace("'", "''") + "'"; + case '"': + return "\"" + name.Replace("\"", "\"\"") + "\""; + default: + throw new NotSupportedException("Not a valid quote character."); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs index 8d88b2d7df..1d3abdac14 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs @@ -1,25 +1,23 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(PublicAccessEntry))] +public sealed class AccessMapper : BaseMapper { - - [MapperFor(typeof(PublicAccessEntry))] - public sealed class AccessMapper : BaseMapper + public AccessMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public AccessMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(PublicAccessEntry.Key), nameof(AccessDto.Id)); - DefineMap(nameof(PublicAccessEntry.LoginNodeId), nameof(AccessDto.LoginNodeId)); - DefineMap(nameof(PublicAccessEntry.NoAccessNodeId), nameof(AccessDto.NoAccessNodeId)); - DefineMap(nameof(PublicAccessEntry.ProtectedNodeId), nameof(AccessDto.NodeId)); - DefineMap(nameof(PublicAccessEntry.CreateDate), nameof(AccessDto.CreateDate)); - DefineMap(nameof(PublicAccessEntry.UpdateDate), nameof(AccessDto.UpdateDate)); - } + protected override void DefineMaps() + { + DefineMap(nameof(PublicAccessEntry.Key), nameof(AccessDto.Id)); + DefineMap(nameof(PublicAccessEntry.LoginNodeId), nameof(AccessDto.LoginNodeId)); + DefineMap(nameof(PublicAccessEntry.NoAccessNodeId), nameof(AccessDto.NoAccessNodeId)); + DefineMap(nameof(PublicAccessEntry.ProtectedNodeId), nameof(AccessDto.NodeId)); + DefineMap(nameof(PublicAccessEntry.CreateDate), nameof(AccessDto.CreateDate)); + DefineMap(nameof(PublicAccessEntry.UpdateDate), nameof(AccessDto.UpdateDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs index 25bf413cc9..6f27cf830f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs @@ -1,31 +1,30 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a mapper for audit entry entities. - /// - [MapperFor(typeof(IAuditEntry))] - [MapperFor(typeof(AuditEntry))] - public sealed class AuditEntryMapper : BaseMapper - { - public AuditEntryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(AuditEntry.Id), nameof(AuditEntryDto.Id)); - DefineMap(nameof(AuditEntry.PerformingUserId), nameof(AuditEntryDto.PerformingUserId)); - DefineMap(nameof(AuditEntry.PerformingDetails), nameof(AuditEntryDto.PerformingDetails)); - DefineMap(nameof(AuditEntry.PerformingIp), nameof(AuditEntryDto.PerformingIp)); - DefineMap(nameof(AuditEntry.EventDateUtc), nameof(AuditEntryDto.EventDateUtc)); - DefineMap(nameof(AuditEntry.AffectedUserId), nameof(AuditEntryDto.AffectedUserId)); - DefineMap(nameof(AuditEntry.AffectedDetails), nameof(AuditEntryDto.AffectedDetails)); - DefineMap(nameof(AuditEntry.EventType), nameof(AuditEntryDto.EventType)); - DefineMap(nameof(AuditEntry.EventDetails), nameof(AuditEntryDto.EventDetails)); - } +/// +/// Represents a mapper for audit entry entities. +/// +[MapperFor(typeof(IAuditEntry))] +[MapperFor(typeof(AuditEntry))] +public sealed class AuditEntryMapper : BaseMapper +{ + public AuditEntryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(AuditEntry.Id), nameof(AuditEntryDto.Id)); + DefineMap(nameof(AuditEntry.PerformingUserId), nameof(AuditEntryDto.PerformingUserId)); + DefineMap(nameof(AuditEntry.PerformingDetails), nameof(AuditEntryDto.PerformingDetails)); + DefineMap(nameof(AuditEntry.PerformingIp), nameof(AuditEntryDto.PerformingIp)); + DefineMap(nameof(AuditEntry.EventDateUtc), nameof(AuditEntryDto.EventDateUtc)); + DefineMap(nameof(AuditEntry.AffectedUserId), nameof(AuditEntryDto.AffectedUserId)); + DefineMap(nameof(AuditEntry.AffectedDetails), nameof(AuditEntryDto.AffectedDetails)); + DefineMap(nameof(AuditEntry.EventType), nameof(AuditEntryDto.EventType)); + DefineMap(nameof(AuditEntry.EventDetails), nameof(AuditEntryDto.EventDetails)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs index 3267a5d14a..c07a309b69 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs @@ -1,25 +1,25 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(AuditItem))] - [MapperFor(typeof(IAuditItem))] - public sealed class AuditItemMapper : BaseMapper - { - public AuditItemMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); - DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); - DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); - // we cannot map that one - because AuditType is an enum but Header is a string - //DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); - DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); - } +[MapperFor(typeof(AuditItem))] +[MapperFor(typeof(IAuditItem))] +public sealed class AuditItemMapper : BaseMapper +{ + public AuditItemMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); + DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); + DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); + + // we cannot map that one - because AuditType is an enum but Header is a string + // DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); + DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs index f046ee9548..f482d7da6d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs @@ -1,82 +1,108 @@ -using System; using System.Collections.Concurrent; +using System.Reflection; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public abstract class BaseMapper { - public abstract class BaseMapper + private readonly object _definedLock = new(); + + private readonly MapperConfigurationStore _maps; + + // note: using a Lazy here because during installs, we are resolving the + // mappers way before we have a configured IUmbracoDatabaseFactory, ie way before we + // have an ISqlContext - this is some nasty temporal coupling which we might want to + // cleanup eventually. + private readonly Lazy _sqlContext; + private bool _defined; + + private ISqlSyntaxProvider? _sqlSyntax; + + protected BaseMapper(Lazy sqlContext, MapperConfigurationStore maps) { - // note: using a Lazy here because during installs, we are resolving the - // mappers way before we have a configured IUmbracoDatabaseFactory, ie way before we - // have an ISqlContext - this is some nasty temporal coupling which we might want to - // cleanup eventually. + _sqlContext = sqlContext; + _maps = maps; + } - private readonly Lazy _sqlContext; - private readonly object _definedLock = new object(); - private readonly MapperConfigurationStore _maps; - - private ISqlSyntaxProvider? _sqlSyntax; - private bool _defined; - - protected BaseMapper(Lazy sqlContext, MapperConfigurationStore maps) + internal string Map(string? propertyName) + { + lock (_definedLock) { - _sqlContext = sqlContext; - _maps = maps; - } - - protected abstract void DefineMaps(); - - internal string Map(string? propertyName) - { - lock (_definedLock) + if (!_defined) { - if (!_defined) + ISqlContext? sqlContext = _sqlContext.Value; + if (sqlContext == null) { - var sqlContext = _sqlContext.Value; - if (sqlContext == null) - throw new InvalidOperationException("Could not get an ISqlContext."); - _sqlSyntax = sqlContext.SqlSyntax; - - DefineMaps(); - - _defined = true; + throw new InvalidOperationException("Could not get an ISqlContext."); } + + _sqlSyntax = sqlContext.SqlSyntax; + + DefineMaps(); + + _defined = true; } - - if (!_maps.TryGetValue(GetType(), out var mapperMaps)) - throw new InvalidOperationException($"No maps defined for mapper {GetType().FullName}."); - if (propertyName is null || !mapperMaps.TryGetValue(propertyName, out var mappedName)) - throw new InvalidOperationException($"No map defined by mapper {GetType().FullName} for property {propertyName}."); - return mappedName; } - // fixme: TSource is used for nothing - protected void DefineMap(string sourceName, string targetName) + if (!_maps.TryGetValue(GetType(), out ConcurrentDictionary? mapperMaps)) { - if (_sqlSyntax == null) - throw new InvalidOperationException("Do not define maps outside of DefineMaps."); - - var targetType = typeof(TTarget); - - // TODO ensure that sourceName is a valid sourceType property (but, slow?) - - var tableNameAttribute = targetType.FirstAttribute(); - if (tableNameAttribute == null) throw new InvalidOperationException($"Type {targetType.FullName} is not marked with a TableName attribute."); - var tableName = tableNameAttribute.Value; - - // TODO maybe get all properties once and then index them - var targetProperty = targetType.GetProperty(targetName); - if (targetProperty == null) throw new InvalidOperationException($"Type {targetType.FullName} does not have a property named {targetName}."); - var columnAttribute = targetProperty.FirstAttribute(); - if (columnAttribute == null) throw new InvalidOperationException($"Property {targetType.FullName}.{targetName} is not marked with a Column attribute."); - - var columnName = columnAttribute.Name; - var columnMap = _sqlSyntax.GetQuotedTableName(tableName) + "." + _sqlSyntax.GetQuotedColumnName(columnName); - - var mapperMaps = _maps.GetOrAdd(GetType(), type => new ConcurrentDictionary()); - mapperMaps[sourceName] = columnMap; + throw new InvalidOperationException($"No maps defined for mapper {GetType().FullName}."); } + + if (propertyName is null || !mapperMaps.TryGetValue(propertyName, out var mappedName)) + { + throw new InvalidOperationException( + $"No map defined by mapper {GetType().FullName} for property {propertyName}."); + } + + return mappedName; + } + + protected abstract void DefineMaps(); + + // fixme: TSource is used for nothing + protected void DefineMap(string sourceName, string targetName) + { + if (_sqlSyntax == null) + { + throw new InvalidOperationException("Do not define maps outside of DefineMaps."); + } + + Type targetType = typeof(TTarget); + + // TODO ensure that sourceName is a valid sourceType property (but, slow?) + TableNameAttribute? tableNameAttribute = targetType.FirstAttribute(); + if (tableNameAttribute == null) + { + throw new InvalidOperationException( + $"Type {targetType.FullName} is not marked with a TableName attribute."); + } + + var tableName = tableNameAttribute.Value; + + // TODO maybe get all properties once and then index them + PropertyInfo? targetProperty = targetType.GetProperty(targetName); + if (targetProperty == null) + { + throw new InvalidOperationException( + $"Type {targetType.FullName} does not have a property named {targetName}."); + } + + ColumnAttribute? columnAttribute = targetProperty.FirstAttribute(); + if (columnAttribute == null) + { + throw new InvalidOperationException( + $"Property {targetType.FullName}.{targetName} is not marked with a Column attribute."); + } + + var columnName = columnAttribute.Name; + var columnMap = _sqlSyntax.GetQuotedTableName(tableName) + "." + _sqlSyntax.GetQuotedColumnName(columnName); + + ConcurrentDictionary mapperMaps = + _maps.GetOrAdd(GetType(), type => new ConcurrentDictionary()); + mapperMaps[sourceName] = columnMap; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs index 884db7c09e..81f3c00c8c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs @@ -1,30 +1,29 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a mapper for consent entities. - /// - [MapperFor(typeof(IConsent))] - [MapperFor(typeof(Consent))] - public sealed class ConsentMapper : BaseMapper - { - public ConsentMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Consent.Id), nameof(ConsentDto.Id)); - DefineMap(nameof(Consent.Current), nameof(ConsentDto.Current)); - DefineMap(nameof(Consent.CreateDate), nameof(ConsentDto.CreateDate)); - DefineMap(nameof(Consent.Source), nameof(ConsentDto.Source)); - DefineMap(nameof(Consent.Context), nameof(ConsentDto.Context)); - DefineMap(nameof(Consent.Action), nameof(ConsentDto.Action)); - DefineMap(nameof(Consent.State), nameof(ConsentDto.State)); - DefineMap(nameof(Consent.Comment), nameof(ConsentDto.Comment)); - } +/// +/// Represents a mapper for consent entities. +/// +[MapperFor(typeof(IConsent))] +[MapperFor(typeof(Consent))] +public sealed class ConsentMapper : BaseMapper +{ + public ConsentMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Consent.Id), nameof(ConsentDto.Id)); + DefineMap(nameof(Consent.Current), nameof(ConsentDto.Current)); + DefineMap(nameof(Consent.CreateDate), nameof(ConsentDto.CreateDate)); + DefineMap(nameof(Consent.Source), nameof(ConsentDto.Source)); + DefineMap(nameof(Consent.Context), nameof(ConsentDto.Context)); + DefineMap(nameof(Consent.Action), nameof(ConsentDto.Action)); + DefineMap(nameof(Consent.State), nameof(ConsentDto.State)); + DefineMap(nameof(Consent.Comment), nameof(ConsentDto.Comment)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs index 77e3b4edc4..9ee4219e04 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs @@ -1,45 +1,44 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Content))] +[MapperFor(typeof(IContent))] +public sealed class ContentMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Content))] - [MapperFor(typeof(IContent))] - public sealed class ContentMapper : BaseMapper + public ContentMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ContentMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Content.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Content.Key), nameof(NodeDto.UniqueId)); + protected override void DefineMaps() + { + DefineMap(nameof(Content.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Content.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); - DefineMap(nameof(Content.Name), nameof(ContentVersionDto.Text)); + DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); + DefineMap(nameof(Content.Name), nameof(ContentVersionDto.Text)); - DefineMap(nameof(Content.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Content.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Content.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Content.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Content.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Content.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Content.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Content.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Content.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Content.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Content.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Content.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Content.ContentTypeId), nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Content.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Content.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Content.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Content.UpdateDate), nameof(ContentVersionDto.VersionDate)); - DefineMap(nameof(Content.Published), nameof(DocumentDto.Published)); + DefineMap(nameof(Content.UpdateDate), nameof(ContentVersionDto.VersionDate)); + DefineMap(nameof(Content.Published), nameof(DocumentDto.Published)); - //DefineMap(nameof(Content.Name), nameof(DocumentDto.Alias)); - //CacheMap(src => src, dto => dto.Newest); - //DefineMap(nameof(Content.Template), nameof(DocumentDto.TemplateId)); - } + // DefineMap(nameof(Content.Name), nameof(DocumentDto.Alias)); + // CacheMap(src => src, dto => dto.Newest); + // DefineMap(nameof(Content.Template), nameof(DocumentDto.TemplateId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs index d24dac2894..48fab6bda1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs @@ -1,40 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(ContentType))] - [MapperFor(typeof(IContentType))] - public sealed class ContentTypeMapper : BaseMapper - { - public ContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(ContentType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(ContentType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(ContentType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(ContentType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(ContentType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(ContentType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(ContentType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(ContentType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(ContentType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(ContentType))] +[MapperFor(typeof(IContentType))] +public sealed class ContentTypeMapper : BaseMapper +{ + public ContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(ContentType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(ContentType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(ContentType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(ContentType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(ContentType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(ContentType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(ContentType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(ContentType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(ContentType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs index 8a84b8b153..e380ff57aa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs @@ -1,35 +1,34 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DataType))] - [MapperFor(typeof(IDataType))] - public sealed class DataTypeMapper : BaseMapper - { - public DataTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(DataType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(DataType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(DataType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(DataType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(DataType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(DataType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(DataType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(DataType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(DataType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(DataType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(DataType.EditorAlias), nameof(DataTypeDto.EditorAlias)); - DefineMap(nameof(DataType.DatabaseType), nameof(DataTypeDto.DbType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DataType))] +[MapperFor(typeof(IDataType))] +public sealed class DataTypeMapper : BaseMapper +{ + public DataTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(DataType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(DataType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(DataType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(DataType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(DataType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(DataType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(DataType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(DataType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(DataType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(DataType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(DataType.EditorAlias), nameof(DataTypeDto.EditorAlias)); + DefineMap(nameof(DataType.DatabaseType), nameof(DataTypeDto.DbType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs index da04c254f8..aea84e5a11 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs @@ -1,27 +1,26 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DictionaryItem))] - [MapperFor(typeof(IDictionaryItem))] - public sealed class DictionaryMapper : BaseMapper - { - public DictionaryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(DictionaryItem.Id), nameof(DictionaryDto.PrimaryKey)); - DefineMap(nameof(DictionaryItem.Key), nameof(DictionaryDto.UniqueId)); - DefineMap(nameof(DictionaryItem.ItemKey), nameof(DictionaryDto.Key)); - DefineMap(nameof(DictionaryItem.ParentId), nameof(DictionaryDto.Parent)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DictionaryItem))] +[MapperFor(typeof(IDictionaryItem))] +public sealed class DictionaryMapper : BaseMapper +{ + public DictionaryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(DictionaryItem.Id), nameof(DictionaryDto.PrimaryKey)); + DefineMap(nameof(DictionaryItem.Key), nameof(DictionaryDto.UniqueId)); + DefineMap(nameof(DictionaryItem.ItemKey), nameof(DictionaryDto.Key)); + DefineMap(nameof(DictionaryItem.ParentId), nameof(DictionaryDto.Parent)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs index ead88959d3..eba2563835 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs @@ -1,27 +1,34 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DictionaryTranslation))] - [MapperFor(typeof(IDictionaryTranslation))] - public sealed class DictionaryTranslationMapper : BaseMapper - { - public DictionaryTranslationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(DictionaryTranslation.Id), nameof(LanguageTextDto.PrimaryKey)); - DefineMap(nameof(DictionaryTranslation.Key), nameof(LanguageTextDto.UniqueId)); - DefineMap(nameof(DictionaryTranslation.Language), nameof(LanguageTextDto.LanguageId)); - DefineMap(nameof(DictionaryTranslation.Value), nameof(LanguageTextDto.Value)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DictionaryTranslation))] +[MapperFor(typeof(IDictionaryTranslation))] +public sealed class DictionaryTranslationMapper : BaseMapper +{ + public DictionaryTranslationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap( + nameof(DictionaryTranslation.Id), + nameof(LanguageTextDto.PrimaryKey)); + DefineMap( + nameof(DictionaryTranslation.Key), + nameof(LanguageTextDto.UniqueId)); + DefineMap( + nameof(DictionaryTranslation.Language), + nameof(LanguageTextDto.LanguageId)); + DefineMap( + nameof(DictionaryTranslation.Value), + nameof(LanguageTextDto.Value)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs index 860d34edbf..2f7b3991d2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs @@ -1,23 +1,22 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IDomain))] - [MapperFor(typeof(UmbracoDomain))] - public sealed class DomainMapper : BaseMapper - { - public DomainMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(UmbracoDomain.Id), nameof(DomainDto.Id)); - DefineMap(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId)); - DefineMap(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage)); - DefineMap(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName)); - } +[MapperFor(typeof(IDomain))] +[MapperFor(typeof(UmbracoDomain))] +public sealed class DomainMapper : BaseMapper +{ + public DomainMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(UmbracoDomain.Id), nameof(DomainDto.Id)); + DefineMap(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId)); + DefineMap(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage)); + DefineMap(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs index 85db7bf553..c587b3ac3a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs @@ -1,25 +1,34 @@ -using System; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IIdentityUserLogin))] - [MapperFor(typeof(IdentityUserLogin))] - public sealed class ExternalLoginMapper : BaseMapper - { - public ExternalLoginMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(IdentityUserLogin.Id), nameof(ExternalLoginDto.Id)); - DefineMap(nameof(IdentityUserLogin.CreateDate), nameof(ExternalLoginDto.CreateDate)); - DefineMap(nameof(IdentityUserLogin.LoginProvider), nameof(ExternalLoginDto.LoginProvider)); - DefineMap(nameof(IdentityUserLogin.ProviderKey), nameof(ExternalLoginDto.ProviderKey)); - DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); - DefineMap(nameof(IdentityUserLogin.UserData), nameof(ExternalLoginDto.UserData)); - } +[MapperFor(typeof(IIdentityUserLogin))] +[MapperFor(typeof(IdentityUserLogin))] +public sealed class ExternalLoginMapper : BaseMapper +{ + public ExternalLoginMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(IdentityUserLogin.Id), nameof(ExternalLoginDto.Id)); + DefineMap( + nameof(IdentityUserLogin.CreateDate), + nameof(ExternalLoginDto.CreateDate)); + DefineMap( + nameof(IdentityUserLogin.LoginProvider), + nameof(ExternalLoginDto.LoginProvider)); + DefineMap( + nameof(IdentityUserLogin.ProviderKey), + nameof(ExternalLoginDto.ProviderKey)); + DefineMap( + nameof(IdentityUserLogin.Key), + nameof(ExternalLoginDto.UserOrMemberKey)); + DefineMap( + nameof(IdentityUserLogin.UserData), + nameof(ExternalLoginDto.UserData)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs index ca8360c626..a344fb9f49 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs @@ -1,25 +1,35 @@ -using System; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IIdentityUserToken))] - [MapperFor(typeof(IdentityUserToken))] - public sealed class ExternalLoginTokenMapper : BaseMapper - { - public ExternalLoginTokenMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(IdentityUserToken.Id), nameof(ExternalLoginTokenDto.Id)); - DefineMap(nameof(IdentityUserToken.CreateDate), nameof(ExternalLoginTokenDto.CreateDate)); - DefineMap(nameof(IdentityUserToken.Name), nameof(ExternalLoginTokenDto.Name)); - DefineMap(nameof(IdentityUserToken.Value), nameof(ExternalLoginTokenDto.Value)); - // separate table - DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); - } +[MapperFor(typeof(IIdentityUserToken))] +[MapperFor(typeof(IdentityUserToken))] +public sealed class ExternalLoginTokenMapper : BaseMapper +{ + public ExternalLoginTokenMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap( + nameof(IdentityUserToken.Id), + nameof(ExternalLoginTokenDto.Id)); + DefineMap( + nameof(IdentityUserToken.CreateDate), + nameof(ExternalLoginTokenDto.CreateDate)); + DefineMap( + nameof(IdentityUserToken.Name), + nameof(ExternalLoginTokenDto.Name)); + DefineMap( + nameof(IdentityUserToken.Value), + nameof(ExternalLoginTokenDto.Value)); + + // separate table + DefineMap( + nameof(IdentityUserLogin.Key), + nameof(ExternalLoginDto.UserOrMemberKey)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs index db2f104ed7..ff70b2e88c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs @@ -1,12 +1,11 @@ -using System; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public interface IMapperCollection : IBuilderCollection { - public interface IMapperCollection : IBuilderCollection - { - bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper); - BaseMapper this[Type type] { get; } - } + BaseMapper this[Type type] { get; } + + bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper); } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs index a133d4066c..05bb514e15 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs @@ -1,22 +1,21 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(KeyValue))] - [MapperFor(typeof(IKeyValue))] - public sealed class KeyValueMapper : BaseMapper - { - public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); - DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); - DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); - } +[MapperFor(typeof(KeyValue))] +[MapperFor(typeof(IKeyValue))] +public sealed class KeyValueMapper : BaseMapper +{ + public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); + DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); + DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs index d4313ad4d5..ed5ef6b224 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs @@ -1,26 +1,25 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(ILanguage))] - [MapperFor(typeof(Language))] - public sealed class LanguageMapper : BaseMapper - { - public LanguageMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Language.Id), nameof(LanguageDto.Id)); - DefineMap(nameof(Language.IsoCode), nameof(LanguageDto.IsoCode)); - DefineMap(nameof(Language.CultureName), nameof(LanguageDto.CultureName)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(ILanguage))] +[MapperFor(typeof(Language))] +public sealed class LanguageMapper : BaseMapper +{ + public LanguageMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Language.Id), nameof(LanguageDto.Id)); + DefineMap(nameof(Language.IsoCode), nameof(LanguageDto.IsoCode)); + DefineMap(nameof(Language.CultureName), nameof(LanguageDto.CultureName)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs index 807e3b6c02..110fe17447 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs @@ -1,22 +1,21 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(ILogViewerQuery))] - [MapperFor(typeof(LogViewerQuery))] - public sealed class LogViewerQueryMapper : BaseMapper - { - public LogViewerQueryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(ILogViewerQuery.Id), nameof(LogViewerQueryDto.Id)); - DefineMap(nameof(ILogViewerQuery.Name), nameof(LogViewerQueryDto.Name)); - DefineMap(nameof(ILogViewerQuery.Query), nameof(LogViewerQueryDto.Query)); - } +[MapperFor(typeof(ILogViewerQuery))] +[MapperFor(typeof(LogViewerQuery))] +public sealed class LogViewerQueryMapper : BaseMapper +{ + public LogViewerQueryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(ILogViewerQuery.Id), nameof(LogViewerQueryDto.Id)); + DefineMap(nameof(ILogViewerQuery.Name), nameof(LogViewerQueryDto.Name)); + DefineMap(nameof(ILogViewerQuery.Query), nameof(LogViewerQueryDto.Query)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs index f40dbdd477..2384f239c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs @@ -1,28 +1,27 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(Macro))] - [MapperFor(typeof(IMacro))] - internal sealed class MacroMapper : BaseMapper - { - public MacroMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Macro.Id), nameof(MacroDto.Id)); - DefineMap(nameof(Macro.Alias), nameof(MacroDto.Alias)); - DefineMap(nameof(Macro.CacheByPage), nameof(MacroDto.CacheByPage)); - DefineMap(nameof(Macro.CacheByMember), nameof(MacroDto.CachePersonalized)); - DefineMap(nameof(Macro.DontRender), nameof(MacroDto.DontRender)); - DefineMap(nameof(Macro.Name), nameof(MacroDto.Name)); - DefineMap(nameof(Macro.CacheDuration), nameof(MacroDto.RefreshRate)); - DefineMap(nameof(Macro.MacroSource), nameof(MacroDto.MacroSource)); - DefineMap(nameof(Macro.UseInEditor), nameof(MacroDto.UseInEditor)); - } +[MapperFor(typeof(Macro))] +[MapperFor(typeof(IMacro))] +internal sealed class MacroMapper : BaseMapper +{ + public MacroMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Macro.Id), nameof(MacroDto.Id)); + DefineMap(nameof(Macro.Alias), nameof(MacroDto.Alias)); + DefineMap(nameof(Macro.CacheByPage), nameof(MacroDto.CacheByPage)); + DefineMap(nameof(Macro.CacheByMember), nameof(MacroDto.CachePersonalized)); + DefineMap(nameof(Macro.DontRender), nameof(MacroDto.DontRender)); + DefineMap(nameof(Macro.Name), nameof(MacroDto.Name)); + DefineMap(nameof(Macro.CacheDuration), nameof(MacroDto.RefreshRate)); + DefineMap(nameof(Macro.MacroSource), nameof(MacroDto.MacroSource)); + DefineMap(nameof(Macro.UseInEditor), nameof(MacroDto.UseInEditor)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs index aab89f8cd9..ceb8bfe49d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs @@ -1,50 +1,50 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperCollection : BuilderCollectionBase, IMapperCollection { - public class MapperCollection : BuilderCollectionBase, IMapperCollection - { - public MapperCollection(Func> items) - : base(items) - { + private readonly Lazy> _index; - _index = new Lazy>(() => + public MapperCollection(Func> items) + : base(items) => + _index = new Lazy>(() => + { + var d = new ConcurrentDictionary(); + foreach (BaseMapper mapper in this) { - var d = new ConcurrentDictionary(); - foreach(var mapper in this) + IEnumerable attributes = + mapper.GetType().GetCustomAttributes(false); + foreach (MapperForAttribute a in attributes) { - var attributes = mapper.GetType().GetCustomAttributes(false); - foreach(var a in attributes) - { - d.TryAdd(a.EntityType, mapper); - } + d.TryAdd(a.EntityType, mapper); } - return d; - }); - } - - private readonly Lazy> _index; - - /// - /// Returns a mapper for this type, throw an exception if not found - /// - /// - /// - public BaseMapper this[Type type] - { - get - { - if (_index.Value.TryGetValue(type, out var mapper)) - return mapper; - throw new Exception($"Could not find a mapper matching type {type.FullName}."); } - } - public bool TryGetMapper(Type type,[MaybeNullWhen(false)] out BaseMapper mapper) => _index.Value.TryGetValue(type, out mapper); + return d; + }); + + /// + /// Returns a mapper for this type, throw an exception if not found + /// + /// + /// + public BaseMapper this[Type type] + { + get + { + if (_index.Value.TryGetValue(type, out BaseMapper? mapper)) + { + return mapper; + } + + throw new Exception($"Could not find a mapper matching type {type.FullName}."); + } } + + public bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper) => + _index.Value.TryGetValue(type, out mapper); } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs index 8b993365a0..3502e32752 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs @@ -1,62 +1,60 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperCollectionBuilder : SetCollectionBuilderBase { - public class MapperCollectionBuilder : SetCollectionBuilderBase + protected override MapperCollectionBuilder This => this; + + public override void RegisterWith(IServiceCollection services) { - protected override MapperCollectionBuilder This => this; + base.RegisterWith(services); - public override void RegisterWith(IServiceCollection services) - { - base.RegisterWith(services); + // default initializer registers + // - service MapperCollectionBuilder, returns MapperCollectionBuilder + // - service MapperCollection, returns MapperCollectionBuilder's collection + // we want to register extra + // - service IMapperCollection, returns MappersCollectionBuilder's collection + services.AddSingleton(); + services.AddSingleton(factory => factory.GetRequiredService()); + } - // default initializer registers - // - service MapperCollectionBuilder, returns MapperCollectionBuilder - // - service MapperCollection, returns MapperCollectionBuilder's collection - // we want to register extra - // - service IMapperCollection, returns MappersCollectionBuilder's collection - - services.AddSingleton(); - services.AddSingleton(factory => factory.GetRequiredService()); - } - - public MapperCollectionBuilder AddCoreMappers() - { - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - return this; - } + public MapperCollectionBuilder AddCoreMappers() + { + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + return this; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs index 460e50677d..28d3857c28 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs @@ -1,8 +1,7 @@ -using System; using System.Collections.Concurrent; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperConfigurationStore : ConcurrentDictionary> { - public class MapperConfigurationStore : ConcurrentDictionary> - { } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs index c2caa89bd9..8cb1fae20a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs @@ -1,16 +1,12 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +/// +/// An attribute used to decorate mappers to be associated with entities +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class MapperForAttribute : Attribute { - /// - /// An attribute used to decorate mappers to be associated with entities - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public sealed class MapperForAttribute : Attribute - { - public Type EntityType { get; private set; } - - public MapperForAttribute(Type entityType) => EntityType = entityType; - } + public MapperForAttribute(Type entityType) => EntityType = entityType; + public Type EntityType { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs index a2c5ff305b..0495cabdd1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs @@ -1,38 +1,37 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMedia))] +[MapperFor(typeof(Core.Models.Media))] +public sealed class MediaMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMedia))] - [MapperFor(typeof(Core.Models.Media))] - public sealed class MediaMapper : BaseMapper + public MediaMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MediaMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Core.Models.Media.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Core.Models.Media.Key), nameof(NodeDto.UniqueId)); + protected override void DefineMaps() + { + DefineMap(nameof(Core.Models.Media.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Core.Models.Media.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); + DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); - DefineMap(nameof(Core.Models.Media.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Core.Models.Media.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Core.Models.Media.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Core.Models.Media.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Core.Models.Media.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Core.Models.Media.Name), nameof(NodeDto.Text)); - DefineMap(nameof(Core.Models.Media.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Core.Models.Media.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Core.Models.Media.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Core.Models.Media.UpdateDate), nameof(ContentVersionDto.VersionDate)); - } + DefineMap(nameof(Core.Models.Media.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Core.Models.Media.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Core.Models.Media.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Core.Models.Media.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Core.Models.Media.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Core.Models.Media.Name), nameof(NodeDto.Text)); + DefineMap(nameof(Core.Models.Media.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Core.Models.Media.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Core.Models.Media.ContentTypeId), nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Core.Models.Media.UpdateDate), nameof(ContentVersionDto.VersionDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs index 823ee7ce88..47f6d2b7b6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs @@ -1,40 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMediaType))] - [MapperFor(typeof(MediaType))] - public sealed class MediaTypeMapper : BaseMapper - { - public MediaTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(MediaType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MediaType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MediaType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(MediaType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(MediaType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(MediaType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(MediaType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MediaType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(MediaType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(MediaType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MediaType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(MediaType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(MediaType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(MediaType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(MediaType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(MediaType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(MediaType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMediaType))] +[MapperFor(typeof(MediaType))] +public sealed class MediaTypeMapper : BaseMapper +{ + public MediaTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(MediaType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MediaType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MediaType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(MediaType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(MediaType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(MediaType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(MediaType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MediaType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(MediaType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(MediaType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MediaType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(MediaType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(MediaType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(MediaType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(MediaType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(MediaType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(MediaType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs index 749335b0a2..46dc9fa4ff 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs @@ -1,24 +1,23 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof (IMemberGroup))] - [MapperFor(typeof (MemberGroup))] - public sealed class MemberGroupMapper : BaseMapper - { - public MemberGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(MemberGroup.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MemberGroup.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MemberGroup.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MemberGroup.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MemberGroup.Key), nameof(NodeDto.UniqueId)); - } +[MapperFor(typeof(IMemberGroup))] +[MapperFor(typeof(MemberGroup))] +public sealed class MemberGroupMapper : BaseMapper +{ + public MemberGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(MemberGroup.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MemberGroup.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MemberGroup.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MemberGroup.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MemberGroup.Key), nameof(NodeDto.UniqueId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs index c9fce21a73..2a39db1bb6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs @@ -1,56 +1,55 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMember))] +[MapperFor(typeof(Member))] +public sealed class MemberMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMember))] - [MapperFor(typeof(Member))] - public sealed class MemberMapper : BaseMapper + public MemberMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MemberMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Member.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Member.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Member.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Member.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Member.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Member.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Member.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Member.Name), nameof(NodeDto.Text)); - DefineMap(nameof(Member.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Member.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Member.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Member.ContentTypeAlias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(Member.UpdateDate), nameof(ContentVersionDto.VersionDate)); + protected override void DefineMaps() + { + DefineMap(nameof(Member.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Member.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Member.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Member.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Member.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Member.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Member.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Member.Name), nameof(NodeDto.Text)); + DefineMap(nameof(Member.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Member.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(Member.ContentTypeId), nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Member.ContentTypeAlias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(Member.UpdateDate), nameof(ContentVersionDto.VersionDate)); - DefineMap(nameof(Member.Email), nameof(MemberDto.Email)); - DefineMap(nameof(Member.Username), nameof(MemberDto.LoginName)); - DefineMap(nameof(Member.RawPasswordValue), nameof(MemberDto.Password)); - DefineMap(nameof(Member.IsApproved), nameof(MemberDto.IsApproved)); - DefineMap(nameof(Member.IsLockedOut), nameof(MemberDto.IsLockedOut)); - DefineMap(nameof(Member.FailedPasswordAttempts), nameof(MemberDto.FailedPasswordAttempts)); - DefineMap(nameof(Member.LastLockoutDate), nameof(MemberDto.LastLockoutDate)); - DefineMap(nameof(Member.LastLoginDate), nameof(MemberDto.LastLoginDate)); - DefineMap(nameof(Member.LastPasswordChangeDate), nameof(MemberDto.LastPasswordChangeDate)); + DefineMap(nameof(Member.Email), nameof(MemberDto.Email)); + DefineMap(nameof(Member.Username), nameof(MemberDto.LoginName)); + DefineMap(nameof(Member.RawPasswordValue), nameof(MemberDto.Password)); + DefineMap(nameof(Member.IsApproved), nameof(MemberDto.IsApproved)); + DefineMap(nameof(Member.IsLockedOut), nameof(MemberDto.IsLockedOut)); + DefineMap(nameof(Member.FailedPasswordAttempts), nameof(MemberDto.FailedPasswordAttempts)); + DefineMap(nameof(Member.LastLockoutDate), nameof(MemberDto.LastLockoutDate)); + DefineMap(nameof(Member.LastLoginDate), nameof(MemberDto.LastLoginDate)); + DefineMap(nameof(Member.LastPasswordChangeDate), nameof(MemberDto.LastPasswordChangeDate)); - DefineMap(nameof(Member.Comments), nameof(PropertyDataDto.TextValue)); + DefineMap(nameof(Member.Comments), nameof(PropertyDataDto.TextValue)); - /* Internal experiment */ - DefineMap(nameof(Member.DateTimePropertyValue), nameof(PropertyDataDto.DateValue)); - DefineMap(nameof(Member.IntegerPropertyValue), nameof(PropertyDataDto.IntegerValue)); - DefineMap(nameof(Member.BoolPropertyValue), nameof(PropertyDataDto.IntegerValue)); - DefineMap(nameof(Member.LongStringPropertyValue), nameof(PropertyDataDto.TextValue)); - DefineMap(nameof(Member.ShortStringPropertyValue), nameof(PropertyDataDto.VarcharValue)); - DefineMap(nameof(Member.PropertyTypeAlias), nameof(PropertyTypeDto.Alias)); - } + /* Internal experiment */ + DefineMap(nameof(Member.DateTimePropertyValue), nameof(PropertyDataDto.DateValue)); + DefineMap(nameof(Member.IntegerPropertyValue), nameof(PropertyDataDto.IntegerValue)); + DefineMap(nameof(Member.BoolPropertyValue), nameof(PropertyDataDto.IntegerValue)); + DefineMap(nameof(Member.LongStringPropertyValue), nameof(PropertyDataDto.TextValue)); + DefineMap(nameof(Member.ShortStringPropertyValue), nameof(PropertyDataDto.VarcharValue)); + DefineMap(nameof(Member.PropertyTypeAlias), nameof(PropertyTypeDto.Alias)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs index d23e219e24..b1f4c1d2e9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs @@ -1,40 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof (MemberType))] - [MapperFor(typeof (IMemberType))] - public sealed class MemberTypeMapper : BaseMapper - { - public MemberTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(MemberType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MemberType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MemberType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(MemberType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(MemberType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(MemberType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(MemberType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MemberType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(MemberType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(MemberType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MemberType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(MemberType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(MemberType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(MemberType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(MemberType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(MemberType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(MemberType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(MemberType))] +[MapperFor(typeof(IMemberType))] +public sealed class MemberTypeMapper : BaseMapper +{ + public MemberTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(MemberType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MemberType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MemberType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(MemberType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(MemberType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(MemberType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(MemberType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MemberType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(MemberType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(MemberType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MemberType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(MemberType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(MemberType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(MemberType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(MemberType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(MemberType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(MemberType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs index 86f5401e52..96e502242f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs @@ -1,33 +1,31 @@ -using System; using System.Reflection; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Extends NPoco default mapper and ensures that nullable dates are not saved to the database. +/// +public class NullableDateMapper : DefaultMapper { - /// - /// Extends NPoco default mapper and ensures that nullable dates are not saved to the database. - /// - public class NullableDateMapper : DefaultMapper + public override Func? GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) { - public override Func? GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) + // ensures that NPoco does not try to insert an invalid date + // from a nullable DateTime property + if (sourceMemberInfo.GetMemberInfoType() == typeof(DateTime)) { - // ensures that NPoco does not try to insert an invalid date - // from a nullable DateTime property - if (sourceMemberInfo.GetMemberInfoType() == typeof(DateTime)) + return datetimeVal => { - return datetimeVal => + var datetime = datetimeVal as DateTime?; + if (datetime.HasValue && datetime.Value > DateTime.MinValue) { - var datetime = datetimeVal as DateTime?; - if (datetime.HasValue && datetime.Value > DateTime.MinValue) - { - return datetime.Value; - } + return datetime.Value; + } - return null; - }; - } - - return null; + return null; + }; } + + return null; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs index 47c7df3a8e..00f23f6187 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs @@ -1,28 +1,27 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(PropertyGroup))] - public sealed class PropertyGroupMapper : BaseMapper - { - public PropertyGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(PropertyGroup.Id), nameof(PropertyTypeGroupDto.Id)); - DefineMap(nameof(PropertyGroup.Key), nameof(PropertyTypeGroupDto.UniqueId)); - DefineMap(nameof(PropertyGroup.Type), nameof(PropertyTypeGroupDto.Type)); - DefineMap(nameof(PropertyGroup.Name), nameof(PropertyTypeGroupDto.Text)); - DefineMap(nameof(PropertyGroup.Alias), nameof(PropertyTypeGroupDto.Alias)); - DefineMap(nameof(PropertyGroup.SortOrder), nameof(PropertyTypeGroupDto.SortOrder)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(PropertyGroup))] +public sealed class PropertyGroupMapper : BaseMapper +{ + public PropertyGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(PropertyGroup.Id), nameof(PropertyTypeGroupDto.Id)); + DefineMap(nameof(PropertyGroup.Key), nameof(PropertyTypeGroupDto.UniqueId)); + DefineMap(nameof(PropertyGroup.Type), nameof(PropertyTypeGroupDto.Type)); + DefineMap(nameof(PropertyGroup.Name), nameof(PropertyTypeGroupDto.Text)); + DefineMap(nameof(PropertyGroup.Alias), nameof(PropertyTypeGroupDto.Alias)); + DefineMap(nameof(PropertyGroup.SortOrder), nameof(PropertyTypeGroupDto.SortOrder)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs index 08ca8d6a13..86a2872c12 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs @@ -1,20 +1,19 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(Property))] - public sealed class PropertyMapper : BaseMapper - { - public PropertyMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Property.Id), nameof(PropertyDataDto.Id)); - DefineMap(nameof(Property.PropertyTypeId), nameof(PropertyDataDto.PropertyTypeId)); - } +[MapperFor(typeof(Property))] +public sealed class PropertyMapper : BaseMapper +{ + public PropertyMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Property.Id), nameof(PropertyDataDto.Id)); + DefineMap(nameof(Property.PropertyTypeId), nameof(PropertyDataDto.PropertyTypeId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs index 3da3d16fcb..f84193230b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs @@ -1,36 +1,35 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(PropertyType))] - public sealed class PropertyTypeMapper : BaseMapper - { - public PropertyTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(PropertyType.Key), nameof(PropertyTypeDto.UniqueId)); - DefineMap(nameof(PropertyType.Id), nameof(PropertyTypeDto.Id)); - DefineMap(nameof(PropertyType.Alias), nameof(PropertyTypeDto.Alias)); - DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId)); - DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description)); - DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory)); - DefineMap(nameof(PropertyType.MandatoryMessage), nameof(PropertyTypeDto.MandatoryMessage)); - DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name)); - DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder)); - DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp)); - DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage)); - DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop)); - DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias)); - DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(PropertyType))] +public sealed class PropertyTypeMapper : BaseMapper +{ + public PropertyTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(PropertyType.Key), nameof(PropertyTypeDto.UniqueId)); + DefineMap(nameof(PropertyType.Id), nameof(PropertyTypeDto.Id)); + DefineMap(nameof(PropertyType.Alias), nameof(PropertyTypeDto.Alias)); + DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId)); + DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description)); + DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory)); + DefineMap(nameof(PropertyType.MandatoryMessage), nameof(PropertyTypeDto.MandatoryMessage)); + DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name)); + DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder)); + DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp)); + DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage)); + DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop)); + DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias)); + DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs index e75492be18..2cc0c2d9ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs @@ -1,29 +1,28 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IRelation))] - [MapperFor(typeof(Relation))] - public sealed class RelationMapper : BaseMapper - { - public RelationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Relation.Id), nameof(RelationDto.Id)); - DefineMap(nameof(Relation.ChildId), nameof(RelationDto.ChildId)); - DefineMap(nameof(Relation.Comment), nameof(RelationDto.Comment)); - DefineMap(nameof(Relation.CreateDate), nameof(RelationDto.Datetime)); - DefineMap(nameof(Relation.ParentId), nameof(RelationDto.ParentId)); - DefineMap(nameof(Relation.RelationTypeId), nameof(RelationDto.RelationType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IRelation))] +[MapperFor(typeof(Relation))] +public sealed class RelationMapper : BaseMapper +{ + public RelationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Relation.Id), nameof(RelationDto.Id)); + DefineMap(nameof(Relation.ChildId), nameof(RelationDto.ChildId)); + DefineMap(nameof(Relation.Comment), nameof(RelationDto.Comment)); + DefineMap(nameof(Relation.CreateDate), nameof(RelationDto.Datetime)); + DefineMap(nameof(Relation.ParentId), nameof(RelationDto.ParentId)); + DefineMap(nameof(Relation.RelationTypeId), nameof(RelationDto.RelationType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs index 732563fef7..5fdf01f21e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs @@ -1,30 +1,29 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(RelationType))] - [MapperFor(typeof(IRelationType))] - public sealed class RelationTypeMapper : BaseMapper - { - public RelationTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(RelationType.Id), nameof(RelationTypeDto.Id)); - DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); - DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType)); - DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); - DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency)); - DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); - DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(RelationType))] +[MapperFor(typeof(IRelationType))] +public sealed class RelationTypeMapper : BaseMapper +{ + public RelationTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(RelationType.Id), nameof(RelationTypeDto.Id)); + DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); + DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType)); + DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); + DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency)); + DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); + DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs index 61b597be46..793bebb8fb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs @@ -1,26 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(ServerRegistration))] - [MapperFor(typeof(IServerRegistration))] - internal sealed class ServerRegistrationMapper : BaseMapper - { - public ServerRegistrationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(ServerRegistration.Id), nameof(ServerRegistrationDto.Id)); - DefineMap(nameof(ServerRegistration.IsActive), nameof(ServerRegistrationDto.IsActive)); - DefineMap(nameof(ServerRegistration.IsSchedulingPublisher), nameof(ServerRegistrationDto.IsSchedulingPublisher)); - DefineMap(nameof(ServerRegistration.ServerAddress), nameof(ServerRegistrationDto.ServerAddress)); - DefineMap(nameof(ServerRegistration.CreateDate), nameof(ServerRegistrationDto.DateRegistered)); - DefineMap(nameof(ServerRegistration.UpdateDate), nameof(ServerRegistrationDto.DateAccessed)); - DefineMap(nameof(ServerRegistration.ServerIdentity), nameof(ServerRegistrationDto.ServerIdentity)); - } +[MapperFor(typeof(ServerRegistration))] +[MapperFor(typeof(IServerRegistration))] +internal sealed class ServerRegistrationMapper : BaseMapper +{ + public ServerRegistrationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap( + nameof(ServerRegistration.Id), + nameof(ServerRegistrationDto.Id)); + DefineMap( + nameof(ServerRegistration.IsActive), + nameof(ServerRegistrationDto.IsActive)); + DefineMap( + nameof(ServerRegistration.IsSchedulingPublisher), + nameof(ServerRegistrationDto.IsSchedulingPublisher)); + DefineMap( + nameof(ServerRegistration.ServerAddress), + nameof(ServerRegistrationDto.ServerAddress)); + DefineMap( + nameof(ServerRegistration.CreateDate), + nameof(ServerRegistrationDto.DateRegistered)); + DefineMap( + nameof(ServerRegistration.UpdateDate), + nameof(ServerRegistrationDto.DateAccessed)); + DefineMap( + nameof(ServerRegistration.ServerIdentity), + nameof(ServerRegistrationDto.ServerIdentity)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs index b2bcc6098a..2024ad7622 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs @@ -1,36 +1,33 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +// TODO: This mapper is actually very useless because the only time it would ever be used is when trying to generate a strongly typed query +// on an IContentBase object which is what exposes ISimpleContentType, however the queries that we execute in the content repositories don't actually +// join on the content type table. The content type data is resolved outside of the query so the end result of the query that is generated by using +// this mapper will either fail or the syntax will target umbracoNode which will be filtering on the actual content NOT the content type. +// I'm leaving this here purely because the ExpressionTests rely on this which is fine for testing that the expressions work, but note that this +// resulting query will not. +[MapperFor(typeof(ISimpleContentType))] +[MapperFor(typeof(SimpleContentType))] +public sealed class SimpleContentTypeMapper : BaseMapper { - // TODO: This mapper is actually very useless because the only time it would ever be used is when trying to generate a strongly typed query - // on an IContentBase object which is what exposes ISimpleContentType, however the queries that we execute in the content repositories don't actually - // join on the content type table. The content type data is resolved outside of the query so the end result of the query that is generated by using - // this mapper will either fail or the syntax will target umbracoNode which will be filtering on the actual content NOT the content type. - // I'm leaving this here purely because the ExpressionTests rely on this which is fine for testing that the expressions work, but note that this - // resulting query will not. - - [MapperFor(typeof(ISimpleContentType))] - [MapperFor(typeof(SimpleContentType))] - public sealed class SimpleContentTypeMapper : BaseMapper + public SimpleContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public SimpleContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - // There is no reason for using ContentType here instead of SimpleContentType, in fact the underlying DefineMap call does nothing with the first type parameter - - DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); - } + protected override void DefineMaps() + { + // There is no reason for using ContentType here instead of SimpleContentType, in fact the underlying DefineMap call does nothing with the first type parameter + DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs index 8c6df88a4d..744f5b400e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs @@ -1,27 +1,26 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Tag))] - [MapperFor(typeof(ITag))] - public sealed class TagMapper : BaseMapper - { - public TagMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Tag.Id), nameof(TagDto.Id)); - DefineMap(nameof(Tag.Text), nameof(TagDto.Text)); - DefineMap(nameof(Tag.Group), nameof(TagDto.Group)); - DefineMap(nameof(Tag.LanguageId), nameof(TagDto.LanguageId)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Tag))] +[MapperFor(typeof(ITag))] +public sealed class TagMapper : BaseMapper +{ + public TagMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Tag.Id), nameof(TagDto.Id)); + DefineMap(nameof(Tag.Text), nameof(TagDto.Text)); + DefineMap(nameof(Tag.Group), nameof(TagDto.Group)); + DefineMap(nameof(Tag.LanguageId), nameof(TagDto.LanguageId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs index f2c8627cc9..f37511cb2b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs @@ -1,27 +1,26 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Template))] - [MapperFor(typeof(ITemplate))] - public sealed class TemplateMapper : BaseMapper - { - public TemplateMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Template.Id), nameof(TemplateDto.NodeId)); - DefineMap(nameof(Template.MasterTemplateId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Template.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Template.Alias), nameof(TemplateDto.Alias)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Template))] +[MapperFor(typeof(ITemplate))] +public sealed class TemplateMapper : BaseMapper +{ + public TemplateMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Template.Id), nameof(TemplateDto.NodeId)); + DefineMap(nameof(Template.MasterTemplateId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Template.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(Template.Alias), nameof(TemplateDto.Alias)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs index 5f60861667..068ee6a74d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs @@ -1,28 +1,27 @@ -using System; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof (IUmbracoEntity))] - public sealed class UmbracoEntityMapper : BaseMapper - { - public UmbracoEntityMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(IUmbracoEntity.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(IUmbracoEntity.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(IUmbracoEntity.Level), nameof(NodeDto.Level)); - DefineMap(nameof(IUmbracoEntity.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(IUmbracoEntity.Path), nameof(NodeDto.Path)); - DefineMap(nameof(IUmbracoEntity.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(IUmbracoEntity.Name), nameof(NodeDto.Text)); - DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); - } +[MapperFor(typeof(IUmbracoEntity))] +public sealed class UmbracoEntityMapper : BaseMapper +{ + public UmbracoEntityMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(IUmbracoEntity.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(IUmbracoEntity.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(IUmbracoEntity.Level), nameof(NodeDto.Level)); + DefineMap(nameof(IUmbracoEntity.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(IUmbracoEntity.Path), nameof(NodeDto.Path)); + DefineMap(nameof(IUmbracoEntity.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(IUmbracoEntity.Name), nameof(NodeDto.Text)); + DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs index 51f4d0bbcc..6c6ef11d35 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs @@ -1,29 +1,28 @@ -using System; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IUserGroup))] - [MapperFor(typeof(UserGroup))] - public sealed class UserGroupMapper : BaseMapper - { - public UserGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(UserGroup.Id), nameof(UserGroupDto.Id)); - DefineMap(nameof(UserGroup.Alias), nameof(UserGroupDto.Alias)); - DefineMap(nameof(UserGroup.Name), nameof(UserGroupDto.Name)); - DefineMap(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon)); - DefineMap(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId)); - DefineMap(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IUserGroup))] +[MapperFor(typeof(UserGroup))] +public sealed class UserGroupMapper : BaseMapper +{ + public UserGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(UserGroup.Id), nameof(UserGroupDto.Id)); + DefineMap(nameof(UserGroup.Alias), nameof(UserGroupDto.Alias)); + DefineMap(nameof(UserGroup.Name), nameof(UserGroupDto.Name)); + DefineMap(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon)); + DefineMap(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId)); + DefineMap(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs index 53c229c935..92af2773cf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs @@ -1,35 +1,35 @@ -using System; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IUser))] - [MapperFor(typeof(User))] - public sealed class UserMapper : BaseMapper - { - public UserMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(User.Id), nameof(UserDto.Id)); - DefineMap(nameof(User.Email), nameof(UserDto.Email)); - DefineMap(nameof(User.Username), nameof(UserDto.Login)); - DefineMap(nameof(User.RawPasswordValue), nameof(UserDto.Password)); - DefineMap(nameof(User.Name), nameof(UserDto.UserName)); - //NOTE: This column in the db is *not* used! - //DefineMap(nameof(User.DefaultPermissions), nameof(UserDto.DefaultPermissions)); - DefineMap(nameof(User.IsApproved), nameof(UserDto.Disabled)); - DefineMap(nameof(User.IsLockedOut), nameof(UserDto.NoConsole)); - DefineMap(nameof(User.Language), nameof(UserDto.UserLanguage)); - DefineMap(nameof(User.CreateDate), nameof(UserDto.CreateDate)); - DefineMap(nameof(User.UpdateDate), nameof(UserDto.UpdateDate)); - DefineMap(nameof(User.LastLockoutDate), nameof(UserDto.LastLockoutDate)); - DefineMap(nameof(User.LastLoginDate), nameof(UserDto.LastLoginDate)); - DefineMap(nameof(User.LastPasswordChangeDate), nameof(UserDto.LastPasswordChangeDate)); - DefineMap(nameof(User.SecurityStamp), nameof(UserDto.SecurityStampToken)); - } +[MapperFor(typeof(IUser))] +[MapperFor(typeof(User))] +public sealed class UserMapper : BaseMapper +{ + public UserMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(User.Id), nameof(UserDto.Id)); + DefineMap(nameof(User.Email), nameof(UserDto.Email)); + DefineMap(nameof(User.Username), nameof(UserDto.Login)); + DefineMap(nameof(User.RawPasswordValue), nameof(UserDto.Password)); + DefineMap(nameof(User.Name), nameof(UserDto.UserName)); + + // NOTE: This column in the db is *not* used! + // DefineMap(nameof(User.DefaultPermissions), nameof(UserDto.DefaultPermissions)); + DefineMap(nameof(User.IsApproved), nameof(UserDto.Disabled)); + DefineMap(nameof(User.IsLockedOut), nameof(UserDto.NoConsole)); + DefineMap(nameof(User.Language), nameof(UserDto.UserLanguage)); + DefineMap(nameof(User.CreateDate), nameof(UserDto.CreateDate)); + DefineMap(nameof(User.UpdateDate), nameof(UserDto.UpdateDate)); + DefineMap(nameof(User.LastLockoutDate), nameof(UserDto.LastLockoutDate)); + DefineMap(nameof(User.LastLoginDate), nameof(UserDto.LastLoginDate)); + DefineMap(nameof(User.LastPasswordChangeDate), nameof(UserDto.LastPasswordChangeDate)); + DefineMap(nameof(User.SecurityStamp), nameof(UserDto.SecurityStampToken)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs index 86aeed4b7e..703d081b10 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs @@ -1,30 +1,30 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - //[MapperFor(typeof(UserSection))] - //public sealed class UserSectionMapper : BaseMapper - //{ - // private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - // //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it - // // otherwise that would fail because there is no public constructor. - // public UserSectionMapper() - // { - // BuildMap(); - // } - // #region Overrides of BaseMapper +// [MapperFor(typeof(UserSection))] +// public sealed class UserSectionMapper : BaseMapper +// { +// private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); - // internal override ConcurrentDictionary PropertyInfoCache - // { - // get { return PropertyInfoCacheInstance; } - // } +// //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it +// // otherwise that would fail because there is no public constructor. +// public UserSectionMapper() +// { +// BuildMap(); +// } - // internal override void BuildMap() - // { - // CacheMap(src => src.UserId, dto => dto.UserId); - // CacheMap(src => src.SectionAlias, dto => dto.AppAlias); - // } +// #region Overrides of BaseMapper - // #endregion - //} -} +// internal override ConcurrentDictionary PropertyInfoCache +// { +// get { return PropertyInfoCacheInstance; } +// } + +// internal override void BuildMap() +// { +// CacheMap(src => src.UserId, dto => dto.UserId); +// CacheMap(src => src.SectionAlias, dto => dto.AppAlias); +// } + +// #endregion +// } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs index c53076ff18..92ccb0fc81 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -1,121 +1,117 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Linq; using Microsoft.Data.SqlClient; using NPoco; using NPoco.SqlServer; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to NPoco Database class. +/// +public static partial class NPocoDatabaseExtensions { /// - /// Provides extension methods to NPoco Database class. + /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the + /// underlying RetryDbConnection and ProfiledDbTransaction /// - public static partial class NPocoDatabaseExtensions + /// + /// This is required to use NPoco's own method because we use + /// wrapped DbConnection and DbTransaction instances. + /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for + /// bulk inserting of records for + /// any other database type and in which case will just insert records one at a time. + /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own + /// BulkInsertRecords methods + /// do not handle this scenario. + /// + public static void ConfigureNPocoBulkExtensions() { - /// - /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the - /// underlying RetryDbConnection and ProfiledDbTransaction - /// - /// - /// This is required to use NPoco's own method because we use - /// wrapped DbConnection and DbTransaction instances. - /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for - /// bulk inserting of records for - /// any other database type and in which case will just insert records one at a time. - /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own - /// BulkInsertRecords methods - /// do not handle this scenario. - /// - public static void ConfigureNPocoBulkExtensions() + SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); + SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); + } + + /// + /// Determines whether a column should be part of a bulk-insert. + /// + /// The PocoData object corresponding to the record's type. + /// The column. + /// A value indicating whether the column should be part of the bulk-insert. + /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. + public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => + column.Value.ResultColumn == false + && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); + + /// + /// Creates bulk-insert commands. + /// + /// The type of the records. + /// The database. + /// The records. + /// The sql commands to execute. + internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) + { + if (database.Connection == null) { - SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); - SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); + throw new ArgumentException("Null database?.connection.", nameof(database)); } + PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); - /// - /// Creates bulk-insert commands. - /// - /// The type of the records. - /// The database. - /// The records. - /// The sql commands to execute. - internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) + // get columns to include, = number of parameters per row + KeyValuePair[] columns = + pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); + var paramsPerRecord = columns.Length; + + // format columns to sql + var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); + var columnNames = string.Join( + ", ", + columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); + + // example: + // assume 4168 records, each record containing 8 fields, ie 8 command parameters + // max 2100 parameter per command + // Math.Floor(2100 / 8) = 262 record per command + // 4168 / 262 = 15.908... = there will be 16 command in total + // (if we have disabled db parameters, then all records will be included, in only one command) + var recordsPerCommand = paramsPerRecord == 0 + ? int.MaxValue + : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); + var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); + + var commands = new IDbCommand[commandsCount]; + var recordsIndex = 0; + var recordsLeftToInsert = records.Length; + var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); + for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) { - if (database?.Connection == null) + DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); + var parameterIndex = 0; + var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); + var recordsValues = new string[commandRecords]; + for (var commandRecordIndex = 0; + commandRecordIndex < commandRecords; + commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) { - throw new ArgumentException("Null database?.connection.", nameof(database)); - } - - PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); - - // get columns to include, = number of parameters per row - KeyValuePair[] columns = - pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); - var paramsPerRecord = columns.Length; - - // format columns to sql - var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); - var columnNames = string.Join(", ", - columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); - - // example: - // assume 4168 records, each record containing 8 fields, ie 8 command parameters - // max 2100 parameter per command - // Math.Floor(2100 / 8) = 262 record per command - // 4168 / 262 = 15.908... = there will be 16 command in total - // (if we have disabled db parameters, then all records will be included, in only one command) - var recordsPerCommand = paramsPerRecord == 0 - ? int.MaxValue - : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); - var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); - - var commands = new IDbCommand[commandsCount]; - var recordsIndex = 0; - var recordsLeftToInsert = records.Length; - var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); - for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) - { - DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); - var parameterIndex = 0; - var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); - var recordsValues = new string[commandRecords]; - for (var commandRecordIndex = 0; - commandRecordIndex < commandRecords; - commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) + T record = records[recordsIndex]; + var recordValues = new string[columns.Length]; + for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) { - T record = records[recordsIndex]; - var recordValues = new string[columns.Length]; - for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) - { - database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); - recordValues[columnIndex] = prefix + parameterIndex++; - } - - recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; + database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); + recordValues[columnIndex] = prefix + parameterIndex++; } - command.CommandText = - $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; - commands[commandIndex] = command; + recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; } - return commands; + command.CommandText = + $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; + commands[commandIndex] = command; } - /// - /// Determines whether a column should be part of a bulk-insert. - /// - /// The PocoData object corresponding to the record's type. - /// The column. - /// A value indicating whether the column should be part of the bulk-insert. - /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. - public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => - column.Value.ResultColumn == false - && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); + return commands; } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs index 7537ffc48f..1a64b44aa6 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; using NPoco; @@ -9,290 +8,320 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to NPoco Database class. +/// +public static partial class NPocoDatabaseExtensions { /// - /// Provides extension methods to NPoco Database class. + /// Iterates over the result of a paged data set with a db reader /// - public static partial class NPocoDatabaseExtensions + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// Specify a custom Sql command to get the total count, if null is specified than the + /// auto-generated sql count will be used + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql, Sql? sqlCount) { - /// - /// Iterates over the result of a paged data set with a db reader - /// - /// - /// - /// - /// The number of rows to load per page - /// - /// - /// Specify a custom Sql command to get the total count, if null is specified than the auto-generated sql count will be used - /// - /// - /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to - /// iterate over each row with a reader using Query vs Fetch. - /// - public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql, Sql? sqlCount) - { - var sqlString = sql.SQL; - var sqlArgs = sql.Arguments; + var sqlString = sql.SQL; + var sqlArgs = sql.Arguments; - int? itemCount = null; - long pageIndex = 0; - do + int? itemCount = null; + long pageIndex = 0; + do + { + // Get the paged queries + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage); + + // get the item count once + if (itemCount == null) { - // Get the paged queries - database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage); + itemCount = database.ExecuteScalar( + sqlCount?.SQL ?? generatedSqlCount, + sqlCount?.Arguments ?? sqlArgs); + } - // get the item count once - if (itemCount == null) - { - itemCount = database.ExecuteScalar(sqlCount?.SQL ?? generatedSqlCount, sqlCount?.Arguments ?? sqlArgs); - } - pageIndex++; + pageIndex++; - // iterate over rows without allocating all items to memory (Query vs Fetch) - foreach (var row in database.Query(sqlPage, sqlArgs)) - { - yield return row; - } + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (T row in database.Query(sqlPage, sqlArgs)) + { + yield return row; + } + } + while (pageIndex * pageSize < itemCount); + } - } while ((pageIndex * pageSize) < itemCount); + /// + /// Iterates over the result of a paged data set with a db reader + /// + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) => + database.QueryPaged(pageSize, sql, null); + + // NOTE + // + // proper way to do it with TSQL and SQLCE + // IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...) + // BEGIN + // UPDATE table SET ... WHERE ... + // END + // ELSE + // BEGIN + // INSERT INTO table (...) VALUES (...) + // END + // + // works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK + // + // TODO: use the proper database syntax, not this kludge + + /// + /// Safely inserts a record, or updates if it exists, based on a unique constraint. + /// + /// + /// + /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the + /// poco object + /// passed in will contain the updated value. + /// + /// + /// + /// We cannot rely on database-specific options because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that + /// would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's + /// value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. + /// + /// + public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco) + where T : class => + db.InsertOrUpdate(poco, null, null); + + /// + /// Safely inserts a record, or updates if it exists, based on a unique constraint. + /// + /// + /// + /// + /// If the entity has a composite key they you need to specify the update command explicitly + /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the + /// poco object + /// passed in will contain the updated value. + /// + /// + /// + /// We cannot rely on database-specific options because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that + /// would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's + /// value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. + /// + /// + public static RecordPersistenceType InsertOrUpdate( + this IUmbracoDatabase db, + T poco, + string? updateCommand, + object? updateArgs) + where T : class + { + if (poco == null) + { + throw new ArgumentNullException(nameof(poco)); } - /// - /// Iterates over the result of a paged data set with a db reader - /// - /// - /// - /// - /// The number of rows to load per page - /// - /// - /// - /// - /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to - /// iterate over each row with a reader using Query vs Fetch. - /// - public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) => database.QueryPaged(pageSize, sql, null); + // TODO: NPoco has a Save method that works with the primary key + // in any case, no point trying to update if there's no primary key! - // NOTE - // - // proper way to do it with TSQL and SQLCE - // IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...) - // BEGIN - // UPDATE table SET ... WHERE ... - // END - // ELSE - // BEGIN - // INSERT INTO table (...) VALUES (...) - // END - // - // works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK - // - // TODO: use the proper database syntax, not this kludge - - /// - /// Safely inserts a record, or updates if it exists, based on a unique constraint. - /// - /// - /// - /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object - /// passed in will contain the updated value. - /// - /// We cannot rely on database-specific options because SQLCE - /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting - /// isolation levels globally. We want to keep it simple for the time being and manage it manually. - /// We handle it by trying to update, then insert, etc. until something works, or we get bored. - /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value - /// once T1 and T2 have completed. Whereas here, it could contain T1's value. - /// - public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco) - where T : class + // try to update + var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null + ? db.Update(poco) + : db.Update(updateCommand!, updateArgs); + if (rowCount > 0) { - return db.InsertOrUpdate(poco, null, null); + return RecordPersistenceType.Update; } - /// - /// Safely inserts a record, or updates if it exists, based on a unique constraint. - /// - /// - /// - /// - /// If the entity has a composite key they you need to specify the update command explicitly - /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object - /// passed in will contain the updated value. - /// - /// We cannot rely on database-specific options because SQLCE - /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting - /// isolation levels globally. We want to keep it simple for the time being and manage it manually. - /// We handle it by trying to update, then insert, etc. until something works, or we get bored. - /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value - /// once T1 and T2 have completed. Whereas here, it could contain T1's value. - /// - public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, - T poco, - string? updateCommand, - object? updateArgs) - where T : class + // failed: does not exist, need to insert + // RC1 race cond here: another thread may insert a record with the same constraint + var i = 0; + while (i++ < 4) { - if (poco == null) - throw new ArgumentNullException(nameof(poco)); + try + { + // try to insert + db.Insert(poco); + return RecordPersistenceType.Insert; + } + catch (SqlException) + { + // assuming all db engines will throw SQLException exception + // failed: exists (due to race cond RC1) + // RC2 race cond here: another thread may remove the record - // TODO: NPoco has a Save method that works with the primary key - // in any case, no point trying to update if there's no primary key! - - // try to update - var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null + // try to update + rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null ? db.Update(poco) : db.Update(updateCommand!, updateArgs); - if (rowCount > 0) - return RecordPersistenceType.Update; - - // failed: does not exist, need to insert - // RC1 race cond here: another thread may insert a record with the same constraint - - var i = 0; - while (i++ < 4) - { - try + if (rowCount > 0) { - // try to insert - db.Insert(poco); - return RecordPersistenceType.Insert; + return RecordPersistenceType.Update; } - catch (SqlException) // assuming all db engines will throw that exception - { - // failed: exists (due to race cond RC1) - // RC2 race cond here: another thread may remove the record - // try to update - rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null - ? db.Update(poco) - : db.Update(updateCommand!, updateArgs); - if (rowCount > 0) - return RecordPersistenceType.Update; - - // failed: does not exist (due to race cond RC2), need to insert - // loop - } - } - - // this can go on forever... have to break at some point and report an error. - throw new DataException("Record could not be inserted or updated."); - } - - /// - /// This will escape single @ symbols for npoco values so it doesn't think it's a parameter - /// - /// - /// - public static string EscapeAtSymbols(string value) - { - if (value.Contains("@") == false) return value; - - //this fancy regex will only match a single @ not a double, etc... - var regex = new Regex("(? - /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TConnection GetTypedConnection(IDbConnection connection) - where TConnection : class, IDbConnection - { - var c = connection; - for (;;) - { - switch (c) - { - case TConnection ofType: - return ofType; - case RetryDbConnection retry: - c = retry.Inner; - break; - case ProfiledDbConnection profiled: - c = profiled.WrappedConnection; - break; - default: - throw new NotSupportedException(connection.GetType().FullName); - } + // failed: does not exist (due to race cond RC2), need to insert + // loop } } - /// - /// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TTransaction GetTypedTransaction(IDbTransaction? transaction) - where TTransaction : class, IDbTransaction + // this can go on forever... have to break at some point and report an error. + throw new DataException("Record could not be inserted or updated."); + } + + /// + /// This will escape single @ symbols for npoco values so it doesn't think it's a parameter + /// + /// + /// + public static string EscapeAtSymbols(string value) + { + if (value.Contains("@") == false) { - var t = transaction; - for (;;) + return value; + } + + // this fancy regex will only match a single @ not a double, etc... + var regex = new Regex("(? + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TConnection GetTypedConnection(IDbConnection connection) + where TConnection : class, IDbConnection + { + IDbConnection? c = connection; + for (; ;) + { + switch (c) { - switch (t) - { - case TTransaction ofType: - return ofType; - case ProfiledDbTransaction profiled: - t = profiled.WrappedTransaction; - break; - default: - throw new NotSupportedException(transaction?.GetType().FullName); - } + case TConnection ofType: + return ofType; + case RetryDbConnection retry: + c = retry.Inner; + break; + case ProfiledDbConnection profiled: + c = profiled.WrappedConnection; + break; + default: + throw new NotSupportedException(connection.GetType().FullName); } } - - /// - /// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TCommand GetTypedCommand(IDbCommand command) - where TCommand : class, IDbCommand - { - var c = command; - for (;;) - { - switch (c) - { - case TCommand ofType: - return ofType; - case FaultHandlingDbCommand faultHandling: - c = faultHandling.Inner; - break; - case ProfiledDbCommand profiled: - c = profiled.InternalCommand; - break; - default: - throw new NotSupportedException(command.GetType().FullName); - } - } - } - - public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName) - { - var sql = new Sql(string.Format( - sqlSyntax.TruncateTable, - sqlSyntax.GetQuotedTableName(tableName))); - db.Execute(sql); - } - - public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database) - { - var transaction = database.Transaction; - return transaction?.IsolationLevel ?? IsolationLevel.Unspecified; - } - - public static IEnumerable FetchByGroups(this IDatabase db, IEnumerable source, int groupSize, Func, Sql> sqlFactory) - { - return source.SelectByGroups(x => db.Fetch(sqlFactory(x)), groupSize); - } } + + /// + /// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TTransaction GetTypedTransaction(IDbTransaction? transaction) + where TTransaction : class, IDbTransaction + { + IDbTransaction? t = transaction; + for (; ;) + { + switch (t) + { + case TTransaction ofType: + return ofType; + case ProfiledDbTransaction profiled: + t = profiled.WrappedTransaction; + break; + default: + throw new NotSupportedException(transaction?.GetType().FullName); + } + } + } + + /// + /// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TCommand GetTypedCommand(IDbCommand command) + where TCommand : class, IDbCommand + { + IDbCommand? c = command; + for (; ;) + { + switch (c) + { + case TCommand ofType: + return ofType; + case FaultHandlingDbCommand faultHandling: + c = faultHandling.Inner; + break; + case ProfiledDbCommand profiled: + c = profiled.InternalCommand; + break; + default: + throw new NotSupportedException(command.GetType().FullName); + } + } + } + + public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName) + { + var sql = new Sql(string.Format( + sqlSyntax.TruncateTable, + sqlSyntax.GetQuotedTableName(tableName))); + db.Execute(sql); + } + + public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database) + { + DbTransaction? transaction = database.Transaction; + return transaction?.IsolationLevel ?? IsolationLevel.Unspecified; + } + + public static IEnumerable FetchByGroups(this IDatabase db, IEnumerable source, int groupSize, Func, Sql> sqlFactory) => + source.SelectByGroups(x => db.Fetch(sqlFactory(x)), groupSize); } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs index b349824591..3159cf34e1 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs @@ -1,19 +1,19 @@ -using System; using NPoco; +using NPoco.DatabaseTypes; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class NPocoDatabaseTypeExtensions { - internal static class NPocoDatabaseTypeExtensions - { - [Obsolete("Usage of this method indicates a code smell.")] - public static bool IsSqlServer(this DatabaseType databaseType) => - // note that because SqlServerDatabaseType is the base class for - // all Sql Server types eg SqlServer2012DatabaseType, this will - // test *any* version of Sql Server. - databaseType is NPoco.DatabaseTypes.SqlServerDatabaseType; + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlServer(this DatabaseType databaseType) => - [Obsolete("Usage of this method indicates a code smell.")] - public static bool IsSqlite(this DatabaseType databaseType) - => databaseType is NPoco.DatabaseTypes.SQLiteDatabaseType; - } + // note that because SqlServerDatabaseType is the base class for + // all Sql Server types eg SqlServer2012DatabaseType, this will + // test *any* version of Sql Server. + databaseType is SqlServerDatabaseType; + + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlite(this DatabaseType databaseType) + => databaseType is SQLiteDatabaseType; } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs index 3d71c0225e..23ae1b2516 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs @@ -1,14 +1,12 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public sealed class NPocoMapperCollection : BuilderCollectionBase { - public sealed class NPocoMapperCollection : BuilderCollectionBase + public NPocoMapperCollection(Func> items) + : base(items) { - public NPocoMapperCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs index 4840ceafe8..df7411d120 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs @@ -1,10 +1,9 @@ using NPoco; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public sealed class NPocoMapperCollectionBuilder : SetCollectionBuilderBase { - public sealed class NPocoMapperCollectionBuilder : SetCollectionBuilderBase - { - protected override NPocoMapperCollectionBuilder This => this; - } + protected override NPocoMapperCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index 5a0088c727..1eefc14097 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -1,7 +1,4 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -10,6 +7,7 @@ using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Extensions { @@ -88,6 +86,17 @@ namespace Umbraco.Extensions return sql; } + public static Sql Union(this Sql sql, Sql sql2) + { + return sql.Append( " UNION ").Append(sql2); + } + + public static Sql.SqlJoinClause InnerJoinNested(this Sql sql, Sql nestedQuery, string alias) + { + return new Sql.SqlJoinClause(sql.Append("INNER JOIN (").Append(nestedQuery) + .Append($") [{alias}]")); + } + public static Sql WhereLike(this Sql sql, Expression> fieldSelector, string likeValue) { var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); @@ -133,13 +142,17 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql WhereAnyIn(this Sql sql, Expression>[] fields, IEnumerable values) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var fieldNames = fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); var sb = new StringBuilder(); sb.Append("("); for (var i = 0; i < fieldNames.Length; i++) { - if (i > 0) sb.Append(" OR "); + if (i > 0) + { + sb.Append(" OR "); + } + sb.Append(fieldNames[i]); sql.Append(" IN (@values)"); } @@ -174,7 +187,10 @@ namespace Umbraco.Extensions for (var i = 0; i < predicates.Length; i++) { if (i > 0) + { wsql.Append(") OR ("); + } + var temp = new Sql(sql.SqlContext); temp = predicates[i](temp); wsql.Append(temp.SQL.TrimStart("WHERE "), temp.Arguments); @@ -225,12 +241,15 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql From(this Sql sql, string? alias = null) { - var type = typeof (TDto); + Type type = typeof (TDto); var tableName = type.GetTableName(); var from = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); if (!string.IsNullOrWhiteSpace(alias)) + { from += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } + sql.From(from); return sql; @@ -252,6 +271,11 @@ namespace Umbraco.Extensions return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ")"); } + public static Sql OrderBy(this Sql sql, Expression> field, string alias) + { + return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field, alias) + ")"); + } + /// /// Appends an ORDER BY clause to the Sql statement. /// @@ -261,7 +285,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql OrderBy(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -289,7 +313,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql OrderByDescending(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -328,7 +352,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql GroupBy(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -344,7 +368,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndBy(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -360,7 +384,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndByDescending(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -380,10 +404,13 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql CrossJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.Append("CROSS JOIN " + join); } @@ -397,10 +424,13 @@ namespace Umbraco.Extensions /// A SqlJoin statement. public static Sql.SqlJoinClause InnerJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.InnerJoin(join); } @@ -432,10 +462,13 @@ namespace Umbraco.Extensions /// A SqlJoin statement. public static Sql.SqlJoinClause LeftJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.LeftJoin(join); } @@ -482,10 +515,13 @@ namespace Umbraco.Extensions /// A SqlJoin statement. public static Sql.SqlJoinClause RightJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.RightJoin(join); } @@ -587,7 +623,11 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql SelectTop(this Sql sql, int count) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.SqlContext.SqlSyntax.SelectTop(sql, count); } @@ -599,9 +639,17 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql SelectCount(this Sql sql, string? alias = null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + var text = "COUNT(*)"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Select(text); } @@ -631,13 +679,21 @@ namespace Umbraco.Extensions /// public static Sql SelectCount(this Sql sql, string? alias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var sqlSyntax = sql.SqlContext.SqlSyntax; + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); var text = "COUNT (" + string.Join(", ", columns) + ")"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Select(text); } @@ -648,7 +704,11 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql SelectAll(this Sql sql) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Select("*"); } @@ -664,7 +724,11 @@ namespace Umbraco.Extensions /// public static Sql Select(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Select(sql.GetColumns(columnExpressions: fields)); } @@ -680,7 +744,11 @@ namespace Umbraco.Extensions /// public static Sql SelectDistinct(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + var columns = sql.GetColumns(columnExpressions: fields); sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); return sql; @@ -707,7 +775,11 @@ namespace Umbraco.Extensions /// public static Sql Select(this Sql sql, string tableAlias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Select(sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)); } @@ -719,7 +791,11 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndSelect(this Sql sql, params string[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Append(", " + string.Join(", ", fields)); } @@ -735,7 +811,11 @@ namespace Umbraco.Extensions /// public static Sql AndSelect(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Append(", " + string.Join(", ", sql.GetColumns(columnExpressions: fields))); } @@ -752,7 +832,11 @@ namespace Umbraco.Extensions /// public static Sql AndSelect(this Sql sql, string tableAlias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields))); } @@ -764,9 +848,17 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndSelectCount(this Sql sql, string? alias = null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + var text = ", COUNT(*)"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Append(text); } @@ -796,13 +888,21 @@ namespace Umbraco.Extensions /// public static Sql AndSelectCount(this Sql sql, string? alias = null, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var sqlSyntax = sql.SqlContext.SqlSyntax; + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); var text = ", COUNT (" + string.Join(", ", columns) + ")"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Append(text); } @@ -815,7 +915,10 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql Select(this Sql sql, Func, SqlRef> reference) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } sql.Select(sql.GetColumns()); @@ -835,7 +938,10 @@ namespace Umbraco.Extensions /// is added, so that it is possible to add (e.g. calculated) columns to the referencing Dto. public static Sql Select(this Sql sql, Func, SqlRef> reference, Func, Sql> sqlexpr) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } sql.Select(sql.GetColumns()); @@ -845,6 +951,21 @@ namespace Umbraco.Extensions return sql; } + /// + /// Creates a SELECT CASE WHEN EXISTS query, which returns 1 if the sub query returns any results, and 0 if not. + /// + /// The original SQL. + /// The nested select to run the query against. + /// The updated Sql statement. + public static Sql SelectAnyIfExists(this Sql sql, Sql nestedSelect) + { + sql.Append("SELECT CASE WHEN EXISTS ("); + sql.Append(nestedSelect); + sql.Append(")"); + sql.Append("THEN 1 ELSE 0 END"); + return sql; + } + /// /// Represents a Dto reference expression. /// @@ -892,7 +1013,7 @@ namespace Umbraco.Extensions /// A SqlRef statement. public SqlRef Select(Expression> field, string? tableAlias, Func, SqlRef>? reference = null) { - var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; + PropertyInfo? property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; return Select(property, tableAlias, reference); } @@ -922,14 +1043,17 @@ namespace Umbraco.Extensions /// public SqlRef Select(Expression>> field, string? tableAlias, Func, SqlRef>? reference = null) { - var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; + PropertyInfo? property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; return Select(property, tableAlias, reference); } private SqlRef Select(PropertyInfo? propertyInfo, string? tableAlias, Func, SqlRef>? nested = null) { var referenceName = propertyInfo?.Name ?? typeof (TDto).Name; - if (Prefix != null) referenceName = Prefix + PocoData.Separator + referenceName; + if (Prefix != null) + { + referenceName = Prefix + PocoData.Separator + referenceName; + } var columns = Sql.GetColumns(tableAlias, referenceName); Sql.Append(", " + string.Join(", ", columns)); @@ -938,7 +1062,6 @@ namespace Umbraco.Extensions return this; } } - /// /// Gets fields for a Dto. /// @@ -951,7 +1074,11 @@ namespace Umbraco.Extensions /// public static string Columns(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false)); } @@ -960,7 +1087,11 @@ namespace Umbraco.Extensions /// public static string ColumnsForInsert(this Sql sql, params Expression>[]? fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, forInsert: true)); } @@ -977,7 +1108,11 @@ namespace Umbraco.Extensions /// public static string Columns(this Sql sql, string alias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, tableAlias: alias)); } @@ -993,7 +1128,7 @@ namespace Umbraco.Extensions public static Sql Delete(this Sql sql) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); // FROM optional SQL server, but not elsewhere. @@ -1013,7 +1148,7 @@ namespace Umbraco.Extensions public static Sql Update(this Sql sql) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); @@ -1022,7 +1157,7 @@ namespace Umbraco.Extensions public static Sql Update(this Sql sql, Func, SqlUpd> updates) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)} SET"); @@ -1030,7 +1165,7 @@ namespace Umbraco.Extensions var u = new SqlUpd(sql.SqlContext); u = updates(u); var first = true; - foreach (var setExpression in u.SetExpressions) + foreach (Tuple setExpression in u.SetExpressions) { switch (setExpression.Item2) { @@ -1049,7 +1184,9 @@ namespace Umbraco.Extensions } if (!first) + { sql.Append(" "); + } return sql; } @@ -1106,8 +1243,8 @@ namespace Umbraco.Extensions // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]" // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]" - var matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); - var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); + MatchCollection matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); + Match? match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); return match == null ? field : match.Groups[2].Value; } @@ -1117,7 +1254,7 @@ namespace Umbraco.Extensions private static string[] GetColumns(this Sql sql, string? tableAlias = null, string? referenceName = null, Expression>[]? columnExpressions = null, bool withAlias = true, bool forInsert = false) { - var pd = sql.SqlContext.PocoDataFactory.ForType(typeof (TDto)); + PocoData? pd = sql.SqlContext.PocoDataFactory.ForType(typeof (TDto)); var tableName = tableAlias ?? pd.TableInfo.TableName; var queryColumns = pd.QueryColumns.ToList(); @@ -1127,13 +1264,16 @@ namespace Umbraco.Extensions { var names = columnExpressions.Select(x => { - (var member, var alias) = ExpressionHelper.FindProperty(x); + (MemberInfo member, var alias) = ExpressionHelper.FindProperty(x); var field = member as PropertyInfo; var fieldName = field?.GetColumnName(); if (alias != null && fieldName is not null) { if (aliases == null) + { aliases = new Dictionary(); + } + aliases[fieldName] = alias; } return fieldName; @@ -1149,7 +1289,9 @@ namespace Umbraco.Extensions string? GetAlias(PocoColumn column) { if (aliases != null && aliases.TryGetValue(column.ColumnName, out var alias)) + { return alias; + } return withAlias ? (string.IsNullOrEmpty(column.ColumnAlias) ? column.MemberInfoKey : column.ColumnAlias) : null; } @@ -1164,13 +1306,13 @@ namespace Umbraco.Extensions // TODO: returning string.Empty for now // BUT the code bits that calls this method cannot deal with string.Empty so we // should either throw, or fix these code bits... - var attr = type.FirstAttribute(); + TableNameAttribute? attr = type.FirstAttribute(); return string.IsNullOrWhiteSpace(attr?.Value) ? string.Empty : attr.Value; } private static string GetColumnName(this PropertyInfo column) { - var attr = column.FirstAttribute(); + ColumnAttribute? attr = column.FirstAttribute(); return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; } @@ -1191,7 +1333,9 @@ namespace Umbraco.Extensions text.AppendLine(sql); if (arguments == null || arguments.Length == 0) + { return; + } text.Append(" --"); diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs b/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs index ae812193c9..2f3041fb33 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs @@ -1,48 +1,46 @@ -using System; using System.Linq.Expressions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents an expression which caches the visitor's result. +/// +internal class CachedExpression : Expression { + private string _visitResult = null!; + /// - /// Represents an expression which caches the visitor's result. + /// Gets or sets the inner Expression. /// - internal class CachedExpression : Expression + public Expression InnerExpression { get; private set; } = null!; + + /// + /// Gets or sets the compiled SQL statement output. + /// + public string VisitResult { - private string _visitResult = null!; - - /// - /// Gets or sets the inner Expression. - /// - public Expression InnerExpression { get; private set; } = null!; - - /// - /// Gets or sets the compiled SQL statement output. - /// - public string VisitResult + get => _visitResult; + set { - get => _visitResult; - set + if (Visited) { - if (Visited) - throw new InvalidOperationException("Cached expression has already been visited."); - _visitResult = value; - Visited = true; + throw new InvalidOperationException("Cached expression has already been visited."); } - } - /// - /// Gets or sets a value indicating whether the cache Expression has been compiled already. - /// - public bool Visited { get; private set; } - - /// - /// Replaces the inner expression. - /// - /// expression. - /// The new expression is assumed to have different parameter but produce the same SQL statement. - public void Wrap(Expression expression) - { - InnerExpression = expression; + _visitResult = value; + Visited = true; } } + + /// + /// Gets or sets a value indicating whether the cache Expression has been compiled already. + /// + public bool Visited { get; private set; } + + /// + /// Replaces the inner expression. + /// + /// expression. + /// The new expression is assumed to have different parameter but produce the same SQL statement. + public void Wrap(Expression expression) => InnerExpression = expression; } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs index f399f66aca..897628f806 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs @@ -1,8 +1,5 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Linq.Expressions; using System.Text; using Umbraco.Cms.Core.Composing; @@ -10,769 +7,840 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +// TODO: are we basically duplicating entire parts of NPoco just because of SqlSyntax ?! +// try to use NPoco's version ! + +/// +/// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. +/// +/// This object is stateful and cannot be re-used to parse an expression. +internal abstract class ExpressionVisitorBase { - // TODO: are we basically duplicating entire parts of NPoco just because of SqlSyntax ?! - // try to use NPoco's version ! + /// + /// Gets the list of SQL parameters. + /// + protected readonly List SqlParameters = new(); + + protected ExpressionVisitorBase(ISqlSyntaxProvider sqlSyntax) => SqlSyntax = sqlSyntax; /// - /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. + /// Gets or sets a value indicating whether the visited expression has been visited already, + /// in which case visiting will just populate the SQL parameters. /// - /// This object is stateful and cannot be re-used to parse an expression. - internal abstract class ExpressionVisitorBase + protected bool Visited { get; set; } + + /// + /// Gets or sets the SQL syntax provider for the current database. + /// + protected ISqlSyntaxProvider SqlSyntax { get; } + + /// + /// Gets the SQL parameters. + /// + /// + public object[] GetSqlParameters() => SqlParameters.ToArray(); + + /// + /// Visits the expression and produces the corresponding SQL statement. + /// + /// The expression + /// The SQL statement corresponding to the expression. + /// Also populates the SQL parameters. + public virtual string Visit(Expression? expression) { - protected ExpressionVisitorBase(ISqlSyntaxProvider sqlSyntax) + if (expression == null) { - SqlSyntax = sqlSyntax; + return string.Empty; } - /// - /// Gets or sets a value indicating whether the visited expression has been visited already, - /// in which case visiting will just populate the SQL parameters. - /// - protected bool Visited { get; set; } - - /// - /// Gets or sets the SQL syntax provider for the current database. - /// - protected ISqlSyntaxProvider SqlSyntax { get; } - - /// - /// Gets the list of SQL parameters. - /// - protected readonly List SqlParameters = new List(); - - /// - /// Gets the SQL parameters. - /// - /// - public object[] GetSqlParameters() + // if the expression is a CachedExpression, + // visit the inner expression if not already visited + var cachedExpression = expression as CachedExpression; + if (cachedExpression != null) { - return SqlParameters.ToArray(); + Visited = cachedExpression.Visited; + expression = cachedExpression.InnerExpression; } - /// - /// Visits the expression and produces the corresponding SQL statement. - /// - /// The expression - /// The SQL statement corresponding to the expression. - /// Also populates the SQL parameters. - public virtual string Visit(Expression? expression) + string result; + + switch (expression.NodeType) { - if (expression == null) return string.Empty; - - // if the expression is a CachedExpression, - // visit the inner expression if not already visited - var cachedExpression = expression as CachedExpression; - if (cachedExpression != null) - { - Visited = cachedExpression.Visited; - expression = cachedExpression.InnerExpression; - } - - string result; - - switch (expression.NodeType) - { - case ExpressionType.Lambda: - result = VisitLambda(expression as LambdaExpression); - break; - case ExpressionType.MemberAccess: - result = VisitMemberAccess(expression as MemberExpression); - break; - case ExpressionType.Constant: - result = VisitConstant(expression as ConstantExpression); - break; - case ExpressionType.Add: - case ExpressionType.AddChecked: - case ExpressionType.Subtract: - case ExpressionType.SubtractChecked: - case ExpressionType.Multiply: - case ExpressionType.MultiplyChecked: - case ExpressionType.Divide: - case ExpressionType.Modulo: - case ExpressionType.And: - case ExpressionType.AndAlso: - case ExpressionType.Or: - case ExpressionType.OrElse: - case ExpressionType.LessThan: - case ExpressionType.LessThanOrEqual: - case ExpressionType.GreaterThan: - case ExpressionType.GreaterThanOrEqual: - case ExpressionType.Equal: - case ExpressionType.NotEqual: - case ExpressionType.Coalesce: - case ExpressionType.ArrayIndex: - case ExpressionType.RightShift: - case ExpressionType.LeftShift: - case ExpressionType.ExclusiveOr: - result = VisitBinary(expression as BinaryExpression); - break; - case ExpressionType.Negate: - case ExpressionType.NegateChecked: - case ExpressionType.Not: - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - case ExpressionType.ArrayLength: - case ExpressionType.Quote: - case ExpressionType.TypeAs: - result = VisitUnary(expression as UnaryExpression); - break; - case ExpressionType.Parameter: - result = VisitParameter(expression as ParameterExpression); - break; - case ExpressionType.Call: - result = VisitMethodCall(expression as MethodCallExpression); - break; - case ExpressionType.New: - result = VisitNew(expression as NewExpression); - break; - case ExpressionType.NewArrayInit: - case ExpressionType.NewArrayBounds: - result = VisitNewArray(expression as NewArrayExpression); - break; - default: - result = expression.ToString(); - break; - } - - // if the expression is a CachedExpression, - // and is not already compiled, assign the result - if (cachedExpression == null) - return result; - if (!cachedExpression.Visited) - cachedExpression.VisitResult = result; - return cachedExpression.VisitResult; + case ExpressionType.Lambda: + result = VisitLambda(expression as LambdaExpression); + break; + case ExpressionType.MemberAccess: + result = VisitMemberAccess(expression as MemberExpression); + break; + case ExpressionType.Constant: + result = VisitConstant(expression as ConstantExpression); + break; + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: + case ExpressionType.Modulo: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.Coalesce: + case ExpressionType.ArrayIndex: + case ExpressionType.RightShift: + case ExpressionType.LeftShift: + case ExpressionType.ExclusiveOr: + result = VisitBinary(expression as BinaryExpression); + break; + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.ArrayLength: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + result = VisitUnary(expression as UnaryExpression); + break; + case ExpressionType.Parameter: + result = VisitParameter(expression as ParameterExpression); + break; + case ExpressionType.Call: + result = VisitMethodCall(expression as MethodCallExpression); + break; + case ExpressionType.New: + result = VisitNew(expression as NewExpression); + break; + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + result = VisitNewArray(expression as NewArrayExpression); + break; + default: + result = expression.ToString(); + break; } - protected abstract string VisitMemberAccess(MemberExpression? m); - - protected virtual string VisitLambda(LambdaExpression? lambda) + // if the expression is a CachedExpression, + // and is not already compiled, assign the result + if (cachedExpression == null) { - if (lambda?.Body.NodeType == ExpressionType.MemberAccess && - lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) - { - //This deals with members that are boolean (i.e. x => IsTrashed ) - var result = VisitMemberAccess(memberExpression); - - SqlParameters.Add(true); - - return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; - } - - return Visit(lambda?.Body); + return result; } - protected virtual string VisitBinary(BinaryExpression? b) + if (!cachedExpression.Visited) { - if (b is null) + cachedExpression.VisitResult = result; + } + + return cachedExpression.VisitResult; + } + + public virtual string GetQuotedTableName(string tableName) + => GetQuotedName(tableName); + + protected abstract string VisitMemberAccess(MemberExpression? m); + + protected virtual string VisitLambda(LambdaExpression? lambda) + { + if (lambda?.Body.NodeType == ExpressionType.MemberAccess && + lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) + { + // This deals with members that are boolean (i.e. x => IsTrashed ) + var result = VisitMemberAccess(memberExpression); + + SqlParameters.Add(true); + + return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; + } + + return Visit(lambda?.Body); + } + + protected virtual string VisitBinary(BinaryExpression? b) + { + if (b is null) + { + return string.Empty; + } + + var left = string.Empty; + var right = string.Empty; + + var operand = BindOperant(b.NodeType); + if (operand == "AND" || operand == "OR") + { + if (b.Left is MemberExpression mLeft && mLeft.Expression != null) { - return string.Empty; - } - var left = string.Empty; - var right = string.Empty; + var r = VisitMemberAccess(mLeft); - var operand = BindOperant(b.NodeType); - if (operand == "AND" || operand == "OR") - { - if (b.Left is MemberExpression mLeft && mLeft.Expression != null) + SqlParameters.Add(true); + + if (Visited == false) { - var r = VisitMemberAccess(mLeft); - - SqlParameters.Add(true); - - if (Visited == false) - left = $"{r} = @{SqlParameters.Count - 1}"; + left = $"{r} = @{SqlParameters.Count - 1}"; } - else - { - left = Visit(b.Left); - } - if (b.Right is MemberExpression mRight && mRight.Expression != null) - { - var r = VisitMemberAccess(mRight); - - SqlParameters.Add(true); - - if (Visited == false) - right = $"{r} = @{SqlParameters.Count - 1}"; - } - else - { - right = Visit(b.Right); - } - } - else if (operand == "=") - { - // deal with (x == true|false) - most common - if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) - return (bool) constRight.Value! ? VisitNotNot(b.Left) : VisitNot(b.Left); - right = Visit(b.Right); - - // deal with (true|false == x) - why not - if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) - return (bool) constLeft.Value! ? VisitNotNot(b.Right) : VisitNot(b.Right); - left = Visit(b.Left); - } - else if (operand == "<>") - { - // deal with (x != true|false) - most common - if (b.Right is ConstantExpression constRight && constRight.Type == typeof (bool)) - return (bool) constRight.Value! ? VisitNot(b.Left) : VisitNotNot(b.Left); - right = Visit(b.Right); - - // deal with (true|false != x) - why not - if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof (bool)) - return (bool) constLeft.Value! ? VisitNot(b.Right) : VisitNotNot(b.Right); - left = Visit(b.Left); } else { left = Visit(b.Left); + } + + if (b.Right is MemberExpression mRight && mRight.Expression != null) + { + var r = VisitMemberAccess(mRight); + + SqlParameters.Add(true); + + if (Visited == false) + { + right = $"{r} = @{SqlParameters.Count - 1}"; + } + } + else + { right = Visit(b.Right); } - - if (operand == "=" && right == "null") operand = "is"; - else if (operand == "<>" && right == "null") operand = "is not"; - else if (operand == "=" || operand == "<>") + } + else if (operand == "=") + { + // deal with (x == true|false) - most common + if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) { - //if (IsTrueExpression(right)) right = GetQuotedTrueValue(); - //else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); - - //if (IsTrueExpression(left)) left = GetQuotedTrueValue(); - //else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); - + return (bool)constRight.Value! ? VisitNotNot(b.Left) : VisitNot(b.Left); } - switch (operand) - { - case "MOD": - case "COALESCE": - return Visited ? string.Empty : $"{operand}({left},{right})"; + right = Visit(b.Right); - default: - return Visited ? string.Empty : $"({left} {operand} {right})"; + // deal with (true|false == x) - why not + if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) + { + return (bool)constLeft.Value! ? VisitNotNot(b.Right) : VisitNot(b.Right); } + + left = Visit(b.Left); + } + else if (operand == "<>") + { + // deal with (x != true|false) - most common + if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) + { + return (bool)constRight.Value! ? VisitNot(b.Left) : VisitNotNot(b.Left); + } + + right = Visit(b.Right); + + // deal with (true|false != x) - why not + if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) + { + return (bool)constLeft.Value! ? VisitNot(b.Right) : VisitNotNot(b.Right); + } + + left = Visit(b.Left); + } + else + { + left = Visit(b.Left); + right = Visit(b.Right); } - protected virtual List VisitExpressionList(ReadOnlyCollection? original) + if (operand == "=" && right == "null") + { + operand = "is"; + } + else if (operand == "<>" && right == "null") + { + operand = "is not"; + } + else if (operand == "=" || operand == "<>") + { + // if (IsTrueExpression(right)) right = GetQuotedTrueValue(); + // else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); + + // if (IsTrueExpression(left)) left = GetQuotedTrueValue(); + // else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); + } + + switch (operand) + { + case "MOD": + case "COALESCE": + return Visited ? string.Empty : $"{operand}({left},{right})"; + + default: + return Visited ? string.Empty : $"({left} {operand} {right})"; + } + } + + protected virtual List VisitExpressionList(ReadOnlyCollection? original) + { + var list = new List(); + if (original is null) { - var list = new List(); - if (original is null) - { - return list; - } - for (int i = 0, n = original.Count; i < n; i++) - { - if (original[i].NodeType == ExpressionType.NewArrayInit || - original[i].NodeType == ExpressionType.NewArrayBounds) - { - list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); - } - else - { - list.Add(Visit(original[i])); - } - } return list; } - protected virtual string VisitNew(NewExpression? newExpression) + for (int i = 0, n = original.Count; i < n; i++) { - if (newExpression is null) + if (original[i].NodeType == ExpressionType.NewArrayInit || + original[i].NodeType == ExpressionType.NewArrayBounds) { - return string.Empty; + list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); } - // TODO: check ! - var member = Expression.Convert(newExpression, typeof(object)); - var lambda = Expression.Lambda>(member); - try + else { - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; - } - catch (InvalidOperationException) - { - if (Visited) - return string.Empty; - - var exprs = VisitExpressionList(newExpression.Arguments); - return string.Join(",", exprs); + list.Add(Visit(original[i])); } } - protected virtual string VisitParameter(ParameterExpression? p) + return list; + } + + protected virtual string VisitNew(NewExpression? newExpression) + { + if (newExpression is null) { - return p?.Name ?? string.Empty; + return string.Empty; } - protected virtual string VisitConstant(ConstantExpression? c) + // TODO: check ! + UnaryExpression member = Expression.Convert(newExpression, typeof(object)); + var lambda = Expression.Lambda>(member); + try { - if (c?.Value == null) - return "null"; + Func getter = lambda.Compile(); + var o = getter(); - SqlParameters.Add(c.Value); + SqlParameters.Add(o); return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; } - - protected virtual string VisitUnary(UnaryExpression? u) + catch (InvalidOperationException) { - switch (u?.NodeType) - { - case ExpressionType.Not: - return VisitNot(u.Operand); - default: - return Visit(u?.Operand); - } - } - - private string VisitNot(Expression exp) - { - var o = Visit(exp); - - // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers - // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") - - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // false property , i.e. x => !Trashed - // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used - // so we want to do an == false - SqlParameters.Add(false); - return Visited ? string.Empty : $"{o} = @{SqlParameters.Count - 1}"; - //return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; - default: - // could be anything else, such as: x => !x.Path.StartsWith("-20") - return Visited ? string.Empty : string.Concat("NOT (", o, ")"); - } - } - - private string VisitNotNot(Expression exp) - { - var o = Visit(exp); - - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // true property, i.e. x => Trashed - SqlParameters.Add(true); - return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; - default: - // could be anything else, such as: x => x.Path.StartsWith("-20") - return Visited ? string.Empty : o; - } - } - - protected virtual string VisitNewArray(NewArrayExpression? na) - { - if (na is null) + if (Visited) { return string.Empty; } - var exprs = VisitExpressionList(na.Expressions); - return Visited ? string.Empty : string.Join(",", exprs); - } - protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression? na) - => VisitExpressionList(na?.Expressions); - - protected virtual string BindOperant(ExpressionType e) - { - switch (e) - { - case ExpressionType.Equal: - return "="; - case ExpressionType.NotEqual: - return "<>"; - case ExpressionType.GreaterThan: - return ">"; - case ExpressionType.GreaterThanOrEqual: - return ">="; - case ExpressionType.LessThan: - return "<"; - case ExpressionType.LessThanOrEqual: - return "<="; - case ExpressionType.AndAlso: - return "AND"; - case ExpressionType.OrElse: - return "OR"; - case ExpressionType.Add: - return "+"; - case ExpressionType.Subtract: - return "-"; - case ExpressionType.Multiply: - return "*"; - case ExpressionType.Divide: - return "/"; - case ExpressionType.Modulo: - return "MOD"; - case ExpressionType.Coalesce: - return "COALESCE"; - default: - return e.ToString(); - } - } - - protected virtual string VisitMethodCall(MethodCallExpression? m) - { - if (m is null) - { - return string.Empty; - } - // m.Object is the expression that represent the instance for instance method class, or null for static method calls - // m.Arguments is the collection of expressions that represent arguments of the called method - // m.MethodInfo is the method info for the method to be called - - // assume that static methods are extension methods (probably not ok) - // and then, the method object is its first argument - get "safe" object - var methodObject = m.Object ?? m.Arguments[0]; - var visitedMethodObject = Visit(methodObject); - // and then, "safe" arguments are what would come after the first arg - var methodArgs = m.Object == null - ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) - : m.Arguments; - - switch (m.Method.Name) - { - case "ToString": - SqlParameters.Add(methodObject.ToString()); - return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; - - case "ToUpper": - return Visited ? string.Empty : $"upper({visitedMethodObject})"; - - case "ToLower": - return Visited ? string.Empty : $"lower({visitedMethodObject})"; - - case "Contains": - // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains - // method, which would then need to become a SQL IN clause - but beware that string is - // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN - - var isCollectionContains = - ( - m.Object == null && // static (extension?) method - m.Arguments.Count == 2 && // with two args - m.Arguments[0].Type != typeof(string) && // but not for string - TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type) && // first arg being an enumerable - m.Arguments[1].NodeType == ExpressionType.MemberAccess // second arg being a member access - ) || - ( - m.Object != null && // instance method - TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable - m.Object.Type != typeof(string) && // but not for string - m.Arguments.Count == 1 && // with 1 arg - m.Arguments[0].NodeType == ExpressionType.MemberAccess // arg being a member access - ); - - if (isCollectionContains) - goto case "SqlIn"; - else - goto case "Contains**String"; - - case nameof(SqlExpressionExtensions.SqlWildcard): - case "StartsWith": - case "EndsWith": - case "Contains**String": // see "Contains" above - case "Equals": - case nameof(SqlExpressionExtensions.SqlStartsWith): - case nameof(SqlExpressionExtensions.SqlEndsWith): - case nameof(SqlExpressionExtensions.SqlContains): - case nameof(SqlExpressionExtensions.SqlEquals): - case nameof(StringExtensions.InvariantStartsWith): - case nameof(StringExtensions.InvariantEndsWith): - case nameof(StringExtensions.InvariantContains): - case nameof(StringExtensions.InvariantEquals): - - string compareValue; - - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - // if it's a field accessor, we could Visit(methodArgs[0]) and get [a].[b] - // but then, what if we want more, eg .StartsWith(node.Path + ',') ? => not - - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - compareValue = getter().ToString()!; - } - else - { - compareValue = methodArgs[0].ToString(); - } - - //default column type - var colType = TextColumnType.NVarchar; - - //then check if the col type argument has been passed to the current method (this will be the case for methods like - // SqlContains and other Sql methods) - if (methodArgs.Count > 1) - { - var colTypeArg = methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); - if (colTypeArg != null) - { - colType = (TextColumnType)((ConstantExpression)colTypeArg).Value!; - } - } - - return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); - - case "Replace": - string searchValue; - - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - searchValue = getter().ToString()!; - } - else - { - searchValue = methodArgs[0].ToString(); - } - - if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - - string replaceValue; - - if (methodArgs[1].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[1], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - replaceValue = getter().ToString()!; - } - else - { - replaceValue = methodArgs[1].ToString(); - } - - if (methodArgs[1].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - - SqlParameters.Add(RemoveQuote(searchValue)!); - SqlParameters.Add(RemoveQuote(replaceValue)!); - - //don't execute if compiled - return Visited ? string.Empty : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; - - //case "Substring": - // var startIndex = Int32.Parse(args[0].ToString()) + 1; - // if (args.Count == 2) - // { - // var length = Int32.Parse(args[1].ToString()); - // return string.Format("substring({0} from {1} for {2})", - // r, - // startIndex, - // length); - // } - // else - // return string.Format("substring({0} from {1})", - // r, - // startIndex); - //case "Round": - //case "Floor": - //case "Ceiling": - //case "Coalesce": - //case "Abs": - //case "Sum": - // return string.Format("{0}({1}{2})", - // m.Method.Name, - // r, - // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); - //case "Concat": - // var s = new StringBuilder(); - // foreach (Object e in args) - // { - // s.AppendFormat(" || {0}", e); - // } - // return string.Format("{0}{1}", r, s); - - case "SqlIn": - - if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) - throw new NotSupportedException("SqlIn must contain the member being accessed."); - - var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); - - var inMember = Expression.Convert(methodObject, typeof(object)); - var inLambda = Expression.Lambda>(inMember); - var inGetter = inLambda.Compile(); - - var inArgs = (IEnumerable) inGetter(); - - var inBuilder = new StringBuilder(); - var inFirst = true; - - inBuilder.Append(memberAccess); - inBuilder.Append(" IN ("); - - foreach (var e in inArgs) - { - SqlParameters.Add(e); - if (inFirst) inFirst = false; else inBuilder.Append(","); - inBuilder.Append("@"); - inBuilder.Append(SqlParameters.Count - 1); - } - - inBuilder.Append(")"); - return inBuilder.ToString(); - - //case "Desc": - // return string.Format("{0} DESC", r); - //case "Alias": - //case "As": - // return string.Format("{0} As {1}", r, - // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); - - case "SqlText": - if (m.Method.DeclaringType != typeof(SqlExtensionsStatics)) - goto default; - if (m.Arguments.Count == 2) - { - var n1 = Visit(m.Arguments[0]); - var f = m.Arguments[1]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1); - } - else if (m.Arguments.Count == 3) - { - var n1 = Visit(m.Arguments[0]); - var n2 = Visit(m.Arguments[1]); - var f = m.Arguments[2]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1, n2); - } - else if (m.Arguments.Count == 4) - { - var n1 = Visit(m.Arguments[0]); - var n2 = Visit(m.Arguments[1]); - var n3 = Visit(m.Arguments[3]); - var f = m.Arguments[3]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1, n2, n3); - } - else - throw new NotSupportedException("Expression is not a proper lambda."); - - // c# 'x == null' becomes sql 'x IS NULL' which is fine - // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, - // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, - // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback - // value which will be used when values are null - turning the comparison into - // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside - // of x and y range - and if that is not possible, then a manual comparison need - // to be written - // TODO: support SqlNullableEquals with 0 parameters, using the full syntax below - case "SqlNullableEquals": - var compareTo = Visit(m.Arguments[1]); - var fallback = Visit(m.Arguments[2]); - // that would work without a fallback value but is more cumbersome - //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; - // use a fallback value - return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; - - default: - - throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); - - //var s2 = new StringBuilder(); - //foreach (Object e in args) - //{ - // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); - //} - //return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); - } - } - - public virtual string GetQuotedTableName(string tableName) - => GetQuotedName(tableName); - - public virtual string GetQuotedColumnName(string columnName) - => GetQuotedName(columnName); - - public virtual string GetQuotedName(string name) - => Visited ? name : "\"" + name + "\""; - - protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) - { - switch (verb) - { - case nameof(SqlExpressionExtensions.SqlWildcard): - SqlParameters.Add(RemoveQuote(val)!); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "Equals": - case nameof(StringExtensions.InvariantEquals): - case nameof(SqlExpressionExtensions.SqlEquals): - SqlParameters.Add(RemoveQuote(val)!); - return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); - - case "StartsWith": - case nameof(StringExtensions.InvariantStartsWith): - case nameof(SqlExpressionExtensions.SqlStartsWith): - SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "EndsWith": - case nameof(StringExtensions.InvariantEndsWith): - case nameof(SqlExpressionExtensions.SqlEndsWith): - SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "Contains": - case nameof(StringExtensions.InvariantContains): - case nameof(SqlExpressionExtensions.SqlContains): - var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); - SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - default: - throw new ArgumentOutOfRangeException(nameof(verb)); - } - } - - public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) - { - return paramValue == null - ? string.Empty - : sqlSyntax.EscapeString(paramValue.ToString()!); - } - - protected virtual string? RemoveQuote(string? exp) - { - if (exp.IsNullOrWhiteSpace()) return exp; - - var c = exp![0]; - return (c == '"' || c == '`' || c == '\'') && exp[exp.Length - 1] == c - ? exp.Length == 1 - ? string.Empty - : exp.Substring(1, exp.Length - 2) - : exp; + List exprs = VisitExpressionList(newExpression.Arguments); + return string.Join(",", exprs); } } + + protected virtual string VisitParameter(ParameterExpression? p) => p?.Name ?? string.Empty; + + protected virtual string VisitConstant(ConstantExpression? c) + { + if (c?.Value == null) + { + return "null"; + } + + SqlParameters.Add(c.Value); + + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + } + + protected virtual string VisitUnary(UnaryExpression? u) + { + switch (u?.NodeType) + { + case ExpressionType.Not: + return VisitNot(u.Operand); + default: + return Visit(u?.Operand); + } + } + + protected virtual string VisitNewArray(NewArrayExpression? na) + { + if (na is null) + { + return string.Empty; + } + + List exprs = VisitExpressionList(na.Expressions); + return Visited ? string.Empty : string.Join(",", exprs); + } + + private string VisitNot(Expression exp) + { + var o = Visit(exp); + + // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers + // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // false property , i.e. x => !Trashed + // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used + // so we want to do an == false + SqlParameters.Add(false); + return Visited ? string.Empty : $"{o} = @{SqlParameters.Count - 1}"; + + // return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; + default: + // could be anything else, such as: x => !x.Path.StartsWith("-20") + return Visited ? string.Empty : string.Concat("NOT (", o, ")"); + } + } + + private string VisitNotNot(Expression exp) + { + var o = Visit(exp); + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // true property, i.e. x => Trashed + SqlParameters.Add(true); + return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; + default: + // could be anything else, such as: x => x.Path.StartsWith("-20") + return Visited ? string.Empty : o; + } + } + + protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression? na) + => VisitExpressionList(na?.Expressions); + + protected virtual string BindOperant(ExpressionType e) + { + switch (e) + { + case ExpressionType.Equal: + return "="; + case ExpressionType.NotEqual: + return "<>"; + case ExpressionType.GreaterThan: + return ">"; + case ExpressionType.GreaterThanOrEqual: + return ">="; + case ExpressionType.LessThan: + return "<"; + case ExpressionType.LessThanOrEqual: + return "<="; + case ExpressionType.AndAlso: + return "AND"; + case ExpressionType.OrElse: + return "OR"; + case ExpressionType.Add: + return "+"; + case ExpressionType.Subtract: + return "-"; + case ExpressionType.Multiply: + return "*"; + case ExpressionType.Divide: + return "/"; + case ExpressionType.Modulo: + return "MOD"; + case ExpressionType.Coalesce: + return "COALESCE"; + default: + return e.ToString(); + } + } + + protected virtual string VisitMethodCall(MethodCallExpression? m) + { + if (m is null) + { + return string.Empty; + } + + // m.Object is the expression that represent the instance for instance method class, or null for static method calls + // m.Arguments is the collection of expressions that represent arguments of the called method + // m.MethodInfo is the method info for the method to be called + + // assume that static methods are extension methods (probably not ok) + // and then, the method object is its first argument - get "safe" object + Expression methodObject = m.Object ?? m.Arguments[0]; + var visitedMethodObject = Visit(methodObject); + + // and then, "safe" arguments are what would come after the first arg + ReadOnlyCollection methodArgs = m.Object == null + ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) + : m.Arguments; + + switch (m.Method.Name) + { + case "ToString": + SqlParameters.Add(methodObject.ToString()); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + + case "ToUpper": + return Visited ? string.Empty : $"upper({visitedMethodObject})"; + + case "ToLower": + return Visited ? string.Empty : $"lower({visitedMethodObject})"; + + case "Contains": + // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains + // method, which would then need to become a SQL IN clause - but beware that string is + // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN + var isCollectionContains = + ( + m.Object == null && // static (extension?) method + m.Arguments.Count == 2 && // with two args + m.Arguments[0].Type != typeof(string) && // but not for string + TypeHelper.IsTypeAssignableFrom(m.Arguments[0] + .Type) && // first arg being an enumerable + m.Arguments[1].NodeType == ExpressionType.MemberAccess) // second arg being a member access + || + ( + m.Object != null && // instance method + TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable + m.Object.Type != typeof(string) && // but not for string + m.Arguments.Count == 1 && // with 1 arg + m.Arguments[0].NodeType == ExpressionType.MemberAccess); // arg being a member access + + if (isCollectionContains) + { + goto case "SqlIn"; + } + + goto case "Contains**String"; + + case nameof(SqlExpressionExtensions.SqlWildcard): + case "StartsWith": + case "EndsWith": + case "Contains**String": // see "Contains" above + case "Equals": + case nameof(SqlExpressionExtensions.SqlStartsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + case nameof(SqlExpressionExtensions.SqlContains): + case nameof(SqlExpressionExtensions.SqlEquals): + case nameof(StringExtensions.InvariantStartsWith): + case nameof(StringExtensions.InvariantEndsWith): + case nameof(StringExtensions.InvariantContains): + case nameof(StringExtensions.InvariantEquals): + + string compareValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + // if it's a field accessor, we could Visit(methodArgs[0]) and get [a].[b] + // but then, what if we want more, eg .StartsWith(node.Path + ',') ? => not + + // This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + compareValue = getter().ToString()!; + } + else + { + compareValue = methodArgs[0].ToString(); + } + + // default column type + TextColumnType colType = TextColumnType.NVarchar; + + // then check if the col type argument has been passed to the current method (this will be the case for methods like + // SqlContains and other Sql methods) + if (methodArgs.Count > 1) + { + Expression? colTypeArg = + methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); + if (colTypeArg != null) + { + colType = (TextColumnType)((ConstantExpression)colTypeArg).Value!; + } + } + + return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); + + case "Replace": + string searchValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + // This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + searchValue = getter().ToString()!; + } + else + { + searchValue = methodArgs[0].ToString(); + } + + if (methodArgs[0].Type != typeof(string) && + TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + string replaceValue; + + if (methodArgs[1].NodeType != ExpressionType.Constant) + { + // This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[1], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + replaceValue = getter().ToString()!; + } + else + { + replaceValue = methodArgs[1].ToString(); + } + + if (methodArgs[1].Type != typeof(string) && + TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + SqlParameters.Add(RemoveQuote(searchValue)!); + SqlParameters.Add(RemoveQuote(replaceValue)!); + + // don't execute if compiled + return Visited + ? string.Empty + : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; + + // case "Substring": + // var startIndex = Int32.Parse(args[0].ToString()) + 1; + // if (args.Count == 2) + // { + // var length = Int32.Parse(args[1].ToString()); + // return string.Format("substring({0} from {1} for {2})", + // r, + // startIndex, + // length); + // } + // else + // return string.Format("substring({0} from {1})", + // r, + // startIndex); + // case "Round": + // case "Floor": + // case "Ceiling": + // case "Coalesce": + // case "Abs": + // case "Sum": + // return string.Format("{0}({1}{2})", + // m.Method.Name, + // r, + // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); + // case "Concat": + // var s = new StringBuilder(); + // foreach (Object e in args) + // { + // s.AppendFormat(" || {0}", e); + // } + // return string.Format("{0}{1}", r, s); + case "SqlIn": + + if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) + { + throw new NotSupportedException("SqlIn must contain the member being accessed."); + } + + var memberAccess = VisitMemberAccess((MemberExpression)methodArgs[0]); + + UnaryExpression inMember = Expression.Convert(methodObject, typeof(object)); + var inLambda = Expression.Lambda>(inMember); + Func inGetter = inLambda.Compile(); + + var inArgs = (IEnumerable)inGetter(); + + var inBuilder = new StringBuilder(); + var inFirst = true; + + inBuilder.Append(memberAccess); + inBuilder.Append(" IN ("); + + foreach (var e in inArgs) + { + SqlParameters.Add(e); + if (inFirst) + { + inFirst = false; + } + else + { + inBuilder.Append(","); + } + + inBuilder.Append("@"); + inBuilder.Append(SqlParameters.Count - 1); + } + + inBuilder.Append(")"); + return inBuilder.ToString(); + + // case "Desc": + // return string.Format("{0} DESC", r); + // case "Alias": + // case "As": + // return string.Format("{0} As {1}", r, + // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); + case "SqlText": + if (m.Method.DeclaringType != typeof(SqlExtensionsStatics)) + { + goto default; + } + + if (m.Arguments.Count == 2) + { + var n1 = Visit(m.Arguments[0]); + Expression f = m.Arguments[1]; + if (!(f is Expression> fl)) + { + throw new NotSupportedException("Expression is not a proper lambda."); + } + + Func ff = fl.Compile(); + return ff(n1); + } + + if (m.Arguments.Count == 3) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + Expression f = m.Arguments[2]; + if (!(f is Expression> fl)) + { + throw new NotSupportedException("Expression is not a proper lambda."); + } + + Func ff = fl.Compile(); + return ff(n1, n2); + } + + if (m.Arguments.Count == 4) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + var n3 = Visit(m.Arguments[3]); + Expression f = m.Arguments[3]; + if (!(f is Expression> fl)) + { + throw new NotSupportedException("Expression is not a proper lambda."); + } + + Func ff = fl.Compile(); + return ff(n1, n2, n3); + } + + throw new NotSupportedException("Expression is not a proper lambda."); + + // c# 'x == null' becomes sql 'x IS NULL' which is fine + // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, + // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, + // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback + // value which will be used when values are null - turning the comparison into + // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside + // of x and y range - and if that is not possible, then a manual comparison need + // to be written + // TODO: support SqlNullableEquals with 0 parameters, using the full syntax below + case "SqlNullableEquals": + var compareTo = Visit(m.Arguments[1]); + var fallback = Visit(m.Arguments[2]); + + // that would work without a fallback value but is more cumbersome + // return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; + // use a fallback value + return Visited + ? string.Empty + : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; + + default: + + throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); + + // var s2 = new StringBuilder(); + // foreach (Object e in args) + // { + // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); + // } + // return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); + } + } + + public virtual string GetQuotedColumnName(string columnName) + => GetQuotedName(columnName); + + public virtual string GetQuotedName(string name) + => Visited ? name : "\"" + name + "\""; + + public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) => paramValue == null ? string.Empty : sqlSyntax.EscapeString(paramValue.ToString()!); + + protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) + { + switch (verb) + { + case nameof(SqlExpressionExtensions.SqlWildcard): + SqlParameters.Add(RemoveQuote(val)!); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Equals": + case nameof(StringExtensions.InvariantEquals): + case nameof(SqlExpressionExtensions.SqlEquals): + SqlParameters.Add(RemoveQuote(val)!); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); + + case "StartsWith": + case nameof(StringExtensions.InvariantStartsWith): + case nameof(SqlExpressionExtensions.SqlStartsWith): + SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "EndsWith": + case nameof(StringExtensions.InvariantEndsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Contains": + case nameof(StringExtensions.InvariantContains): + case nameof(SqlExpressionExtensions.SqlContains): + var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); + SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + default: + throw new ArgumentOutOfRangeException(nameof(verb)); + } + } + + protected virtual string? RemoveQuote(string? exp) + { + if (exp.IsNullOrWhiteSpace()) + { + return exp; + } + + var c = exp![0]; + return (c == '"' || c == '`' || c == '\'') && exp[^1] == c + ? exp.Length == 1 + ? string.Empty + : exp.Substring(1, exp.Length - 2) + : exp; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs index afe00a7fe9..4c1044a411 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -1,140 +1,152 @@ -using System; using System.Linq.Expressions; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, +/// based on Umbraco's business logic models. +/// +/// This object is stateful and cannot be re-used to parse an expression. +internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase { - /// - /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, - /// based on Umbraco's business logic models. - /// - /// This object is stateful and cannot be re-used to parse an expression. - internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase + private readonly BaseMapper? _mapper; + private readonly IMapperCollection? _mappers; + + public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, IMapperCollection? mappers) + : base(sqlSyntax) { - private readonly IMapperCollection? _mappers; - private readonly BaseMapper? _mapper; + _mappers = mappers; + _mapper = mappers?[typeof(T)]; // throws if not found + } - public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, IMapperCollection? mappers) - : base(sqlSyntax) + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) { - _mappers = mappers; - _mapper = mappers?[typeof(T)]; // throws if not found - } - - protected override string VisitMemberAccess(MemberExpression? m) - { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null && - m.Expression.NodeType == ExpressionType.Parameter - && m.Expression.Type == typeof(T)) - { - //don't execute if compiled - if (Visited == false) - { - var field = _mapper?.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); - return field!; - } - - //already compiled, return - return string.Empty; - } - - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) - { - //don't execute if compiled - if (Visited == false) - { - var field = _mapper?.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); - return field!; - } - - //already compiled, return - return string.Empty; - } - - if (m.Expression != null - && m.Expression.Type != typeof(T) - && EndsWithConstant(m) == false - && _mappers is not null - && _mappers.TryGetMapper(m.Expression.Type, out var subMapper)) - { - //if this is the case, it means we have a sub expression / nested property access, such as: x.ContentType.Alias == "Test"; - //and since the sub type (x.ContentType) is not the same as x, we need to resolve a mapper for x.ContentType to get it's mapped SQL column - - //don't execute if compiled - if (Visited == false) - { - var field = subMapper.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}"); - return field; - } - //already compiled, return - return string.Empty; - } - - // TODO: When m.Expression.NodeType == ExpressionType.Constant and it's an expression like: content => aliases.Contains(content.ContentType.Alias); - // then an SQL parameter will be added for aliases as an array, however in SqlIn on the subclass it will manually add these SqlParameters anyways, - // however the query will still execute because the SQL that is written will only contain the correct indexes of SQL parameters, this would be ignored, - // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've - // only just discovered what it is actually doing. - - // TODO - // in most cases we want to convert the value to a plain object, - // but for in some rare cases, we may want to do it differently, - // for instance a Models.AuditType (an enum) may in some cases - // need to be converted to its string value. - // but - we cannot have specific code here, really - and how would - // we configure this? is it even possible? - /* - var toString = typeof(object).GetMethod("ToString"); - var member = Expression.Call(m, toString); - */ - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - //don't execute if compiled - if (Visited == false) - return $"@{SqlParameters.Count - 1}"; - - //already compiled, return return string.Empty; } - /// - /// Determines if the MemberExpression ends in a Constant value - /// - /// - /// - private static bool EndsWithConstant(MemberExpression m) + if (m.Expression != null && + m.Expression.NodeType == ExpressionType.Parameter + && m.Expression.Type == typeof(T)) { - Expression? expr = m; - - while (expr is MemberExpression) + // don't execute if compiled + if (Visited == false) { - var memberExpr = expr as MemberExpression; - if (memberExpr is not null) + var field = _mapper?.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) { - expr = memberExpr.Expression; + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); } + return field!; } - var constExpr = expr as ConstantExpression; - return constExpr != null; + // already compiled, return + return string.Empty; } + + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) + { + // don't execute if compiled + if (Visited == false) + { + var field = _mapper?.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); + } + + return field!; + } + + // already compiled, return + return string.Empty; + } + + if (m.Expression != null + && m.Expression.Type != typeof(T) + && EndsWithConstant(m) == false + && _mappers is not null + && _mappers.TryGetMapper(m.Expression.Type, out BaseMapper? subMapper)) + { + // if this is the case, it means we have a sub expression / nested property access, such as: x.ContentType.Alias == "Test"; + // and since the sub type (x.ContentType) is not the same as x, we need to resolve a mapper for x.ContentType to get it's mapped SQL column + + // don't execute if compiled + if (Visited == false) + { + var field = subMapper.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}"); + } + + return field; + } + + // already compiled, return + return string.Empty; + } + + // TODO: When m.Expression.NodeType == ExpressionType.Constant and it's an expression like: content => aliases.Contains(content.ContentType.Alias); + // then an SQL parameter will be added for aliases as an array, however in SqlIn on the subclass it will manually add these SqlParameters anyways, + // however the query will still execute because the SQL that is written will only contain the correct indexes of SQL parameters, this would be ignored, + // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've + // only just discovered what it is actually doing. + + // TODO + // in most cases we want to convert the value to a plain object, + // but for in some rare cases, we may want to do it differently, + // for instance a Models.AuditType (an enum) may in some cases + // need to be converted to its string value. + // but - we cannot have specific code here, really - and how would + // we configure this? is it even possible? + /* + var toString = typeof(object).GetMethod("ToString"); + var member = Expression.Call(m, toString); + */ + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + // don't execute if compiled + if (Visited == false) + { + return $"@{SqlParameters.Count - 1}"; + } + + // already compiled, return + return string.Empty; + } + + /// + /// Determines if the MemberExpression ends in a Constant value + /// + /// + /// + private static bool EndsWithConstant(MemberExpression m) + { + Expression? expr = m; + + while (expr is MemberExpression) + { + var memberExpr = expr as MemberExpression; + if (memberExpr is not null) + { + expr = memberExpr.Expression; + } + } + + return expr is ConstantExpression constExpr; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs index 87d758ebda..f7aba72bf0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -1,283 +1,321 @@ -using System; -using System.Linq; using System.Linq.Expressions; +using System.Reflection; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of the DTO. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase { - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of the DTO. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + private readonly string? _alias; + private readonly PocoData _pd; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias) + : base(sqlContext.SqlSyntax) { - private readonly PocoData _pd; - private readonly string? _alias; - - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias) - : base(sqlContext.SqlSyntax) - { - _pd = sqlContext.PocoDataFactory.ForType(typeof(TDto)); - _alias = alias; - } - - protected override string VisitMethodCall(MethodCallExpression? m) - { - if (m is null) - { - return string.Empty; - } - var declaring = m.Method.DeclaringType; - if (declaring != typeof (SqlTemplate)) - return base.VisitMethodCall(m); - - if (m.Method.Name != "Arg" && m.Method.Name != "ArgIn") - throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name} is not supported."); - - var parameters = m.Method.GetParameters(); - if (parameters.Length != 1 || parameters[0].ParameterType != typeof (string)) - throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name}({string.Join(", ", parameters.Select(x => x.ParameterType))} is not supported."); - - var arg = m.Arguments[0]; - string? name; - if (arg.NodeType == ExpressionType.Constant) - { - name = arg.ToString(); - } - else - { - // though... we probably should avoid doing this - var member = Expression.Convert(arg, typeof (object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - name = getter().ToString(); - } - - SqlParameters.Add(new SqlTemplate.TemplateArg(RemoveQuote(name))); - - return Visited - ? string.Empty - : $"@{SqlParameters.Count - 1}"; - } - - protected override string VisitMemberAccess(MemberExpression? m) - { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter && m.Expression.Type == typeof(TDto)) - { - return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); - } - - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) - { - return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); - } - - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); - } - - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); - - return tableName + "." + columnName; - } + _pd = sqlContext.PocoDataFactory.ForType(typeof(TDto)); + _alias = alias; } - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of DTO 1. - /// The type of DTO 2. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + protected override string VisitMethodCall(MethodCallExpression? m) { - private readonly PocoData _pocoData1, _pocoData2; - private readonly string? _alias1, _alias2; - private string? _parameterName1, _parameterName2; - - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2) - : base(sqlContext.SqlSyntax) + if (m is null) { - _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof (TDto1)); - _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof (TDto2)); - _alias1 = alias1; - _alias2 = alias2; + return string.Empty; } - protected override string VisitLambda(LambdaExpression? lambda) + Type? declaring = m.Method.DeclaringType; + if (declaring != typeof(SqlTemplate)) { - if (lambda is null) - { - return string.Empty; - } - if (lambda.Parameters.Count == 2) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - } - else - { - _parameterName1 = _parameterName2 = null; - } - return base.VisitLambda(lambda); + return base.VisitMethodCall(m); } - protected override string VisitMemberAccess(MemberExpression? m) + if (m.Method.Name != "Arg" && m.Method.Name != "ArgIn") { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null) - { - if (m.Expression.NodeType == ExpressionType.Parameter) - { - var pex = (ParameterExpression) m.Expression; + throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name} is not supported."); + } - if (pex.Name == _parameterName1) - return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + ParameterInfo[] parameters = m.Method.GetParameters(); + if (parameters.Length != 1 || parameters[0].ParameterType != typeof(string)) + { + throw new NotSupportedException( + $"Method SqlTemplate.{m.Method.Name}({string.Join(", ", parameters.Select(x => x.ParameterType))} is not supported."); + } - if (pex.Name == _parameterName2) - return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); - } - else if (m.Expression.NodeType == ExpressionType.Convert) - { - // here: which _pd should we use?! - throw new NotSupportedException(); - //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); - } - } - - var member = Expression.Convert(m, typeof (object)); + Expression arg = m.Arguments[0]; + string? name; + if (arg.NodeType == ExpressionType.Constant) + { + name = arg.ToString(); + } + else + { + // though... we probably should avoid doing this + UnaryExpression member = Expression.Convert(arg, typeof(object)); var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - // execute if not already compiled - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + Func getter = lambda.Compile(); + name = getter().ToString(); } - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + SqlParameters.Add(new SqlTemplate.TemplateArg(RemoveQuote(name))); - return tableName + "." + columnName; - } + return Visited + ? string.Empty + : $"@{SqlParameters.Count - 1}"; } - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of DTO 1. - /// The type of DTO 2. - /// The type of DTO 3. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + protected override string VisitMemberAccess(MemberExpression? m) { - private readonly PocoData _pocoData1, _pocoData2, _pocoData3; - private readonly string? _alias1, _alias2, _alias3; - private string? _parameterName1, _parameterName2, _parameterName3; - - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2, string? alias3) - : base(sqlContext.SqlSyntax) + if (m is null) { - _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); - _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); - _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3)); - _alias1 = alias1; - _alias2 = alias2; - _alias3 = alias3; + return string.Empty; } - protected override string VisitLambda(LambdaExpression? lambda) + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter && + m.Expression.Type == typeof(TDto)) { - if (lambda is null) - { - return string.Empty; - } - if (lambda.Parameters.Count == 3) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - _parameterName3 = lambda.Parameters[2].Name; - } - else if (lambda.Parameters.Count == 2) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - } - else - { - _parameterName1 = _parameterName2 = null; - } - return base.VisitLambda(lambda); + return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); } - protected override string VisitMemberAccess(MemberExpression? m) + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null) - { - if (m.Expression.NodeType == ExpressionType.Parameter) - { - var pex = (ParameterExpression)m.Expression; - - if (pex.Name == _parameterName1) - return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); - - if (pex.Name == _parameterName2) - return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); - - if (pex.Name == _parameterName3) - return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3); - } - else if (m.Expression.NodeType == ExpressionType.Convert) - { - // here: which _pd should we use?! - throw new NotSupportedException(); - //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); - } - } - - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - // execute if not already compiled - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); } - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); - return tableName + "." + columnName; - } + SqlParameters.Add(o); + + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } + + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + + return tableName + "." + columnName; + } +} + +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of DTO 1. +/// The type of DTO 2. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +{ + private readonly string? _alias1; + private readonly string? _alias2; + private readonly PocoData _pocoData1; + private readonly PocoData _pocoData2; + private string? _parameterName1; + private string? _parameterName2; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2) + : base(sqlContext.SqlSyntax) + { + _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); + _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); + _alias1 = alias1; + _alias2 = alias2; + } + + protected override string VisitLambda(LambdaExpression? lambda) + { + if (lambda is null) + { + return string.Empty; + } + + if (lambda.Parameters.Count == 2) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + } + else + { + _parameterName1 = _parameterName2 = null; + } + + return base.VisitLambda(lambda); + } + + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) + { + return string.Empty; + } + + if (m.Expression != null) + { + if (m.Expression.NodeType == ExpressionType.Parameter) + { + var pex = (ParameterExpression)m.Expression; + + if (pex.Name == _parameterName1) + { + return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + } + + if (pex.Name == _parameterName2) + { + return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); + } + } + else if (m.Expression.NodeType == ExpressionType.Convert) + { + // here: which _pd should we use?! + throw new NotSupportedException(); + + // return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + } + } + + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + // execute if not already compiled + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } + + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + + return tableName + "." + columnName; + } +} + +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of DTO 1. +/// The type of DTO 2. +/// The type of DTO 3. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +{ + private readonly string? _alias1; + private readonly string? _alias2; + private readonly string? _alias3; + private readonly PocoData _pocoData1; + private readonly PocoData _pocoData2; + private readonly PocoData _pocoData3; + private string? _parameterName1; + private string? _parameterName2; + private string? _parameterName3; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2, string? alias3) + : base(sqlContext.SqlSyntax) + { + _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); + _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); + _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3)); + _alias1 = alias1; + _alias2 = alias2; + _alias3 = alias3; + } + + protected override string VisitLambda(LambdaExpression? lambda) + { + if (lambda is null) + { + return string.Empty; + } + + if (lambda.Parameters.Count == 3) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + _parameterName3 = lambda.Parameters[2].Name; + } + else if (lambda.Parameters.Count == 2) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + } + else + { + _parameterName1 = _parameterName2 = null; + } + + return base.VisitLambda(lambda); + } + + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) + { + return string.Empty; + } + + if (m.Expression != null) + { + if (m.Expression.NodeType == ExpressionType.Parameter) + { + var pex = (ParameterExpression)m.Expression; + + if (pex.Name == _parameterName1) + { + return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + } + + if (pex.Name == _parameterName2) + { + return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); + } + + if (pex.Name == _parameterName3) + { + return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3); + } + } + else if (m.Expression.NodeType == ExpressionType.Convert) + { + // here: which _pd should we use?! + throw new NotSupportedException(); + + // return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + } + } + + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + // execute if not already compiled + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } + + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + + return tableName + "." + columnName; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs index f4535f9734..88d1326f44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs @@ -1,100 +1,103 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Linq.Expressions; using System.Text; using NPoco; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying -{ - /// - /// Represents a query builder. - /// - /// A query builder translates Linq queries into Sql queries. - public class Query : IQuery - { - private readonly ISqlContext _sqlContext; - private readonly List> _wheres = new List>(); +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; - public Query(ISqlContext sqlContext) +/// +/// Represents a query builder. +/// +/// A query builder translates Linq queries into Sql queries. +public class Query : IQuery +{ + private readonly ISqlContext _sqlContext; + private readonly List> _wheres = new(); + + public Query(ISqlContext sqlContext) => _sqlContext = sqlContext; + + /// + /// Adds a where clause to the query. + /// + public virtual IQuery Where(Expression>? predicate) + { + if (predicate == null) { - _sqlContext = sqlContext; + return this; } - /// - /// Adds a where clause to the query. - /// - public virtual IQuery Where(Expression> predicate) - { - if (predicate == null) return this; + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(predicate); + _wheres.Add(new Tuple(whereExpression, expressionHelper.GetSqlParameters())); + return this; + } + /// + /// Adds a where-in clause to the query. + /// + public virtual IQuery WhereIn(Expression>? fieldSelector, IEnumerable? values) + { + if (fieldSelector == null) + { + return this; + } + + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(fieldSelector); + _wheres.Add(new Tuple(whereExpression + " IN (@values)", new object[] { new { values } })); + return this; + } + + /// + /// Adds a set of OR-ed where clauses to the query. + /// + public virtual IQuery WhereAny(IEnumerable>>? predicates) + { + if (predicates == null) + { + return this; + } + + StringBuilder? sb = null; + List? parameters = null; + Sql? sql = null; + foreach (Expression> predicate in predicates) + { + // see notes in Where() var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); var whereExpression = expressionHelper.Visit(predicate); - _wheres.Add(new Tuple(whereExpression, expressionHelper.GetSqlParameters())); - return this; - } - /// - /// Adds a where-in clause to the query. - /// - public virtual IQuery WhereIn(Expression> fieldSelector, IEnumerable? values) - { - if (fieldSelector == null) return this; - - var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); - var whereExpression = expressionHelper.Visit(fieldSelector); - _wheres.Add(new Tuple(whereExpression + " IN (@values)", new object[] { new { values } })); - return this; - } - - /// - /// Adds a set of OR-ed where clauses to the query. - /// - public virtual IQuery WhereAny(IEnumerable>> predicates) - { - if (predicates == null) return this; - - StringBuilder? sb = null; - List? parameters = null; - Sql? sql = null; - foreach (var predicate in predicates) + if (sb == null) { - // see notes in Where() - var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); - var whereExpression = expressionHelper.Visit(predicate); - - if (sb == null) - { - sb = new StringBuilder("("); - parameters = new List(); - sql = Sql.BuilderFor(_sqlContext); - } - else - { - sb.Append(" OR "); - sql?.Append(" OR "); - } - - sb.Append(whereExpression); - parameters?.AddRange(expressionHelper.GetSqlParameters()); - sql?.Append(whereExpression, expressionHelper.GetSqlParameters()); + sb = new StringBuilder("("); + parameters = new List(); + sql = Sql.BuilderFor(_sqlContext); + } + else + { + sb.Append(" OR "); + sql?.Append(" OR "); } - if (sb == null) return this; - - sb.Append(")"); - _wheres.Add(Tuple.Create("(" + sql?.SQL + ")", sql?.Arguments)!); + sb.Append(whereExpression); + parameters?.AddRange(expressionHelper.GetSqlParameters()); + sql?.Append(whereExpression, expressionHelper.GetSqlParameters()); + } + if (sb == null) + { return this; } - /// - /// Returns all translated where clauses and their sql parameters - /// - public IEnumerable> GetWhereClauses() - { - return _wheres; - } + sb.Append(")"); + _wheres.Add(Tuple.Create("(" + sql?.SQL + ")", sql?.Arguments)!); + + return this; } + + /// + /// Returns all translated where clauses and their sql parameters + /// + public IEnumerable> GetWhereClauses() => _wheres; } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs index 6abb97a554..5f120194de 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs @@ -1,28 +1,26 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} +/// there's not much we can do. +/// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. +/// We have to wait till v8 to make this change I suppose. +/// +internal static class QueryExtensions { /// - /// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} there's not much we can do. - /// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. - /// We have to wait till v8 to make this change I suppose. + /// Returns all translated where clauses and their sql parameters /// - internal static class QueryExtensions + /// + public static IEnumerable> GetWhereClauses(this IQuery query) { - /// - /// Returns all translated where clauses and their sql parameters - /// - /// - public static IEnumerable> GetWhereClauses(this IQuery query) + if (query is not Query q) { - var q = query as Query; - if (q == null) - { - throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); - } - return q.GetWhereClauses(); + throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); } + + return q.GetWhereClauses(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs b/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs index 85ccbd02ee..26a4c42bee 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs @@ -1,35 +1,29 @@ -using System; using NPoco; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying -{ - /// - /// Represents the Sql Translator for translating a IQuery object to Sql - /// - /// - public class SqlTranslator - { - private readonly Sql _sql; +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; - public SqlTranslator(Sql sql, IQuery? query) +/// +/// Represents the Sql Translator for translating a IQuery object to Sql +/// +/// +public class SqlTranslator +{ + private readonly Sql _sql; + + public SqlTranslator(Sql sql, IQuery? query) + { + _sql = sql ?? throw new ArgumentNullException(nameof(sql)); + if (query is not null) { - _sql = sql ?? throw new ArgumentNullException(nameof(sql)); - if (query is not null) + foreach (Tuple clause in query.GetWhereClauses()) { - foreach (var clause in query.GetWhereClauses()) - _sql.Where(clause.Item1, clause.Item2); + _sql.Where(clause.Item1, clause.Item2); } } - - public Sql Translate() - { - return _sql; - } - - public override string ToString() - { - return _sql.SQL; - } } + + public Sql Translate() => _sql; + + public override string ToString() => _sql.SQL; } diff --git a/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs b/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs index 3162f58d1e..016bc89684 100644 --- a/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs +++ b/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public enum RecordPersistenceType { - public enum RecordPersistenceType - { - Insert, - Update, - Delete - } + Insert, + Update, + Delete, } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs index ac07cd19dd..bafd00ee8e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs @@ -1,34 +1,31 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories; + +public interface IEntityRepositoryExtended : IEntityRepository { - public interface IEntityRepositoryExtended : IEntityRepository - { - /// - /// Gets paged entities for a query and a subset of object types - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// A callback providing the ability to customize the generated SQL used to retrieve entities - /// - /// - /// A collection of mixed entity types which would be of type , , , - /// - /// - IEnumerable GetPagedResultsByQuery( - IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null); - } + /// + /// Gets paged entities for a query and a subset of object types + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// A callback providing the ability to customize the generated SQL used to retrieve entities + /// + /// + /// A collection of mixed entity types which would be of type , + /// , , + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs index b94f99723a..eab408823e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -8,125 +5,123 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository { /// - /// Represents the NPoco implementation of . + /// Initializes a new instance of the class. /// - internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository + public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - /// - /// Initializes a new instance of the class. - /// - public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - } + } - /// - public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + /// + public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + { + Sql sql = Sql() + .Select() + .From() + .OrderByDescending(x => x.EventDateUtc); + + Page page = Database.Page(pageIndex + 1, pageCount, sql); + records = page.TotalItems; + return page.Items.Select(AuditEntryFactory.BuildEntity); + } + + /// + public bool IsAvailable() + { + var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); + return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); + } + + /// + protected override IAuditEntry? PerformGet(int id) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.Id == id); + + AuditEntryDto dto = Database.FirstOrDefault(sql); + return dto == null ? null : AuditEntryFactory.BuildEntity(dto); + } + + /// + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Length == 0) { Sql sql = Sql() .Select() - .From() - .OrderByDescending(x => x.EventDateUtc); + .From(); - Page page = Database.Page(pageIndex + 1, pageCount, sql); - records = page.TotalItems; - return page.Items.Select(AuditEntryFactory.BuildEntity); - } - - /// - public bool IsAvailable() - { - var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); - return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); - } - - /// - protected override IAuditEntry? PerformGet(int id) - { - Sql sql = Sql() - .Select() - .From() - .Where(x => x.Id == id); - - AuditEntryDto dto = Database.FirstOrDefault(sql); - return dto == null ? null : AuditEntryFactory.BuildEntity(dto); - } - - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - if (ids?.Length == 0) - { - Sql sql = Sql() - .Select() - .From(); - - return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); - } - - var entries = new List(); - - foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql() - .Select() - .From() - .WhereIn(x => x.Id, group); - - entries.AddRange(Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity)); - } - - return entries; - } - - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); } - /// - protected override Sql GetBaseQuery(bool isCount) + var entries = new List(); + + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) { - Sql sql = Sql(); - sql = isCount ? sql.SelectCount() : sql.Select(); - sql = sql.From(); - return sql; + Sql sql = Sql() + .Select() + .From() + .WhereIn(x => x.Id, group); + + entries.AddRange(Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity)); } - /// - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; - - /// - protected override IEnumerable GetDeleteClauses() => - throw new NotSupportedException("Audit entries cannot be deleted."); - - /// - protected override void PersistNewItem(IAuditEntry entity) - { - entity.AddingEntity(); - - AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); - Database.Insert(dto); - entity.Id = dto.Id; - entity.ResetDirtyProperties(); - } - - /// - protected override void PersistUpdatedItem(IAuditEntry entity) => - throw new NotSupportedException("Audit entries cannot be updated."); + return entries; } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); + } + + /// + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } + + /// + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; + + /// + protected override IEnumerable GetDeleteClauses() => + throw new NotSupportedException("Audit entries cannot be deleted."); + + /// + protected override void PersistNewItem(IAuditEntry entity) + { + entity.AddingEntity(); + + AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(IAuditEntry entity) => + throw new NotSupportedException("Audit entries cannot be updated."); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index cc5f08d58b..f11e17a236 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,173 +10,181 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class AuditRepository : EntityRepositoryBase, IAuditRepository { - internal class AuditRepository : EntityRepositoryBase, IAuditRepository + public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } - - protected override void PersistNewItem(IAuditItem entity) - { - Database.Insert(new LogDto - { - Comment = entity.Comment, - Datestamp = DateTime.Now, - Header = entity.AuditType.ToString(), - NodeId = entity.Id, - UserId = entity.UserId, - EntityType = entity.EntityType, - Parameters = entity.Parameters - }); - } - - protected override void PersistUpdatedItem(IAuditItem entity) - { - // inserting when updating because we never update a log entry, perhaps this should throw? - Database.Insert(new LogDto - { - Comment = entity.Comment, - Datestamp = DateTime.Now, - Header = entity.AuditType.ToString(), - NodeId = entity.Id, - UserId = entity.UserId, - EntityType = entity.EntityType, - Parameters = entity.Parameters - }); - } - - protected override IAuditItem? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - - var dto = Database.First(sql); - return dto == null - ? null - : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new NotImplementedException(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Cms.Core.Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); - } - - public IEnumerable Get(AuditType type, IQuery query) - { - var sqlClause = GetBaseQuery(false) - .Where("(logHeader=@0)", type.ToString()); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Cms.Core.Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From(); - - if (!isCount) - sql.LeftJoin().On((left, right) => left.UserId == right.Id); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return "id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - throw new NotImplementedException(); - } - - public void CleanLogs(int maximumAgeOfLogsInMinutes) - { - var oldestPermittedLogEntry = DateTime.Now.Subtract(new TimeSpan(0, maximumAgeOfLogsInMinutes, 0)); - - Database.Execute( - "delete from umbracoLog where datestamp < @oldestPermittedLogEntry and logHeader in ('open','system')", - new {oldestPermittedLogEntry = oldestPermittedLogEntry}); - } - - /// - /// Return the audit items as paged result - /// - /// - /// The query coming from the service - /// - /// - /// - /// - /// - /// - /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter - /// so we need to do that here - /// - /// - /// A user supplied custom filter - /// - /// - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, - out long totalRecords, Direction orderDirection, - AuditType[]? auditTypeFilter, - IQuery? customFilter) - { - if (auditTypeFilter == null) auditTypeFilter = Array.Empty(); - - var sql = GetBaseQuery(false); - - var translator = new SqlTranslator(sql, query ?? Query()); - sql = translator.Translate(); - - if (customFilter != null) - foreach (var filterClause in customFilter.GetWhereClauses()) - sql.Where(filterClause.Item1, filterClause.Item2); - - if (auditTypeFilter.Length > 0) - foreach (var type in auditTypeFilter) - sql.Where("(logHeader=@0)", type.ToString()); - - sql = orderDirection == Direction.Ascending - ? sql.OrderBy("Datestamp") - : sql.OrderByDescending("Datestamp"); - - // get page - var page = Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = page.TotalItems; - - var items = page.Items.Select( - dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); - - // map the DateStamp - for (var i = 0; i < items.Count; i++) - items[i].CreateDate = page.Items[i].Datestamp; - - return items; - } } + + public IEnumerable Get(AuditType type, IQuery query) + { + Sql? sqlClause = GetBaseQuery(false) + .Where("(logHeader=@0)", type.ToString()); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } + + public void CleanLogs(int maximumAgeOfLogsInMinutes) + { + DateTime oldestPermittedLogEntry = DateTime.Now.Subtract(new TimeSpan(0, maximumAgeOfLogsInMinutes, 0)); + + Database.Execute( + "delete from umbracoLog where datestamp < @oldestPermittedLogEntry and logHeader in ('open','system')", + new { oldestPermittedLogEntry }); + } + + /// + /// Return the audit items as paged result + /// + /// + /// The query coming from the service + /// + /// + /// + /// + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query + /// or the custom filter + /// so we need to do that here + /// + /// + /// A user supplied custom filter + /// + /// + public IEnumerable GetPagedResultsByQuery( + IQuery query, + long pageIndex, + int pageSize, + out long totalRecords, + Direction orderDirection, + AuditType[]? auditTypeFilter, + IQuery? customFilter) + { + if (auditTypeFilter == null) + { + auditTypeFilter = Array.Empty(); + } + + Sql sql = GetBaseQuery(false); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + + if (customFilter != null) + { + foreach (Tuple filterClause in customFilter.GetWhereClauses()) + { + sql.Where(filterClause.Item1, filterClause.Item2); + } + } + + if (auditTypeFilter.Length > 0) + { + foreach (AuditType type in auditTypeFilter) + { + sql.Where("(logHeader=@0)", type.ToString()); + } + } + + sql = orderDirection == Direction.Ascending + ? sql.OrderBy("Datestamp") + : sql.OrderByDescending("Datestamp"); + + // get page + Page? page = Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = page.TotalItems; + + var items = page.Items.Select( + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); + + // map the DateStamp + for (var i = 0; i < items.Count; i++) + { + items[i].CreateDate = page.Items[i].Datestamp; + } + + return items; + } + + protected override void PersistNewItem(IAuditItem entity) => + Database.Insert(new LogDto + { + Comment = entity.Comment, + Datestamp = DateTime.Now, + Header = entity.AuditType.ToString(), + NodeId = entity.Id, + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters, + }); + + protected override void PersistUpdatedItem(IAuditItem entity) => + + // inserting when updating because we never update a log entry, perhaps this should throw? + Database.Insert(new LogDto + { + Comment = entity.Comment, + Datestamp = DateTime.Now, + Header = entity.AuditType.ToString(), + NodeId = entity.Id, + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters, + }); + + protected override IAuditItem? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + + LogDto? dto = Database.First(sql); + return dto == null + ? null + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotImplementedException(); + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + if (!isCount) + { + sql.LeftJoin().On((left, right) => left.UserId == right.Id); + } + + return sql; + } + + protected override string GetBaseWhereClause() => "id = @id"; + + protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs index 60492773b0..208d0928a3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; @@ -9,65 +6,68 @@ using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class CacheInstructionRepository : ICacheInstructionRepository { - /// - /// Represents the NPoco implementation of . - /// - internal class CacheInstructionRepository : ICacheInstructionRepository + private readonly IScopeAccessor _scopeAccessor; + + public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + private IScope? AmbientScope => _scopeAccessor.AmbientScope; + + /// + public int CountAll() { - private readonly IScopeAccessor _scopeAccessor; + Sql? sql = AmbientScope?.SqlContext.Sql().Select("COUNT(*)") + .From(); - public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + return AmbientScope?.Database.ExecuteScalar(sql) ?? 0; + } - /// - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; + /// + public int CountPendingInstructions(int lastId) => + AmbientScope?.Database.ExecuteScalar( + "SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId }) ?? 0; - /// - public int CountAll() - { - Sql? sql = AmbientScope?.SqlContext.Sql().Select("COUNT(*)") - .From(); + /// + public int GetMaxId() => + AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction") ?? 0; - return AmbientScope?.Database.ExecuteScalar(sql) ?? 0; - } + /// + public bool Exists(int id) => AmbientScope?.Database.Exists(id) ?? false; - /// - public int CountPendingInstructions(int lastId) => - AmbientScope?.Database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId }) ?? 0; + /// + public void Add(CacheInstruction cacheInstruction) + { + CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction); + AmbientScope?.Database.Insert(dto); + } - /// - public int GetMaxId() => - AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction") ?? 0; + /// + public IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve) + { + Sql? sql = AmbientScope?.SqlContext.Sql().SelectAll() + .From() + .Where(dto => dto.Id > lastId) + .OrderBy(dto => dto.Id); + Sql? topSql = sql?.SelectTop(maxNumberToRetrieve); + return AmbientScope?.Database.Fetch(topSql).Select(CacheInstructionFactory.BuildEntity) ?? + Array.Empty(); + } - /// - public bool Exists(int id) => AmbientScope?.Database.Exists(id) ?? false; - - /// - public void Add(CacheInstruction cacheInstruction) - { - CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction); - AmbientScope?.Database.Insert(dto); - } - - /// - public IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve) - { - Sql? sql = AmbientScope?.SqlContext.Sql().SelectAll() - .From() - .Where(dto => dto.Id > lastId) - .OrderBy(dto => dto.Id); - Sql? topSql = sql?.SelectTop(maxNumberToRetrieve); - return AmbientScope?.Database.Fetch(topSql).Select(CacheInstructionFactory.BuildEntity) ?? Array.Empty(); - } - - /// - public void DeleteInstructionsOlderThan(DateTime pruneDate) - { - // Using 2 queries is faster than convoluted joins. - var maxId = AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); - Sql deleteSql = new Sql().Append(@"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", new { pruneDate, maxId }); - AmbientScope?.Database.Execute(deleteSql); - } + /// + public void DeleteInstructionsOlderThan(DateTime pruneDate) + { + // Using 2 queries is faster than convoluted joins. + var maxId = AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); + Sql deleteSql = + new Sql().Append( + @"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", + new { pruneDate, maxId }); + AmbientScope?.Database.Execute(deleteSql); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs index 057fb7e01c..74f3a419e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; @@ -12,89 +10,74 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class ConsentRepository : EntityRepositoryBase, IConsentRepository { /// - /// Represents the NPoco implementation of . + /// Initializes a new instance of the class. /// - internal class ConsentRepository : EntityRepositoryBase, IConsentRepository + public ConsentRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - /// - /// Initializes a new instance of the class. - /// - public ConsentRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - /// - protected override IConsent PerformGet(int id) - { - throw new NotSupportedException(); - } + /// + public void ClearCurrent(string source, string context, string action) + { + Sql sql = Sql() + .Update(u => u.Set(x => x.Current, false)) + .Where(x => x.Source == source && x.Context == context && x.Action == action && x.Current); + Database.Execute(sql); + } - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new NotSupportedException(); - } + /// + protected override IConsent PerformGet(int id) => throw new NotSupportedException(); - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = Sql().Select().From(); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate().OrderByDescending(x => x.CreateDate); - return ConsentFactory.BuildEntities(Database.Fetch(sql)); - } + /// + protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotSupportedException(); - /// - protected override Sql GetBaseQuery(bool isCount) - { - throw new NotSupportedException(); - } + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = Sql().Select().From(); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate().OrderByDescending(x => x.CreateDate); + return ConsentFactory.BuildEntities(Database.Fetch(sql)); + } - /// - protected override string GetBaseWhereClause() - { - throw new NotSupportedException(); - } + /// + protected override Sql GetBaseQuery(bool isCount) => throw new NotSupportedException(); - /// - protected override IEnumerable GetDeleteClauses() - { - throw new NotSupportedException(); - } + /// + protected override string GetBaseWhereClause() => throw new NotSupportedException(); - /// - protected override void PersistNewItem(IConsent entity) - { - entity.AddingEntity(); + /// + protected override IEnumerable GetDeleteClauses() => throw new NotSupportedException(); - var dto = ConsentFactory.BuildDto(entity); - Database.Insert(dto); - entity.Id = dto.Id; - entity.ResetDirtyProperties(); - } + /// + protected override void PersistNewItem(IConsent entity) + { + entity.AddingEntity(); - /// - protected override void PersistUpdatedItem(IConsent entity) - { - entity.UpdatingEntity(); + ConsentDto dto = ConsentFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } - var dto = ConsentFactory.BuildDto(entity); - Database.Update(dto); - entity.ResetDirtyProperties(); + /// + protected override void PersistUpdatedItem(IConsent entity) + { + entity.UpdatingEntity(); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); - } + ConsentDto dto = ConsentFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); - /// - public void ClearCurrent(string source, string context, string action) - { - var sql = Sql() - .Update(u => u.Set(x => x.Current, false)) - .Where(x => x.Source == source && x.Context == context && x.Action == action && x.Current); - Database.Execute(sql); - } + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 651c3fb455..0162ba8d52 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NPoco; @@ -48,6 +45,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// /// /// + /// + /// + /// + /// + /// /// /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors /// @@ -81,8 +83,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected abstract Guid NodeObjectTypeId { get; } protected ILanguageRepository LanguageRepository { get; } + protected IDataTypeService DataTypeService { get; } + protected IRelationRepository RelationRepository { get; } + protected IRelationTypeRepository RelationTypeRepository { get; } protected PropertyEditorCollection PropertyEditors { get; } @@ -102,13 +107,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // gets all version ids, current first public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => tsql.Select(x => x.Id) .From() .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) .OrderByDescending(x => x.Current) // current '1' comes before others '0' - .AndByDescending(x => x.VersionDate) // most recent first - ); + .AndByDescending(x => x.VersionDate)); // most recent first + return Database.Fetch(SqlSyntax.SelectTop(template.Sql(nodeId), maxRows)); } @@ -118,34 +123,45 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // TODO: test object node type? // get the version we want to delete - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => - tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId")) - ); - var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => + tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId"))); + ContentVersionDto? versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); // nothing to delete if (versionDto == null) + { return; + } // don't delete the current version if (versionDto.Current) + { throw new InvalidOperationException("Cannot delete the current version."); + } PerformDeleteVersion(versionDto.NodeId, versionId); } - // deletes all versions of an entity, older than a date. + // deletes all versions of an entity, older than a date. public virtual void DeleteVersions(int nodeId, DateTime versionDate) { // TODO: test object node type? // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersions, tsql => - tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - ); - var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); - foreach (var versionDto in versionDtos) + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.VersionableRepository.GetVersions, + tsql => + tsql.Select() + .From() + .Where(x => + x.NodeId == SqlTemplate.Arg("nodeId") && + !x.Current && + x.VersionDate < SqlTemplate.Arg("versionDate"))); + List? versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); + foreach (ContentVersionDto versionDto in versionDtos) + { PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + } } // actually deletes a version @@ -164,7 +180,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement ? "-1," : "," + parentId + ","; - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .SelectCount() .From(); @@ -194,7 +210,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// public int CountChildren(int parentId, string? contentTypeAlias = null) { - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .SelectCount() .From(); @@ -224,7 +240,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// public int Count(string? contentTypeAlias = null) { - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .SelectCount() .From(); @@ -256,33 +272,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// protected void SetEntityTags(IContentBase entity, ITagRepository tagRepo, IJsonSerializer serializer) { - foreach (var property in entity.Properties) + foreach (IProperty property in entity.Properties) { - var tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); - if (tagConfiguration == null) continue; // not a tags property + TagConfiguration? tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); + if (tagConfiguration == null) + { + continue; // not a tags property + } if (property.PropertyType.VariesByCulture()) { var tags = new List(); - foreach (var pvalue in property.Values) + foreach (IPropertyValue pvalue in property.Values) { - var tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); - var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); + IEnumerable cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); tags.AddRange(cultureTags); } + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); } else { - var tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings - var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings + IEnumerable tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); } } } // TODO: should we do it when un-publishing? or? + /// /// Clears tags for an item. /// @@ -296,18 +317,25 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private Sql PreparePageSql(Sql sql, Sql? filterSql, Ordering ordering) { // non-filtering, non-ordering = nothing to do - if (filterSql == null && ordering.IsEmpty) return sql; + if (filterSql == null && ordering.IsEmpty) + { + return sql; + } // preserve original var psql = new Sql(sql.SqlContext, sql.SQL, sql.Arguments); // apply filter if (filterSql != null) + { psql.Append(filterSql); + } // non-sorting, we're done if (ordering.IsEmpty) + { return psql; + } // else apply ordering ApplyOrdering(ref psql, ordering); @@ -315,7 +343,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column // is empty for many nodes) - see: http://issues.umbraco.org/issue/U4-8831 - var (dbfield, _) = SqlContext.VisitDto(x => x.NodeId); if (ordering.IsCustomField || !ordering.OrderBy.InvariantEquals("id")) { @@ -337,13 +364,21 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } } } + return psql; } private void ApplyOrdering(ref Sql sql, Ordering ordering) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } var orderBy = ordering.IsCustomField ? ApplyCustomOrdering(ref sql, ordering) @@ -361,32 +396,41 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // so anything added here MUST also be part of the inner SELECT statement, ie // the original statement, AND must be using the proper alias, as the inner SELECT // will hide the original table.field names entirely - if (ordering.Direction == Direction.Ascending) + { sql.OrderBy(orderBy); + } else + { sql.OrderByDescending(orderBy); + } } protected virtual string ApplySystemOrdering(ref Sql sql, Ordering ordering) { // id is invariant if (ordering.OrderBy.InvariantEquals("id")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.NodeId), sql); + } // sort order is invariant if (ordering.OrderBy.InvariantEquals("sortOrder")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.SortOrder), sql); + } // path is invariant if (ordering.OrderBy.InvariantEquals("path")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.Path), sql); + } // note: 'owner' is the user who created the item as a whole, // we don't have an 'owner' per culture (should we?) if (ordering.OrderBy.InvariantEquals("owner")) { - var joins = Sql() + Sql joins = Sql() .InnerJoin("ownerUser").On((node, user) => node.UserId == user.Id, aliasRight: "ownerUser"); // see notes in ApplyOrdering: the field MUST be selected + aliased @@ -400,11 +444,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // note: each version culture variation has a date too, // maybe we would want to use it instead? if (ordering.OrderBy.InvariantEquals("versionDate") || ordering.OrderBy.InvariantEquals("updateDate")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.VersionDate), sql); + } // create date is invariant (we don't keep each culture's creation date) if (ordering.OrderBy.InvariantEquals("createDate")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.CreateDate), sql); + } // name is variant if (ordering.OrderBy.InvariantEquals("name")) @@ -412,7 +460,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // no culture = can only work on the invariant name // see notes in ApplyOrdering: the field MUST be aliased if (ordering.Culture.IsNullOrWhiteSpace()) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.Text!), sql); + } // "variantName" alias is defined in DocumentRepository.GetBaseQuery // TODO: what if it is NOT a document but a ... media or whatever? @@ -424,7 +474,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // content type alias is invariant if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) { - var joins = Sql() + Sql joins = Sql() .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); // see notes in ApplyOrdering: the field MUST be selected + aliased @@ -449,7 +499,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var sortedString = "COALESCE(varcharValue,'')"; // assuming COALESCE is ok for all syntaxes // needs to be an outer join since there's no guarantee that any of the nodes have values for this property - var innerSql = Sql().Select($@"CASE + Sql innerSql = Sql().Select($@"CASE WHEN intValue IS NOT NULL THEN {sortedInt} WHEN decimalValue IS NOT NULL THEN {sortedDecimal} WHEN dateValue IS NOT NULL THEN {sortedDate} @@ -471,7 +521,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // create the outer join complete sql fragment var outerJoinTempTable = $@"LEFT OUTER JOIN ({innerSqlString}) AS customPropData - ON customPropData.customPropNodeId = {Cms.Core.Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important! + ON customPropData.customPropNodeId = {Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important! // insert this just above the first WHERE var newSql = InsertBefore(sql.SQL, "WHERE", outerJoinTempTable); @@ -491,16 +541,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // would ensure that items without a value always come last, both in ASC and DESC-ending sorts } - public abstract IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, - Ordering? ordering); + public abstract IEnumerable GetPage(IQuery? query, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering); public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { var report = new Dictionary(); - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .Select() .From() .Where(x => x.NodeObjectType == NodeObjectTypeId) @@ -508,13 +555,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var nodesToRebuild = new Dictionary>(); var validNodes = new Dictionary(); - var rootIds = new[] {Cms.Core.Constants.System.Root, Cms.Core.Constants.System.RecycleBinContent, Cms.Core.Constants.System.RecycleBinMedia}; + var rootIds = new[] { Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia }; var currentParentIds = new HashSet(rootIds); - var prevParentIds = currentParentIds; + HashSet prevParentIds = currentParentIds; var lastLevel = -1; // use a forward cursor (query) - foreach (var node in Database.Query(sql)) + foreach (NodeDto? node in Database.Query(sql)) { if (node.Level != lastLevel) { @@ -541,7 +588,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId)); AppendNodeToFix(nodesToRebuild, node); } - else if (pathParts.Length == 0) + else if (pathParts.Length == 0) { // invalid path report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); @@ -571,7 +618,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // don't track unless we are configured to fix if (options.FixIssues) + { validNodes.Add(node.NodeId, node); + } } } @@ -582,11 +631,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // iterate all valid nodes to see if these are parents for invalid nodes foreach (var (nodeId, node) in validNodes) { - if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue; + if (!nodesToRebuild.TryGetValue(nodeId, out List? invalidNodes)) + { + continue; + } // now we can try to rebuild the invalid paths. - - foreach (var invalidNode in invalidNodes) + foreach (NodeDto invalidNode in invalidNodes) { invalidNode.Level = (short)(node.Level + 1); invalidNode.Path = node.Path + "," + invalidNode.NodeId; @@ -594,11 +645,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } } - foreach (var node in updated) + foreach (NodeDto node in updated) { Database.Update(node); - if (report.TryGetValue(node.NodeId, out var entry)) + if (report.TryGetValue(node.NodeId, out ContentDataIntegrityReportEntry? entry)) + { entry.Fixed = true; + } } } @@ -607,30 +660,44 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) { - if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) + if (nodesToRebuild.TryGetValue(node.ParentId, out List? childIds)) + { childIds.Add(node); + } else + { nodesToRebuild[node.ParentId] = new List { node }; + } } // here, filter can be null and ordering cannot - protected IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, + protected IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, Func, IEnumerable> mapDtos, Sql? filter, Ordering? ordering) { - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } // start with base query, and apply the supplied IQuery - if (query == null) query = Query(); - var sql = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); + if (query == null) + { + query = Query(); + } + + Sql sql = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); // sort and filter sql = PreparePageSql(sql, filter, ordering); // get a page of DTOs and the total count - var pagedResult = Database.Page(pageIndex + 1, pageSize, sql); + Page? pagedResult = Database.Page(pageIndex + 1, pageSize, sql); totalRecords = Convert.ToInt32(pagedResult.TotalItems); // map the DTOs and return @@ -641,13 +708,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement where T : class, IContentBase { var versions = new List(); - foreach (var temp in temps) + foreach (TempContent temp in temps) { versions.Add(temp.VersionId); if (temp.PublishedVersionId > 0) + { versions.Add(temp.PublishedVersionId); + } + } + + if (versions.Count == 0) + { + return new Dictionary(); } - if (versions.Count == 0) return new Dictionary(); // TODO: This is a bugger of a query and I believe is the main issue with regards to SQL performance drain when querying content // which is done when rebuilding caches/indexes/etc... in bulk. We are using an "IN" query on umbracoPropertyData.VersionId @@ -666,7 +739,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // get PropertyDataDto distinct PropertyTypeDto var allPropertyTypeIds = allPropertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList(); - var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => + IEnumerable allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => SqlContext.Sql() .Select(r => r.Select(x => x.DataTypeDto)) .From() @@ -675,15 +748,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // index the types for perfs, and assign to PropertyDataDto var indexedPropertyTypeDtos = allPropertyTypeDtos.ToDictionary(x => x.Id, x => x); - foreach (var a in allPropertyDataDtos) + foreach (PropertyDataDto a in allPropertyDataDtos) + { a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; + } // now we have // - the definitions // - all property data dtos // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems) // and we need to build the proper property collections - return GetPropertyCollections(temps, allPropertyDataDtos); } @@ -696,15 +770,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // index PropertyDataDto per versionId for perfs // merge edited and published dtos var indexedPropertyDataDtos = new Dictionary>(); - foreach (var dto in allPropertyDataDtos) + foreach (PropertyDataDto dto in allPropertyDataDtos) { var versionId = dto.VersionId; - if (indexedPropertyDataDtos.TryGetValue(versionId, out var list) == false) + if (indexedPropertyDataDtos.TryGetValue(versionId, out List? list) == false) + { indexedPropertyDataDtos[versionId] = list = new List(); + } + list.Add(dto); } - foreach (var temp in temps) + foreach (TempContent temp in temps) { // compositionProperties is the property types for the entire composition // use an index for perfs @@ -712,25 +789,39 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { continue; } - if (compositionPropertiesIndex.TryGetValue(temp.ContentType.Id, out var compositionProperties) == false) + + if (compositionPropertiesIndex.TryGetValue(temp.ContentType.Id, out IPropertyType[]? compositionProperties) == false) + { compositionPropertiesIndex[temp.ContentType.Id] = compositionProperties = temp.ContentType.CompositionPropertyTypes.ToArray(); + } // map the list of PropertyDataDto to a list of Property var propertyDataDtos = new List(); - if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out var propertyDataDtos1)) + if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out List? propertyDataDtos1)) { propertyDataDtos.AddRange(propertyDataDtos1); - if (temp.VersionId == temp.PublishedVersionId) // dirty corner case + + // dirty corner case + if (temp.VersionId == temp.PublishedVersionId) + { propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1))); + } } - if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out var propertyDataDtos2)) + + if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out List? propertyDataDtos2)) + { propertyDataDtos.AddRange(propertyDataDtos2); + } + var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList(); if (result.ContainsKey(temp.VersionId)) { if (ContentRepositoryBase.ThrowOnWarning) + { throw new InvalidOperationException($"The query returned multiple property sets for content {temp.Id}, {temp.ContentType.Name}"); + } + Logger.LogWarning("The query returned multiple property sets for content {ContentId}, {ContentTypeName}", temp.Id, temp.ContentType.Name); } @@ -746,7 +837,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected string InsertBefore(string s, string atToken, string insert) { var pos = s.InvariantIndexOf(atToken); - if (pos < 0) throw new Exception($"Could not find token \"{atToken}\"."); + if (pos < 0) + { + throw new Exception($"Could not find token \"{atToken}\"."); + } + return s.Insert(pos, insert); } @@ -775,9 +870,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]" // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]" - - var matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); - var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); + MatchCollection matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); + Match? match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); return match == null ? field : match.Groups[2].Value; } @@ -804,7 +898,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// protected void OnUowRefreshedEntity(INotification notification) => _eventAggregator.Publish(notification); - protected void OnUowRemovingEntity(IContentBase entity) => _eventAggregator.Publish(new ScopedEntityRemoveNotification(entity, new EventMessages())); #endregion @@ -879,55 +972,51 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected virtual string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql + SqlTemplate? template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text!, "name")) .From() - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId")) - ); + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); - var sql = template.Sql(NodeObjectTypeId, parentId); - var names = Database.Fetch(sql); + Sql sql = template.Sql(NodeObjectTypeId, parentId); + List? names = Database.Fetch(sql); return SimilarNodeName.GetUniqueName(names, id, nodeName); } protected virtual int GetNewChildSortOrder(int parentId, int first) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql + SqlTemplate? template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql .Select("MAX(sortOrder)") .From() - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId")) - ); + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); - var sql = template.Sql(NodeObjectTypeId, parentId); + Sql sql = template.Sql(NodeObjectTypeId, parentId); var sortOrder = Database.ExecuteScalar(sql); - return (sortOrder + 1) ?? first; + return sortOrder + 1 ?? first; } protected virtual NodeDto GetParentNodeDto(int parentId) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql + SqlTemplate? template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql .Select() .From() - .Where(x => x.NodeId == SqlTemplate.Arg("parentId")) - ); + .Where(x => x.NodeId == SqlTemplate.Arg("parentId"))); - var sql = template.Sql(parentId); - var nodeDto = Database.First(sql); + Sql sql = template.Sql(parentId); + NodeDto? nodeDto = Database.First(sql); return nodeDto; } protected virtual int GetReservedId(Guid uniqueId) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql .Select(x => x.NodeId) .From() - .Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation) - ); + .Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)); - var sql = template.Sql(new { uniqueId }); + Sql sql = template.Sql(new { uniqueId }); var id = Database.ExecuteScalar(sql); return id ?? 0; @@ -953,39 +1042,47 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var trackedRelations = new List(); trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors)); - //First delete all auto-relations for this entity - RelationRepository.DeleteByParent(entity.Id, Cms.Core.Constants.Conventions.RelationTypes.AutomaticRelationTypes); + // First delete all auto-relations for this entity + RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes); - if (trackedRelations.Count == 0) return; + if (trackedRelations.Count == 0) + { + return; + } trackedRelations = trackedRelations.Distinct().ToList(); var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi) .ToDictionary(x => (Udi)x!, x => x!.Guid); - //lookup in the DB all INT ids for the GUIDs and chuck into a dictionary + // lookup in the DB all INT ids for the GUIDs and chuck into a dictionary var keyToIds = Database.Fetch(Sql().Select(x => x.NodeId, x => x.UniqueId).From().WhereIn(x => x.UniqueId, udiToGuids.Values)) .ToDictionary(x => x.UniqueId, x => x.NodeId); var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty())? .ToDictionary(x => x.Alias, x => x); - var toSave = trackedRelations.Select(rel => + IEnumerable toSave = trackedRelations.Select(rel => { - if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) + if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out IRelationType? relationType)) + { throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); + } - if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) + if (!udiToGuids.TryGetValue(rel.Udi, out Guid guid)) + { return null; // This shouldn't happen! + } if (!keyToIds.TryGetValue(guid, out var id)) + { return null; // This shouldn't happen! + } return new ReadOnlyRelation(entity.Id, id, relationType.Id); }).WhereNotNull(); // Save bulk relations RelationRepository.SaveBulk(toSave); - } /// @@ -1001,11 +1098,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected void InsertPropertyValues(TEntity entity, int publishedVersionId, out bool edited, out HashSet? editedCultures) { // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); - foreach (var propertyDataDto in propertyDataDtos) + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + foreach (PropertyDataDto? propertyDataDto in propertyDataDtos) { Database.Insert(propertyDataDto); } + // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. } @@ -1024,23 +1122,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // Replace the property data. // Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic // and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues. - - var propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == versionId).ForUpdate(); - var existingPropData = Database.Fetch(propDataSql); + Sql propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == versionId).ForUpdate(); + List? existingPropData = Database.Fetch(propDataSql); var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string? segment), PropertyDataDto>(); var existingPropDataIds = new List(); - foreach (var p in existingPropData) + foreach (PropertyDataDto? p in existingPropData) { existingPropDataIds.Add(p.Id); propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p; } - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); - foreach (var propertyDataDto in propertyDataDtos) + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) { - // Check if this already exists and update, else insert a new one - if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out var propData)) + if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out PropertyDataDto? propData)) { propertyDataDto.Id = propData.Id; Database.Update(propertyDataDto); @@ -1055,12 +1152,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // track which ones have been processed existingPropDataIds.Remove(propertyDataDto.Id); } + // For any remaining that haven't been processed they need to be deleted if (existingPropDataIds.Count > 0) { Database.Execute(SqlContext.Sql().Delete().WhereIn(x => x.Id, existingPropDataIds)); } - } private class NodeIdKey diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 3a305a6371..72ebd3a79a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -8,409 +5,413 @@ using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -using Enumerable = System.Linq.Enumerable; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Implements . +/// +internal class ContentTypeCommonRepository : IContentTypeCommonRepository { + private const string CacheKey = + "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; + + private readonly AppCaches _appCaches; + private readonly IScopeAccessor _scopeAccessor; + private readonly IShortStringHelper _shortStringHelper; + private readonly ITemplateRepository _templateRepository; + /// - /// Implements . + /// Initializes a new instance of the class. /// - internal class ContentTypeCommonRepository : IContentTypeCommonRepository + public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, + AppCaches appCaches, IShortStringHelper shortStringHelper) { - private const string CacheKey = - "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; + _scopeAccessor = scopeAccessor; + _templateRepository = templateRepository; + _appCaches = appCaches; + _shortStringHelper = shortStringHelper; + } - private readonly AppCaches _appCaches; - private readonly IScopeAccessor _scopeAccessor; - private readonly IShortStringHelper _shortStringHelper; - private readonly ITemplateRepository _templateRepository; + private IScope? AmbientScope => _scopeAccessor.AmbientScope; - /// - /// Initializes a new instance of the class. - /// - public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, - AppCaches appCaches, IShortStringHelper shortStringHelper) + private IUmbracoDatabase? Database => AmbientScope?.Database; + + private ISqlContext? SqlContext => AmbientScope?.SqlContext; + + // private Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); + // private ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; + // private IQuery Query() => SqlContext.Query(); + + /// + public IEnumerable? GetAllTypes() => + + // use a 5 minutes sliding cache - same as FullDataSet cache policy + _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); + + /// + public void ClearCache() => _appCaches.RuntimeCache.Clear(CacheKey); + + private Sql? Sql() => SqlContext?.Sql(); + + private IEnumerable GetAllTypesInternal() + { + var contentTypes = new Dictionary(); + + // get content types + Sql? sql1 = Sql()? + .Select(r => r.Select(x => x.NodeDto)) + .From() + .InnerJoin().On((ct, n) => ct.NodeId == n.NodeId) + .OrderBy(x => x.NodeId); + + List? contentTypeDtos = Database?.Fetch(sql1); + + // get allowed content types + Sql? sql2 = Sql()? + .Select() + .From() + .OrderBy(x => x.Id); + + List? allowedDtos = Database?.Fetch(sql2); + + if (contentTypeDtos is null) { - _scopeAccessor = scopeAccessor; - _templateRepository = templateRepository; - _appCaches = appCaches; - _shortStringHelper = shortStringHelper; - } - - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; - - private IUmbracoDatabase? Database => AmbientScope?.Database; - - private ISqlContext? SqlContext => AmbientScope?.SqlContext; - //private Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); - //private ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; - //private IQuery Query() => SqlContext.Query(); - - /// - public IEnumerable? GetAllTypes() => - // use a 5 minutes sliding cache - same as FullDataSet cache policy - _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); - - /// - public void ClearCache() => _appCaches.RuntimeCache.Clear(CacheKey); - - private Sql? Sql() => SqlContext?.Sql(); - - private IEnumerable GetAllTypesInternal() - { - var contentTypes = new Dictionary(); - - // get content types - Sql? sql1 = Sql()? - .Select(r => r.Select(x => x.NodeDto)) - .From() - .InnerJoin().On((ct, n) => ct.NodeId == n.NodeId) - .OrderBy(x => x.NodeId); - - List? contentTypeDtos = Database?.Fetch(sql1); - - // get allowed content types - Sql? sql2 = Sql()? - .Select() - .From() - .OrderBy(x => x.Id); - - List? allowedDtos = Database?.Fetch(sql2); - - if (contentTypeDtos is null) - { - return contentTypes.Values; - } - // prepare - // note: same alias could be used for media, content... but always different ids = ok - var aliases = Enumerable.ToDictionary(contentTypeDtos, x => x.NodeId, x => x.Alias); - - // create - var allowedDtoIx = 0; - foreach (ContentTypeDto contentTypeDto in contentTypeDtos) - { - // create content type - IContentTypeComposition contentType; - if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MediaType) - { - contentType = ContentTypeFactory.BuildMediaTypeEntity(_shortStringHelper, contentTypeDto); - } - else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.DocumentType) - { - contentType = ContentTypeFactory.BuildContentTypeEntity(_shortStringHelper, contentTypeDto); - } - else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) - { - contentType = ContentTypeFactory.BuildMemberTypeEntity(_shortStringHelper, contentTypeDto); - } - else - { - throw new PanicException( - $"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); - } - - contentTypes.Add(contentType.Id, contentType); - - // map allowed content types - var allowedContentTypes = new List(); - while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) - { - ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; - if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) - { - continue; - } - - allowedContentTypes.Add(new ContentTypeSort(new Lazy(() => allowedDto.AllowedId), - allowedDto.SortOrder, alias!)); - allowedDtoIx++; - } - - contentType.AllowedContentTypes = allowedContentTypes; - } - - MapTemplates(contentTypes); - MapComposition(contentTypes); - MapGroupsAndProperties(contentTypes); - MapHistoryCleanup(contentTypes); - - // finalize - foreach (IContentTypeComposition contentType in contentTypes.Values) - { - contentType.ResetDirtyProperties(false); - } - return contentTypes.Values; } - private void MapHistoryCleanup(Dictionary contentTypes) + // prepare + // note: same alias could be used for media, content... but always different ids = ok + var aliases = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.Alias); + + // create + var allowedDtoIx = 0; + foreach (ContentTypeDto contentTypeDto in contentTypeDtos) { - // get templates - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ContentTypeId); - - var contentVersionCleanupPolicyDtos = Database?.Fetch(sql1); - - var contentVersionCleanupPolicyDictionary = - contentVersionCleanupPolicyDtos?.ToDictionary(x => x.ContentTypeId); - foreach (IContentTypeComposition c in contentTypes.Values) + // create content type + IContentTypeComposition contentType; + if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MediaType) { - if (!(c is ContentType contentType)) + contentType = ContentTypeFactory.BuildMediaTypeEntity(_shortStringHelper, contentTypeDto); + } + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.DocumentType) + { + contentType = ContentTypeFactory.BuildContentTypeEntity(_shortStringHelper, contentTypeDto); + } + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) + { + contentType = ContentTypeFactory.BuildMemberTypeEntity(_shortStringHelper, contentTypeDto); + } + else + { + throw new PanicException( + $"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); + } + + contentTypes.Add(contentType.Id, contentType); + + // map allowed content types + var allowedContentTypes = new List(); + while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) + { + ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; + if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) { continue; } - var historyCleanup = new HistoryCleanup(); - - if (contentVersionCleanupPolicyDictionary is not null && contentVersionCleanupPolicyDictionary.TryGetValue(contentType.Id, out var versionCleanup)) - { - historyCleanup.PreventCleanup = versionCleanup.PreventCleanup; - historyCleanup.KeepAllVersionsNewerThanDays = versionCleanup.KeepAllVersionsNewerThanDays; - historyCleanup.KeepLatestVersionPerDayForDays = versionCleanup.KeepLatestVersionPerDayForDays; - } - - contentType.HistoryCleanup = historyCleanup; + allowedContentTypes.Add(new ContentTypeSort( + new Lazy(() => allowedDto.AllowedId), + allowedDto.SortOrder, alias!)); + allowedDtoIx++; } + + contentType.AllowedContentTypes = allowedContentTypes; } - private void MapTemplates(Dictionary contentTypes) + MapTemplates(contentTypes); + MapComposition(contentTypes); + MapGroupsAndProperties(contentTypes); + MapHistoryCleanup(contentTypes); + + // finalize + foreach (IContentTypeComposition contentType in contentTypes.Values) { - // get templates - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ContentTypeNodeId); - - List? templateDtos = Database?.Fetch(sql1); - //var templates = templateRepository.GetMany(templateDtos.Select(x => x.TemplateNodeId).ToArray()).ToDictionary(x => x.Id, x => x); - var allTemplates = _templateRepository.GetMany(); - if (allTemplates is null) - { - return; - } - var templates = Enumerable.ToDictionary(allTemplates, x => x.Id, x => x); - var templateDtoIx = 0; - - foreach (IContentTypeComposition c in contentTypes.Values) - { - if (!(c is IContentType contentType)) - { - continue; - } - - // map allowed templates - var allowedTemplates = new List(); - var defaultTemplateId = 0; - while (templateDtoIx < templateDtos?.Count && - templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) - { - ContentTypeTemplateDto allowedDto = templateDtos[templateDtoIx]; - templateDtoIx++; - if (!templates.TryGetValue(allowedDto.TemplateNodeId, out ITemplate? template)) - { - continue; - } - - allowedTemplates.Add(template); - - if (allowedDto.IsDefault) - { - defaultTemplateId = template.Id; - } - } - - contentType.AllowedTemplates = allowedTemplates; - contentType.DefaultTemplateId = defaultTemplateId; - } + contentType.ResetDirtyProperties(false); } - private void MapComposition(IDictionary contentTypes) + return contentTypes.Values; + } + + private void MapHistoryCleanup(Dictionary contentTypes) + { + // get templates + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ContentTypeId); + + List? contentVersionCleanupPolicyDtos = + Database?.Fetch(sql1); + + var contentVersionCleanupPolicyDictionary = + contentVersionCleanupPolicyDtos?.ToDictionary(x => x.ContentTypeId); + foreach (IContentTypeComposition c in contentTypes.Values) { - // get parent/child - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ChildId); - - List? compositionDtos = Database?.Fetch(sql1); - - // map - var compositionIx = 0; - foreach (IContentTypeComposition contentType in contentTypes.Values) + if (!(c is ContentType contentType)) { - while (compositionIx < compositionDtos?.Count && - compositionDtos[compositionIx].ChildId == contentType.Id) - { - ContentType2ContentTypeDto parentDto = compositionDtos[compositionIx]; - compositionIx++; - - if (!contentTypes.TryGetValue(parentDto.ParentId, out IContentTypeComposition? parentContentType)) - { - continue; - } - - contentType.AddContentType(parentContentType); - } - } - } - - private void MapGroupsAndProperties(IDictionary contentTypes) - { - Sql? sql1 = Sql()? - .Select() - .From() - .InnerJoin() - .On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) - .OrderBy(x => x.NodeId) - .AndBy(x => x.SortOrder, x => x.Id); - - List? groupDtos = Database?.Fetch(sql1); - - Sql? sql2 = Sql()? - .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) - .AndSelect() - .From() - .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) - .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) - .InnerJoin() - .On((pt, ct) => pt.ContentTypeId == ct.NodeId) - .LeftJoin() - .On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) - .LeftJoin() - .On((pt, mpt) => pt.Id == mpt.PropertyTypeId) - .OrderBy(x => x.NodeId) - .AndBy< - PropertyTypeGroupDto>(x => x.SortOrder, - x => x.Id) // NULLs will come first or last, never mind, we deal with it below - .AndBy(x => x.SortOrder, x => x.Id); - - List? propertyDtos = Database?.Fetch(sql2); - Dictionary builtinProperties = - ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - - var groupIx = 0; - var propertyIx = 0; - foreach (IContentTypeComposition contentType in contentTypes.Values) - { - // only IContentType is publishing - var isPublishing = contentType is IContentType; - - // get group-less properties (in case NULL is ordered first) - var noGroupPropertyTypes = new PropertyTypeCollection(isPublishing); - while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == null) - { - noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); - propertyIx++; - } - - // get groups and their properties - var groupCollection = new PropertyGroupCollection(); - while (groupIx < groupDtos?.Count && groupDtos[groupIx].ContentTypeNodeId == contentType.Id) - { - PropertyGroup group = MapPropertyGroup(groupDtos[groupIx], isPublishing); - groupCollection.Add(group); - groupIx++; - - while (propertyIx < propertyDtos?.Count && - propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) - { - group.PropertyTypes?.Add(MapPropertyType(contentType, propertyDtos[propertyIx], - builtinProperties)); - propertyIx++; - } - } - - contentType.PropertyGroups = groupCollection; - - // get group-less properties (in case NULL is ordered last) - while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == null) - { - noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); - propertyIx++; - } - - contentType.NoGroupPropertyTypes = noGroupPropertyTypes; - - // ensure builtin properties - if (contentType is IMemberType memberType) - { - // ensure that property types exist (ok if they already exist) - foreach ((var alias, PropertyType propertyType) in builtinProperties) - { - - var added = memberType.AddPropertyType(propertyType, - Constants.Conventions.Member.StandardPropertiesGroupAlias, - Constants.Conventions.Member.StandardPropertiesGroupName); - - if (added) - { - memberType.SetIsSensitiveProperty(alias, false); - memberType.SetMemberCanEditProperty(alias, false); - memberType.SetMemberCanViewProperty(alias, false); - } - } - } - } - } - - private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) => - new PropertyGroup(new PropertyTypeCollection(isPublishing)) - { - Id = dto.Id, - Key = dto.UniqueId, - Type = (PropertyGroupType)dto.Type, - Name = dto.Text, - Alias = dto.Alias, - SortOrder = dto.SortOrder - }; - - private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, - IDictionary builtinProperties) - { - var groupId = dto.PropertyTypeGroupId; - - var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias!, out PropertyType? propertyType); - ValueStorageType storageType = readonlyStorageType - ? propertyType!.ValueStorageType - : Enum.Parse(dto.DataTypeDto.DbType); - - if (contentType is IMemberType memberType && dto.Alias is not null) - { - memberType.SetIsSensitiveProperty(dto.Alias, dto.IsSensitive); - memberType.SetMemberCanEditProperty(dto.Alias, dto.CanEdit); - memberType.SetMemberCanViewProperty(dto.Alias, dto.ViewOnProfile); + continue; } - return new - PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, - dto.Alias) - { - Description = dto.Description, - DataTypeId = dto.DataTypeId, - DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, - Id = dto.Id, - Key = dto.UniqueId, - Mandatory = dto.Mandatory, - MandatoryMessage = dto.MandatoryMessage, - Name = dto.Name ?? string.Empty, - PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, - SortOrder = dto.SortOrder, - ValidationRegExp = dto.ValidationRegExp, - ValidationRegExpMessage = dto.ValidationRegExpMessage, - Variations = (ContentVariation)dto.Variations, - LabelOnTop = dto.LabelOnTop - }; + var historyCleanup = new HistoryCleanup(); + + if (contentVersionCleanupPolicyDictionary is not null && + contentVersionCleanupPolicyDictionary.TryGetValue( + contentType.Id, + out ContentVersionCleanupPolicyDto? versionCleanup)) + { + historyCleanup.PreventCleanup = versionCleanup.PreventCleanup; + historyCleanup.KeepAllVersionsNewerThanDays = versionCleanup.KeepAllVersionsNewerThanDays; + historyCleanup.KeepLatestVersionPerDayForDays = versionCleanup.KeepLatestVersionPerDayForDays; + } + + contentType.HistoryCleanup = historyCleanup; } } + + private void MapTemplates(Dictionary contentTypes) + { + // get templates + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ContentTypeNodeId); + + List? templateDtos = Database?.Fetch(sql1); + + // var templates = templateRepository.GetMany(templateDtos.Select(x => x.TemplateNodeId).ToArray()).ToDictionary(x => x.Id, x => x); + IEnumerable? allTemplates = _templateRepository.GetMany(); + + var templates = allTemplates.ToDictionary(x => x.Id, x => x); + var templateDtoIx = 0; + + foreach (IContentTypeComposition c in contentTypes.Values) + { + if (!(c is IContentType contentType)) + { + continue; + } + + // map allowed templates + var allowedTemplates = new List(); + var defaultTemplateId = 0; + while (templateDtoIx < templateDtos?.Count && + templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) + { + ContentTypeTemplateDto allowedDto = templateDtos[templateDtoIx]; + templateDtoIx++; + if (!templates.TryGetValue(allowedDto.TemplateNodeId, out ITemplate? template)) + { + continue; + } + + allowedTemplates.Add(template); + + if (allowedDto.IsDefault) + { + defaultTemplateId = template.Id; + } + } + + contentType.AllowedTemplates = allowedTemplates; + contentType.DefaultTemplateId = defaultTemplateId; + } + } + + private void MapComposition(IDictionary contentTypes) + { + // get parent/child + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ChildId); + + List? compositionDtos = Database?.Fetch(sql1); + + // map + var compositionIx = 0; + foreach (IContentTypeComposition contentType in contentTypes.Values) + { + while (compositionIx < compositionDtos?.Count && + compositionDtos[compositionIx].ChildId == contentType.Id) + { + ContentType2ContentTypeDto parentDto = compositionDtos[compositionIx]; + compositionIx++; + + if (!contentTypes.TryGetValue(parentDto.ParentId, out IContentTypeComposition? parentContentType)) + { + continue; + } + + contentType.AddContentType(parentContentType); + } + } + } + + private void MapGroupsAndProperties(IDictionary contentTypes) + { + Sql? sql1 = Sql()? + .Select() + .From() + .InnerJoin() + .On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) + .OrderBy(x => x.NodeId) + .AndBy(x => x.SortOrder, x => x.Id); + + List? groupDtos = Database?.Fetch(sql1); + + Sql? sql2 = Sql()? + .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) + .AndSelect() + .From() + .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) + .InnerJoin() + .On((pt, ct) => pt.ContentTypeId == ct.NodeId) + .LeftJoin() + .On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) + .LeftJoin() + .On((pt, mpt) => pt.Id == mpt.PropertyTypeId) + .OrderBy(x => x.NodeId) + .AndBy< + PropertyTypeGroupDto>( + x => x.SortOrder, + x => x.Id) // NULLs will come first or last, never mind, we deal with it below + .AndBy(x => x.SortOrder, x => x.Id); + + List? propertyDtos = Database?.Fetch(sql2); + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + + var groupIx = 0; + var propertyIx = 0; + foreach (IContentTypeComposition contentType in contentTypes.Values) + { + // only IContentType is publishing + var isPublishing = contentType is IContentType; + + // get group-less properties (in case NULL is ordered first) + var noGroupPropertyTypes = new PropertyTypeCollection(isPublishing); + while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) + { + noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + propertyIx++; + } + + // get groups and their properties + var groupCollection = new PropertyGroupCollection(); + while (groupIx < groupDtos?.Count && groupDtos[groupIx].ContentTypeNodeId == contentType.Id) + { + PropertyGroup group = MapPropertyGroup(groupDtos[groupIx], isPublishing); + groupCollection.Add(group); + groupIx++; + + while (propertyIx < propertyDtos?.Count && + propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) + { + group.PropertyTypes?.Add(MapPropertyType(contentType, propertyDtos[propertyIx], + builtinProperties)); + propertyIx++; + } + } + + contentType.PropertyGroups = groupCollection; + + // get group-less properties (in case NULL is ordered last) + while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) + { + noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + propertyIx++; + } + + contentType.NoGroupPropertyTypes = noGroupPropertyTypes; + + // ensure builtin properties + if (contentType is IMemberType memberType) + { + // ensure that property types exist (ok if they already exist) + foreach ((var alias, PropertyType propertyType) in builtinProperties) + { + var added = memberType.AddPropertyType( + propertyType, + Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); + + if (added) + { + memberType.SetIsSensitiveProperty(alias, false); + memberType.SetMemberCanEditProperty(alias, false); + memberType.SetMemberCanViewProperty(alias, false); + } + } + } + } + } + + private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) => + new(new PropertyTypeCollection(isPublishing)) + { + Id = dto.Id, + Key = dto.UniqueId, + Type = (PropertyGroupType)dto.Type, + Name = dto.Text, + Alias = dto.Alias, + SortOrder = dto.SortOrder, + }; + + private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, + IDictionary builtinProperties) + { + var groupId = dto.PropertyTypeGroupId; + + var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias!, out PropertyType? propertyType); + ValueStorageType storageType = readonlyStorageType + ? propertyType!.ValueStorageType + : Enum.Parse(dto.DataTypeDto.DbType); + + if (contentType is IMemberType memberType && dto.Alias is not null) + { + memberType.SetIsSensitiveProperty(dto.Alias, dto.IsSensitive); + memberType.SetMemberCanEditProperty(dto.Alias, dto.CanEdit); + memberType.SetMemberCanViewProperty(dto.Alias, dto.ViewOnProfile); + } + + return new + PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, + dto.Alias) + { + Description = dto.Description, + DataTypeId = dto.DataTypeId, + DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, + Id = dto.Id, + Key = dto.UniqueId, + Mandatory = dto.Mandatory, + MandatoryMessage = dto.MandatoryMessage, + Name = dto.Name ?? string.Empty, + PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, + SortOrder = dto.SortOrder, + ValidationRegExp = dto.ValidationRegExp, + ValidationRegExpMessage = dto.ValidationRegExpMessage, + Variations = (ContentVariation)dto.Variations, + LabelOnTop = dto.LabelOnTop, + }; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9859b1e69a..9e35999071 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,306 +12,306 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository + public ContentTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) { - public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) - { } + } - protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; + protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.DocumentType; + + /// + public IEnumerable GetByQuery(IQuery query) + { + var ints = PerformGetByQuery(query).ToArray(); + return ints.Length > 0 ? GetMany(ints) : Enumerable.Empty(); + } + + /// + /// Gets all property type aliases. + /// + /// + public IEnumerable GetAllPropertyTypeAliases() => + Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); + + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) + { + Sql sql = Sql() + .Select("cmsContentType.alias") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId); + + if (objectTypes.Any()) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + sql = sql.WhereIn(dto => dto.NodeObjectType, objectTypes); } - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job + return Database.Fetch(sql); + } - // TODO: the filtering is highly inefficient as we deep-clone everything - // there should be a way to GetMany(predicate) right from the cache policy! - // and ah, well, this all caching should be refactored + the cache refreshers - // should to repository.Clear() not deal with magic caches by themselves - - protected override IContentType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IContentType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); - - protected override IContentType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - - protected override IEnumerable? GetAllWithFullCachePolicy() + public IEnumerable GetAllContentTypeIds(string[] aliases) + { + if (aliases.Length == 0) { - return CommonRepository.GetAllTypes()?.OfType(); + return Enumerable.Empty(); } - protected override IEnumerable? PerformGetAll(params Guid[]? ids) + Sql sql = Sql() + .Select(x => x.NodeId) + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => aliases.Contains(dto.Alias)); + + return Database.Fetch(sql); + } + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job + + // TODO: the filtering is highly inefficient as we deep-clone everything + // there should be a way to GetMany(predicate) right from the cache policy! + // and ah, well, this all caching should be refactored + the cache refreshers + // should to repository.Clear() not deal with magic caches by themselves + protected override IContentType? PerformGet(int id) + => GetMany().FirstOrDefault(x => x.Id == id); + + protected override IContentType? PerformGet(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id); + + protected override IContentType? PerformGet(string alias) + => GetMany().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + protected override bool PerformExists(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id) != null; + + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + IEnumerable all = GetMany(); + return ids?.Any() ?? false ? all.Where(x => ids.Contains(x.Key)) : all; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate(); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 + ? GetMany(ids).OrderBy(x => x.Name) + : Enumerable.Empty(); + } + + protected IEnumerable PerformGetByQuery(IQuery query) + { + // used by DataTypeService to remove properties + // from content types if they have a deleted data type - see + // notes in DataTypeService.Delete as it's a bit weird + Sql sqlClause = Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.PropertyTypeGroupId) + .InnerJoin() + .On(left => left.DataTypeId, right => right.NodeId); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate() + .OrderBy(x => x.PropertyTypeGroupId); + + return Database + .FetchOneToMany(x => x.PropertyTypeDtos, sql) + .Select(x => x.ContentTypeNodeId).Distinct(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(x => x.NodeId); + + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } + + /// + /// Deletes a content type + /// + /// + /// + /// First checks for children and removes those first + /// + protected override void PersistDeletedItem(IContentType entity) + { + IQuery query = Query().Where(x => x.ParentId == entity.Id); + IEnumerable children = Get(query); + foreach (IContentType child in children) { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; + PersistDeletedItem(child); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate(); - var ids = Database.Fetch(sql).Distinct().ToArray(); + // Before we call the base class methods to run all delete clauses, we need to first + // delete all of the property data associated with this document type. Normally this will + // be done in the ContentTypeService by deleting all associated content first, but in some cases + // like when we switch a document type, there is property data left over that is linked + // to the previous document type. So we need to ensure it's removed. + Sql sql = Sql() + .Select("DISTINCT " + Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid") + .From() + .InnerJoin() + .On(dto => dto.PropertyTypeId, dto => dto.Id) + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.ContentTypeId) + .Where(dto => dto.NodeId == entity.Id); - return ids.Length > 0 ? GetMany(ids)?.OrderBy(x => x.Name) ?? Enumerable.Empty() : Enumerable.Empty(); + // Delete all PropertyData where propertytypeid EXISTS in the subquery above + Database.Execute(SqlSyntax.GetDeleteSubquery(Constants.DatabaseSchema.Tables.PropertyData, "propertytypeid", sql)); + + base.PersistDeletedItem(entity); + } + + protected override void PersistNewItem(IContentType entity) + { + if (string.IsNullOrWhiteSpace(entity.Alias)) + { + var ex = new Exception( + $"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + Logger.LogError( + "ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + entity.Name); + throw ex; } - /// - public IEnumerable GetByQuery(IQuery query) + entity.AddingEntity(); + + PersistNewBaseContentType(entity); + PersistTemplates(entity, false); + PersistHistoryCleanup(entity); + + entity.ResetDirtyProperties(); + } + + protected void PersistTemplates(IContentType entity, bool clearAll) + { + // remove and insert, if required + Database.Delete("WHERE contentTypeNodeId = @Id", new { entity.Id }); + + // we could do it all in foreach if we assume that the default template is an allowed template?? + var defaultTemplateId = entity.DefaultTemplateId; + if (defaultTemplateId > 0) { - var ints = PerformGetByQuery(query).ToArray(); - return ints.Length > 0 ? GetMany(ints) ?? Enumerable.Empty() : Enumerable.Empty(); - } - - protected IEnumerable PerformGetByQuery(IQuery query) - { - // used by DataTypeService to remove properties - // from content types if they have a deleted data type - see - // notes in DataTypeService.Delete as it's a bit weird - - var sqlClause = Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.PropertyTypeGroupId) - .InnerJoin() - .On(left => left.DataTypeId, right => right.NodeId); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.PropertyTypeGroupId); - - return Database - .FetchOneToMany(x => x.PropertyTypeDtos, sql) - .Select(x => x.ContentTypeNodeId).Distinct(); - } - - /// - /// Gets all property type aliases. - /// - /// - public IEnumerable GetAllPropertyTypeAliases() - { - return Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); - } - - /// - /// Gets all content type aliases - /// - /// - /// If this list is empty, it will return all content type aliases for media, members and content, otherwise - /// it will only return content type aliases for the object types specified - /// - /// - public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) - { - var sql = Sql() - .Select("cmsContentType.alias") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId); - - if (objectTypes.Any()) + Database.Insert(new ContentTypeTemplateDto { - sql = sql.WhereIn(dto => dto.NodeObjectType, objectTypes); - } - - return Database.Fetch(sql); + ContentTypeNodeId = entity.Id, TemplateNodeId = defaultTemplateId, IsDefault = true, + }); } - public IEnumerable GetAllContentTypeIds(string[] aliases) + foreach (ITemplate template in entity.AllowedTemplates?.Where(x => x.Id != defaultTemplateId) ?? + Array.Empty()) { - if (aliases.Length == 0) return Enumerable.Empty(); - - var sql = Sql() - .Select(x => x.NodeId) - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => aliases.Contains(dto.Alias)); - - return Database.Fetch(sql); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(x => x.NodeId); - - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @id"); - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; - } - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DocumentType; - - /// - /// Deletes a content type - /// - /// - /// - /// First checks for children and removes those first - /// - protected override void PersistDeletedItem(IContentType entity) - { - var query = Query().Where(x => x.ParentId == entity.Id); - var children = Get(query); - if (children is not null) + Database.Insert(new ContentTypeTemplateDto { - foreach (var child in children) - { - PersistDeletedItem(child); - } - } + ContentTypeNodeId = entity.Id, TemplateNodeId = template.Id, IsDefault = false, + }); + } + } - //Before we call the base class methods to run all delete clauses, we need to first - // delete all of the property data associated with this document type. Normally this will - // be done in the ContentTypeService by deleting all associated content first, but in some cases - // like when we switch a document type, there is property data left over that is linked - // to the previous document type. So we need to ensure it's removed. - var sql = Sql() - .Select("DISTINCT " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid") - .From() - .InnerJoin() - .On(dto => dto.PropertyTypeId, dto => dto.Id) - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.ContentTypeId) - .Where(dto => dto.NodeId == entity.Id); + protected override void PersistUpdatedItem(IContentType entity) + { + ValidateAlias(entity); - //Delete all PropertyData where propertytypeid EXISTS in the subquery above - Database.Execute(SqlSyntax.GetDeleteSubquery(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData, "propertytypeid", sql)); + // Updates Modified date + entity.UpdatingEntity(); - base.PersistDeletedItem(entity); + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; } - protected override void PersistNewItem(IContentType entity) + PersistUpdatedBaseContentType(entity); + PersistTemplates(entity, true); + PersistHistoryCleanup(entity); + + entity.ResetDirtyProperties(); + } + + private void PersistHistoryCleanup(IContentType entity) + { + // historyCleanup property is not mandatory for api endpoint, handle the case where it's not present. + // DocumentTypeSave doesn't handle this for us like ContentType constructors do. + if (entity is IContentType entityWithHistoryCleanup) { - if (string.IsNullOrWhiteSpace(entity.Alias)) + var dto = new ContentVersionCleanupPolicyDto { - var ex = new Exception($"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - Logger.LogError("ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", entity.Name); - throw ex; - } - - entity.AddingEntity(); - - PersistNewBaseContentType(entity); - PersistTemplates(entity, false); - PersistHistoryCleanup(entity); - - entity.ResetDirtyProperties(); - } - - protected void PersistTemplates(IContentType entity, bool clearAll) - { - // remove and insert, if required - Database.Delete("WHERE contentTypeNodeId = @Id", new { Id = entity.Id }); - - // we could do it all in foreach if we assume that the default template is an allowed template?? - var defaultTemplateId = entity.DefaultTemplateId; - if (defaultTemplateId > 0) - { - Database.Insert(new ContentTypeTemplateDto - { - ContentTypeNodeId = entity.Id, - TemplateNodeId = defaultTemplateId, - IsDefault = true - }); - } - foreach (var template in entity.AllowedTemplates?.Where(x => x != null && x.Id != defaultTemplateId) ?? Array.Empty()) - { - Database.Insert(new ContentTypeTemplateDto - { - ContentTypeNodeId = entity.Id, - TemplateNodeId = template.Id, - IsDefault = false - }); - } - } - - protected override void PersistUpdatedItem(IContentType entity) - { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - PersistUpdatedBaseContentType(entity); - PersistTemplates(entity, true); - PersistHistoryCleanup(entity); - - entity.ResetDirtyProperties(); - } - - private void PersistHistoryCleanup(IContentType entity) - { - // historyCleanup property is not mandatory for api endpoint, handle the case where it's not present. - // DocumentTypeSave doesn't handle this for us like ContentType constructors do. - if (entity is IContentType entityWithHistoryCleanup) - { - ContentVersionCleanupPolicyDto dto = new ContentVersionCleanupPolicyDto() - { - ContentTypeId = entity.Id, - Updated = DateTime.Now, - PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, - KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, - KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays - }; - Database.InsertOrUpdate(dto); - } - + ContentTypeId = entity.Id, + Updated = DateTime.Now, + PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, + KeepAllVersionsNewerThanDays = + entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = + entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays, + }; + Database.InsertOrUpdate(dto); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 086542d307..3e92a4ae7f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Globalization; -using System.Linq; +using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -16,1480 +14,1578 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represent an abstract Repository for ContentType based repositories +/// +/// Exposes shared functionality +/// +internal abstract class ContentTypeRepositoryBase : EntityRepositoryBase, + IReadRepository + where TEntity : class, IContentTypeComposition { - /// - /// Represent an abstract Repository for ContentType based repositories - /// - /// Exposes shared functionality - /// - internal abstract class ContentTypeRepositoryBase : EntityRepositoryBase, - IReadRepository - where TEntity : class, IContentTypeComposition + private readonly IShortStringHelper _shortStringHelper; + + protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger> logger, IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger) { - private readonly IShortStringHelper _shortStringHelper; + _shortStringHelper = shortStringHelper; + CommonRepository = commonRepository; + LanguageRepository = languageRepository; + } - protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger> logger, IContentTypeCommonRepository commonRepository, - ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger) + protected IContentTypeCommonRepository CommonRepository { get; } + + protected ILanguageRepository LanguageRepository { get; } + + protected abstract bool SupportsPublishing { get; } + + /// + /// Gets the node object type for the repository's entity + /// + protected abstract Guid NodeObjectTypeId { get; } + + /// + /// Gets an Entity by Id + /// + /// + /// + public TEntity? Get(Guid id) => PerformGet(id); + + /// + /// Gets all entities of the specified type + /// + /// + /// + /// + /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same + /// signature as the main GetAll when there are no parameters + /// + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + PerformGetAll(ids) ?? Enumerable.Empty(); + + /// + /// Boolean indicating whether an Entity with the specified Id exists + /// + /// + /// + public bool Exists(Guid id) => PerformExists(id); + + public IEnumerable> Move(TEntity moving, EntityContainer? container) + { + var parentId = Constants.System.Root; + if (container != null) { - _shortStringHelper = shortStringHelper; - CommonRepository = commonRepository; - LanguageRepository = languageRepository; - } - - protected IContentTypeCommonRepository CommonRepository { get; } - protected ILanguageRepository LanguageRepository { get; } - protected abstract bool SupportsPublishing { get; } - - /// - /// Gets the node object type for the repository's entity - /// - protected abstract Guid NodeObjectTypeId { get; } - - public IEnumerable> Move(TEntity moving, EntityContainer container) - { - var parentId = Cms.Core.Constants.System.Root; - if (container != null) + // check path + if (string.Format(",{0},", container.Path).IndexOf( + string.Format(",{0},", moving.Id), + StringComparison.Ordinal) > -1) { - // check path - if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", moving.Id), - StringComparison.Ordinal) > -1) - throw new DataOperationException(MoveOperationStatusType - .FailedNotAllowedByPath); - - parentId = container.Id; + throw new DataOperationException(MoveOperationStatusType + .FailedNotAllowedByPath); } - // track moved entities - var moveInfo = new List> {new MoveEventInfo(moving, moving.Path, parentId)}; - - - // get the level delta (old pos to new pos) - var levelDelta = container == null - ? 1 - moving.Level - : container.Level + 1 - moving.Level; - - // move to parent (or -1), update path, save - moving.ParentId = parentId; - var movingPath = moving.Path + ","; // save before changing - moving.Path = (container == null ? Cms.Core.Constants.System.RootString : container.Path) + "," + moving.Id; - moving.Level = container == null ? 1 : container.Level + 1; - Save(moving); - - //update all descendants, update in order of level - var descendants = Get(Query().Where(type => type.Path.StartsWith(movingPath))); - var paths = new Dictionary(); - paths[moving.Id] = moving.Path; - - if (descendants is null) - { - return moveInfo; - } - foreach (var descendant in descendants.OrderBy(x => x.Level)) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; - descendant.Level += levelDelta; - - Save(descendant); - } - - return moveInfo; + parentId = container.Id; } - protected override IEnumerable PerformGetAll(params int[]? ids) + // track moved entities + var moveInfo = new List> { new(moving, moving.Path, parentId) }; + + // get the level delta (old pos to new pos) + var levelDelta = container == null + ? 1 - moving.Level + : container.Level + 1 - moving.Level; + + // move to parent (or -1), update path, save + moving.ParentId = parentId; + var movingPath = moving.Path + ","; // save before changing + moving.Path = (container == null ? Constants.System.RootString : container.Path) + "," + moving.Id; + moving.Level = container == null ? 1 : container.Level + 1; + Save(moving); + + // update all descendants, update in order of level + IEnumerable descendants = Get(Query().Where(type => type.Path.StartsWith(movingPath))); + var paths = new Dictionary { - var result = GetAllWithFullCachePolicy(); + [moving.Id] = moving.Path, + }; - // By default the cache policy will always want everything - // even GetMany(ids) gets everything and filters afterwards, - // however if we are using No Cache, we must still be able to support - // collections of Ids, so this is to work around that: - if (ids?.Any() ?? false) - { - return result?.Where(x => ids.Contains(x.Id)) ?? Enumerable.Empty(); - } + foreach (TEntity descendant in descendants.OrderBy(x => x.Level)) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - return result ?? Enumerable.Empty();; + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + + Save(descendant); } - protected abstract IEnumerable? GetAllWithFullCachePolicy(); + return moveInfo; + } - protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, - string propertyTypeAlias) + /// + /// Gets an Entity by alias + /// + /// + /// + public TEntity? Get(string alias) => PerformGet(alias); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + IEnumerable? result = GetAllWithFullCachePolicy(); + + // By default the cache policy will always want everything + // even GetMany(ids) gets everything and filters afterwards, + // however if we are using No Cache, we must still be able to support + // collections of Ids, so this is to work around that: + if (ids?.Any() ?? false) { - return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, propertyTypeAlias); + return result?.Where(x => ids.Contains(x.Id)) ?? Enumerable.Empty(); } - protected override void PersistDeletedItem(TEntity entity) - { - base.PersistDeletedItem(entity); - CommonRepository.ClearCache(); // always - } + return result ?? Enumerable.Empty(); + } - protected void PersistNewBaseContentType(IContentTypeComposition entity) - { - ValidateVariations(entity); + protected abstract IEnumerable? GetAllWithFullCachePolicy(); - var dto = ContentTypeFactory.BuildContentTypeDto(entity); + protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, + string propertyTypeAlias) => + new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, propertyTypeAlias); - //Cannot add a duplicate content type - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType + protected override void PersistDeletedItem(TEntity entity) + { + base.PersistDeletedItem(entity); + CommonRepository.ClearCache(); // always + } + + protected void PersistNewBaseContentType(IContentTypeComposition entity) + { + ValidateVariations(entity); + + ContentTypeDto dto = ContentTypeFactory.BuildContentTypeDto(entity); + + // Cannot add a duplicate content type + var exists = Database.ExecuteScalar( + @"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias AND umbracoNode.nodeObjectType = @objectType", - new {alias = entity.Alias, objectType = NodeObjectTypeId}); - if (exists > 0) + new { alias = entity.Alias, objectType = NodeObjectTypeId }); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); + } + + // Logic for setting Path, Level and SortOrder + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + var level = parent.Level + 1; + var sortOrder = + Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + + // Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) + ? Convert.ToInt32(Database.Insert(nodeDto)) + : Database.Update(nodeDto); + + // Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + // Update entity with correct values + entity.Id = nodeDto.NodeId; // Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // Insert new ContentType entry + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + // Insert ContentType composition in new table + foreach (IContentTypeComposition composition in entity.ContentTypeComposition) + { + if (composition.Id == entity.Id) { - throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); + continue; // Just to ensure that we aren't creating a reference to ourself. } - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new {ParentId = entity.ParentId}); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar( - "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new {ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId}); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) - ? Convert.ToInt32(Database.Insert(nodeDto)) - : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Insert new ContentType entry - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Insert ContentType composition in new table - foreach (var composition in entity.ContentTypeComposition) + if (composition.HasIdentity) { - if (composition.Id == entity.Id) - continue; //Just to ensure that we aren't creating a reference to ourself. - - if (composition.HasIdentity) - { - Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); - } - else - { - //Fallback for ContentTypes with no identity - var contentTypeDto = - Database.FirstOrDefault("WHERE alias = @Alias", - new {Alias = composition.Alias}); - if (contentTypeDto != null) - { - Database.Insert(new ContentType2ContentTypeDto - { - ParentId = contentTypeDto.NodeId, ChildId = entity.Id - }); - } - } + Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); } - - if (entity.AllowedContentTypes is not null) + else { - //Insert collection of allowed content types - foreach (var allowedContentType in entity.AllowedContentTypes) + // Fallback for ContentTypes with no identity + ContentTypeDto? contentTypeDto = + Database.FirstOrDefault( + "WHERE alias = @Alias", + new { composition.Alias }); + if (contentTypeDto != null) { - Database.Insert(new ContentTypeAllowedContentTypeDto + Database.Insert(new ContentType2ContentTypeDto { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder + ParentId = contentTypeDto.NodeId, + ChildId = entity.Id, }); } } + } - //Insert Tabs - foreach (var propertyGroup in entity.PropertyGroups) + if (entity.AllowedContentTypes is not null) + { + // Insert collection of allowed content types + foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) { - var tabDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, nodeDto.NodeId); - var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); - propertyGroup.Id = primaryKey; //Set Id on PropertyGroup - - //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group - //unless the PropertyGroupId has already been changed. - if (propertyGroup.PropertyTypes is not null) + Database.Insert(new ContentTypeAllowedContentTypeDto { - foreach (var propertyType in propertyGroup.PropertyTypes) + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder, + }); + } + } + + // Insert Tabs + foreach (PropertyGroup propertyGroup in entity.PropertyGroups) + { + PropertyTypeGroupDto tabDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, nodeDto.NodeId); + var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); + propertyGroup.Id = primaryKey; // Set Id on PropertyGroup + + // Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group + // unless the PropertyGroupId has already been changed. + if (propertyGroup.PropertyTypes is not null) + { + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) + { + if (propertyType.IsPropertyDirty("PropertyGroupId") == false) { - if (propertyType.IsPropertyDirty("PropertyGroupId") == false) - { - var tempGroup = propertyGroup; - propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); - } + PropertyGroup tempGroup = propertyGroup; + propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); } } } - - //Insert PropertyTypes - foreach (var propertyType in entity.PropertyTypes) - { - var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); - //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) - { - AssignDataTypeFromPropertyEditor(propertyType); - } - - var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType, nodeDto.NodeId); - int typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); - propertyType.Id = typePrimaryKey; //Set Id on new PropertyType - - //Update the current PropertyType with correct PropertyEditorAlias and DatabaseType - var dataTypeDto = - Database.FirstOrDefault("WHERE nodeId = @Id", new {Id = propertyTypeDto.DataTypeId}); - propertyType.PropertyEditorAlias = dataTypeDto.EditorAlias; - propertyType.ValueStorageType = dataTypeDto.DbType.EnumParse(true); - } - - CommonRepository.ClearCache(); // always } - protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) + // Insert PropertyTypes + foreach (IPropertyType propertyType in entity.PropertyTypes) { - CorrectPropertyTypeVariations(entity); - ValidateVariations(entity); + var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default; - var dto = ContentTypeFactory.BuildContentTypeDto(entity); + // If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) + { + AssignDataTypeFromPropertyEditor(propertyType); + } - // ensure the alias is not used already - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType + PropertyTypeDto propertyTypeDto = + PropertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType, nodeDto.NodeId); + var typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); + propertyType.Id = typePrimaryKey; // Set Id on new PropertyType + + // Update the current PropertyType with correct PropertyEditorAlias and DatabaseType + DataTypeDto? dataTypeDto = + Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = propertyTypeDto.DataTypeId }); + propertyType.PropertyEditorAlias = dataTypeDto.EditorAlias; + propertyType.ValueStorageType = dataTypeDto.DbType.EnumParse(true); + } + + CommonRepository.ClearCache(); // always + } + + protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) + { + CorrectPropertyTypeVariations(entity); + ValidateVariations(entity); + + ContentTypeDto dto = ContentTypeFactory.BuildContentTypeDto(entity); + + // ensure the alias is not used already + var exists = Database.ExecuteScalar( + @"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias AND umbracoNode.nodeObjectType = @objectType AND umbracoNode.id <> @id", - new {id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId}); - if (exists > 0) + new { id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId }); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); + } + + // repository should be write-locked when doing this, so we are safe from race-conds + // handle (update) the node + NodeDto nodeDto = dto.NodeDto; + Database.Update(nodeDto); + + // we NEED this: updating, so the .PrimaryKey already exists, but the entity does + // not carry it and therefore the dto does not have it yet - must get it from db, + // look up ContentType entry to get PrimaryKey for updating the DTO + ContentTypeDto? dtoPk = Database.First("WHERE nodeId = @Id", new { entity.Id }); + dto.PrimaryKey = dtoPk.PrimaryKey; + Database.Update(dto); + + // handle (delete then recreate) compositions + Database.Delete("WHERE childContentTypeId = @Id", new { entity.Id }); + foreach (IContentTypeComposition composition in entity.ContentTypeComposition) + { + Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); + } + + // removing a ContentType from a composition (U4-1690) + // 1. Find content based on the current ContentType: entity.Id + // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) + // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one + if (entity.RemovedContentTypes.Any()) + { + // TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? + + // find Content based on the current ContentType + Sql sql = Sql() + .SelectAll() + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .Where(x => x.ContentTypeId == entity.Id); + List? contentDtos = Database.Fetch(sql); + + // loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition + foreach (var key in entity.RemovedContentTypes) { - throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); - } + // find PropertyTypes for the removed ContentType + List? propertyTypes = + Database.Fetch("WHERE contentTypeId = @Id", new { Id = key }); - // repository should be write-locked when doing this, so we are safe from race-conds - // handle (update) the node - var nodeDto = dto.NodeDto; - Database.Update(nodeDto); - - // we NEED this: updating, so the .PrimaryKey already exists, but the entity does - // not carry it and therefore the dto does not have it yet - must get it from db, - // look up ContentType entry to get PrimaryKey for updating the DTO - var dtoPk = Database.First("WHERE nodeId = @Id", new {entity.Id}); - dto.PrimaryKey = dtoPk.PrimaryKey; - Database.Update(dto); - - // handle (delete then recreate) compositions - Database.Delete("WHERE childContentTypeId = @Id", new {entity.Id}); - foreach (var composition in entity.ContentTypeComposition) - Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); - - // removing a ContentType from a composition (U4-1690) - // 1. Find content based on the current ContentType: entity.Id - // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) - // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - if (entity.RemovedContentTypes.Any()) - { - // TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? - - // find Content based on the current ContentType - var sql = Sql() - .SelectAll() - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == Cms.Core.Constants.ObjectTypes.Document) - .Where(x => x.ContentTypeId == entity.Id); - var contentDtos = Database.Fetch(sql); - - // loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition - foreach (var key in entity.RemovedContentTypes) + // loop through the Content that is based on the current ContentType in order to remove the Properties that are + // based on the PropertyTypes that belong to the removed ContentType. + foreach (ContentDto? contentDto in contentDtos) { - // find PropertyTypes for the removed ContentType - var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {Id = key}); - // loop through the Content that is based on the current ContentType in order to remove the Properties that are - // based on the PropertyTypes that belong to the removed ContentType. - foreach (var contentDto in contentDtos) + // TODO: This could be done with bulk SQL statements + foreach (PropertyTypeDto? propertyType in propertyTypes) { - // TODO: This could be done with bulk SQL statements - foreach (var propertyType in propertyTypes) - { - var nodeId = contentDto.NodeId; - var propertyTypeId = propertyType.Id; - var propertySql = Sql() - .Select(x => x.Id) - .From() - .InnerJoin() - .On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin() - .On((left, right) => left.VersionId == right.Id) - .Where(x => x.NodeId == nodeId) - .Where(x => x.Id == propertyTypeId); + var nodeId = contentDto.NodeId; + var propertyTypeId = propertyType.Id; + Sql propertySql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin() + .On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin() + .On((left, right) => left.VersionId == right.Id) + .Where(x => x.NodeId == nodeId) + .Where(x => x.Id == propertyTypeId); - // finally delete the properties that match our criteria for removing a ContentType from the composition - Database.Delete(new Sql("WHERE id IN (" + propertySql.SQL + ")", - propertySql.Arguments)); - } + // finally delete the properties that match our criteria for removing a ContentType from the composition + Database.Delete(new Sql( + "WHERE id IN (" + propertySql.SQL + ")", + propertySql.Arguments)); } } } + } - // delete the allowed content type entries before re-inserting the collection of allowed content types - Database.Delete("WHERE Id = @Id", new {entity.Id}); - if (entity.AllowedContentTypes is not null) + // delete the allowed content type entries before re-inserting the collection of allowed content types + Database.Delete("WHERE Id = @Id", new { entity.Id }); + if (entity.AllowedContentTypes is not null) + { + foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) { - foreach (var allowedContentType in entity.AllowedContentTypes) + Database.Insert(new ContentTypeAllowedContentTypeDto { - Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); - } + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder, + }); } + } - // Delete property types ... by excepting entries from db with entries from collections. - // We check if the entity's own PropertyTypes has been modified and then also check - // any of the property groups PropertyTypes has been modified. - // This specifically tells us if any property type collections have changed. - if (entity.IsPropertyDirty("NoGroupPropertyTypes") || - entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + // Delete property types ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyTypes has been modified and then also check + // any of the property groups PropertyTypes has been modified. + // This specifically tells us if any property type collections have changed. + if (entity.IsPropertyDirty("NoGroupPropertyTypes") || + entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + { + List? dbPropertyTypes = + Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id }); + IEnumerable dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); + IEnumerable entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); + IEnumerable propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); + foreach (var propertyTypeId in propertyTypeToDeleteIds) { - var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {entity.Id}); - var dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); - var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); - var propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); - foreach (var propertyTypeId in propertyTypeToDeleteIds) - DeletePropertyType(entity.Id, propertyTypeId); + DeletePropertyType(entity.Id, propertyTypeId); } + } - // Delete tabs ... by excepting entries from db with entries from collections. - // We check if the entity's own PropertyGroups has been modified. - // This specifically tells us if the property group collections have changed. - List? orphanPropertyTypeIds = null; - if (entity.IsPropertyDirty("PropertyGroups")) - { - // TODO: we used to try to propagate tabs renaming downstream, relying on ParentId, but - // 1) ParentId makes no sense (if a tab can be inherited from multiple composition - // types) so we would need to figure things out differently, visiting downstream - // content types and looking for tabs with the same name... - // 2) It was not deployable as changing a content type changes other content types - // that was not deterministic, because it would depend on the order of the changes. - // That last point could be fixed if (1) is fixed, but then it still is an issue with - // deploy because changing a content type changes other content types that are not - // dependencies but dependents, and then what? - // - // So... for the time being, all renaming propagation is disabled. We just don't do it. - - // (all gone) - - // delete tabs that do not exist anymore - // get the tabs that are currently existing (in the db), get the tabs that we want, - // now, and derive the tabs that we want to delete - var existingPropertyGroups = Database - .Fetch("WHERE contentTypeNodeId = @id", new {id = entity.Id}) - .Select(x => x.Id) - .ToList(); - var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); - var groupsToDelete = existingPropertyGroups - .Except(newPropertyGroups) - .ToArray(); - - // delete the tabs - if (groupsToDelete.Length > 0) - { - // if the tab contains properties, take care of them - // - move them to 'generic properties' so they remain consistent - // - keep track of them, later on we'll figure out what to do with them - // see http://issues.umbraco.org/issue/U4-8663 - orphanPropertyTypeIds = Database.Fetch("WHERE propertyTypeGroupId IN (@ids)", - new {ids = groupsToDelete}) - .Select(x => x.Id).ToList(); - Database.Update( - "SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId IN (@ids)", - new {ids = groupsToDelete}); - - // now we can delete the tabs - Database.Delete("WHERE id IN (@ids)", new {ids = groupsToDelete}); - } - } - - // insert or update groups, assign properties - foreach (var propertyGroup in entity.PropertyGroups) - { - // insert or update group - var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); - var groupId = propertyGroup.HasIdentity - ? Database.Update(groupDto) - : Convert.ToInt32(Database.Insert(groupDto)); - if (propertyGroup.HasIdentity == false) - propertyGroup.Id = groupId; - else - groupId = propertyGroup.Id; - - // assign properties to the group - // (all of them, even those that have .IsPropertyDirty("PropertyGroupId") == true, - // because it should have been set to this group anyways and better be safe) - if (propertyGroup.PropertyTypes is not null) - { - foreach (var propertyType in propertyGroup.PropertyTypes) - { - propertyType.PropertyGroupId = new Lazy(() => groupId); - } - } - } - - //check if the content type variation has been changed - var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); - var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; - var newContentTypeVariation = entity.Variations; - var contentTypeVariationChanging = - contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; - if (contentTypeVariationChanging) - { - MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); - Clear301Redirects(entity); - ClearScheduledPublishing(entity); - } - - // collect property types that have a dirty variation - List? propertyTypeVariationDirty = null; - - // note: this only deals with *local* property types, we're dealing w/compositions later below - foreach (var propertyType in entity.PropertyTypes) - { - // track each property individually - if (propertyType.IsPropertyDirty("Variations")) - { - // allocate the list only when needed - if (propertyTypeVariationDirty == null) - propertyTypeVariationDirty = new List(); - - propertyTypeVariationDirty.Add(propertyType); - } - } - - // figure out dirty property types that have actually changed - // before we insert or update properties, so we can read the old variations - var propertyTypeVariationChanges = propertyTypeVariationDirty != null - ? GetPropertyVariationChanges(propertyTypeVariationDirty) - : null; - - // deal with composition property types - // add changes for property types obtained via composition, which change due - // to this content type variations change - if (contentTypeVariationChanging) - { - // must use RawComposedPropertyTypes here: only those types that are obtained - // via composition, with their original variations (ie not filtered by this - // content type variations - we need this true value to make decisions. - - propertyTypeVariationChanges = propertyTypeVariationChanges ?? - new Dictionary(); - - foreach (var composedPropertyType in entity.GetOriginalComposedPropertyTypes()) - { - if (composedPropertyType.Variations == ContentVariation.Nothing) continue; - - // Determine target variation of the composed property type. - // The composed property is only considered culture variant when the base content type is also culture variant. - // The composed property is only considered segment variant when the base content type is also segment variant. - // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture - var target = newContentTypeVariation & composedPropertyType.Variations; - // Determine the previous variation - // We have to compare with the old content type variation because the composed property might already have changed - // Example: A property with variations in an element type with variations is used in a document without - // when you enable variations the property has already enabled variations from the element type, - // but it's still a change from nothing because the document did not have variations, but it does now. - var from = oldContentTypeVariation & composedPropertyType.Variations; - - propertyTypeVariationChanges[composedPropertyType.Id] = (from, target); - } - } - - // insert or update properties - // all of them, no-group and in-groups - foreach (var propertyType in entity.PropertyTypes) - { - // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) - AssignDataTypeFromPropertyEditor(propertyType); - - // validate the alias - ValidateAlias(propertyType); - - // insert or update property - var groupId = propertyType.PropertyGroupId?.Value ?? default; - var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); - var typeId = propertyType.HasIdentity - ? Database.Update(propertyTypeDto) - : Convert.ToInt32(Database.Insert(propertyTypeDto)); - if (propertyType.HasIdentity == false) - propertyType.Id = typeId; - else - typeId = propertyType.Id; - - // not an orphan anymore - orphanPropertyTypeIds?.Remove(typeId); - } - - // must restrict property data changes to impacted content types - if changing a composing - // type, some composed types (those that do not vary) are not impacted and should be left - // unchanged + // Delete tabs ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyGroups has been modified. + // This specifically tells us if the property group collections have changed. + List? orphanPropertyTypeIds = null; + if (entity.IsPropertyDirty("PropertyGroups")) + { + // TODO: we used to try to propagate tabs renaming downstream, relying on ParentId, but + // 1) ParentId makes no sense (if a tab can be inherited from multiple composition + // types) so we would need to figure things out differently, visiting downstream + // content types and looking for tabs with the same name... + // 2) It was not deployable as changing a content type changes other content types + // that was not deterministic, because it would depend on the order of the changes. + // That last point could be fixed if (1) is fixed, but then it still is an issue with + // deploy because changing a content type changes other content types that are not + // dependencies but dependents, and then what? // - // getting 'all' from the cache policy is prone to race conditions - fast but dangerous - //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); - var all = PerformGetAll(); + // So... for the time being, all renaming propagation is disabled. We just don't do it. - var impacted = GetImpactedContentTypes(entity, all); + // (all gone) - // if some property types have actually changed, move their variant data - if (propertyTypeVariationChanges?.Count > 0) - MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); + // delete tabs that do not exist anymore + // get the tabs that are currently existing (in the db), get the tabs that we want, + // now, and derive the tabs that we want to delete + var existingPropertyGroups = Database + .Fetch("WHERE contentTypeNodeId = @id", new { id = entity.Id }) + .Select(x => x.Id) + .ToList(); + var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); + var groupsToDelete = existingPropertyGroups + .Except(newPropertyGroups) + .ToArray(); - // deal with orphan properties: those that were in a deleted tab, - // and have not been re-mapped to another tab or to 'generic properties' - if (orphanPropertyTypeIds != null) - foreach (var id in orphanPropertyTypeIds) - DeletePropertyType(entity.Id, id); - - CommonRepository.ClearCache(); // always - } - - /// - /// Corrects the property type variations for the given entity - /// to make sure the property type variation is compatible with the - /// variation set on the entity itself. - /// - /// Entity to correct properties for - private void CorrectPropertyTypeVariations(IContentTypeComposition entity) - { - // Update property variations based on the content type variation - foreach (var propertyType in entity.PropertyTypes) + // delete the tabs + if (groupsToDelete.Length > 0) { - // Determine variation for the property type. - // The property is only considered culture variant when the base content type is also culture variant. - // The property is only considered segment variant when the base content type is also segment variant. - // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture - propertyType.Variations = entity.Variations & propertyType.Variations; + // if the tab contains properties, take care of them + // - move them to 'generic properties' so they remain consistent + // - keep track of them, later on we'll figure out what to do with them + // see http://issues.umbraco.org/issue/U4-8663 + orphanPropertyTypeIds = Database.Fetch( + "WHERE propertyTypeGroupId IN (@ids)", + new { ids = groupsToDelete }) + .Select(x => x.Id).ToList(); + Database.Update( + "SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId IN (@ids)", + new { ids = groupsToDelete }); + + // now we can delete the tabs + Database.Delete("WHERE id IN (@ids)", new { ids = groupsToDelete }); } } - /// - /// Ensures that no property types are flagged for a variance that is not supported by the content type itself - /// - /// The entity for which the property types will be validated - private void ValidateVariations(IContentTypeComposition entity) + // insert or update groups, assign properties + foreach (PropertyGroup propertyGroup in entity.PropertyGroups) { - foreach (var prop in entity.PropertyTypes) + // insert or update group + PropertyTypeGroupDto groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); + var groupId = propertyGroup.HasIdentity + ? Database.Update(groupDto) + : Convert.ToInt32(Database.Insert(groupDto)); + if (propertyGroup.HasIdentity == false) { - // The variation of a property is only allowed if all its variation flags - // are also set on the entity itself. It cannot set anything that is not also set by the content type. - // For example, when entity.Variations is set to Culture a property cannot be set to Segment. - var isValid = entity.Variations.HasFlag(prop.Variations); - if (!isValid) - throw new InvalidOperationException( - $"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); - } - } - - private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, - IEnumerable? all) - { - if (all is null) - { - return Enumerable.Empty(); - } - var impact = new List(); - var set = new List {contentType}; - - var tree = new Dictionary>(); - foreach (var x in all) - foreach (var y in x.ContentTypeComposition) - { - if (!tree.TryGetValue(y.Id, out var list)) - list = tree[y.Id] = new List(); - list.Add(x); - } - - var nset = new List(); - do - { - impact.AddRange(set); - - foreach (var x in set) - { - if (!tree.TryGetValue(x.Id, out var list)) continue; - nset.AddRange(list.Where(y => y.VariesByCulture())); - } - - set = nset; - nset = new List(); - } while (set.Count > 0); - - return impact; - } - - // gets property types that have actually changed, and the corresponding changes - // returns null if no property type has actually changed - private Dictionary? - GetPropertyVariationChanges(IEnumerable propertyTypes) - { - var propertyTypesL = propertyTypes.ToList(); - - // select the current variations (before the change) from database - var selectCurrentVariations = Sql() - .Select(x => x.Id, x => x.Variations) - .From() - .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); - - var oldVariations = Database.Dictionary(selectCurrentVariations); - - // build a dictionary of actual changes - Dictionary? changes = null; - - foreach (var propertyType in propertyTypesL) - { - // new property type, ignore - if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) - continue; - var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly - - // only those property types that *actually* changed - var newVariation = propertyType.Variations; - if (oldVariation == newVariation) - continue; - - // allocate the dictionary only when needed - if (changes == null) - changes = new Dictionary(); - - changes[propertyType.Id] = (oldVariation, newVariation); - } - - return changes; - } - - /// - /// Clear any redirects associated with content for a content type - /// - private void Clear301Redirects(IContentTypeComposition contentType) - { - //first clear out any existing property data that might already exists under the default lang - var sqlSelect = Sql().Select(x => x.UniqueId) - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - var sqlDelete = Sql() - .Delete() - .WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), - sqlSelect); - - Database.Execute(sqlDelete); - } - - /// - /// Clear any scheduled publishing associated with content for a content type - /// - private void ClearScheduledPublishing(IContentTypeComposition contentType) - { - // TODO: Fill this in when scheduled publishing is enabled for variants - } - - /// - /// Gets the default language identifier. - /// - private int GetDefaultLanguageId() - { - var selectDefaultLanguageId = Sql() - .Select(x => x.Id) - .From() - .Where(x => x.IsDefault); - - return Database.First(selectDefaultLanguageId); - } - - /// - /// Moves variant data for property type variation changes. - /// - private void MovePropertyTypeVariantData( - IDictionary propertyTypeChanges, - IEnumerable impacted) - { - var defaultLanguageId = GetDefaultLanguageId(); - var impactedL = impacted.Select(x => x.Id).ToList(); - - //Group by the "To" variation so we can bulk update in the correct batches - foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value)) - { - var propertyTypeIds = grouping.Select(x => x.Key).ToList(); - var (FromVariation, ToVariation) = grouping.Key; - - var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture); - var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture); - - if (!fromCultureEnabled && toCultureEnabled) - { - // Culture has been enabled - CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); - CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - } - else if (fromCultureEnabled && !toCultureEnabled) - { - // Culture has been disabled - CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); - CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - } - } - } - - /// - /// Moves variant data for a content type variation change. - /// - private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, - ContentVariation toVariation) - { - var defaultLanguageId = GetDefaultLanguageId(); - - var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture); - var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture); - - if (cultureIsNotEnabled && cultureWillBeEnabled) - { - //move the names - //first clear out any existing names that might already exists under the default lang - //there's 2x tables to update - - //clear out the versionCultureVariation table - var sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin() - .On(x => x.Id, x => x.VersionId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - var sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); - - Database.Execute(sqlDelete); - - //clear out the documentCultureVariation table - sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); - - Database.Execute(sqlDelete); - - //now we need to insert names into these 2 tables based on the invariant data - - //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang - var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLanguageId}") //default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - - Database.Execute(sqlInsert); - - //insert rows into the documentCultureVariation table - cols = Sql().ColumnsForInsert(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) - .AndSelect(x => x.Text) - .Append($", 1, {defaultLanguageId}") //make Available + default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - - Database.Execute(sqlInsert); + propertyGroup.Id = groupId; } else { - //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. - - //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( - // if we want these SQL statements back, look into GIT history + groupId = propertyGroup.Id; } - } - /// - private void CopyTagData( - int? sourceLanguageId, - int? targetLanguageId, - IReadOnlyCollection propertyTypeIds, - IReadOnlyCollection? contentTypeIds = null) - { - // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers - - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); - - // delete existing relations (for target language) - // do *not* delete existing tags - - var sqlSelectTagsToDelete = Sql() - .Select(x => x.Id) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); - - if (contentTypeIds != null) - sqlSelectTagsToDelete - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectTagsToDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); - - var sqlDeleteRelations = Sql() - .Delete() - .WhereIn(x => x.TagId, sqlSelectTagsToDelete); - - Database.Execute(sqlDeleteRelations); - - // do *not* delete the tags - they could be used by other content types / property types - /* - var sqlDeleteTag = Sql() - .Delete() - .WhereIn(x => x.Id, sqlTagToDelete); - Database.Execute(sqlDeleteTag); - */ - - // copy tags from source language to target language - // target tags may exist already, so we have to check for existence here - // - // select tags to insert: tags pointed to by a relation ship, for proper property/content types, - // and of source language, and where we cannot left join to an existing tag with same text, - // group and languageId - - var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; - var sqlSelectTagsToInsert = Sql() - .SelectDistinct(x => x.Text, x => x.Group) - .Append(", " + targetLanguageIdS) - .From(); - - sqlSelectTagsToInsert - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .LeftJoin("xtags") - .On( - (tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && - xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); - - if (contentTypeIds != null) - sqlSelectTagsToInsert - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectTagsToInsert - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .WhereNull(x => x.Id, "xtags") // ie, not exists - .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); - - var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); - var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); - - Database.Execute(sqlInsertTags); - - // create relations to new tags - // any existing relations have been deleted above, no need to check for existence here - // - // select node id and property type id from existing relations to tags of source language, - // for proper property/content types, and select new tag id from tags, with matching text, - // and group, but for the target language - - var sqlSelectRelationsToInsert = Sql() - .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) - .AndSelect("otag", x => x.Id) - .From() - .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) - .InnerJoin("otag") - .On( - (tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && - otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); - - if (contentTypeIds != null) - sqlSelectRelationsToInsert - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectRelationsToInsert - .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - var relationColumnsToInsert = Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); - var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert); - - Database.Execute(sqlInsertRelations); - - // delete original relations - *not* the tags - all of them - // cannot really "go back" with relations, would have to do it with property values - - sqlSelectTagsToDelete = Sql() - .Select(x => x.Id) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); - - if (contentTypeIds != null) - sqlSelectTagsToDelete - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectTagsToDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); - - sqlDeleteRelations = Sql() - .Delete() - .WhereIn(x => x.TagId, sqlSelectTagsToDelete); - - Database.Execute(sqlDeleteRelations); - - // no - /* - var sqlDeleteTag = Sql() - .Delete() - .WhereIn(x => x.Id, sqlTagToDelete); - Database.Execute(sqlDeleteTag); - */ - } - - /// - /// Copies property data from one language to another. - /// - /// The source language (can be null ie invariant). - /// The target language (can be null ie invariant) - /// The property type identifiers. - /// The content type identifiers. - private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, - IReadOnlyCollection propertyTypeIds, IReadOnlyCollection? contentTypeIds = null) - { - // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers - // - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); - - //first clear out any existing property data that might already exists under the target language - var sqlDelete = Sql() - .Delete(); - - // not ok for SqlCe (no JOIN in DELETE) - //if (contentTypeIds != null) - // sqlDelete - // .From() - // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) - // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); - - Sql? inSql = null; - if (contentTypeIds != null) + // assign properties to the group + // (all of them, even those that have .IsPropertyDirty("PropertyGroupId") == true, + // because it should have been set to this group anyways and better be safe) + if (propertyGroup.PropertyTypes is not null) { - inSql = Sql() - .Select(x => x.Id) - .From() - .InnerJoin() - .On((cversion, c) => cversion.NodeId == c.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - sqlDelete.WhereIn(x => x.VersionId, inSql); + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) + { + propertyType.PropertyGroupId = new Lazy(() => groupId); + } + } + } + + // check if the content type variation has been changed + var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); + var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; + ContentVariation newContentTypeVariation = entity.Variations; + var contentTypeVariationChanging = + contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; + if (contentTypeVariationChanging) + { + MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); + Clear301Redirects(entity); + ClearScheduledPublishing(entity); + } + + // collect property types that have a dirty variation + List? propertyTypeVariationDirty = null; + + // note: this only deals with *local* property types, we're dealing w/compositions later below + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // track each property individually + if (propertyType.IsPropertyDirty("Variations")) + { + // allocate the list only when needed + if (propertyTypeVariationDirty == null) + { + propertyTypeVariationDirty = new List(); + } + + propertyTypeVariationDirty.Add(propertyType); + } + } + + // figure out dirty property types that have actually changed + // before we insert or update properties, so we can read the old variations + Dictionary? propertyTypeVariationChanges = + propertyTypeVariationDirty != null + ? GetPropertyVariationChanges(propertyTypeVariationDirty) + : null; + + // deal with composition property types + // add changes for property types obtained via composition, which change due + // to this content type variations change + if (contentTypeVariationChanging) + { + // must use RawComposedPropertyTypes here: only those types that are obtained + // via composition, with their original variations (ie not filtered by this + // content type variations - we need this true value to make decisions. + propertyTypeVariationChanges ??= new Dictionary(); + + foreach (IPropertyType composedPropertyType in entity.GetOriginalComposedPropertyTypes()) + { + if (composedPropertyType.Variations == ContentVariation.Nothing) + { + continue; + } + + // Determine target variation of the composed property type. + // The composed property is only considered culture variant when the base content type is also culture variant. + // The composed property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + ContentVariation target = newContentTypeVariation & composedPropertyType.Variations; + + // Determine the previous variation + // We have to compare with the old content type variation because the composed property might already have changed + // Example: A property with variations in an element type with variations is used in a document without + // when you enable variations the property has already enabled variations from the element type, + // but it's still a change from nothing because the document did not have variations, but it does now. + ContentVariation from = oldContentTypeVariation & composedPropertyType.Variations; + + propertyTypeVariationChanges[composedPropertyType.Id] = (from, target); + } + } + + // insert or update properties + // all of them, no-group and in-groups + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) + { + AssignDataTypeFromPropertyEditor(propertyType); } - sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + // validate the alias + ValidateAlias(propertyType); - sqlDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + // insert or update property + var groupId = propertyType.PropertyGroupId?.Value ?? default; + PropertyTypeDto propertyTypeDto = + PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); + var typeId = propertyType.HasIdentity + ? Database.Update(propertyTypeDto) + : Convert.ToInt32(Database.Insert(propertyTypeDto)); + if (propertyType.HasIdentity == false) + { + propertyType.Id = typeId; + } + else + { + typeId = propertyType.Id; + } - // see note above, not ok for SqlCe - //if (contentTypeIds != null) - // sqlDelete - // .WhereIn(x => x.ContentTypeId, contentTypeIds); + // not an orphan anymore + orphanPropertyTypeIds?.Remove(typeId); + } + + // must restrict property data changes to impacted content types - if changing a composing + // type, some composed types (those that do not vary) are not impacted and should be left + // unchanged + // + // getting 'all' from the cache policy is prone to race conditions - fast but dangerous + // var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); + IEnumerable? all = PerformGetAll(); + + IEnumerable impacted = GetImpactedContentTypes(entity, all); + + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges?.Count > 0) + { + MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); + } + + // deal with orphan properties: those that were in a deleted tab, + // and have not been re-mapped to another tab or to 'generic properties' + if (orphanPropertyTypeIds != null) + { + foreach (var id in orphanPropertyTypeIds) + { + DeletePropertyType(entity.Id, id); + } + } + + CommonRepository.ClearCache(); // always + } + + protected void ValidateAlias(IPropertyType pt) + { + if (string.IsNullOrWhiteSpace(pt.Alias)) + { + var ex = new InvalidOperationException( + $"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + + Logger.LogError( + "Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + pt.Name); + + throw ex; + } + } + + private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) => + (!pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue) + || (!pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue) + || (pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue) + || (pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue) + || (pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue); + + /// + /// Corrects the property type variations for the given entity + /// to make sure the property type variation is compatible with the + /// variation set on the entity itself. + /// + /// Entity to correct properties for + private void CorrectPropertyTypeVariations(IContentTypeComposition entity) + { + // Update property variations based on the content type variation + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // Determine variation for the property type. + // The property is only considered culture variant when the base content type is also culture variant. + // The property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + propertyType.Variations = entity.Variations & propertyType.Variations; + } + } + + /// + /// Ensures that no property types are flagged for a variance that is not supported by the content type itself + /// + /// The entity for which the property types will be validated + private void ValidateVariations(IContentTypeComposition entity) + { + foreach (IPropertyType prop in entity.PropertyTypes) + { + // The variation of a property is only allowed if all its variation flags + // are also set on the entity itself. It cannot set anything that is not also set by the content type. + // For example, when entity.Variations is set to Culture a property cannot be set to Segment. + var isValid = entity.Variations.HasFlag(prop.Variations); + if (!isValid) + { + throw new InvalidOperationException( + $"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); + } + } + } + + private IEnumerable GetImpactedContentTypes( + IContentTypeComposition contentType, + IEnumerable? all) + { + if (all is null) + { + return Enumerable.Empty(); + } + + var impact = new List(); + var set = new List { contentType }; + + var tree = new Dictionary>(); + foreach (IContentTypeComposition x in all) + { + foreach (IContentTypeComposition y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out List? list)) + { + list = tree[y.Id] = new List(); + } + + list.Add(x); + } + } + + var nset = new List(); + do + { + impact.AddRange(set); + + foreach (IContentTypeComposition x in set) + { + if (!tree.TryGetValue(x.Id, out List? list)) + { + continue; + } + + nset.AddRange(list.Where(y => y.VariesByCulture())); + } + + set = nset; + nset = new List(); + } + while (set.Count > 0); + + return impact; + } + + // gets property types that have actually changed, and the corresponding changes + // returns null if no property type has actually changed + private Dictionary? + GetPropertyVariationChanges(IEnumerable propertyTypes) + { + var propertyTypesL = propertyTypes.ToList(); + + // select the current variations (before the change) from database + Sql selectCurrentVariations = Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); + + Dictionary? oldVariations = Database.Dictionary(selectCurrentVariations); + + // build a dictionary of actual changes + Dictionary? changes = null; + + foreach (IPropertyType propertyType in propertyTypesL) + { + // new property type, ignore + if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) + { + continue; + } + + var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly + + // only those property types that *actually* changed + ContentVariation newVariation = propertyType.Variations; + if (oldVariation == newVariation) + { + continue; + } + + // allocate the dictionary only when needed + if (changes == null) + { + changes = new Dictionary(); + } + + changes[propertyType.Id] = (oldVariation, newVariation); + } + + return changes; + } + + /// + /// Clear any redirects associated with content for a content type + /// + private void Clear301Redirects(IContentTypeComposition contentType) + { + // first clear out any existing property data that might already exists under the default lang + Sql sqlSelect = Sql().Select(x => x.UniqueId) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + Sql sqlDelete = Sql() + .Delete() + .WhereIn( + (Expression>)(x => x.ContentKey), + sqlSelect); + + Database.Execute(sqlDelete); + } + + /// + /// Clear any scheduled publishing associated with content for a content type + /// + private void ClearScheduledPublishing(IContentTypeComposition contentType) + { + // TODO: Fill this in when scheduled publishing is enabled for variants + } + + /// + /// Gets the default language identifier. + /// + private int GetDefaultLanguageId() + { + Sql selectDefaultLanguageId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + return Database.First(selectDefaultLanguageId); + } + + /// + /// Moves variant data for property type variation changes. + /// + private void MovePropertyTypeVariantData( + IDictionary propertyTypeChanges, + IEnumerable impacted) + { + var defaultLanguageId = GetDefaultLanguageId(); + var impactedL = impacted.Select(x => x.Id).ToList(); + + // Group by the "To" variation so we can bulk update in the correct batches + foreach (IGrouping<(ContentVariation FromVariation, ContentVariation ToVariation), + KeyValuePair> grouping in + propertyTypeChanges.GroupBy(x => x.Value)) + { + var propertyTypeIds = grouping.Select(x => x.Key).ToList(); + (ContentVariation FromVariation, ContentVariation ToVariation) = grouping.Key; + + var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture); + var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture); + + if (!fromCultureEnabled && toCultureEnabled) + { + // Culture has been enabled + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); + CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); + } + else if (fromCultureEnabled && !toCultureEnabled) + { + // Culture has been disabled + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); + CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); + } + } + } + + /// + /// Moves variant data for a content type variation change. + /// + private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, + ContentVariation toVariation) + { + var defaultLanguageId = GetDefaultLanguageId(); + + var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture); + var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture); + + if (cultureIsNotEnabled && cultureWillBeEnabled) + { + // move the names + // first clear out any existing names that might already exists under the default lang + // there's 2x tables to update + + // clear out the versionCultureVariation table + Sql sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin() + .On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + Sql sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); Database.Execute(sqlDelete); - //now insert all property data into the target language that exists under the source language - var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; - var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append(", " + targetLanguageIdS) //default language ID - .From(); + // clear out the documentCultureVariation table + sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); - if (contentTypeIds != null) - sqlSelectData - .InnerJoin() - .On((pdata, cversion) => pdata.VersionId == cversion.Id) - .InnerJoin() - .On((cversion, c) => cversion.NodeId == c.NodeId); + Database.Execute(sqlDelete); - sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + // now we need to insert names into these 2 tables based on the invariant data - sqlSelectData - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - if (contentTypeIds != null) - sqlSelectData - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + // insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.Name, + x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) + .Append($", {defaultLanguageId}") // default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + Sql? sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})") + .Append(sqlSelect); Database.Execute(sqlInsert); - // when copying from Culture, keep the original values around in case we want to go back - // when copying from Nothing, kill the original values, we don't want them around - if (sourceLanguageId == null) - { - sqlDelete = Sql() - .Delete(); + // insert rows into the documentCultureVariation table + cols = Sql().ColumnsForInsert(x => x.NodeId, x => x.Edited, x => x.Published, + x => x.Name, x => x.Available, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) + .AndSelect(x => x.Text) + .Append($", 1, {defaultLanguageId}") // make Available + default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - if (contentTypeIds != null) - sqlDelete.WhereIn(x => x.VersionId, inSql); - - sqlDelete - .Where(x => x.LanguageId == null) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - Database.Execute(sqlDelete); - } - } - - /// - /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are changed - /// - /// - /// - /// - /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false - /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each - /// property, culture and current/published version. - /// - private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, - IReadOnlyCollection? contentTypeIds = null) - { - var defaultLang = LanguageRepository.GetDefaultId(); - - //This will build up a query to get the property values of both the current and the published version so that we can check - //based on the current variance of each item to see if it's 'edited' value should be true/false. - - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); - - var propertySql = Sql() - .Select() - .AndSelect(x => x.NodeId, x => x.Current) - .AndSelect(x => x.Published) - .AndSelect(x => x.Variations) - .From() - .InnerJoin() - .On((left, right) => left.Id == right.VersionId) - .InnerJoin() - .On((left, right) => left.Id == right.PropertyTypeId); - - if (contentTypeIds != null) - { - propertySql.InnerJoin() - .On((c, cversion) => c.NodeId == cversion.NodeId); - } - - propertySql.LeftJoin() - .On((docversion, cversion) => cversion.Id == docversion.Id) - .Where((docversion, cversion) => - cversion.Current || docversion.Published) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - if (contentTypeIds != null) - { - propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); - } - - propertySql - .OrderBy(x => x.NodeId) - .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); - - //keep track of this node/lang to mark or unmark a culture as edited - var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); - //keep track of which node to mark or unmark as edited - var editedDocument = new Dictionary(); - var nodeId = -1; - var propertyTypeId = -1; - - PropertyValueVersionDto? pubRow = null; - - //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. - //Published data will always come before Current data based on the version id sort. - //There will only be one published row (max) and one current row per property. - foreach (var row in Database.Query(propertySql)) - { - //make sure to reset on each node/property change - if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) - { - nodeId = row.NodeId; - propertyTypeId = row.PropertyTypeId; - pubRow = null; - } - - if (row.Published) - pubRow = row; - - if (row.Current) - { - var propVariations = (ContentVariation)row.Variations; - - //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited - if (!propVariations.VariesByCulture() && row.LanguageId.HasValue - || propVariations.VariesByCulture() && !row.LanguageId.HasValue) - { - //Flag this as not edited for this node/lang if the key doesn't exist - if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) - editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); - - //mark as false if the item doesn't exist, else coerce to true - editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) - ? (edited |= false) - : false; - } - else if (pubRow == null) - { - //this would mean that that this property is 'edited' since there is no published version - editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; - editedDocument[row.NodeId] = true; - } - //compare the property values, if they differ from versions then flag the current version as edited - else if (IsPropertyValueChanged(pubRow, row)) - { - //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang - editedLanguageVersions[ - (row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; - editedDocument[row.NodeId] = true; - } - - //reset - pubRow = null; - } - } - - // lookup all matching rows in umbracoDocumentCultureVariation - // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) - var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); - var nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); - var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) - .SelectMany(group => - { - var sql = Sql().Select().From() - .WhereIn(x => x.LanguageId, languageIds) - .WhereIn(x => x.NodeId, group); - - return Database.Fetch(sql); - }) - .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), - x => x); //convert to dictionary with the same key type - - var toUpdate = new List(); - foreach (var ev in editedLanguageVersions) - { - if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) - { - //check if it needs updating - if (docVariations.Edited != ev.Value) - { - docVariations.Edited = ev.Value; - toUpdate.Add(docVariations); - } - } - else if (ev.Key.langId.HasValue) - { - //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level - throw new PanicException( - $"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); - } - } - - //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false - foreach (var editValue in toUpdate.GroupBy(x => x.Edited)) - { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.Id, editValue.Select(x => x.Id))); - } - - //Now bulk update the umbracoDocument table - foreach (var editValue in editedDocument.GroupBy(x => x.Value)) - { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); - } - } - - private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) - { - return !pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue - || !pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue - || pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue - || pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue - || pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue; - } - - private class NameCompareDto - { - public int NodeId { get; set; } - public int CurrentVersion { get; set; } - public int LanguageId { get; set; } - public string? CurrentName { get; set; } - public string? PublishedName { get; set; } - public int? PublishedVersion { get; set; } - public int Id { get; set; } // the Id of the DocumentCultureVariationDto - public bool Edited { get; set; } - } - - private class PropertyValueVersionDto - { - public int VersionId { get; set; } - public int PropertyTypeId { get; set; } - public int? LanguageId { get; set; } - public string? Segment { get; set; } - public int? IntValue { get; set; } - - private decimal? _decimalValue; - - [Column("decimalValue")] - public decimal? DecimalValue - { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } - - public DateTime? DateValue { get; set; } - public string? VarcharValue { get; set; } - public string? TextValue { get; set; } - - public int NodeId { get; set; } - public bool Current { get; set; } - public bool Published { get; set; } - - public byte Variations { get; set; } - } - - private void DeletePropertyType(int contentTypeId, int propertyTypeId) - { - // first clear dependencies - Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); - Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); - - // then delete the property type - Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", - new {Id = contentTypeId, PropertyTypeId = propertyTypeId}); - } - - protected void ValidateAlias(IPropertyType pt) - { - if (string.IsNullOrWhiteSpace(pt.Alias)) - { - var ex = new InvalidOperationException( - $"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - - Logger.LogError( - "Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - pt.Name); - - throw ex; - } - } - - protected void ValidateAlias(TEntity entity) - { - if (string.IsNullOrWhiteSpace(entity.Alias)) - { - var ex = new InvalidOperationException( - $"{typeof(TEntity).Name} '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - - Logger.LogError( - "{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - typeof(TEntity).Name, - entity.Name); - - throw ex; - } - } - - /// - /// Try to set the data type id based on its ControlId - /// - /// - private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType) - { - //we cannot try to assign a data type of it's empty - if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) - { - var sql = Sql() - .Select(dt => dt.Select(x => x.NodeDto)) - .From() - .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) - .Where("propertyEditorAlias = @propertyEditorAlias", - new {propertyEditorAlias = propertyType.PropertyEditorAlias}) - .OrderBy(typeDto => typeDto.NodeId); - var datatype = Database.FirstOrDefault(sql); - //we cannot assign a data type if one was not found - if (datatype != null) - { - propertyType.DataTypeId = datatype.NodeId; - propertyType.DataTypeKey = datatype.NodeDto.UniqueId; - } - else - { - Logger.LogWarning( - "Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", - propertyType.Alias, propertyType.PropertyEditorAlias); - } - } - } - - protected abstract TEntity? PerformGet(Guid id); - protected abstract TEntity? PerformGet(string alias); - protected abstract IEnumerable? PerformGetAll(params Guid[]? ids); - protected abstract bool PerformExists(Guid id); - - /// - /// Gets an Entity by alias - /// - /// - /// - public TEntity? Get(string alias) - { - return PerformGet(alias); - } - - /// - /// Gets an Entity by Id - /// - /// - /// - public TEntity? Get(Guid id) - { - return PerformGet(id); - } - - /// - /// Gets all entities of the specified type - /// - /// - /// - /// - /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same signature as the main GetAll when there are no parameters - /// - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return PerformGetAll(ids) ?? Enumerable.Empty(); - } - - /// - /// Boolean indicating whether an Entity with the specified Id exists - /// - /// - /// - public bool Exists(Guid id) - { - return PerformExists(id); - } - - public string GetUniqueAlias(string alias) - { - // alias is unique across ALL content types! - var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); - var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType -INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id -WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", - new {pattern = alias + "%", objectType = NodeObjectTypeId}); - var i = 1; - string test; - while (aliases.Contains(test = alias + i)) i++; - return test; - } - - /// - public bool HasContainerInPath(string contentPath) - { - var ids = contentPath.Split(Constants.CharArrays.Comma) - .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray(); - return HasContainerInPath(ids); - } - - /// - public bool HasContainerInPath(params int[] ids) - { - var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType -INNER JOIN {Cms.Core.Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Cms.Core.Constants.DatabaseSchema.Tables.Content}.contentTypeId -WHERE {Cms.Core.Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", - new {ids, isContainer = true}); - return Database.ExecuteScalar(sql) > 0; - } - - /// - /// Returns true or false depending on whether content nodes have been created based on the provided content type id. - /// - public bool HasContentNodes(int id) - { - var sql = new Sql( - $"SELECT CASE WHEN EXISTS (SELECT * FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END", - new {id}); - return Database.ExecuteScalar(sql) == 1; - } - - protected override IEnumerable GetDeleteClauses() - { - // in theory, services should have ensured that content items of the given content type - // have been deleted and therefore PropertyData has been cleared, so PropertyData - // is included here just to be 100% sure since it has a FK on cmsPropertyType. - - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id", - "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id", - "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + - " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyType + - " WHERE contentTypeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup + - " WHERE contenttypeNodeId = @id" - }; - return list; + Database.Execute(sqlInsert); } } + + /// + private void CopyTagData( + int? sourceLanguageId, + int? targetLanguageId, + IReadOnlyCollection propertyTypeIds, + IReadOnlyCollection? contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } + + // delete existing relations (for target language) + // do *not* delete existing tags + Sql sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + { + sqlSelectTagsToDelete + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + Sql sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // do *not* delete the tags - they could be used by other content types / property types + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + + // copy tags from source language to target language + // target tags may exist already, so we have to check for existence here + // + // select tags to insert: tags pointed to by a relation ship, for proper property/content types, + // and of source language, and where we cannot left join to an existing tag with same text, + // group and languageId + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + Sql sqlSelectTagsToInsert = Sql() + .SelectDistinct(x => x.Text, x => x.Group) + .Append(", " + targetLanguageIdS) + .From(); + + sqlSelectTagsToInsert + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .LeftJoin("xtags") + .On( + (tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && + xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); + + if (contentTypeIds != null) + { + sqlSelectTagsToInsert + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectTagsToInsert + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .WhereNull(x => x.Id, "xtags") // ie, not exists + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + + var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); + Sql? sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); + + Database.Execute(sqlInsertTags); + + // create relations to new tags + // any existing relations have been deleted above, no need to check for existence here + // + // select node id and property type id from existing relations to tags of source language, + // for proper property/content types, and select new tag id from tags, with matching text, + // and group, but for the target language + Sql sqlSelectRelationsToInsert = Sql() + .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) + .AndSelect("otag", x => x.Id) + .From() + .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) + .InnerJoin("otag") + .On( + (tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && + otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); + + if (contentTypeIds != null) + { + sqlSelectRelationsToInsert + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectRelationsToInsert + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + var relationColumnsToInsert = + Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + Sql? sqlInsertRelations = + Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})") + .Append(sqlSelectRelationsToInsert); + + Database.Execute(sqlInsertRelations); + + // delete original relations - *not* the tags - all of them + // cannot really "go back" with relations, would have to do it with property values + sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + { + sqlSelectTagsToDelete + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // no + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + } + + /// + /// Copies property data from one language to another. + /// + /// The source language (can be null ie invariant). + /// The target language (can be null ie invariant) + /// The property type identifiers. + /// The content type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, + IReadOnlyCollection propertyTypeIds, IReadOnlyCollection? contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } + + // first clear out any existing property data that might already exists under the target language + Sql sqlDelete = Sql() + .Delete(); + + // not ok for SqlCe (no JOIN in DELETE) + // if (contentTypeIds != null) + // sqlDelete + // .From() + // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + Sql? inSql = null; + if (contentTypeIds != null) + { + inSql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin() + .On((cversion, c) => cversion.NodeId == c.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlDelete.WhereIn(x => x.VersionId, inSql); + } + + sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + sqlDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + // see note above, not ok for SqlCe + // if (contentTypeIds != null) + // sqlDelete + // .WhereIn(x => x.ContentTypeId, contentTypeIds); + Database.Execute(sqlDelete); + + // now insert all property data into the target language that exists under the source language + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, + x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, + x => x.LanguageId); + Sql sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, + x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, + x => x.TextValue) + .Append(", " + targetLanguageIdS) // default language ID + .From(); + + if (contentTypeIds != null) + { + sqlSelectData + .InnerJoin() + .On((pdata, cversion) => pdata.VersionId == cversion.Id) + .InnerJoin() + .On((cversion, c) => cversion.NodeId == c.NodeId); + } + + sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + + sqlSelectData + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + { + sqlSelectData + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + Sql? sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + + Database.Execute(sqlInsert); + + // when copying from Culture, keep the original values around in case we want to go back + // when copying from Nothing, kill the original values, we don't want them around + if (sourceLanguageId == null) + { + sqlDelete = Sql() + .Delete(); + + if (contentTypeIds != null) + { + sqlDelete.WhereIn(x => x.VersionId, inSql); + } + + sqlDelete + .Where(x => x.LanguageId == null) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + Database.Execute(sqlDelete); + } + } + + /// + /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are + /// changed + /// + /// + /// + /// + /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true + /// when it should be false + /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values + /// stored for each + /// property, culture and current/published version. + /// + private void RenormalizeDocumentEditedFlags( + IReadOnlyCollection propertyTypeIds, + IReadOnlyCollection? contentTypeIds = null) + { + var defaultLang = LanguageRepository.GetDefaultId(); + + // This will build up a query to get the property values of both the current and the published version so that we can check + // based on the current variance of each item to see if it's 'edited' value should be true/false. + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } + + Sql propertySql = Sql() + .Select() + .AndSelect(x => x.NodeId, x => x.Current) + .AndSelect(x => x.Published) + .AndSelect(x => x.Variations) + .From() + .InnerJoin() + .On((left, right) => left.Id == right.VersionId) + .InnerJoin() + .On((left, right) => left.Id == right.PropertyTypeId); + + if (contentTypeIds != null) + { + propertySql.InnerJoin() + .On((c, cversion) => c.NodeId == cversion.NodeId); + } + + propertySql.LeftJoin() + .On((docversion, cversion) => cversion.Id == docversion.Id) + .Where((docversion, cversion) => + cversion.Current || docversion.Published) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + { + propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + propertySql + .OrderBy(x => x.NodeId) + .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); + + // keep track of this node/lang to mark or unmark a culture as edited + var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); + + // keep track of which node to mark or unmark as edited + var editedDocument = new Dictionary(); + var nodeId = -1; + var propertyTypeId = -1; + + PropertyValueVersionDto? pubRow = null; + + // This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + // Published data will always come before Current data based on the version id sort. + // There will only be one published row (max) and one current row per property. + foreach (PropertyValueVersionDto? row in Database.Query(propertySql)) + { + // make sure to reset on each node/property change + if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) + { + nodeId = row.NodeId; + propertyTypeId = row.PropertyTypeId; + pubRow = null; + } + + if (row.Published) + { + pubRow = row; + } + + if (row.Current) + { + var propVariations = (ContentVariation)row.Variations; + + // if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited + if ((!propVariations.VariesByCulture() && row.LanguageId.HasValue) + || (propVariations.VariesByCulture() && !row.LanguageId.HasValue)) + { + // Flag this as not edited for this node/lang if the key doesn't exist + if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) + { + editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); + } + + // mark as false if the item doesn't exist, else coerce to true + editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) + ? edited |= false + : false; + } + else if (pubRow == null) + { + // this would mean that that this property is 'edited' since there is no published version + editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + + // compare the property values, if they differ from versions then flag the current version as edited + else if (IsPropertyValueChanged(pubRow, row)) + { + // Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang + editedLanguageVersions[ + (row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + + // reset + pubRow = null; + } + } + + // lookup all matching rows in umbracoDocumentCultureVariation + // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) + var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); + IEnumerable nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); + var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) + .SelectMany(group => + { + Sql sql = Sql().Select().From() + .WhereIn(x => x.LanguageId, languageIds) + .WhereIn(x => x.NodeId, group); + + return Database.Fetch(sql); + }) + .ToDictionary( + x => (x.NodeId, (int?)x.LanguageId), + x => x); // convert to dictionary with the same key type + + var toUpdate = new List(); + foreach (KeyValuePair<(int nodeId, int? langId), bool> ev in editedLanguageVersions) + { + if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out DocumentCultureVariationDto? docVariations)) + { + // check if it needs updating + if (docVariations.Edited != ev.Value) + { + docVariations.Edited = ev.Value; + toUpdate.Add(docVariations); + } + } + else if (ev.Key.langId.HasValue) + { + // This should never happen! If a property culture is flagged as edited then the culture must exist at the document level + throw new PanicException( + $"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); + } + } + + // Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false + foreach (IGrouping editValue in toUpdate.GroupBy(x => x.Edited)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, editValue.Select(x => x.Id))); + } + + // Now bulk update the umbracoDocument table + foreach (IGrouping> editValue in editedDocument.GroupBy(x => x.Value)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); + } + } + + private void DeletePropertyType(int contentTypeId, int propertyTypeId) + { + // first clear dependencies + Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId }); + Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId }); + + // then delete the property type + Database.Delete( + "WHERE contentTypeId = @Id AND id = @PropertyTypeId", + new { Id = contentTypeId, PropertyTypeId = propertyTypeId }); + } + + protected void ValidateAlias(TEntity entity) + { + if (string.IsNullOrWhiteSpace(entity.Alias)) + { + var ex = new InvalidOperationException( + $"{typeof(TEntity).Name} '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + + Logger.LogError( + "{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + typeof(TEntity).Name, + entity.Name); + + throw ex; + } + } + + protected abstract TEntity? PerformGet(Guid id); + + /// + /// Try to set the data type id based on its ControlId + /// + /// + private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType) + { + // we cannot try to assign a data type of it's empty + if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) + { + Sql sql = Sql() + .Select(dt => dt.Select(x => x.NodeDto)) + .From() + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) + .Where( + "propertyEditorAlias = @propertyEditorAlias", + new { propertyEditorAlias = propertyType.PropertyEditorAlias }) + .OrderBy(typeDto => typeDto.NodeId); + DataTypeDto? datatype = Database.FirstOrDefault(sql); + + // we cannot assign a data type if one was not found + if (datatype != null) + { + propertyType.DataTypeId = datatype.NodeId; + propertyType.DataTypeKey = datatype.NodeDto.UniqueId; + } + else + { + Logger.LogWarning( + "Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", + propertyType.Alias, propertyType.PropertyEditorAlias); + } + } + } + + protected abstract TEntity? PerformGet(string alias); + + protected abstract IEnumerable? PerformGetAll(params Guid[]? ids); + + protected abstract bool PerformExists(Guid id); + + public string GetUniqueAlias(string alias) + { + // alias is unique across ALL content types! + var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); + List? aliases = Database.Fetch( + @"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", + new { pattern = alias + "%", objectType = NodeObjectTypeId }); + var i = 1; + string test; + while (aliases.Contains(test = alias + i)) + { + i++; + } + + return test; + } + + public bool HasContainerInPath(string contentPath) + { + var ids = contentPath.Split(Constants.CharArrays.Comma) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray(); + return HasContainerInPath(ids); + } + + public bool HasContainerInPath(params int[] ids) + { + var sql = new Sql( + $@"SELECT COUNT(*) FROM cmsContentType +INNER JOIN {Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Constants.DatabaseSchema.Tables.Content}.contentTypeId +WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", + new { ids, isContainer = true }); + return Database.ExecuteScalar(sql) > 0; + } + + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + public bool HasContentNodes(int id) + { + var sql = new Sql( + $"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END", + new { id }); + return Database.ExecuteScalar(sql) == 1; + } + + protected override IEnumerable GetDeleteClauses() + { + // in theory, services should have ensured that content items of the given content type + // have been deleted and therefore PropertyData has been cleared, so PropertyData + // is included here just to be 100% sure since it has a FK on cmsPropertyType. + var list = new List + { + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id", + "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id", + "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType + + " WHERE contentTypeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup + + " WHERE contenttypeNodeId = @id", + }; + return list; + } + + private class NameCompareDto + { + public int NodeId { get; set; } + + public int CurrentVersion { get; set; } + + public int LanguageId { get; set; } + + public string? CurrentName { get; set; } + + public string? PublishedName { get; set; } + + public int? PublishedVersion { get; set; } + + public int Id { get; set; } // the Id of the DocumentCultureVariationDto + + public bool Edited { get; set; } + } + + private class PropertyValueVersionDto + { + private decimal? _decimalValue; + + public int VersionId { get; set; } + + public int PropertyTypeId { get; set; } + + public int? LanguageId { get; set; } + + public string? Segment { get; set; } + + public int? IntValue { get; set; } + + [Column("decimalValue")] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + public DateTime? DateValue { get; set; } + + public string? VarcharValue { get; set; } + + public string? TextValue { get; set; } + + public int NodeId { get; set; } + + public bool Current { get; set; } + + public bool Published { get; set; } + + public byte Variations { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 6ca327dfab..9f921266ca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.Options; using NPoco; @@ -19,739 +15,738 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; using File = System.IO.File; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +public class CreatedPackageSchemaRepository : ICreatedPackagesRepository { - /// - public class CreatedPackageSchemaRepository : ICreatedPackagesRepository + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + private readonly string _createdPackagesFolderPath; + private readonly IDataTypeService _dataTypeService; + private readonly IFileService _fileService; + private readonly FileSystems _fileSystems; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizationService _localizationService; + private readonly IMacroService _macroService; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IEntityXmlSerializer _serializer; + private readonly string _tempFolderPath; + private readonly IUmbracoDatabase? _umbracoDatabase; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public CreatedPackageSchemaRepository( + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IHostingEnvironment hostingEnvironment, + IOptions globalSettings, + FileSystems fileSystems, + IEntityXmlSerializer serializer, + IDataTypeService dataTypeService, + ILocalizationService localizationService, + IFileService fileService, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IContentService contentService, + MediaFileManager mediaFileManager, + IMacroService macroService, + IContentTypeService contentTypeService, + string? mediaFolderPath = null, + string? tempFolderPath = null) { - private readonly PackageDefinitionXmlParser _xmlParser; - private readonly IUmbracoDatabase? _umbracoDatabase; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly FileSystems _fileSystems; - private readonly IEntityXmlSerializer _serializer; - private readonly IDataTypeService _dataTypeService; - private readonly ILocalizationService _localizationService; - private readonly IFileService _fileService; - private readonly IMediaService _mediaService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IContentService _contentService; - private readonly MediaFileManager _mediaFileManager; - private readonly IMacroService _macroService; - private readonly IContentTypeService _contentTypeService; - private readonly string _tempFolderPath; - private readonly string _createdPackagesFolderPath; + _umbracoDatabase = umbracoDatabaseFactory.CreateDatabase(); + _hostingEnvironment = hostingEnvironment; + _fileSystems = fileSystems; + _serializer = serializer; + _dataTypeService = dataTypeService; + _localizationService = localizationService; + _fileService = fileService; + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + _contentService = contentService; + _mediaFileManager = mediaFileManager; + _macroService = macroService; + _contentTypeService = contentTypeService; + _xmlParser = new PackageDefinitionXmlParser(); + _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; + _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; + } - /// - /// Initializes a new instance of the class. - /// - public CreatedPackageSchemaRepository( - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IHostingEnvironment hostingEnvironment, - IOptions globalSettings, - FileSystems fileSystems, - IEntityXmlSerializer serializer, - IDataTypeService dataTypeService, - ILocalizationService localizationService, - IFileService fileService, - IMediaService mediaService, - IMediaTypeService mediaTypeService, - IContentService contentService, - MediaFileManager mediaFileManager, - IMacroService macroService, - IContentTypeService contentTypeService, - string? mediaFolderPath = null, - string? tempFolderPath = null) + public IEnumerable GetAll() + { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Select() + .From() + .OrderBy(x => x.Id); + + var packageDefinitions = new List(); + + List xmlSchemas = _umbracoDatabase.Fetch(query); + foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) { - _umbracoDatabase = umbracoDatabaseFactory.CreateDatabase(); - _hostingEnvironment = hostingEnvironment; - _fileSystems = fileSystems; - _serializer = serializer; - _dataTypeService = dataTypeService; - _localizationService = localizationService; - _fileService = fileService; - _mediaService = mediaService; - _mediaTypeService = mediaTypeService; - _contentService = contentService; - _mediaFileManager = mediaFileManager; - _macroService = macroService; - _contentTypeService = contentTypeService; - _xmlParser = new PackageDefinitionXmlParser(); - _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; - _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; - } - - public IEnumerable GetAll() - { - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Select() - .From() - .OrderBy(x => x.Id); - - var packageDefinitions = new List(); - - List xmlSchemas = _umbracoDatabase.Fetch(query); - foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) - { - var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); - if (packageDefinition is not null) - { - packageDefinition.Id = packageSchema.Id; - packageDefinition.Name = packageSchema.Name; - packageDefinition.PackageId = packageSchema.PackageId; - packageDefinitions.Add(packageDefinition); - } - } - - return packageDefinitions; - } - - public PackageDefinition? GetById(int id) - { - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Select() - .From() - .Where(x => x.Id == id); - List schemaDtos = _umbracoDatabase.Fetch(query); - - if (schemaDtos.IsCollectionEmpty()) - { - return null; - } - - var packageSchema = schemaDtos.First(); var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); if (packageDefinition is not null) { packageDefinition.Id = packageSchema.Id; packageDefinition.Name = packageSchema.Name; packageDefinition.PackageId = packageSchema.PackageId; + packageDefinitions.Add(packageDefinition); } - - return packageDefinition; } - public void Delete(int id) + return packageDefinitions; + } + + public PackageDefinition? GetById(int id) + { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Select() + .From() + .Where(x => x.Id == id); + List schemaDtos = _umbracoDatabase.Fetch(query); + + if (schemaDtos.IsCollectionEmpty()) { - // Delete package snapshot - var packageDef = GetById(id); - if (File.Exists(packageDef?.PackagePath)) - { - File.Delete(packageDef.PackagePath); - } - - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Delete() - .Where(x => x.Id == id); - - _umbracoDatabase.Execute(query); + return null; } - public bool SavePackage(PackageDefinition definition) + CreatedPackageSchemaDto packageSchema = schemaDtos.First(); + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + if (packageDefinition is not null) { - if (definition == null) - { - throw new NullReferenceException("PackageDefinition cannot be null when saving"); - } + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + } - if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) - { - return false; - } + return packageDefinition; + } - // Ensure it's valid - ValidatePackage(definition); + public void Delete(int id) + { + // Delete package snapshot + PackageDefinition? packageDef = GetById(id); + if (File.Exists(packageDef?.PackagePath)) + { + File.Delete(packageDef.PackagePath); + } + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Delete() + .Where(x => x.Id == id); - if (definition.Id == default) - { - // Create dto from definition - var dto = new CreatedPackageSchemaDto() - { - Name = definition.Name, - Value = _xmlParser.ToXml(definition).ToString(), - UpdateDate = DateTime.Now, - PackageId = Guid.NewGuid() - }; + _umbracoDatabase.Execute(query); + } - // Set the ids, we have to save in database first to get the Id - _umbracoDatabase!.Insert(dto); - definition.Id = dto.Id; - } + public bool SavePackage(PackageDefinition? definition) + { + if (definition == null) + { + throw new NullReferenceException("PackageDefinition cannot be null when saving"); + } - // Save snapshot locally, we do this to the updated packagePath - ExportPackage(definition); + if (string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) + { + return false; + } + + // Ensure it's valid + ValidatePackage(definition); + + if (definition.Id == default) + { // Create dto from definition - var updatedDto = new CreatedPackageSchemaDto() + var dto = new CreatedPackageSchemaDto { Name = definition.Name, Value = _xmlParser.ToXml(definition).ToString(), - Id = definition.Id, - PackageId = definition.PackageId, - UpdateDate = DateTime.Now + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid(), }; - _umbracoDatabase?.Update(updatedDto); - return true; + // Set the ids, we have to save in database first to get the Id + _umbracoDatabase!.Insert(dto); + definition.Id = dto.Id; } - public string ExportPackage(PackageDefinition definition) + // Save snapshot locally, we do this to the updated packagePath + ExportPackage(definition); + + // Create dto from definition + var updatedDto = new CreatedPackageSchemaDto { - // Ensure it's valid - ValidatePackage(definition); + Name = definition.Name, + Value = _xmlParser.ToXml(definition).ToString(), + Id = definition.Id, + PackageId = definition.PackageId, + UpdateDate = DateTime.Now, + }; + _umbracoDatabase?.Update(updatedDto); - // Create a folder for building this package - var temporaryPath = _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString())); - Directory.CreateDirectory(temporaryPath); + return true; + } - try + public string ExportPackage(PackageDefinition definition) + { + // Ensure it's valid + ValidatePackage(definition); + + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString())); + Directory.CreateDirectory(temporaryPath); + + try + { + // Init package file + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + + // Info section + root.Add(GetPackageInfoXml(definition)); + + PackageDocumentsAndTags(definition, root); + PackageDocumentTypes(definition, root); + PackageMediaTypes(definition, root); + PackageTemplates(definition, root); + PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem!); + PackageMacros(definition, root); + PackageDictionaryItems(definition, root); + PackageLanguages(definition, root); + PackageDataTypes(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); + + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) { - // Init package file - XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); - - // Info section - root.Add(GetPackageInfoXml(definition)); - - PackageDocumentsAndTags(definition, root); - PackageDocumentTypes(definition, root); - PackageMediaTypes(definition, root); - PackageTemplates(definition, root); - PackageStylesheets(definition, root); - PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!); - PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem!); - PackageMacros(definition, root); - PackageDictionaryItems(definition, root); - PackageLanguages(definition, root); - PackageDataTypes(definition, root); - Dictionary mediaFiles = PackageMedia(definition, root); - - string fileName; - string tempPackagePath; - if (mediaFiles.Count > 0) + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) { - fileName = "package.zip"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) { - ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); - using (Stream entryStream = packageXmlEntry.Open()) - { - compiledPackageXml.Save(entryStream); - } + compiledPackageXml.Save(entryStream); + } - foreach (KeyValuePair mediaFile in mediaFiles) + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) { - var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); - using (Stream entryStream = mediaEntry.Open()) - using (mediaFile.Value) - { - mediaFile.Value.Seek(0, SeekOrigin.Begin); - mediaFile.Value.CopyTo(entryStream); - } + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); } } } + } + else + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } + } + + var directoryName = + _hostingEnvironment.MapPathContentRoot(Path.Combine( + _createdPackagesFolderPath, + definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); + + var finalPackagePath = Path.Combine(directoryName, fileName); + + // Clean existing files + foreach (var packagePath in new[] { definition.PackagePath, finalPackagePath }) + { + if (File.Exists(packagePath)) + { + File.Delete(packagePath); + } + } + + // Move to final package path + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; + + return finalPackagePath; + } + finally + { + // Clean up + Directory.Delete(temporaryPath, true); + } + } + + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); + + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } + + private XDocument CreateCompiledPackageXml(out XElement root) + { + root = new XElement("umbPackage"); + var compiledPackageXml = new XDocument(root); + return compiledPackageXml; + } + + private void ValidatePackage(PackageDefinition definition) + { + // Ensure it's valid + var context = new ValidationContext(definition, null, null); + var results = new List(); + var isValid = Validator.TryValidateObject(definition, context, results); + if (!isValid) + { + throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + + string.Join(", ", results.Select(x => x.ErrorMessage))); + } + } + + private void PackageDataTypes(PackageDefinition definition, XContainer root) + { + var dataTypes = new XElement("DataTypes"); + foreach (var dtId in definition.DataTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDataType? dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) + { + continue; + } + + dataTypes.Add(_serializer.Serialize(dataType)); + } + + root.Add(dataTypes); + } + + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) + { + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ILanguage? lang = _localizationService.GetLanguageById(outInt); + if (lang == null) + { + continue; + } + + languages.Add(_serializer.Serialize(lang)); + } + + root.Add(languages); + } + + private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + { + var rootDictionaryItems = new XElement("DictionaryItems"); + var items = new Dictionary(); + + foreach (var dictionaryId in definition.DictionaryItems) + { + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDictionaryItem? di = _localizationService.GetDictionaryItemById(outInt); + + if (di == null) + { + continue; + } + + items[di.Key] = (di, _serializer.Serialize(di, false)); + } + + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) + { + foreach (Guid key in items.Keys.ToList()) + { + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; + + if (!dictionaryItem.ParentId.HasValue) + { + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + } else { - fileName = "package.xml"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) { - compiledPackageXml.Save(fileStream); + // we've processed this parent element already so we can just append this xml child to it + AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, serializedDictionaryValue); } - } - - var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); - Directory.CreateDirectory(directoryName); - - var finalPackagePath = Path.Combine(directoryName, fileName); - - // Clean existing files - foreach (var packagePath in new[] - { - definition.PackagePath, - finalPackagePath - }) - { - if (File.Exists(packagePath)) + else if (items.ContainsKey(dictionaryItem.ParentId.Value)) { - File.Delete(packagePath); - } - } - - // Move to final package path - File.Move(tempPackagePath, finalPackagePath); - - definition.PackagePath = finalPackagePath; - - return finalPackagePath; - } - finally - { - // Clean up - Directory.Delete(temporaryPath, true); - } - } - - private XDocument CreateCompiledPackageXml(out XElement root) - { - root = new XElement("umbPackage"); - var compiledPackageXml = new XDocument(root); - return compiledPackageXml; - } - - private void ValidatePackage(PackageDefinition definition) - { - // Ensure it's valid - var context = new ValidationContext(definition, serviceProvider: null, items: null); - var results = new List(); - var isValid = Validator.TryValidateObject(definition, context, results); - if (!isValid) - { - throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + - string.Join(", ", results.Select(x => x.ErrorMessage))); - } - } - - private void PackageDataTypes(PackageDefinition definition, XContainer root) - { - var dataTypes = new XElement("DataTypes"); - foreach (var dtId in definition.DataTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IDataType? dataType = _dataTypeService.GetDataType(outInt); - if (dataType == null) - { - continue; - } - - dataTypes.Add(_serializer.Serialize(dataType)); - } - - root.Add(dataTypes); - } - - private void PackageLanguages(PackageDefinition definition, XContainer root) - { - var languages = new XElement("Languages"); - foreach (var langId in definition.Languages) - { - if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - ILanguage? lang = _localizationService.GetLanguageById(outInt); - if (lang == null) - { - continue; - } - - languages.Add(_serializer.Serialize(lang)); - } - - root.Add(languages); - } - - private void PackageDictionaryItems(PackageDefinition definition, XContainer root) - { - var rootDictionaryItems = new XElement("DictionaryItems"); - var items = new Dictionary(); - - foreach (var dictionaryId in definition.DictionaryItems) - { - if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IDictionaryItem? di = _localizationService.GetDictionaryItemById(outInt); - - if (di == null) - { - continue; - } - - items[di.Key] = (di, _serializer.Serialize(di, false)); - } - - // organize them in hierarchy ... - var itemCount = items.Count; - var processed = new Dictionary(); - while (processed.Count < itemCount) - { - foreach (Guid key in items.Keys.ToList()) - { - (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - - if (!dictionaryItem.ParentId.HasValue) - { - // if it has no parent, its definitely just at the root - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + // we know the parent exists in the dictionary but + // we haven't processed it yet so we'll leave it for the next loop } else { - if (processed.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we've processed this parent element already so we can just append this xml child to it - AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, - serializedDictionaryValue); - } - else if (items.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we know the parent exists in the dictionary but - // we haven't processed it yet so we'll leave it for the next loop - continue; - } - else - { - // in this case, the parent of this item doesn't exist in our collection, we have no - // choice but to add it to the root. - AppendDictionaryElement(rootDictionaryItems, items, processed, key, - serializedDictionaryValue); - } + // in this case, the parent of this item doesn't exist in our collection, we have no + // choice but to add it to the root. + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } } } + } - root.Add(rootDictionaryItems); + root.Add(rootDictionaryItems); - static void AppendDictionaryElement(XElement rootDictionaryItems, - Dictionary items, - Dictionary processed, Guid key, XElement serializedDictionaryValue) + static void AppendDictionaryElement( + XElement rootDictionaryItems, + Dictionary items, + Dictionary processed, + Guid key, + XElement serializedDictionaryValue) + { + // track it + processed.Add(key, serializedDictionaryValue); + + // append it + rootDictionaryItems.Add(serializedDictionaryValue); + + // remove it so its not re-processed + items.Remove(key); + } + } + + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) + { + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - // track it - processed.Add(key, serializedDictionaryValue); + continue; + } - // append it - rootDictionaryItems.Add(serializedDictionaryValue); + XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); + if (macroXml is null) + { + continue; + } - // remove it so its not re-processed - items.Remove(key); + macros.Add(macroXml); + packagedMacros.Add(macro!); + } + + root.Add(macros); + + // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) + IEnumerable views = packagedMacros + .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) + .Select(x => + x.MacroSource[Constants.SystemDirectories.MacroPartials.Length..].Replace('/', '\\')); + PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem!); + } + + private void PackageStylesheets(PackageDefinition definition, XContainer root) + { + var stylesheetsXml = new XElement("Stylesheets"); + foreach (var stylesheet in definition.Stylesheets) + { + if (stylesheet.IsNullOrWhiteSpace()) + { + continue; + } + + XElement? xml = GetStylesheetXml(stylesheet, true); + if (xml != null) + { + stylesheetsXml.Add(xml); } } - private void PackageMacros(PackageDefinition definition, XContainer root) + root.Add(stylesheetsXml); + } + + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) { - var packagedMacros = new List(); - var macros = new XElement("Macros"); - foreach (var macroId in definition.Macros) + if (file.IsNullOrWhiteSpace()) { - if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt)) - { - continue; - } - - XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); - if (macroXml is null) - { - continue; - } - - macros.Add(macroXml); - packagedMacros.Add(macro!); + continue; } - root.Add(macros); - - // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) - IEnumerable views = packagedMacros - .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) - .Select(x => - x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); - PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem!); - } - - private void PackageStylesheets(PackageDefinition definition, XContainer root) - { - var stylesheetsXml = new XElement("Stylesheets"); - foreach (var stylesheet in definition.Stylesheets) + if (!fileSystem.FileExists(file)) { - if (stylesheet.IsNullOrWhiteSpace()) - { - continue; - } - - XElement? xml = GetStylesheetXml(stylesheet, true); - if (xml != null) - { - stylesheetsXml.Add(xml); - } + throw new InvalidOperationException("No file found with path " + file); } - root.Add(stylesheetsXml); + using Stream stream = fileSystem.OpenFile(file); + + using (var reader = new StreamReader(stream)) + { + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); + } } - private void PackageStaticFiles( - IEnumerable filePaths, - XContainer root, - string containerName, - string elementName, - IFileSystem fileSystem) + root.Add(scriptsXml); + } + + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) { - var scriptsXml = new XElement(containerName); - foreach (var file in filePaths) + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (file.IsNullOrWhiteSpace()) - { - continue; - } + continue; + } - if (!fileSystem.FileExists(file)) - { - throw new InvalidOperationException("No file found with path " + file); - } + ITemplate? template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; + } - using Stream? stream = fileSystem.OpenFile(file); - if (stream is not null) + templatesXml.Add(_serializer.Serialize(template)); + } + + root.Add(templatesXml); + } + + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + { + var contentTypes = new HashSet(); + var docTypesXml = new XElement("DocumentTypes"); + foreach (var dtId in definition.DocumentTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IContentType? contentType = _contentTypeService.Get(outInt); + if (contentType == null) + { + continue; + } + + AddDocumentType(contentType, contentTypes); + } + + foreach (IContentType contentType in contentTypes) + { + docTypesXml.Add(_serializer.Serialize(contentType)); + } + + root.Add(docTypesXml); + } + + private void PackageMediaTypes(PackageDefinition definition, XContainer root) + { + var mediaTypes = new HashSet(); + var mediaTypesXml = new XElement("MediaTypes"); + foreach (var mediaTypeId in definition.MediaTypes) + { + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IMediaType? mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) + { + continue; + } + + AddMediaType(mediaType, mediaTypes); + } + + foreach (IMediaType mediaType in mediaTypes) + { + mediaTypesXml.Add(_serializer.Serialize(mediaType)); + } + + root.Add(mediaTypesXml); + } + + private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + { + // Documents and tags + if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse( + definition.ContentNodeId, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var contentNodeId)) + { + if (contentNodeId > 0) + { + // load content from umbraco. + IContent? content = _contentService.GetById(contentNodeId); + if (content != null) { - using (var reader = new StreamReader(stream)) - { - var fileContents = reader.ReadToEnd(); - scriptsXml.Add( + XElement contentXml = definition.ContentLoadChildNodes + ? content.ToDeepXml(_serializer) + : content.ToXml(_serializer); + + // Create the Documents/DocumentSet node + root.Add( + new XElement( + "Documents", new XElement( - elementName, - new XAttribute("path", file), - new XCData(fileContents))); - } - } - } - - root.Add(scriptsXml); - } - - private void PackageTemplates(PackageDefinition definition, XContainer root) - { - var templatesXml = new XElement("Templates"); - foreach (var templateId in definition.Templates) - { - if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - ITemplate? template = _fileService.GetTemplate(outInt); - if (template == null) - { - continue; - } - - templatesXml.Add(_serializer.Serialize(template)); - } - - root.Add(templatesXml); - } - - private void PackageDocumentTypes(PackageDefinition definition, XContainer root) - { - var contentTypes = new HashSet(); - var docTypesXml = new XElement("DocumentTypes"); - foreach (var dtId in definition.DocumentTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IContentType? contentType = _contentTypeService.Get(outInt); - if (contentType == null) - { - continue; - } - - AddDocumentType(contentType, contentTypes); - } - - foreach (IContentType contentType in contentTypes) - { - docTypesXml.Add(_serializer.Serialize(contentType)); - } - - root.Add(docTypesXml); - } - - private void PackageMediaTypes(PackageDefinition definition, XContainer root) - { - var mediaTypes = new HashSet(); - var mediaTypesXml = new XElement("MediaTypes"); - foreach (var mediaTypeId in definition.MediaTypes) - { - if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IMediaType? mediaType = _mediaTypeService.Get(outInt); - if (mediaType == null) - { - continue; - } - - AddMediaType(mediaType, mediaTypes); - } - - foreach (IMediaType mediaType in mediaTypes) - { - mediaTypesXml.Add(_serializer.Serialize(mediaType)); - } - - root.Add(mediaTypesXml); - } - - private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) - { - // Documents and tags - if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, - NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) - { - if (contentNodeId > 0) - { - // load content from umbraco. - IContent? content = _contentService.GetById(contentNodeId); - if (content != null) - { - var contentXml = definition.ContentLoadChildNodes - ? content.ToDeepXml(_serializer) - : content.ToXml(_serializer); - - // Create the Documents/DocumentSet node - - root.Add( - new XElement( - "Documents", - new XElement( - "DocumentSet", - new XAttribute("importMode", "root"), - contentXml))); - } + "DocumentSet", + new XAttribute("importMode", "root"), + contentXml))); } } } + } - private Dictionary PackageMedia(PackageDefinition definition, XElement root) + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); + + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) { - var mediaStreams = new Dictionary(); - - // callback that occurs on each serialized media item - void OnSerializedMedia(IMedia media, XElement xmlMedia) + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + if (mediaFilePath is not null) { - // get the media file path and store that separately in the XML. - // the media file path is different from the URL and is specifically - // extracted using the property editor for this media file and the current media file system. - Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); - if (mediaStream != null) - { - xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath!)); + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); - // add the stream to our outgoing stream - mediaStreams.Add(mediaFilePath!, mediaStream); - } - } - - IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - - var mediaXml = new XElement( - "MediaItems", - medias.Select(media => - { - XElement serializedMedia = _serializer.Serialize( - media, - definition.MediaLoadChildNodes, - OnSerializedMedia); - - return new XElement("MediaSet", serializedMedia); - })); - - root.Add(mediaXml); - - return mediaStreams; - } - - /// - /// Gets a macros xml node - /// - private XElement? GetMacroXml(int macroId, out IMacro? macro) - { - macro = _macroService.GetById(macroId); - if (macro == null) - { - return null; - } - - XElement xml = _serializer.Serialize(macro); - return xml; - } - - /// - /// Converts a umbraco stylesheet to a package xml node - /// - /// The path of the stylesheet. - /// if set to true [include properties]. - private XElement? GetStylesheetXml(string path, bool includeProperties) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - } - - IStylesheet? stylesheet = _fileService.GetStylesheet(path); - if (stylesheet == null) - { - return null; - } - - return _serializer.Serialize(stylesheet, includeProperties); - } - - private void AddDocumentType(IContentType dt, HashSet dtl) - { - if (dt.ParentId > 0) - { - IContentType? parent = _contentTypeService.Get(dt.ParentId); - if (parent != null) - { - AddDocumentType(parent, dtl); - } - } - - if (!dtl.Contains(dt)) - { - dtl.Add(dt); + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); } } - private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) - { - if (mediaType.ParentId > 0) - { - IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); - if (parent != null) - { - AddMediaType(parent, mediaTypes); - } - } + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - if (!mediaTypes.Contains(mediaType)) + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => { - mediaTypes.Add(mediaType); + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); + + return new XElement("MediaSet", serializedMedia); + })); + + root.Add(mediaXml); + + return mediaStreams; + } + + /// + /// Gets a macros xml node + /// + private XElement? GetMacroXml(int macroId, out IMacro? macro) + { + macro = _macroService.GetById(macroId); + if (macro == null) + { + return null; + } + + XElement xml = _serializer.Serialize(macro); + return xml; + } + + /// + /// Converts a umbraco stylesheet to a package xml node + /// + /// The path of the stylesheet. + /// if set to true [include properties]. + private XElement? GetStylesheetXml(string path, bool includeProperties) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + } + + IStylesheet? stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) + { + return null; + } + + return _serializer.Serialize(stylesheet, includeProperties); + } + + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) + { + IContentType? parent = _contentTypeService.Get(dt.ParentId); + if (parent != null) + { + AddDocumentType(parent, dtl); } } - private static XElement GetPackageInfoXml(PackageDefinition definition) + if (!dtl.Contains(dt)) { - var info = new XElement("info"); + dtl.Add(dt); + } + } - // Package info - var package = new XElement("package"); - package.Add(new XElement("name", definition.Name)); - info.Add(package); - return info; + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) + { + IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); + if (parent != null) + { + AddMediaType(parent, mediaTypes); + } + } + + if (!mediaTypes.Contains(mediaType)) + { + mediaTypes.Add(mediaType); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs index f8fc9e14be..e9aaf82e87 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs @@ -1,14 +1,18 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DataTypeContainerRepository : EntityContainerRepository, IDataTypeContainerRepository { - internal class DataTypeContainerRepository : EntityContainerRepository, IDataTypeContainerRepository + public DataTypeContainerRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DataTypeContainer) { - public DataTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.DataTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index 7ca3e8c3c3..9f9d685552 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -23,331 +19,329 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class DataTypeRepository : EntityRepositoryBase, IDataTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class DataTypeRepository : EntityRepositoryBase, IDataTypeRepository + private readonly ILogger _dataTypeLogger; + private readonly PropertyEditorCollection _editors; + private readonly IConfigurationEditorJsonSerializer _serializer; + + public DataTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + PropertyEditorCollection editors, + ILogger logger, + ILoggerFactory loggerFactory, + IConfigurationEditorJsonSerializer serializer) + : base(scopeAccessor, cache, logger) { - private readonly PropertyEditorCollection _editors; - private readonly IConfigurationEditorJsonSerializer _serializer; - private readonly ILogger _dataTypeLogger; + _editors = editors; + _serializer = serializer; + _dataTypeLogger = loggerFactory.CreateLogger(); + } - public DataTypeRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - PropertyEditorCollection editors, - ILogger logger, - ILoggerFactory loggerFactory, - IConfigurationEditorJsonSerializer serializer) - : base(scopeAccessor, cache, logger) + protected Guid NodeObjectTypeId => Constants.ObjectTypes.DataType; + + public IEnumerable> Move(IDataType toMove, EntityContainer? container) + { + var parentId = -1; + if (container != null) { - _editors = editors; - _serializer = serializer; - _dataTypeLogger = loggerFactory.CreateLogger(); - } - - #region Overrides of RepositoryBase - - protected override IDataType? PerformGet(int id) - { - return GetMany(id)?.FirstOrDefault(); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var dataTypeSql = GetBaseQuery(false); - - if (ids?.Any() ?? false) + // Check on paths + if (string.Format(",{0},", container.Path) + .IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { - dataTypeSql.Where("umbracoNode.id in (@ids)", new { ids }); - } - else - { - dataTypeSql.Where(x => x.NodeObjectType == NodeObjectTypeId); + throw new DataOperationException( + MoveOperationStatusType.FailedNotAllowedByPath); } - var dtos = Database.Fetch(dataTypeSql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + parentId = container.Id; } - protected override IEnumerable PerformGetByQuery(IQuery query) + // used to track all the moved entities to be given to the event + var moveInfo = new List> { new(toMove, toMove.Path, parentId) }; + + var origPath = toMove.Path; + + // do the move to a new parent + toMove.ParentId = parentId; + + // set the updated path + toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); + + // schedule it for updating in the transaction + Save(toMove); + + // update all descendants from the original path, update in order of level + IEnumerable descendants = + Get(Query().Where(type => type.Path.StartsWith(origPath + ","))); + + IDataType lastParent = toMove; + if (descendants is not null) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(r => r.Select(x => x.NodeDto)); - - sql - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - return Array.Empty(); - } - - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DataType; - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IDataType entity) - { - entity.AddingEntity(); - - //ensure a datatype has a unique name before creating it - entity.Name = EnsureUniqueNodeName(entity.Name)!; - - // TODO: should the below be removed? - //Cannot add a duplicate data type - var existsSql = Sql() - .SelectCount() - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .Where(x => x.Text == entity.Name); - var exists = Database.ExecuteScalar(existsSql) > 0; - if (exists) + foreach (IDataType descendant in descendants.OrderBy(x => x.Level)) { - throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.ParentId = lastParent.Id; + descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); + + // schedule it for updating in the transaction + Save(descendant); } - - var dto = DataTypeFactory.BuildDto(entity, _serializer); - - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(IDataType entity) + return moveInfo; + } + + public IReadOnlyDictionary> FindUsages(int id) + { + if (id == default) { - - entity.Name = EnsureUniqueNodeName(entity.Name, entity.Id)!; - - //Cannot change to a duplicate alias - var existsSql = Sql() - .SelectCount() - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .Where(x => x.Text == entity.Name && x.NodeId != entity.Id); - var exists = Database.ExecuteScalar(existsSql) > 0; - if (exists) - { - throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); - } - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - var dto = DataTypeFactory.BuildDto(entity, _serializer); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - Database.Update(nodeDto); - Database.Update(dto); - - entity.ResetDirtyProperties(); + return new Dictionary>(); } - protected override void PersistDeletedItem(IDataType entity) - { - //Remove Notifications - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + Sql sql = Sql() + .Select(ct => ct.Select(node => node.NodeDto)) + .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) + .From() + .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) + .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) + .Where(pt => pt.DataTypeId == id) + .OrderBy(node => node.NodeId) + .AndBy(pt => pt.Alias); - //Remove Permissions - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + List? dtos = + Database.FetchOneToMany(ct => ct.PropertyTypes, sql); - //Remove associated tags - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + return dtos.ToDictionary( + x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType!.Value), x.NodeDto.UniqueId) + .EnsureClosed(), + x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); + } - //PropertyTypes containing the DataType being deleted - var propertyTypeDtos = Database.Fetch("WHERE dataTypeId = @Id", new { Id = entity.Id }); - //Go through the PropertyTypes and delete referenced PropertyData before deleting the PropertyType - foreach (var dto in propertyTypeDtos) - { - Database.Delete("WHERE propertytypeid = @Id", new { Id = dto.Id }); - Database.Delete("WHERE id = @Id", new { Id = dto.Id }); - } + #region Overrides of RepositoryBase - //Delete Content specific data - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + protected override IDataType? PerformGet(int id) => GetMany(id).FirstOrDefault(); - //Delete (base) node data - Database.Delete("WHERE uniqueID = @Id", new { Id = entity.Key }); - - entity.DeleteDate = DateTime.Now; - } - - #endregion - - public IEnumerable> Move(IDataType toMove, EntityContainer? container) - { - var parentId = -1; - if (container != null) - { - // Check on paths - if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); - } - parentId = container.Id; - } - - //used to track all the moved entities to be given to the event - var moveInfo = new List> - { - new MoveEventInfo(toMove, toMove.Path, parentId) - }; - - var origPath = toMove.Path; - - //do the move to a new parent - toMove.ParentId = parentId; - - //set the updated path - toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); - - //schedule it for updating in the transaction - Save(toMove); - - //update all descendants from the original path, update in order of level - var descendants = Get(Query().Where(type => type.Path.StartsWith(origPath + ","))); - - var lastParent = toMove; - if (descendants is not null) - { - foreach (var descendant in descendants.OrderBy(x => x.Level)) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.ParentId = lastParent.Id; - descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); - - //schedule it for updating in the transaction - Save(descendant); - } - } - - return moveInfo; - } - - public IReadOnlyDictionary> FindUsages(int id) - { - if (id == default) - return new Dictionary>(); - - var sql = Sql() - .Select(ct => ct.Select(node => node.NodeDto)) - .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) - .From() - .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) - .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) - .Where(pt => pt.DataTypeId == id) - .OrderBy(node => node.NodeId) - .AndBy(pt => pt.Alias); - - var dtos = Database.FetchOneToMany(ct => ct.PropertyTypes, sql); - - return dtos.ToDictionary( - x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType!.Value), x.NodeDto.UniqueId).EnsureClosed(), - x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); - } - - private string? EnsureUniqueNodeName(string? nodeName, int id = 0) - { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.DataTypeRepository.EnsureUniqueNodeName, tsql => tsql + private string? EnsureUniqueNodeName(string? nodeName, int id = 0) + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.DataTypeRepository.EnsureUniqueNodeName, + tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType"))); - var sql = template.Sql(NodeObjectTypeId); - var names = Database.Fetch(sql); + Sql sql = template.Sql(NodeObjectTypeId); + List? names = Database.Fetch(sql); - return SimilarNodeName.GetUniqueName(names, id, nodeName); - } - - - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ContentType)] - private class ContentTypeReferenceDto : ContentTypeDto - { - [ResultColumn] - [Reference(ReferenceType.Many)] - public List PropertyTypes { get; set; } = null!; - } - - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - private class PropertyTypeReferenceDto - { - [Column("ptAlias")] - public string? Alias { get; set; } - - [Column("ptName")] - public string? Name { get; set; } - } + return SimilarNodeName.GetUniqueName(names, id, nodeName); } + + [TableName(Constants.DatabaseSchema.Tables.ContentType)] + private class ContentTypeReferenceDto : ContentTypeDto + { + [ResultColumn] + [Reference(ReferenceType.Many)] + public List PropertyTypes { get; } = null!; + } + + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + private class PropertyTypeReferenceDto + { + [Column("ptAlias")] + public string? Alias { get; set; } + + [Column("ptName")] + public string? Name { get; set; } + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql dataTypeSql = GetBaseQuery(false); + + if (ids?.Any() ?? false) + { + dataTypeSql.Where("umbracoNode.id in (@ids)", new { ids }); + } + else + { + dataTypeSql.Where(x => x.NodeObjectType == NodeObjectTypeId); + } + + List? dtos = Database.Fetch(dataTypeSql); + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(r => r.Select(x => x.NodeDto)); + + sql + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() => "umbracoNode.id = @id"; + + protected override IEnumerable GetDeleteClauses() => Array.Empty(); + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IDataType entity) + { + entity.AddingEntity(); + + // ensure a datatype has a unique name before creating it + entity.Name = EnsureUniqueNodeName(entity.Name)!; + + // TODO: should the below be removed? + // Cannot add a duplicate data type + Sql existsSql = Sql() + .SelectCount() + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .Where(x => x.Text == entity.Name); + var exists = Database.ExecuteScalar(existsSql) > 0; + if (exists) + { + throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); + } + + DataTypeDto dto = DataTypeFactory.BuildDto(entity, _serializer); + + // Logic for setting Path, Level and SortOrder + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + var level = parent.Level + 1; + var sortOrder = + Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + + // Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + // Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + // Update entity with correct values + entity.Id = nodeDto.NodeId; // Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IDataType entity) + { + entity.Name = EnsureUniqueNodeName(entity.Name, entity.Id)!; + + // Cannot change to a duplicate alias + Sql existsSql = Sql() + .SelectCount() + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .Where(x => x.Text == entity.Name && x.NodeId != entity.Id); + var exists = Database.ExecuteScalar(existsSql) > 0; + if (exists) + { + throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); + } + + // Updates Modified date + entity.UpdatingEntity(); + + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + } + + DataTypeDto dto = DataTypeFactory.BuildDto(entity, _serializer); + + // Updates the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + Database.Update(nodeDto); + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + protected override void PersistDeletedItem(IDataType entity) + { + // Remove Notifications + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // Remove Permissions + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // Remove associated tags + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // PropertyTypes containing the DataType being deleted + List? propertyTypeDtos = + Database.Fetch("WHERE dataTypeId = @Id", new { entity.Id }); + + // Go through the PropertyTypes and delete referenced PropertyData before deleting the PropertyType + foreach (PropertyTypeDto? dto in propertyTypeDtos) + { + Database.Delete("WHERE propertytypeid = @Id", new { dto.Id }); + Database.Delete("WHERE id = @Id", new { dto.Id }); + } + + // Delete Content specific data + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // Delete (base) node data + Database.Delete("WHERE uniqueID = @Id", new { Id = entity.Key }); + + entity.DeleteDate = DateTime.Now; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeUsageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeUsageRepository.cs new file mode 100644 index 0000000000..4f7614416f --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeUsageRepository.cs @@ -0,0 +1,39 @@ +using NPoco; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class DataTypeUsageRepository : IDataTypeUsageRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public DataTypeUsageRepository(IScopeAccessor scopeAccessor) + { + _scopeAccessor = scopeAccessor; + } + + public bool HasSavedValues(int dataTypeId) + { + IUmbracoDatabase? database = _scopeAccessor.AmbientScope?.Database; + + if (database is null) + { + throw new InvalidOperationException("A scope is required to query the database"); + } + + Sql selectQuery = database.SqlContext.Sql() + .SelectAll() + .From("pt") + .InnerJoin("pd") + .On((left, right) => left.PropertyTypeId == right.Id, "pd", "pt") + .Where(pt => pt.DataTypeId == dataTypeId, "pt"); + + Sql hasValueQuery = database.SqlContext.Sql() + .SelectAnyIfExists(selectQuery); + + return database.ExecuteScalar(hasValueQuery); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index 7f39bb3ee6..6cd2d8989b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,383 +12,363 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement -{ - /// - /// Represents a repository for doing CRUD operations for - /// - internal class DictionaryRepository : EntityRepositoryBase, IDictionaryRepository - { - private readonly ILoggerFactory _loggerFactory; +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; - public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILoggerFactory loggerFactory) - : base(scopeAccessor, cache, logger) +/// +/// Represents a repository for doing CRUD operations for +/// +internal class DictionaryRepository : EntityRepositoryBase, IDictionaryRepository +{ + private readonly ILoggerFactory _loggerFactory; + + public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + ILoggerFactory loggerFactory) + : base(scopeAccessor, cache, logger) => + _loggerFactory = loggerFactory; + + public IDictionaryItem? Get(Guid uniqueId) + { + var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return uniqueIdRepo.Get(uniqueId); + } + + public IDictionaryItem? Get(string key) + { + var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return keyRepo.Get(key); + } + + public Dictionary GetDictionaryItemKeyMap() + { + var columns = new[] { "key", "id" }.Select(x => (object)SqlSyntax.GetQuotedColumnName(x)).ToArray(); + Sql sql = Sql().Select(columns).From(); + return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); + } + + public IEnumerable GetDictionaryItemDescendants(Guid? parentId) + { + // This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive + // lookup to get descendants. Currently this is the most efficient way to do it + Func>> getItemsFromParents = guids => { - _loggerFactory = loggerFactory; + return guids.InGroupsOf(Constants.Sql.MaxParameterCount) + .Select(group => + { + Sql sqlClause = GetBaseQuery(false) + .Where(x => x.Parent != null) + .WhereIn(x => x.Parent, group); + + var translator = new SqlTranslator(sqlClause, Query()); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.UniqueId); + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + }); + }; + + if (!parentId.HasValue) + { + Sql sql = GetBaseQuery(false) + .Where(x => x.PrimaryKey > 0) + .OrderBy(x => x.UniqueId); + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); } - protected override IRepositoryCachePolicy CreateCachePolicy() + return getItemsFromParents(new[] { parentId.Value }).SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())).SelectMany(items => items); + } + + protected override IRepositoryCachePolicy CreateCachePolicy() + { + var options = new RepositoryCachePolicyOptions + { + // allow zero to be cached + GetAllCacheAllowZeroCount = true, + }; + + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); + } + + protected IDictionaryItem ConvertFromDto(DictionaryDto dto) + { + IDictionaryItem entity = DictionaryItemFactory.BuildEntity(dto); + + entity.Translations = dto.LanguageTextDtos.EmptyNull() + .Where(x => x.LanguageId > 0) + .Select(x => DictionaryTranslationFactory.BuildEntity(x, dto.UniqueId)) + .ToList(); + + return entity; + } + + #region Overrides of RepositoryBase + + protected override IDictionaryItem? PerformGet(int id) + { + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { id }) + .OrderBy(x => x.UniqueId); + + DictionaryDto? dto = Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .FirstOrDefault(); + + if (dto == null) + { + return null; + } + + IDictionaryItem entity = ConvertFromDto(dto); + + // reset dirty initial properties (U4-1946) + ((EntityBase)entity).ResetDirtyProperties(false); + + return entity; + } + + private IEnumerable GetRootDictionaryItems() + { + IQuery query = Query().Where(x => x.ParentId == null); + return Get(query); + } + + private class DictionaryItemKeyIdDto + { + public string Key { get; } = null!; + + public Guid Id { get; set; } + } + + private class DictionaryByUniqueIdRepository : SimpleGetRepository + { + private readonly DictionaryRepository _dictionaryRepository; + + public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, + AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _dictionaryRepository = dictionaryRepository; + + protected override IEnumerable PerformFetch(Sql sql) => + Database + .FetchOneToMany(x => x.LanguageTextDtos, sql); + + protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); + + protected override string GetBaseWhereClause() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " = @id"; + + protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => + _dictionaryRepository.ConvertFromDto(dto); + + protected override object GetBaseWhereClauseArguments(Guid id) => new { id }; + + protected override string GetWhereInClauseForGetAll() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; + + protected override IRepositoryCachePolicy CreateCachePolicy() { var options = new RepositoryCachePolicyOptions { - //allow zero to be cached - GetAllCacheAllowZeroCount = true + // allow zero to be cached + GetAllCacheAllowZeroCount = true, }; - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); } + } - #region Overrides of RepositoryBase + private class DictionaryByKeyRepository : SimpleGetRepository + { + private readonly DictionaryRepository _dictionaryRepository; - protected override IDictionaryItem? PerformGet(int id) + public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, + AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _dictionaryRepository = dictionaryRepository; + + protected override IEnumerable PerformFetch(Sql sql) => + Database + .FetchOneToMany(x => x.LanguageTextDtos, sql); + + protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); + + protected override string GetBaseWhereClause() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " = @id"; + + protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => + _dictionaryRepository.ConvertFromDto(dto); + + protected override object GetBaseWhereClauseArguments(string? id) => new { id }; + + protected override string GetWhereInClauseForGetAll() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; + + protected override IRepositoryCachePolicy CreateCachePolicy() { - var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { id = id }) - .OrderBy(x => x.UniqueId); - - var dto = Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .FirstOrDefault(); - - if (dto == null) - return null; - - var entity = ConvertFromDto(dto); - - // reset dirty initial properties (U4-1946) - ((EntityBase)entity).ResetDirtyProperties(false); - - return entity; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); - if (ids?.Any() ?? false) + var options = new RepositoryCachePolicyOptions { - sql.WhereIn(x => x.PrimaryKey, ids); - } + // allow zero to be cached + GetAllCacheAllowZeroCount = true, + }; - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .Select(ConvertFromDto); + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); + } + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.PrimaryKey, ids); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql.OrderBy(x => x.UniqueId); + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + } - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .Select(ConvertFromDto); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.UniqueId); + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) + { + sql.SelectCount() + .From(); + } + else + { + sql.SelectAll() + .From() + .LeftJoin() + .On(left => left.UniqueId, right => right.UniqueId); } - #endregion + return sql; + } - #region Overrides of EntityRepositoryBase + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.DictionaryEntry}.pk = @id"; - protected override Sql GetBaseQuery(bool isCount) + protected override IEnumerable GetDeleteClauses() => new List(); + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IDictionaryItem entity) + { + var dictionaryItem = (DictionaryItem)entity; + + dictionaryItem.AddingEntity(); + + foreach (IDictionaryTranslation translation in dictionaryItem.Translations) { - var sql = Sql(); - if (isCount) + translation.Value = translation.Value.ToValidXmlString(); + } + + DictionaryDto dto = DictionaryItemFactory.BuildDto(dictionaryItem); + + var id = Convert.ToInt32(Database.Insert(dto)); + dictionaryItem.Id = id; + + foreach (IDictionaryTranslation translation in dictionaryItem.Translations) + { + LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, dictionaryItem.Key); + translation.Id = Convert.ToInt32(Database.Insert(textDto)); + translation.Key = dictionaryItem.Key; + } + + dictionaryItem.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IDictionaryItem entity) + { + entity.UpdatingEntity(); + + foreach (IDictionaryTranslation translation in entity.Translations) + { + translation.Value = translation.Value.ToValidXmlString(); + } + + DictionaryDto dto = DictionaryItemFactory.BuildDto(entity); + + Database.Update(dto); + + foreach (IDictionaryTranslation translation in entity.Translations) + { + LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, entity.Key); + if (translation.HasIdentity) { - sql.SelectCount() - .From(); + Database.Update(textDto); } else { - sql.SelectAll() - .From() - .LeftJoin() - .On(left => left.UniqueId, right => right.UniqueId); - } - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.DictionaryEntry}.pk = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - return new List(); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IDictionaryItem entity) - { - var dictionaryItem = ((DictionaryItem) entity); - - dictionaryItem.AddingEntity(); - - foreach (var translation in dictionaryItem.Translations) - translation.Value = translation.Value.ToValidXmlString(); - - var dto = DictionaryItemFactory.BuildDto(dictionaryItem); - - var id = Convert.ToInt32(Database.Insert(dto)); - dictionaryItem.Id = id; - - foreach (var translation in dictionaryItem.Translations) - { - var textDto = DictionaryTranslationFactory.BuildDto(translation, dictionaryItem.Key); translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = dictionaryItem.Key; - } - - dictionaryItem.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IDictionaryItem entity) - { - entity.UpdatingEntity(); - - foreach (var translation in entity.Translations) - translation.Value = translation.Value.ToValidXmlString(); - - var dto = DictionaryItemFactory.BuildDto(entity); - - Database.Update(dto); - - foreach (var translation in entity.Translations) - { - var textDto = DictionaryTranslationFactory.BuildDto(translation, entity.Key); - if (translation.HasIdentity) - { - Database.Update(textDto); - } - else - { - translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = entity.Key; - } - } - - entity.ResetDirtyProperties(); - - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); - } - - protected override void PersistDeletedItem(IDictionaryItem entity) - { - RecursiveDelete(entity.Key); - - Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); - Database.Delete("WHERE id = @Id", new { Id = entity.Key }); - - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); - - entity.DeleteDate = DateTime.Now; - } - - private void RecursiveDelete(Guid parentId) - { - var list = Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); - foreach (var dto in list) - { - RecursiveDelete(dto.UniqueId); - - Database.Delete("WHERE UniqueId = @Id", new { Id = dto.UniqueId }); - Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); - - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); + translation.Key = entity.Key; } } - #endregion + entity.ResetDirtyProperties(); - protected IDictionaryItem ConvertFromDto(DictionaryDto dto) + // Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + } + + protected override void PersistDeletedItem(IDictionaryItem entity) + { + RecursiveDelete(entity.Key); + + Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); + Database.Delete("WHERE id = @Id", new { Id = entity.Key }); + + // Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + + entity.DeleteDate = DateTime.Now; + } + + private void RecursiveDelete(Guid parentId) + { + List? list = + Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); + foreach (DictionaryDto? dto in list) { - var entity = DictionaryItemFactory.BuildEntity(dto); + RecursiveDelete(dto.UniqueId); - entity.Translations = dto.LanguageTextDtos.EmptyNull() - .Where(x => x.LanguageId > 0) - .Select(x => DictionaryTranslationFactory.BuildEntity(x, dto.UniqueId)) - .ToList(); + Database.Delete("WHERE UniqueId = @Id", new { Id = dto.UniqueId }); + Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); - return entity; - } - - public IDictionaryItem? Get(Guid uniqueId) - { - var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); - return uniqueIdRepo.Get(uniqueId); - } - - public IDictionaryItem? Get(string key) - { - var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); - return keyRepo.Get(key); - } - - private IEnumerable? GetRootDictionaryItems() - { - var query = Query().Where(x => x.ParentId == null); - return Get(query); - } - - public Dictionary GetDictionaryItemKeyMap() - { - var columns = new[] { "key", "id" }.Select(x => (object) SqlSyntax.GetQuotedColumnName(x)).ToArray(); - var sql = Sql().Select(columns).From(); - return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); - } - - private class DictionaryItemKeyIdDto - { - public string Key { get; set; } = null!; - public Guid Id { get; set; } - } - - public IEnumerable GetDictionaryItemDescendants(Guid? parentId) - { - //This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive - // lookup to get descendants. Currently this is the most efficient way to do it - - Func>> getItemsFromParents = guids => - { - return guids.InGroupsOf(Constants.Sql.MaxParameterCount) - .Select(group => - { - var sqlClause = GetBaseQuery(false) - .Where(x => x.Parent != null) - .WhereIn(x => x.Parent, group); - - var translator = new SqlTranslator(sqlClause, Query()); - var sql = translator.Translate(); - sql.OrderBy(x => x.UniqueId); - - return Database - .FetchOneToMany(x=> x.LanguageTextDtos, sql) - .Select(ConvertFromDto); - }); - }; - - var childItems = parentId.HasValue == false - ? new[] { GetRootDictionaryItems()! } - : getItemsFromParents(new[] { parentId.Value }); - - return childItems.SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())).SelectMany(items => items); - - } - - private class DictionaryByUniqueIdRepository : SimpleGetRepository - { - private readonly DictionaryRepository _dictionaryRepository; - - public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _dictionaryRepository = dictionaryRepository; - } - - protected override IEnumerable PerformFetch(Sql sql) - { - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _dictionaryRepository.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " = @id"; - } - - protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) - { - return _dictionaryRepository.ConvertFromDto(dto); - } - - protected override object GetBaseWhereClauseArguments(Guid id) - { - return new { id = id }; - } - - protected override string GetWhereInClauseForGetAll() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; - } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); - } - } - - private class DictionaryByKeyRepository : SimpleGetRepository - { - private readonly DictionaryRepository _dictionaryRepository; - - public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _dictionaryRepository = dictionaryRepository; - } - - protected override IEnumerable PerformFetch(Sql sql) - { - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _dictionaryRepository.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " = @id"; - } - - protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) - { - return _dictionaryRepository.ConvertFromDto(dto); - } - - protected override object GetBaseWhereClauseArguments(string? id) - { - return new { id = id }; - } - - protected override string GetWhereInClauseForGetAll() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; - } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); - } + // Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index f97aec0917..5bd2844405 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -1,5 +1,5 @@ -using System; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Repositories; @@ -8,43 +8,56 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Override the base content repository so we can change the node object type +/// +/// +/// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, +/// however to do that +/// requires quite a lot of work since we'd need to re-organize the inheritance quite a lot or create a helper class to +/// perform a lot of the underlying logic. +/// TODO: Create a helper method to contain most of the underlying logic for the ContentRepository +/// +internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { - /// - /// Override the base content repository so we can change the node object type - /// - /// - /// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, however to do that - /// requires quite a lot of work since we'd need to re-organize the inheritance quite a lot or create a helper class to perform a lot of the underlying logic. - /// - /// TODO: Create a helper method to contain most of the underlying logic for the ContentRepository - /// - internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository + public DocumentBlueprintRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + ILoggerFactory loggerFactory, + IContentTypeRepository contentTypeRepository, + ITemplateRepository templateRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base( + scopeAccessor, + appCaches, + logger, + loggerFactory, + contentTypeRepository, + templateRepository, + tagRepository, + languageRepository, + relationRepository, + relationTypeRepository, + propertyEditorCollection, + dataValueReferenceFactories, + dataTypeService, + serializer, + eventAggregator) { - public DocumentBlueprintRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - ILoggerFactory loggerFactory, - IContentTypeRepository contentTypeRepository, - ITemplateRepository templateRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditorCollection, - IDataTypeService dataTypeService, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, appCaches, logger, loggerFactory, contentTypeRepository, templateRepository, - tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, - dataValueReferenceFactories, dataTypeService, serializer, eventAggregator) - { - } - - protected override bool EnsureUniqueNaming => false; // duplicates are allowed - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DocumentBlueprint; } + + protected override bool EnsureUniqueNaming => false; // duplicates are allowed + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.DocumentBlueprint; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 39084aa5d9..bada35623b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,7 +11,6 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -24,361 +20,1100 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for . +/// +public class DocumentRepository : ContentRepositoryBase, IDocumentRepository { + private readonly AppCaches _appCaches; + private readonly ContentByGuidReadRepository _contentByGuidReadRepository; + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ILoggerFactory _loggerFactory; + private readonly IScopeAccessor _scopeAccessor; + private readonly IJsonSerializer _serializer; + private readonly ITagRepository _tagRepository; + private readonly ITemplateRepository _templateRepository; + private PermissionRepository? _permissionRepository; + /// - /// Represents a repository for doing CRUD operations for . + /// Constructor /// - public class DocumentRepository : ContentRepositoryBase, IDocumentRepository + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors + /// require services, yet these services require property editors + /// + public DocumentRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + ILoggerFactory loggerFactory, + IContentTypeRepository contentTypeRepository, + ITemplateRepository templateRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly IContentTypeRepository _contentTypeRepository; - private readonly ITemplateRepository _templateRepository; - private readonly ITagRepository _tagRepository; - private readonly IJsonSerializer _serializer; - private readonly AppCaches _appCaches; - private readonly ILoggerFactory _loggerFactory; - private PermissionRepository? _permissionRepository; - private readonly ContentByGuidReadRepository _contentByGuidReadRepository; - private readonly IScopeAccessor _scopeAccessor; + _contentTypeRepository = + contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); + _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _serializer = serializer; + _appCaches = appCaches; + _loggerFactory = loggerFactory; + _scopeAccessor = scopeAccessor; + _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, appCaches, + loggerFactory.CreateLogger()); + } - /// - /// Constructor - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors - /// - public DocumentRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - ILoggerFactory loggerFactory, - IContentTypeRepository contentTypeRepository, - ITemplateRepository templateRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditors, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) + protected override DocumentRepository This => this; + + /// + /// Default is to always ensure all documents have unique names + /// + protected virtual bool EnsureUniqueNaming { get; } = true; + + // note: is ok to 'new' the repo here as it's a sub-repo really + private PermissionRepository PermissionRepository => _permissionRepository + ?? (_permissionRepository = + new PermissionRepository( + _scopeAccessor, _appCaches, + _loggerFactory + .CreateLogger< + PermissionRepository>())); + + /// + public ContentScheduleCollection GetContentSchedule(int contentId) + { + var result = new ContentScheduleCollection(); + + List? scheduleDtos = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.NodeId == contentId)); + + foreach (ContentScheduleDto? scheduleDto in scheduleDtos) { - _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); - _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _serializer = serializer; - _appCaches = appCaches; - _loggerFactory = loggerFactory; - _scopeAccessor = scopeAccessor; - _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); + result.Add(new ContentSchedule(scheduleDto.Id, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, + scheduleDto.Date, + scheduleDto.Action == ContentScheduleAction.Release.ToString() + ? ContentScheduleAction.Release + : ContentScheduleAction.Expire)); } - protected override DocumentRepository This => this; + return result; + } - /// - /// Default is to always ensure all documents have unique names - /// - protected virtual bool EnsureUniqueNaming { get; } = true; - - // note: is ok to 'new' the repo here as it's a sub-repo really - private PermissionRepository PermissionRepository => _permissionRepository - ?? (_permissionRepository = new PermissionRepository(_scopeAccessor, _appCaches, _loggerFactory.CreateLogger>())); - - #region Repository Base - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Document; - - protected override IContent? PerformGet(int id) + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + // note: 'updater' is the user who created the latest draft version, + // we don't have an 'updater' per culture (should we?) + if (ordering.OrderBy.InvariantEquals("updater")) { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); + Sql joins = Sql() + .InnerJoin("updaterUser") + .On((version, user) => version.UserId == user.Id, + aliasRight: "updaterUser"); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); + // see notes in ApplyOrdering: the field MUST be selected + aliased + sql = Sql( + InsertBefore(sql, "FROM", + ", " + SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering "), + sql.Arguments); + + sql = InsertJoins(sql, joins); + + return "ordering"; } - protected override IEnumerable PerformGetAll(params int[]? ids) + if (ordering.OrderBy.InvariantEquals("published")) { - var sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); - - return MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(QueryType.Many); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - AddGetByQueryOrderBy(sql); - - return MapDtosToContent(Database.Fetch(sql)); - } - - private void AddGetByQueryOrderBy(Sql sql) - { - sql - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); - } - - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType, true); - } - - // gets the COALESCE expression for variant/invariant name - private string VariantNameSqlExpression - => SqlContext.VisitDto((ccv, node) => ccv.Name ?? node.Text, "ccv").Sql; - - protected Sql GetBaseQuery(QueryType queryType, bool current) - { - var sql = SqlContext.Sql(); - - switch (queryType) + // no culture = can only work on the global 'published' flag + if (ordering.Culture.IsNullOrWhiteSpace()) { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - // R# may flag this ambiguous and red-squiggle it, but it is not - sql = sql.Select(r => - r.Select(documentDto => documentDto.ContentDto, r1 => - r1.Select(contentDto => contentDto.NodeDto)) - .Select(documentDto => documentDto.DocumentVersionDto, r1 => - r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) - .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => - r1.Select(documentVersionDto => documentVersionDto!.ContentVersionDto, "pcv"))) - - // select the variant name, coalesce to the invariant name, as "variantName" - .AndSelect(VariantNameSqlExpression + " AS variantName"); - break; + // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have + // the whole CASE fragment in ORDER BY due to it not being detected by NPoco + sql = Sql(InsertBefore(sql, "FROM", ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering "), + sql.Arguments); + return "ordering"; } - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) + // invariant: left join will yield NULL and we must use pcv to determine published + // variant: left join may yield NULL or something, and that determines published - // inner join on mandatory edited version - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.Id == right.Id) - // left join on optional published version - .LeftJoin(nested => - nested.InnerJoin("pdv") - .On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") - - // TODO: should we be joining this when the query type is not single/many? + Sql joins = Sql() + .InnerJoin("ctype").On( + (content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype") // left join on optional culture variation //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code .LeftJoin(nested => - nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") - .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv"); + nested.InnerJoin("langp").On( + (ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccvp", + "langp"), + "ccvp") + .On((version, ccv) => version.Id == ccv.VersionId, + "pcv", "ccvp"); - sql - .Where(x => x.NodeObjectType == NodeObjectTypeId); + sql = InsertJoins(sql, joins); - // this would ensure we don't get the published version - keep for reference - //sql - // .WhereAny( - // x => x.Where((x1, x2) => x1.Id != x2.Id, alias2: "pcv"), - // x => x.WhereNull(x1 => x1.Id, "pcv") - // ); + // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have + // the whole CASE fragment in ORDER BY due to it not being detected by NPoco + var sqlText = InsertBefore(sql.SQL, "FROM", - if (current) - sql.Where(x => x.Current); // always get the current version + // when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id, + // otherwise check if there's a version culture variation for the lang, via ccv.id + ", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccvp.id IS NULL THEN 0 ELSE 1 END) END) AS ordering "); // trailing space is important! - return sql; + sql = Sql(sqlText, sql.Arguments); + + return "ordering"; } - protected override Sql GetBaseQuery(bool isCount) - { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - } + return base.ApplySystemOrdering(ref sql, ordering); + } - // ah maybe not, that what's used for eg Exists in base repo - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; - } + private IEnumerable MapDtosToContent(List dtos, + bool withCache = false, + bool loadProperties = true, + bool loadTemplates = true, + bool loadVariants = true) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var templateIds = new List(); - protected override IEnumerable GetDeleteClauses() + var content = new Content[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) { - var list = new List + DocumentDto dto = dtos[i]; + + if (withCache) { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.RedirectUrl + " WHERE contentKey IN (SELECT uniqueId FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + // if the cache contains the (proper version of the) item, use it + IContent? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) + { + content[i] = (Content)cached; + continue; + } + } + + // else, need to build it + + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IContentType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _contentTypeRepository.Get(contentTypeId); + } + + Content c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + if (loadTemplates) + { + // need templates + var templateId = dto.DocumentVersionDto.TemplateId; + if (templateId.HasValue) + { + templateIds.Add(templateId.Value); + } + + if (dto.Published) + { + templateId = dto.PublishedVersionDto!.TemplateId; + if (templateId.HasValue) + { + templateIds.Add(templateId.Value); + } + } + } + + // need temps, for properties, templates and variations + var versionId = dto.DocumentVersionDto.Id; + var publishedVersionId = dto.Published ? dto.PublishedVersionDto!.Id : 0; + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType, c) + { + Template1Id = dto.DocumentVersionDto.TemplateId }; - return list; + if (dto.Published) + { + temp.Template2Id = dto.PublishedVersionDto!.TemplateId; + } + + temps.Add(temp); } - #endregion - - #region Versions - - public override IEnumerable GetAllVersions(int nodeId) + Dictionary? templates = null; + if (loadTemplates) { - var sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - return MapDtosToContent(Database.Fetch(sql), true); + // load all required templates in 1 query, and index + templates = _templateRepository.GetMany(templateIds.ToArray())? + .ToDictionary(x => x.Id, x => x); } - // TODO: This method needs to return a readonly version of IContent! The content returned - // from this method does not contain all of the data required to re-persist it and if that - // is attempted some odd things will occur. - // Either we create an IContentReadOnly (which ultimately we should for vNext so we can - // differentiate between methods that return entities that can be re-persisted or not), or - // in the meantime to not break API compatibility, we can add a property to IContentBase - // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw - // an exception if that entity is passed to a Save method. - // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService. - // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly - // which can return IContentReadOnly. - // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a - // content item. Ideally for paged data that populates list views, these would be ultra slim - // content items, there's no reason to populate those with really anything apart from property data, - // but until we do something like the above, we can't do that since it would be breaking and unclear. - public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + IDictionary? properties = null; + if (loadProperties) { - var sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - var pageIndex = skip / take; - - return MapDtosToContent(Database.Page(pageIndex+1, take, sql).Items, true, - // load bare minimum, need variants though since this is used to rollback with variants - false, false, true); + // load all properties for all documents from database in 1 query - indexed by version id + properties = GetPropertyCollections(temps); } - public override IContent? GetVersion(int versionId) + // assign templates and properties + foreach (TempContent temp in temps) { - var sql = GetBaseQuery(QueryType.Single, false) - .Where(x => x.Id == versionId); + if (loadTemplates) + { + // set the template ID if it matches an existing template + if (temp.Template1Id.HasValue && (templates?.ContainsKey(temp.Template1Id.Value) ?? false)) + { + temp.Content!.TemplateId = temp.Template1Id; + } - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); + if (temp.Template2Id.HasValue && (templates?.ContainsKey(temp.Template2Id.Value) ?? false)) + { + temp.Content!.PublishTemplateId = temp.Template2Id; + } + } + + + // set properties + if (loadProperties) + { + if (properties?.ContainsKey(temp.VersionId) ?? false) + { + temp.Content!.Properties = properties[temp.VersionId]; + } + else + { + throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); + } + } } - // deletes a specific version - public override void DeleteVersion(int versionId) + if (loadVariants) { - // TODO: test object node type? - - // get the version we want to delete - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql => - tsql.Select() - .AndSelect() - .From() - .InnerJoin() - .On((c, d) => c.Id == d.Id) - .Where(x => x.Id == SqlTemplate.Arg("versionId")) - ); - var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); - - // nothing to delete - if (versionDto == null) - return; - - // don't delete the current or published version - if (versionDto.ContentVersionDto.Current) - throw new InvalidOperationException("Cannot delete the current version."); - else if (versionDto.Published) - throw new InvalidOperationException("Cannot delete the published version."); - - PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId); + // set variations, if varying + temps = temps.Where(x => x.ContentType?.VariesByCulture() ?? false).ToList(); + if (temps.Count > 0) + { + // load all variations for all documents from database, in one query + IDictionary> contentVariations = GetContentVariations(temps); + IDictionary> documentVariations = GetDocumentVariations(temps); + foreach (TempContent temp in temps) + { + SetVariations(temp.Content, contentVariations, documentVariations); + } + } } - // deletes all versions of an entity, older than a date. - public override void DeleteVersions(int nodeId, DateTime versionDate) - { - // TODO: test object node type? - // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql => - tsql.Select() - .From() - .InnerJoin() - .On((c, d) => c.Id == d.Id) - .Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - .Where(x => !x.Published) - ); - var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); - foreach (var versionDto in versionDtos) - PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + foreach (Content c in content) + { + c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) } - protected override void PerformDeleteVersion(int id, int versionId) + return content; + } + + private IContent MapDtoToContent(DocumentDto dto) + { + IContentType? contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); + Content content = ContentBaseFactory.BuildEntity(dto, contentType); + + try { - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE id = @versionId", new { versionId }); - Database.Delete("WHERE id = @versionId", new { versionId }); + content.DisableChangeTracking(); + + // get template + if (dto.DocumentVersionDto.TemplateId.HasValue) + { + content.TemplateId = dto.DocumentVersionDto.TemplateId; + } + + // get properties - indexed by version id + var versionId = dto.DocumentVersionDto.Id; + + // TODO: shall we get published properties or not? + //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; + var publishedVersionId = dto.PublishedVersionDto?.Id ?? 0; + + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); + var ltemp = new List> {temp}; + IDictionary properties = GetPropertyCollections(ltemp); + content.Properties = properties[dto.DocumentVersionDto.Id]; + + // set variations, if varying + if (contentType?.VariesByCulture() ?? false) + { + IDictionary> contentVariations = GetContentVariations(ltemp); + IDictionary> documentVariations = GetDocumentVariations(ltemp); + SetVariations(content, contentVariations, documentVariations); + } + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally + { + content.EnableChangeTracking(); + } + } + + private void SetVariations(Content? content, IDictionary> contentVariations, + IDictionary> documentVariations) + { + if (content is null) + { + return; } - #endregion - - #region Persist - - protected override void PersistNewItem(IContent entity) + if (contentVariations.TryGetValue(content.VersionId, out List? contentVariation)) { - entity.AddingEntity(); + foreach (ContentVariation v in contentVariation) + { + content.SetCultureInfo(v.Culture, v.Name, v.Date); + } + } - var publishing = entity.PublishedState == PublishedState.Publishing; + if (content.PublishedVersionId > 0 && + contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) + { + foreach (ContentVariation v in contentVariation) + { + content.SetPublishInfo(v.Culture, v.Name, v.Date); + } + } - // ensure that the default template is assigned - if (entity.TemplateId.HasValue == false) - entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; + if (documentVariations.TryGetValue(content.Id, out List? documentVariation)) + { + content.SetCultureEdited(documentVariation.Where(x => x.Edited).Select(x => x.Culture)); + } + } + + private IDictionary> GetContentVariations(List> temps) + where T : class, IContentBase + { + var versions = new List(); + foreach (TempContent temp in temps) + { + versions.Add(temp.VersionId); + if (temp.PublishedVersionId > 0) + { + versions.Add(temp.PublishedVersionId); + } + } + + if (versions.Count == 0) + { + return new Dictionary>(); + } + + IEnumerable dtos = + Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, + batch + => Sql() + .Select() + .From() + .WhereIn(x => x.VersionId, batch)); + + var variations = new Dictionary>(); + + foreach (ContentVersionCultureVariationDto dto in dtos) + { + if (!variations.TryGetValue(dto.VersionId, out List? variation)) + { + variations[dto.VersionId] = variation = new List(); + } + + variation.Add(new ContentVariation + { + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Name = dto.Name, Date = dto.UpdateDate + }); + } + + return variations; + } + + private IDictionary> GetDocumentVariations(List> temps) + where T : class, IContentBase + { + IEnumerable ids = temps.Select(x => x.Id); + + IEnumerable dtos = Database.FetchByGroups(ids, + Constants.Sql.MaxParameterCount, batch => + Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch)); + + var variations = new Dictionary>(); + + foreach (DocumentCultureVariationDto dto in dtos) + { + if (!variations.TryGetValue(dto.NodeId, out List? variation)) + { + variations[dto.NodeId] = variation = new List(); + } + + variation.Add(new DocumentVariation + { + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Edited = dto.Edited + }); + } + + return variations; + } + + private IEnumerable GetContentVariationDtos(IContent content, bool publishing) + { + if (content.CultureInfos is not null) + { + // create dtos for the 'current' (non-published) version, all cultures + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in content.CultureInfos) + { + yield return new ContentVersionCultureVariationDto + { + VersionId = content.VersionId, + LanguageId = + LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = + content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value + }; + } + } + + // if not publishing, we're just updating the 'current' (non-published) version, + // so there are no DTOs to create for the 'published' version which remains unchanged + if (!publishing) + { + yield break; + } + + if (content.PublishCultureInfos is not null) + { + // create dtos for the 'published' version, for published cultures (those having a name) + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in content.PublishCultureInfos) + { + yield return new ContentVersionCultureVariationDto + { + VersionId = content.PublishedVersionId, + LanguageId = + LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = + content.GetPublishDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value + }; + } + } + } + + private IEnumerable GetDocumentVariationDtos(IContent content, + HashSet editedCultures) + { + IEnumerable + allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct + foreach (var culture in allCultures) + { + var dto = new DocumentCultureVariationDto + { + NodeId = content.Id, + LanguageId = + LanguageRepository.GetIdByIsoCode(culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = culture, + Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), + Available = content.IsCultureAvailable(culture), + Published = content.IsCulturePublished(culture), + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem + Edited = content.IsCultureAvailable(culture) && + (!content.IsCulturePublished(culture) || + (editedCultures != null && editedCultures.Contains(culture))) + }; + + yield return dto; + } + } + + private class ContentVariation + { + public string? Culture { get; set; } + public string? Name { get; set; } + public DateTime Date { get; set; } + } + + private class DocumentVariation + { + public string? Culture { get; set; } + public bool Edited { get; set; } + } + + #region Repository Base + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Document; + + protected override IContent? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + DocumentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + private void AddGetByQueryOrderBy(Sql sql) => + sql + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); + + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); + + // gets the COALESCE expression for variant/invariant name + private string VariantNameSqlExpression + => SqlContext.VisitDto((ccv, node) => ccv.Name ?? node.Text, "ccv") + .Sql; + + protected Sql GetBaseQuery(QueryType queryType, bool current) + { + Sql sql = SqlContext.Sql(); + + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + // R# may flag this ambiguous and red-squiggle it, but it is not + sql = sql.Select(r => + r.Select(documentDto => documentDto.ContentDto, r1 => + r1.Select(contentDto => contentDto.NodeDto)) + .Select(documentDto => documentDto.DocumentVersionDto, r1 => + r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) + .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => + r1.Select(documentVersionDto => documentVersionDto!.ContentVersionDto, "pcv"))) + + // select the variant name, coalesce to the invariant name, as "variantName" + .AndSelect(VariantNameSqlExpression + " AS variantName"); + break; + } + + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + + // inner join on mandatory edited version + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.Id == right.Id) + + // left join on optional published version + .LeftJoin(nested => + nested.InnerJoin("pdv") + .On((left, right) => left.Id == right.Id && right.Published, + "pcv", "pdv"), "pcv") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") + + // TODO: should we be joining this when the query type is not single/many? + // left join on optional culture variation + //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code + .LeftJoin(nested => + nested.InnerJoin("lang").On( + (ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") + .On((version, ccv) => version.Id == ccv.VersionId, + aliasRight: "ccv"); + + sql + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + // this would ensure we don't get the published version - keep for reference + //sql + // .WhereAny( + // x => x.Where((x1, x2) => x1.Id != x2.Id, alias2: "pcv"), + // x => x.WhereNull(x1 => x1.Id, "pcv") + // ); + + if (current) + { + sql.Where(x => x.Current); // always get the current version + } + + return sql; + } + + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + + // ah maybe not, that what's used for eg Exists in base repo + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.RedirectUrl + + " WHERE contentKey IN (SELECT uniqueId FROM " + Constants.DatabaseSchema.Tables.Node + + " WHERE id = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + + " SET startContentId = NULL WHERE startContentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.Access + + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + }; + return list; + } + + #endregion + + #region Versions + + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + return MapDtosToContent(Database.Fetch(sql), true); + } + + // TODO: This method needs to return a readonly version of IContent! The content returned + // from this method does not contain all of the data required to re-persist it and if that + // is attempted some odd things will occur. + // Either we create an IContentReadOnly (which ultimately we should for vNext so we can + // differentiate between methods that return entities that can be re-persisted or not), or + // in the meantime to not break API compatibility, we can add a property to IContentBase + // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw + // an exception if that entity is passed to a Save method. + // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService. + // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly + // which can return IContentReadOnly. + // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a + // content item. Ideally for paged data that populates list views, these would be ultra slim + // content items, there's no reason to populate those with really anything apart from property data, + // but until we do something like the above, we can't do that since it would be breaking and unclear. + public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + var pageIndex = skip / take; + + return MapDtosToContent(Database.Page(pageIndex + 1, take, sql).Items, true, + // load bare minimum, need variants though since this is used to rollback with variants + false, false); + } + + public override IContent? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single, false) + .Where(x => x.Id == versionId); + + DocumentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } + + // deletes a specific version + public override void DeleteVersion(int versionId) + { + // TODO: test object node type? + + // get the version we want to delete + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql => + tsql.Select() + .AndSelect() + .From() + .InnerJoin() + .On((c, d) => c.Id == d.Id) + .Where(x => x.Id == SqlTemplate.Arg("versionId")) + ); + DocumentVersionDto? versionDto = + Database.Fetch(template.Sql(new {versionId})).FirstOrDefault(); + + // nothing to delete + if (versionDto == null) + { + return; + } + + // don't delete the current or published version + if (versionDto.ContentVersionDto.Current) + { + throw new InvalidOperationException("Cannot delete the current version."); + } + + if (versionDto.Published) + { + throw new InvalidOperationException("Cannot delete the published version."); + } + + PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId); + } + + // deletes all versions of an entity, older than a date. + public override void DeleteVersions(int nodeId, DateTime versionDate) + { + // TODO: test object node type? + + // get the versions we want to delete, excluding the current one + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql => + tsql.Select() + .From() + .InnerJoin() + .On((c, d) => c.Id == d.Id) + .Where(x => + x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && + x.VersionDate < SqlTemplate.Arg("versionDate")) + .Where(x => !x.Published) + ); + List? versionDtos = + Database.Fetch(template.Sql(new {nodeId, versionDate})); + foreach (ContentVersionDto? versionDto in versionDtos) + { + PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + } + } + + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @versionId", new {versionId}); + Database.Delete("WHERE versionId = @versionId", new {versionId}); + Database.Delete("WHERE id = @versionId", new {versionId}); + Database.Delete("WHERE id = @versionId", new {versionId}); + } + + #endregion + + #region Persist + + protected override void PersistNewItem(IContent entity) + { + entity.AddingEntity(); + + var publishing = entity.PublishedState == PublishedState.Publishing; + + // ensure that the default template is assigned + if (entity.TemplateId.HasValue == false) + { + entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; + } + + // sanitize names + SanitizeNames(entity, publishing); + + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // create the dto + DocumentDto dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; + + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + + // persist the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; + + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) + { + nodeDto.NodeId = id; + } + else + { + Database.Insert(nodeDto); + } + + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // persist the content dto + ContentDto contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + // persist the content version dto + ContentVersionDto contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = !publishing; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the document version dto + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; + documentVersionDto.Id = entity.VersionId; + if (publishing) + { + documentVersionDto.Published = true; + } + + Database.Insert(documentVersionDto); + + // and again in case we're publishing immediately + if (publishing) + { + entity.PublishedVersionId = entity.VersionId; + contentVersionDto.Id = 0; + contentVersionDto.Current = true; + contentVersionDto.Text = entity.Name; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + documentVersionDto.Id = entity.VersionId; + documentVersionDto.Published = false; + Database.Insert(documentVersionDto); + } + + // persist the property data + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, + entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, + out HashSet? editedCultures); + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) + { + Database.Insert(propertyDataDto); + } + + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && entity.PublishName != entity.Name) + { + edited = true; + } + + // persist the document dto + // at that point, when publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + { + dto.Published = true; + } + + dto.NodeId = nodeDto.NodeId; + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Insert(dto); + + // persist the variations + if (entity.ContentType.VariesByCulture()) + { + // names also impact 'edited' + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in entity.CultureInfos!) + { + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) + { + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); + } + } + + // refresh content + entity.SetCultureEdited(editedCultures!); + + // bump dates to align cultures to version + entity.AdjustDates(contentVersionDto.VersionDate, publishing); + + // insert content variations + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + + // insert document variations + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); + } + + // trigger here, before we reset Published etc + OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + + // flip the entity's published property + // this also flips its published state + // note: what depends on variations (eg PublishNames) is managed directly by the content + if (entity.PublishedState == PublishedState.Publishing) + { + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; + + SetEntityTags(entity, _tagRepository, _serializer); + } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; + + ClearEntityTags(entity, _tagRepository); + } + + PersistRelations(entity); + + entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + } + + protected override void PersistUpdatedItem(IContent entity) + { + var isEntityDirty = entity.IsDirty(); + var editedSnapshot = entity.Edited; + + // check if we need to make any database changes at all + if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) + && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) + { + return; // no change to save, do nothing, don't even update dates + } + + // whatever we do, we must check that we are saving the current version + ContentVersionDto? version = Database.Fetch(SqlContext.Sql().Select() + .From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); + if (version == null || !version.Current) + { + throw new InvalidOperationException("Cannot save a non-current version."); + } + + // update + entity.UpdatingEntity(); + + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. + // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost + // copy performance by 95% just like we did for Move + + + var publishing = entity.PublishedState == PublishedState.Publishing; + + if (!isMoving) + { + // check if we need to create a new version + if (publishing && entity.PublishedVersionId > 0) + { + // published version is not published anymore + Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)) + .Where(x => x.Id == entity.PublishedVersionId)); + } // sanitize names SanitizeNames(entity, publishing); @@ -387,80 +1122,67 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // TODO: do we really want to keep doing this here? entity.SanitizeEntityPropertiesForXmlStorage(); - // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } + } - // derive path and level from parent - var parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; + // create the dto + DocumentDto dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // update the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); - // persist the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - nodeDto.NodeId = id; - else - Database.Insert(nodeDto); - - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = !publishing; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - - // persist the document version dto - var documentVersionDto = dto.DocumentVersionDto; - documentVersionDto.Id = entity.VersionId; + // update the content & document version dtos + ContentVersionDto contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; if (publishing) - documentVersionDto.Published = true; - Database.Insert(documentVersionDto); + { + documentVersionDto.Published = true; // now published + contentVersionDto.Current = false; // no more current + } - // and again in case we're publishing immediately + // Ensure existing version retains current preventCleanup flag (both saving and publishing). + contentVersionDto.PreventCleanup = version.PreventCleanup; + + Database.Update(contentVersionDto); + Database.Update(documentVersionDto); + + // and, if publishing, insert new content & document version dtos if (publishing) { entity.PublishedVersionId = entity.VersionId; - contentVersionDto.Id = 0; - contentVersionDto.Current = true; - contentVersionDto.Text = entity.Name; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - documentVersionDto.Id = entity.VersionId; - documentVersionDto.Published = false; + contentVersionDto.Id = 0; // want a new id + contentVersionDto.Current = true; // current version + contentVersionDto.Text = entity.Name; + contentVersionDto.PreventCleanup = false; // new draft version disregards prevent cleanup flag + Database.Insert(contentVersionDto); + entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id + + documentVersionDto.Published = false; // non-published version Database.Insert(documentVersionDto); } - // persist the property data - IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out HashSet? editedCultures); - foreach (PropertyDataDto propertyDataDto in propertyDataDtos) - { - Database.Insert(propertyDataDto); - } + // replace the property data (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + + // insert property data + ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, + out HashSet? editedCultures); // if !publishing, we may have a new name != current publish name, // also impacts 'edited' @@ -469,19 +1191,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement edited = true; } - // persist the document dto - // at that point, when publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) + // To establish the new value of "edited" we compare all properties publishedValue to editedValue and look + // for differences. + // + // If we SaveAndPublish but the publish fails (e.g. already scheduled for release) + // we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison. + // + // This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save + // would change edited back to false. + if (!publishing && editedSnapshot) { - dto.Published = true; + edited = true; } - dto.NodeId = nodeDto.NodeId; - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Insert(dto); - - // persist the variations if (entity.ContentType.VariesByCulture()) { // names also impact 'edited' @@ -490,7 +1212,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) { - (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); + edited = true; + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo + .Culture); + + // TODO: change tracking + // at the moment, we don't do any dirty tracking on property values, so we don't know whether the + // culture has just been edited or not, so we don't update its update date - that date only changes + // when the name is set, and it all works because the controller does it - but, if someone uses a + // service to change a property value and save (without setting name), the update date does not change. } } @@ -500,6 +1230,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // bump dates to align cultures to version entity.AdjustDates(contentVersionDto.VersionDate, publishing); + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + Sql deleteContentVariations = Sql().Delete() + .Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); + + // replace the document version variations (rather than updating) + Sql deleteDocumentVariations = Sql().Delete() + .Where(x => x.NodeId == entity.Id); + Database.Execute(deleteDocumentVariations); + + // TODO: NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // (same in PersistNewItem above) + // insert content variations Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); @@ -507,12 +1254,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); } - // trigger here, before we reset Published etc - OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + // update the document dto + // at that point, when un/publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + { + dto.Published = true; + } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + dto.Published = false; + } + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Update(dto); + + // if entity is publishing, update tags, else leave tags there + // means that implicitly unpublished, or trashed, entities *still* have tags in db + if (entity.PublishedState == PublishedState.Publishing) + { + SetEntityTags(entity, _tagRepository, _serializer); + } + } + + // trigger here, before we reset Published etc + OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + + if (!isMoving) + { // flip the entity's published property // this also flips its published state - // note: what depends on variations (eg PublishNames) is managed directly by the content if (entity.PublishedState == PublishedState.Publishing) { entity.Published = true; @@ -536,1091 +1307,427 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement PersistRelations(entity); - entity.ResetDirtyProperties(); - - // troubleshooting - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} + // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? } - protected override void PersistUpdatedItem(IContent entity) + entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + } + + /// + public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) + { + if (content == null) { - var isEntityDirty = entity.IsDirty(); - var editedSnapshot = entity.Edited; - - // check if we need to make any database changes at all - if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) - && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) - { - return; // no change to save, do nothing, don't even update dates - } - - // whatever we do, we must check that we are saving the current version - var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); - if (version == null || !version.Current) - throw new InvalidOperationException("Cannot save a non-current version."); - - // update - entity.UpdatingEntity(); - - // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. - // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.IsMoving(); - // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. - // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost - // copy performance by 95% just like we did for Move - - - var publishing = entity.PublishedState == PublishedState.Publishing; - - if (!isMoving) - { - // check if we need to create a new version - if (publishing && entity.PublishedVersionId > 0) - { - // published version is not published anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); - } - - // sanitize names - SanitizeNames(entity, publishing); - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - var parent = GetParentNodeDto(entity.ParentId); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } - } - - // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); - - // update the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - if (!isMoving) - { - // update the content dto - Database.Update(dto.ContentDto); - - // update the content & document version dtos - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - var documentVersionDto = dto.DocumentVersionDto; - if (publishing) - { - documentVersionDto.Published = true; // now published - contentVersionDto.Current = false; // no more current - } - - // Ensure existing version retains current preventCleanup flag (both saving and publishing). - contentVersionDto.PreventCleanup = version.PreventCleanup; - - Database.Update(contentVersionDto); - Database.Update(documentVersionDto); - - // and, if publishing, insert new content & document version dtos - if (publishing) - { - entity.PublishedVersionId = entity.VersionId; - - contentVersionDto.Id = 0; // want a new id - contentVersionDto.Current = true; // current version - contentVersionDto.Text = entity.Name; - contentVersionDto.PreventCleanup = false; // new draft version disregards prevent cleanup flag - Database.Insert(contentVersionDto); - entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - - documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); - } - - // replace the property data (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; - - // insert property data - ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, out HashSet? editedCultures); - - // if !publishing, we may have a new name != current publish name, - // also impacts 'edited' - if (!publishing && entity.PublishName != entity.Name) - { - edited = true; - } - - // To establish the new value of "edited" we compare all properties publishedValue to editedValue and look - // for differences. - // - // If we SaveAndPublish but the publish fails (e.g. already scheduled for release) - // we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison. - // - // This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save - // would change edited back to false. - if (!publishing && editedSnapshot) - { - edited = true; - } - - if (entity.ContentType.VariesByCulture()) - { - // names also impact 'edited' - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in entity.CultureInfos!) - { - if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) - { - edited = true; - (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); - - // TODO: change tracking - // at the moment, we don't do any dirty tracking on property values, so we don't know whether the - // culture has just been edited or not, so we don't update its update date - that date only changes - // when the name is set, and it all works because the controller does it - but, if someone uses a - // service to change a property value and save (without setting name), the update date does not change. - } - } - - // refresh content - entity.SetCultureEdited(editedCultures!); - - // bump dates to align cultures to version - entity.AdjustDates(contentVersionDto.VersionDate, publishing); - - // replace the content version variations (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deleteContentVariations); - - // replace the document version variations (rather than updating) - var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); - Database.Execute(deleteDocumentVariations); - - // TODO: NPoco InsertBulk issue? - // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) - // but by using SQL Server and updating a variants name will cause: Unable to cast object of type - // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. - // (same in PersistNewItem above) - - // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); - - // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); - } - - // update the document dto - // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) - { - dto.Published = true; - } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - dto.Published = false; - } - - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Update(dto); - - // if entity is publishing, update tags, else leave tags there - // means that implicitly unpublished, or trashed, entities *still* have tags in db - if (entity.PublishedState == PublishedState.Publishing) - { - SetEntityTags(entity, _tagRepository, _serializer); - } - } - - // trigger here, before we reset Published etc - OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); - - if (!isMoving) - { - // flip the entity's published property - // this also flips its published state - if (entity.PublishedState == PublishedState.Publishing) - { - entity.Published = true; - entity.PublishTemplateId = entity.TemplateId; - entity.PublisherId = entity.WriterId; - entity.PublishName = entity.Name; - entity.PublishDate = entity.UpdateDate; - - SetEntityTags(entity, _tagRepository, _serializer); - } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - entity.Published = false; - entity.PublishTemplateId = null; - entity.PublisherId = null; - entity.PublishName = null; - entity.PublishDate = null; - - ClearEntityTags(entity, _tagRepository); - } - - PersistRelations(entity); - - // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? - } - - entity.ResetDirtyProperties(); - - // troubleshooting - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} + throw new ArgumentNullException(nameof(content)); } - /// - public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) + if (contentSchedule == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (contentSchedule == null) - { - throw new ArgumentNullException(nameof(contentSchedule)); - } - - var schedules = ContentBaseFactory.BuildScheduleDto(content, contentSchedule, LanguageRepository).ToList(); - - //remove any that no longer exist - var ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); - Database.Execute(Sql() - .Delete() - .Where(x => x.NodeId == content.Id) - .WhereNotIn(x => x.Id, ids)); - - //add/update the rest - foreach (var schedule in schedules) - { - if (schedule.Model.Id == Guid.Empty) - { - schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); - Database.Insert(schedule.Dto); - } - else - { - Database.Update(schedule.Dto); - } - } + throw new ArgumentNullException(nameof(contentSchedule)); } - protected override void PersistDeletedItem(IContent entity) + var schedules = ContentBaseFactory.BuildScheduleDto(content, contentSchedule, LanguageRepository).ToList(); + + //remove any that no longer exist + IEnumerable ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); + Database.Execute(Sql() + .Delete() + .Where(x => x.NodeId == content.Id) + .WhereNotIn(x => x.Id, ids)); + + //add/update the rest + foreach ((ContentSchedule Model, ContentScheduleDto Dto) schedule in schedules) { - // Raise event first else potential FK issues - OnUowRemovingEntity(entity); - - //We need to clear out all access rules but we need to do this in a manual way since - // nothing in that table is joined to a content id - var subQuery = SqlContext.Sql() - .Select(x => x.AccessId) - .From() - .InnerJoin() - .On(left => left.AccessId, right => right.Id) - .Where(dto => dto.NodeId == entity.Id); - Database.Execute(SqlContext.SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); - - //now let the normal delete clauses take care of everything else - base.PersistDeletedItem(entity); - } - - #endregion - - #region Content Repository - - public int CountPublished(string? contentTypeAlias = null) - { - var sql = SqlContext.Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) + if (schedule.Model.Id == Guid.Empty) { - sql.SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) - .Where(x => x.Published); + schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); + Database.Insert(schedule.Dto); } else { - sql.SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) - .Where(x => x.Alias == contentTypeAlias) - .Where(x => x.Published); + Database.Update(schedule.Dto); } - - return Database.ExecuteScalar(sql); } + } - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + protected override void PersistDeletedItem(IContent entity) + { + // Raise event first else potential FK issues + OnUowRemovingEntity(entity); + + //We need to clear out all access rules but we need to do this in a manual way since + // nothing in that table is joined to a content id + Sql subQuery = SqlContext.Sql() + .Select(x => x.AccessId) + .From() + .InnerJoin() + .On(left => left.AccessId, right => right.Id) + .Where(dto => dto.NodeId == entity.Id); + Database.Execute(SqlContext.SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); + + //now let the normal delete clauses take care of everything else + base.PersistDeletedItem(entity); + } + + #endregion + + #region Content Repository + + public int CountPublished(string? contentTypeAlias = null) + { + Sql sql = SqlContext.Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) { - PermissionRepository.ReplaceEntityPermissions(permissionSet); - } - - /// - /// Assigns a single permission to the current content item for the specified group ids - /// - /// - /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) - { - PermissionRepository.AssignEntityPermission(entity, permission, groupIds); - } - - public EntityPermissionCollection GetPermissionsForEntity(int entityId) - { - return PermissionRepository.GetPermissionsForEntity(entityId); - } - - /// - /// Used to add/update a permission for a content item - /// - /// - public void AddOrUpdatePermissions(ContentPermissionSet permission) - { - PermissionRepository.Save(permission); - } - - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - Sql? filterSql = null; - - // if we have a filter, map its clauses to an Sql statement - if (filter != null) - { - // if the clause works on "name", we need to swap the field and use the variantName instead, - // so that querying also works on variant content (for instance when searching a listview). - - // figure out how the "name" field is going to look like - so we can look for it - var nameField = SqlContext.VisitModelField(x => x.Name); - - filterSql = Sql(); - foreach (var filterClause in filter.GetWhereClauses()) - { - var clauseSql = filterClause.Item1; - var clauseArgs = filterClause.Item2; - - // replace the name field - // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here - clauseSql = clauseSql.Replace(nameField, VariantNameSqlExpression); - - // append the clause - filterSql.Append($"AND ({clauseSql})", clauseArgs); - } - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - public bool IsPathPublished(IContent? content) - { - // fail fast - if (content?.Path.StartsWith("-1,-20,") ?? false) - return false; - - // succeed fast - if (content?.ParentId == -1) - return content.Published; - - var ids = content?.Path.Split(Constants.CharArrays.Comma).Skip(1).Select(s => int.Parse(s, CultureInfo.InvariantCulture)); - - var sql = SqlContext.Sql() - .SelectCount(x => x.NodeId) + sql.SelectCount() .From() - .InnerJoin().On((n, d) => n.NodeId == d.NodeId && d.Published) - .WhereIn(x => x.NodeId, ids); - - var count = Database.ExecuteScalar(sql); - return count == content?.Level; + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Published); + } + else + { + sql.SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Alias == contentTypeAlias) + .Where(x => x.Published); } - #endregion + return Database.ExecuteScalar(sql); + } - #region Recycle Bin + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) => + PermissionRepository.ReplaceEntityPermissions(permissionSet); - public override int RecycleBinId => Cms.Core.Constants.System.RecycleBinContent; + /// + /// Assigns a single permission to the current content item for the specified group ids + /// + /// + /// + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) => + PermissionRepository.AssignEntityPermission(entity, permission, groupIds); - public bool RecycleBinSmells() + public EntityPermissionCollection GetPermissionsForEntity(int entityId) => + PermissionRepository.GetPermissionsForEntity(entityId); + + /// + /// Used to add/update a permission for a content item + /// + /// + public void AddOrUpdatePermissions(ContentPermissionSet permission) => PermissionRepository.Save(permission); + + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) + { + Sql? filterSql = null; + + // if we have a filter, map its clauses to an Sql statement + if (filter != null) { - var cache = _appCaches.RuntimeCache; - var cacheKey = CacheKeys.ContentRecycleBinCacheKey; + // if the clause works on "name", we need to swap the field and use the variantName instead, + // so that querying also works on variant content (for instance when searching a listview). - // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); - } + // figure out how the "name" field is going to look like - so we can look for it + var nameField = SqlContext.VisitModelField(x => x.Name); - #endregion - - #region Read Repository implementation for Guid keys - - public IContent? Get(Guid id) - { - return _contentByGuidReadRepository.Get(id); - } - - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return _contentByGuidReadRepository.GetMany(ids); - } - - public bool Exists(Guid id) - { - return _contentByGuidReadRepository.Exists(id); - } - - // reading repository purely for looking up by GUID - // TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this - private class ContentByGuidReadRepository : EntityRepositoryBase - { - private readonly DocumentRepository _outerRepo; - - public ContentByGuidReadRepository(DocumentRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) + filterSql = Sql(); + foreach (Tuple filterClause in filter.GetWhereClauses()) { - _outerRepo = outerRepo; - } + var clauseSql = filterClause.Item1; + var clauseArgs = filterClause.Item2; - protected override IContent? PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Single) - .Where(x => x.UniqueId == id); + // replace the name field + // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here + clauseSql = clauseSql.Replace(nameField, VariantNameSqlExpression); - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = _outerRepo.MapDtoToContent(dto); - - return content; - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Many); - if (ids?.Length > 0) - sql.WhereIn(x => x.UniqueId, ids); - - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistNewItem(IContent entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistUpdatedItem(IContent entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); + // append the clause + filterSql.Append($"AND ({clauseSql})", clauseArgs); } } - #endregion + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } - #region Schedule - - /// - public void ClearSchedule(DateTime date) + public bool IsPathPublished(IContent? content) + { + // fail fast + if (content?.Path.StartsWith("-1,-20,") ?? false) { - var sql = Sql().Delete().Where(x => x.Date <= date); - Database.Execute(sql); + return false; } - /// - public void ClearSchedule(DateTime date, ContentScheduleAction action) + // succeed fast + if (content?.ParentId == -1) { - var a = action.ToString(); - var sql = Sql().Delete().Where(x => x.Date <= date && x.Action == a); - Database.Execute(sql); + return content.Published; } - private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + IEnumerable? ids = content?.Path.Split(Constants.CharArrays.Comma).Skip(1) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); + + Sql sql = SqlContext.Sql() + .SelectCount(x => x.NodeId) + .From() + .InnerJoin().On((n, d) => n.NodeId == d.NodeId && d.Published) + .WhereIn(x => x.NodeId, ids); + + var count = Database.ExecuteScalar(sql); + return count == content?.Level; + } + + #endregion + + #region Recycle Bin + + public override int RecycleBinId => Constants.System.RecycleBinContent; + + public bool RecycleBinSmells() + { + IAppPolicyCache cache = _appCaches.RuntimeCache; + var cacheKey = CacheKeys.ContentRecycleBinCacheKey; + + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } + + #endregion + + #region Read Repository implementation for Guid keys + + public IContent? Get(Guid id) => _contentByGuidReadRepository.Get(id); + + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + _contentByGuidReadRepository.GetMany(ids); + + public bool Exists(Guid id) => _contentByGuidReadRepository.Exists(id); + + // reading repository purely for looking up by GUID + // TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this + private class ContentByGuidReadRepository : EntityRepositoryBase + { + private readonly DocumentRepository _outerRepo; + + public ContentByGuidReadRepository(DocumentRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) => + _outerRepo = outerRepo; + + protected override IContent? PerformGet(Guid id) { - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql - .SelectCount() - .From() - .Where(x => x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); + Sql sql = _outerRepo.GetBaseQuery(QueryType.Single) + .Where(x => x.UniqueId == id); - var sql = template.Sql(action.ToString(), date); - return sql; - } + DocumentDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - public bool HasContentForExpiration(DateTime date) - { - var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); - return Database.ExecuteScalar(sql) > 0; - } - - public bool HasContentForRelease(DateTime date) - { - var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); - return Database.ExecuteScalar(sql) > 0; - } - - /// - public IEnumerable GetContentForRelease(DateTime date) - { - var action = ContentScheduleAction.Release.ToString(); - - var sql = GetBaseQuery(QueryType.Many) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date <= date)); - - AddGetByQueryOrderBy(sql); - - return MapDtosToContent(Database.Fetch(sql)); - } - - /// - public IEnumerable GetContentForExpiration(DateTime date) - { - var action = ContentScheduleAction.Expire.ToString(); - - var sql = GetBaseQuery(QueryType.Many) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date <= date)); - - AddGetByQueryOrderBy(sql); - - return MapDtosToContent(Database.Fetch(sql)); - } - - #endregion - - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) - { - // note: 'updater' is the user who created the latest draft version, - // we don't have an 'updater' per culture (should we?) - if (ordering.OrderBy.InvariantEquals("updater")) + if (dto == null) { - var joins = Sql() - .InnerJoin("updaterUser").On((version, user) => version.UserId == user.Id, aliasRight: "updaterUser"); - - // see notes in ApplyOrdering: the field MUST be selected + aliased - sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering "), sql.Arguments); - - sql = InsertJoins(sql, joins); - - return "ordering"; + return null; } - if (ordering.OrderBy.InvariantEquals("published")) - { - // no culture = can only work on the global 'published' flag - if (ordering.Culture.IsNullOrWhiteSpace()) - { - // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have - // the whole CASE fragment in ORDER BY due to it not being detected by NPoco - sql = Sql(InsertBefore(sql, "FROM", ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering "), sql.Arguments); - return "ordering"; - } - - // invariant: left join will yield NULL and we must use pcv to determine published - // variant: left join may yield NULL or something, and that determines published - - - var joins = Sql() - .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype") - // left join on optional culture variation - //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code - .LeftJoin(nested => - nested.InnerJoin("langp").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccvp", "langp"), "ccvp") - .On((version, ccv) => version.Id == ccv.VersionId, aliasLeft: "pcv", aliasRight: "ccvp"); - - sql = InsertJoins(sql, joins); - - // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have - // the whole CASE fragment in ORDER BY due to it not being detected by NPoco - var sqlText = InsertBefore(sql.SQL, "FROM", - - // when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id, - // otherwise check if there's a version culture variation for the lang, via ccv.id - ", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccvp.id IS NULL THEN 0 ELSE 1 END) END) AS ordering "); // trailing space is important! - - sql = Sql(sqlText, sql.Arguments); - - return "ordering"; - } - - return base.ApplySystemOrdering(ref sql, ordering); - } - - private IEnumerable MapDtosToContent(List dtos, - bool withCache = false, - bool loadProperties = true, - bool loadTemplates = true, - bool loadVariants = true) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var templateIds = new List(); - - var content = new Content[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) - { - content[i] = (Content)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out var contentType) == false) - contentTypes[contentTypeId] = contentType = _contentTypeRepository.Get(contentTypeId); - - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - if (loadTemplates) - { - // need templates - var templateId = dto.DocumentVersionDto.TemplateId; - if (templateId.HasValue) - templateIds.Add(templateId.Value); - if (dto.Published) - { - templateId = dto.PublishedVersionDto.TemplateId; - if (templateId.HasValue) - templateIds.Add(templateId.Value); - } - } - - // need temps, for properties, templates and variations - var versionId = dto.DocumentVersionDto.Id; - var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType, c) - { - Template1Id = dto.DocumentVersionDto.TemplateId - }; - if (dto.Published) - temp.Template2Id = dto.PublishedVersionDto.TemplateId; - temps.Add(temp); - } - - Dictionary? templates = null; - if (loadTemplates) - { - // load all required templates in 1 query, and index - templates = _templateRepository.GetMany(templateIds.ToArray())? - .ToDictionary(x => x.Id, x => x); - } - - IDictionary? properties = null; - if (loadProperties) - { - // load all properties for all documents from database in 1 query - indexed by version id - properties = GetPropertyCollections(temps); - } - - // assign templates and properties - foreach (var temp in temps) - { - if (loadTemplates) - { - // set the template ID if it matches an existing template - if (temp.Template1Id.HasValue && (templates?.ContainsKey(temp.Template1Id.Value) ?? false)) - temp.Content!.TemplateId = temp.Template1Id; - if (temp.Template2Id.HasValue && (templates?.ContainsKey(temp.Template2Id.Value) ?? false)) - temp.Content!.PublishTemplateId = temp.Template2Id; - } - - - // set properties - if (loadProperties) - { - if (properties?.ContainsKey(temp.VersionId) ?? false) - temp.Content!.Properties = properties[temp.VersionId]; - else - throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); - } - } - - if (loadVariants) - { - // set variations, if varying - temps = temps.Where(x => x.ContentType?.VariesByCulture() ?? false).ToList(); - if (temps.Count > 0) - { - // load all variations for all documents from database, in one query - var contentVariations = GetContentVariations(temps); - var documentVariations = GetDocumentVariations(temps); - foreach (var temp in temps) - SetVariations(temp.Content, contentVariations, documentVariations); - } - } - - - - foreach (var c in content) - c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) + IContent content = _outerRepo.MapDtoToContent(dto); return content; } - private IContent MapDtoToContent(DocumentDto dto) + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - var contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); - var content = ContentBaseFactory.BuildEntity(dto, contentType); - - try + Sql sql = _outerRepo.GetBaseQuery(QueryType.Many); + if (ids?.Length > 0) { - content.DisableChangeTracking(); - - // get template - if (dto.DocumentVersionDto.TemplateId.HasValue) - content.TemplateId = dto.DocumentVersionDto.TemplateId; - - // get properties - indexed by version id - var versionId = dto.DocumentVersionDto.Id; - - // TODO: shall we get published properties or not? - //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var publishedVersionId = dto.PublishedVersionDto?.Id ?? 0; - - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); - var ltemp = new List> { temp }; - var properties = GetPropertyCollections(ltemp); - content.Properties = properties[dto.DocumentVersionDto.Id]; - - // set variations, if varying - if (contentType?.VariesByCulture() ?? false) - { - var contentVariations = GetContentVariations(ltemp); - var documentVariations = GetDocumentVariations(ltemp); - SetVariations(content, contentVariations, documentVariations); - } - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); + sql.WhereIn(x => x.UniqueId, ids); } + + return _outerRepo.MapDtosToContent(Database.Fetch(sql)); } - /// - public ContentScheduleCollection GetContentSchedule(int contentId) - { - var result = new ContentScheduleCollection(); + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); - var scheduleDtos = Database.Fetch(Sql() - .Select() + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistNewItem(IContent entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistUpdatedItem(IContent entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + } + + #endregion + + #region Schedule + + /// + public void ClearSchedule(DateTime date) + { + Sql sql = Sql().Delete().Where(x => x.Date <= date); + Database.Execute(sql); + } + + /// + public void ClearSchedule(DateTime date, ContentScheduleAction action) + { + var a = action.ToString(); + Sql sql = Sql().Delete() + .Where(x => x.Date <= date && x.Action == a); + Database.Execute(sql); + } + + private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + { + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", + tsql => tsql + .SelectCount() .From() - .Where(x => x.NodeId == contentId )); + .Where(x => + x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); - foreach (var scheduleDto in scheduleDtos) + Sql sql = template.Sql(action.ToString(), date); + return sql; + } + + public bool HasContentForExpiration(DateTime date) + { + Sql sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); + return Database.ExecuteScalar(sql) > 0; + } + + public bool HasContentForRelease(DateTime date) + { + Sql sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); + return Database.ExecuteScalar(sql) > 0; + } + + /// + public IEnumerable GetContentForRelease(DateTime date) + { + var action = ContentScheduleAction.Release.ToString(); + + Sql sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + /// + public IEnumerable GetContentForExpiration(DateTime date) + { + var action = ContentScheduleAction.Expire.ToString(); + + Sql sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + #endregion + + #region Utilities + + private void SanitizeNames(IContent content, bool publishing) + { + // a content item *must* have an invariant name, and invariant published name + // else we just cannot write the invariant rows (node, content version...) to the database + + // ensure that we have an invariant name + // invariant content = must be there already, else throw + // variant content = update with default culture or anything really + EnsureInvariantNameExists(content); + + // ensure that invariant name is unique + EnsureInvariantNameIsUnique(content); + + // and finally, + // ensure that each culture has a unique node name + // no published name = not published + // else, it needs to be unique + EnsureVariantNamesAreUnique(content, publishing); + } + + private void EnsureInvariantNameExists(IContent content) + { + if (content.ContentType.VariesByCulture()) + { + // content varies by culture + // then it must have at least a variant name, else it makes no sense + if (content.CultureInfos?.Count == 0) { - result.Add(new ContentSchedule(scheduleDto.Id, - LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, - scheduleDto.Date, - scheduleDto.Action == ContentScheduleAction.Release.ToString() - ? ContentScheduleAction.Release - : ContentScheduleAction.Expire)); + throw new InvalidOperationException("Cannot save content with an empty name."); } - return result; + // and then, we need to set the invariant name implicitly, + // using the default culture if it has a name, otherwise anything we can + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + content.Name = defaultCulture != null && + (content.CultureInfos?.TryGetValue(defaultCulture, out ContentCultureInfos cultureName) ?? + false) + ? cultureName.Name! + : content.CultureInfos![0].Name!; } - - private void SetVariations(Content? content, IDictionary> contentVariations, IDictionary> documentVariations) + else { - if (content is null) + // content is invariant, and invariant content must have an explicit invariant name + if (string.IsNullOrWhiteSpace(content.Name)) { - return; - } - if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) - foreach (var v in contentVariation) - content.SetCultureInfo(v.Culture, v.Name, v.Date); - - if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) - { - foreach (var v in contentVariation) - content.SetPublishInfo(v.Culture, v.Name, v.Date); - } - - if (documentVariations.TryGetValue(content.Id, out var documentVariation)) - content.SetCultureEdited(documentVariation.Where(x => x.Edited).Select(x => x.Culture)); - } - - private IDictionary> GetContentVariations(List> temps) - where T : class, IContentBase - { - var versions = new List(); - foreach (var temp in temps) - { - versions.Add(temp.VersionId); - if (temp.PublishedVersionId > 0) - versions.Add(temp.PublishedVersionId); - } - if (versions.Count == 0) - return new Dictionary>(); - - var dtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch - => Sql() - .Select() - .From() - .WhereIn(x => x.VersionId, batch)); - - var variations = new Dictionary>(); - - foreach (var dto in dtos) - { - if (!variations.TryGetValue(dto.VersionId, out var variation)) - variations[dto.VersionId] = variation = new List(); - - variation.Add(new ContentVariation - { - Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), - Name = dto.Name, - Date = dto.UpdateDate - }); - } - - return variations; - } - - private IDictionary> GetDocumentVariations(List> temps) - where T : class, IContentBase - { - var ids = temps.Select(x => x.Id); - - var dtos = Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => - Sql() - .Select() - .From() - .WhereIn(x => x.NodeId, batch)); - - var variations = new Dictionary>(); - - foreach (var dto in dtos) - { - if (!variations.TryGetValue(dto.NodeId, out var variation)) - variations[dto.NodeId] = variation = new List(); - - variation.Add(new DocumentVariation - { - Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), - Edited = dto.Edited - }); - } - - return variations; - } - - private IEnumerable GetContentVariationDtos(IContent content, bool publishing) - { - if (content.CultureInfos is not null) - { - // create dtos for the 'current' (non-published) version, all cultures - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.CultureInfos) - yield return new ContentVersionCultureVariationDto - { - VersionId = content.VersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = cultureInfo.Culture, - Name = cultureInfo.Name, - UpdateDate = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value - }; - } - - // if not publishing, we're just updating the 'current' (non-published) version, - // so there are no DTOs to create for the 'published' version which remains unchanged - if (!publishing) - yield break; - - if (content.PublishCultureInfos is not null) - { - // create dtos for the 'published' version, for published cultures (those having a name) - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.PublishCultureInfos) - yield return new ContentVersionCultureVariationDto - { - VersionId = content.PublishedVersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = cultureInfo.Culture, - Name = cultureInfo.Name, - UpdateDate = content.GetPublishDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value - }; + throw new InvalidOperationException("Cannot save content with an empty name."); } } + } - private IEnumerable GetDocumentVariationDtos(IContent content, HashSet editedCultures) - { - var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct - foreach (var culture in allCultures) - { - var dto = new DocumentCultureVariationDto - { - NodeId = content.Id, - LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = culture, + private void EnsureInvariantNameIsUnique(IContent content) => + content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); - Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), - Available = content.IsCultureAvailable(culture), - Published = content.IsCulturePublished(culture), - // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem - Edited = content.IsCultureAvailable(culture) && - (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) - }; + protected override string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) => + EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); - yield return dto; - } - - } - - private class ContentVariation - { - public string? Culture { get; set; } - public string? Name { get; set; } - public DateTime Date { get; set; } - } - - private class DocumentVariation - { - public string? Culture { get; set; } - public bool Edited { get; set; } - } - - #region Utilities - - private void SanitizeNames(IContent content, bool publishing) - { - // a content item *must* have an invariant name, and invariant published name - // else we just cannot write the invariant rows (node, content version...) to the database - - // ensure that we have an invariant name - // invariant content = must be there already, else throw - // variant content = update with default culture or anything really - EnsureInvariantNameExists(content); - - // ensure that invariant name is unique - EnsureInvariantNameIsUnique(content); - - // and finally, - // ensure that each culture has a unique node name - // no published name = not published - // else, it needs to be unique - EnsureVariantNamesAreUnique(content, publishing); - } - - private void EnsureInvariantNameExists(IContent content) - { - if (content.ContentType.VariesByCulture()) - { - // content varies by culture - // then it must have at least a variant name, else it makes no sense - if (content.CultureInfos?.Count == 0) - throw new InvalidOperationException("Cannot save content with an empty name."); - - // and then, we need to set the invariant name implicitly, - // using the default culture if it has a name, otherwise anything we can - var defaultCulture = LanguageRepository.GetDefaultIsoCode(); - content.Name = defaultCulture != null && (content.CultureInfos?.TryGetValue(defaultCulture, out var cultureName) ?? false) - ? cultureName.Name! - : content.CultureInfos![0].Name!; - } - else - { - // content is invariant, and invariant content must have an explicit invariant name - if (string.IsNullOrWhiteSpace(content.Name)) - throw new InvalidOperationException("Cannot save content with an empty name."); - } - } - - private void EnsureInvariantNameIsUnique(IContent content) - { - content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); - } - - protected override string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) - { - return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); - } - - private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql + private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get( + "Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql .Select(x => x.Id, x => x.Name, x => x.LanguageId) .From() - .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin() + .On(x => x.Id, x => x.VersionId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.Current == SqlTemplate.Arg("current")) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && @@ -1628,58 +1735,73 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureVariantNamesAreUnique(IContent content, bool publishing) + private void EnsureVariantNamesAreUnique(IContent content, bool publishing) + { + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos?.Count == 0) { - if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos?.Count == 0) - return; - - // get names per culture, at same level (ie all siblings) - var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); - var names = Database.Fetch(sql) - .GroupBy(x => x.LanguageId) - .ToDictionary(x => x.Key, x => x); - - if (names.Count == 0) - return; - - // note: the code below means we are going to unique-ify every culture names, regardless - // of whether the name has changed (ie the culture has been updated) - some saving culture - // fr-FR could cause culture en-UK name to change - not sure that is clean - - if (content.CultureInfos is null) - { - return; - } - foreach (var cultureInfo in content.CultureInfos) - { - var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); - if (!langId.HasValue) - continue; - if (!names.TryGetValue(langId.Value, out var cultureNames)) - continue; - - // get a unique name - var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); - - if (uniqueName == content.GetCultureName(cultureInfo.Culture)) - continue; - - // update the name, and the publish name if published - content.SetCultureName(uniqueName, cultureInfo.Culture); - if (publishing && (content.PublishCultureInfos?.ContainsKey(cultureInfo.Culture) ?? false)) - content.SetPublishInfo(cultureInfo.Culture, uniqueName, DateTime.Now); //TODO: This is weird, this call will have already been made in the SetCultureName - } + return; } - // ReSharper disable once ClassNeverInstantiated.Local - private class CultureNodeName + // get names per culture, at same level (ie all siblings) + Sql sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); + var names = Database.Fetch(sql) + .GroupBy(x => x.LanguageId) + .ToDictionary(x => x.Key, x => x); + + if (names.Count == 0) { - public int Id { get; set; } - public string? Name { get; set; } - public int LanguageId { get; set; } + return; } - #endregion + // note: the code below means we are going to unique-ify every culture names, regardless + // of whether the name has changed (ie the culture has been updated) - some saving culture + // fr-FR could cause culture en-UK name to change - not sure that is clean + + if (content.CultureInfos is null) + { + return; + } + + foreach (ContentCultureInfos cultureInfo in content.CultureInfos) + { + var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); + if (!langId.HasValue) + { + continue; + } + + if (!names.TryGetValue(langId.Value, out IGrouping? cultureNames)) + { + continue; + } + + // get a unique name + IEnumerable otherNames = + cultureNames.Select(x => new SimilarNodeName {Id = x.Id, Name = x.Name}); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); + + if (uniqueName == content.GetCultureName(cultureInfo.Culture)) + { + continue; + } + + // update the name, and the publish name if published + content.SetCultureName(uniqueName, cultureInfo.Culture); + if (publishing && (content.PublishCultureInfos?.ContainsKey(cultureInfo.Culture) ?? false)) + { + content.SetPublishInfo(cultureInfo.Culture, uniqueName, + DateTime.Now); //TODO: This is weird, this call will have already been made in the SetCultureName + } + } } + + // ReSharper disable once ClassNeverInstantiated.Local + private class CultureNodeName + { + public int Id { get; set; } + public string? Name { get; set; } + public int LanguageId { get; set; } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs index f37886fee2..c97199e76b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs @@ -1,14 +1,15 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository { - internal class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository + public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DocumentTypeContainer) { - public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.DocumentTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs index aff71feb63..e922ed3cdb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -1,35 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DocumentVersionRepository : IDocumentVersionRepository { - internal class DocumentVersionRepository : IDocumentVersionRepository + private readonly IScopeAccessor _scopeAccessor; + + public DocumentVersionRepository(IScopeAccessor scopeAccessor) => + _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + + /// + /// + /// Never includes current draft version.
+ /// Never includes current published version.
+ /// Never includes versions marked as "preventCleanup".
+ ///
+ public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup() { - private readonly IScopeAccessor _scopeAccessor; + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - public DocumentVersionRepository(IScopeAccessor scopeAccessor) => - _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - - /// - /// - /// Never includes current draft version.
- /// Never includes current published version.
- /// Never includes versions marked as "preventCleanup".
- ///
- public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup() - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - - query?.Select(@"umbracoDocument.nodeId as contentId, + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -38,39 +34,39 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .Where(x => !x.Current) // Never delete current draft version - .Where(x => !x.PreventCleanup) // Never delete "pinned" versions - .Where(x => !x.Published); // Never delete published version + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .Where(x => !x.Current) // Never delete current draft version + .Where(x => !x.PreventCleanup) // Never delete "pinned" versions + .Where(x => !x.Published); // Never delete published version - return _scopeAccessor.AmbientScope?.Database.Fetch(query); - } + return _scopeAccessor.AmbientScope?.Database.Fetch(query); + } - /// - public IReadOnlyCollection? GetCleanupPolicies() - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public IReadOnlyCollection? GetCleanupPolicies() + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - query?.Select() - .From(); + query?.Select() + .From(); - return _scopeAccessor.AmbientScope?.Database.Fetch(query); - } + return _scopeAccessor.AmbientScope?.Database.Fetch(query); + } - /// - public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null) - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - query?.Select(@"umbracoDocument.nodeId as contentId, + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -79,86 +75,87 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .LeftJoin() - .On(left => left.VersionId, right => right.Id) - .Where(x => x.NodeId == contentId); + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .LeftJoin() + .On(left => left.VersionId, right => right.Id) + .Where(x => x.NodeId == contentId); - // TODO: If there's not a better way to write this then we need a better way to write this. - query = languageId.HasValue - ? query?.Where(x => x.LanguageId == languageId.Value) - : query?.Where("umbracoContentVersionCultureVariation.languageId is null"); + // TODO: If there's not a better way to write this then we need a better way to write this. + query = languageId.HasValue + ? query?.Where(x => x.LanguageId == languageId.Value) + : query?.Where("umbracoContentVersionCultureVariation.languageId is null"); - query = query?.OrderByDescending(x => x.Id); + query = query?.OrderByDescending(x => x.Id); - Page? page = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); + Page? page = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); - totalRecords = page?.TotalItems ?? 0; + totalRecords = page?.TotalItems ?? 0; - return page?.Items; - } + return page?.Items; + } - /// - /// - /// Deletes in batches of - /// - public void DeleteVersions(IEnumerable versionIds) + /// + /// + /// Deletes in batches of + /// + public void DeleteVersions(IEnumerable versionIds) + { + foreach (IEnumerable group in versionIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - foreach (IEnumerable group in versionIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - var groupedVersionIds = group.ToList(); + var groupedVersionIds = group.ToList(); - /* Note: We had discussed doing this in a single SQL Command. - * If you can work out how to make that work with SQL CE, let me know! - * Can use test PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive to try things out. - */ + /* Note: We had discussed doing this in a single SQL Command. + * If you can work out how to make that work with SQL CE, let me know! + * Can use test PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive to try things out. + */ - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.VersionId, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.VersionId, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.Id, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.Id, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - } - } - - /// - public void SetPreventCleanup(int versionId, bool preventCleanup) - { Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Update(x => x.Set(y => y.PreventCleanup, preventCleanup)) - .Where(x => x.Id == versionId); + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); _scopeAccessor.AmbientScope?.Database.Execute(query); } + } - /// - public ContentVersionMeta? Get(int versionId) - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public void SetPreventCleanup(int versionId, bool preventCleanup) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Update(x => x.Set(y => y.PreventCleanup, preventCleanup)) + .Where(x => x.Id == versionId); - query?.Select(@"umbracoDocument.nodeId as contentId, + _scopeAccessor.AmbientScope?.Database.Execute(query); + } + + /// + public ContentVersionMeta? Get(int versionId) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -167,18 +164,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .Where(x => x.Id == versionId); + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .Where(x => x.Id == versionId); - return _scopeAccessor.AmbientScope?.Database.Single(query); - } + return _scopeAccessor.AmbientScope?.Database.Single(query); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs index 0cc0bc44ad..9304d27b84 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -9,199 +6,218 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +// TODO: We need to get a readonly ISO code for the domain assigned +internal class DomainRepository : EntityRepositoryBase, IDomainRepository { - // TODO: We need to get a readonly ISO code for the domain assigned - - internal class DomainRepository : EntityRepositoryBase, IDomainRepository + public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() + public IDomain? GetByName(string domainName) => + GetMany().FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); + + public bool Exists(string domainName) => GetMany().Any(x => x.DomainName.InvariantEquals(domainName)); + + public IEnumerable GetAll(bool includeWildcards) => + GetMany().Where(x => includeWildcards || x.IsWildcard == false); + + public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) => + GetMany() + .Where(x => x.RootContentId == contentId) + .Where(x => includeWildcards || x.IsWildcard == false); + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ + false); + + protected override IDomain? PerformGet(int id) => + + // use the underlying GetAll which will force cache all domains + GetMany().FirstOrDefault(x => x.Id == id); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id > 0); + if (ids?.Any() ?? false) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + sql.WhereIn(x => x.Id, ids); } - protected override IDomain? PerformGet(int id) + return Database.Fetch(sql).Select(ConvertFromDto); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method"); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - //use the underlying GetAll which will force cache all domains - return GetMany()?.FirstOrDefault(x => x.Id == id); + sql.SelectCount().From(); + } + else + { + sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode") + .From() + .LeftJoin() + .On(dto => dto.DefaultLanguage, dto => dto.Id); } - protected override IEnumerable PerformGetAll(params int[]? ids) + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoDomain WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(IDomain entity) + { + var exists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", + new { domainName = entity.DomainName }); + if (exists > 0) { - var sql = GetBaseQuery(false).Where(x => x.Id > 0); - if (ids?.Any() ?? false) + throw new DuplicateNameException( + string.Format("The domain name {0} is already assigned", entity.DomainName)); + } + + if (entity.RootContentId.HasValue) + { + var contentExists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", + new { id = entity.RootContentId.Value }); + if (contentExists == 0) { - sql.WhereIn(x => x.Id, ids); + throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); } - - return Database.Fetch(sql).Select(ConvertFromDto); } - protected override IEnumerable PerformGetByQuery(IQuery query) + if (entity.LanguageId.HasValue) { - throw new NotSupportedException("This repository does not support this method"); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - if (isCount) + var languageExists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + new { id = entity.LanguageId.Value }); + if (languageExists == 0) { - sql.SelectCount().From(); + throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); } - else + } + + entity.AddingEntity(); + + var factory = new DomainModelFactory(); + DomainDto dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + // if the language changed, we need to resolve the ISO code! + if (entity.LanguageId.HasValue) + { + ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( + "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IDomain entity) + { + entity.UpdatingEntity(); + + var exists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", + new { domainName = entity.DomainName, id = entity.Id }); + + // ensure there is no other domain with the same name on another entity + if (exists > 0) + { + throw new DuplicateNameException( + string.Format("The domain name {0} is already assigned", entity.DomainName)); + } + + if (entity.RootContentId.HasValue) + { + var contentExists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", + new { id = entity.RootContentId.Value }); + if (contentExists == 0) { - sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode") - .From() - .LeftJoin() - .On(dto => dto.DefaultLanguage, dto => dto.Id); + throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); } - - return sql; } - protected override string GetBaseWhereClause() + if (entity.LanguageId.HasValue) { - return $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoDomain WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IDomain entity) - { - var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", new { domainName = entity.DomainName }); - if (exists > 0) throw new DuplicateNameException(string.Format("The domain name {0} is already assigned", entity.DomainName)); - - if (entity.RootContentId.HasValue) + var languageExists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + new { id = entity.LanguageId.Value }); + if (languageExists == 0) { - var contentExists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", new { id = entity.RootContentId.Value }); - if (contentExists == 0) throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); + throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); } - - if (entity.LanguageId.HasValue) - { - var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", new { id = entity.LanguageId.Value }); - if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); - } - - entity.AddingEntity(); - - var factory = new DomainModelFactory(); - var dto = factory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - //if the language changed, we need to resolve the ISO code! - if (entity.LanguageId.HasValue) - { - ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar("SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); - } - - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(IDomain entity) + var factory = new DomainModelFactory(); + DomainDto dto = factory.BuildDto(entity); + + Database.Update(dto); + + // if the language changed, we need to resolve the ISO code! + if (entity.WasPropertyDirty("LanguageId")) { - entity.UpdatingEntity(); + ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( + "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); + } - var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", - new { domainName = entity.DomainName, id = entity.Id }); - //ensure there is no other domain with the same name on another entity - if (exists > 0) throw new DuplicateNameException(string.Format("The domain name {0} is already assigned", entity.DomainName)); + entity.ResetDirtyProperties(); + } - if (entity.RootContentId.HasValue) + private IDomain ConvertFromDto(DomainDto dto) + { + var factory = new DomainModelFactory(); + IDomain entity = factory.BuildEntity(dto); + return entity; + } + + internal class DomainModelFactory + { + public IDomain BuildEntity(DomainDto dto) + { + var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) { - var contentExists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", new { id = entity.RootContentId.Value }); - if (contentExists == 0) throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); - } + Id = dto.Id, + LanguageId = dto.DefaultLanguage, + RootContentId = dto.RootStructureId, + }; - if (entity.LanguageId.HasValue) + // reset dirty initial properties (U4-1946) + domain.ResetDirtyProperties(false); + return domain; + } + + public DomainDto BuildDto(IDomain entity) + { + var dto = new DomainDto { - var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", new { id = entity.LanguageId.Value }); - if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); - } - - var factory = new DomainModelFactory(); - var dto = factory.BuildDto(entity); - - Database.Update(dto); - - //if the language changed, we need to resolve the ISO code! - if (entity.WasPropertyDirty("LanguageId")) - { - ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar("SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new {langId = entity.LanguageId}); - } - - entity.ResetDirtyProperties(); - } - - public IDomain? GetByName(string domainName) - { - return GetMany()?.FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); - } - - public bool Exists(string domainName) - { - return GetMany()?.Any(x => x.DomainName.InvariantEquals(domainName)) ?? false; - } - - public IEnumerable GetAll(bool includeWildcards) - { - return GetMany().Where(x => includeWildcards || x.IsWildcard == false); - } - - public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) - { - return GetMany() - .Where(x => x.RootContentId == contentId) - .Where(x => includeWildcards || x.IsWildcard == false); - } - - private IDomain ConvertFromDto(DomainDto dto) - { - var factory = new DomainModelFactory(); - var entity = factory.BuildEntity(dto); - return entity; - } - - internal class DomainModelFactory - { - - public IDomain BuildEntity(DomainDto dto) - { - var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) - { - Id = dto.Id, - LanguageId = dto.DefaultLanguage, - RootContentId = dto.RootStructureId - }; - // reset dirty initial properties (U4-1946) - domain.ResetDirtyProperties(false); - return domain; - } - - public DomainDto BuildDto(IDomain entity) - { - var dto = new DomainDto { DefaultLanguage = entity.LanguageId, DomainName = entity.DomainName, Id = entity.Id, RootStructureId = entity.RootContentId }; - return dto; - } + DefaultLanguage = entity.LanguageId, + DomainName = entity.DomainName, + Id = entity.Id, + RootStructureId = entity.RootContentId, + }; + return dto; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs index 468b83062c..7d3a9ab4fa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -12,336 +9,333 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// An internal repository for managing entity containers such as doc type, media type, data type containers. +/// +internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository { - /// - /// An internal repository for managing entity containers such as doc type, media type, data type containers. - /// - internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository + public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger, Guid containerObjectType) + : base(scopeAccessor, cache, logger) { - public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger, Guid containerObjectType) - : base(scopeAccessor, cache, logger) + Guid[] allowedContainers = { - Guid[] allowedContainers = new[] - { - Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, - Constants.ObjectTypes.DataTypeContainer - }; - NodeObjectTypeId = containerObjectType; - if (allowedContainers.Contains(NodeObjectTypeId) == false) - { - throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); - } + Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, + Constants.ObjectTypes.DataTypeContainer, + }; + NodeObjectTypeId = containerObjectType; + if (allowedContainers.Contains(NodeObjectTypeId) == false) + { + throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); + } + } + + protected Guid NodeObjectTypeId { get; } + + // temp - so we don't have to implement GetByQuery + public EntityContainer? Get(Guid id) + { + Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + + NodeDto? nodeDto = Database.Fetch(sql).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } + + public IEnumerable Get(string name, int level) + { + Sql sql = GetBaseQuery(false) + .Where( + "text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", + new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); + return Database.Fetch(sql).Select(CreateEntity); + } + + // never cache + protected override IRepositoryCachePolicy CreateCachePolicy() => + NoCacheRepositoryCachePolicy.Instance; + + protected override EntityContainer? PerformGet(int id) + { + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { id, NodeObjectType = NodeObjectTypeId }); + + NodeDto? nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Any() ?? false) + { + return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => + GetBaseQuery(false) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .WhereIn(x => x.NodeId, batch)) + .Select(CreateEntity); } - protected Guid NodeObjectTypeId { get; } + // else + Sql sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) + .OrderBy(x => x.Level); - // never cache - protected override IRepositoryCachePolicy CreateCachePolicy() => - NoCacheRepositoryCachePolicy.Instance; + return Database.Fetch(sql).Select(CreateEntity); + } - protected override EntityContainer? PerformGet(int id) + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotImplementedException(); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - Sql sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { id, NodeObjectType = NodeObjectTypeId }); - - NodeDto? nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - return nodeDto == null ? null : CreateEntity(nodeDto); + sql.SelectCount(); + } + else + { + sql.SelectAll(); } - // temp - so we don't have to implement GetByQuery - public EntityContainer? Get(Guid id) - { - Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + sql.From(); + return sql; + } - NodeDto? nodeDto = Database.Fetch(sql).FirstOrDefault(); - return nodeDto == null ? null : CreateEntity(nodeDto); + private static EntityContainer CreateEntity(NodeDto nodeDto) + { + if (nodeDto.NodeObjectType.HasValue == false) + { + throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); } - public IEnumerable Get(string name, int level) + // throws if node is not a container + Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); + + var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, + nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, + containedObjectType, + nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + + return entity; + } + + protected override string GetBaseWhereClause() => "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; + + protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); + + protected override void PersistDeletedItem(EntityContainer entity) + { + if (entity == null) { - Sql sql = GetBaseQuery(false) - .Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", - new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); - return Database.Fetch(sql).Select(CreateEntity); + throw new ArgumentNullException(nameof(entity)); } - protected override IEnumerable PerformGetAll(params int[]? ids) + EnsureContainerType(entity); + + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + + if (nodeDto == null) { - if (ids?.Any() ?? false) - { - return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => - GetBaseQuery(false) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.NodeId, batch)) - .Select(CreateEntity); - } - - // else - - Sql sql = GetBaseQuery(false) - .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) - .OrderBy(x => x.Level); - - return Database.Fetch(sql).Select(CreateEntity); + return; } - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new NotImplementedException(); + // move children to the parent so they are not orphans + List childDtos = Database.Fetch(Sql().SelectAll() + .From() + .Where( + "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", + new + { + parentID = entity.Id, + containedObjectType = entity.ContainedObjectType, + containerObjectType = entity.ContainerObjectType, + })); - private static EntityContainer CreateEntity(NodeDto nodeDto) + foreach (NodeDto childDto in childDtos) { - if (nodeDto.NodeObjectType.HasValue == false) - { - throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); - } - - // throws if node is not a container - Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); - - var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, - nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, - containedObjectType, - nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - - return entity; + childDto.ParentId = nodeDto.ParentId; + Database.Update(childDto); } - protected override Sql GetBaseQuery(bool isCount) - { - Sql sql = Sql(); - if (isCount) - { - sql.SelectCount(); - } - else - { - sql.SelectAll(); - } + // delete + Database.Delete(nodeDto); - sql.From(); - return sql; + entity.DeleteDate = DateTime.Now; + } + + protected override void PersistNewItem(EntityContainer entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); } - protected override string GetBaseWhereClause() => "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; + EnsureContainerType(entity); - protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); - - protected override void PersistDeletedItem(EntityContainer entity) + if (entity.Name == null) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } - - EnsureContainerType(entity); - - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - - if (nodeDto == null) - { - return; - } - - // move children to the parent so they are not orphans - List childDtos = Database.Fetch(Sql().SelectAll() - .From() - .Where( - "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", - new - { - parentID = entity.Id, - containedObjectType = entity.ContainedObjectType, - containerObjectType = entity.ContainerObjectType - })); - - foreach (NodeDto childDto in childDtos) - { - childDto.ParentId = nodeDto.ParentId; - Database.Update(childDto); - } - - // delete - Database.Delete(nodeDto); - - entity.DeleteDate = DateTime.Now; + throw new InvalidOperationException("Entity name can't be null."); } - protected override void PersistNewItem(EntityContainer entity) + if (string.IsNullOrWhiteSpace(entity.Name)) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } - EnsureContainerType(entity); + entity.Name = entity.Name.Trim(); - if (entity.Name == null) - { - throw new InvalidOperationException("Entity name can't be null."); - } + // guard against duplicates + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto != null) + { + throw new InvalidOperationException("A container with the same name already exists."); + } - if (string.IsNullOrWhiteSpace(entity.Name)) - { - throw new InvalidOperationException( - "Entity name can't be empty or consist only of white-space characters."); - } - - entity.Name = entity.Name.Trim(); - - // guard against duplicates - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + // create + var level = 0; + var path = "-1"; + if (entity.ParentId > -1) + { + NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => - dto.ParentId == entity.ParentId && dto.Text == entity.Name && - dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto != null) + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + + if (parentDto == null) { - throw new InvalidOperationException("A container with the same name already exists."); + throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); } - // create - var level = 0; - var path = "-1"; + level = parentDto.Level; + path = parentDto.Path; + } + + // note: sortOrder is NOT managed and always zero for containers + nodeDto = new NodeDto + { + CreateDate = DateTime.Now, + Level = Convert.ToInt16(level + 1), + NodeObjectType = entity.ContainerObjectType, + ParentId = entity.ParentId, + Path = path, + SortOrder = 0, + Text = entity.Name, + UserId = entity.CreatorId, + UniqueId = entity.Key, + }; + + // insert, get the id, update the path with the id + var id = Convert.ToInt32(Database.Insert(nodeDto)); + nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; + Database.Save(nodeDto); + + // refresh the entity + entity.Id = id; + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.CreateDate = nodeDto.CreateDate; + entity.ResetDirtyProperties(); + } + + // beware! does NOT manage descendants in case of a new parent + protected override void PersistUpdatedItem(EntityContainer entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + EnsureContainerType(entity); + + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } + + entity.Name = entity.Name.Trim(); + + // find container to update + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto == null) + { + throw new InvalidOperationException("Could not find container with id " + entity.Id); + } + + // guard against duplicates + NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); + if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) + { + throw new InvalidOperationException("A container with the same name already exists."); + } + + // update + nodeDto.Text = entity.Name; + if (nodeDto.ParentId != entity.ParentId) + { + nodeDto.Level = 0; + nodeDto.Path = "-1"; if (entity.ParentId > -1) { - NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); - if (parentDto == null) + if (parent == null) { - throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); + throw new InvalidOperationException( + "Could not find parent container with id " + entity.ParentId); } - level = parentDto.Level; - path = parentDto.Path; + nodeDto.Level = Convert.ToInt16(parent.Level + 1); + nodeDto.Path = parent.Path + "," + nodeDto.NodeId; } - // note: sortOrder is NOT managed and always zero for containers - - nodeDto = new NodeDto - { - CreateDate = DateTime.Now, - Level = Convert.ToInt16(level + 1), - NodeObjectType = entity.ContainerObjectType, - ParentId = entity.ParentId, - Path = path, - SortOrder = 0, - Text = entity.Name, - UserId = entity.CreatorId, - UniqueId = entity.Key - }; - - // insert, get the id, update the path with the id - var id = Convert.ToInt32(Database.Insert(nodeDto)); - nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; - Database.Save(nodeDto); - - // refresh the entity - entity.Id = id; - entity.Path = nodeDto.Path; - entity.Level = nodeDto.Level; - entity.SortOrder = 0; - entity.CreateDate = nodeDto.CreateDate; - entity.ResetDirtyProperties(); + nodeDto.ParentId = entity.ParentId; } - // beware! does NOT manage descendants in case of a new parent - // - protected override void PersistUpdatedItem(EntityContainer entity) + // note: sortOrder is NOT managed and always zero for containers + + // update + Database.Update(nodeDto); + + // refresh the entity + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.ResetDirtyProperties(); + } + + private void EnsureContainerType(EntityContainer entity) + { + if (entity.ContainerObjectType != NodeObjectTypeId) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } - - EnsureContainerType(entity); - - if (entity.Name == null) - { - throw new InvalidOperationException("Entity name can't be null."); - } - - if (string.IsNullOrWhiteSpace(entity.Name)) - { - throw new InvalidOperationException( - "Entity name can't be empty or consist only of white-space characters."); - } - - entity.Name = entity.Name.Trim(); - - // find container to update - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto == null) - { - throw new InvalidOperationException("Could not find container with id " + entity.Id); - } - - // guard against duplicates - NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => - dto.ParentId == entity.ParentId && dto.Text == entity.Name && - dto.NodeObjectType == entity.ContainerObjectType)); - if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) - { - throw new InvalidOperationException("A container with the same name already exists."); - } - - // update - nodeDto.Text = entity.Name; - if (nodeDto.ParentId != entity.ParentId) - { - nodeDto.Level = 0; - nodeDto.Path = "-1"; - if (entity.ParentId > -1) - { - NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => - dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); - - if (parent == null) - { - throw new InvalidOperationException( - "Could not find parent container with id " + entity.ParentId); - } - - nodeDto.Level = Convert.ToInt16(parent.Level + 1); - nodeDto.Path = parent.Path + "," + nodeDto.NodeId; - } - - nodeDto.ParentId = entity.ParentId; - } - - // note: sortOrder is NOT managed and always zero for containers - - // update - Database.Update(nodeDto); - - // refresh the entity - entity.Path = nodeDto.Path; - entity.Level = nodeDto.Level; - entity.SortOrder = 0; - entity.ResetDirtyProperties(); - } - - private void EnsureContainerType(EntityContainer entity) - { - if (entity.ContainerObjectType != NodeObjectTypeId) - { - throw new InvalidOperationException("The container type does not match the repository object type"); - } + throw new InvalidOperationException("The container type does not match the repository object type"); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index ef0f02540e..c904b5b440 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; @@ -16,720 +12,769 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the EntityRepository used to query entity objects. +/// +/// +/// Limited to objects that have a corresponding node (in umbracoNode table). +/// Returns objects, i.e. lightweight representation of entities. +/// +internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended { - /// - /// Represents the EntityRepository used to query entity objects. - /// - /// - /// Limited to objects that have a corresponding node (in umbracoNode table). - /// Returns objects, i.e. lightweight representation of entities. - /// - internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended + public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) + : base(scopeAccessor, appCaches) { - public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) - : base(scopeAccessor, appCaches) + } + + #region Repository + + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) => + GetPagedResultsByQuery(query, new[] {objectType}, pageIndex, pageSize, out totalRecords, filter, ordering); + + // get a page of entities + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null) + { + var isContent = objectTypes.Any(objectType => + objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); + var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); + + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, s => { - } + sqlCustomization?.Invoke(s); - #region Repository - - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); - } - - // get a page of entities - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null) - { - var isContent = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint); - var isMedia = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Media); - var isMember = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Member); - - Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, s => + if (filter != null) { - sqlCustomization?.Invoke(s); - - if (filter != null) + foreach (Tuple filterClause in filter.GetWhereClauses()) { - foreach (Tuple filterClause in filter.GetWhereClauses()) - { - s.Where(filterClause.Item1, filterClause.Item2); - } + s.Where(filterClause.Item1, filterClause.Item2); } - }, objectTypes); - - ordering = ordering ?? Ordering.ByDefault(); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - - if (!ordering.IsEmpty) - { - // apply ordering - ApplyOrdering(ref sql, ordering); } + }, objectTypes); - // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - // no matter what we always must have node id ordered at the end - sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); + ordering = ordering ?? Ordering.ByDefault(); - // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names - var pageIndexToFetch = pageIndex + 1; - IEnumerable dtos; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - var entities = dtos.Select(BuildEntity).ToArray(); - - BuildVariants(entities.OfType()); - - return entities; + if (!ordering.IsEmpty) + { + // apply ordering + ApplyOrdering(ref sql, ordering); } - public IEntitySlim? Get(Guid key) + // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently + // no matter what we always must have node id ordered at the end + sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); + + // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names + var pageIndexToFetch = pageIndex + 1; + IEnumerable dtos; + Page? page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + + EntitySlim[] entities = dtos.Select(BuildEntity).ToArray(); + + BuildVariants(entities.OfType()); + + return entities; + } + + public IEntitySlim? Get(Guid key) + { + Sql sql = GetBaseWhere(false, false, false, false, key); + BaseDto? dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(dto); + } + + + private IEntitySlim? GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) + { + // isContent is going to return a 1:M result now with the variants so we need to do different things + if (isContent) { - var sql = GetBaseWhere(false, false, false, false, key); - var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + List? cdtos = Database.Fetch(sql); + + return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } + BaseDto? dto = isMedia + ? Database.FirstOrDefault(sql) + : Database.FirstOrDefault(sql); - private IEntitySlim? GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) + if (dto == null) { - // isContent is going to return a 1:M result now with the variants so we need to do different things - if (isContent) + return null; + } + + EntitySlim entity = BuildEntity(dto); + + return entity; + } + + public IEntitySlim? Get(Guid key, Guid objectTypeId) + { + var isContent = objectTypeId == Constants.ObjectTypes.Document || + objectTypeId == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; + + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key); + return GetEntity(sql, isContent, isMedia, isMember); + } + + public IEntitySlim? Get(int id) + { + Sql sql = GetBaseWhere(false, false, false, false, id); + BaseDto? dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(dto); + } + + public IEntitySlim? Get(int id, Guid objectTypeId) + { + var isContent = objectTypeId == Constants.ObjectTypes.Document || + objectTypeId == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; + + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id); + return GetEntity(sql, isContent, isMedia, isMember); + } + + public IEnumerable GetAll(Guid objectType, params int[] ids) => + ids.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAll(objectType); + + public IEnumerable GetAll(Guid objectType, params Guid[] keys) => + keys.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAll(objectType); + + private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) + { + // isContent is going to return a 1:M result now with the variants so we need to do different things + if (isContent) + { + List? cdtos = Database.Fetch(sql); + + return cdtos.Count == 0 + ? Enumerable.Empty() + : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); + } + + IEnumerable? dtos = isMedia + ? (IEnumerable)Database.Fetch(sql) + : Database.Fetch(sql); + + EntitySlim[] entities = dtos.Select(BuildEntity).ToArray(); + + return entities; + } + + private IEnumerable PerformGetAll(Guid objectType, Action>? filter = null) + { + var isContent = objectType == Constants.ObjectTypes.Document || + objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; + + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); + return GetEntities(sql, isContent, isMedia, isMember); + } + + public IEnumerable GetAllPaths(Guid objectType, params int[]? ids) => + ids?.Any() ?? false + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAllPaths(objectType); + + public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) => + keys.Any() + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAllPaths(objectType); + + private IEnumerable PerformGetAllPaths(Guid objectType, Action>? filter = null) + { + // NodeId is named Id on TreeEntityPath = use an alias + Sql sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path) + .From().Where(x => x.NodeObjectType == objectType); + filter?.Invoke(sql); + return Database.Fetch(sql); + } + + public IEnumerable GetByQuery(IQuery query) + { + Sql sqlClause = GetBase(false, false, false, null); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql = AddGroupBy(false, false, false, sql, true); + List? dtos = Database.Fetch(sql); + return dtos.Select(BuildEntity).ToList(); + } + + public IEnumerable GetByQuery(IQuery query, Guid objectType) + { + var isContent = objectType == Constants.ObjectTypes.Document || + objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; + + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] {objectType}); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, isMember, sql, true); + + return GetEntities(sql, isContent, isMedia, isMember); + } + + public UmbracoObjectTypes GetObjectType(int id) + { + Sql sql = Sql().Select(x => x.NodeObjectType).From() + .Where(x => x.NodeId == id); + return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); + } + + public UmbracoObjectTypes GetObjectType(Guid key) + { + Sql sql = Sql().Select(x => x.NodeObjectType).From() + .Where(x => x.UniqueId == key); + return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); + } + + public int ReserveId(Guid key) + { + NodeDto node; + + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.UniqueId == key && x.NodeObjectType == Constants.ObjectTypes.IdReservation); + + node = Database.SingleOrDefault(sql); + if (node != null) + { + throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + } + + node = new NodeDto + { + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Constants.ObjectTypes.IdReservation, + CreateDate = DateTime.Now, + UserId = null, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + Database.Insert(node); + + return node.NodeId; + } + + public bool Exists(Guid key) + { + Sql sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); + return Database.ExecuteScalar(sql) > 0; + } + + public bool Exists(int id) + { + Sql sql = Sql().SelectCount().From().Where(x => x.NodeId == id); + return Database.ExecuteScalar(sql) > 0; + } + + private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) + => BuildVariants(new[] {entity}).First(); + + private IEnumerable BuildVariants(IEnumerable entities) + { + List? v = null; + var entitiesList = entities.ToList(); + foreach (DocumentEntitySlim e in entitiesList) + { + if (e.Variations.VariesByCulture()) { - var cdtos = Database.Fetch(sql); - - return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); + (v ?? (v = new List())).Add(e); } - - var dto = isMedia - ? Database.FirstOrDefault(sql) - : Database.FirstOrDefault(sql); - - if (dto == null) return null; - - var entity = BuildEntity(dto); - - return entity; } - public IEntitySlim? Get(Guid key, Guid objectTypeId) + if (v == null) { - var isContent = objectTypeId == Cms.Core.Constants.ObjectTypes.Document || objectTypeId == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectTypeId == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key); - return GetEntity(sql, isContent, isMedia, isMember); - } - - public IEntitySlim? Get(int id) - { - var sql = GetBaseWhere(false, false, false, false, id); - var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); - } - - public IEntitySlim? Get(int id, Guid objectTypeId) - { - var isContent = objectTypeId == Cms.Core.Constants.ObjectTypes.Document || objectTypeId == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectTypeId == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id); - return GetEntity(sql, isContent, isMedia, isMember); - } - - public IEnumerable GetAll(Guid objectType, params int[] ids) - { - return ids.Length > 0 - ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) - : PerformGetAll(objectType); - } - - public IEnumerable GetAll(Guid objectType, params Guid[] keys) - { - return keys.Length > 0 - ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) - : PerformGetAll(objectType); - } - - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) - { - // isContent is going to return a 1:M result now with the variants so we need to do different things - if (isContent) - { - var cdtos = Database.Fetch(sql); - - return cdtos.Count == 0 - ? Enumerable.Empty() - : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); - } - - var dtos = isMedia - ? (IEnumerable)Database.Fetch(sql) - : Database.Fetch(sql); - - var entities = dtos.Select(BuildEntity).ToArray(); - - return entities; - } - - private IEnumerable PerformGetAll(Guid objectType, Action>? filter = null) - { - var isContent = objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectType == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); - return GetEntities(sql, isContent, isMedia, isMember); - } - - public IEnumerable GetAllPaths(Guid objectType, params int[]? ids) - { - return ids?.Any() ?? false - ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) - : PerformGetAllPaths(objectType); - } - - public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) - { - return keys.Any() - ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) - : PerformGetAllPaths(objectType); - } - - private IEnumerable PerformGetAllPaths(Guid objectType, Action>? filter = null) - { - // NodeId is named Id on TreeEntityPath = use an alias - var sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path).From().Where(x => x.NodeObjectType == objectType); - filter?.Invoke(sql); - return Database.Fetch(sql); - } - - public IEnumerable GetByQuery(IQuery query) - { - var sqlClause = GetBase(false, false, false, null); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql = AddGroupBy(false, false, false, sql, true); - var dtos = Database.Fetch(sql); - return dtos.Select(BuildEntity).ToList(); - } - - public IEnumerable GetByQuery(IQuery query, Guid objectType) - { - var isContent = objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectType == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, true); - - return GetEntities(sql, isContent, isMedia, isMember); - } - - public UmbracoObjectTypes GetObjectType(int id) - { - var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.NodeId == id); - return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); - } - - public UmbracoObjectTypes GetObjectType(Guid key) - { - var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.UniqueId == key); - return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); - } - - public int ReserveId(Guid key) - { - NodeDto node; - - Sql sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.UniqueId == key && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation); - - node = Database.SingleOrDefault(sql); - if (node != null) - throw new InvalidOperationException("An identifier has already been reserved for this Udi."); - - node = new NodeDto - { - UniqueId = key, - Text = "RESERVED.ID", - NodeObjectType = Cms.Core.Constants.ObjectTypes.IdReservation, - - CreateDate = DateTime.Now, - UserId = null, - ParentId = -1, - Level = 1, - Path = "-1", - SortOrder = 0, - Trashed = false - }; - Database.Insert(node); - - return node.NodeId; - } - - public bool Exists(Guid key) - { - var sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); - return Database.ExecuteScalar(sql) > 0; - } - - public bool Exists(int id) - { - var sql = Sql().SelectCount().From().Where(x => x.NodeId == id); - return Database.ExecuteScalar(sql) > 0; - } - - private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) - => BuildVariants(new[] { entity }).First(); - - private IEnumerable BuildVariants(IEnumerable entities) - { - List? v = null; - var entitiesList = entities.ToList(); - foreach (var e in entitiesList) - { - if (e.Variations.VariesByCulture()) - (v ?? (v = new List())).Add(e); - } - - if (v == null) return entitiesList; - - // fetch all variant info dtos - var dtos = Database.FetchByGroups(v.Select(x => x.Id), Constants.Sql.MaxParameterCount, GetVariantInfos); - - // group by node id (each group contains all languages) - var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); - - foreach (var e in v) - { - // since we're only iterating on entities that vary, we must have something - var edtos = xdtos[e.Id]; - - e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); - e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); - e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); - } - return entitiesList; } - #endregion + // fetch all variant info dtos + IEnumerable dtos = Database.FetchByGroups(v.Select(x => x.Id), + Constants.Sql.MaxParameterCount, GetVariantInfos); - #region Sql + // group by node id (each group contains all languages) + var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); - protected Sql GetVariantInfos(IEnumerable ids) + foreach (DocumentEntitySlim e in v) { - return Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.IsoCode) - .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited")) - .AndSelect("dcv", - x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), x => Alias(x.Edited, "CultureEdited"), - x => Alias(x.Name, "Name")) + // since we're only iterating on entities that vary, we must have something + IGrouping edtos = xdtos[e.Id]; - // from node x language - .From() - .CrossJoin() - - // join to document - always exists - indicates global document published/edited status - .InnerJoin("doc") - .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") - - // left-join do document variation - matches cultures that are *available* + indicates when *edited* - .LeftJoin("dcv") - .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") - - // for selected nodes - .WhereIn(x => x.NodeId, ids) - .OrderBy(x => x.Id); + e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); + e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); + e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); } - // gets the full sql for a given object type and a given unique id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Guid uniqueId) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the full sql for a given object type and a given node id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, int nodeId) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the full sql for a given object type, with a given filter - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action>? filter) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the base SELECT + FROM [+ filter] sql - // always from the 'current' content version - protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action>? filter, bool isCount = false) - { - var sql = Sql(); - - if (isCount) - { - sql.SelectCount(); - } - else - { - sql - .Select(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) - .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate) - .Append(", COUNT(child.id) AS children"); - - if (isContent || isMedia || isMember) - sql - .AndSelect(x => Alias(x.Id, "versionId"), x=>x.VersionDate) - .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); - - if (isContent) - { - sql - .AndSelect(x => x.Published, x => x.Edited); - } - - if (isMedia) - { - sql - .AndSelect(x => Alias(x.Path, "MediaPath")); - } - } - - sql - .From(); - - if (isContent || isMedia || isMember) - { - sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); - } - - if (isContent) - { - sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId); - } - - if (isMedia) - { - sql - .LeftJoin().On((left, right) => left.Id == right.Id); - } - - //Any LeftJoin statements need to come last - if (isCount == false) - { - sql - .LeftJoin("child").On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); - } - - - filter?.Invoke(sql); - - return sql; - } - - // gets the base SELECT + FROM [+ filter] + WHERE sql - // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action>? filter, Guid[] objectTypes) - { - var sql = GetBase(isContent, isMedia, isMember, filter, isCount); - if (objectTypes.Length > 0) - { - sql.WhereIn(x => x.NodeObjectType, objectTypes); - } - return sql; - } - - // gets the base SELECT + FROM + WHERE sql - // for a given node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) - { - var sql = GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.NodeId == id); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the base SELECT + FROM + WHERE sql - // for a given unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) - { - var sql = GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.UniqueId == uniqueId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the base SELECT + FROM + WHERE sql - // for a given object type and node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, int nodeId) - { - return GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); - } - - // gets the base SELECT + FROM + WHERE sql - // for a given object type and unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, Guid uniqueId) - { - return GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); - } - - // gets the GROUP BY / ORDER BY sql - // required in order to count children - protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, bool defaultSort) - { - sql - .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) - .AndBy(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate); - - if (isContent) - { - sql - .AndBy(x => x.Published, x => x.Edited); - } - - if (isMedia) - { - sql - .AndBy(x => Alias(x.Path, "MediaPath")); - } - - - if (isContent || isMedia || isMember) - sql - .AndBy(x => x.Id, x => x.VersionDate) - .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); - - if (defaultSort) - sql.OrderBy(x => x.SortOrder); - - return sql; - } - - private void ApplyOrdering(ref Sql sql, Ordering ordering) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - - // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort - // As more things are attempted to be sorted we'll prob have to add more expressions here - string orderBy; - switch (ordering.OrderBy?.ToUpperInvariant()) - { - case "PATH": - orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); - break; - - default: - orderBy = ordering.OrderBy ?? string.Empty; - break; - } - - if (ordering.Direction == Direction.Ascending) - sql.OrderBy(orderBy); - else - sql.OrderByDescending(orderBy); - } - - #endregion - - #region Classes - - /// - /// The DTO used to fetch results for a generic content item which could be either a document, media or a member - /// - private class GenericContentEntityDto : DocumentEntityDto - { - public string? MediaPath { get; set; } - } - - /// - /// The DTO used to fetch results for a document item with its variation info - /// - private class DocumentEntityDto : BaseDto - { - public ContentVariation Variations { get; set; } - - public bool Published { get; set; } - public bool Edited { get; set; } - } - - /// - /// The DTO used to fetch results for a media item with its media path info - /// - private class MediaEntityDto : BaseDto - { - public string? MediaPath { get; set; } - } - - /// - /// The DTO used to fetch results for a member item - /// - private class MemberEntityDto : BaseDto - { - } - - public class VariantInfoDto - { - public int NodeId { get; set; } - public string IsoCode { get; set; } = null!; - public string Name { get; set; } = null!; - public bool DocumentPublished { get; set; } - public bool DocumentEdited { get; set; } - - public bool CultureAvailable { get; set; } - public bool CulturePublished { get; set; } - public bool CultureEdited { get; set; } - } - - // ReSharper disable once ClassNeverInstantiated.Local - /// - /// the DTO corresponding to fields selected by GetBase - /// - private class BaseDto - { - // ReSharper disable UnusedAutoPropertyAccessor.Local - // ReSharper disable UnusedMember.Local - public int NodeId { get; set; } - public bool Trashed { get; set; } - public int ParentId { get; set; } - public int? UserId { get; set; } - public int Level { get; set; } - public string Path { get; set; } = null!; - public int SortOrder { get; set; } - public Guid UniqueId { get; set; } - public string? Text { get; set; } - public Guid NodeObjectType { get; set; } - public DateTime CreateDate { get; set; } - public DateTime VersionDate { get; set; } - public int Children { get; set; } - public int VersionId { get; set; } - public string Alias { get; set; } = null!; - public string? Icon { get; set; } - public string? Thumbnail { get; set; } - public bool IsContainer { get; set; } - - // ReSharper restore UnusedAutoPropertyAccessor.Local - // ReSharper restore UnusedMember.Local - } - #endregion - - #region Factory - - private EntitySlim BuildEntity(BaseDto dto) - { - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Document) - return BuildDocumentEntity(dto); - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Media) - return BuildMediaEntity(dto); - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Member) - return BuildMemberEntity(dto); - - // EntitySlim does not track changes - var entity = new EntitySlim(); - BuildEntity(entity, dto); - return entity; - } - - private static void BuildEntity(EntitySlim entity, BaseDto dto) - { - entity.Trashed = dto.Trashed; - entity.CreateDate = dto.CreateDate; - entity.UpdateDate = dto.VersionDate; - entity.CreatorId = dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - entity.Id = dto.NodeId; - entity.Key = dto.UniqueId; - entity.Level = dto.Level; - entity.Name = dto.Text; - entity.NodeObjectType = dto.NodeObjectType; - entity.ParentId = dto.ParentId; - entity.Path = dto.Path; - entity.SortOrder = dto.SortOrder; - entity.HasChildren = dto.Children > 0; - entity.IsContainer = dto.IsContainer; - } - - private static void BuildContentEntity(ContentEntitySlim entity, BaseDto dto) - { - BuildEntity(entity, dto); - entity.ContentTypeAlias = dto.Alias; - entity.ContentTypeIcon = dto.Icon; - entity.ContentTypeThumbnail = dto.Thumbnail; - } - - private MediaEntitySlim BuildMediaEntity(BaseDto dto) - { - // EntitySlim does not track changes - var entity = new MediaEntitySlim(); - BuildContentEntity(entity, dto); - - // fill in the media info - if (dto is MediaEntityDto mediaEntityDto) - { - entity.MediaPath = mediaEntityDto.MediaPath; - } - else if (dto is GenericContentEntityDto genericContentEntityDto) - { - entity.MediaPath = genericContentEntityDto.MediaPath; - } - - return entity; - } - - private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) - { - // EntitySlim does not track changes - var entity = new DocumentEntitySlim(); - BuildContentEntity(entity, dto); - - if (dto is DocumentEntityDto contentDto) - { - // fill in the invariant info - entity.Edited = contentDto.Edited; - entity.Published = contentDto.Published; - entity.Variations = contentDto.Variations; - } - - return entity; - } - - private MemberEntitySlim BuildMemberEntity(BaseDto dto) - { - // EntitySlim does not track changes - var entity = new MemberEntitySlim(); - BuildEntity(entity, dto); - - entity.ContentTypeAlias = dto.Alias; - entity.ContentTypeIcon = dto.Icon; - entity.ContentTypeThumbnail = dto.Thumbnail; - - return entity; - } - - #endregion + return entitiesList; } + + #endregion + + #region Sql + + protected Sql GetVariantInfos(IEnumerable ids) => + Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.IsoCode) + .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), + x => Alias(x.Edited, "DocumentEdited")) + .AndSelect("dcv", + x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), + x => Alias(x.Edited, "CultureEdited"), + x => Alias(x.Name, "Name")) + + // from node x language + .From() + .CrossJoin() + + // join to document - always exists - indicates global document published/edited status + .InnerJoin("doc") + .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") + + // left-join do document variation - matches cultures that are *available* + indicates when *edited* + .LeftJoin("dcv") + .On( + (node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") + + // for selected nodes + .WhereIn(x => x.NodeId, ids) + .OrderBy(x => x.Id); + + // gets the full sql for a given object type and a given unique id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + Guid uniqueId) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the full sql for a given object type and a given node id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + int nodeId) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the full sql for a given object type, with a given filter + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + Action>? filter) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] {objectType}); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the base SELECT + FROM [+ filter] sql + // always from the 'current' content version + protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action>? filter, + bool isCount = false) + { + Sql sql = Sql(); + + if (isCount) + { + sql.SelectCount(); + } + else + { + sql + .Select(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, + x => x.Path) + .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, + x => x.CreateDate) + .Append(", COUNT(child.id) AS children"); + + if (isContent || isMedia || isMember) + { + sql + .AndSelect(x => Alias(x.Id, "versionId"), x => x.VersionDate) + .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, + x => x.Variations); + } + + if (isContent) + { + sql + .AndSelect(x => x.Published, x => x.Edited); + } + + if (isMedia) + { + sql + .AndSelect(x => Alias(x.Path, "MediaPath")); + } + } + + sql + .From(); + + if (isContent || isMedia || isMember) + { + sql + .LeftJoin() + .On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin() + .On((left, right) => left.ContentTypeId == right.NodeId); + } + + if (isContent) + { + sql + .LeftJoin().On((left, right) => left.NodeId == right.NodeId); + } + + if (isMedia) + { + sql + .LeftJoin() + .On((left, right) => left.Id == right.Id); + } + + //Any LeftJoin statements need to come last + if (isCount == false) + { + sql + .LeftJoin("child") + .On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); + } + + + filter?.Invoke(sql); + + return sql; + } + + // gets the base SELECT + FROM [+ filter] + WHERE sql + // for a given object type, with a given filter + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, + Action>? filter, Guid[] objectTypes) + { + Sql sql = GetBase(isContent, isMedia, isMember, filter, isCount); + if (objectTypes.Length > 0) + { + sql.WhereIn(x => x.NodeObjectType, objectTypes); + } + + return sql; + } + + // gets the base SELECT + FROM + WHERE sql + // for a given node id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) + { + Sql sql = GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.NodeId == id); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the base SELECT + FROM + WHERE sql + // for a given unique id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) + { + Sql sql = GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.UniqueId == uniqueId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the base SELECT + FROM + WHERE sql + // for a given object type and node id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, + int nodeId) => + GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); + + // gets the base SELECT + FROM + WHERE sql + // for a given object type and unique id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, + Guid uniqueId) => + GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); + + // gets the GROUP BY / ORDER BY sql + // required in order to count children + protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, + bool defaultSort) + { + sql + .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) + .AndBy(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate); + + if (isContent) + { + sql + .AndBy(x => x.Published, x => x.Edited); + } + + if (isMedia) + { + sql + .AndBy(x => Alias(x.Path, "MediaPath")); + } + + + if (isContent || isMedia || isMember) + { + sql + .AndBy(x => x.Id, x => x.VersionDate) + .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, + x => x.Variations); + } + + if (defaultSort) + { + sql.OrderBy(x => x.SortOrder); + } + + return sql; + } + + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } + + // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort + // As more things are attempted to be sorted we'll prob have to add more expressions here + string orderBy; + switch (ordering.OrderBy?.ToUpperInvariant()) + { + case "PATH": + orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); + break; + + default: + orderBy = ordering.OrderBy ?? string.Empty; + break; + } + + if (ordering.Direction == Direction.Ascending) + { + sql.OrderBy(orderBy); + } + else + { + sql.OrderByDescending(orderBy); + } + } + + #endregion + + #region Classes + + /// + /// The DTO used to fetch results for a generic content item which could be either a document, media or a member + /// + private class GenericContentEntityDto : DocumentEntityDto + { + public string? MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a document item with its variation info + /// + private class DocumentEntityDto : BaseDto + { + public ContentVariation Variations { get; set; } + + public bool Published { get; set; } + public bool Edited { get; set; } + } + + /// + /// The DTO used to fetch results for a media item with its media path info + /// + private class MediaEntityDto : BaseDto + { + public string? MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a member item + /// + private class MemberEntityDto : BaseDto + { + } + + public class VariantInfoDto + { + public int NodeId { get; set; } + public string IsoCode { get; set; } = null!; + public string Name { get; set; } = null!; + public bool DocumentPublished { get; set; } + public bool DocumentEdited { get; set; } + + public bool CultureAvailable { get; set; } + public bool CulturePublished { get; set; } + public bool CultureEdited { get; set; } + } + + // ReSharper disable once ClassNeverInstantiated.Local + /// + /// the DTO corresponding to fields selected by GetBase + /// + private class BaseDto + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + public int NodeId { get; set; } + public bool Trashed { get; set; } + public int ParentId { get; set; } + public int? UserId { get; set; } + public int Level { get; set; } + public string Path { get; } = null!; + public int SortOrder { get; set; } + public Guid UniqueId { get; set; } + public string? Text { get; set; } + public Guid NodeObjectType { get; set; } + public DateTime CreateDate { get; set; } + public DateTime VersionDate { get; set; } + public int Children { get; set; } + public int VersionId { get; set; } + public string Alias { get; } = null!; + public string? Icon { get; set; } + public string? Thumbnail { get; set; } + public bool IsContainer { get; set; } + + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore UnusedMember.Local + } + + #endregion + + #region Factory + + private EntitySlim BuildEntity(BaseDto dto) + { + if (dto.NodeObjectType == Constants.ObjectTypes.Document) + { + return BuildDocumentEntity(dto); + } + + if (dto.NodeObjectType == Constants.ObjectTypes.Media) + { + return BuildMediaEntity(dto); + } + + if (dto.NodeObjectType == Constants.ObjectTypes.Member) + { + return BuildMemberEntity(dto); + } + + // EntitySlim does not track changes + var entity = new EntitySlim(); + BuildEntity(entity, dto); + return entity; + } + + private static void BuildEntity(EntitySlim entity, BaseDto dto) + { + entity.Trashed = dto.Trashed; + entity.CreateDate = dto.CreateDate; + entity.UpdateDate = dto.VersionDate; + entity.CreatorId = dto.UserId ?? Constants.Security.UnknownUserId; + entity.Id = dto.NodeId; + entity.Key = dto.UniqueId; + entity.Level = dto.Level; + entity.Name = dto.Text; + entity.NodeObjectType = dto.NodeObjectType; + entity.ParentId = dto.ParentId; + entity.Path = dto.Path; + entity.SortOrder = dto.SortOrder; + entity.HasChildren = dto.Children > 0; + entity.IsContainer = dto.IsContainer; + } + + private static void BuildContentEntity(ContentEntitySlim entity, BaseDto dto) + { + BuildEntity(entity, dto); + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + } + + private MediaEntitySlim BuildMediaEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MediaEntitySlim(); + BuildContentEntity(entity, dto); + + // fill in the media info + if (dto is MediaEntityDto mediaEntityDto) + { + entity.MediaPath = mediaEntityDto.MediaPath; + } + else if (dto is GenericContentEntityDto genericContentEntityDto) + { + entity.MediaPath = genericContentEntityDto.MediaPath; + } + + return entity; + } + + private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new DocumentEntitySlim(); + BuildContentEntity(entity, dto); + + if (dto is DocumentEntityDto contentDto) + { + // fill in the invariant info + entity.Edited = contentDto.Edited; + entity.Published = contentDto.Published; + entity.Variations = contentDto.Variations; + } + + return entity; + } + + private MemberEntitySlim BuildMemberEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MemberEntitySlim(); + BuildEntity(entity, dto); + + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + + return entity; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index 4ac8adbd91..611d89b6cf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,243 +10,234 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Provides a base class to all based repositories. +/// +/// The type of the entity's unique identifier. +/// The type of the entity managed by this repository. +public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository + where TEntity : class, IEntity { + private static RepositoryCachePolicyOptions? _defaultOptions; + private IRepositoryCachePolicy? _cachePolicy; + private IQuery? _hasIdQuery; + /// - /// Provides a base class to all based repositories. + /// Initializes a new instance of the class. /// - /// The type of the entity's unique identifier. - /// The type of the entity managed by this repository. - public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository - where TEntity : class, IEntity + protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) + : base(scopeAccessor, appCaches) => + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// Gets the logger + /// + protected ILogger> Logger { get; } + + /// + /// Gets the isolated cache for the + /// + protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); + + /// + /// Gets the isolated cache. + /// + /// Depends on the ambient scope cache mode. + protected IAppPolicyCache IsolatedCache { - private static RepositoryCachePolicyOptions? s_defaultOptions; - private IRepositoryCachePolicy? _cachePolicy; - private IQuery? _hasIdQuery; - - /// - /// Initializes a new instance of the class. - /// - protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, - ILogger> logger) - : base(scopeAccessor, appCaches) => - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - /// - /// Gets the logger - /// - protected ILogger> Logger { get; } - - /// - /// Gets the isolated cache for the - /// - protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); - - /// - /// Gets the isolated cache. - /// - /// Depends on the ambient scope cache mode. - protected IAppPolicyCache IsolatedCache + get { - get + switch (AmbientScope.RepositoryCacheMode) { - switch (AmbientScope.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - return AppCaches.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.Scoped: - return AmbientScope.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.None: - return NoAppCache.Instance; - default: - throw new Exception("oops: cache mode."); - } + case RepositoryCacheMode.Default: + return AppCaches.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.Scoped: + return AmbientScope.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.None: + return NoAppCache.Instance; + default: + throw new Exception("oops: cache mode."); } } - - /// - /// Gets the default - /// - protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions - = new RepositoryCachePolicyOptions(() => - { - // get count of all entities of current type (TEntity) to ensure cached result is correct - // create query once if it is needed (no need for locking here) - query is static! - IQuery query = _hasIdQuery ?? - (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); - return PerformCount(query); - })); - - /// - /// Gets the repository cache policy - /// - protected IRepositoryCachePolicy CachePolicy - { - get - { - if (AppCaches == AppCaches.NoCache) - { - return NoCacheRepositoryCachePolicy.Instance; - } - - // create the cache policy using IsolatedCache which is either global - // or scoped depending on the repository cache mode for the current scope - - switch (AmbientScope.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - case RepositoryCacheMode.Scoped: - // return the same cache policy in both cases - the cache policy is - // supposed to pick either the global or scope cache depending on the - // scope cache mode - return _cachePolicy ?? (_cachePolicy = CreateCachePolicy()); - case RepositoryCacheMode.None: - return NoCacheRepositoryCachePolicy.Instance; - default: - throw new Exception("oops: cache mode."); - } - } - } - - /// - /// Adds or Updates an entity of type TEntity - /// - /// This method is backed by an cache - public virtual void Save(TEntity entity) - { - if (entity.HasIdentity == false) - { - CachePolicy.Create(entity, PersistNewItem); - } - else - { - CachePolicy.Update(entity, PersistUpdatedItem); - } - } - - /// - /// Deletes the passed in entity - /// - public virtual void Delete(TEntity entity) - => CachePolicy.Delete(entity, PersistDeletedItem); - - /// - /// Gets an entity by the passed in Id utilizing the repository's cache policy - /// - public TEntity? Get(TId? id) - => CachePolicy.Get(id, PerformGet, PerformGetAll); - - /// - /// Gets all entities of type TEntity or a list according to the passed in Ids - /// - public IEnumerable GetMany(params TId[]? ids) - { - // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct() - - // don't query by anything that is a default of T (like a zero) - // TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids - // .Where(x => Equals(x, default(TId)) == false) - .ToArray(); - - // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, - // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group - if (ids?.Length <= Constants.Sql.MaxParameterCount) - { - return CachePolicy.GetAll(ids, PerformGetAll) ?? Enumerable.Empty(); - } - - var entities = new List(); - foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - var groups = CachePolicy.GetAll(group.ToArray(), PerformGetAll); - if (groups is not null) - { - entities.AddRange(groups); - } - } - - return entities; - } - - /// - /// Gets a list of entities by the passed in query - /// - public IEnumerable Get(IQuery query) - { - - // ensure we don't include any null refs in the returned collection! - return PerformGetByQuery(query) - .WhereNotNull(); - } - - /// - /// Returns a boolean indicating whether an entity with the passed Id exists - /// - public bool Exists(TId id) - => CachePolicy.Exists(id, PerformExists, PerformGetAll); - - /// - /// Returns an integer with the count of entities found with the passed in query - /// - public int Count(IQuery query) - => PerformCount(query); - - /// - /// Get the entity id for the - /// - protected virtual TId GetEntityId(TEntity entity) - => (TId)(object)entity.Id; - - /// - /// Create the repository cache policy - /// - protected virtual IRepositoryCachePolicy CreateCachePolicy() - => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - - protected abstract TEntity? PerformGet(TId? id); - - protected abstract IEnumerable PerformGetAll(params TId[]? ids); - - protected abstract IEnumerable PerformGetByQuery(IQuery query); - - protected abstract void PersistNewItem(TEntity item); - - protected abstract void PersistUpdatedItem(TEntity item); - - // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); - protected abstract Sql GetBaseQuery(bool isCount); - - protected abstract string GetBaseWhereClause(); - - protected abstract IEnumerable GetDeleteClauses(); - - protected virtual bool PerformExists(TId id) - { - Sql sql = GetBaseQuery(true); - sql.Where(GetBaseWhereClause(), new { id }); - var count = Database.ExecuteScalar(sql); - return count == 1; - } - - protected virtual int PerformCount(IQuery query) - { - Sql sqlClause = GetBaseQuery(true); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); - - return Database.ExecuteScalar(sql); - } - - protected virtual void PersistDeletedItem(TEntity entity) - { - IEnumerable deletes = GetDeleteClauses(); - foreach (var delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } - - entity.DeleteDate = DateTime.Now; - } + } + + /// + /// Gets the default + /// + protected virtual RepositoryCachePolicyOptions DefaultOptions => _defaultOptions + ??= new RepositoryCachePolicyOptions(() => + { + // get count of all entities of current type (TEntity) to ensure cached result is correct + // create query once if it is needed (no need for locking here) - query is static! + IQuery query = _hasIdQuery ??= AmbientScope.SqlContext.Query().Where(x => x.Id != 0); + return PerformCount(query); + }); + + /// + /// Gets the repository cache policy + /// + protected IRepositoryCachePolicy CachePolicy + { + get + { + if (AppCaches == AppCaches.NoCache) + { + return NoCacheRepositoryCachePolicy.Instance; + } + + // create the cache policy using IsolatedCache which is either global + // or scoped depending on the repository cache mode for the current scope + switch (AmbientScope.RepositoryCacheMode) + { + case RepositoryCacheMode.Default: + case RepositoryCacheMode.Scoped: + // return the same cache policy in both cases - the cache policy is + // supposed to pick either the global or scope cache depending on the + // scope cache mode + return _cachePolicy ??= CreateCachePolicy(); + case RepositoryCacheMode.None: + return NoCacheRepositoryCachePolicy.Instance; + default: + throw new Exception("oops: cache mode."); + } + } + } + + /// + /// Adds or Updates an entity of type TEntity + /// + /// This method is backed by an cache + public virtual void Save(TEntity entity) + { + if (entity.HasIdentity == false) + { + CachePolicy.Create(entity, PersistNewItem); + } + else + { + CachePolicy.Update(entity, PersistUpdatedItem); + } + } + + /// + /// Deletes the passed in entity + /// + public virtual void Delete(TEntity entity) + => CachePolicy.Delete(entity, PersistDeletedItem); + + /// + /// Gets an entity by the passed in Id utilizing the repository's cache policy + /// + public TEntity? Get(TId? id) + => CachePolicy.Get(id, PerformGet, PerformGetAll); + + /// + /// Gets all entities of type TEntity or a list according to the passed in Ids + /// + public IEnumerable GetMany(params TId[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct() + + // don't query by anything that is a default of T (like a zero) + // TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids + // .Where(x => Equals(x, default(TId)) == false) + .ToArray(); + + // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, + // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group + if (ids?.Length <= Constants.Sql.MaxParameterCount) + { + return CachePolicy.GetAll(ids, PerformGetAll); + } + + var entities = new List(); + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + TEntity[] groups = CachePolicy.GetAll(group.ToArray(), PerformGetAll); + entities.AddRange(groups); + } + + return entities; + } + + /// + /// Gets a list of entities by the passed in query + /// + public IEnumerable Get(IQuery query) => + + // ensure we don't include any null refs in the returned collection! + PerformGetByQuery(query) + .WhereNotNull(); + + /// + /// Returns a boolean indicating whether an entity with the passed Id exists + /// + public bool Exists(TId id) + => CachePolicy.Exists(id, PerformExists, PerformGetAll); + + /// + /// Returns an integer with the count of entities found with the passed in query + /// + public int Count(IQuery query) + => PerformCount(query); + + /// + /// Get the entity id for the + /// + protected virtual TId GetEntityId(TEntity entity) + => (TId)(object)entity.Id; + + /// + /// Create the repository cache policy + /// + protected virtual IRepositoryCachePolicy CreateCachePolicy() + => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + + protected abstract TEntity? PerformGet(TId? id); + + protected abstract IEnumerable PerformGetAll(params TId[]? ids); + + protected abstract IEnumerable PerformGetByQuery(IQuery query); + + protected abstract void PersistNewItem(TEntity item); + + protected abstract void PersistUpdatedItem(TEntity item); + + // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); + protected abstract Sql GetBaseQuery(bool isCount); + + protected abstract string GetBaseWhereClause(); + + protected abstract IEnumerable GetDeleteClauses(); + + protected virtual bool PerformExists(TId id) + { + Sql sql = GetBaseQuery(true); + sql.Where(GetBaseWhereClause(), new { id }); + var count = Database.ExecuteScalar(sql); + return count == 1; + } + + protected virtual int PerformCount(IQuery query) + { + Sql sqlClause = GetBaseQuery(true); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database.ExecuteScalar(sql); + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + IEnumerable deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new { id = GetEntityId(entity) }); + } + + entity.DeleteDate = DateTime.Now; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 16933d7e96..e49e2ffda9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,277 +12,295 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginWithKeyRepository { - internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginWithKeyRepository + public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) { - public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } + /// + /// Query for user tokens + /// + /// + /// + public IEnumerable Get(IQuery? query) + { + Sql sqlClause = GetBaseTokenQuery(false); - /// - public void DeleteUserLogins(Guid userOrMemberKey) => Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey }); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - /// - public void Save(Guid userOrMemberKey, IEnumerable logins) + List dtos = Database.Fetch(sql); + + foreach (ExternalLoginTokenDto dto in dtos) { - var sql = Sql() - .Select() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey) - .ForUpdate(); - - // deduplicate the logins - logins = logins.DistinctBy(x => x.ProviderKey + x.LoginProvider).ToList(); - - var toUpdate = new Dictionary(); - var toDelete = new List(); - var toInsert = new List(logins); - - var existingLogins = Database.Fetch(sql); - - foreach (var existing in existingLogins) - { - var found = logins.FirstOrDefault(x => - x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase) - && x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase)); - - if (found != null) - { - toUpdate.Add(existing.Id, found); - // if it's an update then it's not an insert - toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider); - } - else - { - toDelete.Add(existing.Id); - } - } - - // do the deletes, updates and inserts - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } - - foreach (var u in toUpdate) - { - Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key)); - } - - Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i))); + yield return ExternalLoginFactory.BuildEntity(dto); } + } - protected override IIdentityUserLogin? PerformGet(int id) + /// + /// Count for user tokens + /// + /// + /// + public int Count(IQuery query) + { + Sql sql = Sql().SelectCount().From(); + return Database.ExecuteScalar(sql); + } + + /// + public void DeleteUserLogins(Guid userOrMemberKey) => + Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey }); + + /// + public void Save(Guid userOrMemberKey, IEnumerable logins) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey) + .ForUpdate(); + + // deduplicate the logins + logins = logins.DistinctBy(x => x.ProviderKey + x.LoginProvider).ToList(); + + var toUpdate = new Dictionary(); + var toDelete = new List(); + var toInsert = new List(logins); + + List? existingLogins = Database.Fetch(sql); + + foreach (ExternalLoginDto? existing in existingLogins) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id = id }); + IExternalLogin? found = logins.FirstOrDefault(x => + x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase) + && x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase)); - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - if (dto == null) - return null; - - var entity = ExternalLoginFactory.BuildEntity(dto); - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - - return entity; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - if (ids?.Any() ?? false) + if (found != null) { - return PerformGetAllOnIds(ids); + toUpdate.Add(existing.Id, found); + + // if it's an update then it's not an insert + toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider); } - - var sql = GetBaseQuery(false).OrderByDescending(x => x.CreateDate); - - return ConvertFromDtos(Database.Fetch(sql)) - .ToArray();// we don't want to re-iterate again! - } - - private IEnumerable PerformGetAllOnIds(params int[] ids) - { - if (ids.Any() == false) yield break; - foreach (var id in ids) - { - IIdentityUserLogin? identityUserLogin = Get(id); - if (identityUserLogin is not null) - { - yield return identityUserLogin; - } - } - } - - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - foreach (var entity in dtos.Select(ExternalLoginFactory.BuildEntity)) - { - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); - - yield return entity; - } - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - foreach (var dto in dtos) - { - yield return ExternalLoginFactory.BuildEntity(dto); - } - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - if (isCount) - sql.SelectCount(); else - sql.SelectAll(); - sql.From(); - return sql; - } - - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.ExternalLogin}.id = @id"; - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoExternalLogin WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IIdentityUserLogin entity) - { - entity.AddingEntity(); - - var dto = ExternalLoginFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IIdentityUserLogin entity) - { - entity.UpdatingEntity(); - - var dto = ExternalLoginFactory.BuildDto(entity); - - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - /// - /// Query for user tokens - /// - /// - /// - public IEnumerable Get(IQuery? query) - { - Sql sqlClause = GetBaseTokenQuery(false); - - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); - - List dtos = Database.Fetch(sql); - - foreach (ExternalLoginTokenDto dto in dtos) { - yield return ExternalLoginFactory.BuildEntity(dto); + toDelete.Add(existing.Id); } } - /// - /// Count for user tokens - /// - /// - /// - public int Count(IQuery query) + // do the deletes, updates and inserts + if (toDelete.Count > 0) { - Sql sql = Sql().SelectCount().From(); - return Database.ExecuteScalar(sql); + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); } - /// - public void Save(Guid userOrMemberKey, IEnumerable tokens) + foreach (KeyValuePair u in toUpdate) { - // get the existing logins (provider + id) - var existingUserLogins = Database - .Fetch(GetBaseQuery(false).Where(x => x.UserOrMemberKey == userOrMemberKey)) - .ToDictionary(x => x.LoginProvider, x => x.Id); - - // deduplicate the tokens - tokens = tokens.DistinctBy(x => x.LoginProvider + x.Name).ToList(); - - var providers = tokens.Select(x => x.LoginProvider).Distinct().ToList(); - - Sql sql = GetBaseTokenQuery(true) - .WhereIn(x => x.LoginProvider, providers) - .Where(x => x.UserOrMemberKey == userOrMemberKey); - - var toUpdate = new Dictionary(); - var toDelete = new List(); - var toInsert = new List(tokens); - - var existingTokens = Database.Fetch(sql); - - foreach (ExternalLoginTokenDto existing in existingTokens) - { - IExternalLoginToken? found = tokens.FirstOrDefault(x => - x.LoginProvider.InvariantEquals(existing.ExternalLoginDto.LoginProvider) - && x.Name.InvariantEquals(existing.Name)); - - if (found != null) - { - toUpdate.Add(existing.Id, (found, existing.ExternalLoginId)); - // if it's an update then it's not an insert - toInsert.RemoveAll(x => x.LoginProvider.InvariantEquals(found.LoginProvider) && x.Name.InvariantEquals(found.Name)); - } - else - { - toDelete.Add(existing.Id); - } - } - - // do the deletes, updates and inserts - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } - - foreach (KeyValuePair u in toUpdate) - { - Database.Update(ExternalLoginFactory.BuildDto(u.Value.externalLoginId, u.Value.externalLoginToken, u.Key)); - } - - var insertDtos = new List(); - foreach(IExternalLoginToken t in toInsert) - { - if (!existingUserLogins.TryGetValue(t.LoginProvider, out int externalLoginId)) - { - throw new InvalidOperationException($"A token was attempted to be saved for login provider {t.LoginProvider} which is not assigned to this user"); - } - insertDtos.Add(ExternalLoginFactory.BuildDto(externalLoginId, t)); - } - Database.InsertBulk(insertDtos); + Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key)); } - private Sql GetBaseTokenQuery(bool forUpdate) - => forUpdate ? Sql() + Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i))); + } + + /// + public void Save(Guid userOrMemberKey, IEnumerable tokens) + { + // get the existing logins (provider + id) + var existingUserLogins = Database + .Fetch(GetBaseQuery(false) + .Where(x => x.UserOrMemberKey == userOrMemberKey)) + .ToDictionary(x => x.LoginProvider, x => x.Id); + + // deduplicate the tokens + tokens = tokens.DistinctBy(x => x.LoginProvider + x.Name).ToList(); + + var providers = tokens.Select(x => x.LoginProvider).Distinct().ToList(); + + Sql sql = GetBaseTokenQuery(true) + .WhereIn(x => x.LoginProvider, providers) + .Where(x => x.UserOrMemberKey == userOrMemberKey); + + var toUpdate = new Dictionary(); + var toDelete = new List(); + var toInsert = new List(tokens); + + List? existingTokens = Database.Fetch(sql); + + foreach (ExternalLoginTokenDto existing in existingTokens) + { + IExternalLoginToken? found = tokens.FirstOrDefault(x => + x.LoginProvider.InvariantEquals(existing.ExternalLoginDto.LoginProvider) + && x.Name.InvariantEquals(existing.Name)); + + if (found != null) + { + toUpdate.Add(existing.Id, (found, existing.ExternalLoginId)); + + // if it's an update then it's not an insert + toInsert.RemoveAll(x => + x.LoginProvider.InvariantEquals(found.LoginProvider) && x.Name.InvariantEquals(found.Name)); + } + else + { + toDelete.Add(existing.Id); + } + } + + // do the deletes, updates and inserts + if (toDelete.Count > 0) + { + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); + } + + foreach (KeyValuePair u in toUpdate) + { + Database.Update(ExternalLoginFactory.BuildDto(u.Value.externalLoginId, u.Value.externalLoginToken, u.Key)); + } + + var insertDtos = new List(); + foreach (IExternalLoginToken t in toInsert) + { + if (!existingUserLogins.TryGetValue(t.LoginProvider, out var externalLoginId)) + { + throw new InvalidOperationException( + $"A token was attempted to be saved for login provider {t.LoginProvider} which is not assigned to this user"); + } + + insertDtos.Add(ExternalLoginFactory.BuildDto(externalLoginId, t)); + } + + Database.InsertBulk(insertDtos); + } + + protected override IIdentityUserLogin? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + + ExternalLoginDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + if (dto == null) + { + return null; + } + + IIdentityUserLogin entity = ExternalLoginFactory.BuildEntity(dto); + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + + return entity; + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Any() ?? false) + { + return PerformGetAllOnIds(ids); + } + + Sql sql = GetBaseQuery(false).OrderByDescending(x => x.CreateDate); + + return ConvertFromDtos(Database.Fetch(sql)) + .ToArray(); // we don't want to re-iterate again! + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + foreach (ExternalLoginDto? dto in dtos) + { + yield return ExternalLoginFactory.BuildEntity(dto); + } + } + + private IEnumerable PerformGetAllOnIds(params int[] ids) + { + if (ids.Any() == false) + { + yield break; + } + + foreach (var id in ids) + { + IIdentityUserLogin? identityUserLogin = Get(id); + if (identityUserLogin is not null) + { + yield return identityUserLogin; + } + } + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + foreach (IIdentityUserLogin entity in dtos.Select(ExternalLoginFactory.BuildEntity)) + { + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); + + yield return entity; + } + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) + { + sql.SelectCount(); + } + else + { + sql.SelectAll(); + } + + sql.From(); + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.ExternalLogin}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoExternalLogin WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(IIdentityUserLogin entity) + { + entity.AddingEntity(); + + ExternalLoginDto dto = ExternalLoginFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IIdentityUserLogin entity) + { + entity.UpdatingEntity(); + + ExternalLoginDto dto = ExternalLoginFactory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + private Sql GetBaseTokenQuery(bool forUpdate) + => forUpdate + ? Sql() .Select(r => r.Select(x => x.ExternalLoginDto)) .From() .AppendForUpdateHint() // ensure these table values are locked for updates, the ForUpdate ext method does not work here @@ -297,5 +312,4 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .From() .InnerJoin() .On(x => x.ExternalLoginId, x => x.Id); - } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs index 54d0796680..a6e9d517c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.IO; using System.Text; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -7,235 +5,238 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal abstract class FileRepository : IReadRepository, IWriteRepository + where TEntity : IFile { - internal abstract class FileRepository : IReadRepository, IWriteRepository - where TEntity : IFile + protected FileRepository(IFileSystem? fileSystem) => FileSystem = fileSystem; + + protected IFileSystem? FileSystem { get; } + + public virtual void AddFolder(string folderPath) => PersistNewItem(new Folder(folderPath)); + + public virtual void DeleteFolder(string folderPath) => PersistDeletedItem(new Folder(folderPath)); + + public Stream GetFileContentStream(string filepath) { - protected FileRepository(IFileSystem? fileSystem) => FileSystem = fileSystem; - - protected IFileSystem? FileSystem { get; } - - public virtual void AddFolder(string folderPath) => PersistNewItem(new Folder(folderPath)); - - public virtual void DeleteFolder(string folderPath) => PersistDeletedItem(new Folder(folderPath)); - - #region Implementation of IRepository - - public virtual void Save(TEntity entity) + if (FileSystem?.FileExists(filepath) == false) { - if (FileSystem?.FileExists(entity.OriginalPath) == false) + return Stream.Null; + } + + try + { + return FileSystem?.OpenFile(filepath) ?? Stream.Null; + } + catch + { + return Stream.Null; // deal with race conds + } + } + + internal virtual void PersistNewFolder(Folder entity) => FileSystem?.CreateFolder(entity.Path); + + internal virtual void PersistDeletedFolder(Folder entity) => FileSystem?.DeleteDirectory(entity.Path); + + /// + /// Gets a stream that is used to write to the file + /// + /// + /// + protected virtual Stream GetContentStream(string content) => new MemoryStream(Encoding.UTF8.GetBytes(content)); + + /// + /// Returns all files in the file system + /// + /// + /// + /// + /// Returns a list of all files with their paths. For example: + /// \hello.txt + /// \folder1\test.txt + /// \folder1\blah.csv + /// \folder1\folder2\blahhhhh.svg + /// + protected IEnumerable FindAllFiles(string path, string filter) + { + var list = new List(); + IEnumerable? collection = FileSystem?.GetFiles(path, filter); + if (collection is not null) + { + list.AddRange(collection); + } + + IEnumerable? directories = FileSystem?.GetDirectories(path); + if (directories is not null) + { + foreach (var directory in directories) { - PersistNewItem(entity); - } - else - { - PersistUpdatedItem(entity); + list.AddRange(FindAllFiles(directory, filter)); } } - public virtual void Delete(TEntity entity) => PersistDeletedItem(entity); + return list; + } - public abstract TEntity? Get(TId? id); - - public abstract IEnumerable GetMany(params TId[]? ids); - - public virtual bool Exists(TId id) => FileSystem?.FileExists(id!.ToString()!) ?? false; - - #endregion - - #region Implementation of IUnitOfWorkRepository - - public void PersistNewItem(IEntity entity) + protected string? GetFileContent(string? filename) + { + if (filename is null || FileSystem?.FileExists(filename) == false) { - //special case for folder - if (entity is Folder folder) - { - PersistNewFolder(folder); - } - else - { - PersistNewItem((TEntity)entity); - } - } - - public void PersistUpdatedItem(IEntity entity) => PersistUpdatedItem((TEntity)entity); - - public void PersistDeletedItem(IEntity entity) - { - //special case for folder - if (entity is Folder folder) - { - PersistDeletedFolder(folder); - } - else - { - PersistDeletedItem((TEntity)entity); - } - } - - #endregion - - internal virtual void PersistNewFolder(Folder entity) => FileSystem?.CreateFolder(entity.Path); - - internal virtual void PersistDeletedFolder(Folder entity) => FileSystem?.DeleteDirectory(entity.Path); - - #region Abstract IUnitOfWorkRepository Methods - - protected virtual void PersistNewItem(TEntity entity) - { - if (entity.Content is null || FileSystem is null) - { - return; - } - using (Stream stream = GetContentStream(entity.Content)) - { - FileSystem.AddFile(entity.Path, stream, true); - entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; - entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; - //the id can be the hash - entity.Id = entity.Path.GetHashCode(); - entity.Key = entity.Path.EncodeAsGuid(); - entity.VirtualPath = FileSystem?.GetUrl(entity.Path); - } - } - - protected virtual void PersistUpdatedItem(TEntity entity) - { - if (entity.Content is null || FileSystem is null) - { - return; - } - using (Stream stream = GetContentStream(entity.Content)) - { - FileSystem.AddFile(entity.Path, stream, true); - entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; - entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; - //the id can be the hash - entity.Id = entity.Path.GetHashCode(); - entity.Key = entity.Path.EncodeAsGuid(); - entity.VirtualPath = FileSystem.GetUrl(entity.Path); - } - - //now that the file has been written, we need to check if the path had been changed - if (entity.Path.InvariantEquals(entity.OriginalPath) == false) - { - //delete the original file - FileSystem?.DeleteFile(entity.OriginalPath); - //reset the original path on the file - entity.ResetOriginalPath(); - } - } - - protected virtual void PersistDeletedItem(TEntity entity) - { - if (FileSystem?.FileExists(entity.Path) ?? false) - { - FileSystem.DeleteFile(entity.Path); - } - } - - #endregion - - /// - /// Gets a stream that is used to write to the file - /// - /// - /// - protected virtual Stream GetContentStream(string content) => new MemoryStream(Encoding.UTF8.GetBytes(content)); - - /// - /// Returns all files in the file system - /// - /// - /// - /// - /// Returns a list of all files with their paths. For example: - /// - /// \hello.txt - /// \folder1\test.txt - /// \folder1\blah.csv - /// \folder1\folder2\blahhhhh.svg - /// - protected IEnumerable FindAllFiles(string path, string filter) - { - var list = new List(); - var collection = FileSystem?.GetFiles(path, filter); - if (collection is not null) - { - list.AddRange(collection); - } - - IEnumerable? directories = FileSystem?.GetDirectories(path); - if (directories is not null) - { - foreach (var directory in directories) - { - list.AddRange(FindAllFiles(directory, filter)); - } - } - - return list; - } - - protected string? GetFileContent(string? filename) - { - if (filename is null || FileSystem?.FileExists(filename) == false) - { - return null; - } - - try - { - using Stream? stream = FileSystem?.OpenFile(filename!); - if (stream is not null) - { - using var reader = new StreamReader(stream, Encoding.UTF8, true); - return reader.ReadToEnd(); - } - } - catch - { - return null; // deal with race conds - } - return null; } - public Stream GetFileContentStream(string filepath) + try { - if (FileSystem?.FileExists(filepath) == false) + using Stream? stream = FileSystem?.OpenFile(filename); + if (stream is not null) { - return Stream.Null; - } - - try - { - return FileSystem?.OpenFile(filepath) ?? Stream.Null; - } - catch - { - return Stream.Null; // deal with race conds + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); } } - - public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); - - public long GetFileSize(string filename) + catch { - if (FileSystem?.FileExists(filename) == false) - { - return -1; - } + return null; // deal with race conds + } - try - { - return FileSystem!.GetSize(filename); - } - catch - { - return -1; // deal with race conds - } + return null; + } + + public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); + + public long GetFileSize(string filename) + { + if (FileSystem?.FileExists(filename) == false) + { + return -1; + } + + try + { + return FileSystem!.GetSize(filename); + } + catch + { + return -1; // deal with race conds } } + + #region Implementation of IRepository + + public virtual void Save(TEntity entity) + { + if (FileSystem?.FileExists(entity.OriginalPath) == false) + { + PersistNewItem(entity); + } + else + { + PersistUpdatedItem(entity); + } + } + + public virtual void Delete(TEntity entity) => PersistDeletedItem(entity); + + public abstract TEntity? Get(TId? id); + + public abstract IEnumerable GetMany(params TId[]? ids); + + public virtual bool Exists(TId id) => FileSystem?.FileExists(id!.ToString()!) ?? false; + + #endregion + + #region Implementation of IUnitOfWorkRepository + + public void PersistNewItem(IEntity entity) + { + // special case for folder + if (entity is Folder folder) + { + PersistNewFolder(folder); + } + else + { + PersistNewItem((TEntity)entity); + } + } + + public void PersistUpdatedItem(IEntity entity) => PersistUpdatedItem((TEntity)entity); + + public void PersistDeletedItem(IEntity entity) + { + // special case for folder + if (entity is Folder folder) + { + PersistDeletedFolder(folder); + } + else + { + PersistDeletedItem((TEntity)entity); + } + } + + #endregion + + #region Abstract IUnitOfWorkRepository Methods + + protected virtual void PersistNewItem(TEntity entity) + { + if (entity.Content is null || FileSystem is null) + { + return; + } + + using (Stream stream = GetContentStream(entity.Content)) + { + FileSystem.AddFile(entity.Path, stream, true); + entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; + entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; + + // the id can be the hash + entity.Id = entity.Path.GetHashCode(); + entity.Key = entity.Path.EncodeAsGuid(); + entity.VirtualPath = FileSystem?.GetUrl(entity.Path); + } + } + + protected virtual void PersistUpdatedItem(TEntity entity) + { + if (entity.Content is null || FileSystem is null) + { + return; + } + + using (Stream stream = GetContentStream(entity.Content)) + { + FileSystem.AddFile(entity.Path, stream, true); + entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; + entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; + + // the id can be the hash + entity.Id = entity.Path.GetHashCode(); + entity.Key = entity.Path.EncodeAsGuid(); + entity.VirtualPath = FileSystem.GetUrl(entity.Path); + } + + // now that the file has been written, we need to check if the path had been changed + if (entity.Path.InvariantEquals(entity.OriginalPath) == false) + { + // delete the original file + FileSystem?.DeleteFile(entity.OriginalPath); + + // reset the original path on the file + entity.ResetOriginalPath(); + } + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + if (FileSystem?.FileExists(entity.Path) ?? false) + { + FileSystem.DeleteFile(entity.Path); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs index 007e09c4a2..fe7929ccd5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; @@ -10,44 +9,54 @@ public class IdKeyMapRepository : IIdKeyMapRepository { private readonly IScopeAccessor _scopeAccessor; - public IdKeyMapRepository(IScopeAccessor scopeAccessor) - { - _scopeAccessor = scopeAccessor; - } + public IdKeyMapRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; public int? GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType) { - //if it's unknown don't include the nodeObjectType in the query + // if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key}); - } - else - { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", - new { id = key, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key }); } + + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + new + { + id = key, + type = GetNodeObjectTypeGuid(umbracoObjectType), + reservation = Constants.ObjectTypes.IdReservation, + }); } public Guid? GetIdForKey(int id, UmbracoObjectTypes umbracoObjectType) { - //if it's unknown don't include the nodeObjectType in the query + // if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id }); - } - else - { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", - new { id, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id }); } + + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + new + { + id, + type = GetNodeObjectTypeGuid(umbracoObjectType), + reservation = Constants.ObjectTypes.IdReservation, + }); } private Guid GetNodeObjectTypeGuid(UmbracoObjectTypes umbracoObjectType) { - var guid = umbracoObjectType.GetGuid(); + Guid guid = umbracoObjectType.GetGuid(); if (guid == Guid.Empty) + { throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); + } + return guid; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 6fd3da008c..c7259df863 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -12,112 +10,112 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class KeyValueRepository : EntityRepositoryBase, IKeyValueRepository { - internal class KeyValueRepository : EntityRepositoryBase, IKeyValueRepository + public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } - - /// - public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix) - => Get(Query().Where(entity => entity.Identifier!.StartsWith(keyPrefix)))? - .ToDictionary(x => x.Identifier!, x => x.Value); - - #region Overrides of IReadWriteQueryRepository - - public override void Save(IKeyValue entity) - { - if (Get(entity.Identifier) == null) - PersistNewItem(entity); - else - PersistUpdatedItem(entity); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From(); - - return sql; - } - - protected override string GetBaseWhereClause() => Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; - - protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); - - protected override IKeyValue? PerformGet(string? id) - { - var sql = GetBaseQuery(false).Where(x => x.Key == id); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - protected override IEnumerable PerformGetAll(params string[]? ids) - { - var sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); - var dtos = Database.Fetch(sql); - return dtos?.WhereNotNull().Select(Map)!; - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(Map).WhereNotNull(); - } - - protected override void PersistNewItem(IKeyValue entity) - { - var dto = Map(entity); - Database.Insert(dto); - } - - protected override void PersistUpdatedItem(IKeyValue entity) - { - var dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } - } - - private static KeyValueDto? Map(IKeyValue keyValue) - { - if (keyValue == null) return null; - - return new KeyValueDto - { - Key = keyValue.Identifier, - Value = keyValue.Value, - UpdateDate = keyValue.UpdateDate, - }; - } - - private static IKeyValue? Map(KeyValueDto dto) - { - if (dto == null) return null; - - return new KeyValue - { - Identifier = dto.Key, - Value = dto.Value, - UpdateDate = dto.UpdateDate, - }; - } - - #endregion } + + /// + public IReadOnlyDictionary FindByKeyPrefix(string keyPrefix) + => Get(Query().Where(entity => entity.Identifier.StartsWith(keyPrefix))) + .ToDictionary(x => x.Identifier, x => x.Value); + + #region Overrides of IReadWriteQueryRepository + + public override void Save(IKeyValue entity) + { + if (Get(entity.Identifier) == null) + { + PersistNewItem(entity); + } + else + { + PersistUpdatedItem(entity); + } + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + return sql; + } + + protected override string GetBaseWhereClause() => Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; + + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); + + protected override IKeyValue? PerformGet(string? id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Key == id); + KeyValueDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + protected override IEnumerable PerformGetAll(params string[]? ids) + { + Sql sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); + List? dtos = Database.Fetch(sql); + return dtos?.WhereNotNull().Select(Map)!; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(Map).WhereNotNull(); + } + + protected override void PersistNewItem(IKeyValue entity) + { + KeyValueDto? dto = Map(entity); + Database.Insert(dto); + } + + protected override void PersistUpdatedItem(IKeyValue entity) + { + KeyValueDto? dto = Map(entity); + if (dto is not null) + { + Database.Update(dto); + } + } + + private static KeyValueDto? Map(IKeyValue? keyValue) + { + if (keyValue == null) + { + return null; + } + + return new KeyValueDto { Key = keyValue.Identifier, Value = keyValue.Value, UpdateDate = keyValue.UpdateDate }; + } + + private static IKeyValue? Map(KeyValueDto? dto) + { + if (dto == null) + { + return null; + } + + return new KeyValue { Identifier = dto.Key, Value = dto.Value, UpdateDate = dto.UpdateDate }; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs index bf1bc4f4b4..398a55ebaf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; @@ -16,316 +11,350 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class LanguageRepository : EntityRepositoryBase, ILanguageRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class LanguageRepository : EntityRepositoryBase, ILanguageRepository + private readonly Dictionary _codeIdMap = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _idCodeMap = new(); + + public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - private readonly Dictionary _codeIdMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _idCodeMap = new Dictionary(); + } - public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + private FullDataSetRepositoryCachePolicy? TypedCachePolicy => + CachePolicy as FullDataSetRepositoryCachePolicy; - protected override IRepositoryCachePolicy CreateCachePolicy() + public ILanguage? GetByIsoCode(string isoCode) + { + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + TypedCachePolicy.GetAllCached(PerformGetAll); } - private FullDataSetRepositoryCachePolicy? TypedCachePolicy => CachePolicy as FullDataSetRepositoryCachePolicy; + var id = GetIdByIsoCode(isoCode, false); + return id.HasValue ? Get(id.Value) : null; + } - #region Overrides of RepositoryBase - - protected override ILanguage? PerformGet(int id) + // fast way of getting an id for an isoCode - avoiding cloning + // _codeIdMap is rebuilt whenever PerformGetAll runs + public int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true) + { + if (isoCode == null) { - return PerformGetAll(id).FirstOrDefault(); + return null; } - protected override IEnumerable PerformGetAll(params int[]? ids) + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) { - var sql = GetBaseQuery(false).Where(x => x.Id > 0); - if (ids?.Any() ?? false) + TypedCachePolicy.GetAllCached(PerformGetAll); + } + else + { + PerformGetAll(); // We don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap + } + + lock (_codeIdMap) + { + if (_codeIdMap.TryGetValue(isoCode, out var id)) { - sql.WhereIn(x => x.Id, ids); + return id; } + } - //this needs to be sorted since that is the way legacy worked - default language is the first one!! - //even though legacy didn't sort, it should be by id - sql.OrderBy(x => x.Id); + if (throwOnNotFound) + { + throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); + } - // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + return null; + } - // initialize the code-id map - lock (_codeIdMap) + // fast way of getting an isoCode for an id - avoiding cloning + // _idCodeMap is rebuilt whenever PerformGetAll runs + public string? GetIsoCodeById(int? id, bool throwOnNotFound = true) + { + if (id == null) + { + return null; + } + + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) + { + TypedCachePolicy.GetAllCached(PerformGetAll); + } + else + { + PerformGetAll(); + } + + // yes, we want to lock _codeIdMap + lock (_codeIdMap) + { + if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) { - _codeIdMap.Clear(); - _idCodeMap.Clear(); - foreach (var language in languages) - { - _codeIdMap[language.IsoCode] = language.Id; - _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); - } + return isoCode; } - - return languages; } - protected override IEnumerable PerformGetByQuery(IQuery query) + if (throwOnNotFound) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - var dtos = Database.Fetch(sql); - return dtos.Select(ConvertFromDto).ToList(); + throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); } - #endregion + return null; + } - #region Overrides of EntityRepositoryBase + public string GetDefaultIsoCode() => GetDefault().IsoCode; - protected override Sql GetBaseQuery(bool isCount) + public int? GetDefaultId() => GetDefault().Id; + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + + protected ILanguage ConvertFromDto(LanguageDto dto) + => LanguageFactory.BuildEntity(dto); + + // do NOT leak that language, it's not deep-cloned! + private ILanguage GetDefault() + { + // get all cached + var languages = + (TypedCachePolicy + ?.GetAllCached( + PerformGetAll) // Try to get all cached non-cloned if using the correct cache policy (not the case in unit tests) + ?? CachePolicy.GetAll(Array.Empty(), PerformGetAll)).ToList(); + + ILanguage? language = languages.FirstOrDefault(x => x.IsDefault); + if (language != null) { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql.From(); - - return sql; + return language; } - protected override string GetBaseWhereClause() + // this is an anomaly, the service/repo should ensure it cannot happen + Logger.LogWarning( + "There is no default language. Fix this anomaly by editing the language table in database and setting one language as the default language."); + + // still, don't kill the site, and return "something" + ILanguage? first = null; + foreach (ILanguage l in languages) { - return $"{Constants.DatabaseSchema.Tables.Language}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - //NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) - // but we still need to remove them - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Language + " WHERE id = @id" - }; - return list; - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(ILanguage entity) - { - // validate iso code and culture name - if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - - entity.AddingEntity(); - - // deal with entity becoming the new default entity - if (entity.IsDefault) + if (first == null || l.Id < first.Id) { - // set all other entities to non-default - // safe (no race cond) because the service locks languages - var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefault, false)); - Database.Execute(setAllDefaultToFalse); + first = l; } - - // fallback cycles are detected at service level - - // insert - var dto = LanguageFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(ILanguage entity) + return first!; + } + + #region Overrides of RepositoryBase + + protected override ILanguage? PerformGet(int id) => PerformGetAll(id).FirstOrDefault(); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id > 0); + if (ids?.Any() ?? false) { - // validate iso code and culture name - if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - - entity.UpdatingEntity(); - - if (entity.IsDefault) - { - // deal with entity becoming the new default entity - - // set all other entities to non-default - // safe (no race cond) because the service locks languages - var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefault, false)); - Database.Execute(setAllDefaultToFalse); - } - else - { - // deal with the entity not being default anymore - // which is illegal - another entity has to become default - var selectDefaultId = Sql() - .Select(x => x.Id) - .From() - .Where(x => x.IsDefault); - - var defaultId = Database.ExecuteScalar(selectDefaultId); - if (entity.Id == defaultId) - throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); - } - - if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) - { - //if the iso code is changing, ensure there's not another lang with the same code already assigned - var sameCode = Sql() - .SelectCount() - .From() - .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); - - var countOfSameCode = Database.ExecuteScalar(sameCode); - if (countOfSameCode > 0) - throw new InvalidOperationException($"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); - } - - // fallback cycles are detected at service level - - // update - var dto = LanguageFactory.BuildDto(entity); - Database.Update(dto); - entity.ResetDirtyProperties(); + sql.WhereIn(x => x.Id, ids); } - protected override void PersistDeletedItem(ILanguage entity) + // this needs to be sorted since that is the way legacy worked - default language is the first one!! + // even though legacy didn't sort, it should be by id + sql.OrderBy(x => x.Id); + + // get languages + var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + + // initialize the code-id map + lock (_codeIdMap) { - // validate that the entity is not the default language. + _codeIdMap.Clear(); + _idCodeMap.Clear(); + foreach (ILanguage language in languages) + { + _codeIdMap[language.IsoCode] = language.Id; + _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); + } + } + + return languages; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + List? dtos = Database.Fetch(sql); + return dtos.Select(ConvertFromDto).ToList(); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql.From(); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Language}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + // NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) + // but we still need to remove them + "DELETE FROM " + Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Language + " WHERE id = @id", + }; + return list; + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(ILanguage entity) + { + // validate iso code and culture name + if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); + } + + entity.AddingEntity(); + + // deal with entity becoming the new default entity + if (entity.IsDefault) + { + // set all other entities to non-default // safe (no race cond) because the service locks languages + Sql setAllDefaultToFalse = Sql() + .Update(u => u.Set(x => x.IsDefault, false)); + Database.Execute(setAllDefaultToFalse); + } - var selectDefaultId = Sql() + // fallback cycles are detected at service level + + // insert + LanguageDto dto = LanguageFactory.BuildDto(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(ILanguage entity) + { + // validate iso code and culture name + if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); + } + + entity.UpdatingEntity(); + + if (entity.IsDefault) + { + // deal with entity becoming the new default entity + + // set all other entities to non-default + // safe (no race cond) because the service locks languages + Sql setAllDefaultToFalse = Sql() + .Update(u => u.Set(x => x.IsDefault, false)); + Database.Execute(setAllDefaultToFalse); + } + else + { + // deal with the entity not being default anymore + // which is illegal - another entity has to become default + Sql selectDefaultId = Sql() .Select(x => x.Id) .From() .Where(x => x.IsDefault); var defaultId = Database.ExecuteScalar(selectDefaultId); if (entity.Id == defaultId) - throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); - - // We need to remove any references to the language if it's being used as a fall-back from other ones - var clearFallbackLanguage = Sql() - .Update(u => u - .Set(x => x.FallbackLanguageId, null)) - .Where(x => x.FallbackLanguageId == entity.Id); - - Database.Execute(clearFallbackLanguage); - - // delete - base.PersistDeletedItem(entity); - } - - #endregion - - protected ILanguage ConvertFromDto(LanguageDto dto) - => LanguageFactory.BuildEntity(dto); - - public ILanguage? GetByIsoCode(string isoCode) - { - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) { - TypedCachePolicy.GetAllCached(PerformGetAll); + throw new InvalidOperationException( + $"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); } - - var id = GetIdByIsoCode(isoCode, throwOnNotFound: false); - return id.HasValue ? Get(id.Value) : null; } - // fast way of getting an id for an isoCode - avoiding cloning - // _codeIdMap is rebuilt whenever PerformGetAll runs - public int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true) + if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) { - if (isoCode == null) return null; + // If the iso code is changing, ensure there's not another lang with the same code already assigned + Sql sameCode = Sql() + .SelectCount() + .From() + .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) - TypedCachePolicy.GetAllCached(PerformGetAll); - else - PerformGetAll(); //we don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap - - lock (_codeIdMap) + var countOfSameCode = Database.ExecuteScalar(sameCode); + if (countOfSameCode > 0) { - if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; + throw new InvalidOperationException( + $"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); } - if (throwOnNotFound) - throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); - - return null; } - // fast way of getting an isoCode for an id - avoiding cloning - // _idCodeMap is rebuilt whenever PerformGetAll runs - public string? GetIsoCodeById(int? id, bool throwOnNotFound = true) - { - if (id == null) return null; + // fallback cycles are detected at service level - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) - TypedCachePolicy.GetAllCached(PerformGetAll); - else - PerformGetAll(); - - lock (_codeIdMap) // yes, we want to lock _codeIdMap - { - if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) return isoCode; - } - if (throwOnNotFound) - throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); - - return null; - } - - public string GetDefaultIsoCode() - { - return GetDefault().IsoCode; - } - - public int? GetDefaultId() - { - return GetDefault()?.Id; - } - - // do NOT leak that language, it's not deep-cloned! - private ILanguage GetDefault() - { - // get all cached - var languages = (TypedCachePolicy?.GetAllCached(PerformGetAll) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests) - ?? CachePolicy.GetAll(Array.Empty(), PerformGetAll)!).ToList(); - - var language = languages.FirstOrDefault(x => x.IsDefault); - if (language != null) return language; - - // this is an anomaly, the service/repo should ensure it cannot happen - Logger.LogWarning("There is no default language. Fix this anomaly by editing the language table in database and setting one language as the default language."); - - // still, don't kill the site, and return "something" - - ILanguage? first = null; - foreach (var l in languages) - { - if (first == null || l.Id < first.Id) - first = l; - } - - return first!; - } + // update + LanguageDto dto = LanguageFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); } + + protected override void PersistDeletedItem(ILanguage entity) + { + // validate that the entity is not the default language. + // safe (no race cond) because the service locks languages + Sql selectDefaultId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + var defaultId = Database.ExecuteScalar(selectDefaultId); + if (entity.Id == defaultId) + { + throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); + } + + // We need to remove any references to the language if it's being used as a fall-back from other ones + Sql clearFallbackLanguage = Sql() + .Update(u => u + .Set(x => x.FallbackLanguageId, null)) + .Where(x => x.FallbackLanguageId == entity.Id); + + Database.Execute(clearFallbackLanguage); + + // delete + base.PersistDeletedItem(entity); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs index 72324eb874..2caad816fb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs @@ -1,14 +1,17 @@ -using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal static class LanguageRepositoryExtensions { - internal static class LanguageRepositoryExtensions + public static bool IsDefault(this ILanguageRepository repo, string? culture) { - public static bool IsDefault(this ILanguageRepository repo, string culture) + if (culture == null || culture == "*") { - if (culture == null || culture == "*") return false; - return repo.GetDefaultIsoCode().InvariantEquals(culture); + return false; } + + return repo.GetDefaultIsoCode().InvariantEquals(culture); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs index bfecc66765..a00c35de6d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -12,124 +10,116 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository { - internal class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository + public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() + public ILogViewerQuery? GetByName(string name) => + + // use the underlying GetAll which will force cache all log queries + GetMany().FirstOrDefault(x => x.Name == name); + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql? sql = GetBaseQuery(false).Where($"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id > 0"); + if (ids?.Any() ?? false) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + sql.Where($"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id in (@ids)", new { ids }); } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id > 0"); - if (ids?.Any() ?? false) - { - sql.Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id in (@ids)", new { ids = ids }); - } + return Database.Fetch(sql).Select(ConvertFromDto); + } - return Database.Fetch(sql).Select(ConvertFromDto); + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method"); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { $"DELETE FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(ILogViewerQuery entity) + { + var exists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name", + new { name = entity.Name }); + if (exists > 0) + { + throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); } - protected override IEnumerable PerformGetByQuery(IQuery query) + entity.AddingEntity(); + + var factory = new LogViewerQueryModelFactory(); + LogViewerQueryDto dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + } + + protected override void PersistUpdatedItem(ILogViewerQuery entity) + { + entity.UpdatingEntity(); + + var exists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name AND id <> @id", + new { name = entity.Name, id = entity.Id }); + + // ensure there is no other log query with the same name on another entity + if (exists > 0) { - throw new NotSupportedException("This repository does not support this method"); + throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); } - protected override Sql GetBaseQuery(bool isCount) + var factory = new LogViewerQueryModelFactory(); + LogViewerQueryDto dto = factory.BuildDto(entity); + + Database.Update(dto); + } + + protected override ILogViewerQuery? PerformGet(int id) => + + // use the underlying GetAll which will force cache all log queries + GetMany().FirstOrDefault(x => x.Id == id); + + private ILogViewerQuery ConvertFromDto(LogViewerQueryDto dto) + { + var factory = new LogViewerQueryModelFactory(); + ILogViewerQuery entity = factory.BuildEntity(dto); + return entity; + } + + internal class LogViewerQueryModelFactory + { + public ILogViewerQuery BuildEntity(LogViewerQueryDto dto) { - var sql = Sql(); - sql = isCount ? sql.SelectCount() : sql.Select(); - sql = sql.From(); - return sql; + var logViewerQuery = new LogViewerQuery(dto.Name, dto.Query) { Id = dto.Id }; + return logViewerQuery; } - protected override string GetBaseWhereClause() + public LogViewerQueryDto BuildDto(ILogViewerQuery entity) { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - $"DELETE FROM {Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(ILogViewerQuery entity) - { - var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name", - new { name = entity.Name }); - if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); - - entity.AddingEntity(); - - var factory = new LogViewerQueryModelFactory(); - var dto = factory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - } - - protected override void PersistUpdatedItem(ILogViewerQuery entity) - { - entity.UpdatingEntity(); - - var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name AND id <> @id", - new { name = entity.Name, id = entity.Id }); - //ensure there is no other log query with the same name on another entity - if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); - - - var factory = new LogViewerQueryModelFactory(); - var dto = factory.BuildDto(entity); - - Database.Update(dto); - } - - private ILogViewerQuery ConvertFromDto(LogViewerQueryDto dto) - { - var factory = new LogViewerQueryModelFactory(); - var entity = factory.BuildEntity(dto); - return entity; - } - - internal class LogViewerQueryModelFactory - { - - public ILogViewerQuery BuildEntity(LogViewerQueryDto dto) - { - var logViewerQuery = new LogViewerQuery(dto.Name, dto.Query) - { - Id = dto.Id, - }; - return logViewerQuery; - } - - public LogViewerQueryDto BuildDto(ILogViewerQuery entity) - { - var dto = new LogViewerQueryDto { Name = entity.Name, Query = entity.Query, Id = entity.Id }; - return dto; - } - } - - protected override ILogViewerQuery? PerformGet(int id) - { - //use the underlying GetAll which will force cache all log queries - return GetMany()?.FirstOrDefault(x => x.Id == id); - } - - public ILogViewerQuery? GetByName(string name) - { - //use the underlying GetAll which will force cache all log queries - return GetMany()?.FirstOrDefault(x => x.Name == name); + var dto = new LogViewerQueryDto { Name = entity.Name, Query = entity.Query, Id = entity.Id }; + return dto; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs index 158906df20..67fe818358 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -16,250 +13,254 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MacroRepository : EntityRepositoryBase, IMacroRepository { - internal class MacroRepository : EntityRepositoryBase, IMacroRepository + private readonly IRepositoryCachePolicy _macroByAliasCachePolicy; + private readonly IShortStringHelper _shortStringHelper; + + public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger) { - private readonly IShortStringHelper _shortStringHelper; - private readonly IRepositoryCachePolicy _macroByAliasCachePolicy; + _shortStringHelper = shortStringHelper; + _macroByAliasCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + } - public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger) + public IMacro? Get(Guid id) + { + Sql sql = GetBaseQuery().Where(x => x.UniqueId == id); + return GetBySql(sql); + } + + public IEnumerable GetMany(params Guid[]? ids) => + ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); + + public bool Exists(Guid id) => Get(id) != null; + + public IMacro? GetByAlias(string alias) => + _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias); + + public IEnumerable GetAllByAlias(string[] aliases) + { + if (aliases.Any() is false) { - _shortStringHelper = shortStringHelper; - _macroByAliasCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + return base.GetMany(); } - protected override IMacro? PerformGet(int id) + return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias); + } + + protected override IMacro? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + return GetBySql(sql); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) => + ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .Select(x => Get(x.Id)!); + } + + private IMacro? GetBySql(Sql sql) + { + MacroDto? macroDto = Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .FirstOrDefault(); + + if (macroDto == null) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id }); - return GetBySql(sql); + return null; } - public IMacro? Get(Guid id) + IMacro entity = MacroFactory.BuildEntity(_shortStringHelper, macroDto); + + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); + + return entity; + } + + private IMacro? PerformGetByAlias(string? alias) + { + IQuery query = Query().Where(x => x.Alias.Equals(alias)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByAlias(params string[]? aliases) + { + if (aliases is null || aliases.Any() is false) { - var sql = GetBaseQuery().Where(x => x.UniqueId == id); - return GetBySql(sql); + return base.GetMany(); } - private IMacro? GetBySql(Sql sql) + IQuery query = Query().Where(x => aliases.Contains(x.Alias)); + return PerformGetByQuery(query); + } + + private IEnumerable GetAllNoIds() + { + Sql sql = GetBaseQuery(false) + + // must be sorted this way for the relator to work + .OrderBy(x => x.Id); + + return Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .Transform(ConvertFromDtos) + .ToArray(); // do it now and once + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + foreach (IMacro entity in dtos.Select(x => MacroFactory.BuildEntity(_shortStringHelper, x))) { - var macroDto = Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .FirstOrDefault(); - - if (macroDto == null) - return null; - - var entity = MacroFactory.BuildEntity(_shortStringHelper, macroDto); - // reset dirty initial properties (U4-1946) ((BeingDirtyBase)entity).ResetDirtyProperties(false); - return entity; - } - - public IEnumerable GetMany(params Guid[]? ids) - { - return ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); - } - - public bool Exists(Guid id) - { - return Get(id) != null; - } - - public IMacro? GetByAlias(string alias) - { - return _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias); - } - - public IEnumerable GetAllByAlias(string[] aliases) - { - if (aliases.Any() is false) - { - return base.GetMany(); - } - - return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias); - } - - private IMacro? PerformGetByAlias(string? alias) - { - var query = Query().Where(x => x.Alias.Equals(alias)); - return PerformGetByQuery(query)?.FirstOrDefault(); - } - - private IEnumerable PerformGetAllByAlias(params string[]? aliases) - { - if (aliases is null || aliases.Any() is false) - { - return base.GetMany(); - } - - var query = Query().Where(x => aliases.Contains(x.Alias)); - return PerformGetByQuery(query); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - return ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); - } - - private IEnumerable GetAllNoIds() - { - var sql = GetBaseQuery(false) - //must be sorted this way for the relator to work - .OrderBy(x => x.Id); - - return Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .Transform(ConvertFromDtos) - .ToArray(); // do it now and once - } - - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - - foreach (var entity in dtos.Select(x => MacroFactory.BuildEntity(_shortStringHelper, x))) - { - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); - - yield return entity; - } - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .Select(x => Get(x.Id)!); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return isCount ? Sql().SelectCount().From() : GetBaseQuery(); - } - - private Sql GetBaseQuery() - { - return Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.Macro); - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Macro}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsMacroProperty WHERE macro = @id", - "DELETE FROM cmsMacro WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IMacro entity) - { - entity.AddingEntity(); - - var dto = MacroFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - if (dto.MacroPropertyDtos is not null) - { - foreach (var propDto in dto.MacroPropertyDtos) - { - //need to set the id explicitly here - propDto.Macro = id; - var propId = Convert.ToInt32(Database.Insert(propDto)); - entity.Properties[propDto.Alias].Id = propId; - } - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMacro entity) - { - entity.UpdatingEntity(); - var dto = MacroFactory.BuildDto(entity); - - Database.Update(dto); - - //update the properties if they've changed - var macro = (Macro)entity; - if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) - { - var ids = dto.MacroPropertyDtos?.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); - if (ids?.Length > 0) - Database.Delete("WHERE macro=@macro AND id NOT IN (@ids)", new { macro = dto.Id, ids }); - else - Database.Delete("WHERE macro=@macro", new { macro = dto.Id }); - - // detect new aliases, replace with temp aliases - // this ensures that we don't have collisions, ever - var aliases = new Dictionary(); - if (dto.MacroPropertyDtos is null) - { - return; - } - foreach (var propDto in dto.MacroPropertyDtos) - { - var prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); - if (prop == null) throw new Exception("oops: property."); - if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) - { - var tempAlias = Guid.NewGuid().ToString("N").Substring(0, 8); - aliases[tempAlias] = propDto.Alias; - propDto.Alias = tempAlias; - } - } - - // insert or update existing properties, with temp aliases - foreach (var propDto in dto.MacroPropertyDtos) - { - if (propDto.Id == 0) - { - // insert - propDto.Id = Convert.ToInt32(Database.Insert(propDto)); - macro.Properties[aliases[propDto.Alias]].Id = propDto.Id; - } - else - { - // update - var property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); - if (property == null) throw new Exception("oops: property."); - if (property.IsDirty()) - Database.Update(propDto); - } - } - - // replace the temp aliases with the real ones - foreach (var propDto in dto.MacroPropertyDtos) - { - if (aliases.ContainsKey(propDto.Alias) == false) continue; - - propDto.Alias = aliases[propDto.Alias]; - Database.Update(propDto); - } - } - - entity.ResetDirtyProperties(); + yield return entity; } } + + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Macro}.id = @id"; + + private Sql GetBaseQuery() => + Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.Macro); + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsMacroProperty WHERE macro = @id", "DELETE FROM cmsMacro WHERE id = @id", + }; + return list; + } + + protected override void PersistNewItem(IMacro entity) + { + entity.AddingEntity(); + + MacroDto dto = MacroFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + if (dto.MacroPropertyDtos is not null) + { + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + // need to set the id explicitly here + propDto.Macro = id; + var propId = Convert.ToInt32(Database.Insert(propDto)); + entity.Properties[propDto.Alias].Id = propId; + } + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMacro entity) + { + entity.UpdatingEntity(); + MacroDto dto = MacroFactory.BuildDto(entity); + + Database.Update(dto); + + // update the properties if they've changed + var macro = (Macro)entity; + if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) + { + var ids = dto.MacroPropertyDtos?.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); + if (ids?.Length > 0) + { + Database.Delete("WHERE macro=@macro AND id NOT IN (@ids)", new { macro = dto.Id, ids }); + } + else + { + Database.Delete("WHERE macro=@macro", new { macro = dto.Id }); + } + + // detect new aliases, replace with temp aliases + // this ensures that we don't have collisions, ever + var aliases = new Dictionary(); + if (dto.MacroPropertyDtos is null) + { + return; + } + + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + IMacroProperty? prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); + if (prop == null) + { + throw new Exception("oops: property."); + } + + if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) + { + var tempAlias = Guid.NewGuid().ToString("N")[..8]; + aliases[tempAlias] = propDto.Alias; + propDto.Alias = tempAlias; + } + } + + // insert or update existing properties, with temp aliases + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + if (propDto.Id == 0) + { + // insert + propDto.Id = Convert.ToInt32(Database.Insert(propDto)); + macro.Properties[aliases[propDto.Alias]].Id = propDto.Id; + } + else + { + // update + IMacroProperty? property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); + if (property == null) + { + throw new Exception("oops: property."); + } + + if (property.IsDirty()) + { + Database.Update(propDto); + } + } + } + + // replace the temp aliases with the real ones + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + if (aliases.ContainsKey(propDto.Alias) == false) + { + continue; + } + + propDto.Alias = aliases[propDto.Alias]; + Database.Update(propDto); + } + } + + entity.ResetDirtyProperties(); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index c763b3481a..73cb423837 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -21,564 +19,557 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +public class MediaRepository : ContentRepositoryBase, IMediaRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - public class MediaRepository : ContentRepositoryBase, IMediaRepository + private readonly AppCaches _cache; + private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; + private readonly IMediaTypeRepository _mediaTypeRepository; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IJsonSerializer _serializer; + private readonly ITagRepository _tagRepository; + + public MediaRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + ILoggerFactory loggerFactory, + IMediaTypeRepository mediaTypeRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditorCollection, + MediaUrlGeneratorCollection mediaUrlGenerators, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditorCollection, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly AppCaches _cache; - private readonly IMediaTypeRepository _mediaTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IJsonSerializer _serializer; - private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; + _cache = cache; + _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _mediaUrlGenerators = mediaUrlGenerators; + _serializer = serializer; + _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, + loggerFactory.CreateLogger()); + } - public MediaRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - ILogger logger, - ILoggerFactory loggerFactory, - IMediaTypeRepository mediaTypeRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditorCollection, - MediaUrlGeneratorCollection mediaUrlGenerators, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories, dataTypeService, eventAggregator) + protected override MediaRepository This => this; + + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) + { + Sql? filterSql = null; + + if (filter != null) { - _cache = cache; - _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _mediaUrlGenerators = mediaUrlGenerators; - _serializer = serializer; - _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, loggerFactory.CreateLogger()); - } - - protected override MediaRepository This => this; - - - #region Repository Base - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Media; - - protected override IMedia? PerformGet(int id) - { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); - - return MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(QueryType.Many); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - sql - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(sql)); - } - - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType); - } - - protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, bool joinMediaVersion = false) - { - var sql = SqlContext.Sql(); - - switch (queryType) + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - sql = sql.Select(r => + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } + + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Core.Models.Media[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) + { + ContentDto dto = dtos[i]; + + if (withCache) + { + // if the cache contains the (proper version of the) item, use it + IMedia? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) + { + content[i] = (Core.Models.Media)cached; + continue; + } + } + + // else, need to build it + + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMediaType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _mediaTypeRepository.Get(contentTypeId); + } + + Core.Models.Media c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); + } + + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); + + // assign properties + foreach (TempContent temp in temps) + { + if (temp.Content is not null) + { + temp.Content.Properties = properties[temp.VersionId]; + + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); + } + } + + return content; + } + + private IMedia MapDtoToContent(ContentDto dto) + { + IMediaType? contentType = _mediaTypeRepository.Get(dto.ContentTypeId); + Core.Models.Media media = ContentBaseFactory.BuildEntity(dto, contentType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.NodeId, versionId, 0, contentType); + IDictionary properties = + GetPropertyCollections(new List> { temp }); + media.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + media.ResetDirtyProperties(false); + return media; + } + + #region Repository Base + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Media; + + protected override IMedia? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + sql + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType); + + protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, bool joinMediaVersion = false) + { + Sql sql = SqlContext.Sql(); + + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + sql = sql.Select(r => r.Select(x => x.NodeDto) - .Select(x => x.ContentVersionDto)) + .Select(x => x.ContentVersionDto)) - // ContentRepositoryBase expects a variantName field to order by name - // for now, just return the plain invariant node name - .AndSelect(x => Alias(x.Text, "variantName")); - break; - } - - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId); - - if (joinMediaVersion) - sql.InnerJoin().On((left, right) => left.Id == right.Id); - - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); - - if (current) - sql.Where(x => x.Current); // always get the current version - - return sql; + // ContentRepositoryBase expects a variantName field to order by name + // for now, just return the plain invariant node name + .AndSelect(x => Alias(x.Text, "variantName")); + break; } - protected override Sql GetBaseQuery(bool isCount) + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId); + + if (joinMediaVersion) { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + sql.InnerJoin() + .On((left, right) => left.Id == right.Id); } - // ah maybe not, that what's used for eg Exists in base repo - protected override string GetBaseWhereClause() + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + + if (current) { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; + sql.Where(x => x.Current); // always get the current version } - protected override IEnumerable GetDeleteClauses() + return sql; + } + + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + + // ah maybe not, that what's used for eg Exists in base repo + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - var list = new List - { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" - }; - return list; + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + + " SET startContentId = NULL WHERE startContentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id", + }; + return list; + } + + #endregion + + #region Versions + + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + return MapDtosToContent(Database.Fetch(sql), true); + } + + public override IMedia? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.Id == versionId); + + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } + + public IMedia? GetMediaByPath(string mediaPath) + { + var umbracoFileValue = mediaPath; + const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; + var isResized = Regex.IsMatch(mediaPath, pattern); + + // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" URL. + if (isResized) + { + var underscoreIndex = mediaPath.LastIndexOf('_'); + var dotIndex = mediaPath.LastIndexOf('.'); + umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); } - #endregion + Sql sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) + .Where(x => x.Path == umbracoFileValue) + .SelectTop(1); - #region Versions + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } - public override IEnumerable GetAllVersions(int nodeId) + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @versionId", new { versionId }); + Database.Delete("WHERE versionId = @versionId", new { versionId }); + } + + #endregion + + #region Persist + + protected override void PersistNewItem(IMedia entity) + { + entity.AddingEntity(); + + // ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name)!; + + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // create the dto + MediaDto dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); + + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; + + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + + // persist the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; + + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) { - var sql = GetBaseQuery(QueryType.Many, current: false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - return MapDtosToContent(Database.Fetch(sql), true); + nodeDto.NodeId = id; + } + else + { + Database.Insert(nodeDto); } - public override IMedia? GetVersion(int versionId) + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // persist the content dto + ContentDto contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + // persist the content version dto + // assumes a new version id and version date (modified date) has been set + ContentVersionDto contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = true; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the media version dto + MediaVersionDto mediaVersionDto = dto.MediaVersionDto; + mediaVersionDto.Id = entity.VersionId; + Database.Insert(mediaVersionDto); + + // persist the property data + InsertPropertyValues(entity, 0, out _, out _); + + // set tags + SetEntityTags(entity, _tagRepository, _serializer); + + PersistRelations(entity); + + OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMedia entity) + { + // update + entity.UpdatingEntity(); + + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + + if (!isMoving) { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.Id == versionId); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); - } - - public IMedia? GetMediaByPath(string mediaPath) - { - var umbracoFileValue = mediaPath; - const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; - var isResized = Regex.IsMatch(mediaPath, pattern); - - // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" URL. - if (isResized) - { - var underscoreIndex = mediaPath.LastIndexOf('_'); - var dotIndex = mediaPath.LastIndexOf('.'); - umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); - } - - var sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) - .Where(x => x.Path == umbracoFileValue) - .SelectTop(1); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); - } - - protected override void PerformDeleteVersion(int id, int versionId) - { - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE versionId = @versionId", new { versionId }); - } - - #endregion - - #region Persist - - protected override void PersistNewItem(IMedia entity) - { - entity.AddingEntity(); - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name)!; + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id)!; // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? entity.SanitizeEntityPropertiesForXmlStorage(); - // create the dto - var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty(nameof(entity.ParentId))) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); - // derive path and level from parent - var parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } + } - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // create the dto + MediaDto dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); - // persist the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; + // update the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - nodeDto.NodeId = id; - else - Database.Insert(nodeDto); + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - // assumes a new version id and version date (modified date) has been set - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; + // update the content & media version dtos + ContentVersionDto contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + MediaVersionDto mediaVersionDto = dto.MediaVersionDto; contentVersionDto.Current = true; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; + Database.Update(contentVersionDto); + Database.Update(mediaVersionDto); - // persist the media version dto - var mediaVersionDto = dto.MediaVersionDto; - mediaVersionDto.Id = entity.VersionId; - Database.Insert(mediaVersionDto); + // replace the property data + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - // persist the property data - InsertPropertyValues(entity, 0, out _, out _); - - // set tags SetEntityTags(entity, _tagRepository, _serializer); PersistRelations(entity); - - OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(IMedia entity) + OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); + } + + protected override void PersistDeletedItem(IMedia entity) + { + // Raise event first else potential FK issues + OnUowRemovingEntity(entity); + base.PersistDeletedItem(entity); + } + + #endregion + + #region Recycle Bin + + public override int RecycleBinId => Constants.System.RecycleBinMedia; + + public bool RecycleBinSmells() + { + IAppPolicyCache cache = _cache.RuntimeCache; + var cacheKey = CacheKeys.MediaRecycleBinCacheKey; + + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } + + #endregion + + #region Read Repository implementation for Guid keys + + public IMedia? Get(Guid id) => _mediaByGuidReadRepository.Get(id); + + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + _mediaByGuidReadRepository.GetMany(ids); + + public bool Exists(Guid id) => _mediaByGuidReadRepository.Exists(id); + + // A reading repository purely for looking up by GUID + // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this + private class MediaByGuidReadRepository : EntityRepositoryBase + { + private readonly MediaRepository _outerRepo; + + public MediaByGuidReadRepository(MediaRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _outerRepo = outerRepo; + + protected override IMedia? PerformGet(Guid id) { - // update - entity.UpdatingEntity(); + Sql sql = _outerRepo.GetBaseQuery(QueryType.Single) + .Where(x => x.UniqueId == id); - // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. - // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.IsMoving(); + ContentDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - if (!isMoving) + if (dto == null) { - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id)!; - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty(nameof(entity.ParentId))) - { - var parent = GetParentNodeDto(entity.ParentId); - - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } + return null; } - // create the dto - var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); - - // update the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - if (!isMoving) - { - // update the content dto - Database.Update(dto.ContentDto); - - // update the content & media version dtos - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - var mediaVersionDto = dto.MediaVersionDto; - contentVersionDto.Current = true; - Database.Update(contentVersionDto); - Database.Update(mediaVersionDto); - - // replace the property data - ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - - SetEntityTags(entity, _tagRepository, _serializer); - - PersistRelations(entity); - } - - OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); - } - - protected override void PersistDeletedItem(IMedia entity) - { - // Raise event first else potential FK issues - OnUowRemovingEntity(entity); - base.PersistDeletedItem(entity); - } - - #endregion - - #region Recycle Bin - - public override int RecycleBinId => Cms.Core.Constants.System.RecycleBinMedia; - - public bool RecycleBinSmells() - { - var cache = _cache.RuntimeCache; - var cacheKey = CacheKeys.MediaRecycleBinCacheKey; - - // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); - } - - #endregion - - #region Read Repository implementation for Guid keys - - public IMedia? Get(Guid id) - { - return _mediaByGuidReadRepository.Get(id); - } - - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return _mediaByGuidReadRepository.GetMany(ids); - } - - public bool Exists(Guid id) - { - return _mediaByGuidReadRepository.Exists(id); - } - - - // A reading repository purely for looking up by GUID - // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this - private class MediaByGuidReadRepository : EntityRepositoryBase - { - private readonly MediaRepository _outerRepo; - - public MediaByGuidReadRepository(MediaRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _outerRepo = outerRepo; - } - - protected override IMedia? PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Single) - .Where(x => x.UniqueId == id); - - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = _outerRepo.MapDtoToContent(dto); - - return content; - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Many); - if (ids?.Length > 0) - sql.WhereIn(x => x.UniqueId, ids); - - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistNewItem(IMedia entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistUpdatedItem(IMedia entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } - } - - #endregion - - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - Sql? filterSql = null; - - if (filter != null) - { - filterSql = Sql(); - foreach (var clause in filter.GetWhereClauses()) - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Core.Models.Media[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Core.Models.Media)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out IMediaType? contentType) == false) - contentTypes[contentTypeId] = contentType = _mediaTypeRepository.Get(contentTypeId); - - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } - - // load all properties for all documents from database in 1 query - indexed by version id - var properties = GetPropertyCollections(temps); - - // assign properties - foreach (var temp in temps) - { - if (temp.Content is not null) - { - temp.Content.Properties = properties[temp.VersionId]; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } - } + IMedia content = _outerRepo.MapDtoToContent(dto); return content; } - private IMedia MapDtoToContent(ContentDto dto) + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - var contentType = _mediaTypeRepository.Get(dto.ContentTypeId); - var media = ContentBaseFactory.BuildEntity(dto, contentType); + Sql sql = _outerRepo.GetBaseQuery(QueryType.Many); + if (ids?.Length > 0) + { + sql.WhereIn(x => x.UniqueId, ids); + } - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.NodeId, versionId, 0, contentType); - var properties = GetPropertyCollections(new List> { temp }); - media.Properties = properties[versionId]; - - // reset dirty initial properties (U4-1946) - media.ResetDirtyProperties(false); - return media; + return _outerRepo.MapDtosToContent(Database.Fetch(sql)); } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistNewItem(IMedia entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistUpdatedItem(IMedia entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs index 069b49de2f..260cebef9f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs @@ -1,14 +1,15 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository { - class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository + public MediaTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.MediaTypeContainer) { - public MediaTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.MediaTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs index 6742d2457d..51a4c36752 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,126 +11,124 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository + public MediaTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) { - public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) - { } + } - protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; + protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MediaType; + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job + protected override IMediaType? PerformGet(int id) + => GetMany().FirstOrDefault(x => x.Id == id); + + protected override IMediaType? PerformGet(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id); + + protected override bool PerformExists(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id) != null; + + protected override IMediaType? PerformGet(string alias) + => GetMany().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + IEnumerable all = GetMany(); + return ids?.Any() ?? false ? all.Where(x => ids.Contains(x.Key)) : all; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate(); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name).WhereNotNull() : Enumerable.Empty(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(x => x.NodeId); + + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } + + protected override void PersistNewItem(IMediaType entity) + { + entity.AddingEntity(); + + PersistNewBaseContentType(entity); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMediaType entity) + { + ValidateAlias(entity); + + // Updates Modified date + entity.UpdatingEntity(); + + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; } - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job + PersistUpdatedBaseContentType(entity); - protected override IMediaType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IMediaType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); - - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - - protected override IMediaType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - - protected override IEnumerable? GetAllWithFullCachePolicy() - { - return CommonRepository.GetAllTypes()?.OfType(); - } - - protected override IEnumerable? PerformGetAll(params Guid[]? ids) - { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate(); - var ids = Database.Fetch(sql).Distinct().ToArray(); - - return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name).WhereNotNull() : Enumerable.Empty(); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(x => x.NodeId); - - sql - .From() - .InnerJoin().On( left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; - } - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MediaType; - - protected override void PersistNewItem(IMediaType entity) - { - entity.AddingEntity(); - - PersistNewBaseContentType(entity); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMediaType entity) - { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - PersistUpdatedBaseContentType(entity); - - entity.ResetDirtyProperties(); - } + entity.ResetDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs index f94ffe03a3..5dfd164f0d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -15,307 +13,302 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MemberGroupRepository : EntityRepositoryBase, IMemberGroupRepository { - internal class MemberGroupRepository : EntityRepositoryBase, IMemberGroupRepository + private readonly IEventMessagesFactory _eventMessagesFactory; + + public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IEventMessagesFactory eventMessagesFactory) + : base(scopeAccessor, cache, logger) => + _eventMessagesFactory = eventMessagesFactory; + + protected Guid NodeObjectTypeId => Constants.ObjectTypes.MemberGroup; + + public IMemberGroup? Get(Guid uniqueId) { - private readonly IEventMessagesFactory _eventMessagesFactory; + Sql sql = GetBaseQuery(false); + sql.Where(x => x.UniqueId == uniqueId); - public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IEventMessagesFactory eventMessagesFactory) - : base(scopeAccessor, cache, logger) => - _eventMessagesFactory = eventMessagesFactory; + NodeDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - protected override IMemberGroup? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id = id }); + return dto == null ? null : MemberGroupFactory.BuildEntity(dto); + } - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - return dto == null ? null : MemberGroupFactory.BuildEntity(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); - - return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new[] - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", - "DELETE FROM umbracoRelation WHERE childId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", - "DELETE FROM umbracoNode WHERE id = @id" - }; - return list; - } - - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberGroup; - - protected override void PersistNewItem(IMemberGroup entity) - { - //Save to db - entity.AddingEntity(); - var group = (MemberGroup)entity; - var dto = MemberGroupFactory.BuildDto(group); - var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); - group.Id = dto.NodeId; //Set Id on entity to ensure an Id is set - - //Update with new correct path and id - dto.Path = string.Concat("-1,", dto.NodeId); - Database.Update(dto); - //assign to entity - group.Id = o; - group.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMemberGroup entity) - { - var dto = MemberGroupFactory.BuildDto(entity); - - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - public IMemberGroup? Get(Guid uniqueId) - { - var sql = GetBaseQuery(false); - sql.Where(x => x.UniqueId == uniqueId); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - return dto == null ? null : MemberGroupFactory.BuildEntity(dto); - } - - public IMemberGroup? GetByName(string? name) - { - return IsolatedCache.GetCacheItem( - typeof(IMemberGroup).FullName + "." + name, - () => - { - var qry = Query().Where(group => group.Name!.Equals(name)); - var result = Get(qry); - return result?.FirstOrDefault(); - }, - //cache for 5 mins since that is the default in the Runtime app cache - TimeSpan.FromMinutes(5), - //sliding is true - true); - } - - public IMemberGroup? CreateIfNotExists(string roleName) - { - var qry = Query().Where(group => group.Name!.Equals(roleName)); - var result = Get(qry); - - if (result?.Any() ?? false) - return null; - - var grp = new MemberGroup + public IMemberGroup? GetByName(string? name) => + IsolatedCache.GetCacheItem( + typeof(IMemberGroup).FullName + "." + name, + () => { - Name = roleName - }; - PersistNewItem(grp); + IQuery qry = Query().Where(group => group.Name!.Equals(name)); + IEnumerable result = Get(qry); + return result.FirstOrDefault(); + }, - var evtMsgs = _eventMessagesFactory.Get(); - if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(grp, evtMsgs))) - { - return null; - } + // cache for 5 mins since that is the default in the Runtime app cache + TimeSpan.FromMinutes(5), - AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(grp, evtMsgs)); + // sliding is true + true); - return grp; + public IMemberGroup? CreateIfNotExists(string roleName) + { + IQuery qry = Query().Where(group => group.Name!.Equals(roleName)); + IEnumerable result = Get(qry); + + if (result.Any()) + { + return null; } - public IEnumerable GetMemberGroupsForMember(int memberId) + var grp = new MemberGroup { Name = roleName }; + PersistNewItem(grp); + + EventMessages evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(grp, evtMsgs))) { - var sql = Sql() - .Select("umbracoNode.*") + return null; + } + + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(grp, evtMsgs)); + + return grp; + } + + public IEnumerable GetMemberGroupsForMember(int memberId) + { + Sql sql = Sql() + .Select("umbracoNode.*") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.MemberGroup) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Member == memberId); + + return Database.Fetch(sql) + .DistinctBy(dto => dto.NodeId) + .Select(x => MemberGroupFactory.BuildEntity(x)); + } + + public IEnumerable GetMemberGroupsForMember(string? username) + { + Sql? sql = Sql() + .Select("un.*") + .From("umbracoNode AS un") + .InnerJoin("cmsMember2MemberGroup") + .On("cmsMember2MemberGroup.MemberGroup = un.id") + .InnerJoin("cmsMember") + .On("cmsMember.nodeId = cmsMember2MemberGroup.Member") + .Where("un.nodeObjectType=@objectType", new { objectType = NodeObjectTypeId }) + .Where("cmsMember.LoginName=@loginName", new { loginName = username }); + + return Database.Fetch(sql) + .DistinctBy(dto => dto.NodeId) + .Select(x => MemberGroupFactory.BuildEntity(x)); + } + + public void ReplaceRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames, true); + + public void AssignRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames); + + public void DissociateRoles(int[] memberIds, string[] roleNames) => DissociateRolesInternal(memberIds, roleNames); + + protected override IMemberGroup? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + + NodeDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + return dto == null ? null : MemberGroupFactory.BuildEntity(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new[] + { + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoRelation WHERE parentId = @id", "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", "DELETE FROM umbracoNode WHERE id = @id", + }; + return list; + } + + protected override void PersistNewItem(IMemberGroup entity) + { + // Save to db + entity.AddingEntity(); + var group = (MemberGroup)entity; + NodeDto dto = MemberGroupFactory.BuildDto(group); + var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); + group.Id = dto.NodeId; // Set Id on entity to ensure an Id is set + + // Update with new correct path and id + dto.Path = string.Concat("-1,", dto.NodeId); + Database.Update(dto); + + // assign to entity + group.Id = o; + group.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMemberGroup entity) + { + NodeDto dto = MemberGroupFactory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + private void AssignRolesInternal(int[] memberIds, string[] roleNames, bool replace = false) + { + // ensure they're unique + memberIds = memberIds.Distinct().ToArray(); + + // create the missing roles first + Sql existingSql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId) + .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); + IEnumerable existingRoles = Database.Fetch(existingSql).Select(x => x.Text); + IEnumerable missingRoles = roleNames.Except(existingRoles, StringComparer.CurrentCultureIgnoreCase); + MemberGroup[] missingGroups = missingRoles.Select(x => new MemberGroup { Name = x }).ToArray(); + + EventMessages evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(missingGroups, evtMsgs))) + { + return; + } + + foreach (MemberGroup m in missingGroups) + { + PersistNewItem(m); + } + + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(missingGroups, evtMsgs)); + + // now go get all the dto's for roles with these role names + var rolesForNames = Database.Fetch(existingSql) + .ToDictionary(x => x.Text!, StringComparer.InvariantCultureIgnoreCase); + + AssignedRolesDto[] currentlyAssigned; + if (replace) + { + // delete all assigned groups first + Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds)", new { memberIds }); + + currentlyAssigned = Array.Empty(); + } + else + { + // get the groups that are currently assigned to any of these members + Sql assignedSql = Sql() + .Select( + $"{SqlSyntax.GetQuotedColumnName("text")},{SqlSyntax.GetQuotedColumnName("Member")},{SqlSyntax.GetQuotedColumnName("MemberGroup")}") .From() .InnerJoin() .On(dto => dto.NodeId, dto => dto.MemberGroup) .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Member == memberId); + .WhereIn(x => x.Member, memberIds); - return Database.Fetch(sql) - .DistinctBy(dto => dto.NodeId) - .Select(x => MemberGroupFactory.BuildEntity(x)); + currentlyAssigned = Database.Fetch(assignedSql).ToArray(); } - public IEnumerable GetMemberGroupsForMember(string? username) + // assign the roles for each member id + foreach (var memberId in memberIds) { - var sql = Sql() - .Select("un.*") - .From("umbracoNode AS un") - .InnerJoin("cmsMember2MemberGroup") - .On("cmsMember2MemberGroup.MemberGroup = un.id") - .InnerJoin("cmsMember") - .On("cmsMember.nodeId = cmsMember2MemberGroup.Member") - .Where("un.nodeObjectType=@objectType", new { objectType = NodeObjectTypeId }) - .Where("cmsMember.LoginName=@loginName", new { loginName = username }); + // find any roles for the current member that are currently assigned that + // exist in the roleNames list, then determine which ones are not currently assigned. + var mId = memberId; + AssignedRolesDto[] found = currentlyAssigned.Where(x => x.MemberId == mId).ToArray(); + IEnumerable assignedRoles = found + .Where(x => roleNames.Contains(x.RoleName, StringComparer.CurrentCultureIgnoreCase)) + .Select(x => x.RoleName); + IEnumerable nonAssignedRoles = + roleNames.Except(assignedRoles, StringComparer.CurrentCultureIgnoreCase); - return Database.Fetch(sql) - .DistinctBy(dto => dto.NodeId) - .Select(x => MemberGroupFactory.BuildEntity(x)); - } + IEnumerable dtos = nonAssignedRoles + .Select(x => new Member2MemberGroupDto { Member = mId, MemberGroup = rolesForNames[x!].NodeId }); - - - public void ReplaceRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames, true); - - public void AssignRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames); - - private void AssignRolesInternal(int[] memberIds, string[] roleNames, bool replace = false) - { - //ensure they're unique - memberIds = memberIds.Distinct().ToArray(); - - //create the missing roles first - - Sql existingSql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId) - .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); - IEnumerable existingRoles = Database.Fetch(existingSql).Select(x => x.Text); - IEnumerable missingRoles = roleNames.Except(existingRoles, StringComparer.CurrentCultureIgnoreCase); - MemberGroup[] missingGroups = missingRoles.Select(x => new MemberGroup { Name = x }).ToArray(); - - var evtMsgs = _eventMessagesFactory.Get(); - if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(missingGroups, evtMsgs))) - { - return; - } - - foreach (MemberGroup m in missingGroups) - { - PersistNewItem(m); - } - - AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(missingGroups, evtMsgs)); - - //now go get all the dto's for roles with these role names - var rolesForNames = Database.Fetch(existingSql) - .ToDictionary(x => x.Text!, StringComparer.InvariantCultureIgnoreCase); - - AssignedRolesDto[] currentlyAssigned; - if (replace) - { - // delete all assigned groups first - Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds)", new { memberIds }); - - currentlyAssigned = Array.Empty(); - } - else - { - //get the groups that are currently assigned to any of these members - - Sql assignedSql = Sql() - .Select($"{SqlSyntax.GetQuotedColumnName("text")},{SqlSyntax.GetQuotedColumnName("Member")},{SqlSyntax.GetQuotedColumnName("MemberGroup")}") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.MemberGroup) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.Member, memberIds); - - currentlyAssigned = Database.Fetch(assignedSql).ToArray(); - } - - //assign the roles for each member id - - foreach (var memberId in memberIds) - { - //find any roles for the current member that are currently assigned that - //exist in the roleNames list, then determine which ones are not currently assigned. - var mId = memberId; - AssignedRolesDto[] found = currentlyAssigned.Where(x => x.MemberId == mId).ToArray(); - IEnumerable assignedRoles = found.Where(x => roleNames.Contains(x.RoleName, StringComparer.CurrentCultureIgnoreCase)).Select(x => x.RoleName); - IEnumerable nonAssignedRoles = roleNames.Except(assignedRoles, StringComparer.CurrentCultureIgnoreCase); - - IEnumerable dtos = nonAssignedRoles - .Select(x => new Member2MemberGroupDto - { - Member = mId, - MemberGroup = rolesForNames[x!].NodeId - }); - - Database.InsertBulk(dtos); - } - } - - public void DissociateRoles(int[] memberIds, string[] roleNames) - { - DissociateRolesInternal(memberIds, roleNames); - } - - private void DissociateRolesInternal(int[] memberIds, string[] roleNames) - { - var existingSql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId) - .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); - var existingRolesIds = Database.Fetch(existingSql).Select(x => x.NodeId).ToArray(); - - Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds) AND MemberGroup IN (@memberGroups)", - new { /*memberIds =*/ memberIds, memberGroups = existingRolesIds }); - } - - private class AssignedRolesDto - { - [Column("text")] - public string? RoleName { get; set; } - - [Column("Member")] - public int MemberId { get; set; } - - [Column("MemberGroup")] - public int MemberGroupId { get; set; } + Database.InsertBulk(dtos); } } + + private void DissociateRolesInternal(int[] memberIds, string[] roleNames) + { + Sql? existingSql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId) + .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); + var existingRolesIds = Database.Fetch(existingSql).Select(x => x.NodeId).ToArray(); + + Database.Execute( + "DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds) AND MemberGroup IN (@memberGroups)", + new + { + /*memberIds =*/ + memberIds, + memberGroups = existingRolesIds, + }); + } + + private class AssignedRolesDto + { + [Column("text")] + public string? RoleName { get; set; } + + [Column("Member")] + public int MemberId { get; set; } + + [Column("MemberGroup")] + public int MemberGroupId { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 198468ddba..c49f0d2963 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; @@ -15,7 +12,6 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -26,792 +22,792 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +public class MemberRepository : ContentRepositoryBase, IMemberRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - public class MemberRepository : ContentRepositoryBase, IMemberRepository + private readonly IJsonSerializer _jsonSerializer; + private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; + private readonly IMemberGroupRepository _memberGroupRepository; + private readonly IMemberTypeRepository _memberTypeRepository; + private readonly MemberPasswordConfigurationSettings _passwordConfiguration; + private readonly IPasswordHasher _passwordHasher; + private readonly ITagRepository _tagRepository; + private bool _passwordConfigInitialized; + private string? _passwordConfigJson; + + public MemberRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IMemberTypeRepository memberTypeRepository, + IMemberGroupRepository memberGroupRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + IPasswordHasher passwordHasher, + PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator, + IOptions passwordConfiguration) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly IJsonSerializer _jsonSerializer; - private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; - private readonly IMemberGroupRepository _memberGroupRepository; - private readonly IMemberTypeRepository _memberTypeRepository; - private readonly MemberPasswordConfigurationSettings _passwordConfiguration; - private readonly IPasswordHasher _passwordHasher; - private readonly ITagRepository _tagRepository; - private bool _passwordConfigInitialized; - private string? _passwordConfigJson; + _memberTypeRepository = + memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _passwordHasher = passwordHasher; + _jsonSerializer = serializer; + _memberGroupRepository = memberGroupRepository; + _passwordConfiguration = passwordConfiguration.Value; + _memberByUsernameCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + } - public MemberRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - ILogger logger, - IMemberTypeRepository memberTypeRepository, - IMemberGroupRepository memberGroupRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - IPasswordHasher passwordHasher, - PropertyEditorCollection propertyEditors, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator, - IOptions passwordConfiguration) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, - propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) + /// + /// Returns a serialized dictionary of the password configuration that is stored against the member in the database + /// + private string? DefaultPasswordConfigJson + { + get { - _memberTypeRepository = - memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _passwordHasher = passwordHasher; - _jsonSerializer = serializer; - _memberGroupRepository = memberGroupRepository; - _passwordConfiguration = passwordConfiguration.Value; - _memberByUsernameCachePolicy = - new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - } - - /// - /// Returns a serialized dictionary of the password configuration that is stored against the member in the database - /// - private string? DefaultPasswordConfigJson - { - get + if (_passwordConfigInitialized) { - if (_passwordConfigInitialized) - { - return _passwordConfigJson; - } - - var passwordConfig = new PersistedPasswordSettings - { - HashAlgorithm = _passwordConfiguration.HashAlgorithmType - }; - - _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); - _passwordConfigInitialized = true; return _passwordConfigJson; } + + var passwordConfig = new PersistedPasswordSettings + { + HashAlgorithm = _passwordConfiguration.HashAlgorithmType + }; + + _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); + _passwordConfigInitialized = true; + return _passwordConfigJson; } + } - protected override MemberRepository This => this; + protected override MemberRepository This => this; - public override int RecycleBinId => throw new NotSupportedException(); + public override int RecycleBinId => throw new NotSupportedException(); - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, - StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, + StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + //get the group id + IQuery grpQry = Query().Where(group => group.Name!.Equals(roleName)); + IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); + if (memberGroup == null) { - //get the group id - IQuery grpQry = Query().Where(group => group.Name!.Equals(roleName)); - IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); - if (memberGroup == null) - { - return Enumerable.Empty(); - } - - // get the members by username - IQuery query = Query(); - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(usernameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(usernameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(usernameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(usernameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - - IMember[]? matchedMembers = Get(query)?.ToArray(); - - var membersInGroup = new List(); - - if (matchedMembers is null) - { - return membersInGroup; - } - //then we need to filter the matched members that are in the role - foreach (IEnumerable group in matchedMembers.Select(x => x.Id) - .InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql().SelectAll().From() - .Where(dto => dto.MemberGroup == memberGroup.Id) - .WhereIn(dto => dto.Member, group); - - var memberIdsInGroup = Database.Fetch(sql) - .Select(x => x.Member).ToArray(); - - membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); - } - - return membersInGroup; + return Enumerable.Empty(); } - /// - /// Get all members in a specific group - /// - /// - /// - public IEnumerable GetByMemberGroup(string groupName) + // get the members by username + IQuery query = Query(); + switch (matchType) { - IQuery grpQry = Query().Where(group => group.Name!.Equals(groupName)); - IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); - if (memberGroup == null) + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(usernameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(usernameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(usernameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(usernameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + IMember[] matchedMembers = Get(query).ToArray(); + + var membersInGroup = new List(); + + // Then we need to filter the matched members that are in the role + foreach (IEnumerable group in matchedMembers.Select(x => x.Id) + .InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Sql sql = Sql().SelectAll().From() + .Where(dto => dto.MemberGroup == memberGroup.Id) + .WhereIn(dto => dto.Member, group); + + var memberIdsInGroup = Database.Fetch(sql) + .Select(x => x.Member).ToArray(); + + membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); + } + + return membersInGroup; + } + + /// + /// Get all members in a specific group + /// + /// + /// + public IEnumerable GetByMemberGroup(string groupName) + { + IQuery grpQry = Query().Where(group => group.Name!.Equals(groupName)); + IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); + if (memberGroup == null) + { + return Enumerable.Empty(); + } + + Sql subQuery = Sql().Select("Member").From() + .Where(dto => dto.MemberGroup == memberGroup.Id); + + Sql sql = GetBaseQuery(false) + // TODO: An inner join would be better, though I've read that the query optimizer will always turn a + // subquery with an IN clause into an inner join anyways. + .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return MapDtosToContent(Database.Fetch(sql)); + } + + public bool Exists(string username) + { + Sql sql = Sql() + .SelectCount() + .From() + .Where(x => x.LoginName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public int GetCountByQuery(IQuery? query) + { + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + Sql sql = translator.Translate(); + + //get the COUNT base query + Sql fullSql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + + return Database.ExecuteScalar(fullSql); + } + + /// + /// Gets paged member results. + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, + Ordering? ordering) + { + Sql? filterSql = null; + + if (filter != null) + { + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) { - return Enumerable.Empty(); + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } + + public IMember? GetByUsername(string? username) => + _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); + + public int[] GetMemberIds(string[] usernames) + { + Guid memberObjectType = Constants.ObjectTypes.Member; + + Sql memberSql = Sql() + .Select("umbracoNode.id") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(x => x.NodeObjectType == memberObjectType) + .Where("cmsMember.LoginName in (@usernames)", new + { + /*usernames =*/ + usernames + }); + return Database.Fetch(memberSql).ToArray(); + } + + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + if (ordering.OrderBy.InvariantEquals("email")) + { + return SqlSyntax.GetFieldName(x => x.Email); + } + + if (ordering.OrderBy.InvariantEquals("loginName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("userName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("updateDate")) + { + return SqlSyntax.GetFieldName(x => x.VersionDate); + } + + if (ordering.OrderBy.InvariantEquals("createDate")) + { + return SqlSyntax.GetFieldName(x => x.CreateDate); + } + + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + { + return SqlSyntax.GetFieldName(x => x.Alias); + } + + return base.ApplySystemOrdering(ref sql, ordering); + } + + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Member[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) + { + MemberDto dto = dtos[i]; + + if (withCache) + { + // if the cache contains the (proper version of the) item, use it + IMember? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) + { + content[i] = (Member)cached; + continue; + } } - Sql subQuery = Sql().Select("Member").From() - .Where(dto => dto.MemberGroup == memberGroup.Id); + // else, need to build it - Sql sql = GetBaseQuery(false) - // TODO: An inner join would be better, though I've read that the query optimizer will always turn a - // subquery with an IN clause into an inner join anyways. - .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMemberType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); + } - return MapDtosToContent(Database.Fetch(sql)); + Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); } - public bool Exists(string username) + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); + + // assign properties + foreach (TempContent temp in temps) { - Sql sql = Sql() - .SelectCount() - .From() - .Where(x => x.LoginName == username); + if (temp.Content is not null) + { + temp.Content.Properties = properties[temp.VersionId]; - return Database.ExecuteScalar(sql) > 0; + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); + } } - public int GetCountByQuery(IQuery? query) + return content; + } + + private IMember MapDtoToContent(MemberDto dto) + { + IMemberType? memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); + Member member = ContentBaseFactory.BuildEntity(dto, memberType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); + IDictionary properties = + GetPropertyCollections(new List> {temp}); + member.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + member.ResetDirtyProperties(false); + return member; + } + + private IMember? PerformGetByUsername(string? username) + { + IQuery query = Query().Where(x => x.Username.Equals(username)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByUsername(params string[]? usernames) + { + IQuery query = Query().WhereIn(x => x.Username, usernames); + return PerformGetByQuery(query); + } + + #region Repository Base + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; + + protected override IMember? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + + // TODO: why is this different from content/media?! + // check if the query is based on properties or not + + IEnumerable> wheres = query.GetWhereClauses(); + //this is a pretty rudimentary check but will work, we just need to know if this query requires property + // level queries + if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) { Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); var translator = new SqlTranslator(sqlWithProps, query); Sql sql = translator.Translate(); - //get the COUNT base query - Sql fullSql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) + .OrderBy(x => x.SortOrder); - return Database.ExecuteScalar(fullSql); + return MapDtosToContent(Database.Fetch(baseQuery)); } - - /// - /// Gets paged member results. - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, - Ordering? ordering) + else { - Sql? filterSql = null; - - if (filter != null) - { - filterSql = Sql(); - foreach (Tuple clause in filter.GetWhereClauses()) - { - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - public IMember? GetByUsername(string? username) => - _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); - - public int[] GetMemberIds(string[] usernames) - { - Guid memberObjectType = Constants.ObjectTypes.Member; - - Sql memberSql = Sql() - .Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new - { - /*usernames =*/ - usernames - }); - return Database.Fetch(memberSql).ToArray(); - } - - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) - { - if (ordering.OrderBy.InvariantEquals("email")) - { - return SqlSyntax.GetFieldName(x => x.Email); - } - - if (ordering.OrderBy.InvariantEquals("loginName")) - { - return SqlSyntax.GetFieldName(x => x.LoginName); - } - - if (ordering.OrderBy.InvariantEquals("userName")) - { - return SqlSyntax.GetFieldName(x => x.LoginName); - } - - if (ordering.OrderBy.InvariantEquals("updateDate")) - { - return SqlSyntax.GetFieldName(x => x.VersionDate); - } - - if (ordering.OrderBy.InvariantEquals("createDate")) - { - return SqlSyntax.GetFieldName(x => x.CreateDate); - } - - if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) - { - return SqlSyntax.GetFieldName(x => x.Alias); - } - - return base.ApplySystemOrdering(ref sql, ordering); - } - - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Member[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - MemberDto dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - IMember? cached = - IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Member)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out IMemberType? contentType) == false) - { - contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); - } - - Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } - - // load all properties for all documents from database in 1 query - indexed by version id - IDictionary properties = GetPropertyCollections(temps); - - // assign properties - foreach (TempContent temp in temps) - { - if (temp.Content is not null) - { - temp.Content.Properties = properties[temp.VersionId]; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } - } - - return content; - } - - private IMember MapDtoToContent(MemberDto dto) - { - IMemberType? memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); - Member member = ContentBaseFactory.BuildEntity(dto, memberType); - - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); - IDictionary properties = - GetPropertyCollections(new List> { temp }); - member.Properties = properties[versionId]; - - // reset dirty initial properties (U4-1946) - member.ResetDirtyProperties(false); - return member; - } - - private IMember? PerformGetByUsername(string? username) - { - IQuery query = Query().Where(x => x.Username.Equals(username)); - return PerformGetByQuery(query).FirstOrDefault(); - } - - private IEnumerable PerformGetAllByUsername(params string[]? usernames) - { - IQuery query = Query().WhereIn(x => x.Username, usernames); - return PerformGetByQuery(query); - } - - #region Repository Base - - protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; - - protected override IMember? PerformGet(int id) - { - Sql sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); - - MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - Sql sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.NodeId, ids); - } + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate() + .OrderBy(x => x.SortOrder); return MapDtosToContent(Database.Fetch(sql)); } + } - protected override IEnumerable PerformGetByQuery(IQuery query) + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); + + protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + { + Sql sql = SqlContext.Sql(); + + switch (queryType) // TODO: pretend we still need these queries for now { - Sql baseQuery = GetBaseQuery(false); + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + sql = sql.Select(r => + r.Select(x => x.ContentVersionDto) + .Select(x => x.ContentDto, r1 => + r1.Select(x => x.NodeDto))) - // TODO: why is this different from content/media?! - // check if the query is based on properties or not - - IEnumerable> wheres = query.GetWhereClauses(); - //this is a pretty rudimentary check but will work, we just need to know if this query requires property - // level queries - if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) - { - Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - Sql sql = translator.Translate(); - - baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(baseQuery)); - } - else - { - var translator = new SqlTranslator(baseQuery, query); - Sql sql = translator.Translate() - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(sql)); - } + // ContentRepositoryBase expects a variantName field to order by name + // so get it here, though for members it's just the plain node name + .AndSelect(x => Alias(x.Text, "variantName")); + break; } - protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) - protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? + // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content + // types by default on the document and media repos so we can query by content type there too. + .InnerJoin() + .On(left => left.ContentTypeId, right => right.NodeId); + + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + + if (current) { - Sql sql = SqlContext.Sql(); - - switch (queryType) // TODO: pretend we still need these queries for now - { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - sql = sql.Select(r => - r.Select(x => x.ContentVersionDto) - .Select(x => x.ContentDto, r1 => - r1.Select(x => x.NodeDto))) - - // ContentRepositoryBase expects a variantName field to order by name - // so get it here, though for members it's just the plain node name - .AndSelect(x => Alias(x.Text, "variantName")); - break; - } - - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - - // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? - // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content - // types by default on the document and media repos so we can query by content type there too. - .InnerJoin() - .On(left => left.ContentTypeId, right => right.NodeId); - - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); - - if (current) - { - sql.Where(x => x.Current); // always get the current version - } - - return sql; + sql.Where(x => x.Current); // always get the current version } - // TODO: move that one up to Versionable! or better: kill it! - protected override Sql GetBaseQuery(bool isCount) => - GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + return sql; + } - protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? - => - "umbracoNode.id = @id"; + // TODO: move that one up to Versionable! or better: kill it! + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - // TODO: document/understand that one - protected Sql GetNodeIdQueryWithPropertyData() => - Sql() - .Select("DISTINCT(umbracoNode.id)") - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.ContentTypeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin() - .On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin() - .On(left => left.DataTypeId, right => right.NodeId) - .LeftJoin().On(x => x - .Where((left, right) => left.PropertyTypeId == right.Id) - .Where((left, right) => left.VersionId == right.Id)) - .Where(x => x.NodeObjectType == NodeObjectTypeId); + protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? + => + "umbracoNode.id = @id"; - protected override IEnumerable GetDeleteClauses() + // TODO: document/understand that one + protected Sql GetNodeIdQueryWithPropertyData() => + Sql() + .Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.ContentTypeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin() + .On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin() + .On(left => left.DataTypeId, right => right.NodeId) + .LeftJoin().On(x => x + .Where((left, right) => left.PropertyTypeId == right.Id) + .Where((left, right) => left.VersionId == right.Id)) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", - "DELETE FROM umbracoRelation WHERE childId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + - " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + - " WHERE nodeId = @id)", - "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", - "DELETE FROM cmsMember WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM umbracoNode WHERE id = @id" - }; - return list; + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoRelation WHERE parentId = @id", + "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", + "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", + "DELETE FROM cmsMember WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM umbracoNode WHERE id = @id" + }; + return list; + } + + #endregion + + #region Versions + + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + return MapDtosToContent(Database.Fetch(sql), true); + } + + public override IMember? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.Id == versionId); + + MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } + + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @VersionId", new {versionId}); + Database.Delete("WHERE versionId = @VersionId", new {versionId}); + } + + #endregion + + #region Persist + + protected override void PersistNewItem(IMember entity) + { + entity.AddingEntity(); + + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) + { + entity.SecurityStamp = Guid.NewGuid().ToString(); } - #endregion + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - #region Versions + // create the dto + MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - public override IEnumerable GetAllVersions(int nodeId) + // check if we have a user config else use the default + memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; + + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + + // persist the node dto + NodeDto nodeDto = memberDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; + + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) { - Sql sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - return MapDtosToContent(Database.Fetch(sql), true); - } - - public override IMember? GetVersion(int versionId) - { - Sql sql = GetBaseQuery(QueryType.Single) - .Where(x => x.Id == versionId); - - MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); - } - - protected override void PerformDeleteVersion(int id, int versionId) - { - Database.Delete("WHERE versionId = @VersionId", new { versionId }); - Database.Delete("WHERE versionId = @VersionId", new { versionId }); - } - - #endregion - - #region Persist - - protected override void PersistNewItem(IMember entity) - { - entity.AddingEntity(); - - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // create the dto - MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - - // check if we have a user config else use the default - memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - - // derive path and level from parent - NodeDto parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; - - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); - - // persist the node dto - NodeDto nodeDto = memberDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; - - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - { - nodeDto.NodeId = id; - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } - else - { - Database.Insert(nodeDto); - - // update path, now that we have an id - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - ContentDto contentDto = memberDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - // assumes a new version id and version date (modified date) has been set - ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = true; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - - // persist the member dto - memberDto.NodeId = nodeDto.NodeId; - - // if the password is empty, generate one with the special prefix - // this will hash the guid with a salt so should be nicely random - if (entity.RawPasswordValue.IsNullOrWhiteSpace()) - { - memberDto.Password = Constants.Security.EmptyPasswordPrefix + - _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); - entity.RawPasswordValue = memberDto.Password; - } - - Database.Insert(memberDto); - - // persist the property data - InsertPropertyValues(entity, 0, out _, out _); - - SetEntityTags(entity, _tagRepository, _jsonSerializer); - - PersistRelations(entity); - - OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMember entity) - { - // update - entity.UpdatingEntity(); - - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - NodeDto parent = GetParentNodeDto(entity.ParentId); - - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } - - // create the dto - MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - - // update the node dto - NodeDto nodeDto = memberDto.ContentDto.NodeDto; + nodeDto.NodeId = id; + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); - // update the content dto - Database.Update(memberDto.ContentDto); + // update path, now that we have an id + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + } - // update the content version dto - Database.Update(memberDto.ContentVersionDto); + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; - // update the member dto - // but only the changed columns, 'cos we cannot update password if empty - var changedCols = new List(); + // persist the content dto + ContentDto contentDto = memberDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); - if (entity.IsPropertyDirty("SecurityStamp")) + // persist the content version dto + // assumes a new version id and version date (modified date) has been set + ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = true; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the member dto + memberDto.NodeId = nodeDto.NodeId; + + // if the password is empty, generate one with the special prefix + // this will hash the guid with a salt so should be nicely random + if (entity.RawPasswordValue.IsNullOrWhiteSpace()) + { + memberDto.Password = Constants.Security.EmptyPasswordPrefix + + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); + entity.RawPasswordValue = memberDto.Password; + } + + Database.Insert(memberDto); + + // persist the property data + InsertPropertyValues(entity, 0, out _, out _); + + SetEntityTags(entity, _tagRepository, _jsonSerializer); + + PersistRelations(entity); + + OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMember entity) + { + // update + entity.UpdatingEntity(); + + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) + { + entity.SecurityStamp = Guid.NewGuid().ToString(); + } + + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } + + // create the dto + MemberDto memberDto = ContentBaseFactory.BuildDto(entity); + + // update the node dto + NodeDto nodeDto = memberDto.ContentDto.NodeDto; + Database.Update(nodeDto); + + // update the content dto + Database.Update(memberDto.ContentDto); + + // update the content version dto + Database.Update(memberDto.ContentVersionDto); + + // update the member dto + // but only the changed columns, 'cos we cannot update password if empty + var changedCols = new List(); + + if (entity.IsPropertyDirty("SecurityStamp")) + { + changedCols.Add("securityStampToken"); + } + + if (entity.IsPropertyDirty("Email")) + { + changedCols.Add("Email"); + } + + if (entity.IsPropertyDirty("Username")) + { + changedCols.Add("LoginName"); + } + + if (entity.IsPropertyDirty(nameof(entity.FailedPasswordAttempts))) + { + changedCols.Add(nameof(entity.FailedPasswordAttempts)); + } + + if (entity.IsPropertyDirty(nameof(entity.IsApproved))) + { + changedCols.Add(nameof(entity.IsApproved)); + } + + if (entity.IsPropertyDirty(nameof(entity.IsLockedOut))) + { + changedCols.Add(nameof(entity.IsLockedOut)); + } + + if (entity.IsPropertyDirty(nameof(entity.LastLockoutDate))) + { + changedCols.Add(nameof(entity.LastLockoutDate)); + } + + if (entity.IsPropertyDirty(nameof(entity.LastLoginDate))) + { + changedCols.Add(nameof(entity.LastLoginDate)); + } + + if (entity.IsPropertyDirty(nameof(entity.LastPasswordChangeDate))) + { + changedCols.Add(nameof(entity.LastPasswordChangeDate)); + } + + // this can occur from an upgrade + if (memberDto.PasswordConfig.IsNullOrWhiteSpace()) + { + memberDto.PasswordConfig = DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); + } + else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson) + { + changedCols.Add("passwordConfig"); + } + + // do NOT update the password if it has not changed or if it is null or empty + if (entity.IsPropertyDirty("RawPasswordValue") && !string.IsNullOrWhiteSpace(entity.RawPasswordValue)) + { + changedCols.Add("Password"); + + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { + memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); changedCols.Add("securityStampToken"); } - if (entity.IsPropertyDirty("Email")) - { - changedCols.Add("Email"); - } - - if (entity.IsPropertyDirty("Username")) - { - changedCols.Add("LoginName"); - } - - if (entity.IsPropertyDirty(nameof(entity.FailedPasswordAttempts))) - { - changedCols.Add(nameof(entity.FailedPasswordAttempts)); - } - - if (entity.IsPropertyDirty(nameof(entity.IsApproved))) - { - changedCols.Add(nameof(entity.IsApproved)); - } - - if (entity.IsPropertyDirty(nameof(entity.IsLockedOut))) - { - changedCols.Add(nameof(entity.IsLockedOut)); - } - - if (entity.IsPropertyDirty(nameof(entity.LastLockoutDate))) - { - changedCols.Add(nameof(entity.LastLockoutDate)); - } - - if (entity.IsPropertyDirty(nameof(entity.LastLoginDate))) - { - changedCols.Add(nameof(entity.LastLoginDate)); - } - - if (entity.IsPropertyDirty(nameof(entity.LastPasswordChangeDate))) - { - changedCols.Add(nameof(entity.LastPasswordChangeDate)); - } - - // this can occur from an upgrade - if (memberDto.PasswordConfig.IsNullOrWhiteSpace()) - { - memberDto.PasswordConfig = DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } - else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson) - { - changedCols.Add("passwordConfig"); - } - - // do NOT update the password if it has not changed or if it is null or empty - if (entity.IsPropertyDirty("RawPasswordValue") && !string.IsNullOrWhiteSpace(entity.RawPasswordValue)) - { - changedCols.Add("Password"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - - // check if we have a user config else use the default - memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } - - // If userlogin or the email has changed then need to reset security stamp - if (changedCols.Contains("Email") || changedCols.Contains("LoginName")) - { - memberDto.EmailConfirmedDate = null; - changedCols.Add("emailConfirmedDate"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - } - - if (changedCols.Count > 0) - { - Database.Update(memberDto, changedCols); - } - - ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - - SetEntityTags(entity, _tagRepository, _jsonSerializer); - - PersistRelations(entity); - - OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); + // check if we have a user config else use the default + memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); } - #endregion + if (entity.IsPropertyDirty("EmailConfirmedDate")) + { + changedCols.Add("emailConfirmedDate"); + } + + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("Email") || changedCols.Contains("LoginName")) + { + memberDto.EmailConfirmedDate = null; + changedCols.Add("emailConfirmedDate"); + + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) + { + memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); + } + } + + if (changedCols.Count > 0) + { + Database.Update(memberDto, changedCols); + } + + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); + + SetEntityTags(entity, _tagRepository, _jsonSerializer); + + PersistRelations(entity); + + OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs index e26e30f21b..d4790a387a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,229 +12,234 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository + private readonly IShortStringHelper _shortStringHelper; + + public MemberTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) => + _shortStringHelper = shortStringHelper; + + protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MemberType; + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job + protected override IMemberType? PerformGet(int id) + => GetMany().FirstOrDefault(x => x.Id == id); + + protected override IMemberType? PerformGet(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - private readonly IShortStringHelper _shortStringHelper; + IEnumerable all = GetMany(); + return ids?.Any() ?? false ? all.Where(x => ids.Contains(x.Key)) : all; + } - public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) + protected override bool PerformExists(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id) != null; + + protected override IMemberType? PerformGet(string alias) + => GetMany().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql subQuery = GetSubquery(); + var translator = new SqlTranslator(subQuery, query); + Sql subSql = translator.Translate(); + Sql sql = GetBaseQuery(false) + .WhereIn(x => x.NodeId, subSql) + .OrderBy(x => x.SortOrder); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name) : Enumerable.Empty(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) { - _shortStringHelper = shortStringHelper; - } - - protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); - } - - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job - - protected override IMemberType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IMemberType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); - - protected override IEnumerable? PerformGetAll(params Guid[]? ids) - { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; - } - - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - - protected override IMemberType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - - protected override IEnumerable? GetAllWithFullCachePolicy() - { - return CommonRepository.GetAllTypes()?.OfType(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var subQuery = GetSubquery(); - var translator = new SqlTranslator(subQuery, query); - var subSql = translator.Translate(); - var sql = GetBaseQuery(false) - .WhereIn(x => x.NodeId, subSql) - .OrderBy(x => x.SortOrder); - var ids = Database.Fetch(sql).Distinct().ToArray(); - - return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name) : Enumerable.Empty(); - } - - protected override Sql GetBaseQuery(bool isCount) - { - if (isCount) - { - return Sql() - .SelectCount() - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - } - - var sql = Sql() - .Select(x => x.NodeId) + return Sql() + .SelectCount() .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; } - protected Sql GetSubquery() + Sql sql = Sql() + .Select(x => x.NodeId) + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) + .LeftJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected Sql GetSubquery() + { + Sql sql = Sql() + .Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) + .LeftJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsMemberType WHERE NodeId = @id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } + + protected override void PersistNewItem(IMemberType entity) + { + ValidateAlias(entity); + + entity.AddingEntity(); + + // set a default icon if one is not specified + if (entity.Icon.IsNullOrWhiteSpace()) { - var sql = Sql() - .Select("DISTINCT(umbracoNode.id)") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; + entity.Icon = Constants.Icons.Member; } - protected override string GetBaseWhereClause() + // By Convention we add 9 standard PropertyTypes to an Umbraco MemberType + Dictionary standardPropertyTypes = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + foreach (KeyValuePair standardPropertyType in standardPropertyTypes) { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + entity.AddPropertyType( + standardPropertyType.Value, + Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); } - protected override IEnumerable GetDeleteClauses() + EnsureExplicitDataTypeForBuiltInProperties(entity); + PersistNewBaseContentType(entity); + + // Handles the MemberTypeDto (cmsMemberType table) + IEnumerable memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); + foreach (MemberPropertyTypeDto memberTypeDto in memberTypeDtos) { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsMemberType WHERE NodeId = @id"); - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; + Database.Insert(memberTypeDto); } - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberType; + entity.ResetDirtyProperties(); + } - protected override void PersistNewItem(IMemberType entity) + protected override void PersistUpdatedItem(IMemberType entity) + { + ValidateAlias(entity); + + // Updates Modified date + entity.UpdatingEntity(); + + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) { - ValidateAlias(entity); + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + } - entity.AddingEntity(); + EnsureExplicitDataTypeForBuiltInProperties(entity); + PersistUpdatedBaseContentType(entity); - //set a default icon if one is not specified - if (entity.Icon.IsNullOrWhiteSpace()) + // remove and insert - handle cmsMemberType table + Database.Delete("WHERE NodeId = @Id", new { entity.Id }); + IEnumerable memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); + foreach (MemberPropertyTypeDto memberTypeDto in memberTypeDtos) + { + Database.Insert(memberTypeDto); + } + + entity.ResetDirtyProperties(); + } + + /// + /// Override so we can specify explicit db type's on any property types that are built-in. + /// + /// + /// + /// + /// + protected override PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, string propertyTypeAlias) + { + // custom property type constructor logic to set explicit dbtype's for built in properties + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + var readonlyStorageType = builtinProperties.TryGetValue(propertyTypeAlias, out PropertyType? propertyType); + storageType = readonlyStorageType ? propertyType!.ValueStorageType : storageType; + return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, readonlyStorageType, propertyTypeAlias); + } + + /// + /// Ensure that all the built-in membership provider properties have their correct data type + /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. + /// + /// + private void EnsureExplicitDataTypeForBuiltInProperties(IContentTypeBase memberType) + { + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + foreach (IPropertyType propertyType in memberType.PropertyTypes) + { + if (builtinProperties.ContainsKey(propertyType.Alias)) { - entity.Icon = Cms.Core.Constants.Icons.Member; - } - - //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType - var standardPropertyTypes = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - foreach (var standardPropertyType in standardPropertyTypes) - { - entity.AddPropertyType(standardPropertyType.Value, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName); - } - - EnsureExplicitDataTypeForBuiltInProperties(entity); - PersistNewBaseContentType(entity); - - //Handles the MemberTypeDto (cmsMemberType table) - var memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); - foreach (var memberTypeDto in memberTypeDtos) - { - Database.Insert(memberTypeDto); - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMemberType entity) - { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - EnsureExplicitDataTypeForBuiltInProperties(entity); - PersistUpdatedBaseContentType(entity); - - // remove and insert - handle cmsMemberType table - Database.Delete("WHERE NodeId = @Id", new { Id = entity.Id }); - var memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); - foreach (var memberTypeDto in memberTypeDtos) - { - Database.Insert(memberTypeDto); - } - - entity.ResetDirtyProperties(); - } - - /// - /// Override so we can specify explicit db type's on any property types that are built-in. - /// - /// - /// - /// - /// - protected override PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, string propertyTypeAlias) - { - //custom property type constructor logic to set explicit dbtype's for built in properties - var builtinProperties = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - var readonlyStorageType = builtinProperties.TryGetValue(propertyTypeAlias, out var propertyType); - storageType = readonlyStorageType ? propertyType!.ValueStorageType : storageType; - return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, readonlyStorageType, propertyTypeAlias); - } - - /// - /// Ensure that all the built-in membership provider properties have their correct data type - /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. - /// - /// - private void EnsureExplicitDataTypeForBuiltInProperties(IContentTypeBase memberType) - { - var builtinProperties = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - foreach (var propertyType in memberType.PropertyTypes) - { - if (builtinProperties.ContainsKey(propertyType.Alias)) + // this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line + if (builtinProperties.TryGetValue(propertyType.Alias, out PropertyType? propDefinition)) { - //this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line - if (builtinProperties.TryGetValue(propertyType.Alias, out var propDefinition) && propDefinition != null) - { - propertyType.DataTypeId = propDefinition.DataTypeId; - propertyType.DataTypeKey = propDefinition.DataTypeKey; - } - else - { - propertyType.DataTypeId = 0; - propertyType.DataTypeKey = default; - } + propertyType.DataTypeId = propDefinition.DataTypeId; + propertyType.DataTypeKey = propDefinition.DataTypeKey; + } + else + { + propertyType.DataTypeId = 0; + propertyType.DataTypeKey = default; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs index 7c910d7485..47d43a9a4e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs @@ -1,4 +1,4 @@ -using System; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -13,22 +13,20 @@ public class NodeCountRepository : INodeCountRepository public NodeCountRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - /// - + /// public int GetNodeCount(Guid nodeType) { - var query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + Sql? query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectCount() .From() .Where(x => x.NodeObjectType == nodeType && x.Trashed == false); return _scopeAccessor.AmbientScope?.Database.ExecuteScalar(query) ?? 0; - } public int GetMediaCount() { - var query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + Sql? query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectCount() .From() .InnerJoin() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs index be42f7b74f..8e279735d5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs @@ -1,116 +1,110 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class NotificationsRepository : INotificationsRepository { - public class NotificationsRepository : INotificationsRepository + private readonly IScopeAccessor _scopeAccessor; + + public NotificationsRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + private IScope? AmbientScope => _scopeAccessor.AmbientScope; + + public IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType) { - private readonly IScopeAccessor _scopeAccessor; - - public NotificationsRepository(IScopeAccessor scopeAccessor) + var nodeIdsA = nodeIds.ToArray(); + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.UserId, right => right.Id) + .Where(x => x.NodeObjectType == objectType) + .Where(x => x.Disabled == false) // only approved users + .Where(x => x.Action == action); // on the specified action + if (nodeIdsA.Length > 0) { - _scopeAccessor = scopeAccessor; - } - - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; - - public IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType) - { - var nodeIdsA = nodeIds.ToArray(); - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.UserId, right => right.Id) - .Where(x => x.NodeObjectType == objectType) - .Where(x => x.Disabled == false) // only approved users - .Where(x => x.Action == action); // on the specified action - if (nodeIdsA.Length > 0) - sql? - .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes sql? - .OrderBy(x => x.Id) - .OrderBy(dto => dto.NodeId); - return AmbientScope?.Database.Fetch(sql).Select(x => new Notification(x.NodeId, x.UserId, x.Action, objectType)); + .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes } - public IEnumerable? GetUserNotifications(IUser user) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id AS nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => dto.UserId == (int)user.Id) - .OrderBy(dto => dto.NodeId); + sql? + .OrderBy(x => x.Id) + .OrderBy(dto => dto.NodeId); + return AmbientScope?.Database.Fetch(sql) + .Select(x => new Notification(x.NodeId, x.UserId, x.Action, objectType)); + } - var dtos = AmbientScope?.Database.Fetch(sql); - //need to map the results - return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); - } + public IEnumerable? GetUserNotifications(IUser user) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id AS nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => dto.UserId == user.Id) + .OrderBy(dto => dto.NodeId); - public IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions) - { - DeleteNotifications(user, entity); - return actions.Select(action => CreateNotification(user, entity, action)).ToList(); - } + List? dtos = AmbientScope?.Database.Fetch(sql); - public IEnumerable? GetEntityNotifications(IEntity entity) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id as nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => dto.NodeId == entity.Id) - .OrderBy(dto => dto.NodeId); + // need to map the results + return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); + } - var dtos = AmbientScope?.Database.Fetch(sql); - //need to map the results - return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); - } + public IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions) + { + DeleteNotifications(user, entity); + return actions.Select(action => CreateNotification(user, entity, action)).ToList(); + } - public int DeleteNotifications(IEntity entity) - { - return AmbientScope?.Database.Delete("WHERE nodeId = @nodeId", new { nodeId = entity.Id }) ?? 0; - } + public IEnumerable? GetEntityNotifications(IEntity entity) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id as nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => dto.NodeId == entity.Id) + .OrderBy(dto => dto.NodeId); - public int DeleteNotifications(IUser user) - { - return AmbientScope?.Database.Delete("WHERE userId = @userId", new { userId = user.Id }) ?? 0; - } + List? dtos = AmbientScope?.Database.Fetch(sql); - public int DeleteNotifications(IUser user, IEntity entity) - { - // delete all settings on the node for this user - return AmbientScope?.Database.Delete("WHERE userId = @userId AND nodeId = @nodeId", new { userId = user.Id, nodeId = entity.Id }) ?? 0; - } + // need to map the results + return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); + } - public Notification CreateNotification(IUser user, IEntity entity, string action) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT nodeObjectType") - .From() - .Where(nodeDto => nodeDto.NodeId == entity.Id); - var nodeType = AmbientScope?.Database.ExecuteScalar(sql); + public int DeleteNotifications(IEntity entity) => + AmbientScope?.Database.Delete("WHERE nodeId = @nodeId", new { nodeId = entity.Id }) ?? 0; - var dto = new User2NodeNotifyDto - { - Action = action, - NodeId = entity.Id, - UserId = user.Id - }; - AmbientScope?.Database.Insert(dto); - return new Notification(dto.NodeId, dto.UserId, dto.Action, nodeType); - } + public int DeleteNotifications(IUser user) => + AmbientScope?.Database.Delete("WHERE userId = @userId", new { userId = user.Id }) ?? 0; + + public int DeleteNotifications(IUser user, IEntity entity) => + + // delete all settings on the node for this user + AmbientScope?.Database.Delete( + "WHERE userId = @userId AND nodeId = @nodeId", + new { userId = user.Id, nodeId = entity.Id }) ?? 0; + + public Notification CreateNotification(IUser user, IEntity entity, string action) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select("DISTINCT nodeObjectType") + .From() + .Where(nodeDto => nodeDto.NodeId == entity.Id); + Guid? nodeType = AmbientScope?.Database.ExecuteScalar(sql); + + var dto = new User2NodeNotifyDto { Action = action, NodeId = entity.Id, UserId = user.Id }; + AmbientScope?.Database.Insert(dto); + return new Notification(dto.NodeId, dto.UserId, dto.Action, nodeType); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs index b4c8ce4f6c..37c6d67228 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs @@ -1,15 +1,15 @@ -using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement -{ - internal class PartialViewMacroRepository : PartialViewRepository, IPartialViewMacroRepository - { - public PartialViewMacroRepository(FileSystems fileSystems) - : base(fileSystems.MacroPartialsFileSystem) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; - protected override PartialViewType ViewType => PartialViewType.PartialViewMacro; +internal class PartialViewMacroRepository : PartialViewRepository, IPartialViewMacroRepository +{ + public PartialViewMacroRepository(FileSystems fileSystems) + : base(fileSystems.MacroPartialsFileSystem) + { } + + protected override PartialViewType ViewType => PartialViewType.PartialViewMacro; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs index 9fbd5af5cd..ff751a9fe6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs @@ -1,141 +1,142 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PartialViewRepository : FileRepository, IPartialViewRepository { - internal class PartialViewRepository : FileRepository, IPartialViewRepository + public PartialViewRepository(FileSystems fileSystems) + : base(fileSystems.PartialViewsFileSystem) { - public PartialViewRepository(FileSystems fileSystems) - : base(fileSystems.PartialViewsFileSystem) + } + + protected PartialViewRepository(IFileSystem? fileSystem) + : base(fileSystem) + { + } + + protected virtual PartialViewType ViewType => PartialViewType.PartialView; + + public override IPartialView? Get(string? id) + { + if (FileSystem is null) { + return null; } - protected PartialViewRepository(IFileSystem? fileSystem) - : base(fileSystem) + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id!); + + if (FileSystem.FileExists(path) == false) { + return null; } - protected virtual PartialViewType ViewType => PartialViewType.PartialView; + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; - public override IPartialView? Get(string? id) + // var content = GetFileContent(path); + var view = new PartialView(ViewType, path, file => GetFileContent(file.OriginalPath)) { - if (FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id!); + // id can be the hash + Id = path.GetHashCode(), + Key = path.EncodeAsGuid(), - if (FileSystem.FileExists(path) == false) - return null; + // Content = content, + CreateDate = created, + UpdateDate = updated, + VirtualPath = FileSystem.GetUrl(id), + }; - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); + // reset dirty initial properties (U4-1946) + view.ResetDirtyProperties(false); - var view = new PartialView(ViewType, path, file => GetFileContent(file.OriginalPath)) - { - //id can be the hash - Id = path.GetHashCode(), - Key = path.EncodeAsGuid(), - //Content = content, - CreateDate = created, - UpdateDate = updated, - VirtualPath = FileSystem.GetUrl(id) - }; + return view; + } - // reset dirty initial properties (U4-1946) - view.ResetDirtyProperties(false); - - return view; + public override void Save(IPartialView entity) + { + var partialView = entity as PartialView; + if (partialView != null) + { + partialView.ViewType = ViewType; } - public override void Save(IPartialView entity) + base.Save(entity); + + // ensure that from now on, content is lazy-loaded + if (partialView != null && partialView.GetFileContent == null) { - var partialView = entity as PartialView; - if (partialView != null) - partialView.ViewType = ViewType; - - base.Save(entity); - - // ensure that from now on, content is lazy-loaded - if (partialView != null && partialView.GetFileContent == null) - partialView.GetFileContent = file => GetFileContent(file.OriginalPath); - } - - public override IEnumerable GetMany(params string[]? ids) - { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct().ToArray(); - - if (ids?.Any() ?? false) - { - foreach (var id in ids) - { - var partialView = Get(id); - if (partialView is not null) - { - yield return partialView; - } - } - } - else - { - var files = FindAllFiles("", "*.*"); - foreach (var file in files) - { - var partialView = Get(file); - if (partialView is not null) - { - yield return partialView; - } - } - } - } - - public Stream GetFileContentStream(string filepath) - { - if (FileSystem?.FileExists(filepath) == false) - { - return Stream.Null; - } - - try - { - return FileSystem?.OpenFile(filepath) ?? Stream.Null; - } - catch - { - return Stream.Null; // deal with race conds - } - } - - public void SetFileContent(string filepath, Stream content) - { - FileSystem?.AddFile(filepath, content, true); - } - - /// - /// Gets a stream that is used to write to the file - /// - /// - /// - /// - /// This ensures the stream includes a utf8 BOM - /// - protected override Stream GetContentStream(string content) - { - var data = Encoding.UTF8.GetBytes(content); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); - return new MemoryStream(withBom); + partialView.GetFileContent = file => GetFileContent(file.OriginalPath); } } + + public override IEnumerable GetMany(params string[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct().ToArray(); + + if (ids?.Any() ?? false) + { + foreach (var id in ids) + { + IPartialView? partialView = Get(id); + if (partialView is not null) + { + yield return partialView; + } + } + } + else + { + IEnumerable files = FindAllFiles(string.Empty, "*.*"); + foreach (var file in files) + { + IPartialView? partialView = Get(file); + if (partialView is not null) + { + yield return partialView; + } + } + } + } + + public Stream GetFileContentStream(string filepath) + { + if (FileSystem?.FileExists(filepath) == false) + { + return Stream.Null; + } + + try + { + return FileSystem?.OpenFile(filepath) ?? Stream.Null; + } + catch + { + return Stream.Null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); + + /// + /// Gets a stream that is used to write to the file + /// + /// + /// + /// + /// This ensures the stream includes a utf8 BOM + /// + protected override Stream GetContentStream(string content) + { + var data = Encoding.UTF8.GetBytes(content); + var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + return new MemoryStream(withBom); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs index 9919707e8a..85a168997d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,275 +10,163 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// A (sub) repository that exposes functionality to modify assigned permissions to a node +/// +/// +/// +/// This repo implements the base class so that permissions can be +/// queued to be persisted +/// like the normal repository pattern but the standard repository Get commands don't apply and will throw +/// +/// +internal class PermissionRepository : EntityRepositoryBase + where TEntity : class, IEntity { - /// - /// A (sub) repository that exposes functionality to modify assigned permissions to a node - /// - /// - /// - /// This repo implements the base class so that permissions can be - /// queued to be persisted - /// like the normal repository pattern but the standard repository Get commands don't apply and will throw - /// - /// - internal class PermissionRepository : EntityRepositoryBase - where TEntity : class, IEntity + public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) + : base(scopeAccessor, cache, logger) { - public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger> logger) - : base(scopeAccessor, cache, logger) - { - } + } - /// - /// Returns explicitly defined permissions for a user group for any number of nodes - /// - /// - /// The group ids to lookup permissions for - /// - /// - /// - /// - /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. - /// - public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) - { - var result = new EntityPermissionCollection(); + /// + /// Returns explicitly defined permissions for a user group for any number of nodes + /// + /// + /// The group ids to lookup permissions for + /// + /// + /// + /// + /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. + /// + public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) + { + var result = new EntityPermissionCollection(); - if (entityIds.Length == 0) + if (entityIds.Length == 0) + { + foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => group.Contains(dto.UserGroupId)); + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin().On( + (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => group.Contains(dto.UserGroupId)); - List permissions = - AmbientScope.Database.Fetch(sql); - foreach (EntityPermission permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) + { + result.Add(permission); } } - else + } + else + { + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - + groupIds.Length)) { - foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - - groupIds.Length)) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => - groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin().On( + (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => + groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); - List permissions = - AmbientScope.Database.Fetch(sql); - foreach (EntityPermission permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) + { + result.Add(permission); } } - - return result; } - /// - /// Returns permissions directly assigned to the content items for all user groups - /// - /// - /// - public IEnumerable GetPermissionsForEntities(int[] entityIds) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => entityIds.Contains(dto.NodeId)) - .OrderBy(dto => dto.NodeId); + return result; + } - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); + /// + /// Returns permissions directly assigned to the content items for all user groups + /// + /// + /// + public IEnumerable GetPermissionsForEntities(int[] entityIds) + { + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On((left, right) => + left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => entityIds.Contains(dto.NodeId)) + .OrderBy(dto => dto.NodeId); + + List result = AmbientScope.Database.Fetch(sql); + return ConvertToPermissionList(result); + } + + /// + /// Returns permissions directly assigned to the content item for all user groups + /// + /// + /// + public EntityPermissionCollection GetPermissionsForEntity(int entityId) + { + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On((left, right) => + left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => dto.NodeId == entityId) + .OrderBy(dto => dto.NodeId); + + List result = AmbientScope.Database.Fetch(sql); + return ConvertToPermissionList(result); + } + + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// + /// The permissions to assign or null to remove the connection between group and entityIds + /// + /// + /// This will first clear the permissions for this user and entities and recreate them + /// + public void ReplacePermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + { + if (entityIds.Length == 0) + { + return; } - /// - /// Returns permissions directly assigned to the content item for all user groups - /// - /// - /// - public EntityPermissionCollection GetPermissionsForEntity(int entityId) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => dto.NodeId == entityId) - .OrderBy(dto => dto.NodeId); + IUmbracoDatabase db = AmbientScope.Database; - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", new { groupId, nodeIds = group }); + + db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", new { groupId, nodeIds = group }); } - /// - /// Assigns the same permission set for a single group to any number of entities - /// - /// - /// The permissions to assign or null to remove the connection between group and entityIds - /// - /// - /// This will first clear the permissions for this user and entities and recreate them - /// - public void ReplacePermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + if (permissions is not null) { - if (entityIds.Length == 0) - { - return; - } - - IUmbracoDatabase db = AmbientScope.Database; - - foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", - new { groupId, nodeIds = group }); - - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", - new { groupId, nodeIds = group }); - } - - - if (permissions is not null) - { - var toInsert = new List(); - var toInsertPermissions = new List(); - - foreach (var e in entityIds) - { - toInsert.Add(new UserGroup2NodeDto() { NodeId = e, UserGroupId = groupId }); - foreach (var p in permissions) - { - toInsertPermissions.Add(new UserGroup2NodePermissionDto - { - NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId - }); - } - } - - db.BulkInsertRecords(toInsert); - db.BulkInsertRecords(toInsertPermissions); - } - } - - /// - /// Assigns one permission for a user to many entities - /// - /// - /// - /// - public void AssignPermission(int groupId, char permission, params int[] entityIds) - { - IUmbracoDatabase db = AmbientScope.Database; - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@entityIds)", - new { groupId, entityIds }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)", - new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); - - UserGroup2NodeDto[] actionsPermissions = entityIds.Select(id => new UserGroup2NodeDto - { - NodeId = id, UserGroupId = groupId - }).ToArray(); - - UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto - { - NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId - }).ToArray(); - - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); - } - - /// - /// Assigns one permission to an entity for multiple groups - /// - /// - /// - /// - public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) - { - IUmbracoDatabase db = AmbientScope.Database; - var groupIdsA = groupIds.ToArray(); - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId AND userGroupId in (@groupIds)", - new { - nodeId = entity.Id, - groupIds = groupIdsA - }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)", - new - { - nodeId = entity.Id, - permission = permission.ToString(CultureInfo.InvariantCulture), - groupIds = groupIdsA - }); - - UserGroup2NodePermissionDto[] actionsPermissions = groupIdsA.Select(id => new UserGroup2NodePermissionDto - { - NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id - }).ToArray(); - - UserGroup2NodeDto[] actions = groupIdsA.Select(id => new UserGroup2NodeDto - { - NodeId = entity.Id, UserGroupId = id - }).ToArray(); - - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); - } - - /// - /// Assigns permissions to an entity for multiple group/permission entries - /// - /// - /// - /// - /// This will first clear the permissions for this entity then re-create them - /// - public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) - { - IUmbracoDatabase db = AmbientScope.Database; - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); - var toInsert = new List(); var toInsertPermissions = new List(); - foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) + + foreach (var e in entityIds) { - toInsert.Add(new UserGroup2NodeDto - { - NodeId = permissionSet.EntityId, - UserGroupId = entityPermission.UserGroupId - }); - foreach (var permission in entityPermission.AssignedPermissions) + toInsert.Add(new UserGroup2NodeDto { NodeId = e, UserGroupId = groupId }); + foreach (var p in permissions) { toInsertPermissions.Add(new UserGroup2NodePermissionDto { - NodeId = permissionSet.EntityId, - Permission = permission, - UserGroupId = entityPermission.UserGroupId + NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId, }); } } @@ -289,74 +174,174 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement db.BulkInsertRecords(toInsert); db.BulkInsertRecords(toInsertPermissions); } - - /// - /// Used to add or update entity permissions during a content item being updated - /// - /// - protected override void PersistNewItem(ContentPermissionSet entity) => - //does the same thing as update - PersistUpdatedItem(entity); - - /// - /// Used to add or update entity permissions during a content item being updated - /// - /// - protected override void PersistUpdatedItem(ContentPermissionSet entity) - { - var asIEntity = (IEntity)entity; - if (asIEntity.HasIdentity == false) - { - throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); - } - - ReplaceEntityPermissions(entity); - } - - private static EntityPermissionCollection ConvertToPermissionList( - IEnumerable result) - { - var permissions = new EntityPermissionCollection(); - IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); - foreach (IGrouping np in nodePermissions) - { - IEnumerable> userGroupPermissions = - np.GroupBy(x => x.UserGroupId); - foreach (IGrouping permission in userGroupPermissions) - { - var perms = permission.Select(x => x.Permission).Distinct().ToArray(); - - // perms can contain null if there are no permissions assigned, but the node is chosen in the UI. - permissions.Add(new EntityPermission(permission.Key, np.Key, - perms.WhereNotNull().ToArray())); - } - } - - return permissions; - } - - #region Not implemented (don't need to for the purposes of this repo) - - protected override ContentPermissionSet PerformGet(int id) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override IEnumerable PerformGetAll(params int[]? ids) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override Sql GetBaseQuery(bool isCount) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override string GetBaseWhereClause() => - throw new InvalidOperationException("This method won't be implemented."); - - protected override IEnumerable GetDeleteClauses() => new List(); - - protected override void PersistDeletedItem(ContentPermissionSet entity) => - throw new InvalidOperationException("This method won't be implemented."); - - #endregion } + + /// + /// Assigns one permission for a user to many entities + /// + /// + /// + /// + public void AssignPermission(int groupId, char permission, params int[] entityIds) + { + IUmbracoDatabase db = AmbientScope.Database; + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@entityIds)", new { groupId, entityIds }); + db.Execute( + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)", + new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); + + UserGroup2NodeDto[] actionsPermissions = + entityIds.Select(id => new UserGroup2NodeDto { NodeId = id, UserGroupId = groupId }).ToArray(); + + UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto + { + NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId, + }).ToArray(); + + db.BulkInsertRecords(actions); + db.BulkInsertRecords(actionsPermissions); + } + + /// + /// Assigns one permission to an entity for multiple groups + /// + /// + /// + /// + public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) + { + IUmbracoDatabase db = AmbientScope.Database; + var groupIdsA = groupIds.ToArray(); + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId AND userGroupId in (@groupIds)", new { nodeId = entity.Id, groupIds = groupIdsA }); + db.Execute( + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)", + new + { + nodeId = entity.Id, + permission = permission.ToString(CultureInfo.InvariantCulture), + groupIds = groupIdsA, + }); + + UserGroup2NodePermissionDto[] actionsPermissions = groupIdsA.Select(id => new UserGroup2NodePermissionDto + { + NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id, + }).ToArray(); + + UserGroup2NodeDto[] actions = groupIdsA.Select(id => new UserGroup2NodeDto + { + NodeId = entity.Id, UserGroupId = id, + }).ToArray(); + + db.BulkInsertRecords(actions); + db.BulkInsertRecords(actionsPermissions); + } + + /// + /// Assigns permissions to an entity for multiple group/permission entries + /// + /// + /// + /// + /// This will first clear the permissions for this entity then re-create them + /// + public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) + { + IUmbracoDatabase db = AmbientScope.Database; + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); + db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); + + var toInsert = new List(); + var toInsertPermissions = new List(); + foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) + { + toInsert.Add(new UserGroup2NodeDto + { + NodeId = permissionSet.EntityId, UserGroupId = entityPermission.UserGroupId, + }); + foreach (var permission in entityPermission.AssignedPermissions) + { + toInsertPermissions.Add(new UserGroup2NodePermissionDto + { + NodeId = permissionSet.EntityId, + Permission = permission, + UserGroupId = entityPermission.UserGroupId, + }); + } + } + + db.BulkInsertRecords(toInsert); + db.BulkInsertRecords(toInsertPermissions); + } + + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistNewItem(ContentPermissionSet entity) => + + // Does the same thing as update + PersistUpdatedItem(entity); + + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistUpdatedItem(ContentPermissionSet entity) + { + var asIEntity = (IEntity)entity; + if (asIEntity.HasIdentity == false) + { + throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); + } + + ReplaceEntityPermissions(entity); + } + + private static EntityPermissionCollection ConvertToPermissionList( + IEnumerable result) + { + var permissions = new EntityPermissionCollection(); + IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); + foreach (IGrouping np in nodePermissions) + { + IEnumerable> userGroupPermissions = + np.GroupBy(x => x.UserGroupId); + foreach (IGrouping permission in userGroupPermissions) + { + var perms = permission.Select(x => x.Permission).Distinct().ToArray(); + + // perms can contain null if there are no permissions assigned, but the node is chosen in the UI. + permissions.Add(new EntityPermission(permission.Key, np.Key, perms.WhereNotNull().ToArray())); + } + } + + return permissions; + } + + #region Not implemented (don't need to for the purposes of this repo) + + protected override ContentPermissionSet PerformGet(int id) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetAll(params int[]? ids) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => new List(); + + protected override void PersistDeletedItem(ContentPermissionSet entity) => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PropertyTypeUsageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PropertyTypeUsageRepository.cs new file mode 100644 index 0000000000..dd599aa6d5 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PropertyTypeUsageRepository.cs @@ -0,0 +1,40 @@ +using System; +using NPoco; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PropertyTypeUsageRepository : IPropertyTypeUsageRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public PropertyTypeUsageRepository(IScopeAccessor scopeAccessor) + { + _scopeAccessor = scopeAccessor; + } + + public bool HasSavedPropertyValues(string propertyTypeAlias) + { + IUmbracoDatabase? database = _scopeAccessor.AmbientScope?.Database; + + if (database is null) + { + throw new InvalidOperationException("A scope is required to query the database"); + } + + Sql selectQuery = database.SqlContext.Sql() + .SelectAll() + .From("m") + .InnerJoin("p") + .On((left, right) => left.PropertyTypeId == right.Id, "p", "m") + .Where(m => m.Alias == propertyTypeAlias, "m"); + + Sql hasValuesQuery = database.SqlContext.Sql() + .SelectAnyIfExists(selectQuery); + + return database.ExecuteScalar(hasValuesQuery); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs index c7f7724d6d..2716df9315 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,151 +11,149 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PublicAccessRepository : EntityRepositoryBase, IPublicAccessRepository { - internal class PublicAccessRepository : EntityRepositoryBase, IPublicAccessRepository + public PublicAccessRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public PublicAccessRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - } - - protected override PublicAccessEntry? PerformGet(Guid id) - { - //return from GetAll - this will be cached as a collection - return GetMany()?.FirstOrDefault(x => x.Key == id); - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = GetBaseQuery(false); - - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.Id, ids); - } - - sql.OrderBy(x => x.NodeId); - - var dtos = Database.FetchOneToMany(x => x.Rules, sql); - return dtos.Select(PublicAccessEntryFactory.BuildEntity); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.FetchOneToMany(x => x.Rules, sql); - return dtos.Select(PublicAccessEntryFactory.BuildEntity); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.AccessId); - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Access}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoAccessRule WHERE accessId = @id", - "DELETE FROM umbracoAccess WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(PublicAccessEntry entity) - { - entity.AddingEntity(); - foreach (var rule in entity.Rules) - rule.AddingEntity(); - - var dto = PublicAccessEntryFactory.BuildDto(entity); - - Database.Insert(dto); - //update the id so HasEntity is correct - entity.Id = entity.Key.GetHashCode(); - - foreach (var rule in dto.Rules) - { - rule.AccessId = entity.Key; - Database.Insert(rule); - } - - //update the id so HasEntity is correct - foreach (var rule in entity.Rules) - rule.Id = rule.Key.GetHashCode(); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(PublicAccessEntry entity) - { - entity.UpdatingEntity(); - foreach (var rule in entity.Rules) - { - if (rule.HasIdentity) - rule.UpdatingEntity(); - else - rule.AddingEntity(); - } - - var dto = PublicAccessEntryFactory.BuildDto(entity); - - Database.Update(dto); - - foreach (var removedRule in entity.RemovedRules) - { - Database.Delete("WHERE id=@Id", new { Id = removedRule }); - } - - foreach (var rule in entity.Rules) - { - if (rule.HasIdentity) - { - var count = Database.Update(dto.Rules.Single(x => x.Id == rule.Key)); - if (count == 0) - { - throw new InvalidOperationException("No rows were updated for the access rule"); - } - } - else - { - Database.Insert(new AccessRuleDto - { - Id = rule.Key, - AccessId = dto.Id, - RuleValue = rule.RuleValue, - RuleType = rule.RuleType, - CreateDate = rule.CreateDate, - UpdateDate = rule.UpdateDate - }); - //update the id so HasEntity is correct - rule.Id = rule.Key.GetHashCode(); - } - } - - entity.ResetDirtyProperties(); - } - - protected override Guid GetEntityId(PublicAccessEntry entity) - { - return entity.Key; - } } + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + + protected override PublicAccessEntry? PerformGet(Guid id) => + + // return from GetAll - this will be cached as a collection + GetMany().FirstOrDefault(x => x.Key == id); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + Sql sql = GetBaseQuery(false); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.Id, ids); + } + + sql.OrderBy(x => x.NodeId); + + List? dtos = Database.FetchOneToMany(x => x.Rules, sql); + return dtos.Select(PublicAccessEntryFactory.BuildEntity); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.FetchOneToMany(x => x.Rules, sql); + return dtos.Select(PublicAccessEntryFactory.BuildEntity); + } + + protected override Sql GetBaseQuery(bool isCount) => + Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.AccessId); + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Access}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoAccessRule WHERE accessId = @id", "DELETE FROM umbracoAccess WHERE id = @id", + }; + return list; + } + + protected override void PersistNewItem(PublicAccessEntry entity) + { + entity.AddingEntity(); + foreach (PublicAccessRule rule in entity.Rules) + { + rule.AddingEntity(); + } + + AccessDto dto = PublicAccessEntryFactory.BuildDto(entity); + + Database.Insert(dto); + + // update the id so HasEntity is correct + entity.Id = entity.Key.GetHashCode(); + + foreach (AccessRuleDto rule in dto.Rules) + { + rule.AccessId = entity.Key; + Database.Insert(rule); + } + + // update the id so HasEntity is correct + foreach (PublicAccessRule rule in entity.Rules) + { + rule.Id = rule.Key.GetHashCode(); + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(PublicAccessEntry entity) + { + entity.UpdatingEntity(); + foreach (PublicAccessRule rule in entity.Rules) + { + if (rule.HasIdentity) + { + rule.UpdatingEntity(); + } + else + { + rule.AddingEntity(); + } + } + + AccessDto dto = PublicAccessEntryFactory.BuildDto(entity); + + Database.Update(dto); + + foreach (Guid removedRule in entity.RemovedRules) + { + Database.Delete("WHERE id=@Id", new { Id = removedRule }); + } + + foreach (PublicAccessRule rule in entity.Rules) + { + if (rule.HasIdentity) + { + var count = Database.Update(dto.Rules.Single(x => x.Id == rule.Key)); + if (count == 0) + { + throw new InvalidOperationException("No rows were updated for the access rule"); + } + } + else + { + Database.Insert(new AccessRuleDto + { + Id = rule.Key, + AccessId = dto.Id, + RuleValue = rule.RuleValue, + RuleType = rule.RuleType, + CreateDate = rule.CreateDate, + UpdateDate = rule.UpdateDate, + }); + + // update the id so HasEntity is correct + rule.Id = rule.Key.GetHashCode(); + } + } + + entity.ResetDirtyProperties(); + } + + protected override Guid GetEntityId(PublicAccessEntry entity) => entity.Key; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs index 72d7d2dfcc..630dd187c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Specifies the type of base query. +/// +public enum QueryType { /// - /// Specifies the type of base query. + /// Get one single complete item. /// - public enum QueryType - { - /// - /// Get one single complete item. - /// - Single, + Single, - /// - /// Get many complete items. - /// - Many, + /// + /// Get many complete items. + /// + Many, - /// - /// Get item identifiers only. - /// - Ids, + /// + /// Get item identifiers only. + /// + Ids, - /// - /// Count items. - /// - Count - } + /// + /// Count items. + /// + Count, } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index e49ccbdf77..1c2da42561 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,226 +11,266 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository { - internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository + public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger) - : base(scopeAccessor, cache, logger) + } + + public IRedirectUrl? Get(string url, Guid contentKey, string? culture) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false).Where(x => + x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); + RedirectUrlDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); + + public void DeleteContentUrls(Guid contentKey) => + Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); + + public void Delete(Guid id) => Database.Delete(id); + + public IRedirectUrl? GetMostRecentUrl(string url) + { + Sql sql = GetMostRecentSql(url); + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public async Task GetMostRecentUrlAsync(string url) + { + Sql sql = GetMostRecentSql(url); + List dtos = await Database.FetchAsync(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + private Sql GetMostRecentSql(string url) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash) + .OrderByDescending(x => x.CreateDateUtc); + return sql; + } + + public IRedirectUrl? GetMostRecentUrl(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) { + return GetMostRecentUrl(url); } - public IRedirectUrl? Get(string url, Guid contentKey, string? culture) + Sql sql = GetMostRecentUrlSql(url, culture); + + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); + + if (dto == null) { - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false).Where(x => - x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); - RedirectUrlDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); + dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); } - public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); + return dto == null ? null : Map(dto); + } - public void DeleteContentUrls(Guid contentKey) => - Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); + private Sql GetMostRecentUrlSql(string url, string culture) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash && + (x.Culture == culture.ToLower() || x.Culture == null || + x.Culture == string.Empty)) + .OrderByDescending(x => x.CreateDateUtc); + return sql; + } - public void Delete(Guid id) => Database.Delete(id); - - public IRedirectUrl? GetMostRecentUrl(string url) + public async Task GetMostRecentUrlAsync(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) { - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - RedirectUrlDto? dto = dtos.FirstOrDefault(); - return dto == null ? null : Map(dto); + return GetMostRecentUrl(url); } - public IRedirectUrl? GetMostRecentUrl(string url, string culture) + Sql sql = GetMostRecentUrlSql(url, culture); + + List dtos = await Database.FetchAsync(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); + + if (dto == null) { - if (string.IsNullOrWhiteSpace(culture)) - { - return GetMostRecentUrl(url); - } - - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash && - (x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty)) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); - - if (dto == null) - { - dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); - } - - return dto == null ? null : Map(dto); + dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); } - public IEnumerable GetContentUrls(Guid contentKey) + return dto == null ? null : Map(dto); + } + + public IEnumerable GetContentUrls(Guid contentKey) + { + Sql sql = GetBaseQuery(false) + .Where(x => x.ContentKey == contentKey) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + return dtos.Select(Map).WhereNotNull(); + } + + public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + return result.Items.Select(Map).WhereNotNull(); + } + + public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), + SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map).WhereNotNull(); + return rules; + } + + public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), + SqlSyntax.GetQuotedColumnName("Url")), + new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map).WhereNotNull(); + return rules; + } + + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override bool PerformExists(Guid id) => PerformGet(id) != null; + + protected override IRedirectUrl? PerformGet(Guid id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + RedirectUrlDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + if (ids?.Length > Constants.Sql.MaxParameterCount) { - Sql sql = GetBaseQuery(false) - .Where(x => x.ContentKey == contentKey) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - return dtos.Select(Map).WhereNotNull(); + throw new NotSupportedException( + $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); } - public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - Sql sql = GetBaseQuery(false) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - return result.Items.Select(Map).WhereNotNull(); - } - - public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) - { - Sql sql = GetBaseQuery(false) - .Where( - string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), - SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - IEnumerable rules = result.Items.Select(Map).WhereNotNull(); - return rules; - } - - public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) - { - Sql sql = GetBaseQuery(false) - .Where( - string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), - SqlSyntax.GetQuotedColumnName("Url")), - new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - IEnumerable rules = result.Items.Select(Map).WhereNotNull(); - return rules; - } - - protected override int PerformCount(IQuery query) => - throw new NotSupportedException("This repository does not support this method."); - - protected override bool PerformExists(Guid id) => PerformGet(id) != null; - - protected override IRedirectUrl? PerformGet(Guid id) - { - Sql sql = GetBaseQuery(false).Where(x => x.Id == id); - RedirectUrlDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - if (ids?.Length > Constants.Sql.MaxParameterCount) - { - throw new NotSupportedException( - $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); - } - - Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - List dtos = Database.Fetch(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new NotSupportedException("This repository does not support this method."); - - protected override Sql GetBaseQuery(bool isCount) - { - Sql sql = Sql(); - if (isCount) - { - sql.Select(@"COUNT(*) + sql.Select(@"COUNT(*) FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); - } - else - { - sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId + } + else + { + sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); - } - - return sql; } - protected override string GetBaseWhereClause() => "id = @id"; + return sql; + } - protected override IEnumerable GetDeleteClauses() + protected override string GetBaseWhereClause() => "id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoRedirectUrl WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(IRedirectUrl entity) + { + RedirectUrlDto? dto = Map(entity); + Database.Insert(dto); + entity.Id = entity.Key.GetHashCode(); + } + + protected override void PersistUpdatedItem(IRedirectUrl entity) + { + RedirectUrlDto? dto = Map(entity); + if (dto is not null) { - var list = new List { "DELETE FROM umbracoRedirectUrl WHERE id = @id" }; - return list; + Database.Update(dto); + } + } + + private static RedirectUrlDto? Map(IRedirectUrl redirectUrl) + { + if (redirectUrl == null) + { + return null; } - protected override void PersistNewItem(IRedirectUrl entity) + return new RedirectUrlDto { - RedirectUrlDto? dto = Map(entity); - Database.Insert(dto); - entity.Id = entity.Key.GetHashCode(); + Id = redirectUrl.Key, + ContentKey = redirectUrl.ContentKey, + CreateDateUtc = redirectUrl.CreateDateUtc, + Url = redirectUrl.Url, + Culture = redirectUrl.Culture, + UrlHash = redirectUrl.Url.GenerateHash(), + }; + } + + private static IRedirectUrl? Map(RedirectUrlDto dto) + { + if (dto == null) + { + return null; } - protected override void PersistUpdatedItem(IRedirectUrl entity) + var url = new RedirectUrl(); + try { - RedirectUrlDto? dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } + url.DisableChangeTracking(); + url.Key = dto.Id; + url.Id = dto.Id.GetHashCode(); + url.ContentId = dto.ContentId; + url.ContentKey = dto.ContentKey; + url.CreateDateUtc = dto.CreateDateUtc; + url.Culture = dto.Culture; + url.Url = dto.Url; + return url; } - - private static RedirectUrlDto? Map(IRedirectUrl redirectUrl) + finally { - if (redirectUrl == null) - { - return null; - } - - return new RedirectUrlDto - { - Id = redirectUrl.Key, - ContentKey = redirectUrl.ContentKey, - CreateDateUtc = redirectUrl.CreateDateUtc, - Url = redirectUrl.Url, - Culture = redirectUrl.Culture, - UrlHash = redirectUrl.Url.GenerateHash() - }; - } - - private static IRedirectUrl? Map(RedirectUrlDto dto) - { - if (dto == null) - { - return null; - } - - var url = new RedirectUrl(); - try - { - url.DisableChangeTracking(); - url.Key = dto.Id; - url.Id = dto.Id.GetHashCode(); - url.ContentId = dto.ContentId; - url.ContentKey = dto.ContentKey; - url.CreateDateUtc = dto.CreateDateUtc; - url.Culture = dto.Culture; - url.Url = dto.Url; - return url; - } - finally - { - url.EnableChangeTracking(); - } + url.EnableChangeTracking(); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index d7e65adaf4..88f1a6fee9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -19,176 +15,214 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class RelationRepository : EntityRepositoryBase, IRelationRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class RelationRepository : EntityRepositoryBase, IRelationRepository + private readonly IEntityRepositoryExtended _entityRepository; + private readonly IRelationTypeRepository _relationTypeRepository; + + public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository) + : base(scopeAccessor, AppCaches.NoCache, logger) { - private readonly IRelationTypeRepository _relationTypeRepository; - private readonly IEntityRepositoryExtended _entityRepository; + _relationTypeRepository = relationTypeRepository; + _entityRepository = entityRepository; + } - public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository) - : base(scopeAccessor, AppCaches.NoCache, logger) + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + => GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + => GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + + public void Save(IEnumerable relations) + { + foreach (IGrouping hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) { - _relationTypeRepository = relationTypeRepository; - _entityRepository = entityRepository; - } - - #region Overrides of RepositoryBase - - protected override IRelation? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id }); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - if (dto == null) - return null; - - var relationType = _relationTypeRepository.Get(dto.RelationType); - if (relationType == null) - throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); - - return DtoToEntity(dto, relationType); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false); - if (ids?.Length > 0) - sql.WhereIn(x => x.Id, ids); - sql.OrderBy(x => x.RelationType); - var dtos = Database.Fetch(sql); - return DtosToEntities(dtos); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql.OrderBy(x => x.RelationType); - var dtos = Database.Fetch(sql); - return DtosToEntities(dtos); - } - - private IEnumerable DtosToEntities(IEnumerable dtos) - { - //NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter - - return dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).WhereNotNull().ToList(); - } - - private static IRelation? DtoToEntity(RelationDto dto, IRelationType? relationType) - { - if (relationType is null) + if (hasIdentityGroup.Key) { - return null; + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + IRelation[] asArray = hasIdentityGroup.ToArray(); + foreach (IRelation relation in hasIdentityGroup) + { + relation.UpdatingEntity(); + RelationDto dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + + PopulateObjectTypes(asArray); } - var entity = RelationFactory.BuildEntity(dto, relationType); + else + { + // Do bulk inserts + var entitiesAndDtos = hasIdentityGroup.ToDictionary( + r => // key = entity + { + r.AddingEntity(); + return r; + }, + RelationFactory.BuildDto); // value = DTO - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); + foreach (RelationDto dto in entitiesAndDtos.Values) + { + Database.Insert(dto); + } - return entity; + // All dtos now have IDs assigned + foreach (KeyValuePair de in entitiesAndDtos) + { + // re-assign ID to the entity + de.Key.Id = de.Value.Id; + } + + PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); + } + } + } + + public void SaveBulk(IEnumerable relations) + { + foreach (IGrouping hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) + { + if (hasIdentityGroup.Key) + { + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + foreach (ReadOnlyRelation relation in hasIdentityGroup) + { + RelationDto dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + } + else + { + // Do bulk inserts + IEnumerable dtos = hasIdentityGroup.Select(RelationFactory.BuildDto); + + Database.InsertBulk(dtos); + } + } + } + + public IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering) + { + Sql sql = GetBaseQuery(false); + + if (ordering == null || ordering.IsEmpty) + { + ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Constants.DatabaseSchema.Tables.Relation, "id")); } - #endregion + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); - #region Overrides of EntityRepositoryBase + // apply ordering + ApplyOrdering(ref sql, ordering); - protected override Sql GetBaseQuery(bool isCount) + var pageIndexToFetch = pageIndex + 1; + Page? page = Database.Page(pageIndexToFetch, pageSize, sql); + List? dtos = page.Items; + totalRecords = page.TotalItems; + + var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray())? + .ToDictionary(x => x.Id, x => x); + + var result = dtos.Select(r => { - if (isCount) + if (relTypes is null || !relTypes.TryGetValue(r.RelationType, out IRelationType? relType)) { - return Sql().SelectCount().From(); + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); } - var sql = Sql().Select() - .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) - .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) + return DtoToEntity(r, relType); + }).WhereNotNull().ToList(); + + return result; + } + + public void DeleteByParent(int parentId, params string[] relationTypeAliases) + { + // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. + if (Database.DatabaseType.IsSqlite()) + { + Sql? query = Sql().Append(@"delete from umbracoRelation"); + + Sql subQuery = Sql().Select(x => x.Id) .From() - .InnerJoin("uchild").On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") - .InnerJoin("uparent").On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == parentId); + if (relationTypeAliases.Length > 0) + { + subQuery.WhereIn(x => x.Alias, relationTypeAliases); + } - return sql; + Sql fullQuery = query.WhereIn(x => x.Id, subQuery); + + Database.Execute(fullQuery); } - - protected override string GetBaseWhereClause() + else { - return $"{Constants.DatabaseSchema.Tables.Relation}.id = @id"; + if (relationTypeAliases.Length > 0) + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.RelationRepository.DeleteByParentIn, + tsql => Sql().Delete() + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == SqlTemplate.Arg("parentId")) + .WhereIn(x => x.Alias, SqlTemplate.ArgIn("relationTypeAliases"))); + + Sql sql = template.Sql(parentId, relationTypeAliases); + + Database.Execute(sql); + } + else + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.RelationRepository.DeleteByParentAll, + tsql => Sql().Delete() + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == SqlTemplate.Arg("parentId"))); + + Sql sql = template.Sql(parentId); + + Database.Execute(sql); + } } + } - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoRelation WHERE id = @id" - }; - return list; - } + /// + /// Used for joining the entity query with relations for the paging methods + /// + /// + private void SqlJoinRelations(Sql sql) + { + // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for + // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). + sql.LeftJoin() + .On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); + sql.LeftJoin() + .On((left, right) => left.RelationType == right.Id); + } - #endregion + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) => - #region Unit of Work Implementation - - protected override void PersistNewItem(IRelation entity) - { - entity.AddingEntity(); - - var dto = RelationFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - - entity.Id = id; - PopulateObjectTypes(entity); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IRelation entity) - { - entity.UpdatingEntity(); - - var dto = RelationFactory.BuildDto(entity); - Database.Update(dto); - - PopulateObjectTypes(entity); - - entity.ResetDirtyProperties(); - } - - #endregion - - /// - /// Used for joining the entity query with relations for the paging methods - /// - /// - private void SqlJoinRelations(Sql sql) - { - // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for - // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). - sql.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); - sql.LeftJoin().On((left, right) => left.RelationType == right.Id); - } - - public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) - { - return GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); - } - - public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) - { - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } - // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data - // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it - // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we - // will just return the bare minimum entity data. - - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); @@ -200,22 +234,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.WhereIn(rel => rel.RelationType, relationTypes); } }); - } - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) - { - return GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); - } + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) => - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) - { - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } - // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data - // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it - // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we - // will just return the bare minimum entity data. - - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); @@ -227,241 +254,220 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.WhereIn(rel => rel.RelationType, relationTypes); } }); - } - public void Save(IEnumerable relations) - { - foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) - { - if (hasIdentityGroup.Key) - { - // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation - // however we can bulk populate the object types. It might be possible to bulk update - // with SQL but would be pretty ugly and we're not really too worried about that for perf, - // it's the bulk inserts we care about. - var asArray = hasIdentityGroup.ToArray(); - foreach (var relation in hasIdentityGroup) - { - relation.UpdatingEntity(); - var dto = RelationFactory.BuildDto(relation); - Database.Update(dto); - } - PopulateObjectTypes(asArray); - } - else - { - // Do bulk inserts - var entitiesAndDtos = hasIdentityGroup.ToDictionary( - r => // key = entity - { - r.AddingEntity(); - return r; - }, - RelationFactory.BuildDto); // value = DTO - - - foreach (var dto in entitiesAndDtos.Values) - { - Database.Insert(dto); - } - - // All dtos now have IDs assigned - foreach (var de in entitiesAndDtos) - { - // re-assign ID to the entity - de.Key.Id = de.Value.Id; - } - - PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); - } - } - } - - public void SaveBulk(IEnumerable relations) - { - foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) - { - if (hasIdentityGroup.Key) - { - // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation - // however we can bulk populate the object types. It might be possible to bulk update - // with SQL but would be pretty ugly and we're not really too worried about that for perf, - // it's the bulk inserts we care about. - foreach (var relation in hasIdentityGroup) - { - var dto = RelationFactory.BuildDto(relation); - Database.Update(dto); - } - } - else - { - // Do bulk inserts - var dtos = hasIdentityGroup.Select(RelationFactory.BuildDto); - - Database.InsertBulk(dtos); - - } - } - } - - public IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering) - { - var sql = GetBaseQuery(false); - - if (ordering == null || ordering.IsEmpty) - ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Cms.Core.Constants.DatabaseSchema.Tables.Relation, "id")); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - - // apply ordering - ApplyOrdering(ref sql, ordering); - - var pageIndexToFetch = pageIndex + 1; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - var dtos = page.Items; - totalRecords = page.TotalItems; - - var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray())? - .ToDictionary(x => x.Id, x => x); - - var result = dtos.Select(r => - { - if (relTypes is null || !relTypes.TryGetValue(r.RelationType, out var relType)) - throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); - return DtoToEntity(r, relType); - }).WhereNotNull().ToList(); - - return result; - } - - - public void DeleteByParent(int parentId, params string[] relationTypeAliases) - { - // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. - if (Database.DatabaseType.IsSqlite()) - { - var query = Sql().Append(@"delete from umbracoRelation"); - - var subQuery = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == parentId); - - if (relationTypeAliases.Length > 0) - { - subQuery.WhereIn(x => x.Alias, relationTypeAliases); - } - - var fullQuery = query.WhereIn(x => x.Id, subQuery); - - Database.Execute(fullQuery); - } - else - { - if (relationTypeAliases.Length > 0) - { - var template = SqlContext.Templates.Get( - Cms.Core.Constants.SqlTemplates.RelationRepository.DeleteByParentIn, - tsql => Sql().Delete() - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == SqlTemplate.Arg("parentId")) - .WhereIn(x => x.Alias, SqlTemplate.ArgIn("relationTypeAliases"))); - - var sql = template.Sql(parentId, relationTypeAliases); - - Database.Execute(sql); - } - else - { - var template = SqlContext.Templates.Get( - Cms.Core.Constants.SqlTemplates.RelationRepository.DeleteByParentAll, - tsql => Sql().Delete() - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == SqlTemplate.Arg("parentId"))); - - var sql = template.Sql(parentId); - - Database.Execute(sql); - } - } - } - - /// - /// Used to populate the object types after insert/update - /// - /// - private void PopulateObjectTypes(params IRelation[] entities) - { - var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); - - var nodes = Database.Fetch(Sql().Select().From() - .WhereIn(x => x.NodeId, entityIds)) - .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - - foreach (var e in entities) - { - if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) - { - e.ParentObjectType = parentObjectType.GetValueOrDefault(); - } - if (nodes.TryGetValue(e.ChildId, out var childObjectType)) - { - e.ChildObjectType = childObjectType.GetValueOrDefault(); - } - } - } - - private void ApplyOrdering(ref Sql sql, Ordering ordering) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - - // TODO: although this works for name, it probably doesn't work for others without an alias of some sort - var orderBy = ordering.OrderBy; - - if (ordering.Direction == Direction.Ascending) - sql.OrderBy(orderBy); - else - sql.OrderByDescending(orderBy); - } - } - - internal class RelationItemDto + /// + /// Used to populate the object types after insert/update + /// + /// + private void PopulateObjectTypes(params IRelation[] entities) { - [Column(Name = "nodeId")] - public int ChildNodeId { get; set; } + IEnumerable entityIds = + entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); - [Column(Name = "nodeKey")] - public Guid ChildNodeKey { get; set; } + var nodes = Database.Fetch(Sql().Select().From() + .WhereIn(x => x.NodeId, entityIds)) + .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - [Column(Name = "nodeName")] - public string? ChildNodeName { get; set; } + foreach (IRelation e in entities) + { + if (nodes.TryGetValue(e.ParentId, out Guid? parentObjectType)) + { + e.ParentObjectType = parentObjectType.GetValueOrDefault(); + } - [Column(Name = "nodeObjectType")] - public Guid ChildNodeObjectType { get; set; } - - [Column(Name = "contentTypeIcon")] - public string? ChildContentTypeIcon { get; set; } - - [Column(Name = "contentTypeAlias")] - public string? ChildContentTypeAlias { get; set; } - - [Column(Name = "contentTypeName")] - public string? ChildContentTypeName { get; set; } - - [Column(Name = "relationTypeName")] - public string? RelationTypeName { get; set; } - - [Column(Name = "relationTypeAlias")] - public string? RelationTypeAlias { get; set; } - - [Column(Name = "relationTypeIsDependency")] - public bool RelationTypeIsDependency { get; set; } - - [Column(Name = "relationTypeIsBidirectional")] - public bool RelationTypeIsBidirectional { get; set; } + if (nodes.TryGetValue(e.ChildId, out Guid? childObjectType)) + { + e.ChildObjectType = childObjectType.GetValueOrDefault(); + } + } } + + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } + + // TODO: although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; + + if (ordering.Direction == Direction.Ascending) + { + sql.OrderBy(orderBy); + } + else + { + sql.OrderByDescending(orderBy); + } + } + + #region Overrides of RepositoryBase + + protected override IRelation? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + + RelationDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + if (dto == null) + { + return null; + } + + IRelationType? relationType = _relationTypeRepository.Get(dto.RelationType); + if (relationType == null) + { + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); + } + + return DtoToEntity(dto, relationType); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + if (ids?.Length > 0) + { + sql.WhereIn(x => x.Id, ids); + } + + sql.OrderBy(x => x.RelationType); + List? dtos = Database.Fetch(sql); + return DtosToEntities(dtos); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.RelationType); + List? dtos = Database.Fetch(sql); + return DtosToEntities(dtos); + } + + private IEnumerable DtosToEntities(IEnumerable dtos) => + + // NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter + dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).WhereNotNull().ToList(); + + private static IRelation? DtoToEntity(RelationDto dto, IRelationType? relationType) + { + if (relationType is null) + { + return null; + } + + IRelation entity = RelationFactory.BuildEntity(dto, relationType); + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + + return entity; + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) + { + return Sql().SelectCount().From(); + } + + Sql sql = Sql().Select() + .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) + .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) + .From() + .InnerJoin("uchild") + .On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") + .InnerJoin("uparent") + .On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Relation}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoRelation WHERE id = @id" }; + return list; + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IRelation entity) + { + entity.AddingEntity(); + + RelationDto dto = RelationFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + + entity.Id = id; + PopulateObjectTypes(entity); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IRelation entity) + { + entity.UpdatingEntity(); + + RelationDto dto = RelationFactory.BuildDto(entity); + Database.Update(dto); + + PopulateObjectTypes(entity); + + entity.ResetDirtyProperties(); + } + + #endregion +} + +internal class RelationItemDto +{ + [Column(Name = "nodeId")] + public int ChildNodeId { get; set; } + + [Column(Name = "nodeKey")] + public Guid ChildNodeKey { get; set; } + + [Column(Name = "nodeName")] + public string? ChildNodeName { get; set; } + + [Column(Name = "nodeObjectType")] + public Guid ChildNodeObjectType { get; set; } + + [Column(Name = "contentTypeIcon")] + public string? ChildContentTypeIcon { get; set; } + + [Column(Name = "contentTypeAlias")] + public string? ChildContentTypeAlias { get; set; } + + [Column(Name = "contentTypeName")] + public string? ChildContentTypeName { get; set; } + + [Column(Name = "relationTypeName")] + public string? RelationTypeName { get; set; } + + [Column(Name = "relationTypeAlias")] + public string? RelationTypeAlias { get; set; } + + [Column(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } + + [Column(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs index 0d1b258374..af7458bab0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,151 +12,147 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class RelationTypeRepository : EntityRepositoryBase, IRelationTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class RelationTypeRepository : EntityRepositoryBase, IRelationTypeRepository + public RelationTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public RelationTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + private void CheckNullObjectTypeValues(IRelationType entity) + { + if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + entity.ParentObjectType = null; } - #region Overrides of RepositoryBase - - protected override IRelationType? PerformGet(int id) + if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) { - // use the underlying GetAll which will force cache all content types - return GetMany()?.FirstOrDefault(x => x.Id == id); - } - - public IRelationType? Get(Guid id) - { - // use the underlying GetAll which will force cache all content types - return GetMany()?.FirstOrDefault(x => x.Key == id); - } - - public bool Exists(Guid id) - { - return Get(id) != null; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => DtoToEntity(x)); - } - - public IEnumerable GetMany(params Guid[]? ids) - { - // should not happen due to the cache policy - if (ids?.Any() ?? false) - throw new NotImplementedException(); - - return GetMany(new int[0]); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => DtoToEntity(x)); - } - - private static IRelationType DtoToEntity(RelationTypeDto dto) - { - var entity = RelationTypeFactory.BuildEntity(dto); - - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase) entity).ResetDirtyProperties(false); - - return entity; - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From(); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.RelationType}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoRelation WHERE relType = @id", - "DELETE FROM umbracoRelationType WHERE id = @id" - }; - return list; - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IRelationType entity) - { - entity.AddingEntity(); - - CheckNullObjectTypeValues(entity); - - var dto = RelationTypeFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IRelationType entity) - { - entity.UpdatingEntity(); - - CheckNullObjectTypeValues(entity); - - var dto = RelationTypeFactory.BuildDto(entity); - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - #endregion - - private void CheckNullObjectTypeValues(IRelationType entity) - { - if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) - entity.ParentObjectType = null; - if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) - entity.ChildObjectType = null; + entity.ChildObjectType = null; } } + + #region Overrides of RepositoryBase + + protected override IRelationType? PerformGet(int id) => + + // use the underlying GetAll which will force cache all content types + GetMany()?.FirstOrDefault(x => x.Id == id); + + public IRelationType? Get(Guid id) => + + // use the underlying GetAll which will force cache all content types + GetMany()?.FirstOrDefault(x => x.Key == id); + + public bool Exists(Guid id) => Get(id) != null; + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => DtoToEntity(x)); + } + + public IEnumerable GetMany(params Guid[]? ids) + { + // should not happen due to the cache policy + if (ids?.Any() ?? false) + { + throw new NotImplementedException(); + } + + return GetMany(new int[0]); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => DtoToEntity(x)); + } + + private static IRelationType DtoToEntity(RelationTypeDto dto) + { + IRelationType entity = RelationTypeFactory.BuildEntity(dto); + + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); + + return entity; + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.RelationType}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoRelation WHERE relType = @id", "DELETE FROM umbracoRelationType WHERE id = @id", + }; + return list; + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IRelationType entity) + { + entity.AddingEntity(); + + CheckNullObjectTypeValues(entity); + + RelationTypeDto dto = RelationTypeFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IRelationType entity) + { + entity.UpdatingEntity(); + + CheckNullObjectTypeValues(entity); + + RelationTypeDto dto = RelationTypeFactory.BuildDto(entity); + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs index fdcf11304b..7a1f4a2677 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs @@ -1,4 +1,3 @@ -using System; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence; @@ -6,77 +5,76 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Base repository class for all instances +/// +public abstract class RepositoryBase : IRepository { /// - /// Base repository class for all instances + /// Initializes a new instance of the class. /// - public abstract class RepositoryBase : IRepository + protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches) { - /// - /// Initializes a new instance of the class. - /// - protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches) - { - ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); - } - - /// - /// Gets the - /// - protected AppCaches AppCaches { get; } - - /// - /// Gets the - /// - protected IScopeAccessor ScopeAccessor { get; } - - /// - /// Gets the AmbientScope - /// - protected IScope AmbientScope - { - get - { - IScope? scope = ScopeAccessor.AmbientScope; - if (scope == null) - { - throw new InvalidOperationException("Cannot run a repository without an ambient scope."); - } - - return scope; - } - } - - /// - /// Gets the repository's database. - /// - protected IUmbracoDatabase Database => AmbientScope.Database; - - /// - /// Gets the Sql context. - /// - protected ISqlContext SqlContext => AmbientScope.SqlContext; - - /// - /// Gets the - /// - protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; - - /// - /// Creates an expression - /// - protected Sql Sql() => SqlContext.Sql(); - - /// - /// Creates a expression - /// - protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); - - /// - /// Creates a new query expression - /// - protected IQuery Query() => SqlContext.Query(); + ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); } + + /// + /// Gets the + /// + protected AppCaches AppCaches { get; } + + /// + /// Gets the + /// + protected IScopeAccessor ScopeAccessor { get; } + + /// + /// Gets the AmbientScope + /// + protected IScope AmbientScope + { + get + { + IScope? scope = ScopeAccessor.AmbientScope; + if (scope == null) + { + throw new InvalidOperationException("Cannot run a repository without an ambient scope."); + } + + return scope; + } + } + + /// + /// Gets the repository's database. + /// + protected IUmbracoDatabase Database => AmbientScope.Database; + + /// + /// Gets the Sql context. + /// + protected ISqlContext SqlContext => AmbientScope.SqlContext; + + /// + /// Gets the + /// + protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; + + /// + /// Creates an expression + /// + protected Sql Sql() => SqlContext.Sql(); + + /// + /// Creates a expression + /// + protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); + + /// + /// Creates a new query expression + /// + protected IQuery Query() => SqlContext.Query(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs index e50f29fd87..3094d0d04e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs @@ -1,102 +1,99 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Script Repository +/// +internal class ScriptRepository : FileRepository, IScriptRepository { - /// - /// Represents the Script Repository - /// - internal class ScriptRepository : FileRepository, IScriptRepository + public ScriptRepository(FileSystems fileSystems) + : base(fileSystems.ScriptsFileSystem) { - public ScriptRepository(FileSystems fileSystems) - : base(fileSystems.ScriptsFileSystem) + } + + public override IScript? Get(string? id) + { + if (id is null || FileSystem is null) { + return null; } - #region Implementation of IRepository + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id); - public override IScript? Get(string? id) + if (FileSystem.FileExists(path) == false) { - if (id is null || FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id); - - if (FileSystem.FileExists(path) == false) - return null; - - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); - - var script = new Script(path, file => GetFileContent(file.OriginalPath)) - { - //id can be the hash - Id = path.GetHashCode(), - Key = path.EncodeAsGuid(), - //Content = content, - CreateDate = created, - UpdateDate = updated, - VirtualPath = FileSystem.GetUrl(path) - }; - - // reset dirty initial properties (U4-1946) - script.ResetDirtyProperties(false); - - return script; + return null; } - public override void Save(IScript entity) + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; + + var script = new Script(path, file => GetFileContent(file.OriginalPath)) { - // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later - var script = (Script) entity; + // id can be the hash + Id = path.GetHashCode(), + Key = path.EncodeAsGuid(), - base.Save(script); + // Content = content, + CreateDate = created, + UpdateDate = updated, + VirtualPath = FileSystem.GetUrl(path), + }; - // ensure that from now on, content is lazy-loaded - if (script.GetFileContent == null) - script.GetFileContent = file => GetFileContent(file.OriginalPath); + // reset dirty initial properties (U4-1946) + script.ResetDirtyProperties(false); + + return script; + } + + public override void Save(IScript entity) + { + // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later + var script = (Script)entity; + + base.Save(script); + + // ensure that from now on, content is lazy-loaded + if (script.GetFileContent == null) + { + script.GetFileContent = file => GetFileContent(file.OriginalPath); } + } - public override IEnumerable GetMany(params string[]? ids) + public override IEnumerable GetMany(params string[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct().ToArray(); + + if (ids?.Any() ?? false) { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct().ToArray(); - - if (ids?.Any() ?? false) + foreach (var id in ids) { - foreach (var id in ids) + IScript? script = Get(id); + if (script is not null) { - IScript? script = Get(id); - if (script is not null) - { - yield return script; - } - } - } - else - { - var files = FindAllFiles("", "*.*"); - foreach (var file in files) - { - IScript? script = Get(file); - if (script is not null) - { - yield return script; - } + yield return script; + } + } + } + else + { + IEnumerable files = FindAllFiles(string.Empty, "*.*"); + foreach (var file in files) + { + IScript? script = Get(file); + if (script is not null) + { + yield return script; } } } - - #endregion } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 2a0bc9bfa1..b6d5221b59 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -1,129 +1,112 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class ServerRegistrationRepository : EntityRepositoryBase, + IServerRegistrationRepository { - internal class ServerRegistrationRepository : EntityRepositoryBase, IServerRegistrationRepository + public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() - { - // TODO: what are we doing with cache here? - // why are we using disabled cache helper up there? - // - // 7.6 says: - // note: this means that the ServerRegistrationRepository does *not* implement scoped cache, - // and this is because the repository is special and should not participate in scopes - // (cleanup in v8) - // - return new FullDataSetRepositoryCachePolicy(AppCaches.RuntimeCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - } + public void ClearCache() => CachePolicy.ClearAll(); - public void ClearCache() - { - CachePolicy.ClearAll(); - } + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + DateTime timeoutDate = DateTime.Now.Subtract(staleTimeout); - protected override int PerformCount(IQuery query) - { - throw new NotSupportedException("This repository does not support this method."); - } + Database.Update( + "SET isActive=0, isSchedulingPublisher=0 WHERE lastNotifiedDate < @timeoutDate", new + { + /*timeoutDate =*/ + timeoutDate, + }); + ClearCache(); + } - protected override bool PerformExists(int id) - { - // use the underlying GetAll which force-caches all registrations - return GetMany()?.Any(x => x.Id == id) ?? false; - } + protected override IRepositoryCachePolicy CreateCachePolicy() => - protected override IServerRegistration? PerformGet(int id) - { - // use the underlying GetAll which force-caches all registrations - return GetMany()?.FirstOrDefault(x => x.Id == id); - } + // TODO: what are we doing with cache here? + // why are we using disabled cache helper up there? + // + // 7.6 says: + // note: this means that the ServerRegistrationRepository does *not* implement scoped cache, + // and this is because the repository is special and should not participate in scopes + // (cleanup in v8) + new FullDataSetRepositoryCachePolicy(AppCaches.RuntimeCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - return Database.Fetch("WHERE id > 0") - .Select(x => ServerRegistrationFactory.BuildEntity(x)); - } + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotSupportedException("This repository does not support this method."); - } + protected override bool PerformExists(int id) => - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); + // use the underlying GetAll which force-caches all registrations + GetMany().Any(x => x.Id == id); - sql = isCount - ? sql.SelectCount() - : sql.Select(); + protected override IServerRegistration? PerformGet(int id) => - sql - .From(); + // use the underlying GetAll which force-caches all registrations + GetMany().FirstOrDefault(x => x.Id == id); - return sql; - } + protected override IEnumerable PerformGetAll(params int[]? ids) => + Database.Fetch("WHERE id > 0") + .Select(x => ServerRegistrationFactory.BuildEntity(x)); - protected override string GetBaseWhereClause() - { - return "id = @id"; - } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoServer WHERE id = @id" - }; - return list; - } + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); - protected override void PersistNewItem(IServerRegistration entity) - { - entity.AddingEntity(); + sql = isCount + ? sql.SelectCount() + : sql.Select(); - var dto = ServerRegistrationFactory.BuildDto(entity); + sql + .From(); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + return sql; + } - entity.ResetDirtyProperties(); - } + protected override string GetBaseWhereClause() => "id = @id"; - protected override void PersistUpdatedItem(IServerRegistration entity) - { - entity.UpdatingEntity(); + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoServer WHERE id = @id" }; + return list; + } - var dto = ServerRegistrationFactory.BuildDto(entity); + protected override void PersistNewItem(IServerRegistration entity) + { + entity.AddingEntity(); - Database.Update(dto); + ServerRegistrationDto dto = ServerRegistrationFactory.BuildDto(entity); - entity.ResetDirtyProperties(); - } + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; - public void DeactiveStaleServers(TimeSpan staleTimeout) - { - var timeoutDate = DateTime.Now.Subtract(staleTimeout); + entity.ResetDirtyProperties(); + } - Database.Update("SET isActive=0, isSchedulingPublisher=0 WHERE lastNotifiedDate < @timeoutDate", new { /*timeoutDate =*/ timeoutDate }); - ClearCache(); - } + protected override void PersistUpdatedItem(IServerRegistration entity) + { + entity.UpdatingEntity(); + + ServerRegistrationDto dto = ServerRegistrationFactory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs index 2621461a85..9f4bc451c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs @@ -1,242 +1,233 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; using static Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement.SimilarNodeName; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal static class ListExtensions { - internal class SimilarNodeName + internal static bool Contains(this IEnumerable items, StructuredName model) => + items.Any(x => x.FullName.InvariantEquals(model.FullName)); + + internal static bool SimpleNameExists(this IEnumerable items, string name) => + items.Any(x => x.FullName.InvariantEquals(name)); + + internal static bool SuffixedNameExists(this IEnumerable items) => + items.Any(x => x.Suffix.HasValue); +} + +internal class SimilarNodeName +{ + public int Id { get; set; } + + public string? Name { get; set; } + + public static string? GetUniqueName(IEnumerable names, int nodeId, string? nodeName) { - public int Id { get; set; } - public string? Name { get; set; } + IEnumerable items = names + .Where(x => x.Id != nodeId) // ignore same node + .Select(x => x.Name); - public static string? GetUniqueName(IEnumerable names, int nodeId, string? nodeName) - { - var items = names - .Where(x => x.Id != nodeId) // ignore same node - .Select(x => x.Name); + var uniqueName = GetUniqueName(items, nodeName); - var uniqueName = GetUniqueName(items, nodeName); - - return uniqueName; - } - - public static string? GetUniqueName(IEnumerable names, string? name) - { - var model = new StructuredName(name); - var items = names - .Where(x => x?.InvariantStartsWith(model.Text) ?? false) // ignore non-matching names - .Select(x => new StructuredName(x)); - - // name is empty, and there are no other names with suffixes, so just return " (1)" - if (model.IsEmptyName() && !items.Any()) - { - model.Suffix = StructuredName.INITIAL_SUFFIX; - - return model.FullName; - } - - // name is empty, and there are other names with suffixes - if (model.IsEmptyName() && items.SuffixedNameExists()) - { - var emptyNameSuffix = GetSuffixNumber(items); - - if (emptyNameSuffix > 0) - { - model.Suffix = (uint?)emptyNameSuffix; - - return model.FullName; - } - } - - // no suffix - name without suffix does NOT exist - we can just use the name without suffix. - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) - { - model.Suffix = StructuredName.NO_SUFFIX; - - return model.FullName; - } - - // suffix - name with suffix does NOT exist - // We can just return the full name as it is as there's no conflict. - if (model.Suffix.HasValue && !items.SimpleNameExists(model.FullName)) - { - return model.FullName; - } - - // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) - { - model.Suffix = StructuredName.NO_SUFFIX; - - return model.FullName; - } - - // no suffix - name without suffix exists, however name with suffix does NOT exist - if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) - { - var firstSuffix = GetFirstSuffix(items); - model.Suffix = (uint?)firstSuffix; - - return model.FullName; - } - - // no suffix - name without suffix exists, AND name with suffix does exist - if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - // no suffix - name without suffix does NOT exist, however name with suffix exists - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - // has suffix - name without suffix exists - if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - // has suffix - name without suffix does NOT exist - // a case where the user added the suffix, so add a secondary suffix - if (model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) - { - model.Text = model.FullName; - model.Suffix = StructuredName.NO_SUFFIX; - - // filter items based on full name with suffix - items = items.Where(x => x.Text.InvariantStartsWith(model.FullName)); - var secondarySuffix = GetFirstSuffix(items); - model.Suffix = (uint?)secondarySuffix; - - return model.FullName; - } - - // has suffix - name without suffix also exists, therefore we simply increment - if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - return name; - } - - private static int GetFirstSuffix(IEnumerable items) - { - const int suffixStart = 1; - - if (!items.Any(x => x.Suffix == suffixStart)) - { - // none of the suffixes are the same as suffixStart, so we can use suffixStart! - return suffixStart; - } - - return GetSuffixNumber(items); - } - - private static int GetSuffixNumber(IEnumerable items) - { - int current = 1; - foreach (var item in items.OrderBy(x => x.Suffix)) - { - if (item.Suffix == current) - { - current++; - } - else if (item.Suffix > current) - { - // do nothing - we found our number! - // eg. when suffixes are 1 & 3, then this method is required to generate 2 - break; - } - } - - return current; - } - - internal class StructuredName - { - const string SPACE_CHARACTER = " "; - const string SUFFIXED_PATTERN = @"(.*) \(([1-9]\d*)\)$"; - internal const uint INITIAL_SUFFIX = 1; - internal static readonly uint? NO_SUFFIX = default; - - internal string Text { get; set; } - internal uint? Suffix { get; set; } - public string FullName - { - get - { - string text = (Text == SPACE_CHARACTER) ? Text.Trim() : Text; - - return Suffix > 0 ? $"{text} ({Suffix})" : text; - } - } - - internal StructuredName(string? name) - { - if (string.IsNullOrWhiteSpace(name)) - { - Text = SPACE_CHARACTER; - - return; - } - - var rg = new Regex(SUFFIXED_PATTERN); - var matches = rg.Matches(name); - if (matches.Count > 0) - { - var match = matches[0]; - Text = match.Groups[1].Value; - int number = int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) ? number : 0; - Suffix = (uint?)(number); - - return; - } - else - { - Text = name; - } - } - - internal bool IsEmptyName() - { - return string.IsNullOrWhiteSpace(Text); - } - } + return uniqueName; } - internal static class ListExtensions + public static string? GetUniqueName(IEnumerable names, string? name) { - internal static bool Contains(this IEnumerable items, StructuredName model) + var model = new StructuredName(name); + IEnumerable items = names + .Where(x => x?.InvariantStartsWith(model.Text) ?? false) // ignore non-matching names + .Select(x => new StructuredName(x)).ToArray(); + + // name is empty, and there are no other names with suffixes, so just return " (1)" + if (model.IsEmptyName() && !items.Any()) { - return items.Any(x => x.FullName.InvariantEquals(model.FullName)); + model.Suffix = StructuredName.Initialsuffix; + + return model.FullName; } - internal static bool SimpleNameExists(this IEnumerable items, string name) + // name is empty, and there are other names with suffixes + if (model.IsEmptyName() && items.SuffixedNameExists()) { - return items.Any(x => x.FullName.InvariantEquals(name)); + var emptyNameSuffix = GetSuffixNumber(items); + + if (emptyNameSuffix > 0) + { + model.Suffix = (uint?)emptyNameSuffix; + + return model.FullName; + } } - internal static bool SuffixedNameExists(this IEnumerable items) + // no suffix - name without suffix does NOT exist - we can just use the name without suffix. + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) { - return items.Any(x => x.Suffix.HasValue); + model.Suffix = StructuredName._nosuffix; + + return model.FullName; } + + // suffix - name with suffix does NOT exist + // We can just return the full name as it is as there's no conflict. + if (model.Suffix.HasValue && !items.SimpleNameExists(model.FullName)) + { + return model.FullName; + } + + // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) + { + model.Suffix = StructuredName._nosuffix; + + return model.FullName; + } + + // no suffix - name without suffix exists, however name with suffix does NOT exist + if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) + { + var firstSuffix = GetFirstSuffix(items); + model.Suffix = (uint?)firstSuffix; + + return model.FullName; + } + + // no suffix - name without suffix exists, AND name with suffix does exist + if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + // no suffix - name without suffix does NOT exist, however name with suffix exists + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + // has suffix - name without suffix exists + if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + // has suffix - name without suffix does NOT exist + // a case where the user added the suffix, so add a secondary suffix + if (model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) + { + model.Text = model.FullName; + model.Suffix = StructuredName._nosuffix; + + // filter items based on full name with suffix + items = items.Where(x => x.Text.InvariantStartsWith(model.FullName)); + var secondarySuffix = GetFirstSuffix(items); + model.Suffix = (uint?)secondarySuffix; + + return model.FullName; + } + + // has suffix - name without suffix also exists, therefore we simply increment + if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + return name; + } + + private static int GetFirstSuffix(IEnumerable items) + { + const int suffixStart = 1; + + if (!items.Any(x => x.Suffix == suffixStart)) + { + // none of the suffixes are the same as suffixStart, so we can use suffixStart! + return suffixStart; + } + + return GetSuffixNumber(items); + } + + private static int GetSuffixNumber(IEnumerable items) + { + var current = 1; + foreach (StructuredName item in items.OrderBy(x => x.Suffix)) + { + if (item.Suffix == current) + { + current++; + } + else if (item.Suffix > current) + { + // do nothing - we found our number! + // eg. when suffixes are 1 & 3, then this method is required to generate 2 + break; + } + } + + return current; + } + + internal class StructuredName + { + internal const uint Initialsuffix = 1; + private const string Spacecharacter = " "; + private const string Suffixedpattern = @"(.*) \(([1-9]\d*)\)$"; + internal static readonly uint? _nosuffix = default; + + internal StructuredName(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + Text = Spacecharacter; + + return; + } + + var rg = new Regex(Suffixedpattern); + MatchCollection matches = rg.Matches(name); + if (matches.Count > 0) + { + Match match = matches[0]; + Text = match.Groups[1].Value; + int number = int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) + ? number + : 0; + Suffix = (uint?)number; + + return; + } + + Text = name; + } + + public string FullName + { + get + { + var text = Text == Spacecharacter ? Text.Trim() : Text; + + return Suffix > 0 ? $"{text} ({Suffix})" : text; + } + } + + internal string Text { get; set; } + + internal uint? Suffix { get; set; } + + internal bool IsEmptyName() => string.IsNullOrWhiteSpace(Text); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs index bf798b2845..1fe1f1e82a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; @@ -10,87 +7,81 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +// TODO: Obsolete this, change all implementations of this like in Dictionary to just use custom Cache policies like in the member repository. + +/// +/// Simple abstract ReadOnly repository used to simply have PerformGet and PeformGetAll with an underlying cache +/// +internal abstract class SimpleGetRepository : EntityRepositoryBase + where TEntity : class, IEntity + where TDto : class { - // TODO: Obsolete this, change all implementations of this like in Dictionary to just use custom Cache policies like in the member repository. - - /// - /// Simple abstract ReadOnly repository used to simply have PerformGet and PeformGetAll with an underlying cache - /// - internal abstract class SimpleGetRepository : EntityRepositoryBase - where TEntity : class, IEntity - where TDto: class + protected SimpleGetRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) + : base(scopeAccessor, cache, logger) { - protected SimpleGetRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) - : base(scopeAccessor, cache, logger) - { } - - protected abstract TEntity ConvertToEntity(TDto dto); - protected abstract object GetBaseWhereClauseArguments(TId? id); - protected abstract string GetWhereInClauseForGetAll(); - - protected virtual IEnumerable PerformFetch(Sql sql) - { - return Database.Fetch(sql); - } - - protected override TEntity? PerformGet(TId? id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), GetBaseWhereClauseArguments(id)); - - var dto = PerformFetch(sql).FirstOrDefault(); - if (dto == null) - return null; - - var entity = ConvertToEntity(dto); - - if (entity is EntityBase dirtyEntity) - { - // reset dirty initial properties (U4-1946) - dirtyEntity.ResetDirtyProperties(false); - } - - return entity; - } - - protected override IEnumerable PerformGetAll(params TId[]? ids) - { - var sql = Sql().From(); - - if (ids?.Any() ?? false) - { - sql.Where(GetWhereInClauseForGetAll(), new { /*ids =*/ ids }); - } - - return Database.Fetch(sql).Select(ConvertToEntity); - } - - protected sealed override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConvertToEntity); - } - - #region Not implemented and not required - - protected sealed override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected sealed override void PersistNewItem(TEntity entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected sealed override void PersistUpdatedItem(TEntity entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion } + + protected abstract TEntity ConvertToEntity(TDto dto); + + protected abstract object GetBaseWhereClauseArguments(TId? id); + + protected abstract string GetWhereInClauseForGetAll(); + + protected virtual IEnumerable PerformFetch(Sql sql) => Database.Fetch(sql); + + protected override TEntity? PerformGet(TId? id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), GetBaseWhereClauseArguments(id)); + + TDto? dto = PerformFetch(sql).FirstOrDefault(); + if (dto == null) + { + return null; + } + + TEntity entity = ConvertToEntity(dto); + + if (entity is EntityBase dirtyEntity) + { + // reset dirty initial properties (U4-1946) + dirtyEntity.ResetDirtyProperties(false); + } + + return entity; + } + + protected override IEnumerable PerformGetAll(params TId[]? ids) + { + Sql sql = Sql().From(); + + if (ids?.Any() ?? false) + { + sql.Where(GetWhereInClauseForGetAll(), new + { + ids, + }); + } + + return Database.Fetch(sql).Select(ConvertToEntity); + } + + protected sealed override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(ConvertToEntity); + } + + protected sealed override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + + protected sealed override void PersistNewItem(TEntity entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected sealed override void PersistUpdatedItem(TEntity entity) => + throw new InvalidOperationException("This method won't be implemented."); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs index d22e54e76c..ca1f995e2b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs @@ -1,115 +1,117 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Stylesheet Repository +/// +internal class StylesheetRepository : FileRepository, IStylesheetRepository { - /// - /// Represents the Stylesheet Repository - /// - internal class StylesheetRepository : FileRepository, IStylesheetRepository + public StylesheetRepository(FileSystems fileSystems) + : base(fileSystems.StylesheetsFileSystem) { - public StylesheetRepository(FileSystems fileSystems) - : base(fileSystems.StylesheetsFileSystem) - { - } - - #region Overrides of FileRepository - - public override IStylesheet? Get(string? id) - { - if (id is null || FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id); - - path = path.EnsureEndsWith(".css"); - - // if the css directory is changed, references to the old path can still exist (ie in RTE config) - // these old references will throw an error, which breaks the RTE - // try-catch here makes the request fail silently, and allows RTE to load correctly - try - { - if (FileSystem.FileExists(path) == false) - return null; - } catch - { - return null; - } - - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); - - var stylesheet = new Stylesheet(path, file => GetFileContent(file.OriginalPath)) - { - //Content = content, - Key = path.EncodeAsGuid(), - CreateDate = created, - UpdateDate = updated, - Id = path.GetHashCode(), - VirtualPath = FileSystem.GetUrl(path) - }; - - // reset dirty initial properties (U4-1946) - stylesheet.ResetDirtyProperties(false); - - return stylesheet; - - } - - public override void Save(IStylesheet entity) - { - // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later - var stylesheet = (Stylesheet)entity; - - base.Save(stylesheet); - - // ensure that from now on, content is lazy-loaded - if (stylesheet.GetFileContent == null) - stylesheet.GetFileContent = file => GetFileContent(file.OriginalPath); - } - - public override IEnumerable GetMany(params string[]? ids) - { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids? - .Select(x => x.EnsureEndsWith(".css")) - .Distinct() - .ToArray(); - - if (ids?.Any() ?? false) - { - foreach (var id in ids) - { - IStylesheet? stylesheet = Get(id); - if (stylesheet is not null) - { - yield return stylesheet; - } - } - } - else - { - var files = FindAllFiles("", "*.css"); - foreach (var file in files) - { - IStylesheet? stylesheet = Get(file); - if (stylesheet is not null) - { - yield return stylesheet; - } - } - } - } - - #endregion } + + #region Overrides of FileRepository + + public override IStylesheet? Get(string? id) + { + if (id is null || FileSystem is null) + { + return null; + } + + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id); + + path = path.EnsureEndsWith(".css"); + + // if the css directory is changed, references to the old path can still exist (ie in RTE config) + // these old references will throw an error, which breaks the RTE + // try-catch here makes the request fail silently, and allows RTE to load correctly + try + { + if (FileSystem.FileExists(path) == false) + { + return null; + } + } + catch + { + return null; + } + + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; + + // var content = GetFileContent(path); + var stylesheet = new Stylesheet(path, file => GetFileContent(file.OriginalPath)) + { + // Content = content, + Key = path.EncodeAsGuid(), + CreateDate = created, + UpdateDate = updated, + Id = path.GetHashCode(), + VirtualPath = FileSystem.GetUrl(path), + }; + + // reset dirty initial properties (U4-1946) + stylesheet.ResetDirtyProperties(false); + + return stylesheet; + } + + public override void Save(IStylesheet entity) + { + // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later + var stylesheet = (Stylesheet)entity; + + base.Save(stylesheet); + + // ensure that from now on, content is lazy-loaded + if (stylesheet.GetFileContent == null) + { + stylesheet.GetFileContent = file => GetFileContent(file.OriginalPath); + } + } + + public override IEnumerable GetMany(params string[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids? + .Select(x => x.EnsureEndsWith(".css")) + .Distinct() + .ToArray(); + + if (ids?.Any() ?? false) + { + foreach (var id in ids) + { + IStylesheet? stylesheet = Get(id); + if (stylesheet is not null) + { + yield return stylesheet; + } + } + } + else + { + IEnumerable files = FindAllFiles(string.Empty, "*.css"); + foreach (var file in files) + { + IStylesheet? stylesheet = Get(file); + if (stylesheet is not null) + { + yield return stylesheet; + } + } + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index ca171a9b01..ecc6600d4c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; @@ -16,131 +13,131 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TagRepository : EntityRepositoryBase, ITagRepository { - internal class TagRepository : EntityRepositoryBase, ITagRepository + public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) + } + + #region Manage Tag Entities + + /// + protected override ITag? PerformGet(int id) + { + Sql sql = Sql().Select().From().Where(x => x.Id == id); + TagDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + return dto == null ? null : TagFactory.BuildEntity(dto); + } + + /// + protected override IEnumerable PerformGetAll(params int[]? ids) + { + IEnumerable dtos = ids?.Length == 0 + ? Database.Fetch(Sql().Select().From()) + : Database.FetchByGroups(ids!, Constants.Sql.MaxParameterCount, + batch => Sql().Select().From().WhereIn(x => x.Id, batch)); + + return dtos.Select(TagFactory.BuildEntity).ToList(); + } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sql = Sql().Select().From(); + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + + return Database.Fetch(sql).Select(TagFactory.BuildEntity).ToList(); + } + + /// + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); + + private Sql GetBaseQuery() => Sql().Select().From(); + + /// + protected override string GetBaseWhereClause() => "id = @id"; + + /// + protected override IEnumerable GetDeleteClauses() + { + var list = new List { + "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" + }; + return list; + } + + /// + protected override void PersistNewItem(ITag entity) + { + entity.AddingEntity(); + + TagDto dto = TagFactory.BuildDto(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(ITag entity) + { + entity.UpdatingEntity(); + + TagDto dto = TagFactory.BuildDto(entity); + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + #endregion + + #region Assign and Remove Tags + + /// + // only invoked from ContentRepositoryBase with all cultures + replaceTags being true + public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) + { + // to no-duplicates array + ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); + + // replacing = clear all + if (replaceTags) + { + Sql sql0 = Sql().Delete() + .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Database.Execute(sql0); } - #region Manage Tag Entities - - /// - protected override ITag? PerformGet(int id) + // no tags? nothing else to do + if (tagsA.Length == 0) { - Sql sql = Sql().Select().From().Where(x => x.Id == id); - TagDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - return dto == null ? null : TagFactory.BuildEntity(dto); + return; } - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - IEnumerable dtos = ids?.Length == 0 - ? Database.Fetch(Sql().Select().From()) - : Database.FetchByGroups(ids!, Constants.Sql.MaxParameterCount, - batch => Sql().Select().From().WhereIn(x => x.Id, batch)); + // tags + // using some clever logic (?) to insert tags that don't exist in 1 query + // must coalesce languageId because equality of NULLs does not exist - return dtos.Select(TagFactory.BuildEntity).ToList(); - } + var tagSetSql = GetTagSet(tagsA); + var group = SqlSyntax.GetQuotedColumnName("group"); - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sql = Sql().Select().From(); - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - - return Database.Fetch(sql).Select(TagFactory.BuildEntity).ToList(); - } - - /// - protected override Sql GetBaseQuery(bool isCount) => - isCount ? Sql().SelectCount().From() : GetBaseQuery(); - - private Sql GetBaseQuery() => Sql().Select().From(); - - /// - protected override string GetBaseWhereClause() => "id = @id"; - - /// - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" - }; - return list; - } - - /// - protected override void PersistNewItem(ITag entity) - { - entity.AddingEntity(); - - TagDto dto = TagFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - entity.ResetDirtyProperties(); - } - - /// - protected override void PersistUpdatedItem(ITag entity) - { - entity.UpdatingEntity(); - - TagDto dto = TagFactory.BuildDto(entity); - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - #endregion - - #region Assign and Remove Tags - - /// - // only invoked from ContentRepositoryBase with all cultures + replaceTags being true - public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) - { - // to no-duplicates array - ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); - - // replacing = clear all - if (replaceTags) - { - Sql sql0 = Sql().Delete() - .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql0); - } - - // no tags? nothing else to do - if (tagsA.Length == 0) - { - return; - } - - // tags - // using some clever logic (?) to insert tags that don't exist in 1 query - // must coalesce languageId because equality of NULLs does not exist - - var tagSetSql = GetTagSet(tagsA); - var group = SqlSyntax.GetQuotedColumnName("group"); - - // insert tags - var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) + // insert tags + var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) SELECT tagSet.tag, tagSet.{group}, tagSet.languageId FROM {tagSetSql} LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)) WHERE cmsTags.id IS NULL"; - Database.Execute(sql1); + Database.Execute(sql1); - // insert relations - var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) + // insert relations + var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) SELECT {contentId}, {propertyTypeId}, tagSet2.Id FROM ( SELECT t.Id @@ -150,417 +147,416 @@ FROM ( LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId}) WHERE r.tagId IS NULL"; - Database.Execute(sql2); - } + Database.Execute(sql2); + } - /// - // only invoked from tests - public void Remove(int contentId, int propertyTypeId, IEnumerable tags) - { - var tagSetSql = GetTagSet(tags); - var group = SqlSyntax.GetQuotedColumnName("group"); + /// + // only invoked from tests + public void Remove(int contentId, int propertyTypeId, IEnumerable tags) + { + var tagSetSql = GetTagSet(tags); + var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = - $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + var deleteSql = + $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) ) )"; - Database.Execute(deleteSql); - } + Database.Execute(deleteSql); + } - /// - public void RemoveAll(int contentId, int propertyTypeId) => - Database.Execute( - "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", - new { nodeId = contentId, propertyTypeId }); + /// + public void RemoveAll(int contentId, int propertyTypeId) => + Database.Execute( + "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", + new {nodeId = contentId, propertyTypeId}); - /// - public void RemoveAll(int contentId) => - Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", - new { nodeId = contentId }); + /// + public void RemoveAll(int contentId) => + Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", + new {nodeId = contentId}); - // this is a clever way to produce an SQL statement like this: - // - // ( - // SELECT 'Spacesdd' AS Tag, 'default' AS [group] - // UNION - // SELECT 'Cool' AS tag, 'default' AS [group] - // ) AS tagSet - // - // which we can then use to reduce queries - // - private string GetTagSet(IEnumerable tags) + // this is a clever way to produce an SQL statement like this: + // + // ( + // SELECT 'Spacesdd' AS Tag, 'default' AS [group] + // UNION + // SELECT 'Cool' AS tag, 'default' AS [group] + // ) AS tagSet + // + // which we can then use to reduce queries + // + private string GetTagSet(IEnumerable tags) + { + var sql = new StringBuilder(); + var group = SqlSyntax.GetQuotedColumnName("group"); + var first = true; + + sql.Append("("); + + foreach (ITag tag in tags) { - var sql = new StringBuilder(); - var group = SqlSyntax.GetQuotedColumnName("group"); - var first = true; - - sql.Append("("); - - foreach (ITag tag in tags) + if (first) { - if (first) - { - first = false; - } - else - { - sql.Append(" UNION "); - } - - // HACK: SQLite (or rather SQL server setup was a hack) - if (SqlContext.DatabaseType.IsSqlServer()) - { - sql.Append("SELECT N'"); - } - else - { - sql.Append("SELECT '"); - } - - sql.Append(SqlSyntax.EscapeString(tag.Text)); - sql.Append("' AS tag, '"); - sql.Append(SqlSyntax.EscapeString(tag.Group)); - sql.Append("' AS "); - sql.Append(group); - sql.Append(" , "); - if (tag.LanguageId.HasValue) - { - sql.Append(tag.LanguageId); - } - else - { - sql.Append("NULL"); - } - - sql.Append(" AS languageId"); + first = false; + } + else + { + sql.Append(" UNION "); } - sql.Append(") AS tagSet"); + // HACK: SQLite (or rather SQL server setup was a hack) + if (SqlContext.DatabaseType.IsSqlServer()) + { + sql.Append("SELECT N'"); + } + else + { + sql.Append("SELECT '"); + } - return sql.ToString(); + sql.Append(SqlSyntax.EscapeString(tag.Text)); + sql.Append("' AS tag, '"); + sql.Append(SqlSyntax.EscapeString(tag.Group)); + sql.Append("' AS "); + sql.Append(group); + sql.Append(" , "); + if (tag.LanguageId.HasValue) + { + sql.Append(tag.LanguageId); + } + else + { + sql.Append("NULL"); + } + + sql.Append(" AS languageId"); } - // used to run Distinct() on tags - private class TagComparer : IEqualityComparer - { - public bool Equals(ITag? x, ITag? y) => - ReferenceEquals(x, y) // takes care of both being null - || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); + sql.Append(") AS tagSet"); - public int GetHashCode(ITag obj) + return sql.ToString(); + } + + // used to run Distinct() on tags + private class TagComparer : IEqualityComparer + { + public bool Equals(ITag? x, ITag? y) => + ReferenceEquals(x, y) // takes care of both being null + || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); + + public int GetHashCode(ITag obj) + { + unchecked { - unchecked - { - var h = obj.Text?.GetHashCode() ?? 1; - h = (h * 397) ^ obj.Group?.GetHashCode() ?? 0; - h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); - return h; - } + var h = obj.Text.GetHashCode(); + h = (h * 397) ^ obj.Group.GetHashCode(); + h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); + return h; } } + } - #endregion + #endregion - #region Queries + #region Queries - // TODO: consider caching implications - // add lookups for parentId or path (ie get content in tag group, that are descendants of x) + // TODO: consider caching implications + // add lookups for parentId or path (ie get content in tag group, that are descendants of x) - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class TaggedEntityDto + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TaggedEntityDto + { + public int NodeId { get; set; } + public string? PropertyTypeAlias { get; set; } + public int PropertyTypeId { get; set; } + public int TagId { get; set; } + public string TagText { get; set; } = null!; + public string TagGroup { get; set; } = null!; + public int? TagLanguage { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + /// + public TaggedEntity? GetTaggedEntityByKey(Guid key) + { + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql + .Where(dto => dto.UniqueId == key); + + return Map(Database.Fetch(sql)).FirstOrDefault(); + } + + /// + public TaggedEntity? GetTaggedEntityById(int id) + { + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql + .Where(dto => dto.NodeId == id); + + return Map(Database.Fetch(sql)).FirstOrDefault(); + } + + /// + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, + string? culture = null) + { + Sql sql = GetTaggedEntitiesSql(objectType, culture); + + sql = sql + .Where(x => x.Group == group); + + return Map(Database.Fetch(sql)); + } + + /// + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, + string? group = null, string? culture = null) + { + Sql sql = GetTaggedEntitiesSql(objectType, culture); + + sql = sql + .Where(dto => dto.Text == tag); + + if (group.IsNullOrWhiteSpace() == false) { - public int NodeId { get; set; } - public string? PropertyTypeAlias { get; set; } - public int PropertyTypeId { get; set; } - public int TagId { get; set; } - public string TagText { get; set; } = null!; - public string TagGroup { get; set; } = null!; - public int? TagLanguage { get; set; } - } - // ReSharper restore UnusedAutoPropertyAccessor.Local - - /// - public TaggedEntity? GetTaggedEntityByKey(Guid key) - { - Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); - sql = sql - .Where(dto => dto.UniqueId == key); - - return Map(Database.Fetch(sql)).FirstOrDefault(); + .Where(dto => dto.Group == group); } - /// - public TaggedEntity? GetTaggedEntityById(int id) - { - Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + return Map(Database.Fetch(sql)); + } + private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string? culture) + { + Sql sql = Sql() + .Select(x => Alias(x.NodeId, "NodeId")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), + x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), + x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin() + .On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin().On((content, node) => content.NodeId == node.NodeId); + + if (culture == null) + { sql = sql - .Where(dto => dto.NodeId == id); - - return Map(Database.Fetch(sql)).FirstOrDefault(); + .Where(dto => dto.LanguageId == null); } - - /// - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, - string? culture = null) + else if (culture != "*") { - Sql sql = GetTaggedEntitiesSql(objectType, culture); - sql = sql - .Where(x => x.Group == group); - - return Map(Database.Fetch(sql)); + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) + .Where(x => x.IsoCode == culture); } - /// - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, - string? group = null, string? culture = null) + if (objectType != TaggableObjectTypes.All) { - Sql sql = GetTaggedEntitiesSql(objectType, culture); - - sql = sql - .Where(dto => dto.Text == tag); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return Map(Database.Fetch(sql)); + Guid nodeObjectType = GetNodeObjectType(objectType); + sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } - private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string? culture) + return sql; + } + + private static IEnumerable Map(IEnumerable dtos) => + dtos.GroupBy(x => x.NodeId).Select(dtosForNode => { - Sql sql = Sql() - .Select(x => Alias(x.NodeId, "NodeId")) - .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), - x => Alias(x.Id, "PropertyTypeId")) - .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), - x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .InnerJoin() - .On((rel, prop) => rel.PropertyTypeId == prop.Id) - .InnerJoin().On((content, node) => content.NodeId == node.NodeId); - - if (culture == null) + var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { - sql = sql - .Where(dto => dto.LanguageId == null); - } - else if (culture != "*") - { - sql = sql - .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) - .Where(x => x.IsoCode == culture); - } - - if (objectType != TaggableObjectTypes.All) - { - Guid nodeObjectType = GetNodeObjectType(objectType); - sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); - } - - return sql; - } - - private static IEnumerable Map(IEnumerable dtos) => - dtos.GroupBy(x => x.NodeId).Select(dtosForNode => - { - var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => + string? propertyTypeAlias = null; + var tags = dtosForProperty.Select(dto => { - string? propertyTypeAlias = null; - var tags = dtosForProperty.Select(dto => - { - propertyTypeAlias = dto.PropertyTypeAlias; - return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); - }).ToList(); - return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); + propertyTypeAlias = dto.PropertyTypeAlias; + return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); }).ToList(); - - return new TaggedEntity(dtosForNode.Key, taggedProperties); + return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); }).ToList(); - /// - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, - string? culture = null) + return new TaggedEntity(dtosForNode.Key, taggedProperties); + }).ToList(); + + /// + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture, true); + + AddTagsSqlWhere(sql, culture); + + if (objectType != TaggableObjectTypes.All) { - Sql sql = GetTagsSql(culture, true); - - AddTagsSqlWhere(sql, culture); - - if (objectType != TaggableObjectTypes.All) - { - Guid nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); - } - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - + Guid nodeObjectType = GetNodeObjectType(objectType); sql = sql - .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); - - return ExecuteTagsQuery(sql); + .Where(dto => dto.NodeObjectType == nodeObjectType); } - /// - public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + if (group.IsNullOrWhiteSpace() == false) { - Sql sql = GetTagsSql(culture); - - AddTagsSqlWhere(sql, culture); - sql = sql - .Where(dto => dto.NodeId == contentId); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); + .Where(dto => dto.Group == group); } - /// - public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) - { - Sql sql = GetTagsSql(culture); + sql = sql + .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); - AddTagsSqlWhere(sql, culture); - - sql = sql - .Where(dto => dto.UniqueId == contentId); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); - } - - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, - string? culture = null) - { - Sql sql = GetTagsSql(culture); - - sql = sql - .InnerJoin() - .On((prop, rel) => prop.Id == rel.PropertyTypeId) - .Where(x => x.NodeId == contentId) - .Where(x => x.Alias == propertyTypeAlias); - - AddTagsSqlWhere(sql, culture); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); - } - - /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, - string? culture = null) - { - Sql sql = GetTagsSql(culture); - - sql = sql - .InnerJoin() - .On((prop, rel) => prop.Id == rel.PropertyTypeId) - .Where(dto => dto.UniqueId == contentId) - .Where(dto => dto.Alias == propertyTypeAlias); - - AddTagsSqlWhere(sql, culture); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); - } - - private Sql GetTagsSql(string? culture, bool withGrouping = false) - { - Sql sql = Sql() - .Select(); - - if (withGrouping) - { - sql = sql - .AndSelectCount("NodeCount"); - } - - sql = sql - .From() - .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) - .InnerJoin() - .On((content, rel) => content.NodeId == rel.NodeId) - .InnerJoin().On((node, content) => node.NodeId == content.NodeId); - - if (culture != null && culture != "*") - { - sql = sql - .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); - } - - return sql; - } - - private Sql AddTagsSqlWhere(Sql sql, string? culture) - { - if (culture == null) - { - sql = sql - .Where(dto => dto.LanguageId == null); - } - else if (culture != "*") - { - sql = sql - .Where(x => x.IsoCode == culture); - } - - return sql; - } - - private IEnumerable ExecuteTagsQuery(Sql sql) => - Database.Fetch(sql).Select(TagFactory.BuildEntity); - - private Guid GetNodeObjectType(TaggableObjectTypes type) - { - switch (type) - { - case TaggableObjectTypes.Content: - return Constants.ObjectTypes.Document; - case TaggableObjectTypes.Media: - return Constants.ObjectTypes.Media; - case TaggableObjectTypes.Member: - return Constants.ObjectTypes.Member; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } - } - - #endregion + return ExecuteTagsQuery(sql); } + + /// + public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + { + Sql sql = GetTagsSql(culture); + + AddTagsSqlWhere(sql, culture); + + sql = sql + .Where(dto => dto.NodeId == contentId); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + /// + public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) + { + Sql sql = GetTagsSql(culture); + + AddTagsSqlWhere(sql, culture); + + sql = sql + .Where(dto => dto.UniqueId == contentId); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture); + + sql = sql + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(x => x.NodeId == contentId) + .Where(x => x.Alias == propertyTypeAlias); + + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture); + + sql = sql + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(dto => dto.UniqueId == contentId) + .Where(dto => dto.Alias == propertyTypeAlias); + + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + private Sql GetTagsSql(string? culture, bool withGrouping = false) + { + Sql sql = Sql() + .Select(); + + if (withGrouping) + { + sql = sql + .AndSelectCount("NodeCount"); + } + + sql = sql + .From() + .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) + .InnerJoin() + .On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin().On((node, content) => node.NodeId == content.NodeId); + + if (culture != null && culture != "*") + { + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); + } + + return sql; + } + + private Sql AddTagsSqlWhere(Sql sql, string? culture) + { + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .Where(x => x.IsoCode == culture); + } + + return sql; + } + + private IEnumerable ExecuteTagsQuery(Sql sql) => + Database.Fetch(sql).Select(TagFactory.BuildEntity); + + private Guid GetNodeObjectType(TaggableObjectTypes type) + { + switch (type) + { + case TaggableObjectTypes.Content: + return Constants.ObjectTypes.Document; + case TaggableObjectTypes.Media: + return Constants.ObjectTypes.Media; + case TaggableObjectTypes.Member: + return Constants.ObjectTypes.Member; + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs index 4dc3bb71f2..9e01320fdc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -19,613 +17,634 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Template Repository +/// +internal class TemplateRepository : EntityRepositoryBase, ITemplateRepository { - /// - /// Represents the Template Repository - /// - internal class TemplateRepository : EntityRepositoryBase, ITemplateRepository + private readonly IIOHelper _ioHelper; + private readonly IShortStringHelper _shortStringHelper; + private readonly IFileSystem? _viewsFileSystem; + private readonly IViewHelper _viewHelper; + private readonly IOptionsMonitor _runtimeSettings; + + public TemplateRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + FileSystems fileSystems, + IIOHelper ioHelper, + IShortStringHelper shortStringHelper, + IViewHelper viewHelper, + IOptionsMonitor runtimeSettings) + : base(scopeAccessor, cache, logger) { - private readonly IIOHelper _ioHelper; - private readonly IShortStringHelper _shortStringHelper; - private readonly IFileSystem? _viewsFileSystem; - private readonly IViewHelper _viewHelper; + _ioHelper = ioHelper; + _shortStringHelper = shortStringHelper; + _viewsFileSystem = fileSystems.MvcViewsFileSystem; + _viewHelper = viewHelper; + _runtimeSettings = runtimeSettings; + } - public TemplateRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, FileSystems fileSystems, IIOHelper ioHelper, IShortStringHelper shortStringHelper, IViewHelper viewHelper) - : base(scopeAccessor, cache, logger) + public Stream GetFileContentStream(string filepath) + { + IFileSystem? fileSystem = GetFileSystem(filepath); + if (fileSystem?.FileExists(filepath) == false) { - _ioHelper = ioHelper; - _shortStringHelper = shortStringHelper; - _viewsFileSystem = fileSystems.MvcViewsFileSystem; - _viewHelper = viewHelper; + return Stream.Null; } - protected override IRepositoryCachePolicy CreateCachePolicy() => - new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - - #region Overrides of RepositoryBase - - protected override ITemplate? PerformGet(int id) => - //use the underlying GetAll which will force cache all templates - base.GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IEnumerable PerformGetAll(params int[]? ids) + try { - Sql sql = GetBaseQuery(false); + return fileSystem!.OpenFile(filepath); + } + catch + { + return Stream.Null; // deal with race conds + } + } - if (ids?.Any() ?? false) - { - sql.Where("umbracoNode.id in (@ids)", new { ids }); - } - else - { - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); - } + public void SetFileContent(string filepath, Stream content) => + GetFileSystem(filepath)?.AddFile(filepath, content, true); - List dtos = Database.Fetch(sql); + public long GetFileSize(string filename) + { + IFileSystem? fileSystem = GetFileSystem(filename); + if (fileSystem?.FileExists(filename) == false) + { + return -1; + } - if (dtos.Count == 0) - { - return Enumerable.Empty(); - } + try + { + return fileSystem!.GetSize(filename); + } + catch + { + return -1; // deal with race conds + } + } - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - IUmbracoEntity[] childIds = (ids?.Any() ?? false - ? GetAxisDefinitions(dtos.ToArray()) - : dtos.Select(x => new EntitySlim + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ false); + + private IEnumerable GetAxisDefinitions(params TemplateDto[] templates) + { + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + Sql childIdsSql = SqlContext.Sql() + .Select("nodeId,alias,parentID") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + //lookup axis's + .Where( + "umbracoNode." + SqlContext.SqlSyntax.GetQuotedColumnName("id") + + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)", + new { - Id = x.NodeId, - ParentId = x.NodeDto.ParentId, - Name = x.Alias - })).ToArray(); - - return dtos.Select(d => MapFromDto(d, childIds)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); - - List dtos = Database.Fetch(sql); - - if (dtos.Count == 0) - { - return Enumerable.Empty(); - } - - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - IUmbracoEntity[] childIds = GetAxisDefinitions(dtos.ToArray()).ToArray(); - - return dtos.Select(d => MapFromDto(d, childIds)); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - Sql sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(r => r.Select(x => x.NodeDto)); - - sql - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion + " SET templateId = NULL WHERE templateId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentType + " WHERE templateNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Template + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" - }; - return list; - } - - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Template; - - protected override void PersistNewItem(ITemplate entity) - { - EnsureValidAlias(entity); - - //Save to db - var template = (Template)entity; - template.AddingEntity(); - - TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, template.Id); - - //Create the (base) node data - umbracoNode - NodeDto nodeDto = dto.NodeDto; - nodeDto.Path = "-1," + dto.NodeDto.NodeId; - int o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - ITemplate? parent = Get(template.MasterTemplateId!.Value); - if (parent != null) - { - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - } - else - { - nodeDto.Path = "-1," + dto.NodeDto.NodeId; - } - Database.Update(nodeDto); - - //Insert template dto - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Update entity with correct values - template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - template.Path = nodeDto.Path; - - //now do the file work - SaveFile(template); - - template.ResetDirtyProperties(); - - // ensure that from now on, content is lazy-loaded - if (template.GetFileContent == null) - { - template.GetFileContent = file => GetFileContent((Template) file, false); - } - } - - protected override void PersistUpdatedItem(ITemplate entity) - { - EnsureValidAlias(entity); - - //store the changed alias if there is one for use with updating files later - string? originalAlias = entity.Alias; - if (entity.IsPropertyDirty("Alias")) - { - //we need to check what it currently is before saving and remove that file - ITemplate? current = Get(entity.Id); - originalAlias = current?.Alias; - } - - var template = (Template)entity; - - if (entity.IsPropertyDirty("MasterTemplateId")) - { - ITemplate? parent = Get(template.MasterTemplateId!.Value); - if (parent != null) - { - entity.Path = string.Concat(parent.Path, ",", entity.Id); - } - else - { - //this means that the master template has been removed, so we need to reset the template's - //path to be at the root - entity.Path = string.Concat("-1,", entity.Id); - } - } - - //Get TemplateDto from db to get the Primary key of the entity - TemplateDto templateDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { entity.Id }); - - //Save updated entity to db - template.UpdateDate = DateTime.Now; - TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); - Database.Update(dto.NodeDto); - Database.Update(dto); - - //re-update if this is a master template, since it could have changed! - IEnumerable axisDefs = GetAxisDefinitions(dto); - template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); - - //now do the file work - SaveFile((Template) entity, originalAlias); - - entity.ResetDirtyProperties(); - - // ensure that from now on, content is lazy-loaded - if (template.GetFileContent == null) - { - template.GetFileContent = file => GetFileContent((Template) file, false); - } - } - - private void SaveFile(Template template, string? originalAlias = null) - { - string? content; - - if (template is TemplateOnDisk templateOnDisk && templateOnDisk.IsOnDisk) - { - // if "template on disk" load content from disk - content = _viewHelper.GetFileContents(template); - } - else - { - // else, create or write template.Content to disk - content = originalAlias == null - ? _viewHelper.CreateView(template, true) - : _viewHelper.UpdateViewFile(template, originalAlias); - } - - // once content has been set, "template on disk" are not "on disk" anymore - template.Content = content; - SetVirtualPath(template); - } - - protected override void PersistDeletedItem(ITemplate entity) - { - string[] deletes = GetDeleteClauses().ToArray(); - - var descendants = GetDescendants(entity.Id).ToList(); - - //change the order so it goes bottom up! (deepest level first) - descendants.Reverse(); - - //delete the hierarchy - foreach (ITemplate descendant in descendants) - { - foreach (string delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(descendant) }); - } - } - - //now we can delete this one - foreach (string delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } - - string viewName = string.Concat(entity.Alias, ".cshtml"); - _viewsFileSystem?.DeleteFile(viewName); - - entity.DeleteDate = DateTime.Now; - } - - #endregion - - private IEnumerable GetAxisDefinitions(params TemplateDto[] templates) - { - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - Sql childIdsSql = SqlContext.Sql() - .Select("nodeId,alias,parentID") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - //lookup axis's - .Where("umbracoNode." + SqlContext.SqlSyntax.GetQuotedColumnName("id") + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)", - new {parentIds = templates.Select(x => x.NodeDto.ParentId), childIds = templates.Select(x => x.NodeId)}); - - var childIds = Database.Fetch(childIdsSql) - .Select(x => new EntitySlim - { - Id = x.NodeId, - ParentId = x.ParentId, - Name = x.Alias + parentIds = templates.Select(x => x.NodeDto.ParentId), + childIds = templates.Select(x => x.NodeId) }); - return childIds; + IEnumerable childIds = Database.Fetch(childIdsSql) + .Select(x => new EntitySlim {Id = x.NodeId, ParentId = x.ParentId, Name = x.Alias}); + + return childIds; + } + + /// + /// Maps from a dto to an ITemplate + /// + /// + /// + /// This is a collection of template definitions ... either all templates, or the collection of child templates and + /// it's parent template + /// + /// + private ITemplate MapFromDto(TemplateDto dto, IUmbracoEntity[] axisDefinitions) + { + Template template = TemplateFactory.BuildEntity(_shortStringHelper, dto, axisDefinitions, + file => GetFileContent((Template)file, false)); + + if (dto.NodeDto.ParentId > 0) + { + IUmbracoEntity? masterTemplate = axisDefinitions.FirstOrDefault(x => x.Id == dto.NodeDto.ParentId); + if (masterTemplate != null) + { + template.MasterTemplateAlias = masterTemplate.Name; + template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); + } } - /// - /// Maps from a dto to an ITemplate - /// - /// - /// - /// This is a collection of template definitions ... either all templates, or the collection of child templates and it's parent template - /// - /// - private ITemplate MapFromDto(TemplateDto dto, IUmbracoEntity[] axisDefinitions) + // get the infos (update date and virtual path) that will change only if + // path changes - but do not get content, will get loaded only when required + GetFileContent(template, true); + + // reset dirty initial properties (U4-1946) + template.ResetDirtyProperties(false); + + return template; + } + + private void SetVirtualPath(ITemplate template) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) { - - Template template = TemplateFactory.BuildEntity(_shortStringHelper, dto, axisDefinitions, file => GetFileContent((Template) file, false)); - - if (dto.NodeDto.ParentId > 0) + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) { - IUmbracoEntity? masterTemplate = axisDefinitions.FirstOrDefault(x => x.Id == dto.NodeDto.ParentId); - if (masterTemplate != null) - { - template.MasterTemplateAlias = masterTemplate.Name; - template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); - } + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; } - // get the infos (update date and virtual path) that will change only if - // path changes - but do not get content, will get loaded only when required - GetFileContent(template, true); - - // reset dirty initial properties (U4-1946) - template.ResetDirtyProperties(false); - - return template; + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) + { + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + } + } + else + { + // we know the path already + template.VirtualPath = _viewsFileSystem?.GetUrl(path); } - private void SetVirtualPath(ITemplate template) + template.VirtualPath = string.Empty; // file not found... + } + + private string? GetFileContent(ITemplate template, bool init) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) { - string path = template.OriginalPath; - if (string.IsNullOrWhiteSpace(path)) + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) { - // we need to discover the path - path = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - template.VirtualPath = _viewsFileSystem.GetUrl(path); - return; - } - path = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - template.VirtualPath = _viewsFileSystem.GetUrl(path); - return; - } - } - else - { - // we know the path already - template.VirtualPath = _viewsFileSystem?.GetUrl(path); - } - - template.VirtualPath = string.Empty; // file not found... - } - - private string? GetFileContent(ITemplate template, bool init) - { - string path = template.OriginalPath; - if (string.IsNullOrWhiteSpace(path)) - { - // we need to discover the path - path = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - return GetFileContent(template, _viewsFileSystem, path, init); - } - - path = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - return GetFileContent(template, _viewsFileSystem, path, init); - } - } - else - { - // we know the path already return GetFileContent(template, _viewsFileSystem, path, init); } - template.VirtualPath = string.Empty; // file not found... - - return string.Empty; + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) + { + return GetFileContent(template, _viewsFileSystem, path, init); + } + } + else + { + // we know the path already + return GetFileContent(template, _viewsFileSystem, path, init); } - private string? GetFileContent(ITemplate template, IFileSystem? fs, string filename, bool init) + template.VirtualPath = string.Empty; // file not found... + + return string.Empty; + } + + private string? GetFileContent(ITemplate template, IFileSystem? fs, string filename, bool init) + { + // do not update .UpdateDate as that would make it dirty (side-effect) + // unless initializing, because we have to do it once + if (init && fs is not null) { - // do not update .UpdateDate as that would make it dirty (side-effect) - // unless initializing, because we have to do it once - if (init && fs is not null) - { - template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; - } - - // TODO: see if this could enable us to update UpdateDate without messing with change tracking - // and then we'd want to do it for scripts, stylesheets and partial views too (ie files) - // var xtemplate = template as Template; - // xtemplate.DisableChangeTracking(); - // template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; - // xtemplate.EnableChangeTracking(); - - template.VirtualPath = fs?.GetUrl(filename); - - return init ? null : GetFileContent(fs, filename); + template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; } - private string? GetFileContent(IFileSystem? fs, string filename) - { - if (fs is null) - { - return null; - } + // TODO: see if this could enable us to update UpdateDate without messing with change tracking + // and then we'd want to do it for scripts, stylesheets and partial views too (ie files) + // var xtemplate = template as Template; + // xtemplate.DisableChangeTracking(); + // template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; + // xtemplate.EnableChangeTracking(); - using Stream stream = fs.OpenFile(filename); - using var reader = new StreamReader(stream, Encoding.UTF8, true); - return reader.ReadToEnd(); + template.VirtualPath = fs?.GetUrl(filename); + + return init ? null : GetFileContent(fs, filename); + } + + private string? GetFileContent(IFileSystem? fs, string filename) + { + if (fs is null) + { + return null; } - public Stream GetFileContentStream(string filepath) - { - IFileSystem? fileSystem = GetFileSystem(filepath); - if (fileSystem?.FileExists(filepath) == false) - { - return Stream.Null; - } + using Stream stream = fs.OpenFile(filename); + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); + } - try + private IFileSystem? GetFileSystem(string filepath) + { + var ext = Path.GetExtension(filepath); + IFileSystem? fs; + switch (ext) + { + case ".cshtml": + case ".vbhtml": + fs = _viewsFileSystem; + break; + default: + throw new Exception("Unsupported extension " + ext + "."); + } + + return fs; + } + + /// + /// Ensures that there are not duplicate aliases and if so, changes it to be a numbered version and also verifies the + /// length + /// + /// + private void EnsureValidAlias(ITemplate template) + { + //ensure unique alias + template.Alias = template.Alias.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias); + + if (template.Alias.Length > 100) + { + template.Alias = template.Alias.Substring(0, 95); + } + + if (AliasAlreadExists(template)) + { + template.Alias = EnsureUniqueAlias(template, 1); + } + } + + private bool AliasAlreadExists(ITemplate template) + { + Sql sql = GetBaseQuery(true) + .Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); + var count = Database.ExecuteScalar(sql); + return count > 0; + } + + private string EnsureUniqueAlias(ITemplate template, int attempts) + { + // TODO: This is ported from the old data layer... pretty crap way of doing this but it works for now. + if (AliasAlreadExists(template)) + { + return template.Alias + attempts; + } + + attempts++; + return EnsureUniqueAlias(template, attempts); + } + + #region Overrides of RepositoryBase + + protected override ITemplate? PerformGet(int id) => + //use the underlying GetAll which will force cache all templates + GetMany().FirstOrDefault(x => x.Id == id); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + + if (ids?.Any() ?? false) + { + sql.Where("umbracoNode.id in (@ids)", new {ids}); + } + else + { + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + } + + List dtos = Database.Fetch(sql); + + if (dtos.Count == 0) + { + return Enumerable.Empty(); + } + + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + IUmbracoEntity[] childIds = (ids?.Any() ?? false + ? GetAxisDefinitions(dtos.ToArray()) + : dtos.Select(x => new EntitySlim {Id = x.NodeId, ParentId = x.NodeDto.ParentId, Name = x.Alias})) + .ToArray(); + + return dtos.Select(d => MapFromDto(d, childIds)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List dtos = Database.Fetch(sql); + + if (dtos.Count == 0) + { + return Enumerable.Empty(); + } + + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + IUmbracoEntity[] childIds = GetAxisDefinitions(dtos.ToArray()).ToArray(); + + return dtos.Select(d => MapFromDto(d, childIds)); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(r => r.Select(x => x.NodeDto)); + + sql + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.DocumentVersion + + " SET templateId = NULL WHERE templateId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentType + " WHERE templateNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Template + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + }; + return list; + } + + protected Guid NodeObjectTypeId => Constants.ObjectTypes.Template; + + protected override void PersistNewItem(ITemplate entity) + { + EnsureValidAlias(entity); + + //Save to db + var template = (Template)entity; + template.AddingEntity(); + + TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, template.Id); + + //Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = "-1," + dto.NodeDto.NodeId; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + ITemplate? parent = Get(template.MasterTemplateId!.Value); + if (parent != null) + { + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + } + else + { + nodeDto.Path = "-1," + dto.NodeDto.NodeId; + } + + Database.Update(nodeDto); + + //Insert template dto + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Update entity with correct values + template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + template.Path = nodeDto.Path; + + // Only save file when not in production runtime mode + if (_runtimeSettings.CurrentValue.Mode != RuntimeMode.Production) + { + //now do the file work + SaveFile(template); + } + + template.ResetDirtyProperties(); + + // ensure that from now on, content is lazy-loaded + if (template.GetFileContent == null) + { + template.GetFileContent = file => GetFileContent((Template)file, false); + } + } + + protected override void PersistUpdatedItem(ITemplate entity) + { + EnsureValidAlias(entity); + + //store the changed alias if there is one for use with updating files later + var originalAlias = entity.Alias; + if (entity.IsPropertyDirty("Alias")) + { + //we need to check what it currently is before saving and remove that file + ITemplate? current = Get(entity.Id); + originalAlias = current?.Alias; + } + + var template = (Template)entity; + + if (entity.IsPropertyDirty("MasterTemplateId")) + { + ITemplate? parent = Get(template.MasterTemplateId!.Value); + if (parent != null) { - return fileSystem!.OpenFile(filepath); + entity.Path = string.Concat(parent.Path, ",", entity.Id); } - catch + else { - return Stream.Null; // deal with race conds + //this means that the master template has been removed, so we need to reset the template's + //path to be at the root + entity.Path = string.Concat("-1,", entity.Id); } } - public void SetFileContent(string filepath, Stream content) => GetFileSystem(filepath)?.AddFile(filepath, content, true); + //Get TemplateDto from db to get the Primary key of the entity + TemplateDto templateDto = Database.SingleOrDefault("WHERE nodeId = @Id", new {entity.Id}); - public long GetFileSize(string filename) + //Save updated entity to db + template.UpdateDate = DateTime.Now; + TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); + Database.Update(dto.NodeDto); + Database.Update(dto); + + //re-update if this is a master template, since it could have changed! + IEnumerable axisDefs = GetAxisDefinitions(dto); + template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); + + // Only save file when not in production runtime mode + if (_runtimeSettings.CurrentValue.Mode != RuntimeMode.Production) { - IFileSystem? fileSystem = GetFileSystem(filename); - if (fileSystem?.FileExists(filename) == false) - { - return -1; - } + //now do the file work + SaveFile((Template)entity, originalAlias); + } - try + entity.ResetDirtyProperties(); + + // ensure that from now on, content is lazy-loaded + if (template.GetFileContent == null) + { + template.GetFileContent = file => GetFileContent((Template)file, false); + } + } + + private void SaveFile(Template template, string? originalAlias = null) + { + string? content; + + if (template is TemplateOnDisk templateOnDisk && templateOnDisk.IsOnDisk) + { + // if "template on disk" load content from disk + content = _viewHelper.GetFileContents(template); + } + else + { + // else, create or write template.Content to disk + content = originalAlias == null + ? _viewHelper.CreateView(template, true) + : _viewHelper.UpdateViewFile(template, originalAlias); + } + + // once content has been set, "template on disk" are not "on disk" anymore + template.Content = content; + SetVirtualPath(template); + } + + protected override void PersistDeletedItem(ITemplate entity) + { + var deletes = GetDeleteClauses().ToArray(); + + var descendants = GetDescendants(entity.Id).ToList(); + + //change the order so it goes bottom up! (deepest level first) + descendants.Reverse(); + + //delete the hierarchy + foreach (ITemplate descendant in descendants) + { + foreach (var delete in deletes) { - return fileSystem!.GetSize(filename); - } - catch - { - return -1; // deal with race conds + Database.Execute(delete, new {id = GetEntityId(descendant)}); } } - private IFileSystem? GetFileSystem(string filepath) + //now we can delete this one + foreach (var delete in deletes) { - string ext = Path.GetExtension(filepath); - IFileSystem? fs; - switch (ext) - { - case ".cshtml": - case ".vbhtml": - fs = _viewsFileSystem; - break; - default: - throw new Exception("Unsupported extension " + ext + "."); - } - return fs; + Database.Execute(delete, new {id = GetEntityId(entity)}); } - #region Implementation of ITemplateRepository + var viewName = string.Concat(entity.Alias, ".cshtml"); + _viewsFileSystem?.DeleteFile(viewName); - public ITemplate? Get(string? alias) => GetAll(alias)?.FirstOrDefault(); + entity.DeleteDate = DateTime.Now; + } - public IEnumerable GetAll(params string?[] aliases) + #endregion + + #region Implementation of ITemplateRepository + + public ITemplate? Get(string? alias) => GetAll(alias).FirstOrDefault(); + + public IEnumerable GetAll(params string?[] aliases) + { + //We must call the base (normal) GetAll method + // which is cached. This is a specialized method and unfortunately with the params[] it + // overlaps with the normal GetAll method. + if (aliases.Any() == false) { - //We must call the base (normal) GetAll method - // which is cached. This is a specialized method and unfortunately with the params[] it - // overlaps with the normal GetAll method. - if (aliases.Any() == false) - { - return base.GetMany(); - } - - //return from base.GetAll, this is all cached - return base.GetMany().Where(x => aliases.WhereNotNull().InvariantContains(x.Alias)); + return GetMany(); } - public IEnumerable GetChildren(int masterTemplateId) + //return from base.GetAll, this is all cached + return GetMany().Where(x => aliases.WhereNotNull().InvariantContains(x.Alias)); + } + + public IEnumerable GetChildren(int masterTemplateId) + { + //return from base.GetAll, this is all cached + ITemplate[] all = GetMany().ToArray(); + + if (masterTemplateId <= 0) { - //return from base.GetAll, this is all cached - ITemplate[] all = base.GetMany().ToArray(); + return all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); + } - if (masterTemplateId <= 0) - { - return all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); - } + ITemplate? parent = all.FirstOrDefault(x => x.Id == masterTemplateId); + if (parent == null) + { + return Enumerable.Empty(); + } + IEnumerable children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); + return children; + } + + public IEnumerable GetDescendants(int masterTemplateId) + { + //return from base.GetAll, this is all cached + ITemplate[] all = GetMany().ToArray(); + var descendants = new List(); + if (masterTemplateId > 0) + { ITemplate? parent = all.FirstOrDefault(x => x.Id == masterTemplateId); if (parent == null) { return Enumerable.Empty(); } - IEnumerable children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); - return children; + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); } - - public IEnumerable GetDescendants(int masterTemplateId) + else { - //return from base.GetAll, this is all cached - ITemplate[]? all = base.GetMany()?.ToArray(); - var descendants = new List(); - if (masterTemplateId > 0) + descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); + foreach (ITemplate parent in descendants) { - ITemplate? parent = all?.FirstOrDefault(x => x.Id == masterTemplateId); - if (parent == null) - { - return Enumerable.Empty(); - } - //recursively add all children with a level AddChildren(all, descendants, parent.Alias); } - else - { - if (all is not null) - { - descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); - foreach (ITemplate parent in descendants) - { - //recursively add all children with a level - AddChildren(all, descendants, parent.Alias); - } - } - } - - //return the list - it will be naturally ordered by level - return descendants; } - private void AddChildren(ITemplate[]? all, List descendants, string masterAlias) - { - ITemplate[]? c = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); - if (c is null || c.Any() == false) - { - return; - } - descendants.AddRange(c); + //return the list - it will be naturally ordered by level + return descendants; + } - //recurse through all children - foreach (ITemplate child in c) - { - AddChildren(all, descendants, child.Alias); - } + private void AddChildren(ITemplate[]? all, List descendants, string masterAlias) + { + ITemplate[]? c = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); + if (c is null || c.Any() == false) + { + return; } - #endregion + descendants.AddRange(c); - /// - /// Ensures that there are not duplicate aliases and if so, changes it to be a numbered version and also verifies the length - /// - /// - private void EnsureValidAlias(ITemplate template) + //recurse through all children + foreach (ITemplate child in c) { - //ensure unique alias - template.Alias = template.Alias.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias); - - if (template.Alias.Length > 100) - { - template.Alias = template.Alias.Substring(0, 95); - } - - if (AliasAlreadExists(template)) - { - template.Alias = EnsureUniqueAlias(template, 1); - } - } - - private bool AliasAlreadExists(ITemplate template) - { - Sql sql = GetBaseQuery(true).Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); - int count = Database.ExecuteScalar(sql); - return count > 0; - } - - private string EnsureUniqueAlias(ITemplate template, int attempts) - { - // TODO: This is ported from the old data layer... pretty crap way of doing this but it works for now. - if (AliasAlreadExists(template)) - { - return template.Alias + attempts; - } - - attempts++; - return EnsureUniqueAlias(template, attempts); + AddChildren(all, descendants, child.Alias); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index 18098623cf..97d1e8d0f4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NPoco; @@ -25,38 +25,41 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement ///
public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) { + Sql innerUnionSql = GetInnerUnionSql(); var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[pn].[id] as nodeId", - "[pn].[uniqueId] as nodeKey", - "[pn].[text] as nodeName", - "[pn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn"); - + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", aliasRight: "ctn"); if (ids.Any()) { - sql = sql?.Where(x => ids.Contains(x.NodeId), "pn"); + sql = sql?.Where(x => ids.Contains(x.NodeId), "n"); } if (filterMustBeIsDependency) { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); + sql = sql?.Where(rt => rt.IsDependency, "x"); } // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); + sql = sql?.OrderBy(x => x.Alias, "x"); var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); totalRecords = Convert.ToInt32(pagedResult?.TotalItems); @@ -64,6 +67,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); } + private Sql GetInnerUnionSql() + { + if (_scopeAccessor.AmbientScope is null) + { + throw new InvalidOperationException("No Ambient Scope available"); + } + + var innerUnionSqlChild = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[cr].childId as id", "[cr].parentId as otherId", "[rt].[alias]", "[rt].[name]", "[rt].[isDependency]", "[rt].[dual]") + .From("cr").InnerJoin("rt") + .On((cr, rt) => rt.Dual == false && rt.Id == cr.RelationType, "cr", "rt"); + + var innerUnionSqlDualParent = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[dpr].parentId as id", "[dpr].childId as otherId", "[dprt].[alias]", "[dprt].[name]", "[dprt].[isDependency]", "[dprt].[dual]") + .From("dpr").InnerJoin("dprt") + .On((dpr, dprt) => dprt.Dual == true && dprt.Id == dpr.RelationType, "dpr", + "dprt"); + + var innerUnionSql3 = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[dcr].childId as id", "[dcr].parentId as otherId", "[dcrt].[alias]", "[dcrt].[name]", "[dcrt].[isDependency]", "[dcrt].[dual]") + .From("dcr").InnerJoin("dcrt") + .On((dcr, dcrt) => dcrt.Dual == true && dcrt.Id == dcr.RelationType, "dcr", + "dcrt"); + + + var innerUnionSql = innerUnionSqlChild.Union(innerUnionSqlDualParent).Union(innerUnionSql3); + + return innerUnionSql; + } + /// /// Gets a page of the descending items that have any references, given a parent id. /// @@ -83,35 +116,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .From() .WhereLike(x => x.Path, subsubQuery); - // Get all relations where parent is in the sub query + Sql innerUnionSql = GetInnerUnionSql(); var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[pn].[id] as nodeId", - "[pn].[uniqueId] as nodeKey", - "[pn].[text] as nodeName", - "[pn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") - .WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "pn"); + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", aliasRight: "ctn"); + sql = sql?.WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "n"); if (filterMustBeIsDependency) { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); + sql = sql?.Where(rt => rt.IsDependency, "x"); } // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); + sql = sql?.OrderBy(x => x.Alias, "x"); var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); totalRecords = Convert.ToInt32(pagedResult?.TotalItems); @@ -119,41 +155,45 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); } + /// /// Gets a page of items which are in relation with the current item. /// Basically, shows the items which depend on the current item. /// public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) { + Sql innerUnionSql = GetInnerUnionSql(); var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[cn].[id] as nodeId", - "[cn].[uniqueId] as nodeKey", - "[cn].[text] as nodeName", - "[cn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "cn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") - .Where(x => x.NodeId == id, "pn") - .Where(x => x.ChildId == id || x.ParentId == id, "r"); // This last Where is purely to help SqlServer make a smarter query plan. More info https://github.com/umbraco/Umbraco-CMS/issues/12190 + "[x].[otherId] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.OtherId, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", aliasRight: "ctn") + .Where(x => x.Id == id, "x"); if (filterMustBeIsDependency) { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); + sql = sql?.Where(rt => rt.IsDependency, "x"); } // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); + sql = sql?.OrderBy(x => x.Alias, "x"); var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); totalRecords = Convert.ToInt32(pagedResult?.TotalItems); @@ -161,6 +201,27 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); } + private class UnionHelperDto + { + [Column("id")] + public int Id { get; set; } + + [Column("otherId")] + public int OtherId { get; set; } + + [Column("alias")] + public string? Alias { get; set; } + + [Column("name")] + public string? Name { get; set; } + + [Column("isDependency")] + public bool IsDependency { get; set; } + + [Column("dual")] + public bool Dual { get; set; } + } + private RelationItem MapDtoToEntity(RelationItemDto dto) { return new RelationItem() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs index a5a3fe4f21..fa981fa539 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs @@ -1,19 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +internal static class TupleExtensions { - static class TupleExtensions - { - public static IEnumerable Map(this Tuple, List> t, Func relator) - { - return t.Item1.Zip(t.Item2, relator); - } + public static IEnumerable Map( + this Tuple, List> t, + Func relator) => t.Item1.Zip(t.Item2, relator); -// public static IEnumerable Map(this Tuple, List, List> t, Func relator) -// { -// return t.Item1.Zip(t.Item2, t.Item3, relator); -// } - } + // public static IEnumerable Map(this Tuple, List, List> t, Func relator) + // { + // return t.Item1.Zip(t.Item2, t.Item3, relator); + // } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs index b74063df9b..efcd6f1d5c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -13,128 +11,138 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository { - internal class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository + public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) { - public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger) - : base(scopeAccessor, cache, logger) + } + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) => + await DeleteUserLoginsAsync(userOrMemberKey, null); + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey, string? providerName) + { + Sql sql = Sql() + .Delete() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey); + + if (providerName is not null) { + sql = sql.Where(x => x.ProviderName == providerName); } + var deletedRows = await Database.ExecuteAsync(sql); - protected override Sql GetBaseQuery(bool isCount) + return deletedRows > 0; + } + + public async Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey) + { + try { - var sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql.From(); - - return sql; - } - - protected override string GetBaseWhereClause() => - Core.Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id"; - - protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); - - protected override ITwoFactorLogin? PerformGet(int id) - { - var sql = GetBaseQuery(false).Where(x => x.Id == id); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - var dtos = Database.Fetch(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(Map).WhereNotNull(); - } - - protected override void PersistNewItem(ITwoFactorLogin entity) - { - var dto = Map(entity); - Database.Insert(dto); - } - - protected override void PersistUpdatedItem(ITwoFactorLogin entity) - { - var dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } - } - - private static TwoFactorLoginDto? Map(ITwoFactorLogin entity) - { - if (entity == null) return null; - - return new TwoFactorLoginDto - { - Id = entity.Id, - UserOrMemberKey = entity.UserOrMemberKey, - ProviderName = entity.ProviderName, - Secret = entity.Secret, - }; - } - - private static ITwoFactorLogin? Map(TwoFactorLoginDto dto) - { - if (dto == null) return null; - - return new TwoFactorLogin - { - Id = dto.Id, - UserOrMemberKey = dto.UserOrMemberKey, - ProviderName = dto.ProviderName, - Secret = dto.Secret, - }; - } - - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) - { - return await DeleteUserLoginsAsync(userOrMemberKey, null); - } - - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey, string? providerName) - { - var sql = Sql() - .Delete() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey); - - if (providerName is not null) - { - sql = sql.Where(x => x.ProviderName == providerName); - } - - var deletedRows = await Database.ExecuteAsync(sql); - - return deletedRows > 0; - } - - public async Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey) - { - var sql = Sql() + Sql sql = Sql() .Select() .From() .Where(x => x.UserOrMemberKey == userOrMemberKey); - var dtos = await Database.FetchAsync(sql); + List? dtos = await Database.FetchAsync(sql); return dtos.WhereNotNull().Select(Map).WhereNotNull(); } + + // TODO (v11): Remove this as the table should always exist when upgrading from 10.x + // SQL Server - table doesn't exist, likely upgrading from < 9.3.0. + catch (SqlException ex) when (ex.Number == 208 && ex.Message.Contains(TwoFactorLoginDto.TableName)) + { + return Enumerable.Empty(); + } + } + + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql.From(); + + return sql; + } + + protected override string GetBaseWhereClause() => + Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id"; + + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); + + protected override ITwoFactorLogin? PerformGet(int id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + TwoFactorLoginDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List? dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(Map).WhereNotNull(); + } + + protected override void PersistNewItem(ITwoFactorLogin entity) + { + TwoFactorLoginDto? dto = Map(entity); + Database.Insert(dto); + } + + protected override void PersistUpdatedItem(ITwoFactorLogin entity) + { + TwoFactorLoginDto? dto = Map(entity); + if (dto is not null) + { + Database.Update(dto); + } + } + + private static TwoFactorLoginDto? Map(ITwoFactorLogin entity) + { + if (entity == null) + { + return null; + } + + return new TwoFactorLoginDto + { + Id = entity.Id, + UserOrMemberKey = entity.UserOrMemberKey, + ProviderName = entity.ProviderName, + Secret = entity.Secret + }; + } + + private static ITwoFactorLogin? Map(TwoFactorLoginDto dto) + { + if (dto == null) + { + return null; + } + + return new TwoFactorLogin + { + Id = dto.Id, UserOrMemberKey = dto.UserOrMemberKey, ProviderName = dto.ProviderName, Secret = dto.Secret + }; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 7a3ca69db1..f95e86110b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -17,455 +14,494 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the UserGroupRepository for doing CRUD operations for +/// +public class UserGroupRepository : EntityRepositoryBase, IUserGroupRepository { - /// - /// Represents the UserGroupRepository for doing CRUD operations for - /// - public class UserGroupRepository : EntityRepositoryBase, IUserGroupRepository + private readonly PermissionRepository _permissionRepository; + private readonly IShortStringHelper _shortStringHelper; + private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; + + public UserGroupRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper) + : base(scopeAccessor, appCaches, logger) { - private readonly IShortStringHelper _shortStringHelper; - private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; - private readonly PermissionRepository _permissionRepository; + _shortStringHelper = shortStringHelper; + _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); + _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, loggerFactory.CreateLogger>()); + } - public UserGroupRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper) - : base(scopeAccessor, appCaches, logger) + public IUserGroup? Get(string alias) + { + try { - _shortStringHelper = shortStringHelper; - _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); - _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, loggerFactory.CreateLogger>()); - } - - - public static string GetByAliasCacheKey(string alias) - { - return CacheKeys.UserGroupGetByAliasCacheKeyPrefix + alias; - } - - public IUserGroup? Get(string alias) - { - try + // need to do a simple query to get the id - put this cache + var id = IsolatedCache.GetCacheItem(GetByAliasCacheKey(alias), () => { - //need to do a simple query to get the id - put this cache - var id = IsolatedCache.GetCacheItem(GetByAliasCacheKey(alias), () => + var groupId = + Database.ExecuteScalar("SELECT id FROM umbracoUserGroup WHERE userGroupAlias=@alias", new { alias }); + if (groupId.HasValue == false) { - var groupId = Database.ExecuteScalar("SELECT id FROM umbracoUserGroup WHERE userGroupAlias=@alias", new { alias }); - if (groupId.HasValue == false) throw new InvalidOperationException("No group found with alias " + alias); - return groupId.Value; - }); + throw new InvalidOperationException("No group found with alias " + alias); + } - //return from the normal method which will cache - return Get(id); - } - catch (InvalidOperationException) + return groupId.Value; + }); + + // return from the normal method which will cache + return Get(id); + } + catch (InvalidOperationException) + { + // if this is caught it's because we threw this in the caching method + return null; + } + } + + public IEnumerable GetGroupsAssignedToSection(string sectionAlias) + { + // Here we're building up a query that looks like this, a sub query is required because the resulting structure + // needs to still contain all of the section rows per user group. + + // SELECT * + // FROM [umbracoUserGroup] + // LEFT JOIN [umbracoUserGroup2App] + // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + // WHERE umbracoUserGroup.id IN (SELECT umbracoUserGroup.id + // FROM [umbracoUserGroup] + // LEFT JOIN [umbracoUserGroup2App] + // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + // WHERE umbracoUserGroup2App.app = 'content') + Sql sql = GetBaseQuery(QueryType.Many); + Sql innerSql = GetBaseQuery(QueryType.Ids); + innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); + sql.Where($"umbracoUserGroup.id IN ({innerSql.SQL})"); + AppendGroupBy(sql); + + return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } + + public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds) => + _userGroupWithUsersRepository.Save(new UserGroupWithUsers(userGroup, userIds)); + + /// + /// Gets explicitly defined permissions for the group for specified entities + /// + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + public EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds) => + _permissionRepository.GetPermissionsForEntities(groupIds, entityIds); + + /// + /// Gets explicit and default permissions (if requested) permissions for the group for specified entities + /// + /// + /// + /// If true will include the group's default permissions if no permissions are + /// explicitly assigned + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + public EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + var groupIds = groups.Select(x => x.Id).ToArray(); + EntityPermissionCollection explicitPermissions = GetPermissions(groupIds, nodeIds); + var result = new EntityPermissionCollection(explicitPermissions); + + // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults + if (fallbackToDefaultPermissions) + { + // if no node ids are passed in, then we need to determine the node ids for the explicit permissions set + nodeIds = nodeIds.Length == 0 + ? explicitPermissions.Select(x => x.EntityId).Distinct().ToArray() + : nodeIds; + + // if there are still no nodeids we can just exit + if (nodeIds.Length == 0) { - //if this is caught it's because we threw this in the caching method - return null; + return result; } - } - public IEnumerable GetGroupsAssignedToSection(string sectionAlias) - { - //Here we're building up a query that looks like this, a sub query is required because the resulting structure - // needs to still contain all of the section rows per user group. - - //SELECT * - //FROM [umbracoUserGroup] - //LEFT JOIN [umbracoUserGroup2App] - //ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] - //WHERE umbracoUserGroup.id IN (SELECT umbracoUserGroup.id - // FROM [umbracoUserGroup] - // LEFT JOIN [umbracoUserGroup2App] - // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] - // WHERE umbracoUserGroup2App.app = 'content') - - var sql = GetBaseQuery(QueryType.Many); - var innerSql = GetBaseQuery(QueryType.Ids); - innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); - sql.Where($"umbracoUserGroup.id IN ({innerSql.SQL})"); - AppendGroupBy(sql); - - return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } - - public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds) - { - _userGroupWithUsersRepository.Save(new UserGroupWithUsers(userGroup, userIds)); - } - - - /// - /// Gets explicitly defined permissions for the group for specified entities - /// - /// - /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds) - { - return _permissionRepository.GetPermissionsForEntities(groupIds, entityIds); - } - - /// - /// Gets explicit and default permissions (if requested) permissions for the group for specified entities - /// - /// - /// If true will include the group's default permissions if no permissions are explicitly assigned - /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (groups == null) throw new ArgumentNullException(nameof(groups)); - - var groupIds = groups.Select(x => x.Id).ToArray(); - var explicitPermissions = GetPermissions(groupIds, nodeIds); - var result = new EntityPermissionCollection(explicitPermissions); - - // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults - if (fallbackToDefaultPermissions) + foreach (IReadOnlyUserGroup group in groups) { - //if no node ids are passed in, then we need to determine the node ids for the explicit permissions set - nodeIds = nodeIds.Length == 0 - ? explicitPermissions.Select(x => x.EntityId).Distinct().ToArray() - : nodeIds; - - //if there are still no nodeids we can just exit - if (nodeIds.Length == 0) - return result; - - foreach (var group in groups) + foreach (var nodeId in nodeIds) { - foreach (var nodeId in nodeIds) - { - // TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be - // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not - // going to be included in the result! + // TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be + // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not + // going to be included in the result! + var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions?.ToArray() ?? Array.Empty(), true); - var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions?.ToArray() ?? Array.Empty(), isDefaultPermissions: true); - //Since this is a hashset, this will not add anything that already exists by group/node combination - result.Add(defaultPermission); - } + // Since this is a hashset, this will not add anything that already exists by group/node combination + result.Add(defaultPermission); } } - - return result; } - /// - /// Replaces the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of If nothing is specified all permissions are removed. - /// Specify the nodes to replace permissions for. - public void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + return result; + } + + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of group + /// + /// Permissions as enumerable list of If nothing is specified all permissions + /// are removed. + /// + /// Specify the nodes to replace permissions for. + public void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) => + _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); + + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for + public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) => + _permissionRepository.AssignPermission(groupId, permission, entityIds); + + public static string GetByAliasCacheKey(string alias) => CacheKeys.UserGroupGetByAliasCacheKeyPrefix + alias; + + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsers : EntityBase + { + public UserGroupWithUsers(IUserGroup userGroup, int[]? userIds) { - _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); + UserGroup = userGroup; + UserIds = userIds; } - /// - /// Assigns the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for - public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) + public override bool HasIdentity => UserGroup.HasIdentity; + + public IUserGroup UserGroup { get; } + + public int[]? UserIds { get; } + } + + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsersRepository : EntityRepositoryBase + { + private readonly UserGroupRepository _userGroupRepo; + + public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _userGroupRepo = userGroupRepo; + + protected override void PersistNewItem(UserGroupWithUsers entity) { - _permissionRepository.AssignPermission(groupId, permission, entityIds); - } + // save the user group + _userGroupRepo.PersistNewItem(entity.UserGroup); - #region Overrides of RepositoryBase - - protected override IUserGroup? PerformGet(int id) - { - var sql = GetBaseQuery(QueryType.Single); - sql.Where(GetBaseWhereClause(), new { id = id }); - - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references - - var dto = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql).FirstOrDefault(); - - if (dto == null) - return null; - - var userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto); - return userGroup; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.Id, ids); - else - sql.Where(x => x.Id >= 0); - - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references - - var dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); - return dtos.Select(x=>UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(QueryType.Many); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references - - var dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); - return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected Sql GetBaseQuery(QueryType type) - { - var sql = Sql(); - var addFrom = false; - - switch (type) + if (entity.UserIds == null) { - case QueryType.Count: - sql - .SelectCount() - .From(); - break; - case QueryType.Ids: - sql - .Select(x => x.Id); - addFrom = true; - break; - case QueryType.Single: - case QueryType.Many: - sql - .Select(r => - r.Select(x => x.UserGroup2AppDtos), - s => s.Append($", COUNT({sql.Columns(x => x.UserId)}) AS {SqlSyntax.GetQuotedColumnName("UserCount")}")); - addFrom = true; - break; - default: - throw new NotSupportedException(type.ToString()); + return; } - if (addFrom) - sql - .From() - .LeftJoin() - .On(left => left.Id, right => right.UserGroupId) - .LeftJoin() - .On(left => left.UserGroupId, right => right.Id); - - return sql; + // now the user association + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); } - protected override Sql GetBaseQuery(bool isCount) + protected override void PersistUpdatedItem(UserGroupWithUsers entity) { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Many); - } + // save the user group + _userGroupRepo.PersistUpdatedItem(entity.UserGroup); - private static void AppendGroupBy(Sql sql) - { - sql - .GroupBy(x => x.CreateDate, x => x.Icon, x => x.Id, x => x.StartContentId, x => x.StartMediaId, - x => x.UpdateDate, x => x.Alias, x => x.DefaultPermissions, x => x.Name) - .AndBy(x => x.AppAlias, x => x.UserGroupId); - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IUserGroup entity) - { - entity.AddingEntity(); - - var userGroupDto = UserGroupFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(userGroupDto)); - entity.Id = id; - - PersistAllowedSections(entity); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IUserGroup entity) - { - entity.UpdatingEntity(); - - var userGroupDto = UserGroupFactory.BuildDto(entity); - - Database.Update(userGroupDto); - - PersistAllowedSections(entity); - - entity.ResetDirtyProperties(); - } - - private void PersistAllowedSections(IUserGroup entity) - { - var userGroup = entity; - - // First delete all - Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); - - // Then re-add any associated with the group - foreach (var app in userGroup.AllowedSections) + if (entity.UserIds == null) { - var dto = new UserGroup2AppDto - { - UserGroupId = userGroup.Id, - AppAlias = app - }; + return; + } + + // now the user association + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); + } + + /// + /// Adds a set of users to a group, first removing any that exist + /// + /// Id of group + /// Ids of users + private void RefreshUsersInGroup(int groupId, int[] userIds) + { + RemoveAllUsersFromGroup(groupId); + AddUsersToGroup(groupId, userIds); + } + + /// + /// Removes all users from a group + /// + /// Id of group + private void RemoveAllUsersFromGroup(int groupId) => + Database.Delete("WHERE userGroupId = @groupId", new { groupId }); + + /// + /// Adds a set of users to a group + /// + /// Id of group + /// Ids of users + private void AddUsersToGroup(int groupId, int[] userIds) + { + foreach (var userId in userIds) + { + var dto = new User2UserGroupDto { UserGroupId = groupId, UserId = userId }; Database.Insert(dto); } } + #region Not implemented (don't need to for the purposes of this repo) + + protected override UserGroupWithUsers PerformGet(int id) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetAll(params int[]? ids) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + #endregion + } - /// - /// used to persist a user group with associated users at once - /// - private class UserGroupWithUsers : EntityBase + #region Overrides of RepositoryBase + + protected override IUserGroup? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single); + sql.Where(GetBaseWhereClause(), new { id }); + + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references + + UserGroupDto? dto = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql).FirstOrDefault(); + + if (dto == null) { - public UserGroupWithUsers(IUserGroup userGroup, int[]? userIds) - { - UserGroup = userGroup; - UserIds = userIds; - } - - public override bool HasIdentity => UserGroup.HasIdentity; - - public IUserGroup UserGroup { get; } - public int[]? UserIds { get; } + return null; } - /// - /// used to persist a user group with associated users at once - /// - private class UserGroupWithUsersRepository : EntityRepositoryBase + dto.UserGroup2LanguageDtos = GetUserGroupLanguages(id); + + IUserGroup userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto); + return userGroup; + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) { - private readonly UserGroupRepository _userGroupRepo; + sql.WhereIn(x => x.Id, ids); + } + else + { + sql.Where(x => x.Id >= 0); + } - public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _userGroupRepo = userGroupRepo; - } + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references - #region Not implemented (don't need to for the purposes of this repo) + List dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); - protected override UserGroupWithUsers PerformGet(int id) - { - throw new InvalidOperationException("This method won't be implemented."); - } + IDictionary> dic = GetAllUserGroupLanguageGrouped(); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new InvalidOperationException("This method won't be implemented."); - } + foreach (UserGroupDto dto in dtos) + { + dic.TryGetValue(dto.Id, out var userGroup2LanguageDtos); + dto.UserGroup2LanguageDtos = userGroup2LanguageDtos ?? new(); + } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } + List? dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } - #endregion + #endregion - protected override void PersistNewItem(UserGroupWithUsers entity) - { - //save the user group - _userGroupRepo.PersistNewItem(entity.UserGroup); + #region Overrides of EntityRepositoryBase - if (entity.UserIds == null) - return; + protected Sql GetBaseQuery(QueryType type) + { + Sql sql = Sql(); + var addFrom = false; - //now the user association - RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); - } + switch (type) + { + case QueryType.Count: + sql + .SelectCount() + .From(); + break; + case QueryType.Ids: + sql + .Select(x => x.Id); + addFrom = true; + break; + case QueryType.Single: + case QueryType.Many: + sql.Select(r => r.Select(x => x.UserGroup2AppDtos), s => s.Append($", COUNT({sql.Columns(x => x.UserId)}) AS {SqlSyntax.GetQuotedColumnName("UserCount")}")); + addFrom = true; + break; + default: + throw new NotSupportedException(type.ToString()); + } - protected override void PersistUpdatedItem(UserGroupWithUsers entity) - { - //save the user group - _userGroupRepo.PersistUpdatedItem(entity.UserGroup); + if (addFrom) + { + sql + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId) + .LeftJoin() + .On(left => left.UserGroupId, right => right.Id); + } - if (entity.UserIds == null) - return; + return sql; + } - //now the user association - RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); - } + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Many); - /// - /// Adds a set of users to a group, first removing any that exist - /// - /// Id of group - /// Ids of users - private void RefreshUsersInGroup(int groupId, int[] userIds) - { - RemoveAllUsersFromGroup(groupId); - AddUsersToGroup(groupId, userIds); - } + private static void AppendGroupBy(Sql sql) => + sql.GroupBy( + x => x.CreateDate, + x => x.Icon, + x => x.Id, + x => x.StartContentId, + x => x.StartMediaId, + x => x.UpdateDate, + x => x.Alias, + x => x.DefaultPermissions, + x => x.Name, + x => x.HasAccessToAllLanguages) + .AndBy(x => x.AppAlias, x => x.UserGroupId); - /// - /// Removes all users from a group - /// - /// Id of group - private void RemoveAllUsersFromGroup(int groupId) - { - Database.Delete("WHERE userGroupId = @groupId", new { groupId }); - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id"; - /// - /// Adds a set of users to a group - /// - /// Id of group - /// Ids of users - private void AddUsersToGroup(int groupId, int[] userIds) - { - foreach (var userId in userIds) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupId, - UserId = userId, - }; - Database.Insert(dto); - } - } + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup WHERE id = @id", + }; + return list; + } + + protected override void PersistNewItem(IUserGroup entity) + { + entity.AddingEntity(); + + UserGroupDto userGroupDto = UserGroupFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(userGroupDto)); + entity.Id = id; + + PersistAllowedSections(entity); + PersistAllowedLanguages(entity); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IUserGroup entity) + { + entity.UpdatingEntity(); + + UserGroupDto userGroupDto = UserGroupFactory.BuildDto(entity); + + Database.Update(userGroupDto); + + PersistAllowedSections(entity); + PersistAllowedLanguages(entity); + + entity.ResetDirtyProperties(); + } + + private void PersistAllowedSections(IUserGroup entity) + { + IUserGroup userGroup = entity; + + // First delete all + Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); + + // Then re-add any associated with the group + foreach (var app in userGroup.AllowedSections) + { + var dto = new UserGroup2AppDto { UserGroupId = userGroup.Id, AppAlias = app }; + Database.Insert(dto); } } + + private void PersistAllowedLanguages(IUserGroup entity) + { + var userGroup = entity; + + // First delete all + Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); + + // Then re-add any associated with the group + foreach (var language in userGroup.AllowedLanguages) + { + var dto = new UserGroup2LanguageDto + { + UserGroupId = userGroup.Id, + LanguageId = language, + }; + + Database.Insert(dto); + } + } + + private List GetUserGroupLanguages(int userGroupId) + { + Sql query = Sql() + .Select() + .From() + .Where(x => x.UserGroupId == userGroupId); + return Database.Fetch(query); + } + + private IDictionary> GetAllUserGroupLanguageGrouped() + { + Sql query = Sql() + .Select() + .From(); + List userGroupLanguages = Database.Fetch(query); + return userGroupLanguages.GroupBy(x => x.UserGroupId).ToDictionary(x => x.Key, x => x.ToList()); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 0737d89af4..681f2a617e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -22,163 +20,170 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +/// +/// Represents the UserRepository for doing CRUD operations for +/// +internal class UserRepository : EntityRepositoryBase, IUserRepository { + private readonly IMapperCollection _mapperCollection; + private readonly GlobalSettings _globalSettings; + private readonly UserPasswordConfigurationSettings _passwordConfiguration; + private readonly IJsonSerializer _jsonSerializer; + private readonly IRuntimeState _runtimeState; + private string? _passwordConfigJson; + private bool _passwordConfigInitialized; + private readonly object _sqliteValidateSessionLock = new(); + /// - /// Represents the UserRepository for doing CRUD operations for + /// Initializes a new instance of the class. /// - internal class UserRepository : EntityRepositoryBase, IUserRepository + /// The scope accessor. + /// The application caches. + /// The logger. + /// + /// A dictionary specifying the configuration for user passwords. If this is null then no + /// password configuration will be persisted or read. + /// + /// The global settings. + /// The password configuration. + /// The JSON serializer. + /// State of the runtime. + /// + /// mapperCollection + /// or + /// globalSettings + /// or + /// passwordConfiguration + /// + public UserRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + IMapperCollection mapperCollection, + IOptions globalSettings, + IOptions passwordConfiguration, + IJsonSerializer jsonSerializer, + IRuntimeState runtimeState) + : base(scopeAccessor, appCaches, logger) { - private readonly IMapperCollection _mapperCollection; - private readonly GlobalSettings _globalSettings; - private readonly UserPasswordConfigurationSettings _passwordConfiguration; - private readonly IJsonSerializer _jsonSerializer; - private readonly IRuntimeState _runtimeState; - private string? _passwordConfigJson; - private bool _passwordConfigInitialized; - private readonly object _sqliteValidateSessionLock = new(); + _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); + _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); + _passwordConfiguration = + passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + _jsonSerializer = jsonSerializer; + _runtimeState = runtimeState; + } - /// - /// Initializes a new instance of the class. - /// - /// The scope accessor. - /// The application caches. - /// The logger. - /// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read. - /// The global settings. - /// The password configuration. - /// The JSON serializer. - /// State of the runtime. - /// mapperCollection - /// or - /// globalSettings - /// or - /// passwordConfiguration - public UserRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - IMapperCollection mapperCollection, - IOptions globalSettings, - IOptions passwordConfiguration, - IJsonSerializer jsonSerializer, - IRuntimeState runtimeState) - : base(scopeAccessor, appCaches, logger) + /// + /// Returns a serialized dictionary of the password configuration that is stored against the user in the database + /// + private string? DefaultPasswordConfigJson + { + get { - _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); - _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); - _passwordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - _jsonSerializer = jsonSerializer; - _runtimeState = runtimeState; - } - - /// - /// Returns a serialized dictionary of the password configuration that is stored against the user in the database - /// - private string? DefaultPasswordConfigJson - { - get + if (_passwordConfigInitialized) { - if (_passwordConfigInitialized) - { - return _passwordConfigJson; - } - - var passwordConfig = new PersistedPasswordSettings - { - HashAlgorithm = _passwordConfiguration.HashAlgorithmType - }; - - _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); - _passwordConfigInitialized = true; return _passwordConfigJson; } - } - #region Overrides of RepositoryBase - - protected override IUser? PerformGet(int id) - { - // This will never resolve to a user, yet this is asked - // for all of the time (especially in cases of members). - // Don't issue a SQL call for this, we know it will not exist. - if (_runtimeState.Level == RuntimeLevel.Upgrade) + var passwordConfig = new PersistedPasswordSettings { - // when upgrading people might come from version 7 where user 0 was the default, - // only in upgrade mode do we want to fetch the user of Id 0 - if (id < -1) - { - return null; - } - } - else + HashAlgorithm = _passwordConfiguration.HashAlgorithmType + }; + + _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); + _passwordConfigInitialized = true; + return _passwordConfigJson; + } + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) => + dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + + #region Overrides of RepositoryBase + + protected override IUser? PerformGet(int id) + { + // This will never resolve to a user, yet this is asked + // for all of the time (especially in cases of members). + // Don't issue a SQL call for this, we know it will not exist. + if (_runtimeState.Level == RuntimeLevel.Upgrade) + { + // when upgrading people might come from version 7 where user 0 was the default, + // only in upgrade mode do we want to fetch the user of Id 0 + if (id < -1) { - if (id == default || id < -1) - { - return null; - } + return null; + } + } + else + { + if (id == default || id < -1) + { + return null; } - - var sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.Id == id); - - var dtos = Database.Fetch(sql); - if (dtos.Count == 0) return null; - - PerformGetReferencedDtos(dtos); - return UserFactory.BuildEntity(_globalSettings, dtos[0]); } - /// - /// Returns a user by username - /// - /// - /// - /// Can be used for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes). - /// This is really only used for a shim in order to upgrade to 7.6. - /// - /// - /// A non cached instance - /// - public IUser? GetByUsername(string username, bool includeSecurityData) + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.Id == id); + + List? dtos = Database.Fetch(sql); + if (dtos.Count == 0) { - return GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); + return null; } - /// - /// Returns a user by id - /// - /// - /// - /// This is really only used for a shim in order to upgrade to 7.6 but could be used - /// for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes) - /// - /// - /// A non cached instance - /// - public IUser? Get(int? id, bool includeSecurityData) - { - return GetWith(sql => sql.Where(x => x.Id == id), includeSecurityData); - } + PerformGetReferencedDtos(dtos); + return UserFactory.BuildEntity(_globalSettings, dtos[0]); + } - public IProfile? GetProfile(string username) - { - var dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); - return dto == null ? null : new UserProfile(dto.Id, dto.UserName); - } + /// + /// Returns a user by username + /// + /// + /// + /// Can be used for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes). + /// This is really only used for a shim in order to upgrade to 7.6. + /// + /// + /// A non cached instance + /// + public IUser? GetByUsername(string username, bool includeSecurityData) => + GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); - public IProfile? GetProfile(int id) - { - var dto = GetDtoWith(sql => sql.Where(x => x.Id == id), false); - return dto == null ? null : new UserProfile(dto.Id, dto.UserName); - } + /// + /// Returns a user by id + /// + /// + /// + /// This is really only used for a shim in order to upgrade to 7.6 but could be used + /// for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes) + /// + /// + /// A non cached instance + /// + public IUser? Get(int? id, bool includeSecurityData) => + GetWith(sql => sql.Where(x => x.Id == id), includeSecurityData); - public IDictionary GetUserStates() - { - // These keys in this query map to the `Umbraco.Core.Models.Membership.UserState` enum - var sql = @"SELECT -1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser + public IProfile? GetProfile(string username) + { + UserDto? dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); + return dto == null ? null : new UserProfile(dto.Id, dto.UserName); + } + + public IProfile? GetProfile(int id) + { + UserDto? dto = GetDtoWith(sql => sql.Where(x => x.Id == id), false); + return dto == null ? null : new UserProfile(dto.Id, dto.UserName); + } + + public IDictionary GetUserStates() + { + // These keys in this query map to the `Umbraco.Core.Models.Membership.UserState` enum + var sql = @"SELECT -1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser UNION SELECT 0 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL UNION @@ -190,36 +195,36 @@ SELECT 3 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE lastLoginDate IS UNION SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL"; - var result = Database.Dictionary(sql); + Dictionary? result = Database.Dictionary(sql); - return result.ToDictionary(x => (UserState)x.Key, x => x.Value); + return result.ToDictionary(x => (UserState)x.Key, x => x.Value); + } + + public Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true) + { + DateTime now = DateTime.UtcNow; + var dto = new UserLoginDto + { + UserId = userId, + IpAddress = requestingIpAddress, + LoggedInUtc = now, + LastValidatedUtc = now, + LoggedOutUtc = null, + SessionId = Guid.NewGuid() + }; + Database.Insert(dto); + + if (cleanStaleSessions) + { + ClearLoginSessions(TimeSpan.FromDays(15)); } - public Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true) - { - var now = DateTime.UtcNow; - var dto = new UserLoginDto - { - UserId = userId, - IpAddress = requestingIpAddress, - LoggedInUtc = now, - LastValidatedUtc = now, - LoggedOutUtc = null, - SessionId = Guid.NewGuid() - }; - Database.Insert(dto); + return dto.SessionId; + } - if (cleanStaleSessions) - { - ClearLoginSessions(TimeSpan.FromDays(15)); - } - - return dto.SessionId; - } - - public bool ValidateLoginSession(int userId, Guid sessionId) - { - // HACK: Avoid a deadlock - BackOfficeCookieOptions OnValidatePrincipal + public bool ValidateLoginSession(int userId, Guid sessionId) + { + // HACK: Avoid a deadlock - BackOfficeCookieOptions OnValidatePrincipal // After existing session times out and user logs in again ~ 4 requests come in at once that hit the // "update the validate date" code path, check up the call stack there are a few variables that can make this not occur. // TODO: more generic fix, do something with ForUpdate? wait on a mutex? add a distributed lock? etc. @@ -237,719 +242,814 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 private bool ValidateLoginSessionInternal(int userId, Guid sessionId) { // with RepeatableRead transaction mode, read-then-update operations can - // cause deadlocks, and the ForUpdate() hint is required to tell the database - // to acquire an exclusive lock when reading + // cause deadlocks, and the ForUpdate() hint is required to tell the database + // to acquire an exclusive lock when reading - // that query is going to run a *lot*, make it a template - var t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s - .Select() - .From() - .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) - .ForUpdate() - .SelectTop(1)); // Stick at end, SQL server syntax provider will insert at start of query after "select ", but sqlite will append limit to end. + // that query is going to run a *lot*, make it a template + SqlTemplate t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s + .Select() + .From() + .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) + .ForUpdate() + .SelectTop(1)); // Stick at end, SQL server syntax provider will insert at start of query after "select ", but sqlite will append limit to end. - var sql = t.Sql(sessionId); + Sql sql = t.Sql(sessionId); - var found = Database.FirstOrDefault(sql); - if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) - return false; - - // now detect if there's been a timeout - if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) - { - // timeout detected, update the record - Logger.LogDebug("ClearLoginSession for sessionId {sessionId}", sessionId); - ClearLoginSession(sessionId); - return false; - } - - // update the validate date - Logger.LogDebug("Updating LastValidatedUtc for sessionId {sessionId}", sessionId); - found.LastValidatedUtc = DateTime.UtcNow; - Database.Update(found); - return true; + UserLoginDto? found = Database.FirstOrDefault(sql); + if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) + { + return false; } - public int ClearLoginSessions(int userId) + //now detect if there's been a timeout + if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) { - return Database.Delete(Sql().Where(x => x.UserId == userId)); + //timeout detected, update the record + Logger.LogDebug("ClearLoginSession for sessionId {sessionId}", sessionId);ClearLoginSession(sessionId); + return false; } - public int ClearLoginSessions(TimeSpan timespan) + //update the validate date + Logger.LogDebug("Updating LastValidatedUtc for sessionId {sessionId}", sessionId);found.LastValidatedUtc = DateTime.UtcNow; + Database.Update(found); + return true; + } + + public int ClearLoginSessions(int userId) => + Database.Delete(Sql().Where(x => x.UserId == userId)); + + public int ClearLoginSessions(TimeSpan timespan) + { + DateTime fromDate = DateTime.UtcNow - timespan; + return Database.Delete(Sql().Where(x => x.LastValidatedUtc < fromDate)); + } + + public void ClearLoginSession(Guid sessionId) => + // TODO: why is that one updating and not deleting? + Database.Execute(Sql() + .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow)) + .Where(x => x.SessionId == sessionId)); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + List dtos = ids?.Length == 0 + ? GetDtosWith(null, true) + : GetDtosWith(sql => sql.WhereIn(x => x.Id, ids), true); + var users = new IUser[dtos.Count]; + var i = 0; + foreach (UserDto dto in dtos) { - var fromDate = DateTime.UtcNow - timespan; - return Database.Delete(Sql().Where(x => x.LastValidatedUtc < fromDate)); + users[i++] = UserFactory.BuildEntity(_globalSettings, dto); } - public void ClearLoginSession(Guid sessionId) + return users; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var dtos = GetDtosWith(sql => new SqlTranslator(sql, query).Translate(), true) + .DistinctBy(x => x.Id) + .ToList(); + + var users = new IUser[dtos.Count]; + var i = 0; + foreach (UserDto dto in dtos) { - // TODO: why is that one updating and not deleting? - Database.Execute(Sql() - .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow)) - .Where(x => x.SessionId == sessionId)); + users[i++] = UserFactory.BuildEntity(_globalSettings, dto); } - protected override IEnumerable PerformGetAll(params int[]? ids) + return users; + } + + private IUser? GetWith(Action> with, bool includeReferences) + { + UserDto? dto = GetDtoWith(with, includeReferences); + return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); + } + + private UserDto? GetDtoWith(Action> with, bool includeReferences) + { + List dtos = GetDtosWith(with, includeReferences); + return dtos.FirstOrDefault(); + } + + private List GetDtosWith(Action>? with, bool includeReferences) + { + Sql sql = SqlContext.Sql() + .Select() + .From(); + + with?.Invoke(sql); + + List? dtos = Database.Fetch(sql); + + if (includeReferences) { - var dtos = ids?.Length == 0 - ? GetDtosWith(null, true) - : GetDtosWith(sql => sql.WhereIn(x => x.Id, ids), true); - var users = new IUser[dtos.Count]; - var i = 0; - foreach (var dto in dtos) - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); - return users; + PerformGetReferencedDtos(dtos); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var dtos = GetDtosWith(sql => new SqlTranslator(sql, query).Translate(), true) - .DistinctBy(x => x.Id) - .ToList(); + return dtos; + } - var users = new IUser[dtos.Count]; - var i = 0; - foreach (var dto in dtos) - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); - return users; + // NPoco cannot fetch 2+ references at a time + // plus it creates a combinatorial explosion + // better use extra queries + // unfortunately, SqlCe doesn't support multiple result sets + private void PerformGetReferencedDtos(List dtos) + { + if (dtos.Count == 0) + { + return; } - private IUser? GetWith(Action> with, bool includeReferences) + List userIds = dtos.Count == 1 ? new List {dtos[0].Id} : dtos.Select(x => x.Id).ToList(); + Dictionary? xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); + + // get users2groups + + Sql sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserId, userIds); + + List? user2Groups = Database.Fetch(sql); + var groupIds = user2Groups.Select(x => x.UserGroupId).ToList(); + + // get groups + // We wrap this in a try-catch, as this might throw errors when you try to login before having migrated your database + Dictionary groups; + try { - var dto = GetDtoWith(with, includeReferences); - return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); - } - - private UserDto? GetDtoWith(Action> with, bool includeReferences) - { - var dtos = GetDtosWith(with, includeReferences); - return dtos.FirstOrDefault(); - } - - private List GetDtosWith(Action>? with, bool includeReferences) - { - var sql = SqlContext.Sql() - .Select() - .From(); - - with?.Invoke(sql); - - var dtos = Database.Fetch(sql); - - if (includeReferences) - PerformGetReferencedDtos(dtos); - - return dtos; - } - - // NPoco cannot fetch 2+ references at a time - // plus it creates a combinatorial explosion - // better use extra queries - // unfortunately, SqlCe doesn't support multiple result sets - private void PerformGetReferencedDtos(List dtos) - { - if (dtos.Count == 0) return; - - var userIds = dtos.Count == 1 ? new List { dtos[0].Id } : dtos.Select(x => x.Id).ToList(); - var xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); - - // get users2groups - - var sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserId, userIds); - - var users2groups = Database.Fetch(sql); - var groupIds = users2groups.Select(x => x.UserGroupId).ToList(); - - // get groups - sql = SqlContext.Sql() .Select() .From() .WhereIn(x => x.Id, groupIds); - var groups = Database.Fetch(sql) + groups = Database.Fetch(sql) .ToDictionary(x => x.Id, x => x); - - // get groups2apps + } + catch(Exception e) + { + Logger.LogDebug(e, "Couldn't get user groups. This should only happens doing the migration that add new columns to user groups"); sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserGroupId, groupIds); + .Select(x=>x.Id, x=>x.Alias, x=>x.StartContentId, x=>x.StartMediaId) + .From() + .WhereIn(x => x.Id, groupIds); - var groups2apps = Database.Fetch(sql) + groups = Database.Fetch(sql) + .ToDictionary(x => x.Id, x => x); + } + + // get groups2apps + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserGroupId, groupIds); + + var groups2Apps = Database.Fetch(sql) + .GroupBy(x => x.UserGroupId) + .ToDictionary(x => x.Key, x => x); + + // get start nodes + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserId, userIds); + + List? startNodes = Database.Fetch(sql); + + // get groups2languages + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserGroupId, groupIds); + + Dictionary> groups2languages; + try + { + groups2languages = Database.Fetch(sql) .GroupBy(x => x.UserGroupId) .ToDictionary(x => x.Key, x => x); + } + catch + { + // If we get an error, the table has not been made in the database yet, set the list to an empty one + groups2languages = new Dictionary>(); + } - // get start nodes + // map groups - sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserId, userIds); - - var startNodes = Database.Fetch(sql); - - // map groups - - foreach (var user2group in users2groups) + foreach (User2UserGroupDto? user2Group in user2Groups) + { + if (groups.TryGetValue(user2Group.UserGroupId, out UserGroupDto? group)) { - if (groups.TryGetValue(user2group.UserGroupId, out var group)) - { - var dto = xUsers == null ? dtos[0] : xUsers[user2group.UserId]; - dto.UserGroupDtos.Add(group); // user2group is distinct - } - } - - // map start nodes - - foreach (var startNode in startNodes) - { - var dto = xUsers == null ? dtos[0] : xUsers[startNode.UserId]; - dto.UserStartNodeDtos.Add(startNode); // hashset = distinct - } - - // map apps - - foreach (var group in groups.Values) - { - if (groups2apps.TryGetValue(group.Id, out var list)) - group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct + UserDto dto = xUsers == null ? dtos[0] : xUsers[user2Group.UserId]; + dto.UserGroupDtos.Add(group); // user2group is distinct } } - #endregion + // map start nodes - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) + foreach (UserStartNodeDto? startNode in startNodes) { - if (isCount) - return SqlContext.Sql() - .SelectCount() - .From(); + UserDto dto = xUsers == null ? dtos[0] : xUsers[startNode.UserId]; + dto.UserStartNodeDtos.Add(startNode); // hashset = distinct + } + // map apps + + foreach (UserGroupDto? group in groups.Values) + { + if (groups2Apps.TryGetValue(group.Id, out IGrouping? list)) + { + group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct + } + + } + + // map languages + + foreach (var group in groups.Values) + { + if (groups2languages.TryGetValue(group.Id, out var list)) + { + group.UserGroup2LanguageDtos = list.ToList(); // groups2apps is distinct + } + } + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) + { return SqlContext.Sql() - .Select() + .SelectCount() .From(); } - private static void AddGroupLeftJoin(Sql sql) + return SqlContext.Sql() + .Select() + .From(); + } + + private static void AddGroupLeftJoin(Sql sql) => + sql + .LeftJoin() + .On(left => left.UserId, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId) + .LeftJoin() + .On(left => left.UserGroupId, right => right.Id) + .LeftJoin() + .On(left => left.UserId, right => right.Id); + + private Sql GetBaseQuery(string columns) => + SqlContext.Sql() + .Select(columns) + .From(); + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.User}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - sql - .LeftJoin() - .On(left => left.UserId, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserGroupId) - .LeftJoin() - .On(left => left.UserGroupId, right => right.Id) - .LeftJoin() - .On(left => left.UserId, right => right.Id); + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" + }; + return list; + } + + protected override void PersistNewItem(IUser entity) + { + entity.AddingEntity(); + + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) + { + entity.SecurityStamp = Guid.NewGuid().ToString(); } - private Sql GetBaseQuery(string columns) + UserDto userDto = UserFactory.BuildDto(entity); + + // check if we have a user config else use the default + userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + + var id = Convert.ToInt32(Database.Insert(userDto)); + entity.Id = id; + + if (entity.IsPropertyDirty("StartContentIds")) { - return SqlContext.Sql() - .Select(columns) - .From(); + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), + UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); } - protected override string GetBaseWhereClause() + if (entity.IsPropertyDirty("StartMediaIds")) { - return $"{Constants.DatabaseSchema.Tables.User}.id = @id"; + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), + UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); } - protected override IEnumerable GetDeleteClauses() + if (entity.IsPropertyDirty("Groups")) { - var list = new List + // lookup all assigned + List? assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", + new {aliases = entity.Groups.Select(x => x.Alias)}); + + foreach (UserGroupDto? groupDto in assigned) { - $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IUser entity) - { - entity.AddingEntity(); - - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - UserDto userDto = UserFactory.BuildDto(entity); - - // check if we have a user config else use the default - userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - - var id = Convert.ToInt32(Database.Insert(userDto)); - entity.Id = id; - - if (entity.IsPropertyDirty("StartContentIds")) - { - AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); - } - - if (entity.IsPropertyDirty("StartMediaIds")) - { - AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); - } - - if (entity.IsPropertyDirty("Groups")) - { - // lookup all assigned - var assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); - - foreach (var groupDto in assigned) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupDto.Id, - UserId = entity.Id - }; - Database.Insert(dto); - } - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IUser entity) - { - // updates Modified date - entity.UpdatingEntity(); - - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - var userDto = UserFactory.BuildDto(entity); - - // build list of columns to check for saving - we don't want to save the password if it hasn't changed! - // list the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that - var colsToSave = new Dictionary - { - //TODO: Change these to constants + nameof - {"userDisabled", "IsApproved"}, - {"userNoConsole", "IsLockedOut"}, - {"startStructureID", "StartContentId"}, - {"startMediaID", "StartMediaId"}, - {"userName", "Name"}, - {"userLogin", "Username"}, - {"userEmail", "Email"}, - {"userLanguage", "Language"}, - {"securityStampToken", "SecurityStamp"}, - {"lastLockoutDate", "LastLockoutDate"}, - {"lastPasswordChangeDate", "LastPasswordChangeDate"}, - {"lastLoginDate", "LastLoginDate"}, - {"failedLoginAttempts", "FailedPasswordAttempts"}, - {"createDate", "CreateDate"}, - {"updateDate", "UpdateDate"}, - {"avatar", "Avatar"}, - {"emailConfirmedDate", "EmailConfirmedDate"}, - {"invitedDate", "InvitedDate"}, - {"tourData", "TourData"} - }; - - // create list of properties that have changed - var changedCols = colsToSave - .Where(col => entity.IsPropertyDirty(col.Value)) - .Select(col => col.Key) - .ToList(); - - if (entity.IsPropertyDirty("SecurityStamp")) - { - changedCols.Add("securityStampToken"); - } - - // DO NOT update the password if it has not changed or if it is null or empty - if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) - { - changedCols.Add("userPassword"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - - // check if we have a user config else use the default - userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } - - // If userlogin or the email has changed then need to reset security stamp - if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) - { - userDto.EmailConfirmedDate = null; - changedCols.Add("emailConfirmedDate"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - } - - //only update the changed cols - if (changedCols.Count > 0) - { - Database.Update(userDto, changedCols); - } - - if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) - { - var assignedStartNodes = Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", new { userId = entity.Id }); - if (entity.IsPropertyDirty("StartContentIds")) - { - AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); - } - if (entity.IsPropertyDirty("StartMediaIds")) - { - AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); - } - } - - if (entity.IsPropertyDirty("Groups")) - { - //lookup all assigned - var assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); - - //first delete all - // TODO: We could do this a nicer way instead of "Nuke and Pave" - Database.Delete("WHERE UserId = @UserId", new { UserId = entity.Id }); - - foreach (var groupDto in assigned) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupDto.Id, - UserId = entity.Id - }; - Database.Insert(dto); - } - } - - entity.ResetDirtyProperties(); - } - - private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds) - { - if (entityStartIds is null) - { - return; - } - var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); - - //remove the ones not assigned to the entity - var toDelete = assignedIds.Except(entityStartIds).ToArray(); - if (toDelete.Length > 0) - Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", new { UserId = entity.Id, startNodes = toDelete }); - //add the ones not currently in the db - var toAdd = entityStartIds.Except(assignedIds).ToArray(); - foreach (var i in toAdd) - { - var dto = new UserStartNodeDto - { - StartNode = i, - StartNodeType = (int)startNodeType, - UserId = entity.Id - }; + var dto = new User2UserGroupDto {UserGroupId = groupDto.Id, UserId = entity.Id}; Database.Insert(dto); } } - #endregion + entity.ResetDirtyProperties(); + } - #region Implementation of IUserRepository + protected override void PersistUpdatedItem(IUser entity) + { + // updates Modified date + entity.UpdatingEntity(); - public int GetCountByQuery(IQuery? query) + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) { - var sqlClause = GetBaseQuery("umbracoUser.id"); - var translator = new SqlTranslator(sqlClause, query); - var subquery = translator.Translate(); - //get the COUNT base query - var sql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoUser.id IN (" + subquery.SQL + ")", subquery.Arguments)); - - return Database.ExecuteScalar(sql); + entity.SecurityStamp = Guid.NewGuid().ToString(); } - public bool ExistsByUserName(string username) - { - var sql = SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.UserName == username); + UserDto userDto = UserFactory.BuildDto(entity); - return Database.ExecuteScalar(sql) > 0; + // build list of columns to check for saving - we don't want to save the password if it hasn't changed! + // list the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that + var colsToSave = new Dictionary + { + //TODO: Change these to constants + nameof + {"userDisabled", "IsApproved"}, + {"userNoConsole", "IsLockedOut"}, + {"startStructureID", "StartContentId"}, + {"startMediaID", "StartMediaId"}, + {"userName", "Name"}, + {"userLogin", "Username"}, + {"userEmail", "Email"}, + {"userLanguage", "Language"}, + {"securityStampToken", "SecurityStamp"}, + {"lastLockoutDate", "LastLockoutDate"}, + {"lastPasswordChangeDate", "LastPasswordChangeDate"}, + {"lastLoginDate", "LastLoginDate"}, + {"failedLoginAttempts", "FailedPasswordAttempts"}, + {"createDate", "CreateDate"}, + {"updateDate", "UpdateDate"}, + {"avatar", "Avatar"}, + {"emailConfirmedDate", "EmailConfirmedDate"}, + {"invitedDate", "InvitedDate"}, + {"tourData", "TourData"} + }; + + // create list of properties that have changed + var changedCols = colsToSave + .Where(col => entity.IsPropertyDirty(col.Value)) + .Select(col => col.Key) + .ToList(); + + if (entity.IsPropertyDirty("SecurityStamp")) + { + changedCols.Add("securityStampToken"); } - public bool ExistsByLogin(string login) + // DO NOT update the password if it has not changed or if it is null or empty + if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) { - var sql = SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.Login == login); + changedCols.Add("userPassword"); - return Database.ExecuteScalar(sql) > 0; - } - - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - public IEnumerable GetAllInGroup(int groupId) - { - return GetAllInOrNotInGroup(groupId, true); - } - - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - public IEnumerable GetAllNotInGroup(int groupId) - { - return GetAllInOrNotInGroup(groupId, false); - } - - private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) - { - var sql = SqlContext.Sql() - .Select() - .From(); - - var inSql = SqlContext.Sql() - .Select(x => x.UserId) - .From() - .Where(x => x.UserGroupId == groupId); - - if (include) - sql.WhereIn(x => x.Id, inSql); - else - sql.WhereNotIn(x => x.Id, inSql); - - - var dtos = Database.Fetch(sql); - - //adds missing bits like content and media start nodes - PerformGetReferencedDtos(dtos); - - return ConvertFromDtos(dtos); - } - - /// - /// Gets paged user results - /// - /// - /// - /// - /// - /// - /// - /// - /// A filter to only include user that belong to these user groups - /// - /// - /// A filter to only include users that do not belong to these user groups - /// - /// Optional parameter to filter by specified user state - /// - /// - /// - /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using NPoco paging (SQL paging) - /// - public IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, - Expression> orderBy, Direction orderDirection = Direction.Ascending, - string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, IQuery? filter = null) - { - if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); - - Sql? filterSql = null; - var customFilterWheres = filter?.GetWhereClauses().ToArray(); - var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; - if (hasCustomFilter - || includeUserGroups != null && includeUserGroups.Length > 0 - || excludeUserGroups != null && excludeUserGroups.Length > 0 - || userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false) - filterSql = SqlContext.Sql(); - - if (hasCustomFilter) + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { - foreach (var clause in customFilterWheres!) - filterSql?.Append($"AND ({clause.Item1})", clause.Item2); + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); } - if (includeUserGroups != null && includeUserGroups.Length > 0) + // check if we have a user config else use the default + userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); + } + + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + changedCols.Add("emailConfirmedDate"); + + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { - const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id - FROM umbracoUser - INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id - INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId - WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; - filterSql?.Append(subQuery, new { userGroups = includeUserGroups }); + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); + } + } + + //only update the changed cols + if (changedCols.Count > 0) + { + Database.Update(userDto, changedCols); + } + + if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) + { + List? assignedStartNodes = + Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", + new {userId = entity.Id}); + if (entity.IsPropertyDirty("StartContentIds")) + { + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, + entity.StartContentIds); } - if (excludeUserGroups != null && excludeUserGroups.Length > 0) + if (entity.IsPropertyDirty("StartMediaIds")) { - const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id - FROM umbracoUser - INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id - INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId - WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; - filterSql?.Append(subQuery, new { userGroups = excludeUserGroups }); + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, + entity.StartMediaIds); } + } - if (userState != null && userState.Length > 0) + if (entity.IsPropertyDirty("Groups")) + { + //lookup all assigned + List? assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", + new {aliases = entity.Groups.Select(x => x.Alias)}); + + //first delete all + // TODO: We could do this a nicer way instead of "Nuke and Pave" + Database.Delete("WHERE UserId = @UserId", new {UserId = entity.Id}); + + foreach (UserGroupDto? groupDto in assigned) { - //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering - if (userState.Contains(UserState.All) == false) - { - var sb = new StringBuilder("("); - var appended = false; - - if (userState.Contains(UserState.Active)) - { - sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); - appended = true; - } - if (userState.Contains(UserState.Inactive)) - { - if (appended) sb.Append(" OR "); - sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); - appended = true; - } - if (userState.Contains(UserState.Disabled)) - { - if (appended) sb.Append(" OR "); - sb.Append("(userDisabled = 1)"); - appended = true; - } - if (userState.Contains(UserState.LockedOut)) - { - if (appended) sb.Append(" OR "); - sb.Append("(userNoConsole = 1)"); - appended = true; - } - if (userState.Contains(UserState.Invited)) - { - if (appended) sb.Append(" OR "); - sb.Append("(lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); - appended = true; - } - - sb.Append(")"); - filterSql?.Append("AND " + sb); - } + var dto = new User2UserGroupDto {UserGroupId = groupDto.Id, UserId = entity.Id}; + Database.Insert(dto); } - - // create base query - var sql = SqlContext.Sql() - .Select() - .From(); - - // apply query - if (query != null) - sql = new SqlTranslator(sql, query).Translate(); - - // get sorted and filtered sql - var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection); - - // get a page of results and total count - var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); - - // map references - PerformGetReferencedDtos(pagedResult.Items); - return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); } - private Sql ApplyFilter(Sql sql, Sql? filterSql, bool hasWhereClause) + entity.ResetDirtyProperties(); + } + + private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, + UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds) + { + if (entityStartIds is null) { - if (filterSql == null) return sql; - - //ensure we don't append a WHERE if there is already one - var args = filterSql.Arguments; - var sqlFilter = hasWhereClause - ? filterSql.SQL - : " WHERE " + filterSql.SQL.TrimStart("AND "); - - sql.Append(SqlContext.Sql(sqlFilter, args)); - - return sql; + return; } - private Sql ApplySort(Sql sql, Expression> orderBy, Direction orderDirection) + var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); + + //remove the ones not assigned to the entity + var toDelete = assignedIds.Except(entityStartIds).ToArray(); + if (toDelete.Length > 0) { - if (orderBy == null) return sql; - - var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); - var mapper = _mapperCollection[typeof(IUser)]; - var mappedField = mapper.Map(expressionMember?.Name); - - if (mappedField.IsNullOrWhiteSpace()) - throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); - - // beware! NPoco paging code parses the query to isolate the ORDER BY fragment, - // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything - // else in orderBy is going to break NPoco / not be detected - - // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar] - // to [bar] only, so we MUST use aliases, cannot use [table].[field] - - // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which - // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...", - // so anything added here MUST also be part of the inner SELECT statement, ie - // the original statement, AND must be using the proper alias, as the inner SELECT - // will hide the original table.field names entirely - - var orderByField = sql.GetAliasedField(mappedField); - - if (orderDirection == Direction.Ascending) - sql.OrderBy(orderByField); - else - sql.OrderByDescending(orderByField); - - return sql; + Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", + new {UserId = entity.Id, startNodes = toDelete}); } - public IEnumerable GetNextUsers(int id, int count) + //add the ones not currently in the db + var toAdd = entityStartIds.Except(assignedIds).ToArray(); + foreach (var i in toAdd) { - var idsQuery = SqlContext.Sql() - .Select(x => x.Id) - .From() - .Where(x => x.Id >= id) - .OrderBy(x => x.Id); - - // first page is index 1, not zero - var ids = Database.Page(1, count, idsQuery).Items.ToArray(); - - // now get the actual users and ensure they are ordered properly (same clause) - return ids.Length == 0 ? Enumerable.Empty() : GetMany(ids)?.OrderBy(x => x.Id) ?? Enumerable.Empty(); - } - - #endregion - - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - return dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + var dto = new UserStartNodeDto {StartNode = i, StartNodeType = (int)startNodeType, UserId = entity.Id}; + Database.Insert(dto); } } + + #endregion + + #region Implementation of IUserRepository + + public int GetCountByQuery(IQuery? query) + { + Sql sqlClause = GetBaseQuery("umbracoUser.id"); + var translator = new SqlTranslator(sqlClause, query); + Sql subquery = translator.Translate(); + //get the COUNT base query + Sql? sql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoUser.id IN (" + subquery.SQL + ")", subquery.Arguments)); + + return Database.ExecuteScalar(sql); + } + + public bool Exists(string username) => ExistsByUserName(username); + + public bool ExistsByUserName(string username) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.UserName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public bool ExistsByLogin(string login) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.Login == login); + + return Database.ExecuteScalar(sql) > 0; + } + + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + public IEnumerable GetAllInGroup(int groupId) => GetAllInOrNotInGroup(groupId, true); + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + public IEnumerable GetAllNotInGroup(int groupId) => GetAllInOrNotInGroup(groupId, false); + + private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) + { + Sql sql = SqlContext.Sql() + .Select() + .From(); + + Sql inSql = SqlContext.Sql() + .Select(x => x.UserId) + .From() + .Where(x => x.UserGroupId == groupId); + + if (include) + { + sql.WhereIn(x => x.Id, inSql); + } + else + { + sql.WhereNotIn(x => x.Id, inSql); + } + + + List? dtos = Database.Fetch(sql); + + //adds missing bits like content and media start nodes + PerformGetReferencedDtos(dtos); + + return ConvertFromDtos(dtos); + } + + /// + /// Gets paged user results + /// + /// + /// + /// + /// + /// + /// + /// + /// A filter to only include user that belong to these user groups + /// + /// + /// A filter to only include users that do not belong to these user groups + /// + /// Optional parameter to filter by specified user state + /// + /// + /// + /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using NPoco paging + /// (SQL paging) + /// + public IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, + out long totalRecords, + Expression> orderBy, Direction orderDirection = Direction.Ascending, + string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, + IQuery? filter = null) + { + if (orderBy == null) + { + throw new ArgumentNullException(nameof(orderBy)); + } + + Sql? filterSql = null; + Tuple[]? customFilterWheres = filter?.GetWhereClauses().ToArray(); + var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; + if (hasCustomFilter + || (includeUserGroups != null && includeUserGroups.Length > 0) + || (excludeUserGroups != null && excludeUserGroups.Length > 0) + || (userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false)) + { + filterSql = SqlContext.Sql(); + } + + if (hasCustomFilter) + { + foreach (Tuple clause in customFilterWheres!) + { + filterSql?.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + if (includeUserGroups != null && includeUserGroups.Length > 0) + { + const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id + FROM umbracoUser + INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id + INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId + WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; + filterSql?.Append(subQuery, new {userGroups = includeUserGroups}); + } + + if (excludeUserGroups != null && excludeUserGroups.Length > 0) + { + const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id + FROM umbracoUser + INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id + INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId + WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; + filterSql?.Append(subQuery, new {userGroups = excludeUserGroups}); + } + + if (userState != null && userState.Length > 0) + { + //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering + if (userState.Contains(UserState.All) == false) + { + var sb = new StringBuilder("("); + var appended = false; + + if (userState.Contains(UserState.Active)) + { + sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); + appended = true; + } + + if (userState.Contains(UserState.Inactive)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); + appended = true; + } + + if (userState.Contains(UserState.Disabled)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(userDisabled = 1)"); + appended = true; + } + + if (userState.Contains(UserState.LockedOut)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(userNoConsole = 1)"); + appended = true; + } + + if (userState.Contains(UserState.Invited)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); + appended = true; + } + + sb.Append(")"); + filterSql?.Append("AND " + sb); + } + } + + // create base query + Sql sql = SqlContext.Sql() + .Select() + .From(); + + // apply query + if (query != null) + { + sql = new SqlTranslator(sql, query).Translate(); + } + + // get sorted and filtered sql + Sql sqlNodeIdsWithSort = + ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection); + + // get a page of results and total count + Page? pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + // map references + PerformGetReferencedDtos(pagedResult.Items); + return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + } + + private Sql ApplyFilter(Sql sql, Sql? filterSql, bool hasWhereClause) + { + if (filterSql == null) + { + return sql; + } + + //ensure we don't append a WHERE if there is already one + var args = filterSql.Arguments; + var sqlFilter = hasWhereClause + ? filterSql.SQL + : " WHERE " + filterSql.SQL.TrimStart("AND "); + + sql.Append(SqlContext.Sql(sqlFilter, args)); + + return sql; + } + + private Sql ApplySort(Sql sql, Expression>? orderBy, + Direction orderDirection) + { + if (orderBy == null) + { + return sql; + } + + MemberInfo? expressionMember = ExpressionHelper.GetMemberInfo(orderBy); + BaseMapper mapper = _mapperCollection[typeof(IUser)]; + var mappedField = mapper.Map(expressionMember?.Name); + + if (mappedField.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); + } + + // beware! NPoco paging code parses the query to isolate the ORDER BY fragment, + // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything + // else in orderBy is going to break NPoco / not be detected + + // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar] + // to [bar] only, so we MUST use aliases, cannot use [table].[field] + + // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which + // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...", + // so anything added here MUST also be part of the inner SELECT statement, ie + // the original statement, AND must be using the proper alias, as the inner SELECT + // will hide the original table.field names entirely + + var orderByField = sql.GetAliasedField(mappedField); + + if (orderDirection == Direction.Ascending) + { + sql.OrderBy(orderByField); + } + else + { + sql.OrderByDescending(orderByField); + } + + return sql; + } + + public IEnumerable GetNextUsers(int id, int count) + { + Sql idsQuery = SqlContext.Sql() + .Select(x => x.Id) + .From() + .Where(x => x.Id >= id) + .OrderBy(x => x.Id); + + // first page is index 1, not zero + var ids = Database.Page(1, count, idsQuery).Items.ToArray(); + + // now get the actual users and ensure they are ordered properly (same clause) + return ids.Length == 0 + ? Enumerable.Empty() + : GetMany(ids).OrderBy(x => x.Id) ?? Enumerable.Empty(); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs index 827aef1932..83c585dfd2 100644 --- a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs @@ -1,14 +1,12 @@ -using System; - namespace Umbraco.Cms.Infrastructure.Persistence; public abstract class ScalarMapper : IScalarMapper { - /// - /// Performs a strongly typed mapping operation for a scalar value. - /// - protected abstract T Map(object value); - /// object IScalarMapper.Map(object value) => Map(value)!; + + /// + /// Performs a strongly typed mapping operation for a scalar value. + /// + protected abstract T Map(object value); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs index 6eb903c1f5..57e82770c6 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs @@ -1,60 +1,55 @@ -using System; -using System.Linq; using NPoco; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCollection; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Implements . +/// +public class SqlContext : ISqlContext { /// - /// Implements . + /// Initializes a new instance of the class. /// - public class SqlContext : ISqlContext + /// The sql syntax provider. + /// The Poco data factory. + /// The database type. + /// The mappers. + public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection? mappers = null) { - /// - /// Initializes a new instance of the class. - /// - /// The sql syntax provider. - /// The Poco data factory. - /// The database type. - /// The mappers. - public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection? mappers = null) - { - // for tests - Mappers = mappers; + // for tests + Mappers = mappers; - SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); - PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); - DatabaseType = databaseType ?? throw new ArgumentNullException(nameof(databaseType)); - Templates = new SqlTemplates(this); - } - - /// - public ISqlSyntaxProvider SqlSyntax { get; } - - /// - public DatabaseType DatabaseType { get; } - - /// - public Sql Sql() => NPoco.Sql.BuilderFor((ISqlContext) this); - - /// - public Sql Sql(string sql, params object[] args) => Sql().Append(sql, args); - - /// - public IQuery Query() => new Query(this); - - /// - public SqlTemplates Templates { get; } - - /// - public IPocoDataFactory PocoDataFactory { get; } - - /// - public IMapperCollection? Mappers { get; } + SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); + PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); + DatabaseType = databaseType ?? throw new ArgumentNullException(nameof(databaseType)); + Templates = new SqlTemplates(this); } + + /// + public ISqlSyntaxProvider SqlSyntax { get; } + + /// + public DatabaseType DatabaseType { get; } + + /// + public SqlTemplates Templates { get; } + + /// + public IPocoDataFactory PocoDataFactory { get; } + + /// + public IMapperCollection? Mappers { get; } + + /// + public Sql Sql() => NPoco.Sql.BuilderFor((ISqlContext)this); + + /// + public Sql Sql(string sql, params object[] args) => Sql().Append(sql, args); + + /// + public IQuery Query() => new Query(this); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs index b929b0e7ec..e46963d738 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs @@ -1,110 +1,110 @@ -using System; using System.Linq.Expressions; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Querying; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class SqlContextExtensions { /// - /// Provides extension methods to . + /// Visit an expression. /// - public static class SqlContextExtensions + /// The type of the DTO. + /// An . + /// An expression to visit. + /// An optional table alias. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) { - /// - /// Visit an expression. - /// - /// The type of the DTO. - /// An . - /// An expression to visit. - /// An optional table alias. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the DTO. - /// The type returned by the expression. - /// An . - /// An expression to visit. - /// An optional table alias. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the DTO. + /// The type returned by the expression. + /// An . + /// An expression to visit. + /// An optional table alias. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the first DTO. - /// The type of the second DTO. - /// An . - /// An expression to visit. - /// An optional table alias for the first DTO. - /// An optional table alias for the second DTO. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the first DTO. + /// The type of the second DTO. + /// An . + /// An expression to visit. + /// An optional table alias for the first DTO. + /// An optional table alias for the second DTO. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the first DTO. - /// The type of the second DTO. - /// The type returned by the expression. - /// An . - /// An expression to visit. - /// An optional table alias for the first DTO. - /// An optional table alias for the second DTO. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the first DTO. + /// The type of the second DTO. + /// The type returned by the expression. + /// An . + /// An expression to visit. + /// An optional table alias for the first DTO. + /// An optional table alias for the second DTO. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit a model expression. - /// - /// The type of the model. - /// An . - /// An expression to visit. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitModel(this ISqlContext sqlContext, Expression> expression) - { - var visitor = new ModelToSqlExpressionVisitor(sqlContext.SqlSyntax, sqlContext.Mappers); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit a model expression. + /// + /// The type of the model. + /// An . + /// An expression to visit. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitModel( + this ISqlContext sqlContext, + Expression> expression) + { + var visitor = new ModelToSqlExpressionVisitor(sqlContext.SqlSyntax, sqlContext.Mappers); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit a model expression representing a field. - /// - /// The type of the model. - /// An . - /// An expression to visit, representing a field. - /// The name of the field. - public static string VisitModelField(this ISqlContext sqlContext, Expression> field) - { - var (sql, _) = sqlContext.VisitModel(field); + /// + /// Visit a model expression representing a field. + /// + /// The type of the model. + /// An . + /// An expression to visit, representing a field. + /// The name of the field. + public static string VisitModelField(this ISqlContext sqlContext, Expression> field) + { + (string sql, object[] _) = sqlContext.VisitModel(field); - // going to return " = @0" - // take the first part only - var pos = sql.IndexOf(' '); - return sql.Substring(0, pos); - } + // going to return " = @0" + // take the first part only + var pos = sql.IndexOf(' '); + return sql.Substring(0, pos); } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs index fed6e221b5..26aea8965f 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs @@ -1,40 +1,44 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +public class ColumnInfo { - public class ColumnInfo + public ColumnInfo(string tableName, string columnName, int ordinal, string columnDefault, string isNullable, string dataType) { - public ColumnInfo(string tableName, string columnName, int ordinal, string columnDefault, string isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - ColumnDefault = columnDefault; - IsNullable = isNullable.Equals("YES"); - DataType = dataType; - } - - public ColumnInfo(string tableName, string columnName, int ordinal, string isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - IsNullable = isNullable.Equals("YES"); - DataType = dataType; - } - - public ColumnInfo(string tableName, string columnName, int ordinal, bool isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - IsNullable = isNullable; - DataType = dataType; - } - - public string TableName { get; set; } - public string ColumnName { get; set; } - public int Ordinal { get; set; } - public string? ColumnDefault { get; set; } - public bool IsNullable { get; set; } - public string DataType { get; set; } + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + ColumnDefault = columnDefault; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; } + + public ColumnInfo(string tableName, string columnName, int ordinal, string isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; + } + + public ColumnInfo(string tableName, string columnName, int ordinal, bool isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable; + DataType = dataType; + } + + public string TableName { get; set; } + + public string ColumnName { get; set; } + + public int Ordinal { get; set; } + + public string? ColumnDefault { get; set; } + + public bool IsNullable { get; set; } + + public string DataType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs index 18e4791d0b..498b88af61 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax -{ - public class DbTypes - { - public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) - { - ColumnTypeMap = columnTypeMap; - ColumnDbTypeMap = columnDbTypeMap; - } +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; - public IReadOnlyDictionary ColumnTypeMap { get; } - public IReadOnlyDictionary ColumnDbTypeMap { get; } +public class DbTypes +{ + public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) + { + ColumnTypeMap = columnTypeMap; + ColumnDbTypeMap = columnDbTypeMap; } + + public IReadOnlyDictionary ColumnTypeMap { get; } + + public IReadOnlyDictionary ColumnDbTypeMap { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs index bf1e0989f5..343e42d9c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs @@ -1,20 +1,17 @@ -using System; -using System.Collections.Generic; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +internal class DbTypesFactory { - internal class DbTypesFactory + private readonly Dictionary _columnDbTypeMap = new(); + private readonly Dictionary _columnTypeMap = new(); + + public void Set(DbType dbType, string fieldDefinition) { - private readonly Dictionary _columnTypeMap = new Dictionary(); - private readonly Dictionary _columnDbTypeMap = new Dictionary(); - - public void Set(DbType dbType, string fieldDefinition) - { - _columnTypeMap[typeof(T)] = fieldDefinition; - _columnDbTypeMap[typeof(T)] = dbType; - } - - public DbTypes Create() => new DbTypes(_columnTypeMap, _columnDbTypeMap); + _columnTypeMap[typeof(T)] = fieldDefinition; + _columnDbTypeMap[typeof(T)] = dbType; } + + public DbTypes Create() => new(_columnTypeMap, _columnDbTypeMap); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 7760f86476..d9b76a4942 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,166 +1,210 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq.Expressions; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using System.Text.RegularExpressions; using NPoco; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Defines an SqlSyntaxProvider +/// +public interface ISqlSyntaxProvider { + string ProviderName { get; } + + string CreateTable { get; } + + string DropTable { get; } + + string AddColumn { get; } + + string DropColumn { get; } + + string AlterColumn { get; } + + string RenameColumn { get; } + + string RenameTable { get; } + + string CreateSchema { get; } + + string AlterSchema { get; } + + string DropSchema { get; } + + string CreateIndex { get; } + + string DropIndex { get; } + + string InsertData { get; } + + string UpdateData { get; } + + string DeleteData { get; } + + string TruncateTable { get; } + + string CreateConstraint { get; } + + string DeleteConstraint { get; } + + string DeleteDefaultConstraint { get; } + /// - /// Defines an SqlSyntaxProvider + /// Gets a regex matching aliased fields. /// - public interface ISqlSyntaxProvider - { - DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => - current; // Default implementation. + /// + /// Matches "(table.column) AS (alias)" where table, column and alias are properly escaped. + /// + Regex AliasRegex { get; } - string ProviderName { get; } + string ConvertIntegerToOrderableString { get; } - string EscapeString(string val); + string ConvertDateToOrderableString { get; } - string GetWildcardPlaceholder(); - string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); - string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); - string GetConcat(params string[] args); + string ConvertDecimalToOrderableString { get; } - string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false); + /// + /// Returns the default isolation level for the database + /// + IsolationLevel DefaultIsolationLevel { get; } - string GetQuotedTableName(string? tableName); - string GetQuotedColumnName(string? columnName); - string GetQuotedName(string? name); - bool DoesTableExist(IDatabase db, string tableName); - string GetIndexType(IndexTypes indexTypes); - string GetSpecialDbType(SpecialDbType dbType); - string CreateTable { get; } - string DropTable { get; } - string AddColumn { get; } - string DropColumn { get; } - string AlterColumn { get; } - string RenameColumn { get; } - string RenameTable { get; } - string CreateSchema { get; } - string AlterSchema { get; } - string DropSchema { get; } - string CreateIndex { get; } - string DropIndex { get; } - string InsertData { get; } - string UpdateData { get; } - string DeleteData { get; } - string TruncateTable { get; } - string CreateConstraint { get; } - string DeleteConstraint { get; } + string DbProvider { get; } - string DeleteDefaultConstraint { get; } - string FormatDateTime(DateTime date, bool includeTime = true); - string Format(TableDefinition table); - string Format(IEnumerable columns); - List Format(IEnumerable indexes); - List Format(IEnumerable foreignKeys); - string FormatPrimaryKey(TableDefinition table); - string GetQuotedValue(string value); - string Format(ColumnDefinition column); - string Format(ColumnDefinition column, string tableName, out IEnumerable sqls); - string Format(IndexDefinition index); - string Format(ForeignKeyDefinition foreignKey); - string FormatColumnRename(string? tableName, string? oldName, string? newName); - string FormatTableRename(string? oldName, string? newName); + IDictionary? ScalarMappers => null; - void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => + current; // Default implementation. - /// - /// Gets a regex matching aliased fields. - /// - /// - /// Matches "(table.column) AS (alias)" where table, column and alias are properly escaped. - /// - Regex AliasRegex { get; } + string EscapeString(string val); - Sql SelectTop(Sql sql, int top); + string GetWildcardPlaceholder(); - bool SupportsClustered(); - bool SupportsIdentityInsert(); + string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); - string ConvertIntegerToOrderableString { get; } - string ConvertDateToOrderableString { get; } - string ConvertDecimalToOrderableString { get; } + string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); - /// - /// Returns the default isolation level for the database - /// - IsolationLevel DefaultIsolationLevel { get; } + string GetConcat(params string[] args); - string DbProvider { get; } - IEnumerable GetTablesInSchema(IDatabase db); - IEnumerable GetColumnsInSchema(IDatabase db); + string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false); - /// - /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes) - /// - /// - /// - /// A Tuple containing: TableName, ConstraintName - /// - IEnumerable> GetConstraintsPerTable(IDatabase db); + string GetQuotedTableName(string? tableName); - /// - /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes) - /// - /// - /// - /// A Tuple containing: TableName, ColumnName, ConstraintName - /// - IEnumerable> GetConstraintsPerColumn(IDatabase db); + string GetQuotedColumnName(string? columnName); - /// - /// Returns all defined Indexes in the database excluding primary keys - /// - /// - /// - /// A Tuple containing: TableName, IndexName, ColumnName, IsUnique - /// - IEnumerable> GetDefinedIndexes(IDatabase db); + string GetQuotedName(string? name); - /// - /// Tries to gets the name of the default constraint on a column. - /// - /// The database. - /// The table name. - /// The column name. - /// The constraint name. - /// A value indicating whether a default constraint was found. - /// - /// Some database engines may not have names for default constraints, - /// in which case the function may return true, but is - /// unspecified. - /// - bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + bool DoesTableExist(IDatabase db, string tableName); + string GetIndexType(IndexTypes indexTypes); - string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null); + string GetSpecialDbType(SpecialDbType dbType); - /// - /// Appends the relevant ForUpdate hint. - /// - Sql InsertForUpdateHint(Sql sql); + string FormatDateTime(DateTime date, bool includeTime = true); - /// - /// Appends the relevant ForUpdate hint. - /// - Sql AppendForUpdateHint(Sql sql); + string Format(TableDefinition table); - /// - /// Handles left join with nested join - /// - Sql.SqlJoinClause LeftJoinWithNestedJoin( - Sql sql, - Func, Sql> nestedJoin, - string? alias = null); + string Format(IEnumerable columns); - IDictionary? ScalarMappers => null; - } + List Format(IEnumerable indexes); + + List Format(IEnumerable foreignKeys); + + string FormatPrimaryKey(TableDefinition table); + + string GetQuotedValue(string value); + + string Format(ColumnDefinition column); + + string Format(ColumnDefinition column, string tableName, out IEnumerable sqls); + + string Format(IndexDefinition index); + + string Format(ForeignKeyDefinition foreignKey); + + string FormatColumnRename(string? tableName, string? oldName, string? newName); + + string FormatTableRename(string? oldName, string? newName); + + void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + + Sql SelectTop(Sql sql, int top); + + bool SupportsClustered(); + + bool SupportsIdentityInsert(); + + IEnumerable GetTablesInSchema(IDatabase db); + + IEnumerable GetColumnsInSchema(IDatabase db); + + /// + /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not + /// include indexes) + /// + /// + /// + /// A Tuple containing: TableName, ConstraintName + /// + IEnumerable> GetConstraintsPerTable(IDatabase db); + + /// + /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not + /// include indexes) + /// + /// + /// + /// A Tuple containing: TableName, ColumnName, ConstraintName + /// + IEnumerable> GetConstraintsPerColumn(IDatabase db); + + /// + /// Returns all defined Indexes in the database excluding primary keys + /// + /// + /// + /// A Tuple containing: TableName, IndexName, ColumnName, IsUnique + /// + IEnumerable> GetDefinedIndexes(IDatabase db); + + /// + /// Tries to gets the name of the default constraint on a column. + /// + /// The database. + /// The table name. + /// The column name. + /// The constraint name. + /// A value indicating whether a default constraint was found. + /// + /// + /// Some database engines may not have names for default constraints, + /// in which case the function may return true, but is + /// unspecified. + /// + /// + bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + + string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql InsertForUpdateHint(Sql sql); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql AppendForUpdateHint(Sql sql); + + /// + /// Handles left join with nested join + /// + Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, Sql> nestedJoin, + string? alias = null); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs index a3efde1731..79d00e8d96 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs @@ -1,21 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) +/// +/// +/// see: https://support.microsoft.com/en-us/kb/321185 +/// +internal enum SqlServerVersionName { - /// - /// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) - /// - /// - /// see: https://support.microsoft.com/en-us/kb/321185 - /// - internal enum SqlServerVersionName - { - Invalid = -1, - V7 = 0, - V2000 = 1, - V2005 = 2, - V2008 = 3, - V2012 = 4, - V2014 = 5, - V2016 = 6, - Other = 100 - } + Invalid = -1, + V7 = 0, + V2000 = 1, + V2005 = 2, + V2008 = 3, + V2012 = 4, + V2014 = 5, + V2016 = 6, + Other = 100, } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index b2bbd4ac5b..48b882d604 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -1,9 +1,8 @@ +// Don't remove the unused System using, for some reason this breaks docfx, and I have no clue why. using System; -using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Text; using System.Text.RegularExpressions; @@ -12,603 +11,612 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Represents the Base Sql Syntax provider implementation. +/// +/// +/// All Sql Syntax provider implementations should derive from this abstract class. +/// +/// +public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider + where TSyntax : ISqlSyntaxProvider { - /// - /// Represents the Base Sql Syntax provider implementation. - /// - /// - /// All Sql Syntax provider implementations should derive from this abstract class. - /// - /// - public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider - where TSyntax : ISqlSyntaxProvider + private readonly Lazy _dbTypes; + + protected SqlSyntaxProviderBase() { - private readonly Lazy _dbTypes; - - protected SqlSyntaxProviderBase() + ClauseOrder = new List> { - ClauseOrder = new List> - { - FormatString, - FormatType, - FormatNullable, - FormatConstraint, - FormatDefaultValue, - FormatPrimaryKey, - FormatIdentity - }; + FormatString, + FormatType, + FormatNullable, + FormatConstraint, + FormatDefaultValue, + FormatPrimaryKey, + FormatIdentity + }; - //defaults for all providers - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); - DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); + //defaults for all providers + StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; + StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); + DecimalColumnDefinition = + string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); - // ReSharper disable VirtualMemberCallInConstructor - // ok to call virtual GetQuotedXxxName here - they don't depend on any state - var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); - var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; - // ReSharper restore VirtualMemberCallInConstructor - AliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + // ReSharper disable VirtualMemberCallInConstructor + // ok to call virtual GetQuotedXxxName here - they don't depend on any state + var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); + var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; + // ReSharper restore VirtualMemberCallInConstructor + AliasRegex = new Regex( + "(" + fld + @")\s+AS\s+(" + col + ")", + RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); - _dbTypes = new Lazy(InitColumnTypeMap); + _dbTypes = new Lazy(InitColumnTypeMap); + } + + public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; + + public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; + + public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; + + public string DefaultValueFormat { get; } = "DEFAULT ({0})"; + + public int DefaultStringLength { get; } = 255; + + public int DefaultDecimalPrecision { get; } = 20; + + public int DefaultDecimalScale { get; } = 9; + + //Set by Constructor + public virtual string StringColumnDefinition { get; } + + public string StringLengthColumnDefinitionFormat { get; } + + public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; + + public string IntColumnDefinition { get; protected set; } = "INTEGER"; + + public string LongColumnDefinition { get; protected set; } = "BIGINT"; + + public string GuidColumnDefinition { get; protected set; } = "GUID"; + + public string BoolColumnDefinition { get; protected set; } = "BOOL"; + + public string RealColumnDefinition { get; protected set; } = "DOUBLE"; + + public string DecimalColumnDefinition { get; protected set; } + + public string BlobColumnDefinition { get; protected set; } = "BLOB"; + + public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; + + public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; + + public string TimeColumnDefinition { get; protected set; } = "DATETIME"; + + protected IList> ClauseOrder { get; } + + protected DbTypes DbTypeMap => _dbTypes.Value; + + public virtual string CreateForeignKeyConstraint => + "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; + + public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; + + public Regex AliasRegex { get; } + + public string GetWildcardPlaceholder() => "%"; + + public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => current; + + public abstract string ProviderName { get; } + + public virtual string EscapeString(string val) => NPocoDatabaseExtensions.EscapeAtSymbols(val.Replace("'", "''")); + + public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) => + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + $"upper({column}) = upper(@{paramIndex})"; + + public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) => + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + $"upper({column}) LIKE upper(@{paramIndex})"; + + public virtual string GetConcat(params string[] args) => "concat(" + string.Join(",", args) + ")"; + + public virtual string GetQuotedTableName(string? tableName) => $"\"{tableName}\""; + + public virtual string GetQuotedColumnName(string? columnName) => $"\"{columnName}\""; + + public virtual string GetQuotedName(string? name) => $"\"{name}\""; + + public virtual string GetQuotedValue(string value) => $"'{value}'"; + + public virtual string GetIndexType(IndexTypes indexTypes) + { + string indexType; + + if (indexTypes == IndexTypes.Clustered) + { + indexType = "CLUSTERED"; + } + else + { + indexType = indexTypes == IndexTypes.NonClustered + ? "NONCLUSTERED" + : "UNIQUE NONCLUSTERED"; } - public Regex AliasRegex { get; } + return indexType; + } - public string GetWildcardPlaceholder() => "%"; - - public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; - public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; - public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; - - public string DefaultValueFormat { get; } = "DEFAULT ({0})"; - public int DefaultStringLength { get; } = 255; - public int DefaultDecimalPrecision { get; } = 20; - public int DefaultDecimalScale { get; } = 9; - - //Set by Constructor - public virtual string StringColumnDefinition { get; } - public string StringLengthColumnDefinitionFormat { get; } - - public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; - public string IntColumnDefinition { get; protected set; } = "INTEGER"; - public string LongColumnDefinition { get; protected set; } = "BIGINT"; - public string GuidColumnDefinition { get; protected set; } = "GUID"; - public string BoolColumnDefinition { get; protected set; } = "BOOL"; - public string RealColumnDefinition { get; protected set; } = "DOUBLE"; - public string DecimalColumnDefinition { get; protected set; } - public string BlobColumnDefinition { get; protected set; } = "BLOB"; - public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; - public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; - public string TimeColumnDefinition { get; protected set; } = "DATETIME"; - - protected IList> ClauseOrder { get; } - - protected DbTypes DbTypeMap => _dbTypes.Value; - - private DbTypes InitColumnTypeMap() + public virtual string GetSpecialDbType(SpecialDbType dbType) + { + if (dbType == SpecialDbType.NCHAR) { - var dbTypeMap = new DbTypesFactory(); - dbTypeMap.Set(DbType.String, StringColumnDefinition); - dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - dbTypeMap.Set(DbType.String, StringColumnDefinition); - dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - dbTypeMap.Set(DbType.Time, TimeColumnDefinition); - dbTypeMap.Set(DbType.Time, TimeColumnDefinition); - dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - - dbTypeMap.Set(DbType.Byte, IntColumnDefinition); - dbTypeMap.Set(DbType.Byte, IntColumnDefinition); - dbTypeMap.Set(DbType.SByte, IntColumnDefinition); - dbTypeMap.Set(DbType.SByte, IntColumnDefinition); - dbTypeMap.Set(DbType.Int16, IntColumnDefinition); - dbTypeMap.Set(DbType.Int16, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - dbTypeMap.Set(DbType.Int32, IntColumnDefinition); - dbTypeMap.Set(DbType.Int32, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - - dbTypeMap.Set(DbType.Int64, LongColumnDefinition); - dbTypeMap.Set(DbType.Int64, LongColumnDefinition); - dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - - dbTypeMap.Set(DbType.Single, RealColumnDefinition); - dbTypeMap.Set(DbType.Single, RealColumnDefinition); - dbTypeMap.Set(DbType.Double, RealColumnDefinition); - dbTypeMap.Set(DbType.Double, RealColumnDefinition); - - dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - - dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); - - return dbTypeMap.Create(); + return SpecialDbType.NCHAR; } - public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => current; - - public abstract string ProviderName { get; } - - public virtual string EscapeString(string val) + if (dbType == SpecialDbType.NTEXT) { - return NPocoDatabaseExtensions.EscapeAtSymbols(val.Replace("'", "''")); + return SpecialDbType.NTEXT; } - public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + if (dbType == SpecialDbType.NVARCHARMAX) { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return $"upper({column}) = upper(@{paramIndex})"; + return "NVARCHAR(MAX)"; } - public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + return "NVARCHAR"; + } + + public virtual string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false) + { + tableName = GetQuotedTableName(tableName); + columnName = GetQuotedColumnName(columnName); + var column = tableName + "." + columnName; + if (columnAlias == null) { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return $"upper({column}) LIKE upper(@{paramIndex})"; - } - - public virtual string GetConcat(params string[] args) - { - return "concat(" + string.Join(",", args) + ")"; - } - - public virtual string GetQuotedTableName(string? tableName) - { - return $"\"{tableName}\""; - } - - public virtual string GetQuotedColumnName(string? columnName) - { - return $"\"{columnName}\""; - } - - public virtual string GetQuotedName(string? name) - { - return $"\"{name}\""; - } - - public virtual string GetQuotedValue(string value) - { - return $"'{value}'"; - } - - public virtual string GetIndexType(IndexTypes indexTypes) - { - string indexType; - - if (indexTypes == IndexTypes.Clustered) - { - indexType = "CLUSTERED"; - } - else - { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; - } - return indexType; - } - - public virtual string GetSpecialDbType(SpecialDbType dbType) - { - if (dbType == SpecialDbType.NCHAR) - { - return SpecialDbType.NCHAR; - } - else if (dbType == SpecialDbType.NTEXT) - { - return SpecialDbType.NTEXT; - } - else if (dbType == SpecialDbType.NVARCHARMAX) - { - return "NVARCHAR(MAX)"; - } - - return "NVARCHAR"; - } - - public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => $"{GetSpecialDbType(dbType)}({customSize})"; - - public virtual string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false) - { - tableName = GetQuotedTableName(tableName); - columnName = GetQuotedColumnName(columnName); - var column = tableName + "." + columnName; - if (columnAlias == null) return column; - - referenceName = referenceName == null ? string.Empty : referenceName + "__"; - columnAlias = GetQuotedColumnName(referenceName + columnAlias); - column += " AS " + columnAlias; return column; } + referenceName = referenceName == null ? string.Empty : referenceName + "__"; + columnAlias = GetQuotedColumnName(referenceName + columnAlias); + column += " AS " + columnAlias; + return column; + } - public abstract IsolationLevel DefaultIsolationLevel { get; } - public abstract string DbProvider { get; } - public virtual IEnumerable GetTablesInSchema(IDatabase db) + public abstract IsolationLevel DefaultIsolationLevel { get; } + + public abstract string DbProvider { get; } + + public virtual IEnumerable GetTablesInSchema(IDatabase db) => new List(); + + public virtual IEnumerable GetColumnsInSchema(IDatabase db) => new List(); + + public virtual IEnumerable> GetConstraintsPerTable(IDatabase db) => + new List>(); + + public virtual IEnumerable> GetConstraintsPerColumn(IDatabase db) => + new List>(); + + public abstract IEnumerable> GetDefinedIndexes(IDatabase db); + + public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + + public virtual string GetFieldNameForUpdate( + Expression> fieldSelector, + string? tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); + + public virtual Sql InsertForUpdateHint(Sql sql) => sql; + + public virtual Sql AppendForUpdateHint(Sql sql) => sql; + + public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, Sql> nestedJoin, + string? alias = null); + + public virtual IDictionary? ScalarMappers => null; + + public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); + + public virtual bool SupportsClustered() => true; + + public virtual bool SupportsIdentityInsert() => true; + + /// + /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) + /// + /// + /// + /// + /// + /// MSSQL has a DateTime standard that is unambiguous and works on all servers: + /// YYYYMMDD HH:mm:ss + /// + public virtual string FormatDateTime(DateTime date, bool includeTime = true) => + // need CultureInfo.InvariantCulture because ":" here is the "time separator" and + // may be converted to something else in different cultures (eg "." in DK). + date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); + + public virtual string Format(TableDefinition table) + { + var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); + + return statement; + } + + public virtual List Format(IEnumerable indexes) => indexes.Select(Format).ToList(); + + public virtual string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; + + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + return string.Format( + CreateIndex, + GetIndexType(index.IndexType), + " ", + GetQuotedName(name), + GetQuotedTableName(index.TableName), + columns); + } + + public virtual List Format(IEnumerable foreignKeys) => + foreignKeys.Select(Format).ToList(); + + public virtual string Format(ForeignKeyDefinition foreignKey) + { + var constraintName = string.IsNullOrEmpty(foreignKey.Name) + ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" + : foreignKey.Name; + + return string.Format( + CreateForeignKeyConstraint, + GetQuotedTableName(foreignKey.ForeignTable), + GetQuotedName(constraintName), + GetQuotedColumnName(foreignKey.ForeignColumns.First()), + GetQuotedTableName(foreignKey.PrimaryTable), + GetQuotedColumnName(foreignKey.PrimaryColumns.First()), + FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("UPDATE", foreignKey.OnUpdate)); + } + + public virtual string Format(IEnumerable columns) + { + var sb = new StringBuilder(); + foreach (ColumnDefinition column in columns) { - return new List(); + sb.Append(Format(column) + ",\n"); } - public virtual IEnumerable GetColumnsInSchema(IDatabase db) - { - return new List(); - } + return sb.ToString().TrimEnd(",\n"); + } - public virtual IEnumerable> GetConstraintsPerTable(IDatabase db) - { - return new List>(); - } + public virtual string Format(ColumnDefinition column) => + string.Join(" ", ClauseOrder + .Select(action => action(column)) + .Where(clause => string.IsNullOrEmpty(clause) == false)); - public virtual IEnumerable> GetConstraintsPerColumn(IDatabase db) - { - return new List>(); - } + public virtual string Format(ColumnDefinition column, string tableName, out IEnumerable sqls) + { + var sql = new StringBuilder(); + sql.Append(FormatString(column)); + sql.Append(" "); + sql.Append(FormatType(column)); + sql.Append(" "); + sql.Append("NULL"); // always nullable + sql.Append(" "); + sql.Append(FormatConstraint(column)); + sql.Append(" "); + sql.Append(FormatDefaultValue(column)); + sql.Append(" "); + sql.Append(FormatPrimaryKey(column)); + sql.Append(" "); + sql.Append(FormatIdentity(column)); - public abstract IEnumerable> GetDefinedIndexes(IDatabase db); + //var isNullable = column.IsNullable; - public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + //var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); + //var hasConstraint = !string.IsNullOrWhiteSpace(constraint); - public virtual string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); + //var defaultValue = FormatDefaultValue(column); + //var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); - public virtual Sql InsertForUpdateHint(Sql sql) => sql; + // TODO: This used to exit if nullable but that means this would never work + // to return SQL if the column was nullable?!? I don't get it. This was here + // 4 years ago, I've removed it so that this works for nullable columns. + //if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) + //{ + // sqls = Enumerable.Empty(); + // return sql.ToString(); + //} - public virtual Sql AppendForUpdateHint(Sql sql) => sql; + var msql = new List(); + sqls = msql; - public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin(Sql sql, Func, Sql> nestedJoin, string? alias = null); + var alterSql = new StringBuilder(); + alterSql.Append(FormatString(column)); + alterSql.Append(" "); + alterSql.Append(FormatType(column)); + alterSql.Append(" "); + alterSql.Append(FormatNullable(column)); + //alterSql.Append(" "); + //alterSql.Append(FormatPrimaryKey(column)); + //alterSql.Append(" "); + //alterSql.Append(FormatIdentity(column)); + msql.Add(string.Format(AlterColumn, tableName, alterSql)); + //if (hasConstraint) + //{ + // var dropConstraintSql = string.Format(DeleteConstraint, tableName, constraint); + // msql.Add(dropConstraintSql); + // var constraintType = hasDefaultValue ? defaultValue : ""; + // var createConstraintSql = string.Format(CreateConstraint, tableName, constraint, constraintType, FormatString(column)); + // msql.Add(createConstraintSql); + //} - public virtual IDictionary? ScalarMappers => null; + return sql.ToString(); + } - public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); - - public virtual bool SupportsClustered() - { - return true; - } - - public virtual bool SupportsIdentityInsert() - { - return true; - } - - /// - /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) - /// - /// - /// - /// - /// - /// MSSQL has a DateTime standard that is unambiguous and works on all servers: - /// YYYYMMDD HH:mm:ss - /// - public virtual string FormatDateTime(DateTime date, bool includeTime = true) - { - // need CultureInfo.InvariantCulture because ":" here is the "time separator" and - // may be converted to something else in different cultures (eg "." in DK). - return date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); - } - - public virtual string Format(TableDefinition table) - { - var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); - - return statement; - } - - public virtual List Format(IEnumerable indexes) - { - return indexes.Select(Format).ToList(); - } - - public virtual string Format(IndexDefinition index) - { - var name = string.IsNullOrEmpty(index.Name) - ? $"IX_{index.TableName}_{index.ColumnName}" - : index.Name; - - var columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); - - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns); - } - - public virtual List Format(IEnumerable foreignKeys) - { - return foreignKeys.Select(Format).ToList(); - } - - public virtual string Format(ForeignKeyDefinition foreignKey) - { - var constraintName = string.IsNullOrEmpty(foreignKey.Name) - ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" - : foreignKey.Name; - - return string.Format(CreateForeignKeyConstraint, - GetQuotedTableName(foreignKey.ForeignTable), - GetQuotedName(constraintName), - GetQuotedColumnName(foreignKey.ForeignColumns.First()), - GetQuotedTableName(foreignKey.PrimaryTable), - GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), - FormatCascade("UPDATE", foreignKey.OnUpdate)); - } - - public virtual string Format(IEnumerable columns) - { - var sb = new StringBuilder(); - foreach (var column in columns) - { - sb.Append(Format(column) + ",\n"); - } - return sb.ToString().TrimEnd(",\n"); - } - - public virtual string Format(ColumnDefinition column) - { - return string.Join(" ", ClauseOrder - .Select(action => action(column)) - .Where(clause => string.IsNullOrEmpty(clause) == false)); - } - - public virtual string Format(ColumnDefinition column, string tableName, out IEnumerable sqls) - { - var sql = new StringBuilder(); - sql.Append(FormatString(column)); - sql.Append(" "); - sql.Append(FormatType(column)); - sql.Append(" "); - sql.Append("NULL"); // always nullable - sql.Append(" "); - sql.Append(FormatConstraint(column)); - sql.Append(" "); - sql.Append(FormatDefaultValue(column)); - sql.Append(" "); - sql.Append(FormatPrimaryKey(column)); - sql.Append(" "); - sql.Append(FormatIdentity(column)); - - //var isNullable = column.IsNullable; - - //var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); - //var hasConstraint = !string.IsNullOrWhiteSpace(constraint); - - //var defaultValue = FormatDefaultValue(column); - //var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); - - // TODO: This used to exit if nullable but that means this would never work - // to return SQL if the column was nullable?!? I don't get it. This was here - // 4 years ago, I've removed it so that this works for nullable columns. - //if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) - //{ - // sqls = Enumerable.Empty(); - // return sql.ToString(); - //} - - var msql = new List(); - sqls = msql; - - var alterSql = new StringBuilder(); - alterSql.Append(FormatString(column)); - alterSql.Append(" "); - alterSql.Append(FormatType(column)); - alterSql.Append(" "); - alterSql.Append(FormatNullable(column)); - //alterSql.Append(" "); - //alterSql.Append(FormatPrimaryKey(column)); - //alterSql.Append(" "); - //alterSql.Append(FormatIdentity(column)); - msql.Add(string.Format(AlterColumn, tableName, alterSql)); - - //if (hasConstraint) - //{ - // var dropConstraintSql = string.Format(DeleteConstraint, tableName, constraint); - // msql.Add(dropConstraintSql); - // var constraintType = hasDefaultValue ? defaultValue : ""; - // var createConstraintSql = string.Format(CreateConstraint, tableName, constraint, constraintType, FormatString(column)); - // msql.Add(createConstraintSql); - //} - - return sql.ToString(); - } - - public virtual string FormatPrimaryKey(TableDefinition table) - { - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition == null) - return string.Empty; - - var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? $"PK_{table.Name}" - : columnDefinition.PrimaryKeyName; - - var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); - - var primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - - return string.Format(CreateConstraint, - GetQuotedTableName(table.Name), - GetQuotedName(constraintName), - primaryKeyPart, - columns); - } - - public virtual string FormatColumnRename(string? tableName, string? oldName, string? newName) - { - return string.Format(RenameColumn, - GetQuotedTableName(tableName), - GetQuotedColumnName(oldName), - GetQuotedColumnName(newName)); - } - - public virtual string FormatTableRename(string? oldName, string? newName) - { - return string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); - } - - protected virtual string FormatCascade(string onWhat, Rule rule) - { - var action = "NO ACTION"; - switch (rule) - { - case Rule.None: - return ""; - case Rule.Cascade: - action = "CASCADE"; - break; - case Rule.SetNull: - action = "SET NULL"; - break; - case Rule.SetDefault: - action = "SET DEFAULT"; - break; - } - - return $" ON {onWhat} {action}"; - } - - protected virtual string FormatString(ColumnDefinition column) - { - return GetQuotedColumnName(column.Name); - } - - protected virtual string FormatType(ColumnDefinition column) - { - if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) - return column.CustomType; - - if (column.CustomDbType.HasValue) - { - if (column.Size != default) - { - return GetSpecialDbType(column.CustomDbType.Value, column.Size); - } - - return GetSpecialDbType(column.CustomDbType.Value); - } - - var type = column.Type.HasValue - ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key - : column.PropertyType; - - if (type == typeof(string)) - { - var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; - return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); - } - - if (type == typeof(decimal)) - { - var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; - var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; - return string.Format(DecimalColumnDefinitionFormat, precision, scale); - } - - var definition = DbTypeMap.ColumnTypeMap[type!]; - var dbTypeDefinition = column.Size != default - ? $"{definition}({column.Size})" - : definition; - //NOTE Precision is left out - return dbTypeDefinition; - } - - protected virtual string FormatNullable(ColumnDefinition column) - { - return column.IsNullable ? "NULL" : "NOT NULL"; - } - - protected virtual string FormatConstraint(ColumnDefinition column) - { - if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) - return string.Empty; - - return - $"CONSTRAINT {(string.IsNullOrEmpty(column.ConstraintName) ? GetQuotedName($"DF_{column.TableName}_{column.Name}") : column.ConstraintName)}"; - } - - protected virtual string FormatDefaultValue(ColumnDefinition column) - { - if (column.DefaultValue == null) - return string.Empty; - - // HACK: probably not needed with latest changes - if (string.Equals(column.DefaultValue.ToString(), "GETDATE()", StringComparison.OrdinalIgnoreCase)) - column.DefaultValue = SystemMethods.CurrentDateTime; - - // see if this is for a system method - if (column.DefaultValue is SystemMethods) - { - var method = FormatSystemMethods((SystemMethods)column.DefaultValue); - return string.IsNullOrEmpty(method) ? string.Empty : string.Format(DefaultValueFormat, method); - } - - return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString()!)); - } - - protected virtual string FormatPrimaryKey(ColumnDefinition column) + public virtual string FormatPrimaryKey(TableDefinition table) + { + ColumnDefinition? columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition == null) { return string.Empty; } - protected abstract string? FormatSystemMethods(SystemMethods systemMethod); + var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) + ? $"PK_{table.Name}" + : columnDefinition.PrimaryKeyName; - protected abstract string FormatIdentity(ColumnDefinition column); + var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); - public abstract Sql SelectTop(Sql sql, int top); + var primaryKeyPart = + string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - public abstract void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); - - public virtual string DeleteDefaultConstraint => throw new NotSupportedException("Default constraints are not supported"); - - public virtual string CreateTable => "CREATE TABLE {0} ({1})"; - public virtual string DropTable => "DROP TABLE {0}"; - - public virtual string AddColumn => "ALTER TABLE {0} ADD {1}"; - public virtual string DropColumn => "ALTER TABLE {0} DROP COLUMN {1}"; - public virtual string AlterColumn => "ALTER TABLE {0} ALTER COLUMN {1}"; - public virtual string RenameColumn => "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; - - public virtual string RenameTable => "RENAME TABLE {0} TO {1}"; - - public virtual string CreateSchema => "CREATE SCHEMA {0}"; - public virtual string AlterSchema => "ALTER SCHEMA {0} TRANSFER {1}.{2}"; - public virtual string DropSchema => "DROP SCHEMA {0}"; - - public virtual string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; - public virtual string DropIndex => "DROP INDEX {0}"; - - public virtual string InsertData => "INSERT INTO {0} ({1}) VALUES ({2})"; - public virtual string UpdateData => "UPDATE {0} SET {1} WHERE {2}"; - public virtual string DeleteData => "DELETE FROM {0} WHERE {1}"; - public virtual string TruncateTable => "TRUNCATE TABLE {0}"; - - public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; - public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; - public virtual string CreateForeignKeyConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; - public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; - - public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; - public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 120)"; - public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; + return string.Format( + CreateConstraint, + GetQuotedTableName(table.Name), + GetQuotedName(constraintName), + primaryKeyPart, + columns); } + + public virtual string FormatColumnRename(string? tableName, string? oldName, string? newName) => + string.Format( + RenameColumn, + GetQuotedTableName(tableName), + GetQuotedColumnName(oldName), + GetQuotedColumnName(newName)); + + public virtual string FormatTableRename(string? oldName, string? newName) => + string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); + + public abstract Sql SelectTop(Sql sql, int top); + + public abstract void HandleCreateTable( + IDatabase database, + TableDefinition tableDefinition, + bool skipKeysAndIndexes = false); + + public virtual string DeleteDefaultConstraint => + throw new NotSupportedException("Default constraints are not supported"); + + public virtual string CreateTable => "CREATE TABLE {0} ({1})"; + + public virtual string DropTable => "DROP TABLE {0}"; + + public virtual string AddColumn => "ALTER TABLE {0} ADD {1}"; + + public virtual string DropColumn => "ALTER TABLE {0} DROP COLUMN {1}"; + + public virtual string AlterColumn => "ALTER TABLE {0} ALTER COLUMN {1}"; + + public virtual string RenameColumn => "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; + + public virtual string RenameTable => "RENAME TABLE {0} TO {1}"; + + public virtual string CreateSchema => "CREATE SCHEMA {0}"; + + public virtual string AlterSchema => "ALTER SCHEMA {0} TRANSFER {1}.{2}"; + + public virtual string DropSchema => "DROP SCHEMA {0}"; + + public virtual string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; + + public virtual string DropIndex => "DROP INDEX {0}"; + + public virtual string InsertData => "INSERT INTO {0} ({1}) VALUES ({2})"; + + public virtual string UpdateData => "UPDATE {0} SET {1} WHERE {2}"; + + public virtual string DeleteData => "DELETE FROM {0} WHERE {1}"; + + public virtual string TruncateTable => "TRUNCATE TABLE {0}"; + + public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; + + public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; + + public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; + + public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 120)"; + + public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; + + private DbTypes InitColumnTypeMap() + { + var dbTypeMap = new DbTypesFactory(); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + + dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + + return dbTypeMap.Create(); + } + + public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => + $"{GetSpecialDbType(dbType)}({customSize})"; + + protected virtual string FormatCascade(string onWhat, Rule rule) + { + var action = "NO ACTION"; + switch (rule) + { + case Rule.None: + return string.Empty; + case Rule.Cascade: + action = "CASCADE"; + break; + case Rule.SetNull: + action = "SET NULL"; + break; + case Rule.SetDefault: + action = "SET DEFAULT"; + break; + } + + return $" ON {onWhat} {action}"; + } + + protected virtual string FormatString(ColumnDefinition column) => GetQuotedColumnName(column.Name); + + protected virtual string FormatType(ColumnDefinition column) + { + if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) + { + return column.CustomType; + } + + if (column.CustomDbType.HasValue) + { + if (column.Size != default) + { + return GetSpecialDbType(column.CustomDbType.Value, column.Size); + } + + return GetSpecialDbType(column.CustomDbType.Value); + } + + Type type = column.Type.HasValue + ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key + : column.PropertyType; + + if (type == typeof(string)) + { + var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; + return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); + } + + if (type == typeof(decimal)) + { + var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; + return string.Format(DecimalColumnDefinitionFormat, precision, scale); + } + + var definition = DbTypeMap.ColumnTypeMap[type]; + var dbTypeDefinition = column.Size != default + ? $"{definition}({column.Size})" + : definition; + //NOTE Precision is left out + return dbTypeDefinition; + } + + protected virtual string FormatNullable(ColumnDefinition column) => column.IsNullable ? "NULL" : "NOT NULL"; + + protected virtual string FormatConstraint(ColumnDefinition column) + { + if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) + { + return string.Empty; + } + + return + $"CONSTRAINT {(string.IsNullOrEmpty(column.ConstraintName) ? GetQuotedName($"DF_{column.TableName}_{column.Name}") : column.ConstraintName)}"; + } + + protected virtual string FormatDefaultValue(ColumnDefinition column) + { + if (column.DefaultValue == null) + { + return string.Empty; + } + + // HACK: probably not needed with latest changes + if (string.Equals(column.DefaultValue.ToString(), "GETDATE()", StringComparison.OrdinalIgnoreCase)) + { + column.DefaultValue = SystemMethods.CurrentDateTime; + } + + // see if this is for a system method + if (column.DefaultValue is SystemMethods) + { + var method = FormatSystemMethods((SystemMethods)column.DefaultValue); + return string.IsNullOrEmpty(method) ? string.Empty : string.Format(DefaultValueFormat, method); + } + + return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString()!)); + } + + protected virtual string FormatPrimaryKey(ColumnDefinition column) => string.Empty; + + protected abstract string? FormatSystemMethods(SystemMethods systemMethod); + + protected abstract string FormatIdentity(ColumnDefinition column); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 6c816f7c92..f1622c5480 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -1,57 +1,50 @@ -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +internal enum WhereInType { - internal static class SqlSyntaxProviderExtensions - { - public static IEnumerable GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db) - { - return sql.GetDefinedIndexes(db) - .Select(x => new DbIndexDefinition(x)).ToArray(); - } - - /// - /// Returns the quotes tableName.columnName combo - /// - /// - /// - /// - /// - public static string GetQuotedColumn(this ISqlSyntaxProvider sql, string tableName, string columnName) - { - return sql.GetQuotedTableName(tableName) + "." + sql.GetQuotedColumnName(columnName); - } - - /// - /// This is used to generate a delete query that uses a sub-query to select the data, it is required because there's a very particular syntax that - /// needs to be used to work for all servers - /// - /// - /// - /// See: http://issues.umbraco.org/issue/U4-3876 - /// - public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, Sql subQuery, WhereInType whereInType = WhereInType.In) - { - //TODO: This is no longer necessary since this used to be a specific requirement for MySql! - // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example - - return - new Sql(string.Format( - whereInType == WhereInType.In - ? @"DELETE FROM {0} WHERE {1} IN (SELECT {1} FROM ({2}) x)" - : @"DELETE FROM {0} WHERE {1} NOT IN (SELECT {1} FROM ({2}) x)", - sqlProvider.GetQuotedTableName(tableName), - sqlProvider.GetQuotedColumnName(columnName), - subQuery.SQL), subQuery.Arguments); - } - } - - internal enum WhereInType - { - In, - NotIn - } + In, + NotIn, +} + +internal static class SqlSyntaxProviderExtensions +{ + public static IEnumerable + GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db) => + sql.GetDefinedIndexes(db) + .Select(x => new DbIndexDefinition(x)).ToArray(); + + /// + /// Returns the quotes tableName.columnName combo + /// + /// + /// + /// + /// + public static string GetQuotedColumn(this ISqlSyntaxProvider sql, string tableName, string columnName) => + sql.GetQuotedTableName(tableName) + "." + sql.GetQuotedColumnName(columnName); + + /// + /// This is used to generate a delete query that uses a sub-query to select the data, it is required because there's a + /// very particular syntax that + /// needs to be used to work for all servers + /// + /// + /// + /// See: http://issues.umbraco.org/issue/U4-3876 + /// + public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, Sql subQuery, WhereInType whereInType = WhereInType.In) => + + // TODO: This is no longer necessary since this used to be a specific requirement for MySql! + // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example + new Sql( + string.Format( + whereInType == WhereInType.In + ? @"DELETE FROM {0} WHERE {1} IN (SELECT {1} FROM ({2}) x)" + : @"DELETE FROM {0} WHERE {1} NOT IN (SELECT {1} FROM ({2}) x)", + sqlProvider.GetQuotedTableName(tableName), + sqlProvider.GetQuotedColumnName(columnName), + subQuery.SQL), subQuery.Arguments); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs index fca90e9048..4ab8968389 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs @@ -1,40 +1,40 @@ -using System; using System.Linq.Expressions; using System.Reflection; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class SqlSyntaxExtensions { /// - /// Provides extension methods to . + /// Gets a quoted table and field name. /// - public static class SqlSyntaxExtensions + /// The type of the DTO. + /// An . + /// An expression specifying the field. + /// An optional table alias. + /// + public static string GetFieldName( + this ISqlSyntaxProvider sqlSyntax, + Expression> fieldSelector, string? tableAlias = null) { - private static string GetColumnName(this PropertyInfo column) - { - var attr = column.FirstAttribute(); - return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; - } + var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; + var fieldName = field?.GetColumnName(); - /// - /// Gets a quoted table and field name. - /// - /// The type of the DTO. - /// An . - /// An expression specifying the field. - /// An optional table alias. - /// - public static string GetFieldName(this ISqlSyntaxProvider sqlSyntax, Expression> fieldSelector, string? tableAlias = null) - { - var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; - var fieldName = field?.GetColumnName(); + Type type = typeof(TDto); + var tableName = tableAlias ?? type.GetTableName(); - var type = typeof(TDto); - var tableName = tableAlias ?? type.GetTableName(); + return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName); + } - return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName); - } + private static string GetColumnName(this PropertyInfo column) + { + ColumnAttribute? attr = column.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs b/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs index 716b24bb07..ae2fa58f02 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs @@ -1,124 +1,131 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public class SqlTemplate { - public class SqlTemplate + private readonly Dictionary? _args; + private readonly string _sql; + private readonly ISqlContext _sqlContext; + + internal SqlTemplate(ISqlContext sqlContext, string sql, object[] args) { - private readonly ISqlContext _sqlContext; - private readonly string _sql; - private readonly Dictionary? _args; - - // these are created in PocoToSqlExpressionVisitor - internal class TemplateArg + _sqlContext = sqlContext; + _sql = sql; + if (args.Length > 0) { - public TemplateArg(string? name) - { - Name = name; - } - - public string? Name { get; } - - public override string ToString() - { - return "@" + Name; - } + _args = new Dictionary(); } - internal SqlTemplate(ISqlContext sqlContext, string sql, object[] args) + for (var i = 0; i < args.Length; i++) { - _sqlContext = sqlContext; - _sql = sql; - if (args.Length > 0) - _args = new Dictionary(); - for (var i = 0; i < args.Length; i++) - _args![i] = args[i]; - } - - public Sql Sql() - { - return new Sql(_sqlContext, _sql); - } - - // must pass the args, all of them, in the proper order, faster - public Sql Sql(params object[] args) - { - // if the type is an "unspeakable name" it is an anonymous compiler-generated object - // see https://stackoverflow.com/questions/9256594 - // => assume it's an anonymous type object containing named arguments - // (of course this means we cannot use *real* objects here and need SqlNamed - bah) - if (args.Length == 1 && args[0].GetType().Name.Contains("<")) - return SqlNamed(args[0]); - - if (args.Length != _args?.Count) - throw new ArgumentException("Invalid number of arguments.", nameof(args)); - - if (args.Length == 0) - return new Sql(_sqlContext, true, _sql); - - var isBuilt = !args.Any(x => x is IEnumerable); - return new Sql(_sqlContext, isBuilt, _sql, args); - } - - // can pass named args, not necessary all of them, slower - // so, not much different from what Where(...) does (ie reflection) - public Sql SqlNamed(object nargs) - { - var isBuilt = true; - var args = new object[_args?.Count ?? 0]; - var properties = nargs.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(nargs)); - for (var i = 0; i < _args?.Count; i++) - { - object? value; - if (_args[i] is TemplateArg templateArg) - { - if (!properties.TryGetValue(templateArg.Name ?? string.Empty, out value)) - throw new InvalidOperationException($"Missing argument \"{templateArg.Name}\"."); - properties.Remove(templateArg.Name!); - } - else - { - value = _args[i]; - } - - args[i] = value!; - - // if value is enumerable then we'll need to expand arguments - if (value is IEnumerable) - isBuilt = false; - } - if (properties.Count > 0) - throw new InvalidOperationException($"Unknown argument{(properties.Count > 1 ? "s" : "")}: {string.Join(", ", properties.Keys)}"); - return new Sql(_sqlContext, isBuilt, _sql, args); - } - - internal string ToText() - { - var sql = new Sql(_sqlContext, _sql, _args?.Values.ToArray()); - return sql.ToText(); - } - - /// - /// Gets a named argument. - /// - public static object Arg(string name) => new TemplateArg(name); - - /// - /// Gets a WHERE expression argument. - /// - public static T? Arg(string name) => default; - - /// - /// Gets a WHERE IN expression argument. - /// - public static IEnumerable ArgIn(string name) - { - // don't return an empty enumerable, as it breaks NPoco - return new[] { default (T) }; + _args![i] = args[i]; } } + + /// + /// Gets a named argument. + /// + public static object Arg(string name) => new TemplateArg(name); + + public Sql Sql() => new Sql(_sqlContext, _sql); + + // must pass the args, all of them, in the proper order, faster + public Sql Sql(params object[] args) + { + // if the type is an "unspeakable name" it is an anonymous compiler-generated object + // see https://stackoverflow.com/questions/9256594 + // => assume it's an anonymous type object containing named arguments + // (of course this means we cannot use *real* objects here and need SqlNamed - bah) + if (args.Length == 1 && args[0].GetType().Name.Contains("<")) + { + return SqlNamed(args[0]); + } + + if (args.Length != _args?.Count) + { + throw new ArgumentException("Invalid number of arguments.", nameof(args)); + } + + if (args.Length == 0) + { + return new Sql(_sqlContext, true, _sql); + } + + var isBuilt = !args.Any(x => x is IEnumerable); + return new Sql(_sqlContext, isBuilt, _sql, args); + } + + // can pass named args, not necessary all of them, slower + // so, not much different from what Where(...) does (ie reflection) + public Sql SqlNamed(object nargs) + { + var isBuilt = true; + var args = new object[_args?.Count ?? 0]; + var properties = nargs.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(nargs)); + for (var i = 0; i < _args?.Count; i++) + { + object? value; + if (_args[i] is TemplateArg templateArg) + { + if (!properties.TryGetValue(templateArg.Name ?? string.Empty, out value)) + { + throw new InvalidOperationException($"Missing argument \"{templateArg.Name}\"."); + } + + properties.Remove(templateArg.Name!); + } + else + { + value = _args[i]; + } + + args[i] = value!; + + // if value is enumerable then we'll need to expand arguments + if (value is IEnumerable) + { + isBuilt = false; + } + } + + if (properties.Count > 0) + { + throw new InvalidOperationException( + $"Unknown argument{(properties.Count > 1 ? "s" : string.Empty)}: {string.Join(", ", properties.Keys)}"); + } + + return new Sql(_sqlContext, isBuilt, _sql, args); + } + + internal string ToText() + { + var sql = new Sql(_sqlContext, _sql, _args?.Values.ToArray()); + return sql.ToText(); + } + + /// + /// Gets a WHERE expression argument. + /// + public static T? Arg(string name) => default; + + /// + /// Gets a WHERE IN expression argument. + /// + public static IEnumerable ArgIn(string name) => + + // don't return an empty enumerable, as it breaks NPoco + new[] { default(T) }; + + // these are created in PocoToSqlExpressionVisitor + internal class TemplateArg + { + public TemplateArg(string? name) => Name = name; + + public string? Name { get; } + + public override string ToString() => "@" + Name; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs b/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs index 3c180b439f..6fc0148934 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs @@ -1,34 +1,26 @@ -using System; using System.Collections.Concurrent; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public class SqlTemplates { - public class SqlTemplates + private readonly ISqlContext _sqlContext; + private readonly ConcurrentDictionary _templates = new(); + + public SqlTemplates(ISqlContext sqlContext) => _sqlContext = sqlContext; + + public SqlTemplate Get(string key, Func, Sql> sqlBuilder) { - private readonly ConcurrentDictionary _templates = new ConcurrentDictionary(); - private readonly ISqlContext _sqlContext; - - public SqlTemplates(ISqlContext sqlContext) + SqlTemplate CreateTemplate(string _) { - _sqlContext = sqlContext; + Sql sql = sqlBuilder(new Sql(_sqlContext)); + return new SqlTemplate(_sqlContext, sql.SQL, sql.Arguments); } - // for tests - internal void Clear() - { - _templates.Clear(); - } - - public SqlTemplate Get(string key, Func, Sql> sqlBuilder) - { - SqlTemplate CreateTemplate(string _) - { - var sql = sqlBuilder(new Sql(_sqlContext)); - return new SqlTemplate(_sqlContext, sql.SQL, sql.Arguments); - } - - return _templates.GetOrAdd(key, CreateTemplate); - } + return _templates.GetOrAdd(key, CreateTemplate); } + + // for tests + internal void Clear() => _templates.Clear(); } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index 83ab603a35..e0542874d0 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -1,204 +1,201 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; -using StackExchange.Profiling; using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Extends NPoco Database for Umbraco. +/// +/// +/// +/// Is used everywhere in place of the original NPoco Database object, and provides additional features +/// such as profiling, retry policies, logging, etc. +/// +/// Is never created directly but obtained from the . +/// +public class UmbracoDatabase : Database, IUmbracoDatabase { + private readonly ILogger _logger; + private readonly IBulkSqlInsertProvider? _bulkSqlInsertProvider; + private readonly DatabaseSchemaCreatorFactory? _databaseSchemaCreatorFactory; + private readonly IEnumerable? _mapperCollection; + private readonly Guid _instanceGuid = Guid.NewGuid(); + private List? _commands; + + #region Ctor /// - /// Extends NPoco Database for Umbraco. + /// Initializes a new instance of the class. /// /// - /// Is used everywhere in place of the original NPoco Database object, and provides additional features - /// such as profiling, retry policies, logging, etc. - /// Is never created directly but obtained from the . + /// Used by UmbracoDatabaseFactory to create databases. + /// Also used by DatabaseBuilder for creating databases and installing/upgrading. /// - public class UmbracoDatabase : Database, IUmbracoDatabase + public UmbracoDatabase( + string connectionString, + ISqlContext sqlContext, + DbProviderFactory provider, + ILogger logger, + IBulkSqlInsertProvider? bulkSqlInsertProvider, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEnumerable? mapperCollection = null) + : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { - private readonly ILogger _logger; - private readonly IBulkSqlInsertProvider? _bulkSqlInsertProvider; - private readonly DatabaseSchemaCreatorFactory? _databaseSchemaCreatorFactory; - private readonly IEnumerable? _mapperCollection; - private readonly Guid _instanceGuid = Guid.NewGuid(); - private List? _commands; + SqlContext = sqlContext; + _logger = logger; + _bulkSqlInsertProvider = bulkSqlInsertProvider; + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; + _mapperCollection = mapperCollection; - #region Ctor + Init(); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// Used by UmbracoDatabaseFactory to create databases. - /// Also used by DatabaseBuilder for creating databases and installing/upgrading. - /// - public UmbracoDatabase( - string connectionString, - ISqlContext sqlContext, - DbProviderFactory provider, - ILogger logger, - IBulkSqlInsertProvider? bulkSqlInsertProvider, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEnumerable? mapperCollection = null) - : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) + /// + /// Initializes a new instance of the class. + /// + /// Internal for unit tests only. + internal UmbracoDatabase( + DbConnection connection, + ISqlContext sqlContext, + ILogger logger, + IBulkSqlInsertProvider bulkSqlInsertProvider) + : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) + { + SqlContext = sqlContext; + _logger = logger; + _bulkSqlInsertProvider = bulkSqlInsertProvider; + + Init(); + } + + private void Init() + { + EnableSqlTrace = EnableSqlTraceDefault; + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + + if (_mapperCollection != null) { - SqlContext = sqlContext; - _logger = logger; - _bulkSqlInsertProvider = bulkSqlInsertProvider; - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; - _mapperCollection = mapperCollection; - - Init(); + Mappers.AddRange(_mapperCollection); } + } - /// - /// Initializes a new instance of the class. - /// - /// Internal for unit tests only. - internal UmbracoDatabase( - DbConnection connection, - ISqlContext sqlContext, - ILogger logger, - IBulkSqlInsertProvider bulkSqlInsertProvider) - : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) - { - SqlContext = sqlContext; - _logger = logger; - _bulkSqlInsertProvider = bulkSqlInsertProvider; + #endregion - Init(); - } + /// + public ISqlContext SqlContext { get; } - private void Init() - { - EnableSqlTrace = EnableSqlTraceDefault; - NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + #region Testing, Debugging and Troubleshooting - if (_mapperCollection != null) - { - Mappers.AddRange(_mapperCollection); - } - } - - #endregion - - /// - public ISqlContext SqlContext { get; } - - #region Testing, Debugging and Troubleshooting - - private bool _enableCount; + private bool _enableCount; #if DEBUG_DATABASES private int _spid = -1; private const bool EnableSqlTraceDefault = true; #else - private string? _instanceId; - private const bool EnableSqlTraceDefault = false; + private string? _instanceId; + private const bool EnableSqlTraceDefault = false; #endif - /// - public string InstanceId => + /// + public string InstanceId => #if DEBUG_DATABASES _instanceGuid.ToString("N").Substring(0, 8) + ':' + _spid; #else - _instanceId ??= _instanceGuid.ToString("N").Substring(0, 8); + _instanceId ??= _instanceGuid.ToString("N").Substring(0, 8); #endif - /// - public bool InTransaction { get; private set; } + /// + public bool InTransaction { get; private set; } - protected override void OnBeginTransaction() + protected override void OnBeginTransaction() + { + base.OnBeginTransaction(); + InTransaction = true; + } + + protected override void OnAbortTransaction() + { + InTransaction = false; + base.OnAbortTransaction(); + } + + protected override void OnCompleteTransaction() + { + InTransaction = false; + base.OnCompleteTransaction(); + } + + /// + /// Gets or sets a value indicating whether to log all executed Sql statements. + /// + internal bool EnableSqlTrace { get; set; } + + /// + /// Gets or sets a value indicating whether to count all executed Sql statements. + /// + public bool EnableSqlCount + { + get => _enableCount; + set { - base.OnBeginTransaction(); - InTransaction = true; - } + _enableCount = value; - protected override void OnAbortTransaction() - { - InTransaction = false; - base.OnAbortTransaction(); - } - - protected override void OnCompleteTransaction() - { - InTransaction = false; - base.OnCompleteTransaction(); - } - - /// - /// Gets or sets a value indicating whether to log all executed Sql statements. - /// - internal bool EnableSqlTrace { get; set; } - - /// - /// Gets or sets a value indicating whether to count all executed Sql statements. - /// - public bool EnableSqlCount - { - get => _enableCount; - set + if (_enableCount == false) { - _enableCount = value; - - if (_enableCount == false) - { - SqlCount = 0; - } + SqlCount = 0; } } + } - /// - /// Gets the count of all executed Sql statements. - /// - public int SqlCount { get; private set; } + /// + /// Gets the count of all executed Sql statements. + /// + public int SqlCount { get; private set; } - internal bool LogCommands + internal bool LogCommands + { + get => _commands != null; + set => _commands = value ? new List() : null; + } + + internal IEnumerable? Commands => _commands; + + public int BulkInsertRecords(IEnumerable records) => + _bulkSqlInsertProvider?.BulkInsertRecords(this, records) ?? 0; + + /// + /// Returns the for the database + /// + public DatabaseSchemaResult ValidateSchema() + { + DatabaseSchemaCreator? dbSchema = _databaseSchemaCreatorFactory?.Create(this); + DatabaseSchemaResult? databaseSchemaValidationResult = dbSchema?.ValidateSchema(); + + return databaseSchemaValidationResult ?? new DatabaseSchemaResult(); + } + + /// + /// Returns true if Umbraco database tables are detected to be installed + /// + public bool IsUmbracoInstalled() => ValidateSchema().DetermineHasInstalledVersion(); + + #endregion + + #region OnSomething + + protected override DbConnection OnConnectionOpened(DbConnection connection) + { + if (connection == null) { - get => _commands != null; - set => _commands = value ? new List() : null; + throw new ArgumentNullException(nameof(connection)); } - internal IEnumerable? Commands => _commands; - - public int BulkInsertRecords(IEnumerable records) => _bulkSqlInsertProvider?.BulkInsertRecords(this, records) ?? 0; - - /// - /// Returns the for the database - /// - public DatabaseSchemaResult ValidateSchema() - { - var dbSchema = _databaseSchemaCreatorFactory?.Create(this); - var databaseSchemaValidationResult = dbSchema?.ValidateSchema(); - - return databaseSchemaValidationResult ?? new DatabaseSchemaResult(); - } - - /// - /// Returns true if Umbraco database tables are detected to be installed - /// - public bool IsUmbracoInstalled() => ValidateSchema().DetermineHasInstalledVersion(); - - #endregion - - #region OnSomething - - protected override DbConnection OnConnectionOpened(DbConnection connection) - { - if (connection == null) - { - throw new ArgumentNullException(nameof(connection)); - } - - // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. + // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. #if DEBUG_DATABASES // determines the database connection SPID for debugging if (DatabaseType.IsSqlServer()) @@ -216,8 +213,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence } #endif - return connection; - } + return connection; + } #if DEBUG_DATABASES protected override void OnConnectionClosing(DbConnection conn) @@ -227,27 +224,33 @@ namespace Umbraco.Cms.Infrastructure.Persistence } #endif - protected override void OnException(Exception ex) + protected override void OnException(Exception ex) + { + _logger.LogError(ex, "Exception ({InstanceId}).", InstanceId); + _logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace); + + if (EnableSqlTrace == false) { - _logger.LogError(ex, "Exception ({InstanceId}).", InstanceId); - _logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace); - - if (EnableSqlTrace == false) - _logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); - - base.OnException(ex); + _logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); } - private DbCommand? _cmd; + base.OnException(ex); + } - protected override void OnExecutingCommand(DbCommand cmd) + private DbCommand? _cmd; + + protected override void OnExecutingCommand(DbCommand cmd) + { + // if no timeout is specified, and the connection has a longer timeout, use it + if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection?.ConnectionTimeout > 30) { - // if no timeout is specified, and the connection has a longer timeout, use it - if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection?.ConnectionTimeout > 30) - cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + } - if (EnableSqlTrace) - _logger.LogDebug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin + if (EnableSqlTrace) + { + _logger.LogDebug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin + } #if DEBUG_DATABASES // detects whether the command is already in use (eg still has an open reader...) @@ -256,99 +259,105 @@ namespace Umbraco.Cms.Infrastructure.Persistence if (refsobj != null) _logger.LogDebug("Oops!" + Environment.NewLine + refsobj); #endif - _cmd = cmd; + _cmd = cmd; - base.OnExecutingCommand(cmd); - } + base.OnExecutingCommand(cmd); + } - private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, cmd.Parameters.Cast().Select(x => x.Value).WhereNotNull().ToArray()); + private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, cmd.Parameters.Cast().Select(x => x.Value).WhereNotNull().ToArray()); - private string CommandToString(string? sql, object[]? args) - { - var text = new StringBuilder(); + private string CommandToString(string? sql, object[]? args) + { + var text = new StringBuilder(); #if DEBUG_DATABASES text.Append(InstanceId); text.Append(": "); #endif - NPocoSqlExtensions.ToText(sql, args, text); + NPocoSqlExtensions.ToText(sql, args, text); - return text.ToString(); + return text.ToString(); + } + + protected override void OnExecutedCommand(DbCommand cmd) + { + if (_enableCount) + { + SqlCount++; } - protected override void OnExecutedCommand(DbCommand cmd) + _commands?.Add(new CommandInfo(cmd)); + + base.OnExecutedCommand(cmd); + } + + #endregion + + // used for tracking commands + public class CommandInfo + { + public CommandInfo(IDbCommand cmd) { - if (_enableCount) - SqlCount++; - - _commands?.Add(new CommandInfo(cmd)); - - base.OnExecutedCommand(cmd); - } - - #endregion - - // used for tracking commands - public class CommandInfo - { - public CommandInfo(IDbCommand cmd) + Text = cmd.CommandText; + var parameters = new List(); + foreach (IDbDataParameter parameter in cmd.Parameters) { - Text = cmd.CommandText; - var parameters = new List(); - foreach (IDbDataParameter parameter in cmd.Parameters) - parameters.Add(new ParameterInfo(parameter)); - - Parameters = parameters.ToArray(); + parameters.Add(new ParameterInfo(parameter)); } - public string Text { get; } - - public ParameterInfo[] Parameters { get; } + Parameters = parameters.ToArray(); } - // used for tracking commands - public class ParameterInfo + public string Text { get; } + + public ParameterInfo[] Parameters { get; } + } + + // used for tracking commands + public class ParameterInfo + { + public ParameterInfo(IDbDataParameter parameter) { - public ParameterInfo(IDbDataParameter parameter) - { - Name = parameter.ParameterName; - Value = parameter.Value; - DbType = parameter.DbType; - Size = parameter.Size; - } - - public string Name { get; } - public object? Value { get; } - public DbType DbType { get; } - public int Size { get; } + Name = parameter.ParameterName; + Value = parameter.Value; + DbType = parameter.DbType; + Size = parameter.Size; } - /// - public new T ExecuteScalar(string sql, params object[] args) - => ExecuteScalar(new Sql(sql, args)); + public string Name { get; } - /// - public new T ExecuteScalar(Sql sql) - => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); + public object? Value { get; } - /// - /// - /// Be nice if handled upstream GH issue - /// - public new T ExecuteScalar(string sql, CommandType commandType, params object[] args) + public DbType DbType { get; } + + public int Size { get; } + } + + /// + public new T ExecuteScalar(string sql, params object[] args) + => ExecuteScalar(new Sql(sql, args)); + + /// + public new T ExecuteScalar(Sql sql) + => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); + + /// + /// + /// Be nice if handled upstream GH issue + /// + public new T ExecuteScalar(string sql, CommandType commandType, params object[] args) + { + if (SqlContext.SqlSyntax.ScalarMappers == null) { - if (SqlContext.SqlSyntax.ScalarMappers == null) - { - return base.ExecuteScalar(sql, commandType, args); - } - - if (!SqlContext.SqlSyntax.ScalarMappers.TryGetValue(typeof(T), out IScalarMapper? mapper)) - { - return base.ExecuteScalar(sql, commandType, args); - } - - var result = base.ExecuteScalar(sql, commandType, args); - return (T)mapper.Map(result); + return base.ExecuteScalar(sql, commandType, args); } + + if (!SqlContext.SqlSyntax.ScalarMappers.TryGetValue(typeof(T), out IScalarMapper? mapper)) + { + return base.ExecuteScalar(sql, commandType, args); + } + + var result = base.ExecuteScalar(sql, commandType, args); + return (T)mapper.Map(result); } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index fff81a322d..78bcc34f2b 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -1,75 +1,76 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using NPoco; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class UmbracoDatabaseExtensions { - internal static class UmbracoDatabaseExtensions + public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) { - public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) + if (database is not UmbracoDatabase asDatabase) { - if (database is not UmbracoDatabase asDatabase) - { - throw new Exception("oops: database."); - } - - return asDatabase; + throw new Exception("oops: database."); } - /// - /// Gets a dictionary of key/values directly from the database, no scope, nothing. - /// - /// Used by to determine the runtime state. - public static IReadOnlyDictionary? GetFromKeyValueTable(this IUmbracoDatabase database, string keyPrefix) - { - if (database is null) return null; - - // create the wildcard where clause - ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; - var whereParam = sqlSyntax.GetStringColumnWildcardComparison( - sqlSyntax.GetQuotedColumnName("key"), - 0, - TextColumnType.NVarchar); - - var sql = database.SqlContext.Sql() - .Select() - .From() - .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); - - return database.Fetch(sql) - .ToDictionary(x => x.Key!, x => x.Value); - } - - - /// - /// Returns true if the database contains the specified table - /// - /// - /// - /// - public static bool HasTable(this IUmbracoDatabase database, string tableName) - { - try - { - return database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any(table => table.InvariantEquals(tableName)); - } - catch (Exception) - { - return false; // will occur if the database cannot connect - } - } - - /// - /// Returns true if the database contains no tables - /// - /// - /// - public static bool IsDatabaseEmpty(this IUmbracoDatabase database) - => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; - + return asDatabase; } + + /// + /// Gets a dictionary of key/values directly from the database, no scope, nothing. + /// + /// Used by to determine the runtime state. + public static IReadOnlyDictionary? GetFromKeyValueTable( + this IUmbracoDatabase? database, + string keyPrefix) + { + if (database is null) + { + return null; + } + + // create the wildcard where clause + ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; + var whereParam = sqlSyntax.GetStringColumnWildcardComparison( + sqlSyntax.GetQuotedColumnName("key"), + 0, + TextColumnType.NVarchar); + + Sql? sql = database.SqlContext.Sql() + .Select() + .From() + .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); + + return database.Fetch(sql) + .ToDictionary(x => x.Key, x => x.Value); + } + + /// + /// Returns true if the database contains the specified table + /// + /// + /// + /// + public static bool HasTable(this IUmbracoDatabase database, string tableName) + { + try + { + return database.SqlContext.SqlSyntax.GetTablesInSchema(database) + .Any(table => table.InvariantEquals(tableName)); + } + catch (Exception) + { + return false; // will occur if the database cannot connect + } + } + + /// + /// Returns true if the database contains no tables + /// + /// + /// + public static bool IsDatabaseEmpty(this IUmbracoDatabase database) + => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 8677bc1c75..7530ab7854 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -1,301 +1,302 @@ -using System; using System.Data.Common; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using NPoco.FluentMappings; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; +using MapperCollection = NPoco.MapperCollection; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Default implementation of . +/// +/// +/// +/// This factory implementation creates and manages an "ambient" database connection. When running +/// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to +/// the current thread". In this latter case, note that the database connection object is not thread safe. +/// +/// +/// It wraps an NPoco UmbracoDatabaseFactory which is initializes with a proper IPocoDataFactory to ensure +/// that NPoco's plumbing is cached appropriately for the whole application. +/// +/// +// TODO: these comments are not true anymore +// TODO: this class needs not be disposable! +public class UmbracoDatabaseFactory : DisposableObjectSlim, IUmbracoDatabaseFactory { + private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IOptions _globalSettings; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMapperCollection _mappers; + private readonly NPocoMapperCollection _npocoMappers; + private IBulkSqlInsertProvider? _bulkSqlInsertProvider; + private DatabaseType? _databaseType; + + private DbProviderFactory? _dbProviderFactory; + private bool _initialized; + + private object _lock = new(); + + private DatabaseFactory? _npocoDatabaseFactory; + private IPocoDataFactory? _pocoDataFactory; + private MapperCollection? _pocoMappers; + private SqlContext _sqlContext = null!; + private ISqlSyntaxProvider? _sqlSyntax; + + private ConnectionStrings? _umbracoConnectionString; + private bool _upgrading; + + #region Constructors /// - /// Default implementation of . + /// Initializes a new instance of the . /// - /// - /// This factory implementation creates and manages an "ambient" database connection. When running - /// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to - /// the current thread". In this latter case, note that the database connection object is not thread safe. - /// It wraps an NPoco UmbracoDatabaseFactory which is initializes with a proper IPocoDataFactory to ensure - /// that NPoco's plumbing is cached appropriately for the whole application. - /// - // TODO: these comments are not true anymore - // TODO: this class needs not be disposable! - public class UmbracoDatabaseFactory : DisposableObjectSlim, IUmbracoDatabaseFactory + /// Used by the other ctor and in tests. + public UmbracoDatabaseFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IOptions globalSettings, + IOptionsMonitor connectionStrings, + IMapperCollection mappers, + IDbProviderFactoryCreator dbProviderFactoryCreator, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + NPocoMapperCollection npocoMappers) { - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly NPocoMapperCollection _npocoMappers; - private readonly IOptions _globalSettings; - private readonly IMapperCollection _mappers; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; + _globalSettings = globalSettings; + _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); + _dbProviderFactoryCreator = dbProviderFactoryCreator ?? + throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? + throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); + _npocoMappers = npocoMappers; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggerFactory = loggerFactory; - private object _lock = new object(); - - private DatabaseFactory? _npocoDatabaseFactory; - private IPocoDataFactory? _pocoDataFactory; - private DatabaseType? _databaseType; - private ISqlSyntaxProvider? _sqlSyntax; - private IBulkSqlInsertProvider? _bulkSqlInsertProvider; - private NPoco.MapperCollection? _pocoMappers; - private SqlContext _sqlContext = null!; - private bool _upgrading; - private bool _initialized; - - private ConnectionStrings? _umbracoConnectionString; - - private DbProviderFactory? _dbProviderFactory = null; - - private DbProviderFactory? DbProviderFactory + ConnectionStrings umbracoConnectionString = connectionStrings.CurrentValue; + if (!umbracoConnectionString.IsConnectionStringConfigured()) { - get - { - if (_dbProviderFactory == null) - { - _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) - ? null - : _dbProviderFactoryCreator.CreateFactory(ProviderName); - } - - return _dbProviderFactory; - } + logger.LogDebug("Missing connection string, defer configuration."); + return; // not configured } - #region Constructors + Configure(umbracoConnectionString); + } + #endregion - - - /// - /// Initializes a new instance of the . - /// - /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IOptions globalSettings, - IOptionsMonitor connectionStrings, - IMapperCollection mappers, - IDbProviderFactoryCreator dbProviderFactoryCreator, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers) + private DbProviderFactory? DbProviderFactory + { + get { - _globalSettings = globalSettings; - _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); - _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); - _npocoMappers = npocoMappers; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _loggerFactory = loggerFactory; - - ConnectionStrings umbracoConnectionString = connectionStrings.Get(Constants.System.UmbracoConnectionName); - if (!umbracoConnectionString.IsConnectionStringConfigured()) + if (_dbProviderFactory == null) { - logger.LogDebug("Missing connection string, defer configuration."); - return; // not configured + _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) + ? null + : _dbProviderFactoryCreator.CreateFactory(ProviderName); } - Configure(umbracoConnectionString); - } - - #endregion - - /// - public bool Configured - { - get - { - lock (_lock) - { - return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); - } - } - } - - /// - public bool Initialized => Volatile.Read(ref _initialized); - - /// - public string? ConnectionString => _umbracoConnectionString?.ConnectionString; - - /// - public string? ProviderName => _umbracoConnectionString?.ProviderName; - - /// - public bool CanConnect => - // actually tries to connect to the database (regardless of configured/initialized) - !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && - DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); - - /// - public ISqlContext SqlContext - { - get - { - // must be initialized to have a context - EnsureInitialized(); - - return _sqlContext; - } - } - - /// - public IBulkSqlInsertProvider? BulkSqlInsertProvider - { - get - { - // must be initialized to have a bulk insert provider - EnsureInitialized(); - - return _bulkSqlInsertProvider; - } - } - - /// - public void ConfigureForUpgrade() => _upgrading = true; - - /// - public void Configure(ConnectionStrings umbracoConnectionString) - { - if (umbracoConnectionString is null) - { - throw new ArgumentNullException(nameof(umbracoConnectionString)); - } - - lock (_lock) - { - if (Volatile.Read(ref _initialized)) - { - throw new InvalidOperationException("Already initialized."); - } - - _umbracoConnectionString = umbracoConnectionString; - } - - // rest to be lazy-initialized - } - - private void EnsureInitialized() => LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); - - private SqlContext Initialize() - { - _logger.LogDebug("Initializing."); - - if (ConnectionString.IsNullOrWhiteSpace()) - { - throw new InvalidOperationException("The factory has not been configured with a proper connection string."); - } - - if (ProviderName.IsNullOrWhiteSpace()) - { - throw new InvalidOperationException("The factory has not been configured with a proper provider name."); - } - - if (DbProviderFactory == null) - { - throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); - } - - _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, ProviderName); - if (_databaseType == null) - { - throw new Exception($"Can't find an NPoco database type for provider name \"{ProviderName}\"."); - } - - _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(ProviderName!); - if (_sqlSyntax == null) - { - throw new Exception($"Can't find a sql syntax provider for provider name \"{ProviderName}\"."); - } - - _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(ProviderName!); - - _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); - - // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database - // so that everything NPoco is properly cached for the lifetime of the application - _pocoMappers = new NPoco.MapperCollection(); - // add all registered mappers for NPoco - _pocoMappers.AddRange(_npocoMappers); - - _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName!)); - - var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); - _pocoDataFactory = factory; - var config = new FluentConfig(xmappers => factory); - - // create the database factory - _npocoDatabaseFactory = DatabaseFactory.Config(cfg => - { - cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances - .WithFluentConfig(config); // with proper configuration - - foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator.GetProviderSpecificInterceptors(ProviderName!)) - { - cfg.WithInterceptor(interceptor); - } - }); - - if (_npocoDatabaseFactory == null) - { - throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); - } - - _logger.LogDebug("Initialized."); - - return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); - } - - /// - public IUmbracoDatabase CreateDatabase() - { - // must be initialized to create a database - EnsureInitialized(); - return (IUmbracoDatabase) _npocoDatabaseFactory!.GetDatabase(); - } - - // gets initialized poco data builders - private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) - => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); - - // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance - private UmbracoDatabase? CreateDatabaseInstance() - { - if (ConnectionString is null || SqlContext is null || DbProviderFactory is null) - { - return null; - } - - return new UmbracoDatabase( - ConnectionString, - SqlContext, - DbProviderFactory, - _loggerFactory.CreateLogger(), - _bulkSqlInsertProvider, - _databaseSchemaCreatorFactory, - _pocoMappers); - } - - protected override void DisposeResources() - { - // this is weird, because hybrid accessors store different databases per - // thread, so we don't really know what we are disposing here... - // besides, we don't really want to dispose the factory, which is a singleton... - - // TODO: the class does not need be disposable - //var db = _umbracoDatabaseAccessor.UmbracoDatabase; - //_umbracoDatabaseAccessor.UmbracoDatabase = null; - //db?.Dispose(); - Volatile.Write(ref _initialized, false); + return _dbProviderFactory; } } + + /// + public bool Configured + { + get + { + lock (_lock) + { + return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); + } + } + } + + /// + public bool Initialized => Volatile.Read(ref _initialized); + + /// + public string? ConnectionString => _umbracoConnectionString?.ConnectionString; + + /// + public string? ProviderName => _umbracoConnectionString?.ProviderName; + + /// + public bool CanConnect => + + // actually tries to connect to the database (regardless of configured/initialized) + !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && + DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); + + /// + public ISqlContext SqlContext + { + get + { + // must be initialized to have a context + EnsureInitialized(); + + return _sqlContext; + } + } + + /// + public IBulkSqlInsertProvider? BulkSqlInsertProvider + { + get + { + // must be initialized to have a bulk insert provider + EnsureInitialized(); + + return _bulkSqlInsertProvider; + } + } + + /// + public void ConfigureForUpgrade() => _upgrading = true; + + /// + public void Configure(ConnectionStrings umbracoConnectionString) + { + if (umbracoConnectionString is null) + { + throw new ArgumentNullException(nameof(umbracoConnectionString)); + } + + lock (_lock) + { + if (Volatile.Read(ref _initialized)) + { + throw new InvalidOperationException("Already initialized."); + } + + _umbracoConnectionString = umbracoConnectionString; + } + + // rest to be lazy-initialized + } + + /// + public IUmbracoDatabase CreateDatabase() + { + // must be initialized to create a database + EnsureInitialized(); + return (IUmbracoDatabase)_npocoDatabaseFactory!.GetDatabase(); + } + + private void EnsureInitialized() => + LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); + + private SqlContext Initialize() + { + _logger.LogDebug("Initializing."); + + if (ConnectionString.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("The factory has not been configured with a proper connection string."); + } + + if (ProviderName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("The factory has not been configured with a proper provider name."); + } + + if (DbProviderFactory == null) + { + throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); + } + + _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, ProviderName); + if (_databaseType == null) + { + throw new Exception($"Can't find an NPoco database type for provider name \"{ProviderName}\"."); + } + + _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(ProviderName!); + if (_sqlSyntax == null) + { + throw new Exception($"Can't find a sql syntax provider for provider name \"{ProviderName}\"."); + } + + _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(ProviderName!); + + _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); + + // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database + // so that everything NPoco is properly cached for the lifetime of the application + _pocoMappers = new MapperCollection(); + + // add all registered mappers for NPoco + _pocoMappers.AddRange(_npocoMappers); + + _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName!)); + + var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); + _pocoDataFactory = factory; + var config = new FluentConfig(xmappers => factory); + + // create the database factory + _npocoDatabaseFactory = DatabaseFactory.Config(cfg => + { + cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances + .WithFluentConfig(config); // with proper configuration + + foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator + .GetProviderSpecificInterceptors(ProviderName!)) + { + cfg.WithInterceptor(interceptor); + } + }); + + if (_npocoDatabaseFactory == null) + { + throw new NullReferenceException( + "The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); + } + + _logger.LogDebug("Initialized."); + + return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); + } + + // gets initialized poco data builders + private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) + => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); + + // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance + private UmbracoDatabase? CreateDatabaseInstance() + { + if (ConnectionString is null || SqlContext is null || DbProviderFactory is null) + { + return null; + } + + return new UmbracoDatabase( + ConnectionString, + SqlContext, + DbProviderFactory, + _loggerFactory.CreateLogger(), + _bulkSqlInsertProvider, + _databaseSchemaCreatorFactory, + _pocoMappers); + } + + protected override void DisposeResources() => + + // this is weird, because hybrid accessors store different databases per + // thread, so we don't really know what we are disposing here... + // besides, we don't really want to dispose the factory, which is a singleton... + // TODO: the class does not need be disposable + // var db = _umbracoDatabaseAccessor.UmbracoDatabase; + // _umbracoDatabaseAccessor.UmbracoDatabase = null; + // db?.Dispose(); + Volatile.Write(ref _initialized, false); } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs index 753563faff..7b62c212e3 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs @@ -1,31 +1,33 @@ -using System; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence -{ - /// - /// Umbraco's implementation of NPoco . - /// - /// - /// NPoco PocoDataBuilder analyzes DTO classes and returns infos about the tables and - /// their columns. - /// In some very special occasions, a class may expose a column that we do not want to - /// use. This is essentially when adding a column to the User table: if the code wants the - /// column to exist, and it does not exist yet in the database, because a given migration has - /// not run, then the user cannot log into the site, and cannot upgrade = catch 22. - /// So far, this is very manual. We don't try to be clever and figure out whether the - /// columns exist already. We just ignore it. - /// Beware, the application MUST restart when this class behavior changes. - /// You can override the GetColmunnInfo method to control which columns this includes - /// - internal class UmbracoPocoDataBuilder : PocoDataBuilder - { - private readonly bool _upgrading; +namespace Umbraco.Cms.Infrastructure.Persistence; - public UmbracoPocoDataBuilder(Type type, MapperCollection? mapper, bool upgrading) - : base(type, mapper) - { - _upgrading = upgrading; - } - } +/// +/// Umbraco's implementation of NPoco . +/// +/// +/// +/// NPoco PocoDataBuilder analyzes DTO classes and returns infos about the tables and +/// their columns. +/// +/// +/// In some very special occasions, a class may expose a column that we do not want to +/// use. This is essentially when adding a column to the User table: if the code wants the +/// column to exist, and it does not exist yet in the database, because a given migration has +/// not run, then the user cannot log into the site, and cannot upgrade = catch 22. +/// +/// +/// So far, this is very manual. We don't try to be clever and figure out whether the +/// columns exist already. We just ignore it. +/// +/// Beware, the application MUST restart when this class behavior changes. +/// You can override the GetColmunnInfo method to control which columns this includes +/// +internal class UmbracoPocoDataBuilder : PocoDataBuilder +{ + private readonly bool _upgrading; + + public UmbracoPocoDataBuilder(Type type, MapperCollection? mapper, bool upgrading) + : base(type, mapper) => + _upgrading = upgrading; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index 8b6663051e..1455239e53 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Umbraco.Cms.Core.IO; @@ -16,428 +13,489 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Abstract class for block editor based editors +/// +public abstract class BlockEditorPropertyEditor : DataEditor { - /// - /// Abstract class for block editor based editors - /// - public abstract class BlockEditorPropertyEditor : DataEditor + public const string ContentTypeKeyPropertyKey = "contentTypeKey"; + public const string UdiPropertyKey = "udi"; + + public BlockEditorPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors) + : base(dataValueEditorFactory) { - public const string ContentTypeKeyPropertyKey = "contentTypeKey"; - public const string UdiPropertyKey = "udi"; + PropertyEditors = propertyEditors; + SupportsReadOnly = true; + } - public BlockEditorPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors) - : base(dataValueEditorFactory) - => PropertyEditors = propertyEditors; + private PropertyEditorCollection PropertyEditors { get; } - private PropertyEditorCollection PropertyEditors { get; } + #region Value Editor - #region Value Editor + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference + { + private readonly BlockEditorValues _blockEditorValues; + private readonly IDataTypeService _dataTypeService; + private readonly ILogger _logger; + private readonly PropertyEditorCollection _propertyEditors; - internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference + public BlockEditorPropertyValueEditor( + DataEditorAttribute attribute, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IContentTypeService contentTypeService, + ILocalizedTextService textService, + ILogger logger, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + IPropertyValidationService propertyValidationService) + : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly ILogger _logger; - private readonly BlockEditorValues _blockEditorValues; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _logger = logger; - public BlockEditorPropertyValueEditor( - DataEditorAttribute attribute, - PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, - IContentTypeService contentTypeService, - ILocalizedTextService textService, - ILogger logger, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - IPropertyValidationService propertyValidationService) - : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) + _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, _blockEditorValues, contentTypeService)); + Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); + } + + public IEnumerable GetReferences(object? value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); + if (blockEditorData == null) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _logger = logger; - - _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, _blockEditorValues,contentTypeService)); - Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); + return Enumerable.Empty(); } - public IEnumerable GetReferences(object? value) + // loop through all content and settings data + foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue + .SettingsData)) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData == null) - return Enumerable.Empty(); - - // loop through all content and settings data - foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) + foreach (KeyValuePair prop in row.PropertyValues) { - foreach (var prop in row.PropertyValues) + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) { - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - var val = prop.Value.Value?.ToString(); - - var refs = reference.GetReferences(val); - - result.AddRange(refs); + continue; } - } - return result; + var val = prop.Value.Value?.ToString(); + + IEnumerable refs = reference.GetReferences(val); + + result.AddRange(refs); + } } - #region Convert database // editor + return result; + } - // note: there is NO variant support here + #region Convert database // editor - /// - /// Ensure that sub-editor values are translated through their ToEditor methods - /// - /// - /// - /// - /// - /// - public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + // note: there is NO variant support here + + /// + /// Ensure that sub-editor values are translated through their ToEditor methods + /// + /// + /// + /// + /// + /// + public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + { + var val = property.GetValue(culture, segment); + var valEditors = new Dictionary(); + + BlockEditorData? blockEditorData; + try { - var val = property.GetValue(culture, segment); - var valEditors = new Dictionary(); + blockEditorData = _blockEditorValues.DeserializeAndClean(val); + } + catch (JsonSerializationException) + { + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; + } - BlockEditorData? blockEditorData; - try - { - blockEditorData = _blockEditorValues.DeserializeAndClean(val); - } - catch (JsonSerializationException) - { - // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. - return string.Empty; - } + if (blockEditorData == null) + { + return string.Empty; + } - if (blockEditorData == null) - return string.Empty; - - void MapBlockItemData(List items) + void MapBlockItemData(List items) + { + foreach (BlockItemData row in items) { - foreach (var row in items) + foreach (KeyValuePair prop in row.PropertyValues) { - foreach (var prop in row.PropertyValues) + // create a temp property with the value + // - force it to be culture invariant as the block editor can't handle culture variant element properties + prop.Value.PropertyType.Variations = ContentVariation.Nothing; + var tempProp = new Property(prop.Value.PropertyType); + tempProp.SetValue(prop.Value.Value); + + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) - { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); - continue; - } - - var dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); - if (dataType == null) - { - // deal with weird situations by ignoring them (no comment) - row.PropertyValues.Remove(prop.Key); - _logger.LogWarning( - "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", - prop.Key, row.Key, property.PropertyType.Alias); - continue; - } - - if (!valEditors.TryGetValue(dataType.Id, out var valEditor)) - { - var tempConfig = dataType.Configuration; - valEditor = propEditor.GetValueEditor(tempConfig); - - valEditors.Add(dataType.Id, valEditor); - } - - var convValue = valEditor.ToEditor(tempProp); - + // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. + // if the property editor doesn't exist I think everything will break anyways? // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; + row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + continue; } - } - } - MapBlockItemData(blockEditorData.BlockValue.ContentData); - MapBlockItemData(blockEditorData.BlockValue.SettingsData); - - // return json convertable object - return blockEditorData.BlockValue; - } - - /// - /// Ensure that sub-editor values are translated through their FromEditor methods - /// - /// - /// - /// - public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) - { - if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) - return null; - - BlockEditorData? blockEditorData; - try - { - blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value); - } - catch (JsonSerializationException) - { - // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. - return string.Empty; - } - - if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) - return string.Empty; - - void MapBlockItemData(List items) - { - foreach (var row in items) - { - foreach (var prop in row.PropertyValues) + IDataType? dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); + if (dataType == null) { - // Fetch the property types prevalue - var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId)?.Configuration; - - // Lookup the property editor - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) continue; - - // Create a fake content property data object - var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); - - // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); - - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = newValue; + // deal with weird situations by ignoring them (no comment) + row.PropertyValues.Remove(prop.Key); + _logger.LogWarning( + "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", prop.Key, row.Key, property.PropertyType.Alias); + continue; } + + if (!valEditors.TryGetValue(dataType.Id, out IDataValueEditor? valEditor)) + { + var tempConfig = dataType.Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataType.Id, valEditor); + } + + var convValue = valEditor.ToEditor(tempProp); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = convValue; } } - - MapBlockItemData(blockEditorData.BlockValue.ContentData); - MapBlockItemData(blockEditorData.BlockValue.SettingsData); - - // return json - return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } - #endregion + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); + + // return json convertable object + return blockEditorData.BlockValue; } /// - /// Validates the min/max of the block editor + /// Ensure that sub-editor values are translated through their FromEditor methods /// - private class MinMaxValidator : IValueValidator + /// + /// + /// + public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) { - private readonly BlockEditorValues _blockEditorValues; - private readonly ILocalizedTextService _textService; - - public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService) + if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) { - _blockEditorValues = blockEditorValues; - _textService = textService; + return null; } - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + BlockEditorData? blockEditorData; + try { - var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; - if (blockConfig == null) yield break; - - var validationLimit = blockConfig.ValidationLimit; - if (validationLimit == null) yield break; - - var blockEditorData = _blockEditorValues.DeserializeAndClean(value); - - if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0) - || (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout?.Count() < validationLimit.Min)) - { - yield return new ValidationResult( - _textService.Localize("validation", "entriesShort", new[] - { - validationLimit.Min.ToString(), - (validationLimit.Min - (blockEditorData?.Layout?.Count() ?? 0)).ToString() - }), - new[] { "minCount" }); - } - - if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout?.Count() > validationLimit.Max) - { - yield return new ValidationResult( - _textService.Localize("validation", "entriesExceed", new[] - { - validationLimit.Max.ToString(), - (blockEditorData.Layout.Count() - validationLimit.Max).ToString() - }), - new[] { "maxCount" }); - } + blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value); } - } - - internal class BlockEditorValidator : ComplexEditorValidator - { - private readonly BlockEditorValues _blockEditorValues; - private readonly IContentTypeService _contentTypeService; - - public BlockEditorValidator(IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, IContentTypeService contentTypeService) - : base(propertyValidationService) + catch (JsonSerializationException) { - _blockEditorValues = blockEditorValues; - _contentTypeService = contentTypeService; + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; } - protected override IEnumerable GetElementTypeValidation(object? value) + if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) { - var blockEditorData = _blockEditorValues.DeserializeAndClean(value); - if (blockEditorData != null) - { - // There is no guarantee that the client will post data for every property defined in the Element Type but we still - // need to validate that data for each property especially for things like 'required' data to work. - // Lookup all element types for all content/settings and then we can populate any empty properties. - var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList(); - var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); + return string.Empty; + } - foreach (var row in allElements) + void MapBlockItemData(List items) + { + foreach (BlockItemData row in items) + { + foreach (KeyValuePair prop in row.PropertyValues) { - if (!allElementTypes.TryGetValue(row.ContentTypeKey, out var elementType)) - throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId) + ?.Configuration; - // now ensure missing properties - foreach (var elementTypeProp in elementType.CompositionPropertyTypes) + // Lookup the property editor + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) { - if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) - { - // set values to null - row.PropertyValues[elementTypeProp.Alias] = new BlockItemData.BlockPropertyValue(null, elementTypeProp); - row.RawPropertyValues[elementTypeProp.Alias] = null; - } + continue; } - var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); - foreach (var prop in row.PropertyValues) - { - elementValidation.AddPropertyTypeValidation( - new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); - } - yield return elementValidation; + // Create a fake content property data object + var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); + + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = newValue; } } } - } - /// - /// Used to deserialize json values and clean up any values based on the existence of element types and layout structure - /// - internal class BlockEditorValues - { - private readonly Lazy> _contentTypes; - private readonly BlockEditorDataConverter _dataConverter; - private readonly ILogger _logger; + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); - public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) - { - _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); - _dataConverter = dataConverter; - _logger = logger; - } - - private IContentType? GetElementType(BlockItemData item) - { - _contentTypes.Value.TryGetValue(item.ContentTypeKey, out var contentType); - return contentType; - } - - public BlockEditorData? DeserializeAndClean(object? propertyValue) - { - if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - return null; - - var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()!); - - if (blockEditorData.BlockValue.ContentData.Count == 0) - { - // if there's no content ensure there's no settings too - blockEditorData.BlockValue.SettingsData.Clear(); - return null; - } - - var contentTypePropertyTypes = new Dictionary>(); - - // filter out any content that isn't referenced in the layout references - foreach (var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) - { - ResolveBlockItemData(block, contentTypePropertyTypes); - } - // filter out any settings that isn't referenced in the layout references - foreach (var block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) - { - ResolveBlockItemData(block, contentTypePropertyTypes); - } - - // remove blocks that couldn't be resolved - blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); - blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); - - return blockEditorData; - } - - private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes) - { - var contentType = GetElementType(block); - if (contentType == null) - return false; - - // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating - // objects on each iteration. - if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes)) - propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); - - var propValues = new Dictionary(); - - // find any keys that are not real property types and remove them - foreach (var prop in block.RawPropertyValues.ToList()) - { - // doesn't exist so remove it - if (!propertyTypes.TryGetValue(prop.Key, out var propType)) - { - block.RawPropertyValues.Remove(prop.Key); - _logger.LogWarning("The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", - prop.Key, block.Key, prop.Key, contentType.Alias); - } - else - { - // set the value to include the resolved property type - propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); - } - } - - block.ContentTypeAlias = contentType.Alias; - block.PropertyValues = propValues; - - return true; - } + // return json + return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } #endregion - } + + internal class BlockEditorValidator : ComplexEditorValidator + { + private readonly BlockEditorValues _blockEditorValues; + private readonly IContentTypeService _contentTypeService; + + public BlockEditorValidator( + IPropertyValidationService propertyValidationService, + BlockEditorValues blockEditorValues, + IContentTypeService contentTypeService) + : base(propertyValidationService) + { + _blockEditorValues = blockEditorValues; + _contentTypeService = contentTypeService; + } + + protected override IEnumerable GetElementTypeValidation(object? value) + { + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); + if (blockEditorData != null) + { + // There is no guarantee that the client will post data for every property defined in the Element Type but we still + // need to validate that data for each property especially for things like 'required' data to work. + // Lookup all element types for all content/settings and then we can populate any empty properties. + var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData) + .ToList(); + var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()) + .ToDictionary(x => x.Key); + + foreach (BlockItemData row in allElements) + { + if (!allElementTypes.TryGetValue(row.ContentTypeKey, out IContentType? elementType)) + { + throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); + } + + // now ensure missing properties + foreach (IPropertyType elementTypeProp in elementType.CompositionPropertyTypes) + { + if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) + { + // set values to null + row.PropertyValues[elementTypeProp.Alias] = + new BlockItemData.BlockPropertyValue(null, elementTypeProp); + row.RawPropertyValues[elementTypeProp.Alias] = null; + } + } + + var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); + foreach (KeyValuePair prop in row.PropertyValues) + { + elementValidation.AddPropertyTypeValidation( + new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); + } + + yield return elementValidation; + } + } + } + } + + /// + /// Validates the min/max of the block editor + /// + private class MinMaxValidator : IValueValidator + { + private readonly BlockEditorValues _blockEditorValues; + private readonly ILocalizedTextService _textService; + + public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService) + { + _blockEditorValues = blockEditorValues; + _textService = textService; + } + + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; + if (blockConfig == null) + { + yield break; + } + + BlockListConfiguration.NumberRange? validationLimit = blockConfig.ValidationLimit; + if (validationLimit == null) + { + yield break; + } + + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); + + if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0) + || (blockEditorData != null && validationLimit.Min.HasValue && + blockEditorData.Layout?.Count() < validationLimit.Min)) + { + yield return new ValidationResult( + _textService.Localize( + "validation", + "entriesShort", + new[] + { + validationLimit.Min.ToString(), + (validationLimit.Min - (blockEditorData?.Layout?.Count() ?? 0)).ToString(), + }), + new[] { "minCount" }); + } + + if (blockEditorData != null && validationLimit.Max.HasValue && + blockEditorData.Layout?.Count() > validationLimit.Max) + { + yield return new ValidationResult( + _textService.Localize( + "validation", + "entriesExceed", + new[] + { + validationLimit.Max.ToString(), + (blockEditorData.Layout.Count() - validationLimit.Max).ToString(), + }), + new[] { "maxCount" }); + } + } + } + + /// + /// Used to deserialize json values and clean up any values based on the existence of element types and layout + /// structure + /// + internal class BlockEditorValues + { + private readonly Lazy> _contentTypes; + private readonly BlockEditorDataConverter _dataConverter; + private readonly ILogger _logger; + + public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) + { + _contentTypes = + new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); + _dataConverter = dataConverter; + _logger = logger; + } + + public BlockEditorData? DeserializeAndClean(object? propertyValue) + { + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + { + return null; + } + + BlockEditorData blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()!); + + if (blockEditorData.BlockValue.ContentData.Count == 0) + { + // if there's no content ensure there's no settings too + blockEditorData.BlockValue.SettingsData.Clear(); + return null; + } + + var contentTypePropertyTypes = new Dictionary>(); + + // filter out any content that isn't referenced in the layout references + foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x => + blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } + + // filter out any settings that isn't referenced in the layout references + foreach (BlockItemData block in blockEditorData.BlockValue.SettingsData.Where(x => + blockEditorData.References.Any(r => + r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } + + // remove blocks that couldn't be resolved + blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + + return blockEditorData; + } + + private IContentType? GetElementType(BlockItemData item) + { + _contentTypes.Value.TryGetValue(item.ContentTypeKey, out IContentType? contentType); + return contentType; + } + + private bool ResolveBlockItemData( + BlockItemData block, + Dictionary> contentTypePropertyTypes) + { + IContentType? contentType = GetElementType(block); + if (contentType == null) + { + return false; + } + + // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating + // objects on each iteration. + if (!contentTypePropertyTypes.TryGetValue( + contentType.Alias, + out Dictionary? propertyTypes)) + { + propertyTypes = contentTypePropertyTypes[contentType.Alias] = + contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + } + + var propValues = new Dictionary(); + + // find any keys that are not real property types and remove them + foreach (KeyValuePair prop in block.RawPropertyValues.ToList()) + { + // doesn't exist so remove it + if (!propertyTypes.TryGetValue(prop.Key, out IPropertyType? propType)) + { + block.RawPropertyValues.Remove(prop.Key); + _logger.LogWarning( + "The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", + prop.Key, + block.Key, + prop.Key, + contentType.Alias); + } + else + { + // set the value to include the resolved property type + propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); + } + } + + block.ContentTypeAlias = contentType.Alias; + block.PropertyValues = propValues; + + return true; + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs index 28691af7ba..f8e9053722 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs @@ -1,217 +1,226 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A handler for Block editors used to bind to notifications +/// +public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler { - /// - /// A handler for Block editors used to bind to notifications - /// - public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler + private readonly BlockListEditorDataConverter _converter = new(); + private readonly ILogger _logger; + + public BlockEditorPropertyHandler(ILogger logger) => _logger = logger; + + protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList; + + // internal for tests + internal string ReplaceBlockListUdis(string rawJson, Func? createGuid = null) { - private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter(); - private readonly ILogger _logger; - - public BlockEditorPropertyHandler(ILogger logger) + // used so we can test nicely + if (createGuid == null) { - _logger = logger; + createGuid = () => Guid.NewGuid(); } - protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList; - - protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) { - // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process - if (onlyMissingKeys) - return rawJson; - - return ReplaceBlockListUdis(rawJson, null); + return rawJson; } - // internal for tests - internal string ReplaceBlockListUdis(string rawJson, Func? createGuid = null) + // Parse JSON + // This will throw a FormatException if there are null UDIs (expected) + BlockEditorData blockListValue = _converter.Deserialize(rawJson); + + UpdateBlockListRecursively(blockListValue, createGuid); + + return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); + } + + protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) + { + // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process + if (onlyMissingKeys) { - // used so we can test nicely - if (createGuid == null) - createGuid = () => Guid.NewGuid(); - - if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) - return rawJson; - - // Parse JSON - // This will throw a FormatException if there are null UDIs (expected) - var blockListValue = _converter.Deserialize(rawJson); - - UpdateBlockListRecursively(blockListValue, createGuid); - - return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); + return rawJson; } - private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) - { - var oldToNew = new Dictionary(); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); + return ReplaceBlockListUdis(rawJson); + } - for (var i = 0; i < blockListData.References.Count; i++) + private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) + { + var oldToNew = new Dictionary(); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); + + for (var i = 0; i < blockListData.References.Count; i++) + { + ContentAndSettingsReference reference = blockListData.References[i]; + var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out Udi? contentMap); + Udi? settingsMap = null; + var hasSettingsMap = reference.SettingsUdi is not null && + oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); + + if (hasContentMap) { - var reference = blockListData.References[i]; - var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out var contentMap); - Udi? settingsMap = null; - var hasSettingsMap = reference.SettingsUdi is not null && oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); - - if (hasContentMap) - { - // replace the reference - blockListData.References.RemoveAt(i); - blockListData.References.Insert(i, new ContentAndSettingsReference(contentMap!, hasSettingsMap ? settingsMap : null)); - } + // replace the reference + blockListData.References.RemoveAt(i); + blockListData.References.Insert( + i, + new ContentAndSettingsReference(contentMap!, hasSettingsMap ? settingsMap : null)); } - - // build the layout with the new UDIs - var layout = (JArray?)blockListData.Layout; - layout?.Clear(); - foreach (var reference in blockListData.References) - { - layout?.Add(JObject.FromObject(new BlockListLayoutItem - { - ContentUdi = reference.ContentUdi, - SettingsUdi = reference.SettingsUdi - })); - } - - - RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); - RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); } - private void RecursePropertyValues(IEnumerable blockData, Func createGuid) + // build the layout with the new UDIs + var layout = (JArray?)blockListData.Layout; + layout?.Clear(); + foreach (ContentAndSettingsReference reference in blockListData.References) { - foreach (var data in blockData) + layout?.Add(JObject.FromObject(new BlockListLayoutItem { - // check if we need to recurse (make a copy of the dictionary since it will be modified) - foreach (var propertyAliasToBlockItemData in new Dictionary(data.RawPropertyValues)) + ContentUdi = reference.ContentUdi, + SettingsUdi = reference.SettingsUdi, + })); + } + + RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); + RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); + } + + private void RecursePropertyValues(IEnumerable blockData, Func createGuid) + { + foreach (BlockItemData data in blockData) + { + // check if we need to recurse (make a copy of the dictionary since it will be modified) + foreach (KeyValuePair propertyAliasToBlockItemData in new Dictionary( + data.RawPropertyValues)) + { + if (propertyAliasToBlockItemData.Value is JToken jtoken) { - if (propertyAliasToBlockItemData.Value is JToken jtoken) + if (ProcessJToken(jtoken, createGuid, out JToken result)) { - if (ProcessJToken(jtoken, createGuid, out var result)) + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; + } + } + else + { + var asString = propertyAliasToBlockItemData.Value?.ToString(); + + if (asString != null && asString.DetectIsJson()) + { + // this gets a little ugly because there could be some other complex editor that contains another block editor + // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor + // of our type + JToken? json = null; + try + { + json = JToken.Parse(asString); + } + catch (Exception) + { + // See issue https://github.com/umbraco/Umbraco-CMS/issues/10879 + // We are detecting JSON data by seeing if a string is surrounded by [] or {} + // If people enter text like [PLACEHOLDER] JToken parsing fails, it's safe to ignore though + // Logging this just in case in the future we find values that are not safe to ignore + _logger.LogWarning( + "The property {PropertyAlias} on content type {ContentTypeKey} has a value of: {BlockItemValue} - this was recognized as JSON but could not be parsed", + data.Key, propertyAliasToBlockItemData.Key, asString); + } + + if (json != null && ProcessJToken(json, createGuid, out JToken result)) { // need to re-save this back to the RawPropertyValues data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; } } - else - { - var asString = propertyAliasToBlockItemData.Value?.ToString(); - - if (asString != null && asString.DetectIsJson()) - { - // this gets a little ugly because there could be some other complex editor that contains another block editor - // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor - // of our type - JToken? json = null; - try - { - json = JToken.Parse(asString); - } - catch (Exception) - { - // See issue https://github.com/umbraco/Umbraco-CMS/issues/10879 - // We are detecting JSON data by seeing if a string is surrounded by [] or {} - // If people enter text like [PLACEHOLDER] JToken parsing fails, it's safe to ignore though - // Logging this just in case in the future we find values that are not safe to ignore - _logger.LogWarning( "The property {PropertyAlias} on content type {ContentTypeKey} has a value of: {BlockItemValue} - this was recognized as JSON but could not be parsed", - data.Key, propertyAliasToBlockItemData.Key, asString); - } - - if (json != null && ProcessJToken(json, createGuid, out var result)) - { - // need to re-save this back to the RawPropertyValues - data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; - } - } - } } } } - - private bool ProcessJToken(JToken json, Func createGuid, out JToken result) - { - var updated = false; - result = json; - - // select all tokens (flatten) - var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); - foreach (var prop in allProperties) - { - if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) - { - // get it's parent 'layout' and it's parent's container - var layout = prop.Parent?.Parent as JProperty; - if (layout != null && layout.Parent is JObject layoutJson) - { - // recurse - var blockListValue = _converter.ConvertFrom(layoutJson); - UpdateBlockListRecursively(blockListValue, createGuid); - - // set new value - if (layoutJson.Parent != null) - { - // we can replace the object - layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); - updated = true; - } - else - { - // if there is no parent it means that this json property was the root, in which case we just return - result = JObject.FromObject(blockListValue.BlockValue); - return true; - } - } - } - else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey") - { - // this is an arbitrary property that could contain a nested complex editor - var propVal = prop.Value?.ToString(); - // check if this might contain a nested Block Editor - if (!propVal.IsNullOrWhiteSpace() && (propVal?.DetectIsJson() ?? false) && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) - { - if (_converter.TryDeserialize(propVal, out var nestedBlockData)) - { - // recurse - UpdateBlockListRecursively(nestedBlockData, createGuid); - // set the value to the updated one - prop.Value = JObject.FromObject(nestedBlockData.BlockValue); - updated = true; - } - } - } - } - - return updated; - } - - private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, Func createGuid) - { - foreach (var data in blockData) - { - // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here - if (data.Udi is null) - throw new InvalidOperationException("Block data cannot contain a null UDI"); - - // replace the UDIs - var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid()); - oldToNew[data.Udi] = newUdi; - data.Udi = newUdi; - } - } + } + + private bool ProcessJToken(JToken json, Func createGuid, out JToken result) + { + var updated = false; + result = json; + + // select all tokens (flatten) + var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (JProperty prop in allProperties) + { + if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) + { + // get it's parent 'layout' and it's parent's container + if (prop.Parent?.Parent is JProperty layout && layout.Parent is JObject layoutJson) + { + // recurse + BlockEditorData blockListValue = _converter.ConvertFrom(layoutJson); + UpdateBlockListRecursively(blockListValue, createGuid); + + // set new value + if (layoutJson.Parent != null) + { + // we can replace the object + layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); + updated = true; + } + else + { + // if there is no parent it means that this json property was the root, in which case we just return + result = JObject.FromObject(blockListValue.BlockValue); + return true; + } + } + } + else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && + prop.Name != "contentTypeKey") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value.ToString(); + + // check if this might contain a nested Block Editor + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && + propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) + { + if (_converter.TryDeserialize(propVal, out BlockEditorData? nestedBlockData)) + { + // recurse + UpdateBlockListRecursively(nestedBlockData, createGuid); + + // set the value to the updated one + prop.Value = JObject.FromObject(nestedBlockData.BlockValue); + updated = true; + } + } + } + } + + return updated; + } + + private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, + Func createGuid) + { + foreach (BlockItemData data in blockData) + { + // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here + if (data.Udi is null) + { + throw new InvalidOperationException("Block data cannot contain a null UDI"); + } + + // replace the UDIs + var newUdi = Udi.Create(Constants.UdiEntityType.Element, createGuid()); + oldToNew[data.Udi] = newUdi; + data.Udi = newUdi; + } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs index a3b3d62338..431b006b1e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs @@ -1,19 +1,15 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class BlockListConfigurationEditor : ConfigurationEditor { - internal class BlockListConfigurationEditor : ConfigurationEditor + public BlockListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public BlockListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } - } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index c8be6adf40..70a0aa35dc 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -1,58 +1,53 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a block list property editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.BlockList, + "Block List", + "blocklist", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-thumbnail-list")] +public class BlockListPropertyEditor : BlockEditorPropertyEditor { - /// - /// Represents a block list property editor. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.BlockList, - "Block List", - "blocklist", - ValueType = ValueTypes.Json, - Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-thumbnail-list")] - public class BlockListPropertyEditor : BlockEditorPropertyEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public BlockListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IIOHelper ioHelper) + : this(dataValueEditorFactory, propertyEditors, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public BlockListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors, - IIOHelper ioHelper) - : this(dataValueEditorFactory, propertyEditors, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - public BlockListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory, propertyEditors) - { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - #region Pre Value Editor - - protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(_ioHelper, _editorConfigurationParser); - - #endregion } + + public BlockListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory, propertyEditors) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } + + #region Pre Value Editor + + protected override IConfigurationEditor CreateConfigurationEditor() => + new BlockListConfigurationEditor(_ioHelper, _editorConfigurationParser); + + #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs index 226024f8b9..76a7fb5b6d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -1,59 +1,60 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A property editor to allow multiple checkbox selection of pre-defined items. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.CheckBoxList, + "Checkbox list", + "checkboxlist", + Icon = "icon-bulleted-list", + Group = Constants.PropertyEditors.Groups.Lists)] +public class CheckBoxListPropertyEditor : DataEditor { - /// - /// A property editor to allow multiple checkbox selection of pre-defined items. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.CheckBoxList, - "Checkbox list", - "checkboxlist", - Icon = "icon-bulleted-list", - Group = Constants.PropertyEditors.Groups.Lists)] - public class CheckBoxListPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + private readonly ILocalizedTextService _textService; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public CheckBoxListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + ILocalizedTextService textService, + IIOHelper ioHelper) + : this(dataValueEditorFactory, textService, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly ILocalizedTextService _textService; - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public CheckBoxListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - ILocalizedTextService textService, - IIOHelper ioHelper) - : this(dataValueEditorFactory, textService, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - /// - /// The constructor will setup the property editor based on the attribute if one is found - /// - public CheckBoxListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - ILocalizedTextService textService, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) - { - _textService = textService; - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new ValueListConfigurationEditor(_textService, _ioHelper, _editorConfigurationParser); - - /// - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); } + + /// + /// The constructor will setup the property editor based on the attribute if one is found + /// + public CheckBoxListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + ILocalizedTextService textService, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _textService = textService; + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + SupportsReadOnly = true; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new ValueListConfigurationEditor(_textService, _ioHelper, _editorConfigurationParser); + + /// + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index ff72a77788..8c8455ce86 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -1,10 +1,7 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; @@ -13,172 +10,190 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class ColorPickerConfigurationEditor : ConfigurationEditor { - internal class ColorPickerConfigurationEditor : ConfigurationEditor + private readonly IJsonSerializer _jsonSerializer; + + public ColorPickerConfigurationEditor(IIOHelper ioHelper, IJsonSerializer jsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - private readonly IJsonSerializer _jsonSerializer; - public ColorPickerConfigurationEditor(IIOHelper ioHelper, IJsonSerializer jsonSerializer, IEditorConfigurationParser editorConfigurationParser) - : base(ioHelper, editorConfigurationParser) - { - _jsonSerializer = jsonSerializer; - var items = Fields.First(x => x.Key == "items"); + _jsonSerializer = jsonSerializer; + ConfigurationField items = Fields.First(x => x.Key == "items"); - // customize the items field - items.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; - items.Description = "Add, remove or sort colors"; - items.Name = "Colors"; - items.Validators.Add(new ColorListValidator()); + // customize the items field + items.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; + items.Description = "Add, remove or sort colors"; + items.Name = "Colors"; + items.Validators.Add(new ColorListValidator()); + } + + public override Dictionary ToConfigurationEditor(ColorPickerConfiguration? configuration) + { + List? configuredItems = configuration?.Items; // ordered + object editorItems; + + if (configuredItems == null) + { + editorItems = new object(); + } + else + { + var d = new Dictionary(); + editorItems = d; + var sortOrder = 0; + foreach (ValueListConfiguration.ValueListItem item in configuredItems) + { + d[item.Id.ToString()] = GetItemValue(item, configuration!.UseLabel, sortOrder++); + } } - public override Dictionary ToConfigurationEditor(ColorPickerConfiguration? configuration) + var useLabel = configuration?.UseLabel ?? false; + + return new Dictionary { { "items", editorItems }, { "useLabel", useLabel } }; + } + + // send: { "items": { "": { "value": "", "label": "